接入温湿度风扇产品固件(仲裁/条件/定时/显示),并补齐规格书与外设驱动
新增 usr_app 产品逻辑:手动>定时>条件仲裁、条件控温湿、定时任务、数码管显示、 按键/LED、风扇档位、NVM 掉电恢复、配对与开关机,上电按规格恢复上次状态。 新增 usr_periph:AHT20、CT1642、板级引脚与硬件封装;见宝协议 cond_config 改为 7 字节编解码,并接入产品 DP。 补充《温湿度风扇_固件需求规格书》,工程加入对应源文件。 .gitignore 忽略 WorkBuddy/Cursor 本地会话
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
/******************************************************************************
|
||||
* @file app_arbiter.c
|
||||
* @brief CBR3 模式仲裁器实现(规格书第 12 章:手动 > 定时 > 条件)
|
||||
* @author cyWu <1917507415@qq.com>
|
||||
* @date 2026.08.31
|
||||
* @version V1.0.0
|
||||
* @history
|
||||
* - V1.0.0, 2026.08.31, cyWu, 首次发布(P2)
|
||||
*
|
||||
* @note 本模块是唯一模式占用源,运行在 usr_timer_loop(任务上下文)。
|
||||
* 规则落地(规格书 12 章):
|
||||
* - 手动动作 → manual_intervene():owner=NONE;
|
||||
* 条件引擎感知后作废本轮(RAM,须回安全再满时长才重新触发);
|
||||
* 定时引擎感知后本窗剩余时间不自动抢回(等下一窗开始沿)。
|
||||
* - 定时窗开始 → timer_take()(定时 > 条件,条件即使满时长也不开);
|
||||
* - 条件触发 → cond_take()(仅 owner!=TIMER 允许;快照只存第一次);
|
||||
* - 两路条件后触发覆盖 → 仍走 cond_take(),快照不变(不回栈)。
|
||||
******************************************************************************/
|
||||
|
||||
#include "app_arbiter.h"
|
||||
#include "app_mode.h"
|
||||
#include "app_fan.h"
|
||||
|
||||
#define LOG_TAG_CONST APP
|
||||
#define LOG_TAG "[ARB]"
|
||||
#define LOG_INFO_ENABLE
|
||||
#include "debug.h"
|
||||
|
||||
/** 仲裁器上下文 */
|
||||
typedef struct {
|
||||
ArbOwner_t owner; /**< 当前占用者 */
|
||||
uint8_t snap_valid; /**< 条件快照是否有效 */
|
||||
uint8_t snap_mode; /**< 快照模式(第一次条件抢权前) */
|
||||
uint8_t snap_gear; /**< 快照档位 */
|
||||
} AppArbiterCtx_t;
|
||||
|
||||
static AppArbiterCtx_t s_arb;
|
||||
|
||||
/**
|
||||
* @brief 初始化仲裁器
|
||||
* @return 无
|
||||
*/
|
||||
void app_arbiter_init(void)
|
||||
{
|
||||
s_arb.owner = ARB_OWNER_NONE;
|
||||
s_arb.snap_valid = 0;
|
||||
s_arb.snap_mode = 0;
|
||||
s_arb.snap_gear = 0;
|
||||
log_info("arbiter init\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 当前模式占用者
|
||||
* @return ArbOwner_t
|
||||
*/
|
||||
uint8_t app_arbiter_get_owner(void)
|
||||
{
|
||||
return (uint8_t)s_arb.owner;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 手动动作统一入口:释放一切自动化占用
|
||||
* @note 条件/定时引擎下个 tick 查 owner 感知后各自作废;不恢复快照
|
||||
* (手动已接管,快照对应的用户状态已过时)
|
||||
* @return 无
|
||||
*/
|
||||
void app_arbiter_manual_intervene(void)
|
||||
{
|
||||
if (s_arb.owner != ARB_OWNER_NONE) {
|
||||
log_info("manual intervene, owner %d -> none\n", s_arb.owner);
|
||||
}
|
||||
s_arb.owner = ARB_OWNER_NONE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 条件引擎尝试占用
|
||||
* @return 1=成功;0=定时占用中(规格书 12:定时 vs 条件 → 定时胜)
|
||||
*/
|
||||
uint8_t app_arbiter_cond_take(void)
|
||||
{
|
||||
if (s_arb.owner == ARB_OWNER_TIMER) {
|
||||
return 0; /* 定时窗内条件不触发 */
|
||||
}
|
||||
if (!s_arb.snap_valid) {
|
||||
/* 第一次条件抢权前快照用户状态;后覆盖/多次触发不改快照(10.3-6) */
|
||||
s_arb.snap_mode = app_mode_get_active();
|
||||
s_arb.snap_gear = app_fan_get_gear();
|
||||
s_arb.snap_valid = 1;
|
||||
log_info("cond take, snapshot mode=%d gear=%d\n", s_arb.snap_mode, s_arb.snap_gear);
|
||||
}
|
||||
s_arb.owner = ARB_OWNER_COND;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 条件是否仍在占用
|
||||
* @return 1=owner==COND
|
||||
*/
|
||||
uint8_t app_arbiter_cond_active(void)
|
||||
{
|
||||
return (s_arb.owner == ARB_OWNER_COND) ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 条件自然失效:恢复快照后调用(清 owner 与快照)
|
||||
* @return 无
|
||||
*/
|
||||
void app_arbiter_cond_release(void)
|
||||
{
|
||||
if (s_arb.owner == ARB_OWNER_COND) {
|
||||
s_arb.owner = ARB_OWNER_NONE;
|
||||
}
|
||||
s_arb.snap_valid = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 取条件快照
|
||||
* @param mode 输出模式
|
||||
* @param gear 输出档位
|
||||
* @return 无
|
||||
*/
|
||||
void app_arbiter_cond_get_snapshot(uint8_t *mode, uint8_t *gear)
|
||||
{
|
||||
*mode = s_arb.snap_mode;
|
||||
*gear = s_arb.snap_gear;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 条件被手动打断:只清快照(不恢复、不清 owner)
|
||||
* @return 无
|
||||
*/
|
||||
void app_arbiter_cond_snapshot_clear(void)
|
||||
{
|
||||
s_arb.snap_valid = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 定时窗开始占用(优先级高于条件)
|
||||
* @return 1=成功
|
||||
*/
|
||||
uint8_t app_arbiter_timer_take(void)
|
||||
{
|
||||
if (s_arb.owner != ARB_OWNER_TIMER) {
|
||||
log_info("timer take\n");
|
||||
}
|
||||
s_arb.owner = ARB_OWNER_TIMER;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 定时窗结束释放(调用方负责收口:风扇 0、mode 0)
|
||||
* @return 无
|
||||
*/
|
||||
void app_arbiter_timer_release(void)
|
||||
{
|
||||
if (s_arb.owner == ARB_OWNER_TIMER) {
|
||||
s_arb.owner = ARB_OWNER_NONE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/******************************************************************************
|
||||
* @file app_arbiter.h
|
||||
* @brief CBR3 模式仲裁器(规格书第 12 章):手动 > 定时 > 条件
|
||||
* @author cyWu <1917507415@qq.com>
|
||||
* @date 2026.08.31
|
||||
* @version V1.0.0
|
||||
* @history
|
||||
* - V1.0.0, 2026.08.31, cyWu, 首次发布(P2)
|
||||
*
|
||||
* @note 把"谁在驱动模式"收成一处,避免 mode/cond/timer 互调成环。
|
||||
* 唯一状态源是 owner(NONE/COND/TIMER)+ 条件快照;
|
||||
* 条件/定时引擎各自维护占用细节,通过查询 owner 感知被打断:
|
||||
* - 手动动作(短按/APP 写档/mode)→ app_arbiter_manual_intervene()
|
||||
* 清 owner;条件引擎感知后作废本轮(RAM),定时引擎感知后本窗
|
||||
* 不再抢回(规格书 12 章规则 1/2)。
|
||||
* - 条件自然失效(比较不成立)→ 条件引擎恢复快照后
|
||||
* app_arbiter_cond_release()。
|
||||
* - 定时窗结束 → app_arbiter_timer_release() + 收口。
|
||||
* 快照:第一次条件抢权前保存用户 mode+gear(规格书 10.3-3),
|
||||
* 后覆盖/手动介入均不更新它(10.3-6),只在条件自然失效恢复后清空。
|
||||
******************************************************************************/
|
||||
|
||||
#ifndef __APP_ARBITER_H__
|
||||
#define __APP_ARBITER_H__
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/** 当前模式占用者 */
|
||||
typedef enum {
|
||||
ARB_OWNER_NONE = 0, /**< 空闲 / 手动接管 */
|
||||
ARB_OWNER_COND = 1, /**< 条件引擎占用 */
|
||||
ARB_OWNER_TIMER = 2 /**< 定时引擎占用 */
|
||||
} ArbOwner_t;
|
||||
|
||||
/**
|
||||
* @brief 初始化仲裁器
|
||||
* @return 无
|
||||
*/
|
||||
void app_arbiter_init(void);
|
||||
|
||||
/**
|
||||
* @brief 当前模式占用者
|
||||
* @return ArbOwner_t
|
||||
*/
|
||||
uint8_t app_arbiter_get_owner(void);
|
||||
|
||||
/**
|
||||
* @brief 手动动作统一入口(短按 / APP 写 fan_gear / APP 写 mode)
|
||||
* @note 立即释放一切自动化占用;条件/定时引擎下个 tick 感知后各自作废
|
||||
* @return 无
|
||||
*/
|
||||
void app_arbiter_manual_intervene(void);
|
||||
|
||||
/* ═══ 条件引擎接口 ═══ */
|
||||
|
||||
/**
|
||||
* @brief 条件引擎尝试占用(触发模式前调用)
|
||||
* @note 定时占用中返回 0(规格书 12:定时窗 vs 条件 → 定时胜,条件不开);
|
||||
* 占用成功时若尚无快照则保存当前用户 mode+gear(仅第一次)
|
||||
* @return 1=占用成功,0=被定时占用拒绝
|
||||
*/
|
||||
uint8_t app_arbiter_cond_take(void);
|
||||
|
||||
/**
|
||||
* @brief 条件是否仍在占用
|
||||
* @return 1=owner==COND
|
||||
*/
|
||||
uint8_t app_arbiter_cond_active(void);
|
||||
|
||||
/**
|
||||
* @brief 条件自然失效:恢复快照后调用(清 owner 与快照)
|
||||
* @return 无
|
||||
*/
|
||||
void app_arbiter_cond_release(void);
|
||||
|
||||
/**
|
||||
* @brief 取条件快照(恢复用)
|
||||
* @param mode 输出快照模式
|
||||
* @param gear 输出快照档位
|
||||
* @return 无
|
||||
*/
|
||||
void app_arbiter_cond_get_snapshot(uint8_t *mode, uint8_t *gear);
|
||||
|
||||
/**
|
||||
* @brief 条件被手动打断:只清快照(快照对应的用户状态已过时),
|
||||
* 不恢复、不清 owner(owner 已被 manual_intervene 清掉)
|
||||
* @return 无
|
||||
*/
|
||||
void app_arbiter_cond_snapshot_clear(void);
|
||||
|
||||
/* ═══ 定时引擎接口 ═══ */
|
||||
|
||||
/**
|
||||
* @brief 定时引擎窗开始占用
|
||||
* @return 1=成功(恒成功,定时优先级高于条件)
|
||||
*/
|
||||
uint8_t app_arbiter_timer_take(void);
|
||||
|
||||
/**
|
||||
* @brief 定时窗结束释放
|
||||
* @return 无
|
||||
*/
|
||||
void app_arbiter_timer_release(void);
|
||||
|
||||
#endif /* __APP_ARBITER_H__ */
|
||||
@@ -0,0 +1,132 @@
|
||||
/******************************************************************************
|
||||
* @file app_boot.c
|
||||
* @brief CBR3 上电按键判定实现(3s 开机 / 5s 配对 / 12s OTA)
|
||||
* @author cyWu <1917507415@qq.com>
|
||||
* @date 2026.08.29
|
||||
* @version V1.0.0
|
||||
* @history
|
||||
* - V1.0.0, 2026.08.29, cyWu, 首次发布(移植门夹:删 USB 直通、删低电拒绝)
|
||||
******************************************************************************/
|
||||
|
||||
#include "app_boot.h"
|
||||
#include "app_pair.h"
|
||||
#include "bsp_hw.h"
|
||||
#include "board_pin.h"
|
||||
#include "app_main.h"
|
||||
#include "usr_le_api.h"
|
||||
#include "led_manager.h"
|
||||
#include "asm/power/power_api.h"
|
||||
#include "asm/power/power_reset.h"
|
||||
|
||||
#define LOG_TAG_CONST APP
|
||||
#define LOG_TAG "[APP_BOOT]"
|
||||
#define LOG_INFO_ENABLE
|
||||
#include "debug.h"
|
||||
|
||||
extern void clr_wdt(void);
|
||||
extern u32 timer_get_ms(void);
|
||||
|
||||
/**
|
||||
* @brief 忙等若干毫秒,并喂狗
|
||||
* @param ms 等待时长
|
||||
* @return 无
|
||||
*/
|
||||
static void boot_delay_ms(uint32_t ms)
|
||||
{
|
||||
uint32_t t0 = timer_get_ms();
|
||||
|
||||
while ((timer_get_ms() - t0) < ms) {
|
||||
clr_wdt();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief BLE 起来前进深睡(业务定时器尚未启动),原地喂狗不返回
|
||||
* @note power_set_soft_poweroff() 只是向 SDK 底层发起软关机请求(置个标志位),
|
||||
* 不是同步断电——真正切电源是底层电源管理在之后某个时机才执行的。
|
||||
* 此时 BLE 栈、业务定时器都还没起来,如果直接 return,代码会接着把
|
||||
* usr_jb_init 剩下的初始化跑完,等于白判定。所以必须用 while(1) 卡住:
|
||||
* 一边喂狗防止看门狗打断,一边等底层真正把电断掉。
|
||||
* @return 无(不会返回)
|
||||
*/
|
||||
static void boot_sleep(void)
|
||||
{
|
||||
log_info("boot sleep\n");
|
||||
bsp_hw_enter_sleep_io();
|
||||
power_set_soft_poweroff();
|
||||
while (1) {
|
||||
clr_wdt();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 等待并判定开机按键
|
||||
* @note 配对(5s)不在到点时立即提交:继续按住观察是否会按到 12s 触发 OTA,
|
||||
* 真正的配对提交放在松手那一刻做。按满 5s 灯先切到配对快闪提示。
|
||||
* 3s 常亮 / 5s 快闪直接复用 led_manager 的 LED_EVENT_STANDBY /
|
||||
* LED_EVENT_PAIRING 事件。LED 事件库由调用方(usr_jb_init)在本函数
|
||||
* 之前完成初始化,这里直接假定可用。
|
||||
* @return 无(深睡分支不返回)
|
||||
*/
|
||||
void app_boot_wait_key(void)
|
||||
{
|
||||
uint32_t t0;
|
||||
uint32_t elapsed;
|
||||
uint8_t pair_armed = 0;
|
||||
|
||||
usr_var.usr_goto_pair = 0;
|
||||
|
||||
/* ⚠ 无 USB 直通:插电/冷启动一律走按键判定(规格书 5.1)。 */
|
||||
|
||||
if (!bsp_key_is_pressed()) {
|
||||
log_info("boot no key, sleep\n");
|
||||
boot_sleep();
|
||||
}
|
||||
|
||||
t0 = timer_get_ms();
|
||||
log_info("boot key hold, wait 3s on / 5s pair / 12s ota\n");
|
||||
|
||||
while (bsp_key_is_pressed()) {
|
||||
elapsed = timer_get_ms() - t0;
|
||||
clr_wdt();
|
||||
|
||||
/* 12s 优先于配对:按满 12s 直接进 OTA,不再等松手 */
|
||||
if (elapsed >= BOARD_KEY_OTA_MS) {
|
||||
log_info("boot hold %ums, power on + ota\n", (unsigned)elapsed);
|
||||
usr_app_ota_init();
|
||||
return;
|
||||
}
|
||||
|
||||
/* 5s 只武装配对并切到快闪,真正提交放到松手那一刻 */
|
||||
if (!pair_armed && (elapsed >= BOARD_KEY_PAIR_MS)) {
|
||||
pair_armed = 1;
|
||||
LED_EventDelete(LED_EVENT_STANDBY);
|
||||
LED_EventAdd(LED_EVENT_PAIRING);
|
||||
log_info("boot hold %ums, pair hint blink (release now for pair, "
|
||||
"keep holding to %ums for ota)\n",
|
||||
(unsigned)elapsed, (unsigned)BOARD_KEY_OTA_MS);
|
||||
} else if (!pair_armed && !LED_IsEventExist(LED_EVENT_STANDBY)
|
||||
&& (elapsed >= BOARD_KEY_POWER_MS)) {
|
||||
LED_EventAdd(LED_EVENT_STANDBY);
|
||||
log_info("boot hold 3s, will power on\n");
|
||||
}
|
||||
|
||||
user_led_handle(); /* 按事件表当前状态刷新一次灯 */
|
||||
}
|
||||
|
||||
elapsed = timer_get_ms() - t0;
|
||||
if (elapsed < BOARD_KEY_POWER_MS) {
|
||||
log_info("boot hold %ums < 3s, sleep\n", (unsigned)elapsed);
|
||||
boot_sleep();
|
||||
}
|
||||
|
||||
/* 松手时已过 5s(且没按到 12s):提交配对;否则只是正常开机 */
|
||||
if (pair_armed) {
|
||||
usr_var.usr_goto_pair = 1;
|
||||
usr_clear_pair_info_noreset();
|
||||
app_pair_start();
|
||||
log_info("boot hold %ums, power on + pair\n", (unsigned)elapsed);
|
||||
} else {
|
||||
log_info("boot hold %ums, power on\n", (unsigned)elapsed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/******************************************************************************
|
||||
* @file app_boot.h
|
||||
* @brief CBR3 上电按键判定:3s 开机,5s 配对,12s OTA,不足 3s 松手再睡
|
||||
* @author cyWu <1917507415@qq.com>
|
||||
* @date 2026.08.29
|
||||
* @version V1.0.0
|
||||
* @history
|
||||
* - V1.0.0, 2026.08.29, cyWu, 首次发布(移植门夹:删 USB 直通开机、删低电拒绝开机)
|
||||
*
|
||||
* @note 必须在 BLE 协议栈起来、业务定时器启动之前调用。杰理 jiffies 此时
|
||||
* 往往 10ms 才跳一次,所以内部一律用 timer_get_ms 直接量时间。
|
||||
* ⚠ 本产品 USB 常电、无电池:插电/冷启动【不会】自动进入工作态,
|
||||
* 必须按住 3s(规格书 5.1/5.2)。门夹的 USB 直通开机逻辑禁止移植。
|
||||
******************************************************************************/
|
||||
|
||||
#ifndef __APP_BOOT_H__
|
||||
#define __APP_BOOT_H__
|
||||
|
||||
/**
|
||||
* @brief 等待并判定开机按键:不足 3s 松手、或全程未按键,直接原地深睡(不返回);
|
||||
* 满 3s 视为正常开机;按满 5s 绿灯改快闪提示"松手即配网",真正清绑定 /
|
||||
* 开绑定广播仍等松手(内部调用 app_pair_start);持续按满 12s 直接开机
|
||||
* 并进入 OTA(调用 usr_app_ota_init)。
|
||||
* @return 无(深睡分支通过 while(1) 喂狗不返回;其余情况正常返回)
|
||||
*/
|
||||
void app_boot_wait_key(void);
|
||||
|
||||
#endif /* __APP_BOOT_H__ */
|
||||
@@ -0,0 +1,464 @@
|
||||
/******************************************************************************
|
||||
* @file app_cond.c
|
||||
* @brief CBR3 条件引擎(P2):7 组 7 字节条件,比较成立满时长触发模式
|
||||
* @author cyWu <1917507415@qq.com>
|
||||
* @date 2026.08.29
|
||||
* @version V2.0.0
|
||||
* @history
|
||||
* - V1.0.0, 2026.08.29, cyWu, 首次发布(P0 stub)
|
||||
* - V1.1.0, 2026.08.31, cyWu, P1:set_en/set_config 存 VM(引擎仍关)
|
||||
* - V2.0.0, 2026.08.31, cyWu, P2 实现完整条件引擎(规格书第 10 章):
|
||||
* 7 字节解析/AND 比较/时长累计(采样成功才累计)/触发/保持/
|
||||
* 条件强力循环/后覆盖不回栈/手动作废(RAM)/快照恢复/闪烁喂真值
|
||||
*
|
||||
* @note 运行上下文:app_cond_run 由 usr_timer_loop(500ms,任务上下文)调用
|
||||
* (规格书 8.4 时长按采样周期 2s 计,500ms 粒度足够;且触发时可能
|
||||
* 要写 VM——单次定时在 app_timer_task,条件本身不写 VM,但保持
|
||||
* 任务上下文与仲裁器同侧,避免跨上下文竞态)。
|
||||
* 字节布局(规格书 10.2):Byte0=使能 Byte1-2=温度阈值(int16 大端×10)
|
||||
* Byte3=湿度 Byte4=持续分钟(0=立即) Byte5=动作模式
|
||||
* Byte6=Bit7-5 温度比较 / Bit4-2 湿度比较 / Bit1-0 保留。
|
||||
* 比较编码:1=< 2=<= 3-== 4->= 5-> 0/6/7=无条件。
|
||||
******************************************************************************/
|
||||
|
||||
#include "app_cond.h"
|
||||
#include "app_arbiter.h"
|
||||
#include "app_mode.h"
|
||||
#include "app_fan.h"
|
||||
#include "app_th.h"
|
||||
#include "app_disp.h"
|
||||
#include "app_nvm.h"
|
||||
#include "jb_product.h"
|
||||
#include "board_pin.h"
|
||||
|
||||
#define LOG_TAG_CONST APP
|
||||
#define LOG_TAG "[APP_COND]"
|
||||
#define LOG_INFO_ENABLE
|
||||
#include "debug.h"
|
||||
|
||||
/** 每组运行时状态(配置本体在 gDevData.cond_config1~7) */
|
||||
typedef struct {
|
||||
uint32_t acc_ms; /**< 比较成立且采样成功的累计毫秒(未满时长) */
|
||||
uint8_t voided; /**< 1=被手动作废(RAM,不写 VM,须回安全再满时长) */
|
||||
uint8_t trig_by; /**< 触发占用时 Byte5 动作(0=未在占用) */
|
||||
} CondState_t;
|
||||
|
||||
static CondState_t s_cond[7]; /**< [0]~[6] 对应 config1~7 */
|
||||
static uint8_t s_occupy_idx;/**< 当前占用组 0~6,0xFF=无 */
|
||||
|
||||
/**
|
||||
* @brief 比较编码是否有效(1~5;0/6/7 视为无条件)
|
||||
* @param cmp 3bit 比较编码
|
||||
* @return 1=有效,0=无条件
|
||||
*/
|
||||
static uint8_t cond_cmp_valid(uint8_t cmp)
|
||||
{
|
||||
return ((cmp >= 1) && (cmp <= 5)) ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 温度比较(当前温度 ×10 vs 阈值 ×10)
|
||||
* @param cmp 比较编码
|
||||
* @param th 温度阈值 ×10
|
||||
* @return 1=成立
|
||||
*/
|
||||
static uint8_t cond_cmp_temp(uint8_t cmp, int16_t th)
|
||||
{
|
||||
int16_t cur = app_th_get_temp_x10();
|
||||
|
||||
switch (cmp) {
|
||||
case 1: return (cur < th) ? 1 : 0;
|
||||
case 2: return (cur <= th) ? 1 : 0;
|
||||
case 3: return (cur == th) ? 1 : 0;
|
||||
case 4: return (cur >= th) ? 1 : 0;
|
||||
case 5: return (cur > th) ? 1 : 0;
|
||||
default: return 1; /* 无条件 */
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 湿度比较(当前 % vs 阈值 %)
|
||||
* @param cmp 比较编码
|
||||
* @param th 湿度阈值 %
|
||||
* @return 1=成立
|
||||
*/
|
||||
static uint8_t cond_cmp_humi(uint8_t cmp, uint8_t th)
|
||||
{
|
||||
uint8_t cur = app_th_get_humi();
|
||||
|
||||
switch (cmp) {
|
||||
case 1: return (cur < th) ? 1 : 0;
|
||||
case 2: return (cur <= th) ? 1 : 0;
|
||||
case 3: return (cur == th) ? 1 : 0;
|
||||
case 4: return (cur >= th) ? 1 : 0;
|
||||
case 5: return (cur > th) ? 1 : 0;
|
||||
default: return 1; /* 无条件 */
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 取第 idx 组条件配置(gDevData 里是 7 个独立数组字段)
|
||||
* @param idx 0~6
|
||||
* @return 配置指针;越界返回 NULL
|
||||
*/
|
||||
static const uint8_t *cond_cfg_ptr(uint8_t idx)
|
||||
{
|
||||
const uint8_t *tbl[7];
|
||||
|
||||
tbl[0] = gDevData.cond_config1;
|
||||
tbl[1] = gDevData.cond_config2;
|
||||
tbl[2] = gDevData.cond_config3;
|
||||
tbl[3] = gDevData.cond_config4;
|
||||
tbl[4] = gDevData.cond_config5;
|
||||
tbl[5] = gDevData.cond_config6;
|
||||
tbl[6] = gDevData.cond_config7;
|
||||
return (idx < 7) ? tbl[idx] : NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 整组比较是否成立(两侧 AND / 单侧 / 双侧无条件=无效组)
|
||||
* @param cfg 7 字节配置
|
||||
* @return 1=成立,0=不成立或无效组
|
||||
*/
|
||||
static uint8_t cond_group_match(const uint8_t *cfg)
|
||||
{
|
||||
uint8_t temp_cmp = (uint8_t)((cfg[6] >> 5) & 0x7);
|
||||
uint8_t humi_cmp = (uint8_t)((cfg[6] >> 2) & 0x7);
|
||||
uint8_t temp_ok = cond_cmp_valid(temp_cmp);
|
||||
uint8_t humi_ok = cond_cmp_valid(humi_cmp);
|
||||
int16_t temp_th = (int16_t)((cfg[1] << 8) | cfg[2]); /* int16 大端 ×10 */
|
||||
uint8_t humi_th = cfg[3];
|
||||
|
||||
/* 规格书 8.4:-- 或尚无有效采样时比较视为不成立,避免用旧值误触发 */
|
||||
if (app_th_is_fault() || !app_th_get_valid()) {
|
||||
return 0;
|
||||
}
|
||||
if (!temp_ok && !humi_ok) {
|
||||
return 0; /* 两侧都无条件:无效组,不触发(10.2) */
|
||||
}
|
||||
if (temp_ok && humi_ok) {
|
||||
return (cond_cmp_temp(temp_cmp, temp_th) && cond_cmp_humi(humi_cmp, humi_th)) ? 1 : 0;
|
||||
}
|
||||
if (temp_ok) {
|
||||
return cond_cmp_temp(temp_cmp, temp_th);
|
||||
}
|
||||
return cond_cmp_humi(humi_cmp, humi_th);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 触发时长(毫秒);Byte4=0 立即触发
|
||||
* @param cfg 7 字节配置
|
||||
* @return 时长 ms
|
||||
*/
|
||||
static uint32_t cond_duration_ms(const uint8_t *cfg)
|
||||
{
|
||||
return (uint32_t)cfg[4] * 60000u;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 清全部组累计(定时窗抢权后须重新满时长,规格书 12 章)
|
||||
* @return 无
|
||||
*/
|
||||
static void cond_reset_acc_all(void)
|
||||
{
|
||||
uint8_t i;
|
||||
|
||||
for (i = 0; i < 7; i++) {
|
||||
s_cond[i].acc_ms = 0;
|
||||
s_cond[i].trig_by = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 条件动作切模式。Byte5=0 是关风扇,不能走 set_manual(0)(那会恢复旧档)
|
||||
* @param mode 0=关闭,1=通风,2=强力
|
||||
* @return 无
|
||||
*/
|
||||
static void cond_apply_mode(uint8_t mode)
|
||||
{
|
||||
if (mode == 0) {
|
||||
app_mode_stop();
|
||||
app_fan_set_gear(0);
|
||||
} else {
|
||||
app_mode_set_manual(mode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 恢复快照并释放条件占用(比较不成立时,10.3-5)
|
||||
* @return 无
|
||||
*/
|
||||
static void cond_restore_and_release(void)
|
||||
{
|
||||
uint8_t idx = s_occupy_idx;
|
||||
uint8_t m, g;
|
||||
|
||||
app_arbiter_cond_get_snapshot(&m, &g);
|
||||
app_mode_set_manual(m); /* 恢复快照模式(mode=0 时不动档位) */
|
||||
app_fan_set_gear(g); /* 恢复快照档位 */
|
||||
app_arbiter_cond_release();
|
||||
s_cond[idx].acc_ms = 0;
|
||||
s_cond[idx].trig_by = 0;
|
||||
s_occupy_idx = 0xFF;
|
||||
log_info("cond[%d] release, restore mode=%d gear=%d\n", idx + 1, m, g);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 一组触发(占用 + 快照 + 切模式)
|
||||
* @param idx 组索引 0~6
|
||||
* @return 1=已触发,0=被定时占用拒绝
|
||||
*/
|
||||
static uint8_t cond_fire(uint8_t idx)
|
||||
{
|
||||
const uint8_t *cfg = cond_cfg_ptr(idx);
|
||||
|
||||
if (!app_arbiter_cond_take()) {
|
||||
return 0; /* 定时占用中,条件不开(12 章) */
|
||||
}
|
||||
s_occupy_idx = idx;
|
||||
s_cond[idx].trig_by = cfg[5];
|
||||
s_cond[idx].acc_ms = 0;
|
||||
cond_apply_mode(cfg[5]);
|
||||
log_info("cond[%d] fire mode=%d\n", idx + 1, cfg[5]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 扫描阶段:找满时长的组触发(owner==NONE 时)
|
||||
* @param dt 距上次调用的毫秒数
|
||||
* @return 无
|
||||
*/
|
||||
static void cond_scan_trigger(uint16_t dt)
|
||||
{
|
||||
uint8_t idx;
|
||||
|
||||
for (idx = 0; idx < 7; idx++) {
|
||||
const uint8_t *cfg = cond_cfg_ptr(idx);
|
||||
uint8_t ok;
|
||||
|
||||
if (!cfg || (cfg[0] == 0)) {
|
||||
continue; /* 组未使能 */
|
||||
}
|
||||
if (s_cond[idx].voided) {
|
||||
/* 作废组:等比较回安全(不成立)才解除作废(10.3-7) */
|
||||
if (!cond_group_match(cfg)) {
|
||||
s_cond[idx].voided = 0;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
ok = app_th_last_sample_ok(); /* 8.4:只在采样成功时累加 */
|
||||
if (cond_group_match(cfg)) {
|
||||
if (ok) {
|
||||
s_cond[idx].acc_ms += dt;
|
||||
if (s_cond[idx].acc_ms >= cond_duration_ms(cfg)) {
|
||||
if (cond_fire(idx)) {
|
||||
return; /* 一拍只触发一个,下一拍进保持 */
|
||||
}
|
||||
}
|
||||
}
|
||||
/* 采样失败且比较成立:不累加、不清零(8.4) */
|
||||
} else {
|
||||
s_cond[idx].acc_ms = 0; /* 比较不成立:计时清零(10.3-2) */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 保持阶段:扫描其它组后覆盖(10.3-6,不回栈、快照不变)
|
||||
* @param dt 距上次调用的毫秒数
|
||||
* @return 无
|
||||
*/
|
||||
static void cond_scan_override(uint16_t dt)
|
||||
{
|
||||
uint8_t idx;
|
||||
|
||||
for (idx = 0; idx < 7; idx++) {
|
||||
const uint8_t *cfg = cond_cfg_ptr(idx);
|
||||
|
||||
if (idx == s_occupy_idx) {
|
||||
continue;
|
||||
}
|
||||
if (!cfg || (cfg[0] == 0) || s_cond[idx].voided) {
|
||||
continue;
|
||||
}
|
||||
if (cond_group_match(cfg)) {
|
||||
if (app_th_last_sample_ok()) {
|
||||
s_cond[idx].acc_ms += dt;
|
||||
if (s_cond[idx].acc_ms >= cond_duration_ms(cfg)) {
|
||||
/* 后触发覆盖当前条件模式,不回栈(D1) */
|
||||
s_occupy_idx = idx;
|
||||
s_cond[idx].trig_by = cfg[5];
|
||||
s_cond[idx].acc_ms = 0;
|
||||
cond_apply_mode(cfg[5]);
|
||||
log_info("cond[%d] override mode=%d\n", idx + 1, cfg[5]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
s_cond[idx].acc_ms = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 更新告警闪烁输入(规格书 8.2:任一使能组温度/湿度比较成立 → 对应管闪)
|
||||
* @note 只看单侧比较成立,不看整组 AND;fault "--" 优先由 app_disp 处理
|
||||
* @return 无
|
||||
*/
|
||||
static void cond_update_disp_alarm(void)
|
||||
{
|
||||
uint8_t t_alarm = 0;
|
||||
uint8_t h_alarm = 0;
|
||||
uint8_t idx;
|
||||
|
||||
if (app_th_is_fault() || !app_th_get_valid() || !gDevData.cond_en) {
|
||||
app_disp_set_temp_alarm(0);
|
||||
app_disp_set_humi_alarm(0);
|
||||
return;
|
||||
}
|
||||
|
||||
for (idx = 0; idx < 7; idx++) {
|
||||
const uint8_t *cfg = cond_cfg_ptr(idx);
|
||||
|
||||
if (!cfg || (cfg[0] == 0) || s_cond[idx].voided) {
|
||||
continue;
|
||||
}
|
||||
{
|
||||
uint8_t temp_cmp = (uint8_t)((cfg[6] >> 5) & 0x7);
|
||||
uint8_t humi_cmp = (uint8_t)((cfg[6] >> 2) & 0x7);
|
||||
int16_t temp_th = (int16_t)((cfg[1] << 8) | cfg[2]);
|
||||
|
||||
if (cond_cmp_valid(temp_cmp) && cond_cmp_temp(temp_cmp, temp_th)) {
|
||||
t_alarm = 1;
|
||||
}
|
||||
if (cond_cmp_valid(humi_cmp) && cond_cmp_humi(humi_cmp, cfg[3])) {
|
||||
h_alarm = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
app_disp_set_temp_alarm(t_alarm);
|
||||
app_disp_set_humi_alarm(h_alarm);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 初始化条件引擎
|
||||
* @return 无
|
||||
*/
|
||||
void app_cond_init(void)
|
||||
{
|
||||
uint8_t i;
|
||||
|
||||
#if !BOARD_COND_ENABLE
|
||||
log_info("cond engine disabled\n");
|
||||
return;
|
||||
#endif
|
||||
for (i = 0; i < 7; i++) {
|
||||
s_cond[i].acc_ms = 0;
|
||||
s_cond[i].voided = 0;
|
||||
s_cond[i].trig_by = 0;
|
||||
}
|
||||
s_occupy_idx = 0xFF;
|
||||
log_info("cond init\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 推进条件引擎(500ms,任务上下文,由 usr_timer_loop 调用)
|
||||
* @param dt 距上次调用的毫秒数
|
||||
* @return 无
|
||||
*/
|
||||
void app_cond_run(uint16_t dt)
|
||||
{
|
||||
#if !BOARD_COND_ENABLE
|
||||
(void)dt;
|
||||
return;
|
||||
#endif
|
||||
/* 总开关:cond_en=0 不扫描;若正在跑视为条件结束恢复快照(10.1) */
|
||||
if (!gDevData.cond_en) {
|
||||
if (s_occupy_idx != 0xFF) {
|
||||
cond_restore_and_release();
|
||||
}
|
||||
app_disp_set_temp_alarm(0);
|
||||
app_disp_set_humi_alarm(0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (s_occupy_idx != 0xFF) {
|
||||
/* ── 占用保持阶段 ── */
|
||||
if (!app_arbiter_cond_active()) {
|
||||
uint8_t vidx = s_occupy_idx;
|
||||
|
||||
if (app_arbiter_get_owner() == ARB_OWNER_TIMER) {
|
||||
/* 定时抢走:重新计时长,不作废(规格书 12:窗结束须重新满时长) */
|
||||
cond_reset_acc_all();
|
||||
s_occupy_idx = 0xFF;
|
||||
log_info("cond[%d] preempted by timer, re-time\n", vidx + 1);
|
||||
} else {
|
||||
/* 手动/APP 介入:本轮作废,须回安全再满时长(10.3-7) */
|
||||
s_cond[vidx].voided = 1;
|
||||
app_arbiter_cond_snapshot_clear();
|
||||
s_occupy_idx = 0xFF;
|
||||
log_info("cond[%d] voided by manual\n", vidx + 1);
|
||||
}
|
||||
cond_update_disp_alarm();
|
||||
return;
|
||||
}
|
||||
|
||||
/* 其它组满时长 → 后触发覆盖(不回栈) */
|
||||
cond_scan_override(dt);
|
||||
|
||||
/* 当前占用组:成立保持 / 强力到点再开一轮 / 不成立恢复快照 */
|
||||
{
|
||||
const uint8_t *cfg = cond_cfg_ptr(s_occupy_idx);
|
||||
|
||||
if (cfg && cond_group_match(cfg)) {
|
||||
/* 条件强力:15min 到点(mode 已回 0)且仍成立 → 立即再开一轮(10.3-4) */
|
||||
if ((s_cond[s_occupy_idx].trig_by == 2) && (app_mode_get_active() == 0)) {
|
||||
app_mode_set_manual(2);
|
||||
log_info("cond[%d] power 15min over, restart\n", s_occupy_idx + 1);
|
||||
}
|
||||
/* 通风 25/5 循环由 app_mode 自理 */
|
||||
} else {
|
||||
cond_restore_and_release(); /* 比较不成立 → 恢复快照,重新计时 */
|
||||
}
|
||||
}
|
||||
cond_update_disp_alarm();
|
||||
return;
|
||||
}
|
||||
|
||||
/* ── 空闲扫描触发阶段 ── */
|
||||
if (app_arbiter_get_owner() == ARB_OWNER_TIMER) {
|
||||
/* 定时占用中:条件即使满时长也不开;累计清零,窗结束重新计(12 章) */
|
||||
cond_reset_acc_all();
|
||||
cond_update_disp_alarm();
|
||||
return;
|
||||
}
|
||||
cond_scan_trigger(dt);
|
||||
cond_update_disp_alarm();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief APP 写 DP5 条件总开关:存 gDevData + 触发 VM 持久化
|
||||
* @param en 0=关,1=开
|
||||
* @return 无
|
||||
*/
|
||||
void app_cond_set_en(uint8_t en)
|
||||
{
|
||||
app_nvm_mark_dirty();
|
||||
log_info("cond_en=%d (stored)\n", en);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief APP 写 DP6~12 条件配置:触发 VM 持久化 + 复位该组计时(15 章)
|
||||
* @param idx 1~7
|
||||
* @note 引擎关闭时仍保存;作废标志不回安全不主动清(10.3-7 语义)
|
||||
* @return 无
|
||||
*/
|
||||
void app_cond_set_config(uint8_t idx)
|
||||
{
|
||||
if ((idx >= 1) && (idx <= 7)) {
|
||||
s_cond[idx - 1].acc_ms = 0; /* 条件状态机复位该组计时(15 章) */
|
||||
}
|
||||
app_nvm_mark_dirty();
|
||||
log_info("cond_config[%d] stored\n", idx);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/******************************************************************************
|
||||
* @file app_cond.h
|
||||
* @brief CBR3 条件引擎(P2):7 组 7 字节条件,比较成立满时长触发模式
|
||||
* @author cyWu <1917507415@qq.com>
|
||||
* @date 2026.08.29
|
||||
* @version V1.0.0
|
||||
* @history
|
||||
* - V1.0.0, 2026.08.29, cyWu, 首次发布(P0 仅接口与 stub)
|
||||
*
|
||||
* @note P2 实现(BOARD_COND_ENABLE=1)。字节布局见规格书 10.2:
|
||||
* Byte0=使能 Byte1-2=温度阈值(int16 大端 ×10) Byte3=湿度阈值
|
||||
* Byte4=持续分钟(0=立即) Byte5=动作模式 Byte6=Bit7-5 温度比较 /
|
||||
* Bit4-2 湿度比较 / Bit1-0 保留。比较编码:1=< 2=<= 3-== 4->= 5->
|
||||
* 0/6/7=无条件;两侧都无条件=无效组;两侧有效=AND。
|
||||
* ⚠ app_cond_run 由 usr_timer_loop(500ms,任务上下文)调用,
|
||||
* 与仲裁器/定时引擎同侧;不放在 20ms usr_timer_tick(中断上下文)。
|
||||
******************************************************************************/
|
||||
|
||||
#ifndef __APP_COND_H__
|
||||
#define __APP_COND_H__
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/**
|
||||
* @brief 初始化条件引擎
|
||||
* @return 无
|
||||
*/
|
||||
void app_cond_init(void);
|
||||
|
||||
/**
|
||||
* @brief 推进条件引擎,每个业务 tick 调用一次
|
||||
* @param dt 距上次调用的毫秒数
|
||||
* @return 无
|
||||
*/
|
||||
void app_cond_run(uint16_t dt);
|
||||
|
||||
/**
|
||||
* @brief APP 写 DP5 条件总开关
|
||||
* @param en 0=关,1=开
|
||||
* @return 无
|
||||
*/
|
||||
void app_cond_set_en(uint8_t en);
|
||||
|
||||
/**
|
||||
* @brief APP 写 DP6~12 条件配置(7 字节)
|
||||
* @param idx 1~7
|
||||
* @return 无
|
||||
*/
|
||||
void app_cond_set_config(uint8_t idx);
|
||||
|
||||
#endif /* __APP_COND_H__ */
|
||||
@@ -0,0 +1,278 @@
|
||||
/******************************************************************************
|
||||
* @file app_disp.c
|
||||
* @brief CBR3 数码管显示实现
|
||||
* @author cyWu <1917507415@qq.com>
|
||||
* @date 2026.08.29
|
||||
* @version V1.1.0
|
||||
* @history
|
||||
* - V1.0.0, 2026.08.29, cyWu, 首次发布
|
||||
* - V1.0.1, 2026.08.29, cyWu, 改动态扫描:20ms 节拍只算数据写缓冲,
|
||||
* 另起 BOARD_DISP_SCAN_MS 快节拍定时器轮询点亮 1 位,解决闪烁
|
||||
* - V1.1.0, 2026.08.31, cyWu, P1 加告警闪烁:温度/湿度管独立 500ms 亮灭,
|
||||
* BOARD_DISP_ALARM_TEST 宏可强制模拟超限;fault 时 "--" 优先
|
||||
*
|
||||
* @note ⚠ CT1642 内部只有单个 12 位锁存器(4 位选 + 8 段码),无按位显示 RAM、
|
||||
* 不自动扫描,一次只点亮当前锁存的那 1 位。要让 4 位都亮且不闪,必须由 MCU
|
||||
* 持续轮询:本模块用 app_disp_run()(20ms)算好 4 位段码放进 s_disp_buf,
|
||||
* 用 app_disp_scan()(BOARD_DISP_SCAN_MS)每次点亮 1 位、4 位轮询。
|
||||
* 告警闪烁(规格书 8.2):比较成立侧 500ms 亮/500ms 灭,与吹风解耦;
|
||||
* 闪烁触发源由 app_disp_set_temp/humi_alarm 喂入(P2 条件引擎),
|
||||
* 传感器故障(连续 3 次失败)优先显示 "--" 不闪。
|
||||
******************************************************************************/
|
||||
|
||||
#include "app_disp.h"
|
||||
#include "app_th.h"
|
||||
#include "app_power.h"
|
||||
#include "bsp_ct1642.h"
|
||||
#include "board_pin.h"
|
||||
|
||||
#define LOG_TAG_CONST APP
|
||||
#define LOG_TAG "[APP_DISP]"
|
||||
#define LOG_INFO_ENABLE
|
||||
#include "debug.h"
|
||||
|
||||
static uint8_t s_inited;
|
||||
|
||||
/* 4 位段码缓冲,下标顺序 = com 顺序:0 温度十 / 1 温度个 / 2 湿度十 / 3 湿度个 */
|
||||
static uint8_t s_disp_buf[4];
|
||||
static const uint8_t s_com_tbl[4] = {
|
||||
BOARD_DISP_COM_TEMP_TEN, BOARD_DISP_COM_TEMP_ONE,
|
||||
BOARD_DISP_COM_HUMI_TEN, BOARD_DISP_COM_HUMI_ONE
|
||||
};
|
||||
static uint8_t s_scan_idx; /* 动态扫描轮询指针 0..3 */
|
||||
|
||||
/* 告警闪烁(P1,规格书 8.2):比较成立侧 500ms 亮/500ms 灭 */
|
||||
static uint8_t s_temp_alarm; /* 1=温度比较成立(P2 条件引擎喂) */
|
||||
static uint8_t s_humi_alarm; /* 1=湿度比较成立(P2 条件引擎喂) */
|
||||
static uint8_t s_blink_phase; /* 0=亮半周期,1=灭半周期 */
|
||||
static uint16_t s_blink_ms; /* 半周期内累计毫秒 */
|
||||
|
||||
/* 段码索引:bsp_ct1642_seg(0~9)=数字,10='-',11=灭 */
|
||||
#define SEG_MINUS 10
|
||||
#define SEG_OFF 11
|
||||
|
||||
/**
|
||||
* @brief 温度值 → 两位段码(0~99;<0 显示 -9~-1;<-9 钳位 -9;>99 钳位 99)
|
||||
* @param temp_x10 温度 ×10
|
||||
* @param out_t1 十位段码索引
|
||||
* @param out_t0 个位段码索引
|
||||
* @return 无
|
||||
*/
|
||||
static void disp_split_temp(int16_t temp_x10, uint8_t *out_t1, uint8_t *out_t0)
|
||||
{
|
||||
int16_t t = temp_x10 / 10; /* 取整数 ℃(P0 不显示小数) */
|
||||
uint8_t tens;
|
||||
uint8_t ones;
|
||||
|
||||
if (t < 0) {
|
||||
if (t < -9) {
|
||||
t = -9; /* 钳位 */
|
||||
}
|
||||
*out_t1 = SEG_MINUS;
|
||||
*out_t0 = (uint8_t)(-t % 10);
|
||||
return;
|
||||
}
|
||||
if (t > 99) {
|
||||
t = 99;
|
||||
}
|
||||
tens = (uint8_t)(t / 10);
|
||||
ones = (uint8_t)(t % 10);
|
||||
*out_t1 = tens;
|
||||
*out_t0 = ones;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 湿度值 → 两位段码(0~99;>99 钳位 99)
|
||||
* @param humi 湿度 %
|
||||
* @param out_h1 十位段码索引
|
||||
* @param out_h0 个位段码索引
|
||||
* @return 无
|
||||
*/
|
||||
static void disp_split_humi(uint8_t humi, uint8_t *out_h1, uint8_t *out_h0)
|
||||
{
|
||||
uint8_t h = humi;
|
||||
|
||||
if (h > 99) {
|
||||
h = 99;
|
||||
}
|
||||
*out_h1 = (uint8_t)(h / 10);
|
||||
*out_h0 = (uint8_t)(h % 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 初始化显示模块;注册快节拍动态扫描定时器
|
||||
* @return 无
|
||||
*/
|
||||
void app_disp_init(void)
|
||||
{
|
||||
#if !BOARD_DISP_ENABLE
|
||||
log_info("disp disabled\n");
|
||||
return;
|
||||
#endif
|
||||
if (s_inited) {
|
||||
return;
|
||||
}
|
||||
s_inited = 1;
|
||||
s_scan_idx = 0;
|
||||
bsp_ct1642_init(); /* PG0(CLK)/PG1(DATA) 推挽输出 */
|
||||
/* 默认写死 25 / 60,供 P0 台上扫段验证(规格书 P0 验收) */
|
||||
s_disp_buf[0] = bsp_ct1642_seg(2);
|
||||
s_disp_buf[1] = bsp_ct1642_seg(5);
|
||||
s_disp_buf[2] = bsp_ct1642_seg(6);
|
||||
s_disp_buf[3] = bsp_ct1642_seg(0);
|
||||
log_info("disp init, scan %ums\n", (unsigned)BOARD_DISP_SCAN_MS);
|
||||
/* 注意:快节拍动态扫描定时器在 usr_jb_init 里注册(usr_timer_add 声明在那) */
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 业务节拍(20ms):计算 4 位要显示什么,写入段码缓冲(不直接打管)
|
||||
* @note 软关机由 app_disp_scan 处理。本函数只更新 s_disp_buf。
|
||||
* @return 无
|
||||
*/
|
||||
void app_disp_run(uint16_t dt)
|
||||
{
|
||||
#if !BOARD_DISP_ENABLE
|
||||
(void)dt;
|
||||
return;
|
||||
#endif
|
||||
if (!s_inited) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* 闪烁相位推进:满 BOARD_DISP_BLINK_MS 切换亮/灭半周期 */
|
||||
s_blink_ms = (uint16_t)(s_blink_ms + dt);
|
||||
if (s_blink_ms >= BOARD_DISP_BLINK_MS) {
|
||||
s_blink_ms = 0;
|
||||
s_blink_phase ^= 1;
|
||||
}
|
||||
|
||||
#if BOARD_DISP_ALARM_TEST == 1
|
||||
s_temp_alarm = 1; /* P1 调试:强制温度管闪 */
|
||||
#elif BOARD_DISP_ALARM_TEST == 2
|
||||
s_humi_alarm = 1; /* P1 调试:强制湿度管闪 */
|
||||
#endif
|
||||
|
||||
#if BOARD_DISP_SCAN_TEST == 1
|
||||
/* 段扫描:4 位同时点亮同一段,每 600ms 切 A→B→...→G→灭→全亮,定性段码位序 */
|
||||
{
|
||||
static uint32_t tick;
|
||||
static uint8_t step;
|
||||
static const uint8_t seg_seq[] = {
|
||||
0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x00, 0x7F
|
||||
};
|
||||
if (++tick >= 30) { /* 30*20ms=600ms */
|
||||
tick = 0;
|
||||
step = (uint8_t)((step + 1) % (sizeof(seg_seq) / sizeof(seg_seq[0])));
|
||||
}
|
||||
uint8_t s = seg_seq[step];
|
||||
s_disp_buf[0] = s; s_disp_buf[1] = s; s_disp_buf[2] = s; s_disp_buf[3] = s;
|
||||
return;
|
||||
}
|
||||
#elif BOARD_DISP_SCAN_TEST == 2
|
||||
/* 位扫描:4 位分别显示 1/2/3/4,定性 C1~C4 与物理管位对应 */
|
||||
s_disp_buf[0] = bsp_ct1642_seg(1);
|
||||
s_disp_buf[1] = bsp_ct1642_seg(2);
|
||||
s_disp_buf[2] = bsp_ct1642_seg(3);
|
||||
s_disp_buf[3] = bsp_ct1642_seg(4);
|
||||
return;
|
||||
#endif
|
||||
|
||||
if (app_th_is_fault()) {
|
||||
/* 连续失败 3 次:双管 "--"(规格书 8.1;故障优先于闪烁,8.2) */
|
||||
uint8_t m = bsp_ct1642_seg(SEG_MINUS);
|
||||
s_disp_buf[0] = m; s_disp_buf[1] = m; s_disp_buf[2] = m; s_disp_buf[3] = m;
|
||||
} else if (app_th_get_valid()) {
|
||||
/* 有有效采样:显示真实值;比较成立且当前在灭半周期 → 该管熄灭 */
|
||||
uint8_t t1, t0, h1, h0;
|
||||
disp_split_temp(app_th_get_temp_x10(), &t1, &t0);
|
||||
disp_split_humi(app_th_get_humi(), &h1, &h0);
|
||||
if (s_temp_alarm && s_blink_phase) {
|
||||
t1 = SEG_OFF;
|
||||
t0 = SEG_OFF;
|
||||
}
|
||||
if (s_humi_alarm && s_blink_phase) {
|
||||
h1 = SEG_OFF;
|
||||
h0 = SEG_OFF;
|
||||
}
|
||||
s_disp_buf[0] = bsp_ct1642_seg(t1);
|
||||
s_disp_buf[1] = bsp_ct1642_seg(t0);
|
||||
s_disp_buf[2] = bsp_ct1642_seg(h1);
|
||||
s_disp_buf[3] = bsp_ct1642_seg(h0);
|
||||
} else {
|
||||
/* 尚未有有效采样:写死 25 / 60,供 P0 台上扫段验证(规格书 P0 验收) */
|
||||
s_disp_buf[0] = bsp_ct1642_seg(2);
|
||||
s_disp_buf[1] = bsp_ct1642_seg(5);
|
||||
s_disp_buf[2] = bsp_ct1642_seg(6);
|
||||
s_disp_buf[3] = bsp_ct1642_seg(0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 显示动态扫描(BOARD_DISP_SCAN_MS 快节拍定时器回调):点亮 1 位、4 位轮询
|
||||
* @note 必须短、不可阻塞。每次只发 1 帧,靠高频轮询让 4 位视觉同亮不闪。
|
||||
* IO 分离后 CT1642 独占 PG0/PG1,与 AHT20 的 PC4/PC5 无总线竞争,
|
||||
* 扫描回调无需锁、无跳过。
|
||||
* 软关机流程:4 位全灭(com=0x00 → 所有 Cx 高,位选全关)。
|
||||
* @return 无
|
||||
*/
|
||||
void app_disp_scan(void)
|
||||
{
|
||||
#if !BOARD_DISP_ENABLE
|
||||
return;
|
||||
#endif
|
||||
if (!s_inited) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* 软关机流程:数码管锁存灭(规格书 5.1:风扇 0、数码管锁存灭、LED 灭) */
|
||||
if (app_power_poweroff_pending()) {
|
||||
bsp_ct1642_send_frame(0x00, 0x00); /* com=0x00 → 4 位全灭 */
|
||||
return;
|
||||
}
|
||||
|
||||
#if BOARD_DISP_SCAN_TEST == 1
|
||||
/* 段扫描:4 位同显一段。⭐ 值变化才刷(无鬼影),com=0xF 四位全选同显。 */
|
||||
static uint8_t seg = 0;
|
||||
if (seg == s_disp_buf[0]) {
|
||||
return;
|
||||
}
|
||||
seg = s_disp_buf[0];
|
||||
|
||||
bsp_ct1642_send_frame(s_disp_buf[s_scan_idx], 0xF);
|
||||
s_scan_idx = (uint8_t)((s_scan_idx + 1) % 4);
|
||||
#else
|
||||
/* 正常显示(0) / 位选验证(2):4 位分显不同数字,必须分时轮询。
|
||||
* 鬼影(切位杂段)靠 ct1642_delay 8→2 提速缩短杂段窗口来减轻。 */
|
||||
bsp_ct1642_send_frame(s_disp_buf[s_scan_idx], s_com_tbl[s_scan_idx]);
|
||||
s_scan_idx = (uint8_t)((s_scan_idx + 1) % 4);
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 设置温度比较成立标志:成立时温度管 500ms 闪烁(P2 条件引擎喂)
|
||||
* @param en 1=成立(闪),0=不成立(常亮数值)
|
||||
* @return 无
|
||||
*/
|
||||
void app_disp_set_temp_alarm(uint8_t en)
|
||||
{
|
||||
s_temp_alarm = en ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 设置湿度比较成立标志:成立时湿度管 500ms 闪烁(P2 条件引擎喂)
|
||||
* @param en 1=成立(闪),0=不成立(常亮数值)
|
||||
* @return 无
|
||||
*/
|
||||
void app_disp_set_humi_alarm(uint8_t en)
|
||||
{
|
||||
s_humi_alarm = en ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 温度或湿度管是否处于告警闪烁
|
||||
* @return 1=任一侧告警
|
||||
*/
|
||||
uint8_t app_disp_is_alarm(void)
|
||||
{
|
||||
return (s_temp_alarm || s_humi_alarm) ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/******************************************************************************
|
||||
* @file app_disp.h
|
||||
* @brief CBR3 数码管显示:温度/湿度整数、负号、-- 故障、告警闪烁
|
||||
* @author cyWu <1917507415@qq.com>
|
||||
* @date 2026.08.29
|
||||
* @version V1.1.0
|
||||
* @history
|
||||
* - V1.0.0, 2026.08.29, cyWu, 首次发布
|
||||
* - V1.1.0, 2026.08.31, cyWu, P1 加告警闪烁:温度/湿度管独立 500ms 亮灭,
|
||||
* 由 app_disp_set_temp/humi_alarm 驱动(P2 条件引擎喂真值,
|
||||
* P1 可用 BOARD_DISP_ALARM_TEST 宏强制验证)
|
||||
*
|
||||
* @note 两块 2 位 7 段数码管:温度管整数 ℃、湿度管整数 %。DP 恒灭。
|
||||
* 0~99 显示绝对值;<0 显示 -9~-1(G 段负号);<-9 钳位 -9;>99 钳位 99。
|
||||
* 连续失败 3 次双管 "--"(优先于闪烁)。告警闪烁(规格书 8.2)
|
||||
* 与吹风解耦:比较成立侧 500ms 亮/500ms 灭,不成立侧常亮数值。
|
||||
******************************************************************************/
|
||||
|
||||
#ifndef __APP_DISP_H__
|
||||
#define __APP_DISP_H__
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/**
|
||||
* @brief 初始化显示模块
|
||||
* @return 无
|
||||
*/
|
||||
void app_disp_init(void);
|
||||
|
||||
/**
|
||||
* @brief 业务节拍:计算 4 位要显示什么,写入段码缓冲(不直接打管)
|
||||
* @param dt 距上次调用的毫秒数(闪烁相位累加)
|
||||
* @note 软关机由 app_disp_scan 处理;本函数只算数据。
|
||||
* @return 无
|
||||
*/
|
||||
void app_disp_run(uint16_t dt);
|
||||
|
||||
/**
|
||||
* @brief 显示动态扫描(快节拍定时器回调):每次点亮 1 位,4 位轮询刷新
|
||||
* @note 由 app_disp_init 注册的 BOARD_DISP_SCAN_MS 定时器调用;
|
||||
* 必须短、不可阻塞;软关机时 4 位全灭。
|
||||
* CT1642 无内部扫描 RAM,靠本函数持续轮询维持 4 位同亮不闪。
|
||||
* @return 无
|
||||
*/
|
||||
void app_disp_scan(void);
|
||||
|
||||
/**
|
||||
* @brief 设置温度比较成立标志:成立时温度管 500ms 闪烁(P2 条件引擎喂)
|
||||
* @param en 1=成立(闪),0=不成立(常亮数值)
|
||||
* @return 无
|
||||
*/
|
||||
void app_disp_set_temp_alarm(uint8_t en);
|
||||
|
||||
/**
|
||||
* @brief 设置湿度比较成立标志:成立时湿度管 500ms 闪烁(P2 条件引擎喂)
|
||||
* @param en 1=成立(闪),0=不成立(常亮数值)
|
||||
* @return 无
|
||||
*/
|
||||
void app_disp_set_humi_alarm(uint8_t en);
|
||||
|
||||
/**
|
||||
* @brief 温度或湿度管是否处于告警闪烁(供 fault_code=1)
|
||||
* @return 1=任一侧告警,0=无
|
||||
*/
|
||||
uint8_t app_disp_is_alarm(void);
|
||||
|
||||
#endif /* __APP_DISP_H__ */
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/******************************************************************************
|
||||
* @file app_fan.c
|
||||
* @brief CBR3 风扇档位实现
|
||||
* @author cyWu <1917507415@qq.com>
|
||||
* @date 2026.08.29
|
||||
* @version V1.0.0
|
||||
* @history
|
||||
* - V1.0.0, 2026.08.29, cyWu, 首次发布
|
||||
******************************************************************************/
|
||||
|
||||
#include "app_fan.h"
|
||||
#include "bsp_hw.h"
|
||||
#include "board_pin.h"
|
||||
|
||||
#define LOG_TAG_CONST APP
|
||||
#define LOG_TAG "[APP_FAN]"
|
||||
#define LOG_INFO_ENABLE
|
||||
#include "debug.h"
|
||||
|
||||
static uint8_t s_gear;
|
||||
|
||||
/**
|
||||
* @brief 初始化风扇(档位 0,PWM 关)
|
||||
* @return 无
|
||||
*/
|
||||
void app_fan_init(void)
|
||||
{
|
||||
#if !BOARD_FAN_ENABLE
|
||||
log_info("fan disabled\n");
|
||||
return;
|
||||
#endif
|
||||
s_gear = 0;
|
||||
bsp_fan_set_duty(BOARD_FAN_DUTY_GEAR0);
|
||||
log_info("fan init gear=0\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 设置档位并立即生效(P0 无软爬坡)
|
||||
* @param gear 0=关,1=50%,2=75%,3=100%;>3 钳位到 3
|
||||
* @return 无
|
||||
*/
|
||||
void app_fan_set_gear(uint8_t gear)
|
||||
{
|
||||
#if !BOARD_FAN_ENABLE
|
||||
(void)gear;
|
||||
s_gear = 0;
|
||||
return;
|
||||
#endif
|
||||
static const uint16_t duty_tbl[4] = {
|
||||
BOARD_FAN_DUTY_GEAR0, BOARD_FAN_DUTY_GEAR1,
|
||||
BOARD_FAN_DUTY_GEAR2, BOARD_FAN_DUTY_GEAR3
|
||||
};
|
||||
|
||||
if (gear > 3) {
|
||||
gear = 3;
|
||||
}
|
||||
if (gear == s_gear) {
|
||||
return;
|
||||
}
|
||||
s_gear = gear;
|
||||
bsp_fan_set_duty(duty_tbl[gear]);
|
||||
log_info("fan gear=%d duty=%u/10000\n", gear, (unsigned)duty_tbl[gear]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 查询当前真实档位(DP2 必须等于它,通风停段时为 0)
|
||||
* @return 0~3
|
||||
*/
|
||||
uint8_t app_fan_get_gear(void)
|
||||
{
|
||||
return s_gear;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 业务节拍(P0 无动作,保留接口供 P1 爬坡)
|
||||
* @param dt 距上次调用的毫秒数
|
||||
* @return 无
|
||||
*/
|
||||
void app_fan_run(uint16_t dt)
|
||||
{
|
||||
(void)dt;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/******************************************************************************
|
||||
* @file app_fan.h
|
||||
* @brief CBR3 风扇档位:0/1/2/3 → 0%/50%/75%/100%
|
||||
* @author cyWu <1917507415@qq.com>
|
||||
* @date 2026.08.29
|
||||
* @version V1.0.0
|
||||
* @history
|
||||
* - V1.0.0, 2026.08.29, cyWu, 首次发布
|
||||
******************************************************************************/
|
||||
|
||||
#ifndef __APP_FAN_H__
|
||||
#define __APP_FAN_H__
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/**
|
||||
* @brief 初始化风扇(档位 0,PWM 关)
|
||||
* @return 无
|
||||
*/
|
||||
void app_fan_init(void);
|
||||
|
||||
/**
|
||||
* @brief 设置档位并立即生效(P0 无软爬坡)
|
||||
* @param gear 0=关,1=50%,2=75%,3=100%;>3 钳位到 3
|
||||
* @return 无
|
||||
*/
|
||||
void app_fan_set_gear(uint8_t gear);
|
||||
|
||||
/**
|
||||
* @brief 查询当前真实档位(DP2 必须等于它,通风停段时为 0)
|
||||
* @return 0~3
|
||||
*/
|
||||
uint8_t app_fan_get_gear(void);
|
||||
|
||||
/**
|
||||
* @brief 业务节拍(P0 无动作,保留接口供 P1 爬坡)
|
||||
* @param dt 距上次调用的毫秒数
|
||||
* @return 无
|
||||
*/
|
||||
void app_fan_run(uint16_t dt);
|
||||
|
||||
#endif /* __APP_FAN_H__ */
|
||||
@@ -0,0 +1,220 @@
|
||||
/******************************************************************************
|
||||
* @file app_function.c
|
||||
* @brief CBR3 功能总装实现
|
||||
* @author cyWu <1917507415@qq.com>
|
||||
* @date 2026.08.29
|
||||
* @version V1.1.0
|
||||
* @history
|
||||
* - V1.0.0, 2026.08.29, cyWu, 首次发布
|
||||
* - V1.1.0, 2026.08.31, cyWu, 长按关机改为延后到任务上下文执行:按键在
|
||||
* usr_timer 20ms(硬件定时器中断上下文)里处理,直接调
|
||||
* app_power_request_poweroff 会在中断里跑 VM 落盘(栈溢出复位);
|
||||
* 现改为置 off_pending,由 app_function_loop(usr_timer_loop,
|
||||
* BLE 线程)先落盘再关机
|
||||
******************************************************************************/
|
||||
|
||||
#include "app_function.h"
|
||||
#include "app_power.h"
|
||||
#include "app_pair.h"
|
||||
#include "app_mode.h"
|
||||
#include "app_fan.h"
|
||||
#include "app_nvm.h"
|
||||
#include "app_arbiter.h"
|
||||
#include "app_cond.h"
|
||||
#include "app_timer_task.h"
|
||||
#include "app_th.h"
|
||||
#include "app_disp.h"
|
||||
#include "app_led.h"
|
||||
#include "jb_product.h"
|
||||
#include "system/includes.h"
|
||||
#include <string.h>
|
||||
|
||||
#define LOG_TAG_CONST APP
|
||||
#define LOG_TAG "[APP_FUNC]"
|
||||
#define LOG_INFO_ENABLE
|
||||
#include "debug.h"
|
||||
|
||||
/** 功能模块状态 */
|
||||
typedef struct {
|
||||
uint8_t app_power; /**< DP0:1=开机态(本产品无 APP 软关) */
|
||||
uint8_t off_pending; /**< 1=长按已触发关机,等任务上下文落盘后再关机 */
|
||||
} AppFunctionCtx_t;
|
||||
|
||||
static AppFunctionCtx_t s_func;
|
||||
|
||||
/**
|
||||
* @brief 初始化功能模块状态
|
||||
* @return 无
|
||||
*/
|
||||
void app_function_init(void)
|
||||
{
|
||||
s_func.app_power = 0;
|
||||
s_func.off_pending = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 任务上下文周期处理(由 usr_timer_loop 500ms 调用)
|
||||
* @note ⚠ 本函数是唯一允许做 VM 落盘与关机收口的地方:usr_timer_loop 跑在
|
||||
* BLE 线程(sys_timer 同步接口,任务上下文,栈充足)。20ms 的
|
||||
* usr_timer_tick 是硬件定时器中断上下文,禁止做这两件事。
|
||||
* @return 无
|
||||
*/
|
||||
void app_function_loop(void)
|
||||
{
|
||||
app_nvm_flush(); /* 防抖满后真正写 VM(syscfg_write 必须在任务上下文) */
|
||||
|
||||
if (s_func.off_pending) {
|
||||
s_func.off_pending = 0;
|
||||
app_power_request_poweroff(); /* 落盘后再走关机流程 */
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 同步 DP(power 恒 1、fan_gear、mode、fault_code),每个业务 tick 调用
|
||||
* @note DP3/DP4 温湿度在 userHandle(500ms)填;DP0 写忽略,运行中恒上报 1
|
||||
* @return 无
|
||||
*/
|
||||
void app_function_poll(void)
|
||||
{
|
||||
gDevData.power = (s_func.app_power) ? 1 : 0;
|
||||
gDevData.fan_gear = app_fan_get_gear(); /* DP2 必须等于当前真实档位 */
|
||||
gDevData.mode = app_mode_get_active();
|
||||
/* 规格书 8.2/15:传感器故障优先 2;否则温湿度比较成立报 1 */
|
||||
if (app_th_is_fault()) {
|
||||
gDevData.fault_code = 2;
|
||||
} else if (app_disp_is_alarm()) {
|
||||
gDevData.fault_code = 1;
|
||||
} else {
|
||||
gDevData.fault_code = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 停手动 + 模式,风扇关(关机请求 / 软关时用)
|
||||
* @return 无
|
||||
*/
|
||||
void app_function_stop_all(void)
|
||||
{
|
||||
app_mode_stop();
|
||||
app_fan_set_gear(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 软关:风扇停、DP0=0(本产品 DP0 写忽略,软关仅按键 3s 触发)
|
||||
* @return 无
|
||||
*/
|
||||
void app_function_soft_off(void)
|
||||
{
|
||||
s_func.app_power = 0;
|
||||
gDevData.power = 0;
|
||||
app_function_stop_all();
|
||||
log_info("soft off\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 上电默认开启功能(DP0=1),不带开机灯效提示
|
||||
* @return 无
|
||||
*/
|
||||
void app_function_power_on_silent(void)
|
||||
{
|
||||
s_func.app_power = 1;
|
||||
gDevData.power = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 查询功能开关状态(DP0)
|
||||
* @return 1=开,0=关
|
||||
*/
|
||||
uint8_t app_function_is_power_on(void)
|
||||
{
|
||||
return s_func.app_power;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 短按:风扇档位循环 1→2→3→0→1…(规格书 5.3)
|
||||
* @note 立刻退出通风/强力改为手动档:用 app_mode_stop() 只清模式、
|
||||
* 不动档位(若用 set_manual(0) 会恢复模式前快照档,导致通风 1 档
|
||||
* 短按跳到旧档+1 而非 2 档);配对进行中短按仍可切档
|
||||
* @return 无
|
||||
*/
|
||||
void usr_on_key_short(void)
|
||||
{
|
||||
uint8_t g;
|
||||
|
||||
if (app_power_poweroff_pending()) {
|
||||
return;
|
||||
}
|
||||
if (!s_func.app_power) {
|
||||
/* 软关态短按只唤醒,不切档(P0 无 APP 软关,保留分支) */
|
||||
s_func.app_power = 1;
|
||||
gDevData.power = 1;
|
||||
app_led_hint_power_on();
|
||||
return;
|
||||
}
|
||||
|
||||
app_arbiter_manual_intervene(); /* 手动动作:释放条件/定时占用(12 章) */
|
||||
app_mode_stop(); /* 退模式(不动档位,下面立刻切循环下一档) */
|
||||
g = app_fan_get_gear(); /* 当前实际档:手动=手动档;模式中=模式强制档(1/3) */
|
||||
g = (uint8_t)((g + 1) % 4); /* 1→2→3→0→1 */
|
||||
app_fan_set_gear(g);
|
||||
log_info("key short gear=%d\n", g);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 长按 3s:请求软关机(配对中忽略,避免配到一半睡死,规格书 5.3)
|
||||
* @note ⚠ 本回调在 usr_timer 20ms(硬件定时器中断上下文)执行,禁止直接
|
||||
* 调 app_power_request_poweroff(其钩子会做 VM 落盘 → 栈溢出)。
|
||||
* 只置 off_pending,由 app_function_loop(任务上下文)落盘后关机。
|
||||
* @return 无
|
||||
*/
|
||||
void usr_on_key_long(void)
|
||||
{
|
||||
if (app_power_poweroff_pending()) {
|
||||
return;
|
||||
}
|
||||
if (app_pair_is_active()) {
|
||||
log_info("key hold 3s ignored, pairing\n");
|
||||
return;
|
||||
}
|
||||
log_info("key hold 3s, poweroff pending (wait vm flush)\n");
|
||||
s_func.off_pending = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 出厂重置:清业务 VM 与 RAM,保留 BLE 配对(规格书 14 章)
|
||||
* @return 无
|
||||
*/
|
||||
void app_function_factory_reset(void)
|
||||
{
|
||||
uint8_t i;
|
||||
uint8_t *c[7] = {
|
||||
gDevData.cond_config1, gDevData.cond_config2, gDevData.cond_config3,
|
||||
gDevData.cond_config4, gDevData.cond_config5, gDevData.cond_config6,
|
||||
gDevData.cond_config7
|
||||
};
|
||||
uint8_t *tm[7] = {
|
||||
gDevData.time_config1, gDevData.time_config2, gDevData.time_config3,
|
||||
gDevData.time_config4, gDevData.time_config5, gDevData.time_config6,
|
||||
gDevData.time_config7
|
||||
};
|
||||
|
||||
app_nvm_reset();
|
||||
app_arbiter_init();
|
||||
app_cond_init();
|
||||
app_timer_init();
|
||||
app_mode_stop();
|
||||
app_fan_set_gear(0);
|
||||
gDevData.mode = 0;
|
||||
gDevData.fan_gear = 0;
|
||||
gDevData.cond_en = 0;
|
||||
gDevData.time_en = 0;
|
||||
gDevData.fault_en = 0;
|
||||
gDevData.alarm_en = 0;
|
||||
gDevData.fault_code = 0;
|
||||
gDevData.alarm_code = 0;
|
||||
for (i = 0; i < 7; i++) {
|
||||
memset(c[i], 0, 7);
|
||||
memset(tm[i], 0, 8);
|
||||
}
|
||||
log_info("factory reset: business vm cleared, pairing kept\n");
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/******************************************************************************
|
||||
* @file app_function.h
|
||||
* @brief CBR3 功能总装:按键动作(短按切档/长按关机)+ DP 同步
|
||||
* @author cyWu <1917507415@qq.com>
|
||||
* @date 2026.08.29
|
||||
* @version V1.0.0
|
||||
* @history
|
||||
* - V1.0.0, 2026.08.29, cyWu, 首次发布(移植门夹语义重写)
|
||||
*
|
||||
* @note 本模块只做"按键 → 动作"和"状态 → DP",不写业务算法。
|
||||
* 档位/模式/条件的执行在各自 app_* 模块(规格书第 0 章第 2 条)。
|
||||
******************************************************************************/
|
||||
|
||||
#ifndef __APP_FUNCTION_H__
|
||||
#define __APP_FUNCTION_H__
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/**
|
||||
* @brief 初始化功能模块状态
|
||||
* @return 无
|
||||
*/
|
||||
void app_function_init(void);
|
||||
|
||||
/**
|
||||
* @brief 同步 DP(power 恒 1、fan_gear、mode、fault_code),每个业务 tick 调用
|
||||
* @return 无
|
||||
*/
|
||||
void app_function_poll(void);
|
||||
|
||||
/**
|
||||
* @brief 任务上下文周期处理(由 usr_timer_loop 500ms 调用)
|
||||
* @note ⚠ VM 落盘(syscfg_write)与关机收口只允许在这里做:usr_timer_loop
|
||||
* 跑在 BLE 线程(任务上下文,栈充足);usr_timer_tick 是硬件定时器
|
||||
* 中断上下文,做这两件事会栈溢出复位。
|
||||
* @return 无
|
||||
*/
|
||||
void app_function_loop(void);
|
||||
|
||||
/**
|
||||
* @brief 停手动 + 模式,风扇关(关机请求 / 软关时用)
|
||||
* @return 无
|
||||
*/
|
||||
void app_function_stop_all(void);
|
||||
|
||||
/**
|
||||
* @brief 软关:风扇停、DP0=0(本产品 DP0 写忽略,软关仅按键 3s 触发)
|
||||
* @return 无
|
||||
*/
|
||||
void app_function_soft_off(void);
|
||||
|
||||
/**
|
||||
* @brief 上电默认开启功能(DP0=1),不带开机灯效提示
|
||||
* @return 无
|
||||
*/
|
||||
void app_function_power_on_silent(void);
|
||||
|
||||
/**
|
||||
* @brief 查询功能开关状态(DP0)
|
||||
* @return 1=开,0=关
|
||||
*/
|
||||
uint8_t app_function_is_power_on(void);
|
||||
|
||||
/**
|
||||
* @brief 业务层短按回调(key_manager 触发):档位 1→2→3→0→1…
|
||||
* @return 无
|
||||
*/
|
||||
void usr_on_key_short(void);
|
||||
|
||||
/**
|
||||
* @brief 业务层长按 3s 回调(key_manager 触发):请求软关机
|
||||
* @return 无
|
||||
*/
|
||||
void usr_on_key_long(void);
|
||||
|
||||
/**
|
||||
* @brief 出厂重置:清业务 VM 与 RAM,保留 BLE 配对(规格书 14 章)
|
||||
* @return 无
|
||||
*/
|
||||
void app_function_factory_reset(void);
|
||||
|
||||
#endif /* __APP_FUNCTION_H__ */
|
||||
@@ -0,0 +1,143 @@
|
||||
/******************************************************************************
|
||||
* @file app_led.c
|
||||
* @brief CBR3 灯效适配:单灯 PC2,OTA/配对/常亮
|
||||
* @author cyWu <1917507415@qq.com>
|
||||
* @date 2026.08.29
|
||||
* @version V1.0.0
|
||||
* @history
|
||||
* - V1.0.0, 2026.08.29, cyWu, 首次发布(移植门夹,删红绿双通道/充电/低电/空电)
|
||||
******************************************************************************/
|
||||
|
||||
#include "app_led.h"
|
||||
#include "led_manager.h"
|
||||
#include "bsp_hw.h"
|
||||
#include "board_pin.h"
|
||||
#include "app_pair.h"
|
||||
|
||||
static uint8_t s_initialized;
|
||||
static uint8_t s_ota; /* 1=OTA 灯效中,app_led_run 不再叠其它灯 */
|
||||
|
||||
/**
|
||||
* @brief 重新播放一次「一次性/限时」灯效,让它从第 0 帧重新开始
|
||||
* @note 先 Delete 清空槽位再 Add,强制重新计时(见 led.c LED_EventAdd 语义)
|
||||
* @param evt 要重启的事件
|
||||
* @return 无
|
||||
*/
|
||||
static void led_restart(LED_Event_t evt)
|
||||
{
|
||||
LED_EventDelete(evt);
|
||||
LED_EventAdd(evt);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 灯效初始化
|
||||
* @return APP_LED_OK=成功
|
||||
*/
|
||||
AppLed_Ret_t app_led_init(void)
|
||||
{
|
||||
if (s_initialized) {
|
||||
return APP_LED_OK;
|
||||
}
|
||||
user_led_init();
|
||||
s_initialized = 1;
|
||||
return APP_LED_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 查询模块是否已初始化
|
||||
* @return APP_LED_OK=已初始化,APP_LED_ERR_NOT_INIT=未初始化
|
||||
*/
|
||||
AppLed_Ret_t app_led_get_status(void)
|
||||
{
|
||||
return s_initialized ? APP_LED_OK : APP_LED_ERR_NOT_INIT;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 重新挂开机常亮
|
||||
* @return 无
|
||||
*/
|
||||
void app_led_hint_power_on(void)
|
||||
{
|
||||
if (!s_initialized || s_ota) {
|
||||
return;
|
||||
}
|
||||
led_restart(LED_EVENT_STANDBY);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 进入 OTA:清掉其它灯,挂 200ms 闪一直闪
|
||||
* @return 无
|
||||
*/
|
||||
void app_led_request_ota(void)
|
||||
{
|
||||
if (!s_initialized) {
|
||||
return;
|
||||
}
|
||||
s_ota = 1;
|
||||
LED_EventAllDelete();
|
||||
bsp_led_set(0);
|
||||
LED_EventAdd(LED_EVENT_OTA);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 查询是否处于 OTA 灯效
|
||||
* @return 1=OTA 灯效中,0=否
|
||||
*/
|
||||
uint8_t app_led_is_ota(void)
|
||||
{
|
||||
return s_ota;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 按优先级刷新灯
|
||||
* @param dt 距上次调用的毫秒数
|
||||
* @param power_on 1=开机态(挂常亮),0=关机态(灯灭)
|
||||
* @return 无
|
||||
*/
|
||||
void app_led_run(uint16_t dt, uint8_t power_on)
|
||||
{
|
||||
AppPairEvt_t pair_evt;
|
||||
|
||||
if (!s_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* OTA 期间只跑 200ms 闪 */
|
||||
if (s_ota) {
|
||||
LED_EventAdd(LED_EVENT_OTA);
|
||||
user_led_handle();
|
||||
return;
|
||||
}
|
||||
|
||||
/* 配对成功/失败是边沿事件,只在那一拍切灯效 */
|
||||
pair_evt = app_pair_tick_1ms(dt);
|
||||
if (pair_evt == APP_PAIR_EVT_OK) {
|
||||
LED_EventDelete(LED_EVENT_PAIRING);
|
||||
led_restart(LED_EVENT_PAIR_OK);
|
||||
} else if (pair_evt == APP_PAIR_EVT_FAIL) {
|
||||
LED_EventDelete(LED_EVENT_PAIRING);
|
||||
led_restart(LED_EVENT_PAIR_FAIL);
|
||||
}
|
||||
|
||||
if (!power_on) {
|
||||
LED_EventDelete(LED_EVENT_STANDBY);
|
||||
LED_EventDelete(LED_EVENT_PAIRING);
|
||||
} else if (app_pair_is_active()) {
|
||||
LED_EventAdd(LED_EVENT_PAIRING);
|
||||
LED_EventDelete(LED_EVENT_STANDBY);
|
||||
} else if (!LED_IsEventExist(LED_EVENT_PAIR_OK) && !LED_IsEventExist(LED_EVENT_PAIR_FAIL)) {
|
||||
LED_EventAdd(LED_EVENT_STANDBY);
|
||||
}
|
||||
|
||||
user_led_handle();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 清空全部灯事件并灭灯
|
||||
* @return 无
|
||||
*/
|
||||
void app_led_clear(void)
|
||||
{
|
||||
LED_EventAllDelete();
|
||||
bsp_led_set(0);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/******************************************************************************
|
||||
* @file app_led.h
|
||||
* @brief CBR3 灯效:单灯 PC2,OTA/配对/常亮按优先级
|
||||
* @author cyWu <1917507415@qq.com>
|
||||
* @date 2026.08.29
|
||||
* @version V1.0.0
|
||||
* @history
|
||||
* - V1.0.0, 2026.08.29, cyWu, 首次发布(移植门夹,删红绿双通道/充电/低电)
|
||||
******************************************************************************/
|
||||
|
||||
#ifndef __APP_LED_H__
|
||||
#define __APP_LED_H__
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
typedef enum {
|
||||
APP_LED_OK = 0,
|
||||
APP_LED_ERR_NOT_INIT
|
||||
} AppLed_Ret_t;
|
||||
|
||||
/**
|
||||
* @brief 灯效初始化,先全灭
|
||||
* @return APP_LED_OK=成功
|
||||
*/
|
||||
AppLed_Ret_t app_led_init(void);
|
||||
|
||||
/**
|
||||
* @brief 按优先级刷新灯:配对从 app_pair 取,功能开关由调用方传入
|
||||
* @param dt 距上次调用的毫秒数
|
||||
* @param power_on 1=开机态,0=关机态(灯灭)
|
||||
* @return 无
|
||||
*/
|
||||
void app_led_run(uint16_t dt, uint8_t power_on);
|
||||
|
||||
/**
|
||||
* @brief 重新挂开机常亮
|
||||
* @return 无
|
||||
*/
|
||||
void app_led_hint_power_on(void);
|
||||
|
||||
/**
|
||||
* @brief 进入 OTA:清掉其它灯,挂 200ms 闪一直闪
|
||||
* @return 无
|
||||
*/
|
||||
void app_led_request_ota(void);
|
||||
|
||||
/**
|
||||
* @brief 查询是否处于 OTA 灯效
|
||||
* @return 1=OTA 灯效中,0=否
|
||||
*/
|
||||
uint8_t app_led_is_ota(void);
|
||||
|
||||
/**
|
||||
* @brief 清空全部灯事件并灭灯(关机前调用)
|
||||
* @return 无
|
||||
*/
|
||||
void app_led_clear(void);
|
||||
|
||||
/**
|
||||
* @brief 查询是否已初始化
|
||||
* @return APP_LED_OK=已初始化,APP_LED_ERR_NOT_INIT=未初始化
|
||||
*/
|
||||
AppLed_Ret_t app_led_get_status(void);
|
||||
|
||||
#endif /* __APP_LED_H__ */
|
||||
@@ -0,0 +1,177 @@
|
||||
/******************************************************************************
|
||||
* @file app_mode.c
|
||||
* @brief CBR3 工作模式实现(P1):通风 25/5 循环、强力 15min 一轮
|
||||
* @author cyWu <1917507415@qq.com>
|
||||
* @date 2026.08.29
|
||||
* @version V1.1.0
|
||||
* @history
|
||||
* - V1.0.0, 2026.08.29, cyWu, 首次发布(P0 stub)
|
||||
* - V1.1.0, 2026.08.31, cyWu, P1 实现状态机(规格书第 9 章):
|
||||
* 通风 25/5、强力 15min 后停、手动档快照、退出语义、开机从周期头
|
||||
*
|
||||
* @note 模式激活时强制覆盖风扇档位(通风=1 档、强力=3 档、通风停段=0),
|
||||
* DP2 由 app_fan_get_gear 上报真实档位(app_function_poll 同步)。
|
||||
* 退出方式(规格书 9 章):短按切档(app_mode_stop)、APP 写档位、
|
||||
* APP 写 mode=0(恢复进入模式前的手动档快照)、强力自然到点(风扇 0)。
|
||||
* 开机恢复不存相位:从 25min 开段 / 15min 从头计。
|
||||
******************************************************************************/
|
||||
|
||||
#include "app_mode.h"
|
||||
#include "app_fan.h"
|
||||
#include "app_nvm.h"
|
||||
#include "board_pin.h"
|
||||
|
||||
#define LOG_TAG_CONST APP
|
||||
#define LOG_TAG "[APP_MODE]"
|
||||
#define LOG_INFO_ENABLE
|
||||
#include "debug.h"
|
||||
|
||||
/** 通风循环段 */
|
||||
typedef enum {
|
||||
MODE_VENT_ON = 0, /**< 开段 25min,强制 1 档 */
|
||||
MODE_VENT_OFF /**< 停段 5min,风扇 0 */
|
||||
} ModeVentPhase_t;
|
||||
|
||||
/** 模式状态机上下文 */
|
||||
typedef struct {
|
||||
uint8_t mode; /**< 0=关闭 1=通风 2=强力 */
|
||||
uint8_t phase; /**< 通风段;其它模式恒 0 */
|
||||
uint32_t run_ms; /**< 当前段已运行毫秒 */
|
||||
uint8_t saved_gear;/**< 进入模式前的手动档快照(APP 写 mode=0 时恢复) */
|
||||
} AppModeCtx_t;
|
||||
|
||||
static AppModeCtx_t s_mode;
|
||||
|
||||
/**
|
||||
* @brief 初始化模式状态机
|
||||
* @return 无
|
||||
*/
|
||||
void app_mode_init(void)
|
||||
{
|
||||
#if !BOARD_MODE_ENABLE
|
||||
log_info("mode disabled\n");
|
||||
return;
|
||||
#endif
|
||||
s_mode.mode = 0;
|
||||
s_mode.phase = 0;
|
||||
s_mode.run_ms = 0;
|
||||
s_mode.saved_gear = 0;
|
||||
log_info("mode init\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 推进模式状态机(每个业务 tick 调用)
|
||||
* @param dt 距上次调用的毫秒数
|
||||
* @return 无
|
||||
*/
|
||||
void app_mode_run(uint16_t dt)
|
||||
{
|
||||
#if !BOARD_MODE_ENABLE
|
||||
(void)dt;
|
||||
return;
|
||||
#endif
|
||||
switch (s_mode.mode) {
|
||||
case 1: /* 通风:开 25min 强制 1 档,停 5min 风扇 0,循环 */
|
||||
if (s_mode.phase == MODE_VENT_ON) {
|
||||
app_fan_set_gear(1);
|
||||
s_mode.run_ms += dt;
|
||||
if (s_mode.run_ms >= BOARD_MODE_VENT_ON_MS) {
|
||||
s_mode.phase = MODE_VENT_OFF;
|
||||
s_mode.run_ms = 0;
|
||||
app_fan_set_gear(0);
|
||||
app_nvm_mark_dirty();
|
||||
log_info("vent on 25min over, off 5min\n");
|
||||
}
|
||||
} else {
|
||||
s_mode.run_ms += dt;
|
||||
if (s_mode.run_ms >= BOARD_MODE_VENT_OFF_MS) {
|
||||
s_mode.phase = MODE_VENT_ON;
|
||||
s_mode.run_ms = 0;
|
||||
app_fan_set_gear(1);
|
||||
app_nvm_mark_dirty();
|
||||
log_info("vent off 5min over, on 25min\n");
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 2: /* 强力:强制 3 档,15min 到点风扇 0、mode 回 0 */
|
||||
app_fan_set_gear(3);
|
||||
s_mode.run_ms += dt;
|
||||
if (s_mode.run_ms >= BOARD_MODE_POWER_ON_MS) {
|
||||
log_info("power 15min over, mode=0 fan=0\n");
|
||||
s_mode.mode = 0;
|
||||
s_mode.phase = 0;
|
||||
s_mode.run_ms = 0;
|
||||
app_fan_set_gear(0);
|
||||
app_nvm_mark_dirty();
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief APP 写 DP1 / 手动抢权 / 开机恢复:切到指定模式或退出
|
||||
* @param mode 0=关闭模式(恢复手动档快照),1=通风,2=强力
|
||||
* @note 模式 1/2 进入时快照当前实际档(即进入前的手动档),并立即强制档位;
|
||||
* mode=0 时若原在模式中则恢复快照档。短按退出走 app_mode_stop()
|
||||
* (不恢复快照,档位由短按逻辑立刻切到循环下一档)。
|
||||
* @return 无
|
||||
*/
|
||||
void app_mode_set_manual(uint8_t mode)
|
||||
{
|
||||
#if !BOARD_MODE_ENABLE
|
||||
(void)mode;
|
||||
return;
|
||||
#endif
|
||||
if (mode > 2) {
|
||||
mode = 0;
|
||||
}
|
||||
if (mode != 0) {
|
||||
s_mode.saved_gear = app_fan_get_gear(); /* 快照进入前的手动档 */
|
||||
s_mode.mode = mode;
|
||||
s_mode.phase = MODE_VENT_ON;
|
||||
s_mode.run_ms = 0; /* 不存相位:开机/手动切入都从周期头计 */
|
||||
app_fan_set_gear((mode == 1) ? 1 : 3);
|
||||
log_info("mode set %d, gear=%d\n", mode, app_fan_get_gear());
|
||||
} else {
|
||||
if (s_mode.mode != 0) {
|
||||
/* 只在实际退出模式时恢复快照档,避免 load(0) 把恢复档清零 */
|
||||
app_fan_set_gear(s_mode.saved_gear);
|
||||
log_info("mode exit, restore gear=%d\n", s_mode.saved_gear);
|
||||
}
|
||||
s_mode.mode = 0;
|
||||
s_mode.phase = 0;
|
||||
s_mode.run_ms = 0;
|
||||
}
|
||||
app_nvm_mark_dirty();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 查询当前逻辑模式
|
||||
* @return 0/1/2
|
||||
*/
|
||||
uint8_t app_mode_get_active(void)
|
||||
{
|
||||
return s_mode.mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 停止当前模式(短按退模式用:只清模式状态,不动风扇档位,
|
||||
* 由调用方立刻切到循环下一档;关机流程则另行停风扇)
|
||||
* @return 无
|
||||
*/
|
||||
void app_mode_stop(void)
|
||||
{
|
||||
#if !BOARD_MODE_ENABLE
|
||||
return;
|
||||
#endif
|
||||
if (s_mode.mode != 0) {
|
||||
app_nvm_mark_dirty();
|
||||
}
|
||||
s_mode.mode = 0;
|
||||
s_mode.phase = 0;
|
||||
s_mode.run_ms = 0;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/******************************************************************************
|
||||
* @file app_mode.h
|
||||
* @brief CBR3 工作模式:通风(25/5 循环)/ 强力(15min 一轮)
|
||||
* @author cyWu <1917507415@qq.com>
|
||||
* @date 2026.08.29
|
||||
* @version V1.0.0
|
||||
* @history
|
||||
* - V1.0.0, 2026.08.29, cyWu, 首次发布(P0 仅接口与 stub,P1 实现状态机)
|
||||
*
|
||||
* @note P1 语义(规格书第 9 章):mode=1 强制 1 档开 25min 停 5min 循环;
|
||||
* mode=2 强制 3 档跑 15min 后风扇 0、mode 回 0。短按切档走
|
||||
* app_mode_stop(档位由短按逻辑续切);APP 写档 / 写 mode 走
|
||||
* app_mode_set_manual 手动抢权(mode=0 恢复进入前的手动档快照)。
|
||||
* 开机恢复不存相位,从头计。BOARD_MODE_ENABLE=0 时全部空操作。
|
||||
******************************************************************************/
|
||||
|
||||
#ifndef __APP_MODE_H__
|
||||
#define __APP_MODE_H__
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/**
|
||||
* @brief 初始化模式状态机
|
||||
* @return 无
|
||||
*/
|
||||
void app_mode_init(void);
|
||||
|
||||
/**
|
||||
* @brief 推进模式状态机,每个业务 tick 调用一次
|
||||
* @param dt 距上次调用的毫秒数
|
||||
* @return 无
|
||||
*/
|
||||
void app_mode_run(uint16_t dt);
|
||||
|
||||
/**
|
||||
* @brief APP 写 DP1 / 手动抢权:切到指定模式或退出(P0 关闭时忽略)
|
||||
* @param mode 0=关闭模式,1=通风,2=强力
|
||||
* @return 无
|
||||
*/
|
||||
void app_mode_set_manual(uint8_t mode);
|
||||
|
||||
/**
|
||||
* @brief 查询当前逻辑模式
|
||||
* @return 0/1/2
|
||||
*/
|
||||
uint8_t app_mode_get_active(void);
|
||||
|
||||
/**
|
||||
* @brief 停止当前模式(风扇由 app_fan 接管,这里只清模式状态)
|
||||
* @return 无
|
||||
*/
|
||||
void app_mode_stop(void);
|
||||
|
||||
#endif /* __APP_MODE_H__ */
|
||||
@@ -0,0 +1,423 @@
|
||||
/******************************************************************************
|
||||
* @file app_nvm.c
|
||||
* @brief CBR3 VM 持久化实现(P1):mode/fan_gear/条件/定时/使能
|
||||
* @author cyWu <1917507415@qq.com>
|
||||
* @date 2026.08.29
|
||||
* @version V1.1.0
|
||||
* @history
|
||||
* - V1.0.0, 2026.08.29, cyWu, 首次发布(P0 stub)
|
||||
* - V1.1.0, 2026.08.31, cyWu, P1 实现:3 个 VM item + 防抖 + 立即写
|
||||
*
|
||||
* @note VM item 分配在 syscfg 用户自定义区 [1~49](syscfg_id.h 已占用
|
||||
* CFG_USER_PAIR_INFO=2 / CFG_USER_TIM_FLAG=3 / VM_ALARM*=4~24 /
|
||||
* CFG_USER_DEVICE_STA=25 / CFG_USER_PLAN_INFO1=26 /
|
||||
* CFG_USER_BATTERY_LEVEL=27),本产品从 28 开始:
|
||||
* CFG_USER_CBR3_STATE=28 状态(magic+ver+mode+gear+en 等,10B)
|
||||
* CFG_USER_CBR3_COND =29 条件配置 cond_config[7][7](49B)
|
||||
* CFG_USER_CBR3_TIME =30 定时配置 time_config[7][8](56B)
|
||||
* 拆 3 个 item:改档/改模式只擦写 10B 小 item,不重写 105B 配置区,
|
||||
* 减少 flash 磨损;首次上电(magic 不符)保持 gDevData 全 0 默认。
|
||||
*
|
||||
* 写入时机(规格书第 13 章):APP 改配置后(mark_dirty)、档位/模式
|
||||
* 稳定变化(500ms 防抖)、软关机前(save 立即写)。
|
||||
*
|
||||
* ⚠⚠ 上下文硬约束(2026-08-31 复位事故定论,勿再踩):
|
||||
* usr_timer_add() 是**异步接口**,回调在**硬件定时器中断上下文**执行
|
||||
* (栈极小);sys_timer_add() 是同步接口,回调在 add 它的线程执行。
|
||||
* 因此:
|
||||
* - app_nvm_tick() 跑在 20ms 中断上下文 → **只能做变化检测与计时,
|
||||
* 绝对不能调 syscfg_write**(VM 擦写 flash 栈用量大且中断里做
|
||||
* flash 操作 → 栈溢出软复位;实测 APP 下发数据后 500ms 必崩)。
|
||||
* - 真正写入由 app_nvm_flush() 在 usr_timer_loop()(BLE 线程、
|
||||
* 任务上下文、栈充足)执行;usr_poweroff_request_hook 里的
|
||||
* app_nvm_save() 也必须保证在任务上下文被调用(见 app_function_loop)。
|
||||
* 另:nvm_write_item 的比较缓冲改 static,再省 128B 栈。
|
||||
******************************************************************************/
|
||||
|
||||
#include "app_nvm.h"
|
||||
#include "app_mode.h"
|
||||
#include "app_fan.h"
|
||||
#include "jb_product.h"
|
||||
#include "board_pin.h"
|
||||
#include "system/includes.h"
|
||||
#include "syscfg_id.h"
|
||||
|
||||
#define LOG_TAG_CONST APP
|
||||
#define LOG_TAG "[APP_NVM]"
|
||||
#define LOG_ERROR_ENABLE
|
||||
#define LOG_INFO_ENABLE
|
||||
#include "debug.h"
|
||||
|
||||
/* ── VM item id(用户区 1~49 内申请)── */
|
||||
#define CFG_USER_CBR3_STATE 28
|
||||
#define CFG_USER_CBR3_COND 29
|
||||
#define CFG_USER_CBR3_TIME 30
|
||||
|
||||
#define CBR3_VM_MAGIC 0xCB03
|
||||
#define CBR3_VM_VER 1
|
||||
#define CBR3_VM_DEBOUNCE_MS 500 /* 档位/模式稳定变化防抖(规格书 13 章) */
|
||||
|
||||
/** 状态 item(10 字节) */
|
||||
typedef struct {
|
||||
uint16_t magic; /**< CBR3_VM_MAGIC */
|
||||
uint8_t ver; /**< CBR3_VM_VER */
|
||||
uint8_t mode; /**< 逻辑模式 0/1/2 */
|
||||
uint8_t fan_gear; /**< 真实档位 0~3(模式未激活时即手动档) */
|
||||
uint8_t cond_en; /**< DP5 条件总开关 */
|
||||
uint8_t time_en; /**< DP13 定时总开关 */
|
||||
uint8_t fault_en; /**< DP21 */
|
||||
uint8_t alarm_en; /**< DP23 */
|
||||
} __packed Cbr3StateVm_t;
|
||||
|
||||
/** 条件配置 item(49 字节) */
|
||||
typedef struct {
|
||||
uint8_t cond_config[7][7];
|
||||
} __packed Cbr3CondVm_t;
|
||||
|
||||
/** 定时配置 item(56 字节) */
|
||||
typedef struct {
|
||||
uint8_t time_config[7][8];
|
||||
} __packed Cbr3TimeVm_t;
|
||||
|
||||
#if BOARD_NVM_ENABLE
|
||||
static uint8_t s_dirty; /**< 1=防抖计时中 */
|
||||
static uint8_t s_need_save; /**< 1=防抖已满,待任务上下文落盘 */
|
||||
static uint16_t s_deb_ms; /**< 防抖计时 */
|
||||
|
||||
/* 上次已同步到 VM 的状态快照:变化检测基准(覆盖按键/APP/模式引擎所有入口) */
|
||||
static uint8_t s_last_gear;
|
||||
static uint8_t s_last_mode;
|
||||
static uint8_t s_last_cond_en;
|
||||
static uint8_t s_last_time_en;
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief 从 gDevData 收集条件配置到 VM 结构
|
||||
* @note gDevData.cond_config1~7 是 7 个独立数组字段,用指针表统一搬运
|
||||
* @param out 输出结构
|
||||
* @return 无
|
||||
*/
|
||||
static void nvm_collect_cond(Cbr3CondVm_t *out)
|
||||
{
|
||||
const uint8_t *src[7] = {
|
||||
gDevData.cond_config1, gDevData.cond_config2, gDevData.cond_config3,
|
||||
gDevData.cond_config4, gDevData.cond_config5, gDevData.cond_config6,
|
||||
gDevData.cond_config7
|
||||
};
|
||||
uint8_t i;
|
||||
|
||||
for (i = 0; i < 7; i++) {
|
||||
memcpy(out->cond_config[i], src[i], 7);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 从 gDevData 收集定时配置到 VM 结构
|
||||
* @param out 输出结构
|
||||
* @return 无
|
||||
*/
|
||||
static void nvm_collect_time(Cbr3TimeVm_t *out)
|
||||
{
|
||||
const uint8_t *src[7] = {
|
||||
gDevData.time_config1, gDevData.time_config2, gDevData.time_config3,
|
||||
gDevData.time_config4, gDevData.time_config5, gDevData.time_config6,
|
||||
gDevData.time_config7
|
||||
};
|
||||
uint8_t i;
|
||||
|
||||
for (i = 0; i < 7; i++) {
|
||||
memcpy(out->time_config[i], src[i], 8);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 写单个 item:先读回比较,内容相同跳过,避免无谓擦写 flash
|
||||
* @param item_id VM item id
|
||||
* @param buf 新数据
|
||||
* @param len 数据长度
|
||||
* @return 无
|
||||
*/
|
||||
static void nvm_write_item(u16 item_id, void *buf, u16 len)
|
||||
{
|
||||
/* static:比较缓冲不放栈上(省 128B 栈);本函数只在任务上下文调用,无重入 */
|
||||
static uint8_t old[128];
|
||||
|
||||
if (len > sizeof(old)) {
|
||||
log_error("nvm item %d len %d too big\n", item_id, len);
|
||||
return;
|
||||
}
|
||||
if (syscfg_read(item_id, old, len) == len) {
|
||||
if (memcmp(old, buf, len) == 0) {
|
||||
return; /* 没变化,不擦写 */
|
||||
}
|
||||
}
|
||||
if (syscfg_write(item_id, buf, len) > 0) {
|
||||
log_info("nvm item %d saved %dB\n", item_id, len);
|
||||
} else {
|
||||
log_error("nvm item %d write fail\n", item_id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 写状态 item(mode/gear/使能)
|
||||
* @return 无
|
||||
*/
|
||||
static void nvm_write_state(void)
|
||||
{
|
||||
Cbr3StateVm_t st;
|
||||
|
||||
st.magic = CBR3_VM_MAGIC;
|
||||
st.ver = CBR3_VM_VER;
|
||||
st.mode = app_mode_get_active();
|
||||
st.fan_gear = app_fan_get_gear();
|
||||
st.cond_en = gDevData.cond_en ? 1 : 0;
|
||||
st.time_en = gDevData.time_en ? 1 : 0;
|
||||
st.fault_en = gDevData.fault_en ? 1 : 0;
|
||||
st.alarm_en = gDevData.alarm_en ? 1 : 0;
|
||||
nvm_write_item(CFG_USER_CBR3_STATE, &st, sizeof(st));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 写条件配置 item
|
||||
* @return 无
|
||||
*/
|
||||
static void nvm_write_cond(void)
|
||||
{
|
||||
Cbr3CondVm_t cf;
|
||||
|
||||
nvm_collect_cond(&cf);
|
||||
nvm_write_item(CFG_USER_CBR3_COND, &cf, sizeof(cf));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 写定时配置 item
|
||||
* @return 无
|
||||
*/
|
||||
static void nvm_write_time(void)
|
||||
{
|
||||
Cbr3TimeVm_t tf;
|
||||
|
||||
nvm_collect_time(&tf);
|
||||
nvm_write_item(CFG_USER_CBR3_TIME, &tf, sizeof(tf));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 初始化:校验 VM 有效性并打日志(加载在 app_nvm_load)
|
||||
* @return 无
|
||||
*/
|
||||
void app_nvm_init(void)
|
||||
{
|
||||
#if !BOARD_NVM_ENABLE
|
||||
log_info("nvm disabled\n");
|
||||
return;
|
||||
#else
|
||||
Cbr3StateVm_t st;
|
||||
int ret = syscfg_read(CFG_USER_CBR3_STATE, &st, sizeof(st));
|
||||
|
||||
if ((ret == (int)sizeof(st)) && (st.magic == CBR3_VM_MAGIC) && (st.ver == CBR3_VM_VER)) {
|
||||
log_info("nvm init, vm valid (mode=%d gear=%d cond_en=%d time_en=%d)\n",
|
||||
st.mode, st.fan_gear, st.cond_en, st.time_en);
|
||||
} else {
|
||||
log_info("nvm init, vm invalid (ret=%d), use defaults\n", ret);
|
||||
}
|
||||
s_dirty = 0;
|
||||
s_deb_ms = 0;
|
||||
s_need_save = 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 同步"已落盘状态"快照为当前值(save/load 后调用)
|
||||
* @return 无
|
||||
*/
|
||||
static void nvm_sync_snapshot(void)
|
||||
{
|
||||
#if BOARD_NVM_ENABLE
|
||||
s_last_gear = app_fan_get_gear();
|
||||
s_last_mode = app_mode_get_active();
|
||||
s_last_cond_en = gDevData.cond_en ? 1 : 0;
|
||||
s_last_time_en = gDevData.time_en ? 1 : 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 立即保存全部业务字段(软关机前调用)
|
||||
* @return 无
|
||||
*/
|
||||
void app_nvm_save(void)
|
||||
{
|
||||
#if !BOARD_NVM_ENABLE
|
||||
return;
|
||||
#else
|
||||
nvm_write_state();
|
||||
nvm_write_cond();
|
||||
nvm_write_time();
|
||||
s_dirty = 0;
|
||||
s_deb_ms = 0;
|
||||
s_need_save = 0;
|
||||
nvm_sync_snapshot();
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 从 VM 加载并应用(开机 3s 后由 usr_jb_init 调用)
|
||||
* @note 顺序:先恢复档位再恢复模式(app_mode_set_manual 内部会快照当前档、
|
||||
* 并让通风/强力从周期头开跑,规格书第 9 章"开机恢复不存相位")。
|
||||
* VM 无效时保持 gDevData 全 0(首次上电默认 mode=0 gear=0)。
|
||||
* @return 无
|
||||
*/
|
||||
void app_nvm_load(void)
|
||||
{
|
||||
#if !BOARD_NVM_ENABLE
|
||||
return;
|
||||
#else
|
||||
Cbr3StateVm_t st;
|
||||
Cbr3CondVm_t cf;
|
||||
Cbr3TimeVm_t tf;
|
||||
uint8_t i;
|
||||
|
||||
if ((syscfg_read(CFG_USER_CBR3_STATE, &st, sizeof(st)) != (int)sizeof(st))
|
||||
|| (st.magic != CBR3_VM_MAGIC) || (st.ver != CBR3_VM_VER)) {
|
||||
log_info("nvm load: no valid vm, defaults (mode=0 gear=0)\n");
|
||||
return;
|
||||
}
|
||||
|
||||
/* 条件/定时配置回填 gDevData(只存不跑,P2 引擎再消费) */
|
||||
if (syscfg_read(CFG_USER_CBR3_COND, &cf, sizeof(cf)) == (int)sizeof(cf)) {
|
||||
uint8_t *dst[7] = {
|
||||
gDevData.cond_config1, gDevData.cond_config2, gDevData.cond_config3,
|
||||
gDevData.cond_config4, gDevData.cond_config5, gDevData.cond_config6,
|
||||
gDevData.cond_config7
|
||||
};
|
||||
for (i = 0; i < 7; i++) {
|
||||
memcpy(dst[i], cf.cond_config[i], 7);
|
||||
}
|
||||
}
|
||||
if (syscfg_read(CFG_USER_CBR3_TIME, &tf, sizeof(tf)) == (int)sizeof(tf)) {
|
||||
uint8_t *dst[7] = {
|
||||
gDevData.time_config1, gDevData.time_config2, gDevData.time_config3,
|
||||
gDevData.time_config4, gDevData.time_config5, gDevData.time_config6,
|
||||
gDevData.time_config7
|
||||
};
|
||||
for (i = 0; i < 7; i++) {
|
||||
memcpy(dst[i], tf.time_config[i], 8);
|
||||
}
|
||||
}
|
||||
|
||||
gDevData.cond_en = st.cond_en ? 1 : 0;
|
||||
gDevData.time_en = st.time_en ? 1 : 0;
|
||||
gDevData.fault_en = st.fault_en ? 1 : 0;
|
||||
gDevData.alarm_en = st.alarm_en ? 1 : 0;
|
||||
|
||||
/* 先档位后模式;mode 激活时引擎会从周期头强制档位 */
|
||||
if (st.fan_gear > 3) {
|
||||
st.fan_gear = 0;
|
||||
}
|
||||
app_fan_set_gear(st.fan_gear);
|
||||
app_mode_set_manual(st.mode);
|
||||
nvm_sync_snapshot(); /* 恢复值即为已落盘值,同步基准 */
|
||||
log_info("nvm load: mode=%d gear=%d cond_en=%d time_en=%d\n",
|
||||
st.mode, st.fan_gear, st.cond_en, st.time_en);
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 业务节拍:做档位/模式/总开关变化检测 + 防抖计时,**不执行写入**
|
||||
* @param dt 距上次调用的毫秒数
|
||||
* @note ⚠ 本函数跑在 usr_timer_add 的 20ms 回调里 = **硬件定时器中断上下文**
|
||||
* (异步接口,栈极小)。这里只允许做轻量比较与计时;真正的
|
||||
* syscfg_write 由 app_nvm_flush() 在 usr_timer_loop(BLE 线程、
|
||||
* 任务上下文)执行。曾在此直接调 app_nvm_save 导致栈溢出软复位。
|
||||
* @return 无
|
||||
*/
|
||||
void app_nvm_tick(uint16_t dt)
|
||||
{
|
||||
#if !BOARD_NVM_ENABLE
|
||||
(void)dt;
|
||||
return;
|
||||
#else
|
||||
uint8_t gear = app_fan_get_gear();
|
||||
uint8_t mode = app_mode_get_active();
|
||||
uint8_t cond_en = gDevData.cond_en ? 1 : 0;
|
||||
uint8_t time_en = gDevData.time_en ? 1 : 0;
|
||||
|
||||
if ((gear != s_last_gear) || (mode != s_last_mode)
|
||||
|| (cond_en != s_last_cond_en) || (time_en != s_last_time_en)) {
|
||||
s_last_gear = gear;
|
||||
s_last_mode = mode;
|
||||
s_last_cond_en = cond_en;
|
||||
s_last_time_en = time_en;
|
||||
s_dirty = 1;
|
||||
s_deb_ms = 0; /* 变化重新计满防抖窗口 */
|
||||
}
|
||||
|
||||
if (!s_dirty) {
|
||||
return;
|
||||
}
|
||||
s_deb_ms = (uint16_t)(s_deb_ms + dt);
|
||||
if (s_deb_ms >= CBR3_VM_DEBOUNCE_MS) {
|
||||
s_deb_ms = 0;
|
||||
s_dirty = 0;
|
||||
s_need_save = 1; /* 只置标志:等任务上下文 app_nvm_flush 真正落盘 */
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 任务上下文落盘:防抖满后真正写 VM(由 usr_timer_loop 500ms 调用)
|
||||
* @note 必须且只能在**非中断上下文**调用(sys_timer 回调 / BLE 线程 /
|
||||
* app_core 任务)。VM 擦写 flash 栈用量大,禁止放进 20ms 业务节拍。
|
||||
* @return 无
|
||||
*/
|
||||
void app_nvm_flush(void)
|
||||
{
|
||||
#if !BOARD_NVM_ENABLE
|
||||
return;
|
||||
#else
|
||||
if (!s_need_save) {
|
||||
return;
|
||||
}
|
||||
s_need_save = 0;
|
||||
app_nvm_save();
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 标记业务字段有变化,防抖计时归零(重新计满 CBR3_VM_DEBOUNCE_MS 才写)
|
||||
* @return 无
|
||||
*/
|
||||
void app_nvm_mark_dirty(void)
|
||||
{
|
||||
#if BOARD_NVM_ENABLE
|
||||
s_dirty = 1;
|
||||
s_deb_ms = 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 出厂重置:清全部业务 VM(保留 BLE 配对,规格书第 14 章)
|
||||
* @note 3 个 item 写全 0 → magic 失效,下次开机走首次上电默认值;
|
||||
* BLE 配对三元组在 CFG_USER_PAIR_INFO(item 2),不受影响
|
||||
* @return 无
|
||||
*/
|
||||
void app_nvm_reset(void)
|
||||
{
|
||||
#if !BOARD_NVM_ENABLE
|
||||
return;
|
||||
#else
|
||||
Cbr3StateVm_t st;
|
||||
Cbr3CondVm_t cf;
|
||||
Cbr3TimeVm_t tf;
|
||||
|
||||
memset(&st, 0, sizeof(st));
|
||||
memset(&cf, 0, sizeof(cf));
|
||||
memset(&tf, 0, sizeof(tf));
|
||||
syscfg_write(CFG_USER_CBR3_STATE, &st, sizeof(st));
|
||||
syscfg_write(CFG_USER_CBR3_COND, &cf, sizeof(cf));
|
||||
syscfg_write(CFG_USER_CBR3_TIME, &tf, sizeof(tf));
|
||||
s_dirty = 0;
|
||||
s_deb_ms = 0;
|
||||
s_need_save = 0;
|
||||
log_info("nvm factory reset, business vm cleared (pairing kept)\n");
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/******************************************************************************
|
||||
* @file app_nvm.h
|
||||
* @brief CBR3 VM 持久化(P1 起):mode/fan_gear/条件/定时/使能
|
||||
* @author cyWu <1917507415@qq.com>
|
||||
* @date 2026.08.29
|
||||
* @version V1.1.0
|
||||
* @history
|
||||
* - V1.0.0, 2026.08.29, cyWu, 首次发布(P0 仅接口与 stub)
|
||||
* - V1.1.0, 2026.08.31, cyWu, P1 实现:3 个 VM item(状态/条件/定时)+ 500ms
|
||||
* 防抖写入 + 立即写接口
|
||||
*
|
||||
* @note 存:mode、fan_gear、cond_en、cond_config1~7、time_en、time_config1~7、
|
||||
* fault_en、alarm_en。不存:DP0、条件作废标志、模式已运行秒数、原始采样。
|
||||
* 写 VM 时机:APP 改配置后、单次定时自毁、软关机前、档位/模式稳定变化
|
||||
* (500ms 防抖)。
|
||||
******************************************************************************/
|
||||
|
||||
#ifndef __APP_NVM_H__
|
||||
#define __APP_NVM_H__
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/**
|
||||
* @brief 初始化(校验 VM 有效性,仅日志;数据加载在 app_nvm_load)
|
||||
* @return 无
|
||||
*/
|
||||
void app_nvm_init(void);
|
||||
|
||||
/**
|
||||
* @brief 立即保存全部业务字段到 VM(软关机前等关键时机调用)
|
||||
* @note syscfg 写入不能在裸中断里调用;本函数由业务时基回调触发,
|
||||
* 与 SDK norflash 缓存同步(sys_timeout 回调)同级,可安全执行
|
||||
* @return 无
|
||||
*/
|
||||
void app_nvm_save(void);
|
||||
|
||||
/**
|
||||
* @brief 从 VM 加载业务字段并应用(开机 3s 后调用):
|
||||
* mode/fan_gear 恢复、条件/定时配置回填 gDevData;mode 从周期头开跑
|
||||
* @return 无
|
||||
*/
|
||||
void app_nvm_load(void);
|
||||
|
||||
/**
|
||||
* @brief 业务节拍(中断上下文):变化检测 + 防抖计时,**不执行写入**
|
||||
* @param dt 距上次调用的毫秒数
|
||||
* @note ⚠ 跑在 usr_timer 20ms 回调 = 硬件定时器中断上下文,栈极小,
|
||||
* 只允许轻量比较计时;禁止在此调 syscfg_write(会栈溢出复位)。
|
||||
* @return 无
|
||||
*/
|
||||
void app_nvm_tick(uint16_t dt);
|
||||
|
||||
/**
|
||||
* @brief 任务上下文落盘:防抖满后真正写 VM(由 usr_timer_loop 500ms 调用)
|
||||
* @note 必须且只能在非中断上下文调用(VM 擦写 flash 栈用量大)
|
||||
* @return 无
|
||||
*/
|
||||
void app_nvm_flush(void);
|
||||
|
||||
/**
|
||||
* @brief 标记业务字段有变化,防抖计时归零(档位/模式/配置变化时调用)
|
||||
* @return 无
|
||||
*/
|
||||
void app_nvm_mark_dirty(void);
|
||||
|
||||
/**
|
||||
* @brief 出厂重置:清全部业务 VM(保留 BLE 配对,规格书第 14 章)
|
||||
* @note 由 jb_product DP25 调用;RAM 复位由调用方完成
|
||||
* @return 无
|
||||
*/
|
||||
void app_nvm_reset(void);
|
||||
|
||||
#endif /* __APP_NVM_H__ */
|
||||
@@ -0,0 +1,76 @@
|
||||
/******************************************************************************
|
||||
* @file app_pair.c
|
||||
* @brief 配对会话计时实现
|
||||
* @author cyWu <1917507415@qq.com>
|
||||
* @date 2026.08.25
|
||||
* @version V1.0.0
|
||||
* @history
|
||||
* - V1.0.0, 2026.08.25, cyWu, 首次发布,实现配对会话计时与成功/超时判定
|
||||
******************************************************************************/
|
||||
|
||||
#include "app_pair.h"
|
||||
#include "board_pin.h"
|
||||
#include "usr_le_api.h"
|
||||
#include "system/includes.h"
|
||||
|
||||
#define LOG_TAG_CONST APP
|
||||
#define LOG_TAG "[APP_PAIR]"
|
||||
#define LOG_INFO_ENABLE
|
||||
#include "debug.h"
|
||||
|
||||
/** 配对会话状态,替代原来散落的 s_pairing / s_pair_ms */
|
||||
typedef struct {
|
||||
uint8_t active;
|
||||
uint32_t elapsed_ms;
|
||||
} AppPairCtx_t;
|
||||
|
||||
static AppPairCtx_t s_pair;
|
||||
|
||||
/**
|
||||
* @brief 开始一次配对会话(计时清零)
|
||||
* @return 无
|
||||
*/
|
||||
void app_pair_start(void)
|
||||
{
|
||||
s_pair.active = 1;
|
||||
s_pair.elapsed_ms = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 推进配对计时并检查结果,每个业务 tick 调用一次
|
||||
* @param dt 距上次调用的毫秒数
|
||||
* @return APP_PAIR_EVT_OK=配对成功,APP_PAIR_EVT_FAIL=超时,否则 NONE
|
||||
*/
|
||||
AppPairEvt_t app_pair_tick_1ms(uint16_t dt)
|
||||
{
|
||||
if (!s_pair.active) {
|
||||
return APP_PAIR_EVT_NONE;
|
||||
}
|
||||
|
||||
/* BLE 侧绑定成功:结束本次配对会话 */
|
||||
if (usr_get_pair_flag()) {
|
||||
s_pair.active = 0;
|
||||
s_pair.elapsed_ms = 0;
|
||||
log_info("pair ok\n");
|
||||
return APP_PAIR_EVT_OK;
|
||||
}
|
||||
|
||||
s_pair.elapsed_ms += dt;
|
||||
if (s_pair.elapsed_ms >= BOARD_PAIR_TIMEOUT_MS) {
|
||||
s_pair.active = 0;
|
||||
s_pair.elapsed_ms = 0;
|
||||
log_info("pair timeout 120s, stay on\n");
|
||||
return APP_PAIR_EVT_FAIL;
|
||||
}
|
||||
|
||||
return APP_PAIR_EVT_NONE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 查询是否仍在配对会话中
|
||||
* @return 1=进行中,0=未在配对
|
||||
*/
|
||||
uint8_t app_pair_is_active(void)
|
||||
{
|
||||
return s_pair.active;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/******************************************************************************
|
||||
* @file app_pair.h
|
||||
* @brief 配对会话计时:开机配对窗口是否成功/超时,与 BLE 配对本身解耦
|
||||
* @author cyWu <1917507415@qq.com>
|
||||
* @date 2026.08.25
|
||||
* @version V1.0.0
|
||||
* @history
|
||||
* - V1.0.0, 2026.08.25, cyWu, 首次发布,实现配对会话计时与成功/超时判定
|
||||
*
|
||||
* @note 真正的 BLE 广播/绑定逻辑在 usr_le_code;本模块只负责“进入配对后
|
||||
* 120s 内 usr_get_pair_flag() 是否变 1”这件事,纯状态,不碰灯/按键。
|
||||
******************************************************************************/
|
||||
|
||||
#ifndef __APP_PAIR_H__
|
||||
#define __APP_PAIR_H__
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/** 配对结果边沿事件,仅在状态变化那一次返回一次 */
|
||||
typedef enum {
|
||||
APP_PAIR_EVT_NONE = 0,
|
||||
APP_PAIR_EVT_OK,
|
||||
APP_PAIR_EVT_FAIL
|
||||
} AppPairEvt_t;
|
||||
|
||||
/**
|
||||
* @brief 开始一次配对会话(计时清零)
|
||||
* @return 无
|
||||
*/
|
||||
void app_pair_start(void);
|
||||
|
||||
/**
|
||||
* @brief 推进配对计时并检查结果,每个业务 tick 调用一次
|
||||
* @param dt 距上次调用的毫秒数
|
||||
* @return APP_PAIR_EVT_OK=配对成功,APP_PAIR_EVT_FAIL=超时,否则 NONE
|
||||
*/
|
||||
AppPairEvt_t app_pair_tick_1ms(uint16_t dt);
|
||||
|
||||
/**
|
||||
* @brief 查询是否仍在配对会话中
|
||||
* @return 1=进行中,0=未在配对
|
||||
*/
|
||||
uint8_t app_pair_is_active(void);
|
||||
|
||||
#endif /* __APP_PAIR_H__ */
|
||||
@@ -0,0 +1,100 @@
|
||||
/******************************************************************************
|
||||
* @file app_power.c
|
||||
* @brief CBR3 软关机生命周期实现:无电池、无充电
|
||||
* @author cyWu <1917507415@qq.com>
|
||||
* @date 2026.08.29
|
||||
* @version V1.0.0
|
||||
* @history
|
||||
* - V1.0.0, 2026.08.29, cyWu, 首次发布(移植门夹精简:删 USB/充电/电量/低电)
|
||||
******************************************************************************/
|
||||
|
||||
#include "app_power.h"
|
||||
#include "system/includes.h"
|
||||
|
||||
#define LOG_TAG_CONST APP
|
||||
#define LOG_TAG "[APP_PWR]"
|
||||
#define LOG_INFO_ENABLE
|
||||
#include "debug.h"
|
||||
|
||||
/** 电源模块总状态 */
|
||||
typedef struct {
|
||||
uint8_t req_poweroff;
|
||||
AppPowerHook_t on_request;
|
||||
AppPowerHook_t on_finalize;
|
||||
} AppPowerCtx_t;
|
||||
|
||||
static AppPowerCtx_t s_power;
|
||||
|
||||
static void power_do_poweroff(void *priv);
|
||||
|
||||
/**
|
||||
* @brief 初始化电源模块状态
|
||||
* @return 无
|
||||
*/
|
||||
void app_power_init(void)
|
||||
{
|
||||
s_power.req_poweroff = 0;
|
||||
s_power.on_request = NULL;
|
||||
s_power.on_finalize = NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 注册深睡请求的两段式回调
|
||||
* @param on_request 刚请求深睡时调用(停风扇;灯由调用方按关机类型决定)
|
||||
* @param on_finalize 延时到点后调用(真正进深睡,不会返回)
|
||||
* @return 无
|
||||
*/
|
||||
void app_power_set_hooks(AppPowerHook_t on_request, AppPowerHook_t on_finalize)
|
||||
{
|
||||
s_power.on_request = on_request;
|
||||
s_power.on_finalize = on_finalize;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 请求深睡(按键 3s 等):立即触发 on_request 钩子,50ms 后触发 on_finalize
|
||||
* @return 无
|
||||
*/
|
||||
void app_power_request_poweroff(void)
|
||||
{
|
||||
if (s_power.req_poweroff) {
|
||||
return; /* 已经在关机流程里,不要重复挂定时器 */
|
||||
}
|
||||
s_power.req_poweroff = 1;
|
||||
log_info("request deep off\n");
|
||||
if (s_power.on_request) {
|
||||
s_power.on_request(); /* 立刻停风扇、灯全灭 */
|
||||
}
|
||||
sys_timeout_add(NULL, power_do_poweroff, 50); /* 给 GPIO 落稳一点再真正掉电 */
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 查询深睡请求是否已发起
|
||||
* @return 1=已发起,0=未发起
|
||||
*/
|
||||
uint8_t app_power_poweroff_pending(void)
|
||||
{
|
||||
return s_power.req_poweroff;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 当前这次关机是否为空电路径(本产品无电池,恒 0)
|
||||
* @return 0
|
||||
*/
|
||||
uint8_t app_power_empty_off_pending(void)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 50ms 延时到点:真正进入深睡
|
||||
* @param priv 未使用
|
||||
* @return 无
|
||||
*/
|
||||
static void power_do_poweroff(void *priv)
|
||||
{
|
||||
(void)priv;
|
||||
log_info("enter deep off\n");
|
||||
if (s_power.on_finalize) {
|
||||
s_power.on_finalize();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/******************************************************************************
|
||||
* @file app_power.h
|
||||
* @brief CBR3 软关机生命周期:无电池、无充电,只保留 request/finalize
|
||||
* @author cyWu <1917507415@qq.com>
|
||||
* @date 2026.08.29
|
||||
* @version V1.0.0
|
||||
* @history
|
||||
* - V1.0.0, 2026.08.29, cyWu, 首次发布(移植门夹精简:删 USB/充电/电量/低电)
|
||||
*
|
||||
* @note 本产品 USB 常电、无电池,插电不会自动开机,必须长按 3s。
|
||||
* SDK 层 TCFG_CHARGE 相关配置保留为板级模板默认,但业务层完全不
|
||||
* 参与充电/电量逻辑(规格书第 0 章第 3 条)。
|
||||
******************************************************************************/
|
||||
|
||||
#ifndef __APP_POWER_H__
|
||||
#define __APP_POWER_H__
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/** 深睡请求的钩子:on_request 立刻停执行体并按关机类型处理灯;on_finalize 真正掉电 */
|
||||
typedef void (*AppPowerHook_t)(void);
|
||||
|
||||
/**
|
||||
* @brief 初始化电源模块状态
|
||||
* @return 无
|
||||
*/
|
||||
void app_power_init(void);
|
||||
|
||||
/**
|
||||
* @brief 注册深睡请求的两段式回调
|
||||
* @param on_request 刚请求深睡时调用(停风扇;灯由调用方按关机类型决定)
|
||||
* @param on_finalize 延时到点后调用(真正进深睡,不会返回)
|
||||
* @return 无
|
||||
*/
|
||||
void app_power_set_hooks(AppPowerHook_t on_request, AppPowerHook_t on_finalize);
|
||||
|
||||
/**
|
||||
* @brief 请求深睡(按键 3s 等):立即触发 on_request 钩子,50ms 后触发 on_finalize
|
||||
* @return 无
|
||||
*/
|
||||
void app_power_request_poweroff(void);
|
||||
|
||||
/**
|
||||
* @brief 查询深睡请求是否已发起(发起后其它模块应停止响应新操作)
|
||||
* @return 1=已发起,0=未发起
|
||||
*/
|
||||
uint8_t app_power_poweroff_pending(void);
|
||||
|
||||
/**
|
||||
* @brief 当前这次关机是否为空电路径(本产品无电池,恒 0,保留接口)
|
||||
* @return 0
|
||||
*/
|
||||
uint8_t app_power_empty_off_pending(void);
|
||||
|
||||
#endif /* __APP_POWER_H__ */
|
||||
@@ -0,0 +1,171 @@
|
||||
/******************************************************************************
|
||||
* @file app_th.c
|
||||
* @brief CBR3 温湿度采集实现
|
||||
* @author cyWu <1917507415@qq.com>
|
||||
* @date 2026.08.29
|
||||
* @version V1.0.0
|
||||
* @history
|
||||
* - V1.0.0, 2026.08.29, cyWu, 首次发布
|
||||
******************************************************************************/
|
||||
|
||||
#include "app_th.h"
|
||||
#include "bsp_aht20.h"
|
||||
#include "board_pin.h"
|
||||
#include "system/includes.h"
|
||||
|
||||
#define LOG_TAG_CONST APP
|
||||
#define LOG_TAG "[APP_TH]"
|
||||
#define LOG_ERROR_ENABLE
|
||||
#define LOG_INFO_ENABLE
|
||||
#include "debug.h"
|
||||
|
||||
extern u32 timer_get_ms(void);
|
||||
extern void clr_wdt(void);
|
||||
|
||||
/** 采样状态机 */
|
||||
typedef enum {
|
||||
TH_ST_IDLE = 0, /**< 空闲,等待下一个采样周期 */
|
||||
TH_ST_WAIT_DATA /**< 命令已发出,等 ≥80ms 后读数 */
|
||||
} ThState_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t inited;
|
||||
ThState_t state;
|
||||
uint16_t poll_ms; /**< 距上次采样的累计毫秒 */
|
||||
uint32_t wait_t0; /**< 测量命令发出时刻 */
|
||||
int16_t temp_x10; /**< 最近有效温度 ×10 */
|
||||
uint8_t humi; /**< 最近有效湿度 % */
|
||||
uint8_t valid; /**< 1=有有效采样 */
|
||||
uint8_t fail_cnt; /**< 连续失败次数 */
|
||||
uint8_t last_ok; /**< 最近一次采样是否成功(P2 条件引擎 8.4 用) */
|
||||
} AppThCtx_t;
|
||||
|
||||
static AppThCtx_t s_th;
|
||||
|
||||
/**
|
||||
* @brief 初始化温湿度模块
|
||||
* @return 无
|
||||
*/
|
||||
void app_th_init(void)
|
||||
{
|
||||
#if !BOARD_TH_ENABLE
|
||||
log_info("th disabled\n");
|
||||
return;
|
||||
#endif
|
||||
if (s_th.inited) {
|
||||
return;
|
||||
}
|
||||
bsp_aht20_init();
|
||||
s_th.state = TH_ST_IDLE;
|
||||
s_th.poll_ms = 0;
|
||||
s_th.valid = 0;
|
||||
s_th.fail_cnt = 0;
|
||||
s_th.last_ok = 0;
|
||||
s_th.inited = 1;
|
||||
log_info("th init period=%ums\n", (unsigned)BOARD_TH_PERIOD_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 业务节拍:内部 2s 才真正采一次;IO 分离后采集与显示互不干扰
|
||||
* @param dt 距上次调用的毫秒数
|
||||
* @return 无
|
||||
*/
|
||||
void app_th_poll(uint16_t dt)
|
||||
{
|
||||
#if !BOARD_TH_ENABLE
|
||||
(void)dt;
|
||||
return;
|
||||
#endif
|
||||
if (!s_th.inited) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (s_th.state == TH_ST_IDLE) {
|
||||
s_th.poll_ms = (uint16_t)(s_th.poll_ms + dt);
|
||||
if (s_th.poll_ms >= BOARD_TH_PERIOD_MS) {
|
||||
s_th.poll_ms = 0;
|
||||
if (bsp_aht20_measure_start() == 0) {
|
||||
s_th.state = TH_ST_WAIT_DATA;
|
||||
s_th.wait_t0 = timer_get_ms();
|
||||
}
|
||||
/* measure_start 非 0=通讯失败,跳过本次采样 */
|
||||
}
|
||||
} else if (s_th.state == TH_ST_WAIT_DATA) {
|
||||
clr_wdt();
|
||||
if ((timer_get_ms() - s_th.wait_t0) >= BOARD_AHT20_WAIT_MS) {
|
||||
int16_t t = 0;
|
||||
uint8_t h = 0;
|
||||
int8_t ret = bsp_aht20_measure_finish(&t, &h);
|
||||
|
||||
/* IO 分离后无总线竞争,finish 不会返回 -2;0=成功,-1=通讯失败 */
|
||||
s_th.state = TH_ST_IDLE;
|
||||
if (ret == 0) {
|
||||
s_th.temp_x10 = t;
|
||||
s_th.humi = h;
|
||||
s_th.fail_cnt = 0;
|
||||
s_th.valid = 1;
|
||||
s_th.last_ok = 1;
|
||||
log_info("th %d.%dC %d%%\n",
|
||||
(int)(t / 10), (int)((t < 0) ? (-t % 10) : (t % 10)), h);
|
||||
} else {
|
||||
s_th.fail_cnt++;
|
||||
s_th.last_ok = 0;
|
||||
log_error("th sample fail %d\n", s_th.fail_cnt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 是否已有有效采样
|
||||
* @return 1=有效,0=从未成功
|
||||
*/
|
||||
uint8_t app_th_get_valid(void)
|
||||
{
|
||||
return s_th.valid;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 最近一次有效温度
|
||||
* @return 温度 ×10
|
||||
*/
|
||||
int16_t app_th_get_temp_x10(void)
|
||||
{
|
||||
return s_th.temp_x10;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 最近一次有效湿度
|
||||
* @return 湿度 0~100 整数 %
|
||||
*/
|
||||
uint8_t app_th_get_humi(void)
|
||||
{
|
||||
return s_th.humi;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 连续失败次数
|
||||
* @return 连续失败次数
|
||||
*/
|
||||
uint8_t app_th_get_fail_cnt(void)
|
||||
{
|
||||
return s_th.fail_cnt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 传感器故障(连续失败 ≥ BOARD_TH_FAIL_MAX)
|
||||
* @return 1=故障(显示 --、fault_code=2),0=正常
|
||||
*/
|
||||
uint8_t app_th_is_fault(void)
|
||||
{
|
||||
return (s_th.fail_cnt >= BOARD_TH_FAIL_MAX) ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 最近一次采样是否成功(P2 条件引擎 8.4:时长只在采样成功时累加)
|
||||
* @return 1=成功,0=失败/未采样过
|
||||
*/
|
||||
uint8_t app_th_last_sample_ok(void)
|
||||
{
|
||||
return s_th.last_ok;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/******************************************************************************
|
||||
* @file app_th.h
|
||||
* @brief CBR3 温湿度采集:2s 周期、失败计数、最近有效值
|
||||
* @author cyWu <1917507415@qq.com>
|
||||
* @date 2026.08.29
|
||||
* @version V1.0.0
|
||||
* @history
|
||||
* - V1.0.0, 2026.08.29, cyWu, 首次发布
|
||||
*
|
||||
* @note 规格书 8.4:条件"持续时长"只在本次采样成功且比较成立时累加;
|
||||
* 采样失败不累加、不清零;连续 3 次失败显示 "--" 且 fault_code=2。
|
||||
* P0 不做条件比较,只做采样与失败计数。
|
||||
******************************************************************************/
|
||||
|
||||
#ifndef __APP_TH_H__
|
||||
#define __APP_TH_H__
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/**
|
||||
* @brief 初始化温湿度模块
|
||||
* @return 无
|
||||
*/
|
||||
void app_th_init(void);
|
||||
|
||||
/**
|
||||
* @brief 业务节拍:内部 2s 才真正采一次;采的时候走总线互斥
|
||||
* @param dt 距上次调用的毫秒数
|
||||
* @return 无
|
||||
*/
|
||||
void app_th_poll(uint16_t dt);
|
||||
|
||||
/**
|
||||
* @brief 是否已有有效采样
|
||||
* @return 1=有效,0=从未成功
|
||||
*/
|
||||
uint8_t app_th_get_valid(void);
|
||||
|
||||
/**
|
||||
* @brief 最近一次有效温度
|
||||
* @return 温度 ×10(如 25.3℃ → 253)
|
||||
*/
|
||||
int16_t app_th_get_temp_x10(void);
|
||||
|
||||
/**
|
||||
* @brief 最近一次有效湿度
|
||||
* @return 湿度 0~100 整数 %
|
||||
*/
|
||||
uint8_t app_th_get_humi(void);
|
||||
|
||||
/**
|
||||
* @brief 连续失败次数
|
||||
* @return 连续失败次数
|
||||
*/
|
||||
uint8_t app_th_get_fail_cnt(void);
|
||||
|
||||
/**
|
||||
* @brief 传感器故障(连续失败 ≥ BOARD_TH_FAIL_MAX)
|
||||
* @return 1=故障(显示 --、fault_code=2),0=正常
|
||||
*/
|
||||
uint8_t app_th_is_fault(void);
|
||||
|
||||
/**
|
||||
* @brief 最近一次采样是否成功(P2 条件引擎用,规格书 8.4)
|
||||
* @return 1=成功,0=失败/未采样过
|
||||
*/
|
||||
uint8_t app_th_last_sample_ok(void);
|
||||
|
||||
#endif /* __APP_TH_H__ */
|
||||
@@ -0,0 +1,264 @@
|
||||
/******************************************************************************
|
||||
* @file app_timer_task.c
|
||||
* @brief CBR3 定时引擎(P2):7 组 8 字节定时窗,RTC 门闩
|
||||
* @author cyWu <1917507415@qq.com>
|
||||
* @date 2026.08.29
|
||||
* @version V2.0.0
|
||||
* @history
|
||||
* - V1.0.0, 2026.08.29, cyWu, 首次发布(P0 stub)
|
||||
* - V1.1.0, 2026.08.31, cyWu, P1:set_en/set_config 存 VM(引擎仍关)
|
||||
* - V2.0.0, 2026.08.31, cyWu, P2 实现完整定时引擎(规格书第 11 章):
|
||||
* RTC 门闩(未对时一律不跑)/对时补窗/7 组窗判断(跨午夜、星期位图)/
|
||||
* 窗内切模式/结束收口(风扇 0 mode 0)/单次自毁清使能写 VM
|
||||
*
|
||||
* @note 运行上下文:app_timer_run 由 usr_timer_loop(500ms,任务上下文)
|
||||
* 调用——窗判断分钟级精度足够,且单次自毁要写 VM(禁止中断上下文)。
|
||||
* 字节布局(规格书 11.1):Byte0=使能 Byte1=模式 Byte2=类型(0单次/
|
||||
* 1每天/2每周) Byte3=星期位图(Bit0=周一…Bit6=周日,仅类型2)
|
||||
* Byte4-5=开始时分 Byte6-7=结束时分。
|
||||
* 12 章仲裁:定时 > 条件;手动介入后本窗剩余时间不自动抢回,
|
||||
* 等下一窗开始沿或对时补窗;窗结束时若定时仍在占用才收口。
|
||||
******************************************************************************/
|
||||
|
||||
#include "app_timer_task.h"
|
||||
#include "app_arbiter.h"
|
||||
#include "app_mode.h"
|
||||
#include "app_fan.h"
|
||||
#include "app_nvm.h"
|
||||
#include "usr_rtc.h"
|
||||
#include "jb_product.h"
|
||||
#include "board_pin.h"
|
||||
|
||||
#define LOG_TAG_CONST APP
|
||||
#define LOG_TAG "[APP_TMR]"
|
||||
#define LOG_INFO_ENABLE
|
||||
#include "debug.h"
|
||||
|
||||
/** 定时引擎运行时状态 */
|
||||
typedef struct {
|
||||
uint8_t taken; /**< 1=本窗已接管过(手动打断后也不抢回,直到窗结束) */
|
||||
uint8_t win_idx; /**< 当前/最近命中组 0~6,0xFF=无 */
|
||||
uint8_t last_rtc_ok; /**< 上次 RTC 门闩状态(对时补窗沿检测) */
|
||||
} TimerState_t;
|
||||
|
||||
static TimerState_t s_timer;
|
||||
|
||||
/**
|
||||
* @brief 取第 idx 组定时配置(gDevData 里是 7 个独立数组字段)
|
||||
* @param idx 0~6
|
||||
* @return 配置指针;越界返回 NULL
|
||||
*/
|
||||
static uint8_t *timer_cfg_ptr(uint8_t idx)
|
||||
{
|
||||
switch (idx) {
|
||||
case 0: return gDevData.time_config1;
|
||||
case 1: return gDevData.time_config2;
|
||||
case 2: return gDevData.time_config3;
|
||||
case 3: return gDevData.time_config4;
|
||||
case 4: return gDevData.time_config5;
|
||||
case 5: return gDevData.time_config6;
|
||||
case 6: return gDevData.time_config7;
|
||||
default: return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 计算某日期是星期几
|
||||
* @param y 年
|
||||
* @param m 月 1~12
|
||||
* @param d 日
|
||||
* @return 0=周一 … 6=周日(规格书 11.1 位图 Bit0=周一)
|
||||
*/
|
||||
static uint8_t timer_calc_dow(uint16_t y, uint8_t m, uint8_t d)
|
||||
{
|
||||
uint8_t mm = m;
|
||||
uint16_t yy = y;
|
||||
uint16_t k;
|
||||
int h;
|
||||
|
||||
if (mm < 3) {
|
||||
mm += 12;
|
||||
yy--;
|
||||
}
|
||||
k = yy % 100;
|
||||
/* 蔡勒公式(格里高利历):h=0 周六 … 6 周五 */
|
||||
h = (d + 13 * (mm + 1) / 5 + k + k / 4 + (yy / 100) / 4 + 5 * (yy / 100)) % 7;
|
||||
return (uint8_t)((h + 5) % 7); /* 0=周一 … 6=周日 */
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 当前时刻是否在窗口内
|
||||
* @param cfg 8 字节配置
|
||||
* @param t 本地时间
|
||||
* @return 1=在窗内
|
||||
*/
|
||||
static uint8_t timer_in_window(const uint8_t *cfg, const struct sys_time *t)
|
||||
{
|
||||
uint8_t type = cfg[2];
|
||||
int16_t start = (int16_t)cfg[4] * 60 + cfg[5];
|
||||
int16_t end = (int16_t)cfg[6] * 60 + cfg[7];
|
||||
int16_t now = (int16_t)t->hour * 60 + t->min;
|
||||
|
||||
if (start == end) {
|
||||
return 0; /* 非法窗(零时长) */
|
||||
}
|
||||
|
||||
if (type == 2) {
|
||||
/* 每周:星期位图;跨午夜窗以"开始时刻所在本地星期"判断(11.1) */
|
||||
uint8_t dow = timer_calc_dow(t->year, t->month, t->day);
|
||||
uint8_t dow_bit;
|
||||
|
||||
if (start > end) {
|
||||
/* 跨午夜窗:凌晨 [0,end) 属于昨晚的窗,用昨天星期 */
|
||||
if (now < end) {
|
||||
dow = (uint8_t)((dow + 6) % 7); /* 昨天 */
|
||||
}
|
||||
}
|
||||
dow_bit = (uint8_t)(1u << dow);
|
||||
if (!(cfg[3] & dow_bit)) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (start < end) {
|
||||
return ((now >= start) && (now < end)) ? 1 : 0;
|
||||
}
|
||||
return ((now >= start) || (now < end)) ? 1 : 0; /* 跨午夜 */
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 窗结束收口:释放定时占用 + 风扇 0 / mode 0 + 单次自毁
|
||||
* @note 仅定时仍在占用时才收口(手动打断后不干预,12 章规则 2);
|
||||
* 单次(类型 0)无论谁占用,结束时刻后清 Byte0 并写 VM(11.1)
|
||||
* @return 无
|
||||
*/
|
||||
static void timer_window_close(void)
|
||||
{
|
||||
uint8_t idx = s_timer.win_idx;
|
||||
|
||||
s_timer.taken = 0;
|
||||
s_timer.win_idx = 0xFF;
|
||||
|
||||
if (app_arbiter_get_owner() == ARB_OWNER_TIMER) {
|
||||
app_arbiter_timer_release();
|
||||
app_mode_stop();
|
||||
app_fan_set_gear(0);
|
||||
log_info("timer window out, stop (mode=0 fan=0)\n");
|
||||
} else {
|
||||
log_info("timer window out (manual taken)\n");
|
||||
}
|
||||
|
||||
/* 单次自毁:结束时刻过后 Byte0 清 0 并写 VM,不再跑(11.1) */
|
||||
if ((idx != 0xFF) && (timer_cfg_ptr(idx)[2] == 0)) {
|
||||
timer_cfg_ptr(idx)[0] = 0;
|
||||
app_nvm_mark_dirty();
|
||||
log_info("timer[%d] once done, en cleared\n", idx + 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 初始化定时引擎
|
||||
* @return 无
|
||||
*/
|
||||
void app_timer_init(void)
|
||||
{
|
||||
#if !BOARD_TIMER_ENABLE
|
||||
log_info("timer engine disabled\n");
|
||||
return;
|
||||
#endif
|
||||
s_timer.taken = 0;
|
||||
s_timer.win_idx = 0xFF;
|
||||
s_timer.last_rtc_ok = 0;
|
||||
log_info("timer init\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 推进定时引擎(500ms,任务上下文,由 usr_timer_loop 调用)
|
||||
* @param dt 距上次调用的毫秒数
|
||||
* @return 无
|
||||
*/
|
||||
void app_timer_run(uint16_t dt)
|
||||
{
|
||||
#if !BOARD_TIMER_ENABLE
|
||||
(void)dt;
|
||||
return;
|
||||
#endif
|
||||
struct sys_time t;
|
||||
uint8_t rtc_ok = usr_rtc_get_time(&t);
|
||||
uint8_t win_idx = 0xFF;
|
||||
uint8_t i;
|
||||
|
||||
(void)dt;
|
||||
|
||||
/* 对时补窗(11.2):RTC 从无效→有效瞬间立即重判窗口 */
|
||||
if (rtc_ok && !s_timer.last_rtc_ok) {
|
||||
log_info("timer rtc synced, re-check window\n");
|
||||
}
|
||||
s_timer.last_rtc_ok = rtc_ok;
|
||||
|
||||
/* RTC 门闩 / time_en=0:等同所有窗立刻结束(11.2) */
|
||||
if (!rtc_ok || !gDevData.time_en) {
|
||||
if (s_timer.taken) {
|
||||
timer_window_close();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/* 找第一个命中的使能窗 */
|
||||
for (i = 0; i < 7; i++) {
|
||||
const uint8_t *cfg = timer_cfg_ptr(i);
|
||||
|
||||
if (!cfg || (cfg[0] == 0)) {
|
||||
continue;
|
||||
}
|
||||
if (timer_in_window(cfg, &t)) {
|
||||
win_idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (win_idx != 0xFF) {
|
||||
/* ── 窗内 ── */
|
||||
if (!s_timer.taken) {
|
||||
/* 窗开始沿:接管(定时优先级最高,条件让位由条件引擎感知) */
|
||||
const uint8_t *cfg = timer_cfg_ptr(win_idx);
|
||||
|
||||
app_arbiter_timer_take();
|
||||
app_mode_set_manual(cfg[1]); /* 切 Byte1 模式(通风循环/强力一轮) */
|
||||
s_timer.taken = 1;
|
||||
s_timer.win_idx = win_idx;
|
||||
log_info("timer[%d] window in, mode=%d\n", win_idx + 1, cfg[1]);
|
||||
}
|
||||
/* 已接管(或手动打断后不抢回,12 章规则 2):保持;
|
||||
* 通风循环由 app_mode 自理;强力只一轮 15min 自停(11.3),
|
||||
* 结束时刻由本函数窗结束沿收口 */
|
||||
} else {
|
||||
if (s_timer.taken) {
|
||||
timer_window_close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief APP 写 DP13 定时总开关:存 gDevData + 触发 VM 持久化
|
||||
* @param en 0=关,1=开
|
||||
* @note 引擎关闭时仍保存(P1 验收);置 1 后下个 tick 补窗判断
|
||||
* @return 无
|
||||
*/
|
||||
void app_timer_set_en(uint8_t en)
|
||||
{
|
||||
app_nvm_mark_dirty();
|
||||
log_info("time_en=%d (stored)\n", en);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief APP 写 DP14~20 定时配置:触发 VM 持久化(数据已在 gDevData)
|
||||
* @param idx 1~7
|
||||
* @note 引擎关闭时仍保存
|
||||
* @return 无
|
||||
*/
|
||||
void app_timer_set_config(uint8_t idx)
|
||||
{
|
||||
app_nvm_mark_dirty();
|
||||
log_info("time_config[%d] stored\n", idx);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/******************************************************************************
|
||||
* @file app_timer_task.h
|
||||
* @brief CBR3 定时引擎(P2):7 组 8 字节定时窗,RTC 门闩
|
||||
* @author cyWu <1917507415@qq.com>
|
||||
* @date 2026.08.29
|
||||
* @version V1.0.0
|
||||
* @history
|
||||
* - V1.0.0, 2026.08.29, cyWu, 首次发布(P0 仅接口与 stub)
|
||||
*
|
||||
* @note 文件名避开 usr_rtc。字节布局见规格书 11.1:Byte0=使能 Byte1=模式
|
||||
* Byte2=类型(0单次/1每天/2每周) Byte3=星期位图 Byte4-5=开始时/分
|
||||
* Byte6-7=结束时/分。RTC 未对时一律不执行(规格书 11.2)。
|
||||
* ⚠ app_timer_run 由 usr_timer_loop(500ms,任务上下文)调用
|
||||
* (单次自毁要写 VM,禁止放 20ms 中断上下文)。
|
||||
******************************************************************************/
|
||||
|
||||
#ifndef __APP_TIMER_TASK_H__
|
||||
#define __APP_TIMER_TASK_H__
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/**
|
||||
* @brief 初始化定时引擎
|
||||
* @return 无
|
||||
*/
|
||||
void app_timer_init(void);
|
||||
|
||||
/**
|
||||
* @brief 推进定时引擎,每个业务 tick 调用一次
|
||||
* @param dt 距上次调用的毫秒数
|
||||
* @return 无
|
||||
*/
|
||||
void app_timer_run(uint16_t dt);
|
||||
|
||||
/**
|
||||
* @brief APP 写 DP13 定时总开关
|
||||
* @param en 0=关,1=开
|
||||
* @return 无
|
||||
*/
|
||||
void app_timer_set_en(uint8_t en);
|
||||
|
||||
/**
|
||||
* @brief APP 写 DP14~20 定时配置(8 字节)
|
||||
* @param idx 1~7
|
||||
* @return 无
|
||||
*/
|
||||
void app_timer_set_config(uint8_t idx);
|
||||
|
||||
#endif /* __APP_TIMER_TASK_H__ */
|
||||
@@ -0,0 +1,267 @@
|
||||
/******************************************************************************
|
||||
* @file key.c
|
||||
* @brief 按键状态机实现
|
||||
* @author cyWu <1917507415@qq.com>
|
||||
* @date 2026.08.25
|
||||
* @version V1.0.0
|
||||
* @history
|
||||
* - V1.0.0, 2026.08.25, cyWu, 首次发布,实现按键短按/长按/释放状态机
|
||||
******************************************************************************/
|
||||
|
||||
#include "key.h"
|
||||
#include <stddef.h>
|
||||
|
||||
static uint32_t s_key_count_time;
|
||||
static uint8_t s_key_num = USR_KEY_COUNT;
|
||||
keyCategory_t keyTable[USR_KEY_COUNT];
|
||||
|
||||
/**
|
||||
* @brief 读按键电平到 fsm
|
||||
* @param key 状态机
|
||||
* @return 1=已读,0=被屏蔽
|
||||
*/
|
||||
static uint8_t get_key_level(keyFSM_t *key)
|
||||
{
|
||||
if (key->keyShield == KEY_DISABLE) {
|
||||
return 0;
|
||||
}
|
||||
if (key->keyReadValue == NULL) {
|
||||
return 0;
|
||||
}
|
||||
key->keyLevel = (key->keyReadValue() == key->keyDownLevel) ? Bit_SET : Bit_RESET;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 单击 / 双击间隔判定
|
||||
* @param click 状态机
|
||||
* @return 1=可以出事件
|
||||
*/
|
||||
static uint8_t key_click_frequency(keyFSM_t *click)
|
||||
{
|
||||
click->keyInterval++;
|
||||
switch (click->keyFrequency) {
|
||||
case CLICK:
|
||||
if (click->keyInterval >= (KEY_INTERVAL / (DEBOUNCE_TIME * KEY_TIMER_MS))) {
|
||||
click->eventType = SHORT_Event;
|
||||
return 1;
|
||||
}
|
||||
break;
|
||||
case DbLCLICK:
|
||||
if (click->keyInterval >= (KEY_INTERVAL / (DEBOUNCE_TIME * KEY_TIMER_MS))) {
|
||||
click->eventType = DBCL_Event;
|
||||
return 1;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 按键状态机步进
|
||||
* @param key_buf 状态机
|
||||
* @return 1=已处理
|
||||
*/
|
||||
static uint8_t read_key_status(keyFSM_t *key_buf)
|
||||
{
|
||||
if (!get_key_level(key_buf)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
switch (key_buf->keyStatus) {
|
||||
case KEY_ST_NULL:
|
||||
if (key_buf->keyLevel == Bit_SET) {
|
||||
key_buf->keyStatus = KEY_ST_SURE;
|
||||
}
|
||||
break;
|
||||
case KEY_ST_SURE:
|
||||
if (key_buf->keyLevel == Bit_SET) {
|
||||
key_buf->eventType = DOWN_Event;
|
||||
key_buf->keyStatus = KEY_ST_DOWN;
|
||||
key_buf->keyCount = 0;
|
||||
key_buf->keyLongFlag = 1;
|
||||
} else {
|
||||
key_buf->keyStatus = KEY_ST_NULL;
|
||||
}
|
||||
break;
|
||||
case KEY_ST_DOWN:
|
||||
if (key_buf->keyLevel != Bit_SET) {
|
||||
if (key_buf->keyShortFlag == 0) {
|
||||
key_buf->keyShortFlag = 1;
|
||||
key_buf->keyFrequency++;
|
||||
key_buf->keyInterval = 0;
|
||||
}
|
||||
if (key_click_frequency(key_buf)) {
|
||||
key_buf->keyFrequency = 0;
|
||||
key_buf->keyStatus = KEY_ST_NULL;
|
||||
}
|
||||
if ((key_buf->keyCount >= key_buf->keyLongTime / DEBOUNCE_TIME)
|
||||
&& (key_buf->keyCount < key_buf->keyLastTime / DEBOUNCE_TIME)) {
|
||||
key_buf->keyFrequency = 0;
|
||||
key_buf->keyStatus = KEY_ST_NULL;
|
||||
key_buf->eventType = RELEASE_Event;
|
||||
}
|
||||
key_buf->keyCount = 0;
|
||||
key_buf->keyLongFlag = 1;
|
||||
} else {
|
||||
if ((++key_buf->keyCount >= key_buf->keyLastTime / DEBOUNCE_TIME)) {
|
||||
key_buf->keyCount = 0;
|
||||
key_buf->keyFrequency = 0;
|
||||
key_buf->keyStatus = KEY_ST_LONG;
|
||||
key_buf->eventType = LAST_Event;
|
||||
key_buf->keyLongFlag = 1;
|
||||
}
|
||||
if ((key_buf->keyCount >= key_buf->keyLongTime / DEBOUNCE_TIME)
|
||||
&& (key_buf->keyCount < key_buf->keyLastTime / DEBOUNCE_TIME)) {
|
||||
key_buf->keyFrequency = 0;
|
||||
if (key_buf->keyLongFlag == 1) {
|
||||
key_buf->eventType = LONG_Event;
|
||||
key_buf->keyLongFlag = 0;
|
||||
}
|
||||
}
|
||||
key_buf->keyShortFlag = 0;
|
||||
}
|
||||
break;
|
||||
case KEY_ST_LONG:
|
||||
if (key_buf->keyLevel != Bit_SET) {
|
||||
key_buf->keyStatus = KEY_ST_NULL;
|
||||
key_buf->eventType = RELEASE_Event;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 扫描全部按键状态机
|
||||
* @return 无
|
||||
*/
|
||||
static void key_event_process(void)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
for (i = 0; i < s_key_num; i++) {
|
||||
(void)read_key_status(&keyTable[i].fsm);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 1ms 调用一次;满 DEBOUNCE_TIME 才真正采样
|
||||
* @return 无
|
||||
*/
|
||||
void keyCheckProcess(void)
|
||||
{
|
||||
s_key_count_time++;
|
||||
if (s_key_count_time >= (DEBOUNCE_TIME / KEY_TIMER_MS)) {
|
||||
s_key_count_time = 0;
|
||||
key_event_process();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 拷贝产品按键表
|
||||
* @param keys 按键数组,长度 USR_KEY_COUNT
|
||||
* @return 无
|
||||
*/
|
||||
void keyParaInit(keyCategory_t *keys)
|
||||
{
|
||||
if (keys == NULL) {
|
||||
return;
|
||||
}
|
||||
if (USR_KEY_COUNT >= KEY_MAX_NUMBER) {
|
||||
s_key_num = KEY_MAX_NUMBER;
|
||||
}
|
||||
memcpy(keyTable, keys, sizeof(keyCategory_t) * s_key_num);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 运行中改长按/连按时长并复位状态
|
||||
* @param key_event 按键索引
|
||||
* @param key_param 新参数
|
||||
* @return 无
|
||||
*/
|
||||
void setKeyEventParams(uint8_t key_event, keyCategory_t key_param)
|
||||
{
|
||||
if (key_event >= s_key_num) {
|
||||
return;
|
||||
}
|
||||
keyTable[key_event].fsm.keyShield = key_param.fsm.keyShield;
|
||||
keyTable[key_event].fsm.keyLongTime = key_param.fsm.keyLongTime;
|
||||
keyTable[key_event].fsm.keyLastTime = key_param.fsm.keyLastTime;
|
||||
keyTable[key_event].fsm.keyStatus = KEY_ST_NULL;
|
||||
keyTable[key_event].fsm.eventType = NULL_Event;
|
||||
keyTable[key_event].fsm.keyCount = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 派发按键事件回调
|
||||
* @return 无
|
||||
*/
|
||||
void keyHandle(void)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
for (i = 0; i < s_key_num; i++) {
|
||||
if (keyTable[i].fsm.eventType == NULL_Event) {
|
||||
continue;
|
||||
}
|
||||
switch (keyTable[i].fsm.eventType) {
|
||||
case RELEASE_Event:
|
||||
if (keyTable[i].func.releasePressCb != NULL) {
|
||||
keyTable[i].func.releasePressCb();
|
||||
}
|
||||
keyTable[i].fsm.eventType = NULL_Event;
|
||||
break;
|
||||
case SHORT_Event:
|
||||
if (keyTable[i].func.ShortPressCb != NULL) {
|
||||
keyTable[i].func.ShortPressCb();
|
||||
}
|
||||
keyTable[i].fsm.eventType = NULL_Event;
|
||||
break;
|
||||
case DOWN_Event:
|
||||
if (keyTable[i].func.downPressCb != NULL) {
|
||||
keyTable[i].func.downPressCb();
|
||||
}
|
||||
keyTable[i].fsm.eventType = NULL_Event;
|
||||
break;
|
||||
case LONG_Event:
|
||||
if (keyTable[i].func.longPressCb != NULL) {
|
||||
keyTable[i].func.longPressCb();
|
||||
}
|
||||
keyTable[i].fsm.eventType = NULL_Event;
|
||||
break;
|
||||
case LAST_Event:
|
||||
if (keyTable[i].func.lastPressCb != NULL) {
|
||||
keyTable[i].func.lastPressCb();
|
||||
}
|
||||
break;
|
||||
case DBCL_Event:
|
||||
if (keyTable[i].func.dbclPressCb != NULL) {
|
||||
keyTable[i].func.dbclPressCb();
|
||||
}
|
||||
keyTable[i].fsm.eventType = NULL_Event;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 复位指定按键状态机
|
||||
* @param key_index 按键索引
|
||||
* @return 无
|
||||
*/
|
||||
void reset_key_Status(keyList key_index)
|
||||
{
|
||||
if (key_index >= USR_KEY_COUNT) {
|
||||
return;
|
||||
}
|
||||
keyTable[key_index].fsm.keyStatus = KEY_ST_NULL;
|
||||
keyTable[key_index].fsm.eventType = NULL_Event;
|
||||
keyTable[key_index].fsm.keyCount = 0;
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/******************************************************************************
|
||||
* @file key.h
|
||||
* @brief 按键状态机:短按 / 长按 / 释放
|
||||
* @author cyWu <1917507415@qq.com>
|
||||
* @date 2026.08.25
|
||||
* @version V1.0.0
|
||||
* @history
|
||||
* - V1.0.0, 2026.08.25, cyWu, 首次发布,实现按键短按/长按/释放状态机
|
||||
******************************************************************************/
|
||||
|
||||
#ifndef __KEY_H__
|
||||
#define __KEY_H__
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
/*============================================================================*/
|
||||
/* 宏定义 */
|
||||
/*============================================================================*/
|
||||
|
||||
#define KEY_TIMER_MS 1
|
||||
#define KEY_MAX_NUMBER 12
|
||||
#define DEBOUNCE_TIME 30
|
||||
#define KEY_INTERVAL 30
|
||||
|
||||
#define CLICK 1
|
||||
#define DbLCLICK 2
|
||||
|
||||
typedef enum {
|
||||
USR_KEY_USER = 0,
|
||||
USR_KEY_COUNT
|
||||
} keyList;
|
||||
|
||||
typedef enum {
|
||||
Bit_RESET = 0,
|
||||
Bit_SET
|
||||
} BitAction_t;
|
||||
|
||||
typedef enum {
|
||||
KEY_ST_NULL = 0,
|
||||
KEY_ST_RELEASE,
|
||||
KEY_ST_SURE,
|
||||
KEY_ST_UP,
|
||||
KEY_ST_DOWN,
|
||||
KEY_ST_LONG
|
||||
} keyStatus_t;
|
||||
|
||||
typedef enum {
|
||||
NULL_Event = 0,
|
||||
DOWN_Event,
|
||||
SHORT_Event,
|
||||
LONG_Event,
|
||||
LAST_Event,
|
||||
DBCL_Event,
|
||||
RELEASE_Event
|
||||
} keyEvent_t;
|
||||
|
||||
typedef enum {
|
||||
KEY_DISABLE = 0,
|
||||
KEY_ENABLE = !KEY_DISABLE
|
||||
} keyEnable_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t keyShortFlag;
|
||||
uint8_t keyLongFlag;
|
||||
uint8_t keyInterval;
|
||||
uint8_t keyFrequency;
|
||||
uint16_t keyLongTime;
|
||||
uint16_t keyLastTime;
|
||||
uint32_t keyCount;
|
||||
keyEnable_t keyShield;
|
||||
BitAction_t keyLevel;
|
||||
BitAction_t keyDownLevel;
|
||||
keyStatus_t keyStatus;
|
||||
keyEvent_t eventType;
|
||||
uint8_t (*keyReadValue)(void);
|
||||
} keyFSM_t;
|
||||
|
||||
typedef struct {
|
||||
void (*nullPressCb)(void);
|
||||
void (*releasePressCb)(void);
|
||||
void (*downPressCb)(void);
|
||||
void (*ShortPressCb)(void);
|
||||
void (*longPressCb)(void);
|
||||
void (*lastPressCb)(void);
|
||||
void (*dbclPressCb)(void);
|
||||
} keyFunc_t;
|
||||
|
||||
typedef struct {
|
||||
keyFSM_t fsm;
|
||||
keyFunc_t func;
|
||||
} keyCategory_t;
|
||||
|
||||
void keyParaInit(keyCategory_t *keys);
|
||||
void setKeyEventParams(uint8_t key_event, keyCategory_t key_param);
|
||||
void keyCheckProcess(void);
|
||||
void keyHandle(void);
|
||||
void reset_key_Status(keyList key_index);
|
||||
|
||||
#endif /* __KEY_H__ */
|
||||
@@ -0,0 +1,148 @@
|
||||
/******************************************************************************
|
||||
* @file key_manager.c
|
||||
* @brief CBR3 按键:读 PB1,短按切档、长按 3s 关机交给 usr_jb_main
|
||||
* @author cyWu <1917507415@qq.com>
|
||||
* @date 2026.08.29
|
||||
* @version V1.0.0
|
||||
* @history
|
||||
* - V1.0.0, 2026.08.29, cyWu, 首次发布(移植门夹,短按语义改切档)
|
||||
******************************************************************************/
|
||||
|
||||
#include "system/includes.h"
|
||||
#include "key_manager.h"
|
||||
#include "bsp_hw.h"
|
||||
#include "board_pin.h"
|
||||
|
||||
#define LOG_TAG_CONST APP
|
||||
#define LOG_TAG "[KEY]"
|
||||
#define LOG_INFO_ENABLE
|
||||
#include "debug.h"
|
||||
|
||||
/** 模块状态,替代原来 2 个散落的 static */
|
||||
typedef struct {
|
||||
uint8_t initialized;
|
||||
uint8_t boot_key_lock; /**< 1=开机判定时键还按着,等松开才认短按/长按 */
|
||||
} KeyManagerCtx_t;
|
||||
|
||||
static KeyManagerCtx_t s_km;
|
||||
|
||||
/**
|
||||
* @brief 读按键:1=按下
|
||||
* @return 1=按下,0=松开
|
||||
*/
|
||||
static uint8_t key_user_read(void)
|
||||
{
|
||||
return bsp_key_is_pressed();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 松开:清掉开机按着键的锁
|
||||
* @return 无
|
||||
*/
|
||||
static void key_user_release(void)
|
||||
{
|
||||
if (s_km.boot_key_lock) {
|
||||
s_km.boot_key_lock = 0;
|
||||
log_info("boot key released\n");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 短按:开机未松手前忽略
|
||||
* @return 无
|
||||
*/
|
||||
static void key_user_short(void)
|
||||
{
|
||||
if (s_km.boot_key_lock) {
|
||||
s_km.boot_key_lock = 0; /* 开机松手当成释放,不当短按切档 */
|
||||
log_info("boot key released\n");
|
||||
return;
|
||||
}
|
||||
usr_on_key_short();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 长按 3s:开机未松手前忽略,避免开机手还按着就关机
|
||||
* @return 无
|
||||
*/
|
||||
static void key_user_long(void)
|
||||
{
|
||||
if (s_km.boot_key_lock) {
|
||||
return; /* 开机手还按着,忽略这次 3s 长按,避免一开机就关机 */
|
||||
}
|
||||
usr_on_key_long();
|
||||
}
|
||||
|
||||
static keyCategory_t s_keys[USR_KEY_COUNT] = {
|
||||
[USR_KEY_USER] = {
|
||||
.fsm.keyShield = KEY_ENABLE,
|
||||
.fsm.keyDownLevel = Bit_SET,
|
||||
.fsm.eventType = NULL_Event,
|
||||
.fsm.keyLongTime = BOARD_KEY_POWER_MS,
|
||||
.fsm.keyLastTime = (BOARD_KEY_POWER_MS + 7000), /* 大于长按即可,不使用持续按 */
|
||||
.fsm.keyReadValue = key_user_read,
|
||||
.func.ShortPressCb = key_user_short,
|
||||
.func.longPressCb = key_user_long,
|
||||
.func.releasePressCb = key_user_release,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 装载按键表
|
||||
* @return 无
|
||||
*/
|
||||
void user_key_init(void)
|
||||
{
|
||||
if (s_km.initialized) {
|
||||
return;
|
||||
}
|
||||
keyParaInit(s_keys);
|
||||
s_km.boot_key_lock = 0;
|
||||
s_km.initialized = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 开机判定结束后:键还按着则锁住,松开才认短按/长按
|
||||
* @return 无
|
||||
*/
|
||||
void user_key_lock_if_held(void)
|
||||
{
|
||||
reset_key_Status(USR_KEY_USER);
|
||||
if (bsp_key_is_pressed()) {
|
||||
s_km.boot_key_lock = 1; /* 开机手还按着,等松开才认短按/长按,避免立刻关机 */
|
||||
log_info("boot key held, wait release\n");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 按真实毫秒推进状态机
|
||||
* @param dt 距上次调用的毫秒数
|
||||
* @return 无
|
||||
*/
|
||||
void user_key_scan(uint16_t dt)
|
||||
{
|
||||
uint16_t i;
|
||||
|
||||
if (!s_km.initialized) {
|
||||
return;
|
||||
}
|
||||
if (dt == 0) {
|
||||
dt = 1;
|
||||
}
|
||||
/* 按键库按 1ms 步进,业务节拍是 20ms,这里循环 dt 次对齐真实时间 */
|
||||
for (i = 0; i < dt; i++) {
|
||||
keyCheckProcess();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 派发按键回调
|
||||
* @return 无
|
||||
*/
|
||||
void user_key_handle(void)
|
||||
{
|
||||
if (!s_km.initialized) {
|
||||
return;
|
||||
}
|
||||
keyHandle();
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/******************************************************************************
|
||||
* @file key_manager.h
|
||||
* @brief CBR3 单按键:短按切档,长按 3s 关机;开机 3s/5s/12s 不走本库
|
||||
* @author cyWu <1917507415@qq.com>
|
||||
* @date 2026.08.29
|
||||
* @version V1.0.0
|
||||
* @history
|
||||
* - V1.0.0, 2026.08.29, cyWu, 首次发布(移植门夹,语义改切档/关机)
|
||||
******************************************************************************/
|
||||
|
||||
#ifndef __KEY_MANAGER_H__
|
||||
#define __KEY_MANAGER_H__
|
||||
|
||||
#include "key.h"
|
||||
|
||||
/**
|
||||
* @brief 业务层短按回调(app_function 实现)
|
||||
* @return 无
|
||||
*/
|
||||
void usr_on_key_short(void);
|
||||
|
||||
/**
|
||||
* @brief 业务层长按 3s 回调(app_function 实现)
|
||||
* @return 无
|
||||
*/
|
||||
void usr_on_key_long(void);
|
||||
|
||||
/**
|
||||
* @brief 装载按键表
|
||||
* @return 无
|
||||
*/
|
||||
void user_key_init(void);
|
||||
|
||||
/**
|
||||
* @brief 开机判定结束后:若键还按着则锁住,松开才认短按/长按
|
||||
* @return 无
|
||||
*/
|
||||
void user_key_lock_if_held(void);
|
||||
|
||||
/**
|
||||
* @brief 按真实毫秒推进状态机(20ms 节拍会循环 dt 次)
|
||||
* @param dt 距上次调用的毫秒数
|
||||
* @return 无
|
||||
*/
|
||||
void user_key_scan(uint16_t dt);
|
||||
|
||||
/**
|
||||
* @brief 派发按键回调
|
||||
* @return 无
|
||||
*/
|
||||
void user_key_handle(void);
|
||||
|
||||
#endif /* __KEY_MANAGER_H__ */
|
||||
@@ -0,0 +1,328 @@
|
||||
/******************************************************************************
|
||||
* @file led.c
|
||||
* @brief LED 事件框架实现:只改 ledMask 对应通道,红绿互不抢
|
||||
* @author cyWu <1917507415@qq.com>
|
||||
* @date 2026.08.25
|
||||
* @version V1.0.0
|
||||
* @history
|
||||
* - V1.0.0, 2026.08.25, cyWu, 首次发布,实现按通道掩码独立开关的 LED 事件框架
|
||||
******************************************************************************/
|
||||
|
||||
#include "led.h"
|
||||
#include <stddef.h>
|
||||
|
||||
static LED_EventTableItem_t s_led_priority_table[PRIORITY_MAX];
|
||||
static LED_EventTableItem_t s_led_event_table[LED_EVENT_MAX];
|
||||
static LED_SetState_t s_led_set_state;
|
||||
static TIME_Get_t s_time_get;
|
||||
|
||||
static uint32_t get_tick_diff(uint32_t meiosis);
|
||||
static uint8_t led_lighting_update(LED_Config_t *led);
|
||||
#if (BLINK_ENABLE)
|
||||
static uint8_t led_blinking_update(LED_Config_t *led);
|
||||
#endif
|
||||
#if (ALTERNATING_BLINK_ENABLE)
|
||||
static uint8_t led_alternating_update(LED_Config_t *led);
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief 注册写灯回调,只对掩码里的通道动手
|
||||
* @param setState 回调,led=通道掩码,state=LED_ON/OFF
|
||||
* @return 无
|
||||
*/
|
||||
void LED_SetStateInit(LED_SetState_t setState)
|
||||
{
|
||||
s_led_set_state = setState;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 注册毫秒时间戳回调
|
||||
* @param getTime 回调
|
||||
* @return 无
|
||||
*/
|
||||
void SET_TimeGetInit(TIME_Get_t getTime)
|
||||
{
|
||||
s_time_get = getTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 拷贝产品事件表
|
||||
* @param ledEvent 长度必须为 LED_EVENT_MAX
|
||||
* @return 无
|
||||
*/
|
||||
void LED_EventTableInit(LED_EventTableItem_t ledEvent[])
|
||||
{
|
||||
if (ledEvent == NULL) {
|
||||
return;
|
||||
}
|
||||
memset(s_led_priority_table, 0, sizeof(s_led_priority_table));
|
||||
memset(s_led_event_table, 0, sizeof(s_led_event_table));
|
||||
memcpy(s_led_event_table, ledEvent, sizeof(s_led_event_table));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 把事件挂到对应优先级槽;同优先级会被覆盖
|
||||
* @param ledEvent 事件号
|
||||
* @return 无
|
||||
*/
|
||||
void LED_EventAdd(LED_Event_t ledEvent)
|
||||
{
|
||||
size_t i;
|
||||
LED_EventTableItem_t *item;
|
||||
LED_EventPriority_t prio;
|
||||
|
||||
if ((s_time_get == NULL) || (s_led_set_state == NULL)) {
|
||||
return;
|
||||
}
|
||||
if (ledEvent >= LED_EVENT_MAX) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (i = 0; i < LED_EVENT_MAX; i++) {
|
||||
item = &s_led_event_table[i];
|
||||
if (item->event != ledEvent) {
|
||||
continue;
|
||||
}
|
||||
prio = item->priority;
|
||||
if (prio >= PRIORITY_MAX) {
|
||||
return;
|
||||
}
|
||||
if (s_led_priority_table[prio].event != ledEvent) {
|
||||
item->config.currentTime = s_time_get();
|
||||
if (s_led_priority_table[prio].event != LED_EVENT_NONE) {
|
||||
/* 同优先级换事件:只关掉旧灯里新事件用不到的通道 */
|
||||
s_led_set_state((uint16_t)(s_led_priority_table[prio].config.ledMask
|
||||
^ item->config.ledMask),
|
||||
LED_OFF);
|
||||
}
|
||||
}
|
||||
s_led_priority_table[prio] = *item;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 清空优先级表(不改 GPIO,由调用方灭灯)
|
||||
* @return 无
|
||||
*/
|
||||
void LED_EventAllDelete(void)
|
||||
{
|
||||
memset(s_led_priority_table, 0, sizeof(s_led_priority_table));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 删除指定事件并关掉它占用的通道
|
||||
* @param ledEvent 事件号
|
||||
* @return 无
|
||||
*/
|
||||
void LED_EventDelete(LED_Event_t ledEvent)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
for (i = 0; i < PRIORITY_MAX; i++) {
|
||||
if (s_led_priority_table[i].event != ledEvent) {
|
||||
continue;
|
||||
}
|
||||
memset(&s_led_priority_table[i], 0, sizeof(LED_EventTableItem_t));
|
||||
if ((s_led_set_state != NULL) && (ledEvent < LED_EVENT_MAX)) {
|
||||
s_led_set_state(s_led_event_table[ledEvent].config.ledMask, LED_OFF);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 从高优先级往下跑事件
|
||||
* @note handler 返回 1 会拦住更低优先级。红绿要共存时 handler 必须返回 0
|
||||
* @return 无
|
||||
*/
|
||||
void LED_EventHandle(void)
|
||||
{
|
||||
int i;
|
||||
uint8_t ret;
|
||||
|
||||
for (i = (int)PRIORITY_MAX - 1; i >= 0; i--) {
|
||||
if (s_led_priority_table[i].event == LED_EVENT_NONE) {
|
||||
continue;
|
||||
}
|
||||
if (s_led_priority_table[i].ledEventHandler == NULL) {
|
||||
continue;
|
||||
}
|
||||
ret = s_led_priority_table[i].ledEventHandler(&s_led_priority_table[i].config);
|
||||
if (ret) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 是否有事件在跑
|
||||
* @return 1=有,0=无
|
||||
*/
|
||||
uint8_t LED_IsEventActive(void)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
for (i = 0; i < PRIORITY_MAX; i++) {
|
||||
if (s_led_priority_table[i].event != LED_EVENT_NONE) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 指定事件是否挂在优先级表里
|
||||
* @param ledEvent 事件号
|
||||
* @return 1=在,0=不在
|
||||
*/
|
||||
uint8_t LED_IsEventExist(LED_Event_t ledEvent)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
for (i = 0; i < PRIORITY_MAX; i++) {
|
||||
if (s_led_priority_table[i].event == ledEvent) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 按配置刷新一次灯效
|
||||
* @param led 配置,不可为空
|
||||
* @return 1=还在跑,0=次数用完
|
||||
*/
|
||||
uint8_t LED_Update(LED_Config_t *led)
|
||||
{
|
||||
if (led == NULL) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
switch (led->state) {
|
||||
case LED_LIGHT:
|
||||
return led_lighting_update(led);
|
||||
#if (BLINK_ENABLE)
|
||||
case LED_BLINKING:
|
||||
return led_blinking_update(led);
|
||||
#endif
|
||||
#if (ALTERNATING_BLINK_ENABLE)
|
||||
case LED_ALTERNATING_BLINK:
|
||||
return led_alternating_update(led);
|
||||
#endif
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 毫秒时间差,兼容溢出
|
||||
* @param meiosis 起点时间戳
|
||||
* @return 差值 ms
|
||||
*/
|
||||
static uint32_t get_tick_diff(uint32_t meiosis)
|
||||
{
|
||||
uint32_t now;
|
||||
|
||||
if (s_time_get == NULL) {
|
||||
return 0;
|
||||
}
|
||||
now = s_time_get();
|
||||
if (now >= meiosis) {
|
||||
return now - meiosis;
|
||||
}
|
||||
return (0xFFFFFFFFu - meiosis) + now;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 常亮;runNum 按秒计,0=一直亮
|
||||
* @param led 配置
|
||||
* @return 1=还在亮,0=到点灭
|
||||
*/
|
||||
static uint8_t led_lighting_update(LED_Config_t *led)
|
||||
{
|
||||
uint16_t sec_count;
|
||||
|
||||
if (s_led_set_state == NULL) {
|
||||
return 0;
|
||||
}
|
||||
sec_count = (uint16_t)(get_tick_diff(led->currentTime) / 1000u);
|
||||
if ((led->runNum > 0) && (sec_count >= led->runNum)) {
|
||||
s_led_set_state(led->ledMask, LED_OFF);
|
||||
return 0;
|
||||
}
|
||||
s_led_set_state(led->ledMask, LED_ON);
|
||||
return 1;
|
||||
}
|
||||
|
||||
#if (BLINK_ENABLE)
|
||||
/**
|
||||
* @brief 闪烁;runNum 按完整周期计,0=一直闪
|
||||
* @param led 配置
|
||||
* @return 1=还在闪,0=到点灭
|
||||
*/
|
||||
static uint8_t led_blinking_update(LED_Config_t *led)
|
||||
{
|
||||
uint32_t run_cycle;
|
||||
uint16_t blink_count;
|
||||
uint32_t sec_left;
|
||||
|
||||
if (s_led_set_state == NULL) {
|
||||
return 0;
|
||||
}
|
||||
run_cycle = led->blinkParams.onTime + led->blinkParams.offTime;
|
||||
if (run_cycle == 0) {
|
||||
return 0;
|
||||
}
|
||||
blink_count = (uint16_t)(get_tick_diff(led->currentTime) / run_cycle);
|
||||
sec_left = get_tick_diff(led->currentTime) % run_cycle;
|
||||
if ((led->runNum > 0) && (blink_count >= led->runNum)) {
|
||||
s_led_set_state(led->ledMask, LED_OFF);
|
||||
return 0;
|
||||
}
|
||||
if (sec_left <= led->blinkParams.onTime) {
|
||||
s_led_set_state(led->ledMask, led->blinkParams.orDer ? LED_OFF : LED_ON);
|
||||
} else {
|
||||
s_led_set_state(led->ledMask, led->blinkParams.orDer ? LED_ON : LED_OFF);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if (ALTERNATING_BLINK_ENABLE)
|
||||
/**
|
||||
* @brief 两组灯按时间片轮流亮
|
||||
* @param led 配置
|
||||
* @return 1=还在跑,0=到点灭
|
||||
*/
|
||||
static uint8_t led_alternating_update(LED_Config_t *led)
|
||||
{
|
||||
uint16_t blink_count;
|
||||
uint32_t sec_left;
|
||||
uint32_t time_slice;
|
||||
size_t i;
|
||||
|
||||
if (s_led_set_state == NULL) {
|
||||
return 0;
|
||||
}
|
||||
if (led->alternatingBlinkParams.Cycle == 0) {
|
||||
return 0;
|
||||
}
|
||||
blink_count = (uint16_t)(get_tick_diff(led->currentTime)
|
||||
/ led->alternatingBlinkParams.Cycle);
|
||||
sec_left = get_tick_diff(led->currentTime) % led->alternatingBlinkParams.Cycle;
|
||||
if ((led->runNum > 0) && (blink_count >= led->runNum)) {
|
||||
s_led_set_state(led->ledMask, LED_OFF);
|
||||
return 0;
|
||||
}
|
||||
time_slice = led->alternatingBlinkParams.Cycle / ALTERNAT_GROUP;
|
||||
s_led_set_state(led->ledMask, LED_OFF);
|
||||
for (i = 0; i < ALTERNAT_GROUP; i++) {
|
||||
if ((sec_left > (time_slice * i)) && (sec_left <= (time_slice * (i + 1)))) {
|
||||
s_led_set_state(led->alternatingBlinkParams.Group[i], LED_ON);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,120 @@
|
||||
/******************************************************************************
|
||||
* @file led.h
|
||||
* @brief LED 事件框架:按通道掩码独立开关
|
||||
* @author cyWu <1917507415@qq.com>
|
||||
* @date 2026.08.31
|
||||
* @version V1.2.0
|
||||
* @history
|
||||
* - V1.0.0, 2026.08.25, cyWu, 首次发布,实现按通道掩码独立开关的 LED 事件框架
|
||||
* - V1.1.0, 2026.08.27, cyWu, 新增 LED_EVENT_BOOT_REJECT
|
||||
* - V1.2.0, 2026.08.31, cyWu, CBR3 单灯:裁掉充电/低电/空电等门夹事件
|
||||
******************************************************************************/
|
||||
|
||||
#ifndef __LED_H__
|
||||
#define __LED_H__
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
/*============================================================================*/
|
||||
/* 宏定义 */
|
||||
/*============================================================================*/
|
||||
|
||||
#define BLINK_ENABLE 1
|
||||
#define ALTERNATING_BLINK_ENABLE 0
|
||||
#define BREATH_ENABLE 0
|
||||
#define RUNNING_ENABLE 0
|
||||
#define MARQUEE_ENABLE 0
|
||||
|
||||
#define LED_ON 1
|
||||
#define LED_OFF 0
|
||||
|
||||
#define LED_CHANNEL(user_led) (1u << (user_led))
|
||||
|
||||
#define ledPrintf(level, fmt, ...) ((void)0)
|
||||
|
||||
/*============================================================================*/
|
||||
/* 类型 */
|
||||
/*============================================================================*/
|
||||
|
||||
typedef enum {
|
||||
LED_LIGHT = 0, /**< 常亮 */
|
||||
LED_BLINKING, /**< 闪烁 */
|
||||
LED_ALTERNATING_BLINK /**< 交替闪(本产品未用) */
|
||||
} LED_State_t;
|
||||
|
||||
typedef enum {
|
||||
LED_EVENT_NONE = 0,
|
||||
LED_EVENT_STANDBY, /**< 开机常亮 */
|
||||
LED_EVENT_PAIRING, /**< 配对快闪 */
|
||||
LED_EVENT_PAIR_OK, /**< 配对成功常亮若干秒 */
|
||||
LED_EVENT_PAIR_FAIL, /**< 配对失败闪若干秒 */
|
||||
LED_EVENT_OTA, /**< OTA 闪,直到升级结束/复位 */
|
||||
LED_EVENT_MAX
|
||||
} LED_Event_t;
|
||||
|
||||
/**
|
||||
* 同优先级会互相覆盖
|
||||
*/
|
||||
typedef enum {
|
||||
PRIORITY_1 = 0, /**< 开机常亮 */
|
||||
PRIORITY_2, /**< 配对中 / 配对结果 */
|
||||
PRIORITY_3, /**< OTA */
|
||||
PRIORITY_MAX
|
||||
} LED_EventPriority_t;
|
||||
|
||||
#if (BLINK_ENABLE)
|
||||
typedef struct {
|
||||
uint8_t orDer; /**< 1=先灭后亮,0=先亮后灭 */
|
||||
uint32_t onTime; /**< 亮时间 ms */
|
||||
uint32_t offTime; /**< 灭时间 ms */
|
||||
} LED_BlinkParams_t;
|
||||
#endif
|
||||
|
||||
#if (ALTERNATING_BLINK_ENABLE)
|
||||
#define ALTERNAT_GROUP 2
|
||||
typedef struct {
|
||||
uint16_t Group[ALTERNAT_GROUP]; /**< 交替两组的通道掩码 */
|
||||
uint32_t Cycle; /**< 一整轮交替周期 ms */
|
||||
} LED_AlternatingBlinkParams_t;
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
LED_State_t state;
|
||||
uint16_t ledMask; /**< 本事件只动这些通道,其它灯不受影响 */
|
||||
uint16_t runNum; /**< 0=无限;常亮按秒计,闪烁按周期计 */
|
||||
uint32_t currentTime;
|
||||
#if (BLINK_ENABLE)
|
||||
LED_BlinkParams_t blinkParams;
|
||||
#endif
|
||||
#if (ALTERNATING_BLINK_ENABLE)
|
||||
LED_AlternatingBlinkParams_t alternatingBlinkParams;
|
||||
#endif
|
||||
} LED_Config_t;
|
||||
|
||||
typedef struct {
|
||||
LED_Event_t event;
|
||||
LED_EventPriority_t priority;
|
||||
LED_Config_t config;
|
||||
uint8_t (*ledEventHandler)(LED_Config_t *config);
|
||||
} LED_EventTableItem_t;
|
||||
|
||||
typedef void (*LED_SetState_t)(uint16_t led, uint8_t state);
|
||||
typedef uint32_t (*TIME_Get_t)(void);
|
||||
|
||||
/*============================================================================*/
|
||||
/* 外部接口 */
|
||||
/*============================================================================*/
|
||||
|
||||
void LED_SetStateInit(LED_SetState_t setState);
|
||||
void SET_TimeGetInit(TIME_Get_t getTime);
|
||||
void LED_EventTableInit(LED_EventTableItem_t ledEvent[]);
|
||||
void LED_EventAdd(LED_Event_t ledEvent);
|
||||
void LED_EventDelete(LED_Event_t ledEvent);
|
||||
void LED_EventAllDelete(void);
|
||||
void LED_EventHandle(void);
|
||||
uint8_t LED_IsEventActive(void);
|
||||
uint8_t LED_IsEventExist(LED_Event_t ledEvent);
|
||||
uint8_t LED_Update(LED_Config_t *led);
|
||||
|
||||
#endif /* __LED_H__ */
|
||||
@@ -0,0 +1,193 @@
|
||||
/******************************************************************************
|
||||
* @file led_manager.c
|
||||
* @brief CBR3 灯事件:单灯 PC2,高优先级覆盖低优先级
|
||||
* @author cyWu <1917507415@qq.com>
|
||||
* @date 2026.08.29
|
||||
* @version V1.0.0
|
||||
* @history
|
||||
* - V1.0.0, 2026.08.29, cyWu, 首次发布(移植门夹,删红绿双通道改单灯)
|
||||
*
|
||||
* @note 灯效优先级(见规格书第 6 章,高者覆盖):
|
||||
* OTA(200ms 闪) > 配对中(100ms 快闪) / 配对结果 > 正常开机常亮。
|
||||
* 单灯无充电/低电/空电事件。
|
||||
******************************************************************************/
|
||||
|
||||
#include "led_manager.h"
|
||||
#include "system/includes.h"
|
||||
#include "bsp_hw.h"
|
||||
#include "board_pin.h"
|
||||
|
||||
static uint8_t led_keep_handler(LED_Config_t *led_config);
|
||||
static uint8_t led_standby_handler(LED_Config_t *led_config);
|
||||
static uint8_t led_pairing_handler(LED_Config_t *led_config);
|
||||
static uint8_t led_pair_ok_handler(LED_Config_t *led_config);
|
||||
static uint8_t led_pair_fail_handler(LED_Config_t *led_config);
|
||||
static uint8_t led_ota_handler(LED_Config_t *led_config);
|
||||
|
||||
/**
|
||||
* @brief 只改掩码里的通道(单灯即唯一通道)
|
||||
* @param led 通道掩码
|
||||
* @param state LED_ON / LED_OFF
|
||||
* @return 无
|
||||
*/
|
||||
static void set_led_multi(uint16_t led, uint8_t state)
|
||||
{
|
||||
if (led & LED_CHANNEL(LED_CH_ON)) {
|
||||
bsp_led_set(state);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 常亮/闪烁一直跑:返回 1 独占,单灯不允许叠加
|
||||
* @param led_config 配置
|
||||
* @return 1
|
||||
*/
|
||||
static uint8_t led_keep_handler(LED_Config_t *led_config)
|
||||
{
|
||||
(void)LED_Update(led_config);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 待机常亮;runNum>0 时亮满后删事件
|
||||
* @param led_config 配置
|
||||
* @return 1
|
||||
*/
|
||||
static uint8_t led_standby_handler(LED_Config_t *led_config)
|
||||
{
|
||||
if (LED_Update(led_config) == 0) {
|
||||
LED_EventDelete(LED_EVENT_STANDBY);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 配对快闪(无限)
|
||||
* @param led_config 配置
|
||||
* @return 1
|
||||
*/
|
||||
static uint8_t led_pairing_handler(LED_Config_t *led_config)
|
||||
{
|
||||
(void)LED_Update(led_config);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 配对成功常亮若干秒后删事件
|
||||
* @param led_config 配置
|
||||
* @return 1
|
||||
*/
|
||||
static uint8_t led_pair_ok_handler(LED_Config_t *led_config)
|
||||
{
|
||||
if (LED_Update(led_config) == 0) {
|
||||
LED_EventDelete(LED_EVENT_PAIR_OK);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 配对失败 200ms 闪若干秒后删事件
|
||||
* @param led_config 配置
|
||||
* @return 1
|
||||
*/
|
||||
static uint8_t led_pair_fail_handler(LED_Config_t *led_config)
|
||||
{
|
||||
if (LED_Update(led_config) == 0) {
|
||||
LED_EventDelete(LED_EVENT_PAIR_FAIL);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief OTA 200ms 闪(无限):最高优先级
|
||||
* @param led_config 配置
|
||||
* @return 1
|
||||
*/
|
||||
static uint8_t led_ota_handler(LED_Config_t *led_config)
|
||||
{
|
||||
(void)LED_Update(led_config);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 注册回调并装载 CBR3 事件表(GPIO 已在 bsp_hw_init)
|
||||
* @return 无
|
||||
*/
|
||||
void user_led_init(void)
|
||||
{
|
||||
static LED_EventTableItem_t led_event_category[LED_EVENT_MAX] = {
|
||||
[LED_EVENT_STANDBY] = {
|
||||
/* 正常开机常亮 */
|
||||
.event = LED_EVENT_STANDBY,
|
||||
.priority = PRIORITY_1,
|
||||
.config.ledMask = LED_CHANNEL(LED_CH_ON),
|
||||
.config.state = LED_LIGHT,
|
||||
#if BOARD_LED_STANDBY_ON
|
||||
.config.runNum = 0, /* 0=一直亮 */
|
||||
#else
|
||||
.config.runNum = 1,
|
||||
#endif
|
||||
.ledEventHandler = led_standby_handler,
|
||||
},
|
||||
[LED_EVENT_PAIRING] = {
|
||||
/* 配对中 100ms 快闪(无限) */
|
||||
.event = LED_EVENT_PAIRING,
|
||||
.priority = PRIORITY_2,
|
||||
.config.ledMask = LED_CHANNEL(LED_CH_ON),
|
||||
.config.state = LED_BLINKING,
|
||||
.config.blinkParams.orDer = 0,
|
||||
.config.blinkParams.onTime = BOARD_LED_FAST_ON_MS,
|
||||
.config.blinkParams.offTime = BOARD_LED_FAST_OFF_MS,
|
||||
.config.runNum = 0,
|
||||
.ledEventHandler = led_pairing_handler,
|
||||
},
|
||||
[LED_EVENT_PAIR_OK] = {
|
||||
/* 配对成功常亮若干秒(允许直接回到常亮) */
|
||||
.event = LED_EVENT_PAIR_OK,
|
||||
.priority = PRIORITY_2,
|
||||
.config.ledMask = LED_CHANNEL(LED_CH_ON),
|
||||
.config.state = LED_LIGHT,
|
||||
.config.runNum = (uint16_t)((BOARD_LED_PAIR_OK_MS + 999u) / 1000u),
|
||||
.ledEventHandler = led_pair_ok_handler,
|
||||
},
|
||||
[LED_EVENT_PAIR_FAIL] = {
|
||||
/* 配对失败 200ms 闪提示若干秒 */
|
||||
.event = LED_EVENT_PAIR_FAIL,
|
||||
.priority = PRIORITY_2,
|
||||
.config.ledMask = LED_CHANNEL(LED_CH_ON),
|
||||
.config.state = LED_BLINKING,
|
||||
.config.blinkParams.orDer = 0,
|
||||
.config.blinkParams.onTime = BOARD_LED_OTA_ON_MS,
|
||||
.config.blinkParams.offTime = BOARD_LED_OTA_OFF_MS,
|
||||
.config.runNum = (uint16_t)(BOARD_LED_PAIR_FAIL_MS
|
||||
/ (BOARD_LED_OTA_ON_MS + BOARD_LED_OTA_OFF_MS)),
|
||||
.ledEventHandler = led_pair_fail_handler,
|
||||
},
|
||||
[LED_EVENT_OTA] = {
|
||||
/* OTA:200ms 闪一直闪,直到升级结束或复位 */
|
||||
.event = LED_EVENT_OTA,
|
||||
.priority = PRIORITY_3,
|
||||
.config.ledMask = LED_CHANNEL(LED_CH_ON),
|
||||
.config.state = LED_BLINKING,
|
||||
.config.blinkParams.orDer = 0,
|
||||
.config.blinkParams.onTime = BOARD_LED_OTA_ON_MS,
|
||||
.config.blinkParams.offTime = BOARD_LED_OTA_OFF_MS,
|
||||
.config.runNum = 0, /* 0=无限 */
|
||||
.ledEventHandler = led_ota_handler,
|
||||
},
|
||||
};
|
||||
|
||||
LED_SetStateInit(set_led_multi);
|
||||
SET_TimeGetInit((TIME_Get_t)timer_get_ms);
|
||||
LED_EventTableInit(led_event_category);
|
||||
bsp_led_set(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 跑一轮 LED 事件
|
||||
* @return 无
|
||||
*/
|
||||
void user_led_handle(void)
|
||||
{
|
||||
LED_EventHandle();
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/******************************************************************************
|
||||
* @file led_manager.h
|
||||
* @brief CBR3 单 LED(PC2)通道与事件表
|
||||
* @author cyWu <1917507415@qq.com>
|
||||
* @date 2026.08.29
|
||||
* @version V1.0.0
|
||||
* @history
|
||||
* - V1.0.0, 2026.08.29, cyWu, 首次发布(单灯版,删红绿双通道)
|
||||
******************************************************************************/
|
||||
|
||||
#ifndef __LED_MANAGER_H__
|
||||
#define __LED_MANAGER_H__
|
||||
|
||||
#include "led.h"
|
||||
|
||||
typedef enum {
|
||||
LED_CH_ON = 0, /**< 单灯唯一通道 */
|
||||
LED_CH_MAX
|
||||
} LedChannel_t;
|
||||
|
||||
/**
|
||||
* @brief 注册 GPIO 回调并装载 CBR3 事件表
|
||||
* @return 无
|
||||
*/
|
||||
void user_led_init(void);
|
||||
|
||||
/**
|
||||
* @brief 跑一轮 LED 事件
|
||||
* @return 无
|
||||
*/
|
||||
void user_led_handle(void);
|
||||
|
||||
#endif /* __LED_MANAGER_H__ */
|
||||
Reference in New Issue
Block a user