/****************************************************************************** * @file app_cbc2.c * @brief CBC2 电源 / 充电 / 按键 / 配对 / DP 协调 * @author cyWu <1917507415@qq.com> * @date 2026.08.24 * @version V1.0.2 * @history * - V1.0.0, 2026.08.21, cyWu, 首次发布 * - V1.0.2, 2026.08.24, cyWu, 上电即绑定广播,与 81bd2f7 行为对齐 ******************************************************************************/ #include "system/includes.h" #include "app_main.h" #include "app_cbc2.h" #include "app_nv.h" #include "app_led.h" #include "app_rope.h" #include "app_mode.h" #include "app_dnd.h" #include "bsp_hw.h" #include "usr_le_api.h" #include "asm/power/power_api.h" #define LOG_TAG_CONST APP #define LOG_TAG "[APP_CBC2]" #define LOG_ERROR_ENABLE #define LOG_DEBUG_ENABLE #define LOG_INFO_ENABLE #include "debug.h" extern void sys_enter_soft_poweroff(void *priv); /*============================================================================*/ /* 类型 */ /*============================================================================*/ typedef enum { APP_LIFE_CHARGE_ONLY = 0, APP_LIFE_ON, APP_LIFE_PAIR } AppLife_t; typedef struct { uint8_t inited; AppLife_t life; uint8_t power; /* DP0 APP 功能开关 */ uint8_t play_mode; uint8_t battery; uint8_t charge_dp; /* 1 充电中 2 未充电 */ uint8_t alm_low; uint8_t charge_led; /* 0 无 1 充 2 满 */ uint8_t usb_raw; uint8_t usb_debounced; uint8_t usb_cnt; uint8_t restore_on; /* 插电前是否开机 */ uint8_t full_latched; uint8_t from_deep_boot; uint8_t boot_key_lock; /* 开机按键未松开前,忽略短按/关机 */ uint8_t key_raw; uint8_t key_stable; uint8_t key_cnt; uint8_t key_pressed; uint16_t key_hold_ms; uint8_t req_click; uint8_t req_poweroff; uint8_t req_pair; uint32_t pair_ms; uint32_t lowbat_ms; uint8_t lowbat_run; uint16_t vbat_mv; uint8_t pause_mode; uint8_t pause_cal; uint8_t pause_percent; uint8_t pause_percent_valid; uint8_t run_timer; } AppCbc2_t; /*============================================================================*/ /* 私有变量 */ /*============================================================================*/ static AppCbc2_t s_app; /*============================================================================*/ /* 私有函数 */ /*============================================================================*/ static uint8_t app_cbc2_ctrl_allowed(void) { return (uint8_t)(s_app.life != APP_LIFE_CHARGE_ONLY); } static void app_cbc2_apply_stop_reset(void) { app_mode_stop(); app_rope_stop(); s_app.play_mode = 0; } static void app_cbc2_apply_play_mode(uint8_t mode, uint8_t blink) { if (mode > 6) { mode = 0; } app_rope_stop(); if (mode == 0) { app_mode_stop(); } else { app_mode_start(mode); } s_app.play_mode = mode; if (blink) { app_led_request_mode_blink(); } log_info("play_mode=%d\n", mode); } static void app_cbc2_do_poweroff(void) { log_info("enter deep off\n"); app_mode_stop(); bsp_hw_enter_sleep_io(); app_nv_save_if_dirty(); sys_enter_soft_poweroff(NULL); } static void app_cbc2_boot_poweroff(void) { log_info("boot deep off\n"); bsp_hw_enter_sleep_io(); app_nv_save_if_dirty(); power_set_soft_poweroff(); } static void app_cbc2_enter_pair(void) { log_info("enter pair mode\n"); app_cbc2_apply_stop_reset(); s_app.life = APP_LIFE_PAIR; s_app.power = 1; s_app.pair_ms = 0; s_app.from_deep_boot = 0; usr_var.usr_goto_pair = 1; usr_goto_pair_mode(); } static void app_cbc2_pair_end(uint8_t ok) { log_info("pair %s\n", ok ? "ok" : "fail"); app_led_start_pair_result(ok); s_app.life = APP_LIFE_ON; s_app.power = 1; s_app.play_mode = 0; usr_var.usr_goto_pair = 0; } static void app_cbc2_pause_functions(void) { s_app.pause_mode = s_app.play_mode; s_app.pause_cal = app_rope_is_calibrating(); s_app.pause_percent_valid = 0; if (app_rope_is_busy() && !s_app.pause_cal && app_rope_is_calibrated() && app_rope_get_turns()) { int32_t max_x = (int32_t)app_rope_get_turns() * 100; if (max_x > 0) { int32_t pct = app_rope_remaining_x100() * 100 / max_x; if (pct < 0) { pct = 0; } if (pct > 100) { pct = 100; } s_app.pause_percent = (uint8_t)pct; s_app.pause_percent_valid = 1; } } app_mode_stop(); app_rope_stop(); log_info("app power pause mode=%d cal=%d\n", s_app.pause_mode, s_app.pause_cal); } static void app_cbc2_resume_functions(void) { log_info("app power resume mode=%d cal=%d\n", s_app.pause_mode, s_app.pause_cal); if (s_app.pause_cal) { app_rope_cal_start(); } else if (s_app.pause_percent_valid) { app_rope_goto_percent(s_app.pause_percent); } else if (s_app.pause_mode >= 1 && s_app.pause_mode <= 6) { app_cbc2_apply_play_mode(s_app.pause_mode, 0); } s_app.pause_cal = 0; s_app.pause_percent_valid = 0; } static uint8_t app_cbc2_bat_percent(uint16_t mv) { int32_t span = BOARD_VBAT_FULL_MV - BOARD_VBAT_EMPTY_MV; int32_t pct; if (mv <= BOARD_VBAT_EMPTY_MV) { return 0; } if (mv >= BOARD_VBAT_FULL_MV) { return 100; } pct = ((int32_t)mv - BOARD_VBAT_EMPTY_MV) * 100 / span; if (pct < 0) { pct = 0; } if (pct > 100) { pct = 100; } return (uint8_t)pct; } static void app_cbc2_sample_charge(void) { uint8_t usb = bsp_vpwr_is_online(); if (usb == s_app.usb_raw) { if (s_app.usb_cnt < 5) { s_app.usb_cnt++; } } else { s_app.usb_raw = usb; s_app.usb_cnt = 0; } if (s_app.usb_cnt >= 5 && s_app.usb_debounced != s_app.usb_raw) { s_app.usb_debounced = s_app.usb_raw; if (s_app.usb_debounced) { s_app.restore_on = (uint8_t)(s_app.life != APP_LIFE_CHARGE_ONLY); s_app.full_latched = 0; app_cbc2_apply_stop_reset(); s_app.life = APP_LIFE_CHARGE_ONLY; log_info("usb in, charge-only (restore_on=%d)\n", s_app.restore_on); } else { log_info("usb out full_latched=%d restore_on=%d\n", s_app.full_latched, s_app.restore_on); if (s_app.full_latched || !s_app.restore_on) { s_app.req_poweroff = 1; } else { s_app.life = APP_LIFE_ON; app_cbc2_apply_stop_reset(); } } } if (s_app.usb_debounced) { if (bsp_chg_is_low()) { s_app.charge_led = 1; s_app.charge_dp = 1; s_app.full_latched = 0; } else { s_app.charge_led = 2; s_app.charge_dp = 2; /* 充满:DP 报未充电 */ s_app.full_latched = 1; } usr_var.usr_power_charge_flag = 1; } else { s_app.charge_led = 0; s_app.charge_dp = 2; usr_var.usr_power_charge_flag = 2; } } static void app_cbc2_sample_battery(void) { uint16_t mv = bsp_vbat_mv(); uint8_t alarm_on; uint8_t alarm_off; s_app.vbat_mv = mv; s_app.battery = app_cbc2_bat_percent(mv); alarm_on = BOARD_VBAT_LOW_MV; alarm_off = BOARD_VBAT_LOW_MV + BOARD_VBAT_LOW_HYST_MV; if (mv <= BOARD_VBAT_EMPTY_MV && !s_app.usb_debounced) { if (s_app.life != APP_LIFE_CHARGE_ONLY) { log_info("vbat %d mV lock\n", mv); s_app.req_poweroff = 1; } s_app.alm_low = 1; return; } if (!s_app.alm_low && mv < alarm_on) { s_app.alm_low = 1; s_app.lowbat_ms = 0; s_app.lowbat_run = 1; log_info("low battery warn %d mV\n", mv); } else if (s_app.alm_low && mv > alarm_off) { s_app.alm_low = 0; s_app.lowbat_run = 0; s_app.lowbat_ms = 0; } if (s_app.usb_debounced || s_app.life == APP_LIFE_CHARGE_ONLY) { return; } if (s_app.lowbat_run && s_app.alm_low) { s_app.lowbat_ms += 50; if (s_app.lowbat_ms >= BOARD_LOWBAT_SLEEP_MS) { log_info("low battery 1min, deep off\n"); s_app.req_poweroff = 1; } } } static void app_cbc2_handle_click(void) { uint8_t next; if (s_app.life != APP_LIFE_ON) { return; } if (s_app.power == 0) { return; } if (app_rope_is_calibrating()) { return; } next = (uint8_t)((s_app.play_mode + 1) % 7); app_cbc2_apply_play_mode(next, 1); } static void app_cbc2_key_hold_check(void) { if (!s_app.key_pressed || s_app.boot_key_lock) { return; } s_app.key_hold_ms++; if (s_app.life == APP_LIFE_CHARGE_ONLY || s_app.life == APP_LIFE_PAIR) { return; } if (s_app.life == APP_LIFE_ON && s_app.key_hold_ms == BOARD_KEY_POWER_MS) { s_app.req_poweroff = 1; } } static void app_cbc2_key_tick_1ms(void) { uint8_t raw = bsp_key_is_pressed(); if (raw == s_app.key_raw) { if (s_app.key_cnt < BOARD_KEY_DEBOUNCE_MS) { s_app.key_cnt++; } } else { s_app.key_raw = raw; s_app.key_cnt = 0; } if (s_app.key_cnt != BOARD_KEY_DEBOUNCE_MS) { app_cbc2_key_hold_check(); return; } if (raw && !s_app.key_pressed) { s_app.key_pressed = 1; s_app.key_hold_ms = 0; } else if (!raw && s_app.key_pressed) { if (s_app.boot_key_lock) { s_app.boot_key_lock = 0; s_app.key_pressed = 0; s_app.key_hold_ms = 0; log_info("boot key released\n"); return; } if (s_app.life == APP_LIFE_ON && s_app.power && s_app.key_hold_ms < BOARD_KEY_CLICK_MS && s_app.key_hold_ms >= 10) { s_app.req_click = 1; } s_app.key_pressed = 0; s_app.key_hold_ms = 0; } app_cbc2_key_hold_check(); } static void app_cbc2_boot_decide(void) { s_app.usb_raw = bsp_vpwr_is_online(); s_app.usb_debounced = s_app.usb_raw; s_app.usb_cnt = 5; s_app.vbat_mv = bsp_vbat_mv(); if (s_app.usb_debounced) { s_app.life = APP_LIFE_CHARGE_ONLY; s_app.restore_on = 0; s_app.power = 0; log_info("boot CHARGE_ONLY vbat=%d\n", s_app.vbat_mv); return; } if (s_app.vbat_mv <= BOARD_VBAT_EMPTY_MV) { log_info("boot vbat %d too low\n", s_app.vbat_mv); app_cbc2_boot_poweroff(); return; } /* 与 81bd2f7 一致:上电即绑定广播,不等按键、不 deep off */ s_app.power = 1; s_app.play_mode = 0; s_app.from_deep_boot = 0; s_app.boot_key_lock = bsp_key_is_pressed() ? 1 : 0; s_app.key_pressed = s_app.boot_key_lock; s_app.key_hold_ms = 0; s_app.key_raw = s_app.key_pressed; s_app.key_stable = s_app.key_pressed; s_app.key_cnt = BOARD_KEY_DEBOUNCE_MS; usr_var.usr_goto_pair = 1; s_app.life = APP_LIFE_PAIR; s_app.pair_ms = 0; log_info("boot PAIR power-on bind adv\n"); } /*============================================================================*/ /* 公有函数 */ /*============================================================================*/ AppCbc2_Ret_t app_cbc2_init(void) { if (s_app.inited) { return APP_CBC2_OK; } memset(&s_app, 0, sizeof(s_app)); s_app.charge_dp = 2; s_app.power = 0; bsp_hw_init(); app_nv_init(); app_led_init(); app_rope_init(); app_mode_init(); app_dnd_init(); app_cbc2_boot_decide(); s_app.inited = 1; sys_timer_add(NULL, app_cbc2_run, 50); log_info("cbc2 init life=%d power=%d\n", s_app.life, s_app.power); return APP_CBC2_OK; } AppCbc2_Ret_t app_cbc2_get_status(void) { return s_app.inited ? APP_CBC2_OK : APP_CBC2_ERR_NOT_INIT; } void app_cbc2_tick_1ms(void) { AppLedCtx_t led; if (!s_app.inited) { return; } app_rope_tick_1ms(); app_cbc2_key_tick_1ms(); (void)bsp_aloft_is_active(); memset(&led, 0, sizeof(led)); led.charge_led = s_app.charge_led; led.pairing = (uint8_t)(s_app.life == APP_LIFE_PAIR); led.app_power = s_app.power; led.low_bat = s_app.alm_low; led.life_on = (uint8_t)(s_app.life != APP_LIFE_CHARGE_ONLY); app_led_tick_1ms(&led, 1); } void app_cbc2_run(void *priv) { (void)priv; if (!s_app.inited) { return; } app_cbc2_sample_charge(); app_cbc2_sample_battery(); app_dnd_run(); app_mode_run(); if (s_app.life == APP_LIFE_PAIR) { s_app.pair_ms += 50; if (usr_get_pair_flag()) { app_cbc2_pair_end(1); } else if (s_app.pair_ms >= BOARD_PAIR_TIMEOUT_MS) { app_cbc2_pair_end(0); } } /* 仅 BLE 已起来后才允许再调 usr_goto_pair_mode,避免和 bt_ble_init 抢广播 */ if (s_app.req_pair) { s_app.req_pair = 0; if (s_app.life != APP_LIFE_CHARGE_ONLY) { app_cbc2_enter_pair(); } } if (s_app.req_click) { s_app.req_click = 0; app_cbc2_handle_click(); } if (s_app.req_poweroff) { s_app.req_poweroff = 0; app_cbc2_do_poweroff(); } app_nv_save_if_dirty(); } void app_cbc2_fill_report(AppCbc2Report_t *out) { if (!out) { return; } memset(out, 0, sizeof(*out)); out->power = s_app.power; out->play_mode = s_app.play_mode; out->calibration = app_rope_is_calibrating(); out->numberturns = app_rope_get_turns(); out->alm_low_battery = s_app.alm_low; out->battery = s_app.battery; out->charge = s_app.charge_dp; app_dnd_get_raw(out->dnd); } AppCbc2_Ret_t app_cbc2_ctrl_power(uint8_t on) { if (!app_cbc2_ctrl_allowed()) { log_info("ctrl power ignored (charge)\n"); return APP_CBC2_ERR_DENIED; } if (s_app.life == APP_LIFE_PAIR) { return APP_CBC2_ERR_BUSY; } on = on ? 1 : 0; if (on == s_app.power) { return APP_CBC2_OK; } if (on) { s_app.power = 1; app_cbc2_resume_functions(); } else { app_cbc2_pause_functions(); s_app.power = 0; } return APP_CBC2_OK; } AppCbc2_Ret_t app_cbc2_ctrl_play_mode(uint8_t mode) { if (!app_cbc2_ctrl_allowed() || s_app.power == 0) { return APP_CBC2_ERR_DENIED; } if (app_rope_is_calibrating()) { return APP_CBC2_ERR_BUSY; } app_cbc2_apply_play_mode(mode, (uint8_t)(mode != s_app.play_mode)); return APP_CBC2_OK; } AppCbc2_Ret_t app_cbc2_ctrl_hand(uint8_t reserved, uint8_t percent) { (void)reserved; if (!app_cbc2_ctrl_allowed() || s_app.power == 0) { return APP_CBC2_ERR_DENIED; } if (app_rope_is_calibrating()) { return APP_CBC2_ERR_BUSY; } app_cbc2_apply_stop_reset(); if (app_rope_goto_percent(percent) != APP_ROPE_OK) { return APP_CBC2_ERR_DENIED; } return APP_CBC2_OK; } AppCbc2_Ret_t app_cbc2_ctrl_cal(uint8_t on) { if (!app_cbc2_ctrl_allowed() || s_app.power == 0) { return APP_CBC2_ERR_DENIED; } if (on) { app_mode_stop(); s_app.play_mode = 0; if (app_rope_cal_start() != APP_ROPE_OK) { return APP_CBC2_ERR_DENIED; } } else { app_rope_cal_stop(); } return APP_CBC2_OK; } AppCbc2_Ret_t app_cbc2_ctrl_turns(uint8_t turns) { if (!app_cbc2_ctrl_allowed()) { return APP_CBC2_ERR_DENIED; } app_rope_set_turns(turns); return APP_CBC2_OK; } AppCbc2_Ret_t app_cbc2_ctrl_dnd(const uint8_t raw[APP_CBC2_DND_LEN]) { if (!raw) { return APP_CBC2_ERR_PARAM; } if (!app_cbc2_ctrl_allowed()) { return APP_CBC2_ERR_DENIED; } app_dnd_set_raw(raw); return APP_CBC2_OK; }