622 lines
16 KiB
C
622 lines
16 KiB
C
/******************************************************************************
|
|
* @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;
|
|
}
|