/****************************************************************************** * @file app_function.c * @brief 设备功能开关 + 自动玩法 + 手动点动 + 按键动作实现 * @author cyWu <1917507415@qq.com> * @date 2026.08.26 * @version V1.1.0 * @history * - V1.0.0, 2026.08.25, cyWu, 首次发布,实现 DP0 开关、自动玩法、手动点动与按键动作 * - V1.1.0, 2026.08.26, cyWu, 切模式闪灯改到 function_play_mode_apply 统一 * 触发(自动互切/手动切自动都会闪);手动点动 * 启动也补上闪灯;DP2 停转(cmd=3)改边沿触发, * 避免残留的 03 一直把刚启动的自动模式打断; * 自动模式新增单次最长运行时长,到点回待机 ******************************************************************************/ #include "app_function.h" #include "app_power.h" #include "app_pair.h" #include "app_mode.h" #include "app_led.h" #include "app_shake_wake.h" #include "app_no_disturb.h" #include "car_audio_test.h" #include "app_audio_debug.h" #include "app_time_util.h" #include "bsp_hw.h" #include "board_pin.h" #include "jb_product.h" #include "usr_le_api.h" #include "system/includes.h" #define LOG_TAG_CONST APP #define LOG_TAG "[APP_FUNC]" #define LOG_INFO_ENABLE #include "debug.h" /** 功能模块总状态,替代原来一堆散落的 static 变量 */ typedef struct { uint8_t app_power; /**< DP0:0=软关,电机/玩法停,BLE 仍在 */ uint8_t play_mode; /**< 当前自动模式 0~5 */ uint32_t play_ms; /**< 当前自动模式已运行时长,超时回待机 */ uint8_t hand_busy; /**< 手动点动进行中 */ uint8_t hand_cmd_last; /**< 最近一次手动命令 */ uint16_t hand_ms; uint8_t hand_queue[BOARD_HAND_QUEUE_LEN]; /**< DP2 连点排队:待执行命令 FIFO */ uint8_t hand_queue_head; /**< 队首下标 */ uint8_t hand_queue_count; /**< 待执行条数 */ volatile uint8_t hand_write_pending; /**< DP2 刚写入(03 停转要靠事件) */ volatile uint8_t hand_write_cmd; volatile uint8_t power_write_pending; volatile uint8_t power_write_value; volatile uint8_t play_write_pending; volatile uint8_t play_write_mode; uint8_t shake_wake; } AppFunctionCtx_t; static AppFunctionCtx_t s_func; static void function_hand_apply(uint8_t cmd); static uint16_t function_hand_action_ms(void); static void function_hand_queue_push(uint8_t cmd); static uint8_t function_hand_queue_pop(uint8_t *cmd); static void function_hand_stop(void); static void function_play_mode_apply(uint8_t mode); static void function_play_mode_poll(void); static void function_play_mode_timeout(uint16_t dt); static void function_hand_poll(void); static void function_on_click(void); static uint8_t function_power_on(uint8_t show_hint); static uint8_t function_shake_wake_allowed(void); static void function_shake_wake_trigger(void); /** * @brief 初始化功能模块状态 * @return 无 */ void app_function_init(void) { memset(&s_func, 0, sizeof(s_func)); app_shake_wake_init(function_shake_wake_allowed, function_shake_wake_trigger); s_func.shake_wake = !!gDevData.shake_wake; app_shake_wake_set_enable(s_func.shake_wake); } void app_function_dp_power_write(uint8_t enable) { s_func.power_write_value = !!enable; s_func.power_write_pending = 1; } void app_function_dp_play_write(uint8_t mode) { s_func.play_write_mode = mode; s_func.play_write_pending = 1; } /** * @brief DP2 写入钩子:仅记录第 1 字节,真正执行在 app_function_poll * @param cmd 0=停止,1=上部电机探出,2=下电机顺时针转动,3=下电机逆时针转动 * @return 无 */ void app_function_dp_hand_write(uint8_t cmd) { s_func.hand_write_cmd = cmd; s_func.hand_write_pending = 1; } void app_function_set_shake_wake(uint8_t enable) { s_func.shake_wake = !!enable; gDevData.shake_wake = s_func.shake_wake; app_shake_wake_set_enable(s_func.shake_wake); } /** * @brief 跟随 APP 下发的 DP0/DP1,并处理 DP2 手动点动 * @return 无 */ void app_function_poll(void) { if (s_func.shake_wake != !!gDevData.shake_wake) { app_function_set_shake_wake(gDevData.shake_wake); } if (s_func.power_write_pending) { uint8_t enable = s_func.power_write_value; s_func.power_write_pending = 0; if (enable) { if (function_power_on(1)) { log_info("APP power on\n"); } else { gDevData.power = s_func.app_power; } } else if (!enable && s_func.app_power) { app_function_soft_off(); } else { gDevData.power = s_func.app_power; } } function_play_mode_poll(); function_hand_poll(); } /** * @brief 推进当前玩法节拍或手动刹车 * @param dt 距上次调用的毫秒数 * @return 无 */ void app_function_tick_1ms(uint16_t dt) { /* 与业务节拍共用一次唤醒,避免震动检测另开 10ms sys_timer。 */ app_shake_wake_poll(); if (s_func.hand_busy) { uint8_t next_cmd; bsp_motor_switch_tick(); /* 手动点动期间也要走换向死区 */ if (gDevData.hand_mode == HAND_MODE_1) { app_mode_run(); /* cmd=1 的上电机波形靠自动引擎推进 */ } s_func.hand_ms = app_add_ms(s_func.hand_ms, dt); if (s_func.hand_ms >= function_hand_action_ms()) { if (function_hand_queue_pop(&next_cmd)) { s_func.hand_ms = 0; /* 接着补做排队的,不打断 */ function_hand_apply(next_cmd); log_info("manual next %d\n", next_cmd); } else { function_hand_stop(); app_led_request_mode_blink(); log_info("manual done\n"); } } } else { app_mode_run(); function_play_mode_timeout(dt); /* 自动模式跑满 10 分钟回待机 */ } } /** * @brief 停手动点动(走电机反转刹车) * @return 无 */ static void function_hand_stop(void) { s_func.hand_busy = 0; s_func.hand_ms = 0; s_func.hand_queue_head = 0; s_func.hand_queue_count = 0; gDevData.hand_mode = HAND_MODE_0; app_mode_stop(); bsp_drive_stop(); } /** * @brief 把一条 DP2 命令压入排队 FIFO * @param cmd HAND_MODE_1/2/3 * @return 无 */ static void function_hand_queue_push(uint8_t cmd) { uint8_t idx; if (s_func.hand_queue_count >= BOARD_HAND_QUEUE_LEN) { log_info("hand queue full, drop %d\n", cmd); return; } idx = (uint8_t)((s_func.hand_queue_head + s_func.hand_queue_count) % BOARD_HAND_QUEUE_LEN); s_func.hand_queue[idx] = cmd; s_func.hand_queue_count++; } /** * @brief 取出队首命令 * @param cmd 出参 * @return 1=取到,0=队列空 */ static uint8_t function_hand_queue_pop(uint8_t *cmd) { if (s_func.hand_queue_count == 0) { return 0; } *cmd = s_func.hand_queue[s_func.hand_queue_head]; s_func.hand_queue_head = (uint8_t)((s_func.hand_queue_head + 1) % BOARD_HAND_QUEUE_LEN); s_func.hand_queue_count--; return 1; } /** * @brief 下发一轮手动动作(驱动层唯一出口) * @param cmd HAND_MODE_1/2/3 * @return 无 */ static void function_hand_apply(uint8_t cmd) { app_mode_stop(); /* 清掉上一轮残留,含 cmd=1 借用的自动引擎 */ gDevData.hand_mode = cmd; switch (cmd) { case HAND_MODE_1: app_mode_start(APP_MODE_UPPER_ONLY); break; case HAND_MODE_2: bsp_drive_set(BSP_MOTOR_STOP, 0, BSP_MOTOR_FORWARD, 8000); break; case HAND_MODE_3: bsp_drive_set(BSP_MOTOR_STOP, 0, BSP_MOTOR_BACKWARD, 8000); break; default: bsp_drive_stop(); break; } } /** * @brief 当前手动动作的单轮时长 * @return 毫秒 */ static uint16_t function_hand_action_ms(void) { if (gDevData.hand_mode == HAND_MODE_2 || gDevData.hand_mode == HAND_MODE_3) { return BOARD_HAND_STEP_MS; /* 下电机:一轮 = 半圈 */ } return BOARD_HAND_PULSE_MS; /* 上电机波形:保留 5s 上限 */ } /** * @brief 停手动 + 自动模式,并清 DP1 * @return 无 */ void app_function_stop_all(void) { s_func.play_mode = 0; s_func.play_ms = 0; gDevData.play_mode = 0; gDevData.hand_mode = HAND_MODE_0; app_mode_stop(); function_hand_stop(); } /** * @brief 软关:电机停、DP0=0,等 APP/短按再开。不断 BLE * @return 无 */ void app_function_soft_off(void) { s_func.app_power = 0; gDevData.power = 0; s_func.play_mode = 0; s_func.play_ms = 0; gDevData.play_mode = 0; app_mode_stop(); function_hand_stop(); /* 不依赖系统下一次轻睡回调,软关生效后立刻关四路 MCPWM。 */ bsp_hw_sleep_pwm_off(); app_shake_wake_reset(); usr_ble_set_standby(1); log_info("soft off, wait APP power on\n"); } /** * @brief 上电默认开启功能(DP0=1),不带开机灯效提示 * @return 无 */ void app_function_power_on_silent(void) { (void)function_power_on(0); } /** * @brief 查询功能开关状态(DP0) * @return 1=开,0=关 */ uint8_t app_function_is_power_on(void) { return s_func.app_power; } /** * @brief 查询手动点动是否进行中 * @return 1=进行中,0=空闲 */ uint8_t app_function_is_hand_busy(void) { return s_func.hand_busy; } /** * @brief 查询当前自动玩法编号 * @return 0=停转,1~5=对应玩法 */ uint8_t app_function_play_mode(void) { return s_func.play_mode; } /** * @brief 切换自动模式 0~5,并统一在这里判定要不要闪灯提示 * @note 闪灯规则:只要 play_mode 真的变了就闪两下。手动点动期间 play_mode * 恒为 0(见 function_hand_poll),所以“手动切到自动”落到这里时 * old_mode 必是 0、新 mode 非 0,天然满足“变了”,不用额外判断; * 由充电/软关/配对等条件被动摁回 0 且本来就是 0 的情况才不闪, * 避免刷屏式误闪 * @param mode 0=停转,1~5=对应玩法 * @return 无 */ static void function_play_mode_apply(uint8_t mode) { uint8_t old_mode = s_func.play_mode; MAGIC_AUDIO_LOG("模式切换请求:旧=%u,请求=%u,DP4=%u,功能开关=%u,充电=%u,关机中=%u,OTA=%u", old_mode, mode, gDevData.beep_trig, s_func.app_power, app_power_usb_online(), app_power_poweroff_pending(), app_led_is_ota()); if (mode > PLAY_MODE_5) { mode = 0; } /* Pairing is an online overlay; only charging, OTA and power-off stop motion. */ if (app_power_usb_online() || !s_func.app_power || app_power_poweroff_pending() || app_led_is_ota()) { mode = 0; } if (mode != 0 && s_func.hand_busy) { function_hand_stop(); /* 手动切到自动:先停手动点动,再进自动模式 */ } s_func.play_mode = mode; s_func.play_ms = 0; /* 每次切模式都重新计 10 分钟 */ gDevData.play_mode = mode; if (mode == 0) { app_mode_stop(); } else { app_mode_start(mode); } // MAGIC_AUDIO_LOG("模式切换结果:旧=%u,实际=%u;当前测试不拦截DP4", old_mode, mode); if (mode != old_mode) { app_led_request_mode_blink(); /* 模式真变了才闪两下 */ /* DP4:0=关闭模式切换鸟叫,1=允许播放 */ //&& gDevData.beep_trig if ((mode != PLAY_MODE_0) && gDevData.beep_trig) { Car_Audio_Test_Play(); } } } /** * @brief 跟随 APP 下发的 DP1 自动模式 * @note 手动点动进行中也允许响应,因为选中一个自动模式本身就代表要从 * 手动切回自动(function_play_mode_apply 内部会先停手动) * @return 无 */ static void function_play_mode_poll(void) { uint8_t mode; if (!s_func.play_write_pending) { return; } s_func.play_write_pending = 0; if (app_power_usb_online() || !s_func.app_power || app_power_poweroff_pending() || app_led_is_ota()) { gDevData.play_mode = s_func.play_mode; return; } mode = s_func.play_write_mode; if (mode > PLAY_MODE_5) { mode = 0; } if (mode != PLAY_MODE_0 && mode == s_func.play_mode) { mode = PLAY_MODE_0; } function_play_mode_apply(mode); log_info("APP play_mode=%d\n", mode); } /** * @brief 自动模式单次最长运行 BOARD_AUTO_MODE_MAX_MS,到点自动回待机 * @note 不走 function_play_mode_apply 的闪灯提示:这是保护性超时,不是 * 用户主动切模式,闪灯提示反而会让用户误以为是自己按到了什么 * @param dt 距上次调用的毫秒数 * @return 无 */ static void function_play_mode_timeout(uint16_t dt) { if (s_func.play_mode == 0) { return; } s_func.play_ms += dt; if (s_func.play_ms < BOARD_AUTO_MODE_MAX_MS) { return; } log_info("auto mode %d run %ums, timeout to standby\n", s_func.play_mode, (unsigned)s_func.play_ms); app_function_soft_off(); } /** * @brief DP2 手动:只看第 1 字节。0=上下都停,1=仅上电机往复,2=仅下电机顺时针,3=仅下电机逆时针 * @return 无 */ static void function_hand_poll(void) { uint8_t cmd; uint8_t got_write = s_func.hand_write_pending; /* 优先用 DP 写入钩子带来的边沿;没有新写入才读协议层当前值 */ if (got_write) { s_func.hand_write_pending = 0; cmd = s_func.hand_write_cmd; } else { cmd = gDevData.hand_mode; } if (app_power_usb_online() || !s_func.app_power || app_power_poweroff_pending() || app_led_is_ota()) { if (s_func.hand_busy) { app_function_stop_all(); } s_func.hand_cmd_last = cmd; return; } if (!got_write) { s_func.hand_cmd_last = cmd; return; } if (cmd == HAND_MODE_0) { if (s_func.hand_busy) { function_hand_stop(); app_led_request_mode_blink(); } s_func.hand_cmd_last = cmd; return; } if (cmd >= HAND_MODE_1 && cmd <= HAND_MODE_3) { if (s_func.hand_busy) { /* 正在动作中:不打断、不清零计时,只把新指令排到队尾 */ function_hand_queue_push(cmd); s_func.hand_cmd_last = cmd; return; } s_func.play_mode = 0; s_func.play_ms = 0; gDevData.play_mode = 0; s_func.hand_busy = 1; s_func.hand_ms = 0; function_hand_apply(cmd); app_led_request_mode_blink(); if (gDevData.beep_trig) { Car_Audio_Test_Play(); } } else { gDevData.hand_mode = HAND_MODE_0; if (s_func.hand_busy) { function_hand_stop(); } } s_func.hand_cmd_last = cmd; } /** * @brief 短按:自动模式 0→1→…→5→0(闪灯提示交给 function_play_mode_apply) * @return 无 */ static void function_on_click(void) { if (s_func.hand_busy) { log_info("key click ignored, hand busy\n"); return; } function_play_mode_apply((s_func.play_mode >= PLAY_MODE_5) ? PLAY_MODE_1 : (uint8_t)(s_func.play_mode + 1)); log_info("key click play_mode=%d\n", s_func.play_mode); } /** * @brief 短按回调:充电/配对忽略;软关时唤醒;否则切模式 * @return 无 */ void usr_on_key_short(void) { if (app_power_poweroff_pending()) { return; } if (app_power_usb_online()) { log_info("key click ignored, charging\n"); return; } if (app_led_is_ota()) { log_info("key click ignored, ota\n"); return; } if (!s_func.app_power) { /* 软关状态下短按:只唤醒功能,不切模式 */ if (function_power_on(1)) { log_info("key click cancel soft off\n"); } return; } function_on_click(); } /** * @brief 长按 3s:运行中深睡(开机 3s/5s 不走这里) * @return 无 */ void usr_on_key_long(void) { if (app_power_poweroff_pending()) { return; } if (app_power_usb_online()) { log_info("key hold 3s ignored, charging\n"); return; } if (app_led_is_ota()) { log_info("key hold 3s, exit ota and poweroff\n"); usr_app_ota_exit(); app_led_exit_ota(); app_power_request_poweroff(); return; } log_info("key hold 3s, poweroff\n"); app_power_request_poweroff(); } /** Shared standby-to-on transition for APP, key and vibration wake. */ static uint8_t function_power_on(uint8_t show_hint) { if (app_power_usb_online() || app_power_poweroff_pending() || app_led_is_ota()) { return 0; } if (s_func.app_power) { gDevData.power = 1; return 1; } app_function_stop_all(); s_func.app_power = 1; gDevData.power = 1; app_shake_wake_reset(); usr_ble_set_standby(0); if (show_hint) { app_led_hint_power_on(); } return 1; } static uint8_t function_shake_wake_allowed(void) { return s_func.shake_wake && !app_no_disturb_is_active() && !s_func.app_power && !app_power_usb_online() && !app_power_poweroff_pending() && !app_led_is_ota(); } static void function_shake_wake_trigger(void) { if (function_power_on(1)) { log_info("shake wake power on\n"); } }