P0/P1/P2功能实现:接入业务组合根(按键/LED/过零/配网/OTA),联动、11B定时、运行记录、灯泡寿命(DP3/DP4/DP25)、CBR0 VM、512分包

This commit is contained in:
2026-09-08 11:44:20 +08:00
parent 61821de0f8
commit 426bb8dab6
52 changed files with 6042 additions and 313 deletions
+113
View File
@@ -0,0 +1,113 @@
/******************************************************************************
* @file app_arbiter.c
* @brief 加热占用仲裁:手动 > 定时 > 联动
* @author cyWu <1917507415@qq.com>
* @date 2026.09.04
* @version V1.1.0
* @history
* - V1.1.0, 2026.09.04, cyWu, 精简注释(单标志不包结构体)
* - V1.0.0, 2026.09.04, cyWu, 由风扇仲裁器改为继电器开关
******************************************************************************/
#include "system/includes.h"
#include "app_arbiter.h"
#include "app_linkage.h"
#define LOG_TAG_CONST APP
#define LOG_TAG "[ARB]"
#define LOG_INFO_ENABLE
#include "debug.h"
static uint8_t s_owner;
/**
* @brief 初始化仲裁器
* @return 无
*/
void app_arbiter_init(void)
{
s_owner = ARB_OWNER_NONE;
log_info("arbiter init\n");
}
/**
* @brief 当前加热占用者
* @return ArbOwner_t
*/
uint8_t app_arbiter_get_owner(void)
{
return s_owner;
}
/**
* @brief 短按 / DP0:清占用并作废本轮联动
* @return 无
*/
void app_arbiter_manual_intervene(void)
{
if (s_owner != ARB_OWNER_NONE) {
log_info("manual intervene, owner %d -> none\n", s_owner);
}
s_owner = ARB_OWNER_NONE;
app_linkage_void();
}
/**
* @brief 联动引擎尝试占用
* @return 1=成功;0=定时占用中(12 章:定时窗内联动不插嘴)
*/
uint8_t app_arbiter_linkage_take(void)
{
if (s_owner == ARB_OWNER_TIMER) {
return 0;
}
if (s_owner != ARB_OWNER_LINKAGE) {
log_info("linkage take\n");
}
s_owner = ARB_OWNER_LINKAGE;
return 1;
}
/**
* @brief 联动是否仍在占用
* @return 1=owner==LINKAGE
*/
uint8_t app_arbiter_linkage_active(void)
{
return (s_owner == ARB_OWNER_LINKAGE) ? 1 : 0;
}
/**
* @brief 联动自然失效:让出占用(请求关由调用方发)
* @return 无
*/
void app_arbiter_linkage_release(void)
{
if (s_owner == ARB_OWNER_LINKAGE) {
s_owner = ARB_OWNER_NONE;
}
}
/**
* @brief 定时窗开始占用(优先级高于联动)
* @return 1=成功
*/
uint8_t app_arbiter_timer_take(void)
{
if (s_owner != ARB_OWNER_TIMER) {
log_info("timer take\n");
}
s_owner = ARB_OWNER_TIMER;
return 1;
}
/**
* @brief 定时窗结束释放(调用方负责收口:请求关 + 立刻按 10.3 补判联动)
* @return 无
*/
void app_arbiter_timer_release(void)
{
if (s_owner == ARB_OWNER_TIMER) {
s_owner = ARB_OWNER_NONE;
}
}
+68
View File
@@ -0,0 +1,68 @@
/******************************************************************************
* @file app_arbiter.h
* @brief 加热占用仲裁:手动 > 定时 > 联动(规格书第 12 章)
* @author cyWu <1917507415@qq.com>
* @date 2026.09.04
* @version V1.1.0
******************************************************************************/
#ifndef __APP_ARBITER_H__
#define __APP_ARBITER_H__
#include <stdint.h>
typedef enum {
ARB_OWNER_NONE = 0, /**< 空闲 / 手动接管 */
ARB_OWNER_LINKAGE = 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 短按 / DP0:清占用并作废本轮联动
* @return 无
*/
void app_arbiter_manual_intervene(void);
/**
* @brief 联动尝试占用
* @return 1=成功;0=定时占用中
*/
uint8_t app_arbiter_linkage_take(void);
/**
* @brief 联动是否仍在占用
* @return 1=是
*/
uint8_t app_arbiter_linkage_active(void);
/**
* @brief 联动自然失效,让出占用
* @return 无
*/
void app_arbiter_linkage_release(void);
/**
* @brief 定时窗开始占用
* @return 1=成功
*/
uint8_t app_arbiter_timer_take(void);
/**
* @brief 定时窗结束释放
* @return 无
*/
void app_arbiter_timer_release(void);
#endif /* __APP_ARBITER_H__ */
+57
View File
@@ -0,0 +1,57 @@
/******************************************************************************
* @file app_defer.c
* @brief ISR→任务上下文的延后执行标志位实现
* @author cyWu <1917507415@qq.com>
* @date 2026.08.31
* @version V1.0.0,首次发布
******************************************************************************/
#include "system/includes.h"
#include "app_defer.h"
/** 位掩码,volatilerequest() 在中断上下文写,take()/is_pending()/cancel()
* 在任务上下文读写,两侧都用 local_irq_disable/enable 包一下读改写,
* 避免"任务侧正在清 A 位时中断侧设 B 位"这种读改写竞态把 B 位冲丢。 */
static volatile uint8_t s_flags;
void app_defer_request(AppDeferBit_t bit)
{
if (bit >= APP_DEFER_MAX) {
return;
}
local_irq_disable();
s_flags |= (uint8_t)(1u << bit);
local_irq_enable();
}
uint8_t app_defer_take(AppDeferBit_t bit)
{
uint8_t was_set;
if (bit >= APP_DEFER_MAX) {
return 0;
}
local_irq_disable();
was_set = (s_flags & (uint8_t)(1u << bit)) ? 1 : 0;
s_flags &= (uint8_t)~(1u << bit);
local_irq_enable();
return was_set;
}
uint8_t app_defer_is_pending(AppDeferBit_t bit)
{
if (bit >= APP_DEFER_MAX) {
return 0;
}
return (s_flags & (uint8_t)(1u << bit)) ? 1 : 0;
}
void app_defer_cancel(AppDeferBit_t bit)
{
if (bit >= APP_DEFER_MAX) {
return;
}
local_irq_disable();
s_flags &= (uint8_t)~(1u << bit);
local_irq_enable();
}
+59
View File
@@ -0,0 +1,59 @@
/******************************************************************************
* @file app_defer.h
* @brief ISR→任务上下文的延后执行标志位(统一收口 2026-08-31 复位事故教训)
* @author cyWu <1917507415@qq.com>
* @date 2026.08.31
* @version V1.0.0,首次发布
*
* @note ⚠ 硬约束:flash 擦写(syscfg_write)、BLE 广播切换、关机收口等"重活"
* 禁止在 usr_timer_add 的中断上下文回调(本产品 20ms usr_timer_tick /
* 按键长按回调)里直接执行,只能在 usr_timer_loopBLE 线程,任务
* 上下文,栈充足)里执行。之前每个模块各自发明一个 pending 标志
* off_pending/pair_pending/ota_pending/nvm 的 s_need_save),
* 容易被后来的功能遗漏这条红线——本模块把"打标志"这一步收成统一
* 接口:ISR 只准调 app_defer_request(),真正执行永远在
* app_function_loop() 里用 app_defer_take() 消费。
******************************************************************************/
#ifndef __APP_DEFER_H__
#define __APP_DEFER_H__
#include <stdint.h>
/** 延后动作位,新增一种"需要写flash/切广播/关机"的动作就加一项 */
typedef enum {
APP_DEFER_NVM_SAVE = 0, /**< VM 防抖到点,需要落盘 */
APP_DEFER_PAIR, /**< 长按 5s 松手,需要清绑定进配对 */
APP_DEFER_OTA, /**< 长按 12s 到点,需要进 OTA */
APP_DEFER_MAX
} AppDeferBit_t;
/**
* @brief 中断上下文调用:只置位,不做任何实际动作
* @param bit 要延后的动作
* @return 无
*/
void app_defer_request(AppDeferBit_t bit);
/**
* @brief 任务上下文调用:查询并清除标志(取出即消费,最多处理一次)
* @param bit 要消费的动作
* @return 1=之前挂起过(现在可以执行真正动作),0=没挂起
*/
uint8_t app_defer_take(AppDeferBit_t bit);
/**
* @brief 任务上下文调用:只查询是否挂起,不清除(多个动作互斥判断用)
* @param bit 要查询的动作
* @return 1=挂起中,0=未挂起
*/
uint8_t app_defer_is_pending(AppDeferBit_t bit);
/**
* @brief 任务上下文调用:撤销挂起(如 OTA 触发时要撤销配对)
* @param bit 要撤销的动作
* @return 无
*/
void app_defer_cancel(AppDeferBit_t bit);
#endif /* __APP_DEFER_H__ */
+178
View File
@@ -0,0 +1,178 @@
/******************************************************************************
* @file app_function.c
* @brief 功能总装:短按切灯、任务上下文落地配对/OTA
* @author cyWu <1917507415@qq.com>
* @date 2026.09.04
* @version V1.2.0
* @history
* - V1.2.0, 2026.09.08, cyWu, 灯罩适配:出厂复位顺带复位寿命、定时 11B
* - V1.1.0, 2026.09.04, cyWu, 精简注释
* - V1.0.0, 2026.09.04, cyWu, 首次发布
******************************************************************************/
#include "system/includes.h" /* 必须先于 app_main.hu8/u16/u32 等类型由 SDK 定义 */
#include "app_function.h"
#include "app_pair.h"
#include "app_relay.h"
#include "app_zc.h"
#include "app_arbiter.h"
#include "app_nvm.h"
#include "app_linkage.h"
#include "app_timer_task.h"
#include "app_runrec.h"
#include "app_defer.h"
#include "app_led.h"
#include "app_light_life.h"
#include "app_main.h"
#include "usr_le_api.h"
#include "jb_product.h"
#include "board_pin.h"
#include <string.h>
#define LOG_TAG_CONST APP
#define LOG_TAG "[APP_FUNC]"
#define LOG_INFO_ENABLE
#include "debug.h"
/**
* @brief 初始化功能模块
* @return 无
*/
void app_function_init(void)
{
}
/**
* @brief 任务上下文周期处理(配对 / OTA / 写 VM 只准在此落地)
* @return 无
*/
void app_function_loop(void)
{
uint8_t zc_evt = app_zc_consume_timeout();
if (zc_evt == 1) {
log_info("zc timeout 100ms: open request dropped, stay off, fault=2\n");
} else if (zc_evt == 2) {
log_info("zc timeout 100ms: force relay off, fault=2\n");
}
app_nvm_flush();
if (app_defer_take(APP_DEFER_OTA)) {
app_defer_cancel(APP_DEFER_PAIR);
usr_app_ota_init();
app_led_request_ota();
log_info("key hold 12s -> ota\n");
} else if (app_defer_take(APP_DEFER_PAIR)) {
usr_goto_pair_mode();
app_pair_start();
app_led_clear_pair_hint();
log_info("key hold 5s -> pair\n");
}
}
/**
* @brief 同步 DP0 = 继电器实际态
* @return 无
*/
void app_function_poll(void)
{
gDevData.heat_switch = app_relay_get_actual();
}
/**
* @brief 短按切换灯泡请求(OTA 中忽略)
* @return 无
*/
void usr_on_key_short(void)
{
uint8_t req;
if (app_led_is_ota()) {
return;
}
app_arbiter_manual_intervene();
req = app_relay_get_request() ? 0 : 1;
app_relay_request(req);
log_info("key short light request=%d\n", req);
}
/**
* @brief 按住满 5s:配对提示灯(ISR 只动 RAM)
* @return 无
*/
void usr_on_key_pair_hint(void)
{
if (app_led_is_ota()) {
return;
}
app_led_hint_pair();
log_info("key hold 5s, pair hint (release to pair, keep to 12s for ota)\n");
}
/**
* @brief 按满 5s 后松手:置配对标志(写 VM 在 loop)
* @return 无
*/
void usr_on_key_pair_start(void)
{
if (app_led_is_ota()) {
return;
}
app_defer_request(APP_DEFER_PAIR);
log_info("key hold 5s released, pair pending\n");
}
/**
* @brief 按住满 12s:置 OTA 标志(BLE 切换在 loop
* @return 无
*/
void usr_on_key_ota_start(void)
{
if (app_led_is_ota()) {
return;
}
app_defer_request(APP_DEFER_OTA);
log_info("key hold 12s, ota pending\n");
}
/**
* @brief 出厂重置:清业务 VM/RAM,保留 BLE 配对(规格书 14 章)
* @note VM 由 app_nvm_reset 全清(含 4 个 item);RAM 清灯泡请求、
* 联动 20B、定时 7 组(11B)、31 天运行记录与各使能;剩余寿命回额定
* 2,592,000、告警清 0app_light_life_reset 立刻写回 VM)。
* 不断开、不软复位。
* @return 无
*/
void app_function_factory_reset(void)
{
uint8_t i;
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_relay_request(0);
app_arbiter_init();
app_linkage_init();
app_timer_init();
app_runrec_reset();
app_light_life_reset(); /* 寿命回额定 + 告警 0 + 立刻落盘(14 章/13.2 边沿) */
gDevData.heat_switch = 0;
gDevData.factory_reset = 0;
gDevData.linkage_delete = 0;
gDevData.time_en = 0;
gDevData.fault_en = 0;
gDevData.alarm_en = 0;
gDevData.fault_code = 0;
gDevData.alarm_code = 0;
memset(gDevData.linkage_config, 0, BOARD_LINKAGE_CFG_LEN);
memset(gDevData.run_record, 0, sizeof(gDevData.run_record));
for (i = 0; i < 7; i++) {
memset(tm[i], 0, BOARD_TIME_CFG_LEN);
}
log_info("factory reset: business vm cleared, pairing kept\n");
}
+68
View File
@@ -0,0 +1,68 @@
/******************************************************************************
* @file app_function.h
* @brief 功能总装:按键动作 + DP 同步(配对/OTA 只在任务上下文落地)
* @author cyWu <1917507415@qq.com>
* @date 2026.09.04
* @version V1.1.0
******************************************************************************/
#ifndef __APP_FUNCTION_H__
#define __APP_FUNCTION_H__
#include <stdint.h>
/**
* @brief 初始化功能模块状态
* @return 无
*/
void app_function_init(void);
/**
* @brief 同步 DPDP0=继电器实际态、DP23=fault_code),每个 20ms 业务 tick 调用
* @note DP0 上报**实际吸合状态**(过零完成之后),让 APP 看见过零完成
* @return 无
*/
void app_function_poll(void);
/**
* @brief 任务上下文周期处理(usr_timer_loop 500ms
* @note 写 VM / 配对 / OTA 只准在此落地,禁止放进 20ms ISR
* @return 无
*/
void app_function_loop(void);
/**
* @brief 业务层短按回调(key_manager 触发):切换灯泡请求(开↔关,手动抢权)
* @return 无
*/
void usr_on_key_short(void);
/**
* @brief 业务层"按住满 5s"回调(key_manager 触发,20ms 中断上下文)
* @note 只切提示灯(绿灯快闪),不清绑定
* @return 无
*/
void usr_on_key_pair_hint(void);
/**
* @brief 业务层"按满 5s 后松手"回调(key_manager 触发,20ms 中断上下文)
* @note 只置标志:清绑定要写 VM,由 app_function_loop 在任务上下文执行
* @return 无
*/
void usr_on_key_pair_start(void);
/**
* @brief 业务层"按住满 12s"回调(key_manager 触发,20ms 中断上下文)
* @note 只置标志:usr_app_ota_init 由 app_function_loop 在任务上下文执行
* @return 无
*/
void usr_on_key_ota_start(void);
/**
* @brief 出厂重置(DP26 写 1):清业务 VM 与 RAM(含寿命回额定),保留 BLE 配对
* @note 规格书第 14 章;由 jb_product DP26 调用
* @return 无
*/
void app_function_factory_reset(void);
#endif /* __APP_FUNCTION_H__ */
+190
View File
@@ -0,0 +1,190 @@
/******************************************************************************
* @file app_led.c
* @brief 灯效:跟继电器实际态;OTA / 配对优先(规格书第 6 章)
* @author cyWu <1917507415@qq.com>
* @date 2026.09.04
* @version V1.1.0
* @history
* - V1.1.0, 2026.09.04, cyWu, 精简注释
* - V1.0.0, 2026.09.04, cyWu, 首次发布
******************************************************************************/
#include "app_led.h"
#include "led_manager.h"
#include "bsp_hw.h"
#include "board_pin.h"
#include "app_pair.h"
#include "app_relay.h"
#include "system/includes.h"
#define LOG_TAG_CONST APP
#define LOG_TAG "[APP_LED]"
#define LOG_INFO_ENABLE
#include "debug.h"
typedef struct {
uint8_t inited; /**< 1=已初始化 */
uint8_t ota; /**< 1=OTA 灯效中,app_led_run 只跑交替 */
uint8_t pair_hint; /**< 1=长按已满 5s 但还没松手提交配对,维持配对快闪 */
} AppLedCtx_t;
static AppLedCtx_t s_led;
/**
* @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_led.inited) {
return APP_LED_OK;
}
#if BOARD_LED_ENABLE
user_led_init();
s_led.ota = 0;
s_led.pair_hint = 0;
s_led.inited = 1;
#else
log_info("led disabled\n");
#endif
return APP_LED_OK;
}
/**
* @brief 查询模块是否已初始化
* @return APP_LED_OK=已初始化,APP_LED_ERR_NOT_INIT=未初始化
*/
AppLed_Ret_t app_led_get_status(void)
{
return s_led.inited ? APP_LED_OK : APP_LED_ERR_NOT_INIT;
}
/**
* @brief 长按满 5s:绿灯 100ms 快闪提示(红灯灭)
* @return 无
*/
void app_led_hint_pair(void)
{
if (!s_led.inited || s_led.ota) {
return;
}
s_led.pair_hint = 1;
LED_EventAllDelete(); /* 清掉跟继电器灯,避免红绿混色 */
bsp_led_all_off();
LED_EventAdd(LED_EVENT_PAIRING);
}
/**
* @brief 结束"将配网"提示,交回 app_led_run 按 app_pair 状态接管
* @return 无
*/
void app_led_clear_pair_hint(void)
{
s_led.pair_hint = 0;
}
/**
* @brief 进入 OTA:清掉其它灯,挂红绿交替一直闪
* @return 无
*/
void app_led_request_ota(void)
{
if (!s_led.inited) {
return;
}
s_led.ota = 1;
s_led.pair_hint = 0; /* 已改判 OTA,撤销配网提示 */
LED_EventAllDelete();
bsp_led_all_off();
LED_EventAdd(LED_EVENT_OTA);
}
/**
* @brief 查询是否处于 OTA 灯效
* @return 1=OTA 灯效中,0=否
*/
uint8_t app_led_is_ota(void)
{
return s_led.ota;
}
/**
* @brief 按优先级刷新灯(20ms 业务节拍)
* @param dt 距上次调用的毫秒数
* @return 无
*/
void app_led_run(uint16_t dt)
{
AppPairEvt_t pair_evt;
if (!s_led.inited) {
return;
}
#if BOARD_LED_ENABLE
/* OTA 期间只跑红绿交替 */
if (s_led.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_HEAT_ON);
LED_EventDelete(LED_EVENT_HEAT_OFF);
LED_EventDelete(LED_EVENT_PAIRING);
led_restart(LED_EVENT_PAIR_OK);
log_info("led pair ok 3s\n");
} else if (pair_evt == APP_PAIR_EVT_FAIL) {
LED_EventDelete(LED_EVENT_HEAT_ON);
LED_EventDelete(LED_EVENT_HEAT_OFF);
LED_EventDelete(LED_EVENT_PAIRING);
led_restart(LED_EVENT_PAIR_FAIL);
log_info("led pair fail 3s\n");
}
/* 配对提示 / 配对中:维持绿灯快闪(add 幂等,不重置计时) */
if (s_led.pair_hint || app_pair_is_active()) {
if (!LED_IsEventExist(LED_EVENT_PAIR_OK) &&
!LED_IsEventExist(LED_EVENT_PAIR_FAIL)) {
LED_EventAdd(LED_EVENT_PAIRING);
}
} else if (!LED_IsEventExist(LED_EVENT_PAIR_OK) &&
!LED_IsEventExist(LED_EVENT_PAIR_FAIL)) {
/* 无高事件:灯跟继电器实际态(过零完成之后) */
if (app_relay_get_actual()) {
LED_EventAdd(LED_EVENT_HEAT_ON);
} else {
LED_EventAdd(LED_EVENT_HEAT_OFF);
}
}
user_led_handle();
#endif
}
/**
* @brief 清空全部灯事件并灭灯
* @return 无
*/
void app_led_clear(void)
{
#if BOARD_LED_ENABLE
LED_EventAllDelete();
bsp_led_all_off();
#endif
}
+71
View File
@@ -0,0 +1,71 @@
/******************************************************************************
* @file app_led.h
* @brief 灯效:跟继电器实际态;OTA / 配对优先(规格书第 6 章)
* @author cyWu <1917507415@qq.com>
* @date 2026.09.04
* @version V1.1.0
******************************************************************************/
#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 按优先级刷新灯(每 20ms 业务节拍调用)
* @param dt 距上次调用的毫秒数
* @note 内部读继电器实际态挂 HEAT 事件;同时推进配对会话计时
* app_pair_tick_1ms)并消费配对成功/失败边沿事件。
* @return 无
*/
void app_led_run(uint16_t dt);
/**
* @brief 长按满 5s 的"松手即配网"提示:绿灯 100ms 快闪(红灯灭)
* @note 此时还没清绑定(要等松手),只是给按住的用户反馈
* @return 无
*/
void app_led_hint_pair(void);
/**
* @brief 结束"将配网"提示(配对会话已由 app_pair 接管,或改判为 OTA
* @return 无
*/
void app_led_clear_pair_hint(void);
/**
* @brief 进入 OTA:清掉其它灯,挂红绿交替一直闪(最高优先级)
* @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__ */
+207
View File
@@ -0,0 +1,207 @@
/******************************************************************************
* @file app_light_life.c
* @brief 灯泡剩余寿命(规格书 13.2 节):实际吸合扣秒 / DP4 复位 /
* 到期告警 / DP3 60s 节流上报 / 边沿立刻落盘
* @author cyWu <1917507415@qq.com>
* @date 2026.09.08
* @version V1.0.0
*
* @note 扣秒与运行记录同口径:只看 app_relay_get_actual()==1(过零后已吸合)。
* 上下文分工(避免 ISR 写 VM / 切 BLE):
* - tick20ms ISR):只折秒到 s_life.remain,标记 need_sync /
* need_edge(到期),不碰 gDevData / flash。
* - loop500ms 任务):60s 节流同步 gDevData.lighe_remain_time
* 边沿立刻同步 + 推导 gDevData.alarm_code + 立刻落盘。
* BOARD_LIGHT_LIFE_ENABLE=0:不扣秒、不置到期告警;DP3 走 VM/默认
* 值原样上报(规格书 0.4 条)。
******************************************************************************/
#include <string.h>
#include "system/includes.h" /* local_irq_disable / local_irq_enable */
#include "app_light_life.h"
#include "app_relay.h"
#include "app_nvm.h"
#include "jb_product.h"
#include "board_pin.h"
#define LOG_TAG_CONST APP
#define LOG_TAG "[LIGHT]"
#define LOG_INFO_ENABLE
#include "debug.h"
/** 剩余寿命运行上下文 */
typedef struct {
volatile uint32_t remain; /**< 权威剩余寿命秒(ISR 扣减,任务读) */
volatile uint16_t ms; /**< 不足 1 秒的毫秒零头(仅 ISR) */
volatile uint16_t since_sync; /**< 距上次同步 gDevData 的秒数(60s 节流) */
volatile uint32_t since_save; /**< 距上次落盘的累计运行秒(600s 节奏) */
volatile uint8_t need_sync; /**< 1=ISR 要求任务同步 gDevData60s 到点) */
volatile uint8_t need_edge; /**< 1=边沿(到期)需立刻同步 + 落盘 */
} LightLifeCtx_t;
static LightLifeCtx_t s_life;
#if BOARD_LIGHT_LIFE_ENABLE
/**
* @brief 按当前 remain 推导告警并同步 gDevData(任务上下文)
* @param immediate 1=边沿立刻落盘;0=仅同步(60s 节流点,落盘随 600s)
* @return 无
*/
static void life_sync_gdata(uint8_t immediate)
{
gDevData.lighe_remain_time = s_life.remain;
gDevData.alarm_code = (s_life.remain == 0) ? 1 : 0;
if (immediate) {
app_nvm_save_runrec(); /* 寿命随 runrec item 落盘(规格书 13.2 边沿) */
s_life.since_save = 0;
}
}
#endif
/**
* @brief 初始化剩余寿命:默认额定值(VM 有效时 app_nvm_load 会覆盖)
* @return 无
*/
void app_light_life_init(void)
{
#if !BOARD_LIGHT_LIFE_ENABLE
log_info("light life disabled\n");
return;
#else
memset(&s_life, 0, sizeof(s_life));
s_life.remain = BOARD_LIGHT_RATED_SEC;
/* 首上电(VM 无效)DP3 即额定值、DP25=0(规格书 17 章 P1 末条);
* VM 有效时 app_nvm_load 会再调 set_remain 覆盖 */
gDevData.lighe_remain_time = BOARD_LIGHT_RATED_SEC;
gDevData.alarm_code = 0;
log_info("light life init, remain=%lu\n", (unsigned long)s_life.remain);
#endif
}
/**
* @brief 20ms 业务节拍:实际吸合才扣秒(与运行记录同口径)
* @param dt 距上次调用的毫秒数
* @return 无
*/
void app_light_life_tick(uint16_t dt)
{
#if !BOARD_LIGHT_LIFE_ENABLE
(void)dt;
return;
#else
if (!app_relay_get_actual()) {
s_life.ms = 0; /* 没在点灯:丢弃不足 1 秒的零头(13.2 同口径) */
return;
}
if (s_life.remain == 0) {
return; /* 已到期:停在 0,不回绕 */
}
s_life.ms = (uint16_t)(s_life.ms + dt);
while (s_life.ms >= 1000u) {
s_life.ms = (uint16_t)(s_life.ms - 1000u);
s_life.remain--;
s_life.since_sync++;
s_life.since_save++;
if (s_life.remain == 0) {
s_life.need_edge = 1; /* 到期边沿:任务立刻同步 + 落盘 + 上报 */
}
}
/* 60s 节流:点灯且未到期时满 60s 才允许同步一次 DP3(规格书 13.2) */
if ((s_life.since_sync >= BOARD_LIGHT_REPORT_MS / 1000u) && !s_life.need_edge) {
s_life.since_sync = 0;
s_life.need_sync = 1;
}
#endif
}
/**
* @brief 500ms 任务上下文:节流同步 / 边沿立刻同步 + 落盘
* @return 无
*/
void app_light_life_loop(void)
{
#if !BOARD_LIGHT_LIFE_ENABLE
return;
#else
uint8_t edge;
uint8_t sync;
local_irq_disable();
edge = s_life.need_edge;
sync = s_life.need_sync;
s_life.need_edge = 0;
s_life.need_sync = 0;
local_irq_enable();
if (edge) {
life_sync_gdata(1);
log_info("life expired, remain=0 alarm=1\n");
return;
}
if (sync) {
life_sync_gdata(0); /* 60s 节流点:只同步,flash 随 600s */
}
/* 平时落盘节奏:累计点灯满 600s 写一次(与运行记录同次可接受误差) */
if (s_life.since_save >= BOARD_RUN_RECORD_SAVE_SEC) {
s_life.since_save = 0;
app_nvm_save_runrec();
}
#endif
}
/**
* @brief 设置剩余寿命(APP 写 DP3 / VM 恢复 / 出厂复位,任务上下文)
* @param remain 剩余寿命秒,0~0xFFFFFFFF 不封顶
* @return 无
*/
void app_light_life_set_remain(uint32_t remain)
{
#if BOARD_LIGHT_LIFE_ENABLE
local_irq_disable();
s_life.remain = remain;
s_life.since_sync = 0;
s_life.ms = 0;
local_irq_enable();
/* 边沿立刻生效:同步 gDevData + 推导告警 + 立刻落盘 */
gDevData.lighe_remain_time = remain;
gDevData.alarm_code = (remain == 0) ? 1 : 0;
app_nvm_save_runrec();
s_life.since_save = 0;
log_info("life set remain=%lu alarm=%d\n", (unsigned long)remain, gDevData.alarm_code);
#else
/* 模块关闭:仅按值回填(VM/默认原样上报,不扣秒不置告警,规格书 0.4) */
gDevData.lighe_remain_time = remain;
if (remain == 0) {
gDevData.alarm_code = 1;
}
#endif
}
/**
* @brief 灯泡复位(DP4 写 1 / 出厂复位):回额定值、告警清 0
* @return 无
*/
void app_light_life_reset(void)
{
app_light_life_set_remain(BOARD_LIGHT_RATED_SEC);
log_info("light reset to rated %lu\n", (unsigned long)BOARD_LIGHT_RATED_SEC);
}
/**
* @brief 查询剩余寿命
* @return 剩余寿命秒
*/
uint32_t app_light_life_get_remain(void)
{
#if !BOARD_LIGHT_LIFE_ENABLE
return gDevData.lighe_remain_time; /* 关闭时按上报值原样返回 */
#else
return s_life.remain;
#endif
}
+65
View File
@@ -0,0 +1,65 @@
/******************************************************************************
* @file app_light_life.h
* @brief 灯泡剩余寿命(规格书 13.2 节,DP2/DP3/DP4/DP25
* @author cyWu <1917507415@qq.com>
* @date 2026.09.08
* @version V1.0.0
*
* @note 额定工时 2592000s(30 天),按继电器**实际吸合**每秒扣 1;
* 扣到 0 停住不回绕,置 alarm_code=1(灯泡到期告警,粘住)但**不互锁**,
* 灯仍可手动/定时/联动再开(第 12 章仲裁不参与)。
* DP3 上报节流:平时满 60s 才同步一次 gDevData(协议层 2s 防抖后
* 自然最多 60s 一包);到期 / DP4 / APP 写 DP3 / 出厂复位走边沿
* **立刻**同步 + 落盘(任务上下文,禁止 ISR 写 VM)。
* 剩余寿命持久化随运行记录 item(与 runrec 同写节奏 600s,边沿立刻)。
******************************************************************************/
#ifndef __APP_LIGHT_LIFE_H__
#define __APP_LIGHT_LIFE_H__
#include <stdint.h>
/**
* @brief 初始化剩余寿命:RAM 复位为额定值,告警按值推导
* @note 之后 app_nvm_load 若 VM 有效会调 app_light_life_set_remain 覆盖
* @return 无
*/
void app_light_life_init(void);
/**
* @brief 20ms 业务节拍(中断上下文):实际吸合才折秒扣减
* @param dt 距上次调用的毫秒数
* @note ISR 只碰 s_lifeRAM);gDevData 同步 / 落盘 / 上报在 app_light_life_loop
* @return 无
*/
void app_light_life_tick(uint16_t dt);
/**
* @brief 500ms 任务上下文:60s 节流同步 DP3、边沿立刻同步 + 落盘、推导告警
* @return 无
*/
void app_light_life_loop(void);
/**
* @brief 设置剩余寿命(APP 写 DP3 / VM 恢复 / 出厂复位共用,任务上下文)
* @param remain 剩余寿命秒,范围 0~0xFFFFFFFF 不封顶
* @note 立刻生效:同步 gDevData + 推导 alarm_code + 立刻落盘 + 变化上报由
* 协议层差分自动发出(2s 防抖后);remain>0 清到期告警,=0 置告警
* @return 无
*/
void app_light_life_set_remain(uint32_t remain);
/**
* @brief 灯泡复位(DP4 写 1):剩余寿命回到额定值、告警清 0
* @note WO 不上报;由 jb_product DP4 调用;边沿立刻落盘
* @return 无
*/
void app_light_life_reset(void);
/**
* @brief 查询剩余寿命
* @return 剩余寿命秒
*/
uint32_t app_light_life_get_remain(void);
#endif /* __APP_LIGHT_LIFE_H__ */
+315
View File
@@ -0,0 +1,315 @@
/******************************************************************************
* @file app_linkage.c
* @brief CBR0 联动引擎(规格书第 10 章 + 第 12 章仲裁):只看温度回差
* @author cyWu <1917507415@qq.com>
* @date 2026.09.04
* @version V1.0.1
* @history
* - V1.0.1, 2026.09.04, cyWu, 过热关不再限 owner==LINKAGEVM 恢复/手动开的垫
* 过热时也能关(10.3 + 再武装语义,用例 6/2)
* - V1.0.0, 2026.09.04, cyWu, P2 完整实现(P0/P1 为骨架)
*
* @note 开停只看温度(10.3,本垫过冷开 / 过热关,与风扇相反):
* temp_x10 < Tmin*10 → 请求开加热(T<Tmin,过冷)
* temp_x10 > Tmax*10 → 请求关加热(T>Tmax,过热)
* [Tmin,Tmax](含等于)→ 保持上一态(回差)
* 温度源(8 章):BLE 已连接 = 服务器 DP6set_connected_temp);
* 断连已配对 = 扫描 JBao-{tank}set_scan_temp);丢包保持上一态。
* 无任何有效温度 → 联动不改继电器(10.2)。
*
* 手动作废(12 章,RAM 不落 VM):须先走到「关」条件(T>Tmax)
* 再重新出现 T<Tmin 才允许联动再开;重写 DP11 清手动作废并立刻重判。
*
* ⚠ 判定只在 usr_timer_loop500ms 任务上下文)与 DP11 写路径执行,
* 不提供 20ms 中断接口。温度 setter 由 jb_productDP6)与
* app_th_scan(扫描回调)在任务上下文调用。
******************************************************************************/
#include <string.h>
#include "system/includes.h"
#include "app_linkage.h"
#include "app_arbiter.h"
#include "app_relay.h"
#include "app_nvm.h"
#include "jb_product.h"
#include "board_pin.h"
#define LOG_TAG_CONST APP
#define LOG_TAG "[APP_LKG]"
#define LOG_INFO_ENABLE
#include "debug.h"
/** 20B 配置解析结果(规格书 10.2;湿度/版本/预留只存不用) */
typedef struct {
uint16_t tank_id; /**< Byte0-1 大端,0=无效 */
int8_t tmin; /**< Byte2 ℃ */
int8_t tmax; /**< Byte3 ℃ */
} LinkageCfg_t;
/** 联动运行时(RAM;温度与作废标志均不落 VM) */
typedef struct {
int16_t temp_x10; /**< 联动用温度(0.1℃,有符号) */
uint8_t temp_valid; /**< 1=收到过任意来源的有效温度 */
uint8_t voided; /**< 1=被手动作废,须先到「关」条件再武装 */
} LinkageCtx_t;
static LinkageCtx_t s_lk;
/**
* @brief 解析 20B 配置并判定有效性(10.1:缸体≠0 且 Tmin≤Tmax
* @param c 20B 配置
* @param out 解析结果
* @return 1=有效
*/
static uint8_t lk_parse(const uint8_t *c, LinkageCfg_t *out)
{
out->tank_id = (uint16_t)(((uint16_t)c[0] << 8) | c[1]);
out->tmin = (int8_t)c[2];
out->tmax = (int8_t)c[3];
if (out->tank_id == 0) {
return 0;
}
if (out->tmin > out->tmax) {
return 0;
}
return 1;
}
/**
* @brief 联动让出加热:仅当联动正占着时才关(手动/定时占着不动)
* @return 无
*/
static void lk_stop(void)
{
if (app_arbiter_linkage_active()) {
app_arbiter_linkage_release();
app_relay_request(0);
log_info("linkage stop, heat=0\n");
}
}
/**
* @brief 初始化联动引擎
* @return 无
*/
void app_linkage_init(void)
{
#if !BOARD_LINKAGE_ENABLE
log_info("linkage disabled\n");
return;
#endif
memset(&s_lk, 0, sizeof(s_lk));
log_info("linkage init\n");
}
/**
* @brief 核心判定(幂等,10.3 + 12 章)
* @note 步骤:无效配置 → 联动占着则关;无有效温度 → 不动;
* 走到「关」条件清作废(再武装);作废期间不动作;
* 仲裁定时窗占用时不插嘴;否则按回差开/关。
* @return 无
*/
static void lk_reeval(void)
{
LinkageCfg_t cfg;
uint8_t valid = lk_parse(gDevData.linkage_config, &cfg);
uint8_t hot; /* T > Tmax:过热,关条件 */
uint8_t cold; /* T < Tmin:过冷,开条件 */
/* 10.1:无效配置(缸体 0 / min>max)→ 不自动开;原先联动占着则关掉 */
if (!valid) {
lk_stop();
return;
}
/* 10.2:尚无任何有效温度 → 联动不改继电器(保持上电恢复值/上一态) */
if (!s_lk.temp_valid) {
return;
}
hot = (s_lk.temp_x10 > (int16_t)cfg.tmax * 10) ? 1 : 0;
cold = (s_lk.temp_x10 < (int16_t)cfg.tmin * 10) ? 1 : 0;
/* 12 章再武装:先走到「关」条件(T>Tmax)→ 清作废;
* 之后重新出现 T<Tmin 才允许再开。等于 Tmax/Tmin 都不算越界 */
if (hot) {
if (s_lk.voided) {
log_info("linkage rearmed (T>Tmax reached)\n");
}
s_lk.voided = 0;
}
if (s_lk.voided) {
return; /* 作废期间保持上一态,不动作 */
}
/* 12 章仲裁:定时窗占着(ARB_OWNER_TIMER)时联动不开不关,
* 窗结束收口后由 app_timer 调 reeval 补判 */
if (app_arbiter_get_owner() == ARB_OWNER_TIMER) {
return;
}
if (cold) {
/* 10.3 开:T<Tmin 过冷 → 请求开加热 */
if (app_arbiter_linkage_take()) {
if (!app_relay_get_request()) {
app_relay_request(1);
log_info("linkage open (cold), heat=1\n");
}
}
} else if (hot) {
/* 10.3 过热关:走到 T>Tmax 一律关,与谁开的无关——上电 VM 恢复的
* 加热(owner=NONE,用例 6)或手动开的垫子在过热时都要能关;
* 手动作废在本 tick 已先清(再武装语义,见上)。定时窗占用时前面
* 已 return,不插嘴;删除/无效配置路径仍走 lk_stop(只关联动的) */
if (app_arbiter_linkage_active()) {
app_arbiter_linkage_release();
}
if (app_relay_get_request()) {
app_relay_request(0);
log_info("linkage overheat close, heat=0\n");
}
}
/* 否则 [Tmin,Tmax](含等于):保持上一态(回差) */
}
/**
* @brief 立刻按第 10.3 节重判一次(DP11 重写 / 定时窗收口后调用)
* @return 无
*/
void app_linkage_reeval(void)
{
#if !BOARD_LINKAGE_ENABLE
return;
#endif
lk_reeval();
}
/**
* @brief 推进联动引擎(500ms,任务上下文)
* @param dt 距上次调用的毫秒数
* @return 无
*/
void app_linkage_run(uint16_t dt)
{
#if !BOARD_LINKAGE_ENABLE
(void)dt;
return;
#endif
(void)dt; /* 本模块不计时:每拍完整重判 */
lk_reeval();
}
/**
* @brief DP7 联动删除写 1(脉冲):清 RAM+VM 20B;联动占着则关
* @note gDevData.linkage_delete 由调用方清回 0(脉冲);引擎关掉也落盘
* @return 无
*/
void app_linkage_delete(void)
{
memset(gDevData.linkage_config, 0, BOARD_LINKAGE_CFG_LEN);
app_nvm_mark_dirty();
#if BOARD_LINKAGE_ENABLE
s_lk.voided = 0;
lk_stop(); /* 10.4:若联动占着加热 → 关 */
#endif
log_info("linkage config deleted\n");
}
/**
* @brief APP 重写 DP11memcpy 已完成):存 VM + 清手动作废 + 立刻重判
* @note 引擎关掉也落盘;重判只在 BOARD_LINKAGE_ENABLE=1 时执行(10.5
* @return 无
*/
void app_linkage_set_config(void)
{
app_nvm_mark_dirty();
#if BOARD_LINKAGE_ENABLE
s_lk.voided = 0; /* 10.5:清手动作废标志 */
lk_reeval(); /* 立刻按新阈值重判(有有效温度时) */
#endif
log_info("linkage_config stored\n");
}
/**
* @brief 手动作废本轮联动(短按/DP0 触发,仲裁器唯一转调点)
* @return 无
*/
void app_linkage_void(void)
{
#if !BOARD_LINKAGE_ENABLE
return;
#endif
if (!s_lk.voided) {
s_lk.voided = 1;
log_info("linkage voided by manual\n");
}
}
/**
* @brief 配置是否有效(10.1
* @return 1=有效
*/
uint8_t app_linkage_is_valid(void)
{
LinkageCfg_t cfg;
return lk_parse(gDevData.linkage_config, &cfg);
}
/**
* @brief 缸体编号(Byte0-1 大端;引擎关掉也可读,供扫描匹配用)
* @return 缸体编号,0=未配
*/
uint16_t app_linkage_get_tank_id(void)
{
return (uint16_t)(((uint16_t)gDevData.linkage_config[0] << 8) |
gDevData.linkage_config[1]);
}
/**
* @brief 联动配置版本(Byte6;扫描匹配用)
* @return 配置版本
*/
uint8_t app_linkage_get_cfg_ver(void)
{
return gDevData.linkage_config[6];
}
/**
* @brief BLE 已连接:服务器 DP6 温度(℃ float)→ 联动温度
* @param degc 服务器下发物理量(floatscale=0.1 已还原,如 28.5
* @return 无
*/
void app_linkage_set_connected_temp(float degc)
{
s_lk.temp_x10 = (int16_t)(degc * 10.0f);
s_lk.temp_valid = 1;
log_info("linkage temp from dp6: %.1f C\n", (double)degc);
}
/**
* @brief 断连扫描:匹配到 JBao-{tank} 广播 → 联动温度
* @param degc 风扇广播温度 int8(单位 ℃)
* @return 无
*/
void app_linkage_set_scan_temp(int8_t degc)
{
int16_t x10 = (int16_t)degc * 10;
if (s_lk.temp_valid && (s_lk.temp_x10 == x10)) {
return;
}
s_lk.temp_x10 = x10;
s_lk.temp_valid = 1;
log_info("linkage temp from scan: %d C\n", degc);
}
/**
* @brief 是否已有任何有效温度
* @return 1=有
*/
uint8_t app_linkage_has_temp(void)
{
return s_lk.temp_valid;
}
+96
View File
@@ -0,0 +1,96 @@
/******************************************************************************
* @file app_linkage.h
* @brief CBR0 联动引擎(第 10 章:1 条 20B DP11 配置,温度回差开/关)
* @author cyWu <1917507415@qq.com>
* @date 2026.09.04
* @version V1.0.0P0 = 骨架(BOARD_LINKAGE_ENABLE=0P2 填充)
*
* @note 开停只看温度(temp_x10,0.1℃ 有符号):
* T < Tmin → 请求开加热
* T > Tmax → 请求关加热
* [Tmin,Tmax] → 保持上一态
* 温度源:已连接=DP9(float×10);断连=扫描风扇 JBao-{tank} 广播。
* 丢广播/无效包 → 当没收到,保持上一态(禁止丢包关加热)。
******************************************************************************/
#ifndef __APP_LINKAGE_H__
#define __APP_LINKAGE_H__
#include <stdint.h>
/**
* @brief 初始化联动(配置有效性由 is_valid 判;作废标志清 0
* @return 无
*/
void app_linkage_init(void);
/**
* @brief 500ms 任务上下文跑联动:有效配置 + 未被手动作废 + 有有效温度才判
* @param dt 距上次调用的毫秒数(=500)
* @return 无
*/
void app_linkage_run(uint16_t dt);
/**
* @brief 立刻按当前温度重判一次(APP 重写 DP11 / 定时窗收口后调用)
* @return 无
*/
void app_linkage_reeval(void);
/**
* @brief DP7 联动删除(脉冲):清 RAM+VM 20B;联动占着则关;delete 清 0
* @return 无
*/
void app_linkage_delete(void);
/**
* @brief APP 重写 DP11memcpy 已完成):存 VM、清手动作废、立刻重判
* @return 无
*/
void app_linkage_set_config(void);
/**
* @brief 手动作废本轮联动(RAM,不写 VM;仲裁器唯一转调点)
* @return 无
*/
void app_linkage_void(void);
/**
* @brief 配置是否有效(缸体号≠0 且 Tmin≤Tmax)
* @return 1=有效
*/
uint8_t app_linkage_is_valid(void);
/**
* @brief 缸体编号(DP11 Byte0-1uint16 大端)
* @return tank_id
*/
uint16_t app_linkage_get_tank_id(void);
/**
* @brief 配置版本(DP11 Byte6,扫描匹配用)
* @return 版本号
*/
uint8_t app_linkage_get_cfg_ver(void);
/**
* @brief BLE 已连接:服务器下发 DP6 温度(℃ float)→ 联动温度
* @param degc 服务器下发物理量(如 28.5)
* @return 无
*/
void app_linkage_set_connected_temp(float degc);
/**
* @brief 断连扫描:匹配到 JBao-{tank} 广播 → 联动温度(℃ int8)
* @param degc 风扇广播温度 int8(单位 ℃)
* @return 无
*/
void app_linkage_set_scan_temp(int8_t degc);
/**
* @brief 是否已有任何有效温度(连接 DP6 或扫描匹配到过)
* @return 1=有;0=尚无温度(联动不动作)
*/
uint8_t app_linkage_has_temp(void);
#endif /* __APP_LINKAGE_H__ */
+432
View File
@@ -0,0 +1,432 @@
/******************************************************************************
* @file app_nvm.c
* @brief CBR0 VM 持久化:heat_switch / linkage 20B / time_en /
* time_config1~7 / run_record 31天 / fault_en / alarm_en
* @author cyWu <1917507415@qq.com>
* @date 2026.09.04
* @version V1.1.0
* @history
* - V1.1.0, 2026.09.04, cyWu, P1 完整实现(4 item 存取 + 加热请求恢复)
* - V1.0.0, 2026.09.04, cyWu, P0 空骨架
*
* @note VM item 分配在 syscfg 用户自定义区 [1~49]syscfg_id.h 已占用
* CFG_USER_PAIR_INFO=2 / TIM_FLAG=3 / VM_ALARM*=4~24 /
* DEVICE_STA=25 / PLAN_INFO1=26 / BATTERY_LEVEL=27),
* 本产品从 28 开始:
* CFG_USER_CBR0_STATE =28 状态(magic+ver+灯泡开关+使能,9B
* CFG_USER_CBR0_LINKAGE =29 联动配置 linkage_config20BP2 写)
* CFG_USER_CBR0_TIME =30 定时配置 time_config[7][11]77B
* CFG_USER_CBR0_RUNREC =31 运行记录 31×uint32 + slot0 日期 +
* 灯泡剩余寿命(132B13.2 随 runrec 同写)
* 拆 4 个 item:改灯开关只擦写 9B 小 item,不重写配置区,减少 flash
* 磨损;首次上电(magic/ver 不符)保持 gDevData 全 0 默认。
*
* 落盘基准 = 灯泡**请求**app_relay_get_request,规格书 13 章"上次
* 灯泡开关请求"),不是实际吸合态;上电按它恢复请求、等过零再吸合。
*
* ⚠ 上下文硬约束:usr_timer_add 回调在硬件定时器中断上下文
* (栈极小),app_nvm_tick 只能做变化检测与防抖计时,**绝对禁止
* 调 syscfg_write**VM 擦写 flash 栈用量大 → 栈溢出软复位)。
* 真正写入由 app_nvm_flush() 在 usr_timer_loop(任务上下文)执行。
* nvm_write_item 的比较缓冲必须 static(放栈上会撑爆中断栈)。
******************************************************************************/
#include <string.h>
#include "app_nvm.h"
#include "app_relay.h"
#include "app_light_life.h"
#include "app_defer.h"
#include "app_runrec.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_CBR0_STATE 28
#define CFG_USER_CBR0_LINKAGE 29
#define CFG_USER_CBR0_TIME 30
#define CFG_USER_CBR0_RUNREC 31
#define CBR0_VM_MAGIC 0xCB00
#define CBR0_VM_VER 1
#define CBR0_VM_DEBOUNCE_MS 500 /* 灯泡/使能稳定变化防抖(规格书 13 章) */
/** 状态 item10 字节) */
typedef struct {
uint16_t magic; /**< CBR0_VM_MAGIC */
uint8_t ver; /**< CBR0_VM_VER */
uint8_t heat_switch;/**< 上次灯泡请求(DP0;上电按此恢复) */
uint8_t time_en; /**< DP12 定时总开关 */
uint8_t fault_en; /**< DP22 */
uint8_t alarm_en; /**< DP24 */
uint8_t rsv[2]; /**< 对齐到 10B */
} __packed Cbr0StateVm_t;
/** 联动配置 item20 字节,DP11 原样落盘) */
typedef struct {
uint8_t linkage_config[BOARD_LINKAGE_CFG_LEN];
} __packed Cbr0LinkageVm_t;
/** 定时配置 item(77 字节:7 组 × 11B,规格书 11.1 */
typedef struct {
uint8_t time_config[7][BOARD_TIME_CFG_LEN];
} __packed Cbr0TimeVm_t;
/** 运行记录 item124B 秒数 + 4B slot0 日期 + 4B 剩余寿命) */
typedef struct {
uint8_t run_record[APP_RUNREC_PACK_LEN]; /**< 31 × uint32 大端 */
uint32_t slot0_date; /**< slot[0] 对应本地日期 yyyymmdd */
uint32_t lighe_remain_time; /**< 灯泡剩余寿命(随 runrec 同写节奏,13.2 */
} __packed Cbr0RunrecVm_t;
#if BOARD_NVM_ENABLE
/** NVM 运行时:防抖落盘 + 已落盘状态快照(变化检测基准) */
typedef struct {
uint8_t dirty; /**< 1=防抖计时中 */
uint16_t deb_ms; /**< 防抖累计毫秒 */
uint8_t last_req; /**< 上次已同步到 VM 的灯泡请求 */
uint8_t last_time_en; /**< 上次已同步到 VM 的定时总开关 */
} AppNvmCtx_t;
static AppNvmCtx_t s_nvm;
#endif
/**
* @brief 从 gDevData 收集定时配置到 VM 结构
* @param out 输出结构
* @return 无
*/
static void nvm_collect_time(Cbr0TimeVm_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], BOARD_TIME_CFG_LEN);
}
}
/**
* @brief 写单个 item:先读回比较,内容相同跳过,避免无谓擦写 flash
* @param item_id VM item id
* @param buf 新数据
* @param len 数据长度(≤128,比较缓冲大小)
* @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 写状态 itemheat_switch/time_en/使能)
* @return 无
*/
static void nvm_write_state(void)
{
Cbr0StateVm_t st;
memset(&st, 0, sizeof(st));
st.magic = CBR0_VM_MAGIC;
st.ver = CBR0_VM_VER;
st.heat_switch = app_relay_get_request();
st.time_en = gDevData.time_en ? 1 : 0;
st.fault_en = gDevData.fault_en;
st.alarm_en = gDevData.alarm_en;
nvm_write_item(CFG_USER_CBR0_STATE, &st, sizeof(st));
}
/**
* @brief 写联动配置 item20B 原样落盘;P1 恒 0P2 由 DP11 写入)
* @return 无
*/
static void nvm_write_linkage(void)
{
Cbr0LinkageVm_t lf;
memcpy(lf.linkage_config, gDevData.linkage_config, BOARD_LINKAGE_CFG_LEN);
nvm_write_item(CFG_USER_CBR0_LINKAGE, &lf, sizeof(lf));
}
/**
* @brief 写定时配置 item
* @return 无
*/
static void nvm_write_time(void)
{
Cbr0TimeVm_t tf;
nvm_collect_time(&tf);
nvm_write_item(CFG_USER_CBR0_TIME, &tf, sizeof(tf));
}
/**
* @brief 写运行记录 item31 格秒数 + slot0 日期 + 灯泡剩余寿命)
* @note 寿命字段随 runrec 同写节奏(600s / 边沿立刻),规格书 13.2
* @return 无
*/
static void nvm_write_runrec(void)
{
Cbr0RunrecVm_t rf;
app_runrec_pack(rf.run_record);
rf.slot0_date = app_runrec_get_slot0_date();
rf.lighe_remain_time = app_light_life_get_remain();
nvm_write_item(CFG_USER_CBR0_RUNREC, &rf, sizeof(rf));
}
/**
* @brief 同步"已落盘状态"快照为当前值(save/load 后调用)
* @return 无
*/
static void nvm_sync_snapshot(void)
{
#if BOARD_NVM_ENABLE
s_nvm.last_req = app_relay_get_request();
s_nvm.last_time_en = gDevData.time_en ? 1 : 0;
#endif
}
/**
* @brief 初始化:校验 VM 有效性并打日志(数据加载在 app_nvm_load
* @return 无
*/
void app_nvm_init(void)
{
#if !BOARD_NVM_ENABLE
log_info("nvm disabled\n");
return;
#else
Cbr0StateVm_t st;
int ret = syscfg_read(CFG_USER_CBR0_STATE, &st, sizeof(st));
if ((ret == (int)sizeof(st)) && (st.magic == CBR0_VM_MAGIC) && (st.ver == CBR0_VM_VER)) {
log_info("nvm init, vm valid v%d (heat=%d time_en=%d)\n",
st.ver, st.heat_switch, st.time_en);
} else {
log_info("nvm init, vm invalid (ret=%d), use defaults\n", ret);
}
memset(&s_nvm, 0, sizeof(s_nvm));
app_defer_cancel(APP_DEFER_NVM_SAVE);
#endif
}
/**
* @brief 立即保存全部业务字段
* @return 无
*/
void app_nvm_save(void)
{
#if !BOARD_NVM_ENABLE
return;
#else
nvm_write_state();
nvm_write_linkage();
nvm_write_time();
nvm_write_runrec();
s_nvm.dirty = 0;
s_nvm.deb_ms = 0;
app_defer_cancel(APP_DEFER_NVM_SAVE);
nvm_sync_snapshot();
#endif
}
/**
* @brief 立即保存运行记录项(跨天旋转用,任务上下文)
* @return 无
*/
void app_nvm_save_runrec(void)
{
#if !BOARD_NVM_ENABLE
return;
#else
nvm_write_runrec();
#endif
}
/**
* @brief 从 VM 加载并应用(开机由 usr_jb_init 调用)
* @note 加热恢复 = 按上次**请求**恢复(等过零吸合,规格书 13 章 / 5.1);
* 联动/定时/运行记录回填 gDevData。VM 无效(首次上电)保持全 0 默认。
* @return 无
*/
void app_nvm_load(void)
{
#if !BOARD_NVM_ENABLE
return;
#else
Cbr0StateVm_t st;
Cbr0LinkageVm_t lf;
Cbr0TimeVm_t tf;
Cbr0RunrecVm_t rf;
uint8_t i;
if ((syscfg_read(CFG_USER_CBR0_STATE, &st, sizeof(st)) != (int)sizeof(st))
|| (st.magic != CBR0_VM_MAGIC) || (st.ver != CBR0_VM_VER)) {
log_info("nvm load: no valid vm, defaults (heat=0)\n");
return;
}
/* 联动配置回填 gDevData(引擎由 BOARD_LINKAGE_ENABLE 决定是否跑) */
if (syscfg_read(CFG_USER_CBR0_LINKAGE, &lf, sizeof(lf)) == (int)sizeof(lf)) {
memcpy(gDevData.linkage_config, lf.linkage_config, BOARD_LINKAGE_CFG_LEN);
}
/* 定时配置回填 gDevData(只存不跑时也要回填,P1 验收) */
if (syscfg_read(CFG_USER_CBR0_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], BOARD_TIME_CFG_LEN);
}
}
/* 运行记录回填 RAM(13.1:掉电再上电先把 31 格灌回来) */
if (syscfg_read(CFG_USER_CBR0_RUNREC, &rf, sizeof(rf)) == (int)sizeof(rf)) {
app_runrec_unpack(rf.run_record);
app_runrec_set_slot0_date(rf.slot0_date);
/* 灯泡剩余寿命覆盖 RAM 默认额定值(13.2;值相同不擦写 flash) */
app_light_life_set_remain(rf.lighe_remain_time);
}
gDevData.time_en = st.time_en ? 1 : 0;
gDevData.fault_en = st.fault_en;
gDevData.alarm_en = st.alarm_en;
/* 上电按上次加热请求恢复(开则等过零再吸合;无过零由 app_relay 超时兜底) */
app_relay_request(st.heat_switch ? 1 : 0);
nvm_sync_snapshot(); /* 恢复值即为已落盘值,同步基准 */
log_info("nvm load v%d: heat=%d time_en=%d\n",
st.ver, st.heat_switch, st.time_en);
#endif
}
/**
* @brief 业务节拍:做加热请求/总开关变化检测 + 防抖计时,**不执行写入**
* @param dt 距上次调用的毫秒数
* @note ⚠ 本函数跑在 usr_timer_add 的 20ms 回调里 = **硬件定时器中断上下文**
* (异步接口,栈极小)。这里只允许做轻量比较与计时;真正的
* syscfg_write 由 app_nvm_flush() 在 usr_timer_loop(任务上下文)执行。
* 曾在此直接调 app_nvm_save 导致栈溢出软复位。
* @return 无
*/
void app_nvm_tick(uint16_t dt)
{
#if !BOARD_NVM_ENABLE
(void)dt;
return;
#else
uint8_t req = app_relay_get_request();
uint8_t time_en = gDevData.time_en ? 1 : 0;
if ((req != s_nvm.last_req) || (time_en != s_nvm.last_time_en)) {
s_nvm.last_req = req;
s_nvm.last_time_en = time_en;
s_nvm.dirty = 1;
s_nvm.deb_ms = 0; /* 变化重新计满防抖窗口 */
}
if (!s_nvm.dirty) {
return;
}
s_nvm.deb_ms = (uint16_t)(s_nvm.deb_ms + dt);
if (s_nvm.deb_ms >= CBR0_VM_DEBOUNCE_MS) {
s_nvm.deb_ms = 0;
s_nvm.dirty = 0;
app_defer_request(APP_DEFER_NVM_SAVE); /* 只置标志:真正落盘在 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 (!app_defer_take(APP_DEFER_NVM_SAVE)) {
return;
}
app_nvm_save();
#endif
}
/**
* @brief 标记业务字段有变化,防抖计时归零(重新计满防抖窗口才写)
* @note 任务上下文调用(APP 下行改配置:定时/使能/故障告警使能)
* @return 无
*/
void app_nvm_mark_dirty(void)
{
#if BOARD_NVM_ENABLE
s_nvm.dirty = 1;
s_nvm.deb_ms = 0;
#endif
}
/**
* @brief 出厂重置:清全部业务 VM(保留 BLE 配对,规格书第 14 章)
* @note 4 个 item 写全 0 → magic 失效,下次开机走首次上电默认值;
* BLE 配对三元组在 CFG_USER_PAIR_INFOitem 2),不受影响
* @return 无
*/
void app_nvm_reset(void)
{
#if !BOARD_NVM_ENABLE
return;
#else
Cbr0StateVm_t st;
Cbr0LinkageVm_t lf;
Cbr0TimeVm_t tf;
Cbr0RunrecVm_t rf;
memset(&st, 0, sizeof(st));
memset(&lf, 0, sizeof(lf));
memset(&tf, 0, sizeof(tf));
memset(&rf, 0, sizeof(rf));
syscfg_write(CFG_USER_CBR0_STATE, &st, sizeof(st));
syscfg_write(CFG_USER_CBR0_LINKAGE, &lf, sizeof(lf));
syscfg_write(CFG_USER_CBR0_TIME, &tf, sizeof(tf));
syscfg_write(CFG_USER_CBR0_RUNREC, &rf, sizeof(rf));
s_nvm.dirty = 0;
s_nvm.deb_ms = 0;
app_defer_cancel(APP_DEFER_NVM_SAVE);
log_info("nvm factory reset, business vm cleared (pairing kept)\n");
#endif
}
+85
View File
@@ -0,0 +1,85 @@
/******************************************************************************
* @file app_nvm.h
* @brief CBR0 VM 持久化(P1 起):灯泡开关 / linkage_config(20B) /
* time_en / time_config1~7(11B) / run_record(31×uint32+日期+寿命) /
* fault_en / alarm_en(规格书第 13 章)
* @author cyWu <1917507415@qq.com>
* @date 2026.09.04
* @version V1.1.0P0 = 空骨架(BOARD_NVM_ENABLE=0
*
* @note 存:灯泡开关(DP0)、linkage_config(20B, DP11)、time_en(DP12)、
* time_config1~7(11B, DP13~19)、run_record(DP2031×uint32 +
* slot0 日期 + 灯泡剩余寿命 DP3)、fault_en(DP22)、alarm_en(DP24)。
* 不存:DP1–6(温控/模式等易失)、DP9 当前温度(WO)、联动作废标志、
* 过零 pending、linkage_delete(DP10 脉冲)、BLE 三元组(出厂不清)。
* 写 VM 时机:APP 改配置后(500ms 防抖);运行记录每累计 600s 一次、
* 跨天旋转立刻写;寿命随 runrec 同写(到期/复位/写 DP3 边沿立刻)。
*
* ⚠ 写 flash 只能在任务上下文(app_nvm_flush / app_nvm_save /
* app_nvm_save_runrec),20ms 中断上下文只允许 app_nvm_tick 打脏。
******************************************************************************/
#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 写入不能在裸中断里调用;只由 app_nvm_flush()(任务上下文)调用
* @return 无
*/
void app_nvm_save(void);
/**
* @brief 立即保存运行记录 item(跨天旋转时用,任务上下文)
* @note 与 app_nvm_save 的区别:只写 31 天记录这一项,避免跨天时把
* 状态/配置一起无谓擦写一遍
* @return 无
*/
void app_nvm_save_runrec(void);
/**
* @brief 从 VM 加载业务字段并应用(开机调用):
* 灯泡开关请求恢复(开则等过零吸合)、联动/定时/运行记录/剩余寿命
* 回填 gDevData
* @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 DP26 调用;RAM 复位由调用方完成(含寿命回额定)
* @return 无
*/
void app_nvm_reset(void);
#endif /* __APP_NVM_H__ */
+74
View File
@@ -0,0 +1,74 @@
/******************************************************************************
* @file app_pair.c
* @brief 配对会话计时实现
* @author cyWu <1917507415@qq.com>
* @date 2026.08.25
* @version V1.0.0,首次发布,实现配对会话计时与成功/超时判定
******************************************************************************/
#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;
}
+43
View File
@@ -0,0 +1,43 @@
/******************************************************************************
* @file app_pair.h
* @brief 配对会话计时:开机配对窗口是否成功/超时,与 BLE 配对本身解耦
* @author cyWu <1917507415@qq.com>
* @date 2026.08.25
* @version V1.0.0,首次发布,实现配对会话计时与成功/超时判定
*
* @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__ */
+156
View File
@@ -0,0 +1,156 @@
/******************************************************************************
* @file app_relay.c
* @brief 继电器:请求等过零执行;超时则关强断、开放弃
* @author cyWu <1917507415@qq.com>
* @date 2026.09.04
* @version V1.1.0
* @history
* - V1.1.0, 2026.09.04, cyWu, 去掉未使用计数,精简注释
* - V1.0.0, 2026.09.04, cyWu, 首次发布
******************************************************************************/
#include "system/includes.h"
#include "app_relay.h"
#include "bsp_hw.h"
#include "board_pin.h"
#include "jb_product.h"
#define LOG_TAG_CONST APP
#define LOG_TAG "[RELAY]"
#define LOG_INFO_ENABLE
#include "debug.h"
typedef struct {
uint8_t request; /**< 上层意图:0=关 1=开 */
uint8_t actual; /**< 过零后的 GPIO 真值 */
} AppRelayCtx_t;
static AppRelayCtx_t s_relay;
/**
* @brief 写继电器 GPIO 并更新 actual(过零 ISR / 超时路径)
* @param on 1=吸合 0=断开
* @return 无
*/
static void relay_apply(uint8_t on)
{
bsp_relay_set(on);
s_relay.actual = on;
}
/**
* @brief 初始化继电器:GPIO 上电默认关;P0 无 VM,请求=实际=关
* @return 无
*/
void app_relay_init(void)
{
s_relay.request = 0;
s_relay.actual = 0;
bsp_relay_set(0);
log_info("relay init, request=off actual=off\n");
}
/**
* @brief 请求加热开/关(手动/APP 写 DP0/定时/联动统一入口)
* @param on 1=开 0=关
* @return 无
*/
void app_relay_request(uint8_t on)
{
on = on ? 1 : 0;
if (on == s_relay.request) {
return; /* 意图没变:不重复发起(避免连续同向请求反复计超时) */
}
s_relay.request = on;
#if BOARD_ZC_ENABLE
if (on == s_relay.actual) {
/* 例如 PC3 已因超时强关(actual=0),收到关请求:无需等沿,直接同步 */
log_info("request %s but actual already %d, sync\n", on ? "ON" : "OFF",
s_relay.actual);
return;
}
log_info("request %s, wait zc edge\n", on ? "ON" : "OFF");
/* 等 app_zc_poll 计数 / bsp_zc 沿回调执行 */
#else
/* 无过零(调试):开不吸合,关立即强断(规格书 7.2 / 4 章) */
if (on) {
gDevData.fault_code = 2;
log_info("zc disabled: open request rejected, stay off\n");
s_relay.request = 0; /* 放弃开请求,回落到实际(不吸合) */
} else {
log_info("zc disabled: force relay off\n");
relay_apply(0);
}
#endif
}
/**
* @brief 过零沿:有 pending 则切 GPIO
* @note FPIN ISR 上下文,禁止 printf / 写 VM / 切 BLE
* @return 无
*/
void app_relay_zc_edge(void)
{
if (s_relay.request == s_relay.actual) {
return; /* 无 pending,仅记录过零(由 app_zc 管超时) */
}
relay_apply(s_relay.request);
/* 7.2:下一次成功过零切变后清设备故障(RAM 写,ISR 安全) */
gDevData.fault_code = 0;
}
/**
* @brief 过零超时:关则强断,开则放弃并保持关,fault_code=2
* @note 20ms 节拍调用;与 FPIN ISR 抢状态,关中断后复检
* @return 无
*/
void app_relay_zc_timeout(void)
{
local_irq_disable();
if (s_relay.request == s_relay.actual) {
local_irq_enable();
return;
}
gDevData.fault_code = 2;
if (s_relay.request) {
/* 开请求超时:禁止无过零吸合,放弃本次开请求,保持关 */
s_relay.request = 0;
s_relay.actual = 0;
bsp_relay_set(0);
} else {
/* 关请求超时:强制断开(安全路径,规格书 7.2) */
bsp_relay_set(0);
s_relay.actual = 0;
}
local_irq_enable();
}
/**
* @brief 当前请求态
* @return 1=请求开,0=请求关
*/
uint8_t app_relay_get_request(void)
{
return s_relay.request;
}
/**
* @brief 当前实际吸合态
* @return 1=已吸合,0=已断开
*/
uint8_t app_relay_get_actual(void)
{
return s_relay.actual;
}
/**
* @brief 是否已同步(无 pending
* @return 1=request==actual
*/
uint8_t app_relay_synced(void)
{
return (s_relay.request == s_relay.actual) ? 1 : 0;
}
+57
View File
@@ -0,0 +1,57 @@
/******************************************************************************
* @file app_relay.h
* @brief 继电器:request 等过零落到 actual;灯和 DP0 读 actual
* @author cyWu <1917507415@qq.com>
* @date 2026.09.04
* @version V1.1.0
******************************************************************************/
#ifndef __APP_RELAY_H__
#define __APP_RELAY_H__
#include <stdint.h>
/**
* @brief 初始化继电器(默认关)
* @return 无
*/
void app_relay_init(void);
/**
* @brief 请求开/关(短按 / DP0 / 定时 / 联动统一入口)
* @param on 1=开(等过零)0=关(等过零)
* @return 无
*/
void app_relay_request(uint8_t on);
/**
* @brief 过零沿回调:有 pending 则切 GPIO
* @return 无
*/
void app_relay_zc_edge(void);
/**
* @brief 过零超时:关强断 / 开放弃,fault_code=2
* @return 无
*/
void app_relay_zc_timeout(void);
/**
* @brief 当前请求
* @return 1=开 0=关
*/
uint8_t app_relay_get_request(void);
/**
* @brief 实际吸合态(DP0 / 灯效)
* @return 1=吸合 0=断开
*/
uint8_t app_relay_get_actual(void);
/**
* @brief request 是否已等于 actual
* @return 1=已同步
*/
uint8_t app_relay_synced(void);
#endif /* __APP_RELAY_H__ */
+317
View File
@@ -0,0 +1,317 @@
/******************************************************************************
* @file app_runrec.c
* @brief CBR0 31 天运行记录(规格书 13.1 节,DP20 RO124B
* @author cyWu <1917507415@qq.com>
* @date 2026.09.04
* @version V1.1.0
* @history
* - V1.1.0, 2026.09.04, cyWu, P2 完整实现(实际吸合折秒 + 跨天旋转落盘)
* - V1.0.0, 2026.09.04, cyWu, P0 骨架
*
* @note 什么叫在运行:过零后继电器**实际已吸合**app_relay_get_actual==1),
* pending 开还没过零不算(13.1)。日期轴:未对时只往 slot[0] 加、
* 不旋转;对时后按本地日历左移;掉电再上电先由 app_nvm_load 灌 VM。
*
* ⚠ 上下文分工(勿再踩复位事故):
* - app_runrec_tick20ms 中断):只把在吸合的毫秒折成 pending 秒,
* 不碰 s_rr.rec[]、不写 flash。
* - app_runrec_rotate_if_needed500ms 任务):收下 pending 加到
* slot[0]、每拍灌 DP20、跨天旋转、满 600s / 跨天 / 首次对时才
* 写 VM。拔电没有软关机钩子,接受最多丢不满 10 分钟的秒数。
******************************************************************************/
#include <string.h>
#include "system/includes.h" /* local_irq_disable / local_irq_enable */
#include "app_runrec.h"
#include "app_relay.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_RREC]"
#define LOG_INFO_ENABLE
#include "debug.h"
#if BOARD_RUNREC_ENABLE
/** 运行记录上下文:31 格日历 + ISR/任务两侧的折秒状态 */
typedef struct {
uint32_t rec[BOARD_RUN_RECORD_DAYS]; /**< slot[0]=今天 … slot[30]=前 30 天(任务侧) */
uint32_t ymd; /**< slot[0] 对应本地日期 yyyymmdd0=未对时 */
uint16_t ms; /**< 不足 1 秒的毫秒零头(仅 ISR) */
volatile uint16_t pending_sec; /**< ISR 折秒,任务收下后加到 slot[0] */
uint16_t unsaved; /**< 距上次落盘累计的运行秒数(仅任务) */
} AppRunrecCtx_t;
static AppRunrecCtx_t s_rr;
#endif
/**
* @brief 公历日期 → 天数序号(Howard Hinnant 的 days_from_civil
* @param y 年(如 2026
* @param m 月 1~12
* @param d 日 1~31
* @return 距 1970-01-01 的天数(可为负)
*/
static int32_t runrec_days_from_civil(int y, int m, int d)
{
int era;
int yoe;
int doy;
int doe;
y -= (m <= 2) ? 1 : 0;
era = ((y >= 0) ? y : (y - 399)) / 400;
yoe = y - era * 400; /* [0,399] */
doy = (153 * (m + ((m > 2) ? -3 : 9)) + 2) / 5 + d - 1; /* [0,365] */
doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; /* [0,146096]*/
return (int32_t)(era * 146097L + (long)doe - 719468L);
}
/**
* @brief yyyymmdd 压缩日期 → 天数序号
* @param ymd yyyymmdd(如 20260904
* @return 天数序号
*/
static int32_t runrec_ymd_to_days(uint32_t ymd)
{
return runrec_days_from_civil((int)(ymd / 10000u),
(int)((ymd / 100u) % 100u),
(int)(ymd % 100u));
}
/**
* @brief 初始化运行记录
* @return 无
*/
void app_runrec_init(void)
{
#if !BOARD_RUNREC_ENABLE
log_info("runrec disabled\n");
return;
#else
memset(&s_rr, 0, sizeof(s_rr));
app_runrec_pack(gDevData.run_record); /* DP20 报全 0 */
log_info("runrec init, %u days\n", (unsigned)BOARD_RUN_RECORD_DAYS);
#endif
}
#if BOARD_RUNREC_ENABLE
/**
* @brief 把 ISR 折好的秒数加到 slot[0],满 10 分钟则请求写 VM
* @return 1=需要立刻写运行记录 item
*/
static uint8_t runrec_apply_pending(void)
{
uint16_t sec;
uint8_t need_vm = 0;
local_irq_disable();
sec = s_rr.pending_sec;
s_rr.pending_sec = 0;
local_irq_enable();
if (sec == 0) {
return 0;
}
s_rr.rec[0] += sec;
s_rr.unsaved = (uint16_t)(s_rr.unsaved + sec);
if (s_rr.unsaved >= BOARD_RUN_RECORD_SAVE_SEC) {
s_rr.unsaved = (uint16_t)(s_rr.unsaved % BOARD_RUN_RECORD_SAVE_SEC);
need_vm = 1;
}
if (need_vm) {
log_info("runrec %us reached, save vm\n", (unsigned)BOARD_RUN_RECORD_SAVE_SEC);
}
return need_vm;
}
#endif /* BOARD_RUNREC_ENABLE */
/**
* @brief 20ms 中断上下文节拍:继电器实际吸合才折秒到 pending,不碰 s_rr.rec
* @param dt 距上次调用的毫秒数
* @return 无
*/
void app_runrec_tick(uint16_t dt)
{
#if !BOARD_RUNREC_ENABLE
(void)dt;
return;
#else
if (!app_relay_get_actual()) {
/* 没在加热:不累加,并丢弃不足 1 秒的零头(13.1:未吸合不计入) */
s_rr.ms = 0;
return;
}
s_rr.ms = (uint16_t)(s_rr.ms + dt);
while (s_rr.ms >= 1000u) {
s_rr.ms = (uint16_t)(s_rr.ms - 1000u);
s_rr.pending_sec++; /* 任务侧再加到 slot[0],避免与跨天旋转打架 */
}
#endif
}
/**
* @brief 500ms 任务上下文:收下 pending、灌 DP20、跨天旋转、必要时写 VM
* @return 无
*/
void app_runrec_rotate_if_needed(void)
{
#if !BOARD_RUNREC_ENABLE
return;
#else
struct sys_time t;
uint32_t ymd;
int32_t diff;
int i;
uint8_t need_vm = runrec_apply_pending();
if (usr_rtc_get_time(&t)) {
ymd = (uint32_t)t.year * 10000u + (uint32_t)t.month * 100u + (uint32_t)t.day;
if (s_rr.ymd == 0) {
/* 首次对时:slot[0] 已是当天累计值,只登记日期,不旋转不清零 */
s_rr.ymd = ymd;
need_vm = 1;
log_info("runrec rtc synced, ymd=%lu\n", (unsigned long)ymd);
} else if (ymd != s_rr.ymd) {
diff = runrec_ymd_to_days(ymd) - runrec_ymd_to_days(s_rr.ymd);
if (diff <= 0) {
s_rr.ymd = ymd; /* 时钟回拨/异常:只纠正日期,不乱移数据 */
} else {
if (diff > BOARD_RUN_RECORD_DAYS) {
diff = BOARD_RUN_RECORD_DAYS; /* 跨太多天:整表作废 */
}
/* 左移 N 天:old[i] → new[i+N],前面 N 格清零 */
for (i = (int)BOARD_RUN_RECORD_DAYS - 1; i >= diff; i--) {
s_rr.rec[i] = s_rr.rec[i - diff];
}
for (i = 0; i < diff; i++) {
s_rr.rec[i] = 0;
}
s_rr.ymd = ymd;
need_vm = 1;
log_info("runrec rotate %ld day(s), ymd=%lu\n",
(long)diff, (unsigned long)ymd);
}
}
}
/* 每拍灌 DP20:读请求 / 10min 全量能拿到当前秒数;差分上报不带本 DP */
app_runrec_pack(gDevData.run_record);
if (need_vm) {
app_nvm_save_runrec(); /* 满 10 分钟或跨天 / 首次对时才写 flash */
}
#endif
}
/**
* @brief 取某天的运行秒数
* @param day 0=今天 … 30=前 30 天
* @return 秒数
*/
uint32_t app_runrec_get(uint8_t day)
{
#if !BOARD_RUNREC_ENABLE
(void)day;
return 0;
#else
if (day >= BOARD_RUN_RECORD_DAYS) {
return 0;
}
return s_rr.rec[day];
#endif
}
/**
* @brief 打包成 DP20 的 124B 载荷(31 × uint32 大端)
* @param out 输出缓冲
* @note 模块关闭时仍输出全 0(13.1:关闭时报全 0),
* 避免 app_nvm 把未初始化栈内存写进 VM 的 RUNREC item
* @return 无
*/
void app_runrec_pack(uint8_t *out)
{
uint8_t i;
#if !BOARD_RUNREC_ENABLE
memset(out, 0, APP_RUNREC_PACK_LEN);
return;
#else
for (i = 0; i < BOARD_RUN_RECORD_DAYS; i++) {
uint32_t v = s_rr.rec[i];
out[(uint8_t)(i * 4u) + 0u] = (uint8_t)(v >> 24);
out[(uint8_t)(i * 4u) + 1u] = (uint8_t)(v >> 16);
out[(uint8_t)(i * 4u) + 2u] = (uint8_t)(v >> 8);
out[(uint8_t)(i * 4u) + 3u] = (uint8_t)(v & 0xFFu);
}
#endif
}
/**
* @brief 从 VM 的 124B 载荷灌回 RAM
* @param in 输入缓冲
* @return 无
*/
void app_runrec_unpack(const uint8_t *in)
{
uint8_t i;
#if !BOARD_RUNREC_ENABLE
(void)in;
return;
#else
for (i = 0; i < BOARD_RUN_RECORD_DAYS; i++) {
s_rr.rec[i] = ((uint32_t)in[(uint8_t)(i * 4u) + 0u] << 24)
| ((uint32_t)in[(uint8_t)(i * 4u) + 1u] << 16)
| ((uint32_t)in[(uint8_t)(i * 4u) + 2u] << 8)
| (uint32_t)in[(uint8_t)(i * 4u) + 3u];
}
app_runrec_pack(gDevData.run_record); /* 开机立刻灌 DP20,不等 500ms */
#endif
}
/**
* @brief slot[0] 对应的本地日期
* @return yyyymmdd0=未对时
*/
uint32_t app_runrec_get_slot0_date(void)
{
#if !BOARD_RUNREC_ENABLE
return 0;
#else
return s_rr.ymd;
#endif
}
/**
* @brief 设置 slot[0] 对应日期
* @param ymd yyyymmdd0=未对时
* @return 无
*/
void app_runrec_set_slot0_date(uint32_t ymd)
{
#if BOARD_RUNREC_ENABLE
s_rr.ymd = ymd;
#else
(void)ymd;
#endif
}
/**
* @brief 清零 31 格与日期(出厂重置)
* @return 无
*/
void app_runrec_reset(void)
{
#if !BOARD_RUNREC_ENABLE
return;
#else
memset(&s_rr, 0, sizeof(s_rr));
app_runrec_pack(gDevData.run_record);
#endif
}
+80
View File
@@ -0,0 +1,80 @@
/******************************************************************************
* @file app_runrec.h
* @brief CBR0 31 天运行记录(第 13.1 节,DP20 RO,124B):实际吸合计秒
* @author cyWu <1917507415@qq.com>
* @date 2026.09.04
* @version V1.0.0P0 = 骨架(BOARD_RUNREC_ENABLE=0P2 填充)
*
* @note 什么叫在运行:过零后继电器**实际已吸合**app_relay_get_actual()==1)。
* 日期轴:未对时只往 slot[0] 加不旋转;对时后按本地日历左移。
* 落盘:每累计 600s 写一次 VM;跨天立刻写。
******************************************************************************/
#ifndef __APP_RUNREC_H__
#define __APP_RUNREC_H__
#include <stdint.h>
#include "board_pin.h"
#define APP_RUNREC_PACK_LEN (BOARD_RUN_RECORD_DAYS * 4u) /**< 124B */
/**
* @brief 初始化(未对时:只往 slot0 加,不旋转)
* @return 无
*/
void app_runrec_init(void);
/**
* @brief 20ms 节拍:继电器实际吸合则累计秒,满 10 分钟打脏(不写 flash)
* @param dt 距上次调用的毫秒数
* @return 无
*/
void app_runrec_tick(uint16_t dt);
/**
* @brief 500ms 任务上下文:收下 pending 秒并灌 DP20;对时后跨天左移旋转
* @return 无
*/
void app_runrec_rotate_if_needed(void);
/**
* @brief 读某天秒数(0=今天)
* @param day 0~30
* @return 秒
*/
uint32_t app_runrec_get(uint8_t day);
/**
* @brief 打包 124B(每格 uint32 大端),灌 gDevData.run_record
* @param out 输出缓冲(≥124B
* @return 无
*/
void app_runrec_pack(uint8_t *out);
/**
* @brief 从 VM 解包 124Buint32 大端)
* @param in 输入缓冲
* @return 无
*/
void app_runrec_unpack(const uint8_t *in);
/**
* @brief slot[0] 对应本地日期(yyyymmdd 打包)
* @return 日期
*/
uint32_t app_runrec_get_slot0_date(void);
/**
* @brief 设置 slot[0] 日期(对时/旋转后写回)
* @param ymd yyyymmdd
* @return 无
*/
void app_runrec_set_slot0_date(uint32_t ymd);
/**
* @brief 清零 31 天记录与日期(出厂重置)
* @return 无
*/
void app_runrec_reset(void);
#endif /* __APP_RUNREC_H__ */
+325
View File
@@ -0,0 +1,325 @@
/******************************************************************************
* @file app_th_scan.c
* @brief CBR0 断连温湿度扫描(规格书第 8 章):500ms 重连广播 ↔ 500ms 扫描
* @author cyWu <1917507415@qq.com>
* @date 2026.09.07
* @version V1.1.6
* @history
* - V1.1.6, 2026.09.07, cyWu, 扫描上报只留 GAP 0xE2
* - V1.0.0, 2026.09.04, cyWu, P2 完整实现(P0/P1 为骨架)
*
* @note 门控(8 章表格):已连接 / 未配对 / 配对中(usr_goto_pair) /
* OTA 中(usr_goto_updata) / 联动配置无效(缸体 0)→ 不扫描,纯广播。
* 进入交替后偶拍重连广播、奇拍扫描(8.2),一拍 = BOARD_ADV_SCAN_SLOT_MS。
*
* 匹配(8.1):名字 == "JBao-"+DP11 缸体号(uint16 十进制,无前导零)
* 且厂商段 0xFFFF 首字节 == DP11 配置版本 → temp_x10=int8℃×10 喂联动;
* 湿度只打日志。名字/版本/解析任何一项不符 → 当没收到,不更新温度、
* 不关加热(保持上一态)。低开销:先滤名字,名字对了才找厂商段。
*
* ⚠ 上报回调跑在 BLE 栈线程;扫描只允许断连态,占用射频期间广播已
* 暂停(usr_le_set_adv_pause),不允许「边广播边扫」。
******************************************************************************/
#include <string.h>
#include "system/includes.h"
#include "app_config.h"
#include "app_main.h" /* usr_var:配对/OTA 流程标志 */
#include "app_th_scan.h"
#include "app_linkage.h"
#include "usr_le_api.h" /* usr_ble_is_connected / usr_get_pair_flag / pause */
#include "board_pin.h"
#include "btstack/le/ble_api.h" /* ble_op_set_scan_param / ble_op_scan_enable2 */
#define LOG_TAG_CONST APP
#define LOG_TAG "[THSCAN]"
#define LOG_INFO_ENABLE
#include "debug.h"
/* adv AD type */
#define TH_SCAN_AD_NAME_SHORT 0x08u /* Shortened Local Name */
#define TH_SCAN_AD_NAME_COMPLETE 0x09u /* Complete Local Name */
#define TH_SCAN_AD_MFR 0xFFu /* Manufacturer Specific Data */
/* 厂商 0xFFFF(小端)后 3B:配置版本 / 温度 int8 / 湿度(8.1 */
#define TH_SCAN_MFR_PAYLOAD_LEN 3u
/* 期望名 "JBao-"(5) + uint16 十进制(<=5) + '\0' */
#define TH_SCAN_EXPECT_MAX 11u
/* 扫描参数:passive;一拍连续听 BOARD_ADV_SCAN_SLOT_MS8.2)。
* 不去重:风扇同一地址先发回连再发 JBao-。单位 0.625ms → ms*8/5500ms→800 */
#define TH_SCAN_SCAN_TYPE 0u
#define TH_SCAN_INTERVAL_UNIT (uint16_t)((BOARD_ADV_SCAN_SLOT_MS * 8u) / 5u)
#define TH_SCAN_WINDOW_UNIT TH_SCAN_INTERVAL_UNIT
/** 扫描模块运行时(全部状态集中在此) */
typedef struct {
uint8_t active; /**< 交替运行中(满足门控后置 1) */
uint8_t scan_on; /**< 控制器扫描已使能(上报过滤用) */
uint32_t tick; /**< 交替拍计数:偶=重连广播,奇=扫描(8.2) */
uint16_t tank_id; /**< 期望缸体号(配置变化时重建 expect) */
uint8_t cfg_ver; /**< 期望配置版本 */
uint8_t expect_len;
char expect[TH_SCAN_EXPECT_MAX]; /**< "JBao-"+十进制缸体号 */
} ThScanCtx_t;
static ThScanCtx_t s_scan;
static void th_scan_disabled_once(void)
{
static uint8_t s_logged;
if (!s_logged) {
s_logged = 1;
log_info("app_th_scan disabled\n");
}
}
/**
* @brief 门控:只有断连已配对、非配对/OTA 流程、联动配置有效才扫描
* @return 1=进入交替;0=纯广播(找回 APP 优先)
*/
static uint8_t th_scan_should_run(void)
{
if (usr_ble_is_connected()) {
return 0;
}
if (!usr_get_pair_flag()) {
return 0;
}
if (usr_var.usr_goto_pair) {
return 0;
}
if (usr_var.usr_goto_updata) {
return 0;
}
if (!app_linkage_is_valid()) { /* 缸体 0 / Tmin>Tmax:无联动可跑,不扫 */
return 0;
}
return 1;
}
/**
* @brief 配置变化时重建期望名(tank/版本取自 DP11 联动配置)
* @return 无
*/
static void th_scan_sync_cfg(void)
{
uint16_t tank = app_linkage_get_tank_id();
uint8_t ver = app_linkage_get_cfg_ver();
char num[6];
uint16_t t;
int n = 0;
if (s_scan.tank_id == tank && s_scan.cfg_ver == ver) {
return;
}
s_scan.tank_id = tank;
s_scan.cfg_ver = ver;
memcpy(s_scan.expect, "JBao-", 5);
s_scan.expect_len = 5;
t = tank;
do { /* 十进制,无前导零 */
num[n++] = (char)('0' + (t % 10u));
t /= 10u;
} while (t);
while (n > 0 && s_scan.expect_len < sizeof(s_scan.expect)) {
s_scan.expect[s_scan.expect_len++] = num[--n];
}
s_scan.expect[s_scan.expect_len] = '\0';
}
/**
* @brief 开扫描(幂等;先由调用方暂停广播,再使能扫描)
* @return 无
*/
static void th_scan_start(void)
{
ble_cmd_ret_e ret;
if (s_scan.scan_on) {
return;
}
ble_op_set_scan_param(TH_SCAN_SCAN_TYPE, TH_SCAN_INTERVAL_UNIT, TH_SCAN_WINDOW_UNIT);
ret = ble_op_scan_enable2(1, 0);
if (ret != BLE_CMD_RET_SUCESS) {
log_info("scan enable fail %d\n", (int)ret);
return;
}
s_scan.scan_on = 1;
}
/**
* @brief 关扫描(幂等)
* @return 无
*/
static void th_scan_stop(void)
{
if (!s_scan.scan_on) {
return;
}
ble_op_scan_enable2(0, 0);
s_scan.scan_on = 0;
}
/**
* @brief 退出交替:停扫、恢复广播,回到纯 adv 策略
* @return 无
*/
static void th_scan_leave(void)
{
th_scan_stop();
usr_le_set_adv_pause(0);
s_scan.active = 0;
s_scan.tick = 0;
}
/**
* @brief 名字是否等于期望 "JBao-{tank}"
* @param data adv 数据
* @param dlen adv 数据长度
* @return 1=匹配
*/
static uint8_t th_scan_name_ok(const uint8_t *data, int dlen)
{
int i = 0;
while (i + 1 < dlen) {
uint8_t alen = data[i];
if (alen == 0 || i + 1 + alen > dlen) {
break;
}
if ((data[i + 1] == TH_SCAN_AD_NAME_COMPLETE ||
data[i + 1] == TH_SCAN_AD_NAME_SHORT) &&
(uint8_t)(alen - 1u) == s_scan.expect_len &&
memcmp(&data[i + 2], s_scan.expect, s_scan.expect_len) == 0) {
return 1;
}
i += 1 + alen;
}
return 0;
}
/**
* @brief 名字已匹配前提下取厂商段;版本相等才更新联动温度
* @note 版本不等 / 厂商段缺失 → 当没收到(保持上一态)
* @param data adv 数据
* @param dlen adv 数据长度
* @return 无
*/
static void th_scan_try_feed(const uint8_t *data, int dlen)
{
int i = 0;
while (i + 1 < dlen) {
uint8_t alen = data[i];
if (alen == 0 || i + 1 + alen > dlen) {
break;
}
if (data[i + 1] == TH_SCAN_AD_MFR && alen - 1u >= 2u + TH_SCAN_MFR_PAYLOAD_LEN) {
const uint8_t *pl = &data[i + 4]; /* 跳过 len/type/2B 厂商号 */
if (data[i + 2] == 0xFFu && data[i + 3] == 0xFFu &&
pl[0] == s_scan.cfg_ver) {
app_linkage_set_scan_temp((int8_t)pl[1]);
}
return;
}
i += 1 + alen;
}
}
/**
* @brief 一包 adv payload 的匹配入口
* @param data AD 结构
* @param dlen 长度
* @return 无
*/
static void th_scan_on_adv(const uint8_t *data, int dlen)
{
if (data == NULL || dlen <= 0) {
return;
}
if (th_scan_name_ok(data, dlen)) {
th_scan_try_feed(data, dlen);
}
}
/**
* @brief 初始化扫描模块
* @return 无
*/
void app_th_scan_init(void)
{
#if BOARD_TH_SCAN_ENABLE
memset(&s_scan, 0, sizeof(s_scan));
log_info("app_th_scan init\n");
#else
th_scan_disabled_once();
#endif
}
/**
* @brief 500ms 任务上下文(usr_timer_loop 内):偶拍广播 / 奇拍扫描
* @return 无
*/
void app_th_scan_tick(void)
{
#if BOARD_TH_SCAN_ENABLE
if (!th_scan_should_run()) {
th_scan_leave();
return;
}
th_scan_sync_cfg();
if (!s_scan.active) {
s_scan.active = 1;
s_scan.tick = 0; /* 8.2:进入先广播一拍,让 APP 优先找回 */
}
if ((s_scan.tick & 1u) == 0u) {
th_scan_stop(); /* 偶:重连广播 */
usr_le_set_adv_pause(0);
} else {
usr_le_set_adv_pause(1); /* 奇:关广播、开扫描 ≤500ms */
th_scan_start();
}
s_scan.tick++;
#else
th_scan_disabled_once();
#endif
}
/**
* @brief GAP_EVENT_ADVERTISING_REPORT(栈线程)
* @note 杰理布局与 wireless_mic 相同:packet[0]=0xE2 [1]=len [2]=adv_report_t
* @param packet HCI 事件包
* @param size 事件包长度
* @return 无
*/
void app_th_scan_report_event(uint8_t *packet, uint16_t size)
{
#if BOARD_TH_SCAN_ENABLE
adv_report_t *rpt;
if (!s_scan.active || !s_scan.scan_on || packet == NULL) {
return;
}
if (size < 12) {
return;
}
rpt = (adv_report_t *)&packet[2];
if (rpt->length == 0 || rpt->length > 31) {
return;
}
th_scan_on_adv(rpt->data, rpt->length);
#else
(void)packet;
(void)size;
#endif
}
+45
View File
@@ -0,0 +1,45 @@
/******************************************************************************
* @file app_th_scan.h
* @brief CBR0 断连温湿度扫描(规格书第 8 章):500ms 重连广播 ↔ 500ms 扫描
* @author cyWu <1917507415@qq.com>
* @date 2026.09.07
* @version V1.1.6
* @history
* - V1.1.6, 2026.09.07, cyWu, 扫描上报只留 GAP 0xE2(杰理包装后的 adv_report_t
* - V1.0.0, 2026.09.04, cyWu, P2 完整实现
*
* @note 只在「断连 + 已配对 + 非配对/OTA 流程 + 联动配置有效(缸体≠0)」
* 时进入 500ms 交替:偶拍重连广播 / 奇拍扫描。匹配规则(8.1):
* 名字 == "JBao-"+本机 DP11 缸体号(uint16 十进制)+ 配置版本相等
* → temp_x10 = int8℃×10;湿度只打日志不参与开停。
* 名字/版本不对或解析失败 → 当没收到,保持上一温度与上一加热态。
******************************************************************************/
#ifndef __APP_TH_SCAN_H__
#define __APP_TH_SCAN_H__
#include <stdint.h>
/**
* @brief 初始化扫描模块
* @return 无
*/
void app_th_scan_init(void);
/**
* @brief 500ms 任务上下文:满足条件时重连广播/扫描各占一拍交替
* @return 无
*/
void app_th_scan_tick(void);
/**
* @brief GAP 扫描上报(栈线程)
* @note 杰理把扫描结果包成 GAP_EVENT_ADVERTISING_REPORT(0xE2)
* packet[2] 起为 adv_report_t。与 wireless_mic 中央示例同一布局。
* @param packet HCI 事件包
* @param size 事件包长度
* @return 无
*/
void app_th_scan_report_event(uint8_t *packet, uint16_t size);
#endif /* __APP_TH_SCAN_H__ */
+282
View File
@@ -0,0 +1,282 @@
/******************************************************************************
* @file app_timer_task.c
* @brief CBR0 定时引擎(规格书第 11 章):7 组 11B 定时窗 + RTC 门闩
* @author cyWu <1917507415@qq.com>
* @date 2026.09.04
* @version V1.3.0
* @history
* - V1.3.0, 2026.09.08, cyWu, 改为灯罩 11B 布局(无模式字节,Byte1=类型)
* - V1.2.1, 2026.09.04, cyWu, RTC 失效/time_en=0 只停不毁单次(防开机吞单次)
* - V1.2.0, 2026.09.04, cyWu, P2 完整引擎(窗开始开/结束关 + RTC 门闩补窗)
* - V1.1.0, 2026.09.04, cyWu, P1 只存配置(mark_dirty
* - V1.0.0, 2026.09.04, cyWu, P0 骨架
*
* @note 运行上下文:app_timer_run 由 usr_timer_loop500ms,任务上下文)
* 调用——窗判断分钟级精度足够,且单次自毁要写 VM(禁止中断上下文)。
* 字节布局(规格书 11.1,无加热垫的模式字节):
* Byte0=使能 Byte1=类型(0单次/1每天/2每周)
* Byte2=星期位图(Bit0=周一…Bit6=周日,仅类型2)
* Byte3-4=开始时分(时/分) Byte5-6=结束时分 Byte7-10=预留原样存。
* 窗开始沿 → 请求开继电器;窗结束沿 → 请求关 + 立刻按 10.3 补判
* 联动(11.1)。跨午夜以开始时刻所在星期判断(11.1)。
* 仲裁(12 章):手动介入后本窗剩余时间不自动抢回,等下一窗开始沿
* 或对时补窗。
******************************************************************************/
#include "system/includes.h"
#include "app_timer_task.h"
#include "app_arbiter.h"
#include "app_linkage.h"
#include "app_relay.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 in_mask; /**< bit i = 第 i 组当前在窗内(重叠 + 单次自毁用) */
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 11 字节配置(规格书 11.1
* @param t 本地时间
* @return 1=在窗内
*/
static uint8_t timer_in_window(const uint8_t *cfg, const struct sys_time *t)
{
uint8_t type = cfg[1];
int16_t start = (int16_t)cfg[3] * 60 + cfg[4];
int16_t end = (int16_t)cfg[5] * 60 + cfg[6];
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[2] & dow_bit)) {
return 0;
}
}
if (start < end) {
return ((now >= start) && (now < end)) ? 1 : 0;
}
return ((now >= start) || (now < end)) ? 1 : 0; /* 跨午夜 */
}
/**
* @brief 自然离开的窗:类型 0 单次清 Byte0 并落盘
* @param left_mask 本拍由在窗→不在窗的组
* @return 无
*/
static void timer_once_expire(uint8_t left_mask)
{
uint8_t i;
for (i = 0; i < 7; i++) {
uint8_t *cfg;
if (!(left_mask & (uint8_t)(1u << i))) {
continue;
}
cfg = timer_cfg_ptr(i);
if (cfg && cfg[0] && (cfg[1] == 0)) {
cfg[0] = 0;
app_nvm_mark_dirty();
log_info("timer[%d] once done, en cleared\n", i + 1);
}
}
}
/**
* @brief 全部窗结束:释放定时占用;仍占着则关灯并补判联动
* @param force_off 1=time_en=0,即使手动打断过也关再按 10.3 补判
* @return 无
*/
static void timer_window_close(uint8_t force_off)
{
s_timer.taken = 0;
s_timer.in_mask = 0;
if (app_arbiter_get_owner() == ARB_OWNER_TIMER) {
app_arbiter_timer_release();
app_relay_request(0);
log_info("timer window out, light=0\n");
} else {
log_info("timer window out (manual taken)\n");
if (force_off) {
app_relay_request(0);
}
}
app_linkage_reeval();
}
/**
* @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.in_mask = 0;
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 in_now = 0;
uint8_t i;
(void)dt;
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:停窗不毁单次(开机 RTC 必无效,不能把未到点的单次吞掉) */
if (!rtc_ok || !gDevData.time_en) {
if (s_timer.taken) {
timer_window_close(1); /* 11.2:未对时 / time_en=0 等同所有窗结束 */
}
s_timer.in_mask = 0;
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)) {
in_now |= (uint8_t)(1u << i);
}
}
/* 自然离开的组:单次自毁(重叠时只清离开的那组,其它组继续) */
timer_once_expire((uint8_t)(s_timer.in_mask & (uint8_t)~in_now));
s_timer.in_mask = in_now;
if (in_now) {
if (!s_timer.taken) {
s_timer.taken = 1;
app_arbiter_timer_take();
app_relay_request(1);
log_info("timer window in, mask=0x%02x, light=1\n", in_now);
}
} else if (s_timer.taken) {
timer_window_close(0);
}
}
/**
* @brief APP 写 DP12 定时总开关:存 gDevData + 触发 VM 持久化
* @param en 0=关 1=开
* @note 引擎关闭时仍保存;time_en=0 立即收口(关灯 + 10.3
* @return 无
*/
void app_timer_set_en(uint8_t en)
{
app_nvm_mark_dirty();
#if BOARD_TIMER_ENABLE
if (!en && s_timer.taken) {
timer_window_close(1); /* 11.2time_en=0 立刻结束所有窗(关灯 + 10.3) */
}
#endif
log_info("time_en=%d (stored)\n", en);
}
/**
* @brief APP 写 DP13~19 定时配置:触发 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);
}
+41
View File
@@ -0,0 +1,41 @@
/******************************************************************************
* @file app_timer_task.h
* @brief CBR0 定时引擎(规格书第 11 章,P27 组 11B 配置 + time_en + RTC 门闩)
* @author cyWu <1917507415@qq.com>
* @date 2026.09.04
* @version V1.1.0P0 = 骨架(BOARD_TIMER_ENABLE=0
******************************************************************************/
#ifndef __APP_TIMER_TASK_H__
#define __APP_TIMER_TASK_H__
#include <stdint.h>
/**
* @brief 初始化定时引擎(RTC 门闩:usr_rtc_get_time 失败等同 time_en=0
* @return 无
*/
void app_timer_init(void);
/**
* @brief 500ms 任务上下文跑定时引擎(窗开始开 / 窗结束关 + 联动立刻重判)
* @param dt 距上次调用的毫秒数(=500)
* @return 无
*/
void app_timer_run(uint16_t dt);
/**
* @brief APP 写 DP12 定时总开关
* @param en 0=关(所有窗立刻视为结束,关灯),1=开
* @return 无
*/
void app_timer_set_en(uint8_t en);
/**
* @brief APP 写 DP13~19 定时配置(11 字节,规格书 11.1)
* @param idx 1~7
* @return 无
*/
void app_timer_set_config(uint8_t idx);
#endif /* __APP_TIMER_TASK_H__ */
+93
View File
@@ -0,0 +1,93 @@
/******************************************************************************
* @file app_zc.c
* @brief 过零业务:20ms 节拍判定 100ms 无沿超时
* @author cyWu <1917507415@qq.com>
* @date 2026.09.04
* @version V1.1.0
* @history
* - V1.1.0, 2026.09.04, cyWu, 精简注释
* - V1.0.0, 2026.09.04, cyWu, 首次发布
******************************************************************************/
#include "system/includes.h"
#include "app_zc.h"
#include "app_relay.h"
#include "bsp_zc.h"
#include "board_pin.h"
#define LOG_TAG_CONST APP
#define LOG_TAG "[APP_ZC]"
#define LOG_INFO_ENABLE
#include "debug.h"
typedef struct {
uint8_t initialized;
uint8_t last_request;
uint8_t timeout_evt; /**< 1=开放弃 2=关强断,任务上下文取走打印 */
uint16_t pending_ms;
} AppZcCtx_t;
static AppZcCtx_t s_zc;
/**
* @brief 初始化过零业务并挂继电器沿回调
* @return 无
*/
void app_zc_init(void)
{
if (s_zc.initialized) {
return;
}
#if BOARD_ZC_ENABLE
bsp_zc_init(app_relay_zc_edge);
s_zc.pending_ms = 0;
s_zc.last_request = app_relay_get_request();
s_zc.initialized = 1;
log_info("zc init ok\n");
#else
log_info("zc disabled\n");
#endif
}
/**
* @brief 20ms 节拍:pending 满 100ms 无沿则超时处理
* @param dt 距上次调用的毫秒数
* @return 无
*/
void app_zc_poll(uint16_t dt)
{
#if !BOARD_ZC_ENABLE
(void)dt;
return;
#endif
if (!s_zc.initialized) {
return;
}
if (app_relay_get_request() != s_zc.last_request) {
s_zc.pending_ms = 0;
s_zc.last_request = app_relay_get_request();
}
if (!app_relay_synced()) {
s_zc.pending_ms = (uint16_t)(s_zc.pending_ms + dt);
if (s_zc.pending_ms >= BOARD_ZC_TIMEOUT_MS) {
s_zc.pending_ms = 0;
s_zc.timeout_evt = app_relay_get_request() ? 1 : 2;
app_relay_zc_timeout();
}
} else {
s_zc.pending_ms = 0;
}
}
/**
* @brief 取走一次超时事件(任务上下文打印)
* @return 0=无 1=开放弃 2=关强断
*/
uint8_t app_zc_consume_timeout(void)
{
uint8_t evt = s_zc.timeout_evt;
s_zc.timeout_evt = 0;
return evt;
}
+33
View File
@@ -0,0 +1,33 @@
/******************************************************************************
* @file app_zc.h
* @brief 过零业务:装配 bsp_zc20ms 节拍判 100ms 超时
* @author cyWu <1917507415@qq.com>
* @date 2026.09.04
* @version V1.1.0
******************************************************************************/
#ifndef __APP_ZC_H__
#define __APP_ZC_H__
#include <stdint.h>
/**
* @brief 初始化过零业务(挂继电器沿回调)
* @return 无
*/
void app_zc_init(void);
/**
* @brief 20ms 节拍,只判超时,不扫沿
* @param dt 距上次调用的毫秒数
* @return 无
*/
void app_zc_poll(uint16_t dt);
/**
* @brief 取走一次超时事件
* @return 0=无 1=开放弃 2=关强断
*/
uint8_t app_zc_consume_timeout(void);
#endif /* __APP_ZC_H__ */
+265
View File
@@ -0,0 +1,265 @@
/******************************************************************************
* @file key.c
* @brief 按键状态机实现
* @author cyWu <1917507415@qq.com>
* @date 2026.08.25
* @version V1.0.0,首次发布,实现按键短按/长按/释放状态机
******************************************************************************/
#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;
}
+98
View File
@@ -0,0 +1,98 @@
/******************************************************************************
* @file key.h
* @brief 按键状态机:短按 / 长按 / 释放
* @author cyWu <1917507415@qq.com>
* @date 2026.08.25
* @version V1.0.0,首次发布,实现按键短按/长按/释放状态机
******************************************************************************/
#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__ */
+170
View File
@@ -0,0 +1,170 @@
/******************************************************************************
* @file key_manager.c
* @brief 按键:短按切加热;按住 5s 配对 / 12s OTA
* @author cyWu <1917507415@qq.com>
* @date 2026.09.04
* @version V3.1.0
* @history
* - V3.1.0, 2026.09.04, cyWu, 精简注释
* - V3.0.0, 2026.08.31, cyWu, 复用 key.c 长按分级
******************************************************************************/
#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"
/** 模块状态 */
typedef struct {
uint8_t initialized;
uint8_t boot_key_lock; /**< 1=上电时键还按着,等松开才认短按(不挡长按分级) */
uint8_t ota_fired; /**< 1=已到点触发 OTA,本次按住松手不再提交配对 */
} 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 短按(SHORT_Event,松手时刻 <5s):上电未松手前忽略,否则切加热
* @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 长按到点(LONG_Event,满 5s 仍按住):挂配对提示灯
* @note ⚠ 跑在 usr_timer 20ms(硬件定时器中断上下文):只准打标志 + 切灯效
* 事件(纯 RAM)。清绑定(写 VM)留给 app_function_loop(任务上下文)。
* @return 无
*/
static void key_user_pair_hint(void)
{
usr_on_key_pair_hint();
}
/**
* @brief 连按到点(LAST_Event,满 12s 仍按住,到点即触发不等松手):进 OTA
* @note ⚠ 同上,只置标志;usr_app_ota_init 由 app_function_loop 执行。
* key.c 的 keyHandle 对 LAST_Event 不清 eventType,松手前每 20ms 都会
* 再派发一次,这里用 ota_fired 挡住重复调用。
* @return 无
*/
static void key_user_ota_start(void)
{
if (s_km.ota_fired) {
return;
}
s_km.ota_fired = 1;
usr_on_key_ota_start();
}
/**
* @brief 释放(RELEASE_Event,只有按满 ≥5s 才会触发):未进 OTA 则提交配对
* @note 按满 12s 已触发 OTA 的这次按住,松手不再提交配对(ota_fired 拦住)。
* @return 无
*/
static void key_user_release(void)
{
s_km.boot_key_lock = 0;
if (!s_km.ota_fired) {
usr_on_key_pair_start();
}
s_km.ota_fired = 0;
}
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_PAIR_MS, /* 满 5s 仍按住 -> longPressCb */
.fsm.keyLastTime = BOARD_KEY_OTA_MS, /* 满 12s 仍按住 -> lastPressCb */
.fsm.keyReadValue = key_user_read,
.func.ShortPressCb = key_user_short,
.func.releasePressCb = key_user_release,
.func.longPressCb = key_user_pair_hint,
.func.lastPressCb = key_user_ota_start,
},
};
/**
* @brief 装载按键表
* @return 无
*/
void user_key_init(void)
{
if (s_km.initialized) {
return;
}
keyParaInit(s_keys);
s_km.boot_key_lock = 0;
s_km.ota_fired = 0;
s_km.initialized = 1;
}
/**
* @brief 上电后:键还按着则锁住短按,松开才认短按
* @note 只锁短按,**不锁长按分级**:上电就按住 5s 进配对是受支持的用法。
* @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();
}
+65
View File
@@ -0,0 +1,65 @@
/******************************************************************************
* @file key_manager.h
* @brief 按键:短按切加热;按住 5s 配对 / 12s OTA
* @author cyWu <1917507415@qq.com>
* @date 2026.09.04
* @version V3.1.0
******************************************************************************/
#ifndef __KEY_MANAGER_H__
#define __KEY_MANAGER_H__
#include "key.h"
/**
* @brief 业务层短按回调(app_function 实现)
* @return 无
*/
void usr_on_key_short(void);
/**
* @brief 业务层"按住满 5s"回调(app_function 实现):切配对提示灯,尚未清绑定
* @return 无
*/
void usr_on_key_pair_hint(void);
/**
* @brief 业务层"按满 5s 后松手"回调(app_function 实现):提交配对
* @note 只置标志;清绑定(写 VM)在 app_function_loop(任务上下文)执行
* @return 无
*/
void usr_on_key_pair_start(void);
/**
* @brief 业务层"按住满 12s"回调(app_function 实现):到点即进 OTA
* @note 只置标志;usr_app_ota_init 在 app_function_loop(任务上下文)执行
* @return 无
*/
void usr_on_key_ota_start(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__ */
+328
View File
@@ -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
+123
View File
@@ -0,0 +1,123 @@
/******************************************************************************
* @file led.h
* @brief LED 事件框架:按通道掩码独立开关,红绿可同时亮
* @author cyWu <1917507415@qq.com>
* @date 2026.08.25
* @version V1.1.0
* @history
* - V1.0.0, 2026.08.25, cyWu, 首次发布,实现按通道掩码独立开关的 LED 事件框架
* - V1.1.0, 2026.08.27, cyWu, 新增 LED_EVENT_BOOT_REJECT(拒绝开机低电闪),
* 与运行时 LED_EVENT_EMPTY 参数解耦,各自独立可调
******************************************************************************/
#ifndef __LED_H__
#define __LED_H__
#include <stdint.h>
#include <string.h>
/*============================================================================*/
/* 宏定义 */
/*============================================================================*/
#define BLINK_ENABLE 1
#define ALTERNATING_BLINK_ENABLE 1
#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_HEAT_OFF, /**< 跟继电器-关:绿灯常亮,红灯灭(低优先级) */
LED_EVENT_HEAT_ON, /**< 跟继电器-开:红灯常亮,绿灯灭(低优先级) */
LED_EVENT_PAIRING, /**< 按住满5s提示/配对中:绿灯100ms快闪,红灯灭 */
LED_EVENT_PAIR_OK, /**< 配对成功:绿灯常亮若干秒,再回跟继电器 */
LED_EVENT_PAIR_FAIL, /**< 配对失败:红绿交替若干秒,再回跟继电器 */
LED_EVENT_OTA, /**< OTA:红绿交替,直到升级结束/复位(最高) */
LED_EVENT_MAX
} LED_Event_t;
/**
* 优先级:高者覆盖低者(handler 返回 1 会拦下更低优先级)。
* 加热垫红绿互斥:HEAT 只占最低槽,配对/结果/OTA 事件添加前先全清灯,
* 结束后 app_led_run 按继电器实际态自动重新挂 HEAT(无需显式恢复)。
*/
typedef enum {
PRIORITY_1 = 0, /**< 跟继电器(HEAT_OFF / HEAT_ON */
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__ */
+188
View File
@@ -0,0 +1,188 @@
/******************************************************************************
* @file led_manager.c
* @brief CBR0 红绿 LED 事件表:跟继电器 / 配对 / OTA(规格书第 6 章)
* @author cyWu <1917507415@qq.com>
* @date 2026.09.04
* @version V1.0.0P0 Bring-up(由门夹双通道事件表裁剪:删电池/充电/模式事件)
*
* @note 灯罩红绿互斥(一次只一种图案),高事件(配对/结果/OTA)添加前
* app_led 层先全清事件+灭灯;事件结束自删后 app_led_run 按继电器
* 实际态自动重新挂 HEAT 事件。
******************************************************************************/
#include "led_manager.h"
#include "system/includes.h"
#include "bsp_hw.h"
#include "board_pin.h"
static uint8_t led_heat_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_RED)) {
bsp_led_red_set(state);
}
if (led & LED_CHANNEL(LED_CH_GREEN)) {
bsp_led_green_set(state);
}
}
/**
* @brief 跟继电器常亮:返回 1 占住最低槽
* @param led_config 配置
* @return 1
*/
static uint8_t led_heat_handler(LED_Config_t *led_config)
{
(void)LED_Update(led_config);
return 1;
}
/**
* @brief 配对绿快闪(无限):返回 1 拦下更低优先级,配对期间只此一灯
* @param led_config 配置
* @return 1
*/
static uint8_t led_pairing_handler(LED_Config_t *led_config)
{
(void)LED_Update(led_config);
return 1;
}
/**
* @brief 配对成功常绿若干秒:到点自删,灯交回 app_led_run
* @param led_config 配置
* @return 1=还在亮,0=已自删
*/
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 0;
}
return 1;
}
/**
* @brief 配对失败红绿交替若干秒:到点自删
* @param led_config 配置
* @return 1=还在跑,0=已自删
*/
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 0;
}
return 1;
}
/**
* @brief OTA 红绿交替(无限):最高优先级,拦下其它一切
* @param led_config 配置
* @return 1
*/
static uint8_t led_ota_handler(LED_Config_t *led_config)
{
(void)LED_Update(led_config);
return 1;
}
/**
* @brief 注册回调并装载 CBR0 事件表(GPIO 已在 bsp_hw_init
* @note 事件枚举数值 = 事件表下标(led.c LED_EventDelete 依赖)
* @return 无
*/
void user_led_init(void)
{
static LED_EventTableItem_t led_event_category[LED_EVENT_MAX] = {
[LED_EVENT_HEAT_OFF] = {
/* 加热关(实际断开):绿灯常亮,红灯灭 */
.event = LED_EVENT_HEAT_OFF,
.priority = PRIORITY_1,
.config.ledMask = LED_CHANNEL(LED_CH_GREEN),
.config.state = LED_LIGHT,
.config.runNum = 0, /* 0=一直亮 */
.ledEventHandler = led_heat_handler,
},
[LED_EVENT_HEAT_ON] = {
/* 加热开(实际吸合):红灯常亮,绿灯灭 */
.event = LED_EVENT_HEAT_ON,
.priority = PRIORITY_1,
.config.ledMask = LED_CHANNEL(LED_CH_RED),
.config.state = LED_LIGHT,
.config.runNum = 0,
.ledEventHandler = led_heat_handler,
},
[LED_EVENT_PAIRING] = {
/* 按住满5s提示 / 配对中:绿灯 100ms 快闪(红灯由 app_led 层保证灭) */
.event = LED_EVENT_PAIRING,
.priority = PRIORITY_2,
.config.ledMask = LED_CHANNEL(LED_CH_GREEN),
.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] = {
/* 配对成功:绿灯常亮 3s 后回跟继电器 */
.event = LED_EVENT_PAIR_OK,
.priority = PRIORITY_2,
.config.ledMask = LED_CHANNEL(LED_CH_GREEN),
.config.state = LED_LIGHT,
.config.runNum = (uint16_t)((BOARD_LED_PAIR_OK_MS + 999u) / 1000u),
.ledEventHandler = led_pair_ok_handler,
},
[LED_EVENT_PAIR_FAIL] = {
/* 配对失败:红绿交替 3s 后回跟继电器(半周期 BOARD_LED_ALT_MS */
.event = LED_EVENT_PAIR_FAIL,
.priority = PRIORITY_2,
.config.ledMask = (uint16_t)(LED_CHANNEL(LED_CH_RED) | LED_CHANNEL(LED_CH_GREEN)),
.config.state = LED_ALTERNATING_BLINK,
.config.alternatingBlinkParams.Group[0] = LED_CHANNEL(LED_CH_RED),
.config.alternatingBlinkParams.Group[1] = LED_CHANNEL(LED_CH_GREEN),
.config.alternatingBlinkParams.Cycle = (uint32_t)(BOARD_LED_ALT_MS * 2u),
.config.runNum = (uint16_t)((BOARD_LED_PAIR_FAIL_MS + (BOARD_LED_ALT_MS * 2u) - 1u)
/ (BOARD_LED_ALT_MS * 2u)),
.ledEventHandler = led_pair_fail_handler,
},
[LED_EVENT_OTA] = {
/* OTA:红绿交替一直闪(200ms 半周期),直到升级结束/复位 */
.event = LED_EVENT_OTA,
.priority = PRIORITY_3,
.config.ledMask = (uint16_t)(LED_CHANNEL(LED_CH_RED) | LED_CHANNEL(LED_CH_GREEN)),
.config.state = LED_ALTERNATING_BLINK,
.config.alternatingBlinkParams.Group[0] = LED_CHANNEL(LED_CH_RED),
.config.alternatingBlinkParams.Group[1] = LED_CHANNEL(LED_CH_GREEN),
.config.alternatingBlinkParams.Cycle = (uint32_t)(BOARD_LED_ALT_MS * 2u),
.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_all_off();
}
/**
* @brief 跑一轮 LED 事件
* @return 无
*/
void user_led_handle(void)
{
LED_EventHandle();
}
+32
View File
@@ -0,0 +1,32 @@
/******************************************************************************
* @file led_manager.h
* @brief CBR0 红绿 LED 通道与事件表(跟继电器/配对/OTA)
* @author cyWu <1917507415@qq.com>
* @date 2026.09.04
* @version V1.0.0P0 Bring-up(门夹双通道框架裁剪:红 PA7 / 绿 PA8)
******************************************************************************/
#ifndef __LED_MANAGER_H__
#define __LED_MANAGER_H__
#include "led.h"
typedef enum {
LED_CH_RED = 0,
LED_CH_GREEN,
LED_CH_MAX
} LedChannel_t;
/**
* @brief 注册 GPIO 回调并装载 CBR0 事件表
* @return 无
*/
void user_led_init(void);
/**
* @brief 跑一轮 LED 事件
* @return 无
*/
void user_led_handle(void);
#endif /* __LED_MANAGER_H__ */