From 426bb8dab6d66a2d319210ca4eb99897b0bf27d5 Mon Sep 17 00:00:00 2001 From: Wuchuyuan <1917507415@qq.com> Date: Tue, 8 Sep 2026 11:42:03 +0800 Subject: [PATCH] =?UTF-8?q?P0/P1/P2=E5=8A=9F=E8=83=BD=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=EF=BC=9A=E6=8E=A5=E5=85=A5=E4=B8=9A=E5=8A=A1=E7=BB=84=E5=90=88?= =?UTF-8?q?=E6=A0=B9=EF=BC=88=E6=8C=89=E9=94=AE/LED/=E8=BF=87=E9=9B=B6/?= =?UTF-8?q?=E9=85=8D=E7=BD=91/OTA=EF=BC=89=EF=BC=8C=E8=81=94=E5=8A=A8?= =?UTF-8?q?=E3=80=8111B=E5=AE=9A=E6=97=B6=E3=80=81=E8=BF=90=E8=A1=8C?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E3=80=81=E7=81=AF=E6=B3=A1=E5=AF=BF=E5=91=BD?= =?UTF-8?q?=EF=BC=88DP3/DP4/DP25=EF=BC=89=E3=80=81CBR0=20VM=E3=80=81512?= =?UTF-8?q?=E5=88=86=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AC701N.cbp | 78 ++++ Makefile | 20 ++ apps/earphone/app_main.c | 8 +- apps/usr_app/app_arbiter.c | 113 ++++++ apps/usr_app/app_arbiter.h | 68 ++++ apps/usr_app/app_defer.c | 57 +++ apps/usr_app/app_defer.h | 59 ++++ apps/usr_app/app_function.c | 178 ++++++++++ apps/usr_app/app_function.h | 68 ++++ apps/usr_app/app_led.c | 190 ++++++++++ apps/usr_app/app_led.h | 71 ++++ apps/usr_app/app_light_life.c | 207 +++++++++++ apps/usr_app/app_light_life.h | 65 ++++ apps/usr_app/app_linkage.c | 315 +++++++++++++++++ apps/usr_app/app_linkage.h | 96 +++++ apps/usr_app/app_nvm.c | 432 +++++++++++++++++++++++ apps/usr_app/app_nvm.h | 85 +++++ apps/usr_app/app_pair.c | 74 ++++ apps/usr_app/app_pair.h | 43 +++ apps/usr_app/app_relay.c | 156 ++++++++ apps/usr_app/app_relay.h | 57 +++ apps/usr_app/app_runrec.c | 317 +++++++++++++++++ apps/usr_app/app_runrec.h | 80 +++++ apps/usr_app/app_th_scan.c | 325 +++++++++++++++++ apps/usr_app/app_th_scan.h | 45 +++ apps/usr_app/app_timer_task.c | 282 +++++++++++++++ apps/usr_app/app_timer_task.h | 41 +++ apps/usr_app/app_zc.c | 93 +++++ apps/usr_app/app_zc.h | 33 ++ apps/usr_app/key.c | 265 ++++++++++++++ apps/usr_app/key.h | 98 +++++ apps/usr_app/key_manager.c | 170 +++++++++ apps/usr_app/key_manager.h | 65 ++++ apps/usr_app/led.c | 328 +++++++++++++++++ apps/usr_app/led.h | 123 +++++++ apps/usr_app/led_manager.c | 188 ++++++++++ apps/usr_app/led_manager.h | 32 ++ apps/usr_jb_proto/Protocol/jb_product.c | 349 +++++++----------- apps/usr_jb_proto/Protocol/jb_protocol.c | 37 +- apps/usr_jb_proto/Protocol/jb_protocol.h | 6 +- apps/usr_jb_proto/usr_jb_main.c | 181 +++++++++- apps/usr_le_code/bleproto_packer.c | 96 +++-- apps/usr_le_code/bleproto_packer.h | 12 +- apps/usr_le_code/usr_le_adv.c | 68 +++- apps/usr_le_code/usr_le_api.h | 19 + apps/usr_le_code/usr_le_recieve.c | 22 +- apps/usr_periph/board_pin.h | 137 +++++++ apps/usr_periph/bsp_hw.c | 177 ++++++++++ apps/usr_periph/bsp_hw.h | 77 ++++ apps/usr_periph/bsp_zc.c | 192 ++++++++++ apps/usr_periph/bsp_zc.h | 55 +++ dp_definition.json | 2 +- 52 files changed, 6042 insertions(+), 313 deletions(-) create mode 100644 apps/usr_app/app_arbiter.c create mode 100644 apps/usr_app/app_arbiter.h create mode 100644 apps/usr_app/app_defer.c create mode 100644 apps/usr_app/app_defer.h create mode 100644 apps/usr_app/app_function.c create mode 100644 apps/usr_app/app_function.h create mode 100644 apps/usr_app/app_led.c create mode 100644 apps/usr_app/app_led.h create mode 100644 apps/usr_app/app_light_life.c create mode 100644 apps/usr_app/app_light_life.h create mode 100644 apps/usr_app/app_linkage.c create mode 100644 apps/usr_app/app_linkage.h create mode 100644 apps/usr_app/app_nvm.c create mode 100644 apps/usr_app/app_nvm.h create mode 100644 apps/usr_app/app_pair.c create mode 100644 apps/usr_app/app_pair.h create mode 100644 apps/usr_app/app_relay.c create mode 100644 apps/usr_app/app_relay.h create mode 100644 apps/usr_app/app_runrec.c create mode 100644 apps/usr_app/app_runrec.h create mode 100644 apps/usr_app/app_th_scan.c create mode 100644 apps/usr_app/app_th_scan.h create mode 100644 apps/usr_app/app_timer_task.c create mode 100644 apps/usr_app/app_timer_task.h create mode 100644 apps/usr_app/app_zc.c create mode 100644 apps/usr_app/app_zc.h create mode 100644 apps/usr_app/key.c create mode 100644 apps/usr_app/key.h create mode 100644 apps/usr_app/key_manager.c create mode 100644 apps/usr_app/key_manager.h create mode 100644 apps/usr_app/led.c create mode 100644 apps/usr_app/led.h create mode 100644 apps/usr_app/led_manager.c create mode 100644 apps/usr_app/led_manager.h create mode 100644 apps/usr_periph/board_pin.h create mode 100644 apps/usr_periph/bsp_hw.c create mode 100644 apps/usr_periph/bsp_hw.h create mode 100644 apps/usr_periph/bsp_zc.c create mode 100644 apps/usr_periph/bsp_zc.h diff --git a/AC701N.cbp b/AC701N.cbp index f570e7a..feb43e8 100644 --- a/AC701N.cbp +++ b/AC701N.cbp @@ -177,6 +177,7 @@ + @@ -778,10 +779,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Makefile b/Makefile index 3b8cbd5..7b88fc1 100644 --- a/Makefile +++ b/Makefile @@ -263,6 +263,7 @@ INCLUDES := \ -Iapps/usr_jb_proto/Protocol \ -Iapps/usr_jb_proto/Utils \ -Iapps/usr_periph \ + -Iapps/usr_app \ -I$(SYS_INC_DIR) \ @@ -397,7 +398,26 @@ c_SRC_FILES := \ apps/usr_jb_proto/Utils/jb_common.c \ apps/usr_jb_proto/Utils/jb_ringbuffer.c \ apps/usr_jb_proto/usr_jb_main.c \ + apps/usr_periph/bsp_hw.c \ + apps/usr_periph/bsp_zc.c \ apps/usr_periph/usr_rtc.c \ + apps/usr_app/app_arbiter.c \ + apps/usr_app/app_defer.c \ + apps/usr_app/app_function.c \ + apps/usr_app/app_led.c \ + apps/usr_app/app_light_life.c \ + apps/usr_app/app_linkage.c \ + apps/usr_app/app_nvm.c \ + apps/usr_app/app_pair.c \ + apps/usr_app/app_relay.c \ + apps/usr_app/app_runrec.c \ + apps/usr_app/app_th_scan.c \ + apps/usr_app/app_timer_task.c \ + apps/usr_app/app_zc.c \ + apps/usr_app/key.c \ + apps/usr_app/key_manager.c \ + apps/usr_app/led.c \ + apps/usr_app/led_manager.c \ apps/usr_le_code/ble_proto.c \ apps/usr_le_code/bleproto.c \ apps/usr_le_code/bleproto_packer.c \ diff --git a/apps/earphone/app_main.c b/apps/earphone/app_main.c index 4aca9ac..6b774c2 100644 --- a/apps/earphone/app_main.c +++ b/apps/earphone/app_main.c @@ -327,13 +327,9 @@ void app_main() } /* - * 上电强制进入配对模式: - * 1) usr_goto_pair=1:后续 usr_ble_adv_init() 走绑定广播(encode4bind),不走重连广播 - * 2) usr_clear_pair_info_noreset():清 VM 中三元组,保证 paired_flag=0 - * 注意:须在 BLE 协议栈调用 usr_ble_adv_init() 之前设置;此处不清复位,避免上电死循环复位 + * 配对只由运行态长按 5s(key_manager 分级)置 usr_goto_pair;禁止上电清三元组 + * (规格书 5.1/5.2/14 章:220V 常电上电即开机,已配对走重连广播) */ - usr_var.usr_goto_pair = 1; - usr_clear_pair_info_noreset(); ui_manage_init(); ui_update_status(STATUS_POWERON); diff --git a/apps/usr_app/app_arbiter.c b/apps/usr_app/app_arbiter.c new file mode 100644 index 0000000..e7ec907 --- /dev/null +++ b/apps/usr_app/app_arbiter.c @@ -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; + } +} diff --git a/apps/usr_app/app_arbiter.h b/apps/usr_app/app_arbiter.h new file mode 100644 index 0000000..ee5ab43 --- /dev/null +++ b/apps/usr_app/app_arbiter.h @@ -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 + +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__ */ diff --git a/apps/usr_app/app_defer.c b/apps/usr_app/app_defer.c new file mode 100644 index 0000000..fd04b09 --- /dev/null +++ b/apps/usr_app/app_defer.c @@ -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" + +/** 位掩码,volatile:request() 在中断上下文写,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(); +} diff --git a/apps/usr_app/app_defer.h b/apps/usr_app/app_defer.h new file mode 100644 index 0000000..8094035 --- /dev/null +++ b/apps/usr_app/app_defer.h @@ -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_loop(BLE 线程,任务 + * 上下文,栈充足)里执行。之前每个模块各自发明一个 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 + +/** 延后动作位,新增一种"需要写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__ */ diff --git a/apps/usr_app/app_function.c b/apps/usr_app/app_function.c new file mode 100644 index 0000000..25541c7 --- /dev/null +++ b/apps/usr_app/app_function.c @@ -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.h:u8/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 + +#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、告警清 0(app_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"); +} diff --git a/apps/usr_app/app_function.h b/apps/usr_app/app_function.h new file mode 100644 index 0000000..3688df5 --- /dev/null +++ b/apps/usr_app/app_function.h @@ -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 + +/** + * @brief 初始化功能模块状态 + * @return 无 + */ +void app_function_init(void); + +/** + * @brief 同步 DP(DP0=继电器实际态、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__ */ diff --git a/apps/usr_app/app_led.c b/apps/usr_app/app_led.c new file mode 100644 index 0000000..b90161d --- /dev/null +++ b/apps/usr_app/app_led.c @@ -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 +} diff --git a/apps/usr_app/app_led.h b/apps/usr_app/app_led.h new file mode 100644 index 0000000..2ba1702 --- /dev/null +++ b/apps/usr_app/app_led.h @@ -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 + +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__ */ diff --git a/apps/usr_app/app_light_life.c b/apps/usr_app/app_light_life.c new file mode 100644 index 0000000..94f19ca --- /dev/null +++ b/apps/usr_app/app_light_life.c @@ -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): + * - tick(20ms ISR):只折秒到 s_life.remain,标记 need_sync / + * need_edge(到期),不碰 gDevData / flash。 + * - loop(500ms 任务):60s 节流同步 gDevData.lighe_remain_time; + * 边沿立刻同步 + 推导 gDevData.alarm_code + 立刻落盘。 + * BOARD_LIGHT_LIFE_ENABLE=0:不扣秒、不置到期告警;DP3 走 VM/默认 + * 值原样上报(规格书 0.4 条)。 + ******************************************************************************/ + +#include + +#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 要求任务同步 gDevData(60s 到点) */ + 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 +} diff --git a/apps/usr_app/app_light_life.h b/apps/usr_app/app_light_life.h new file mode 100644 index 0000000..9518238 --- /dev/null +++ b/apps/usr_app/app_light_life.h @@ -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 + +/** + * @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_life(RAM);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__ */ diff --git a/apps/usr_app/app_linkage.c b/apps/usr_app/app_linkage.c new file mode 100644 index 0000000..0a08129 --- /dev/null +++ b/apps/usr_app/app_linkage.c @@ -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==LINKAGE:VM 恢复/手动开的垫 + * 过热时也能关(10.3 + 再武装语义,用例 6/2) + * - V1.0.0, 2026.09.04, cyWu, P2 完整实现(P0/P1 为骨架) + * + * @note 开停只看温度(10.3,本垫过冷开 / 过热关,与风扇相反): + * temp_x10 < Tmin*10 → 请求开加热(T Tmax*10 → 请求关加热(T>Tmax,过热) + * [Tmin,Tmax](含等于)→ 保持上一态(回差) + * 温度源(8 章):BLE 已连接 = 服务器 DP6(set_connected_temp); + * 断连已配对 = 扫描 JBao-{tank}(set_scan_temp);丢包保持上一态。 + * 无任何有效温度 → 联动不改继电器(10.2)。 + * + * 手动作废(12 章,RAM 不落 VM):须先走到「关」条件(T>Tmax) + * 再重新出现 T + +#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)→ 清作废; + * 之后重新出现 TTmax 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 开:TTmax 一律关,与谁开的无关——上电 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 重写 DP11(memcpy 已完成):存 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 服务器下发物理量(float,scale=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; +} diff --git a/apps/usr_app/app_linkage.h b/apps/usr_app/app_linkage.h new file mode 100644 index 0000000..42c0b38 --- /dev/null +++ b/apps/usr_app/app_linkage.h @@ -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.0,P0 = 骨架(BOARD_LINKAGE_ENABLE=0;P2 填充) + * + * @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 + +/** + * @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 重写 DP11(memcpy 已完成):存 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-1,uint16 大端) + * @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__ */ diff --git a/apps/usr_app/app_nvm.c b/apps/usr_app/app_nvm.c new file mode 100644 index 0000000..e01716c --- /dev/null +++ b/apps/usr_app/app_nvm.c @@ -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_config(20B,P2 写) + * CFG_USER_CBR0_TIME =30 定时配置 time_config[7][11](77B) + * CFG_USER_CBR0_RUNREC =31 运行记录 31×uint32 + slot0 日期 + + * 灯泡剩余寿命(132B,13.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 + +#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 章) */ + +/** 状态 item(10 字节) */ +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; + +/** 联动配置 item(20 字节,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; + +/** 运行记录 item(124B 秒数 + 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 写状态 item(heat_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 写联动配置 item(20B 原样落盘;P1 恒 0,P2 由 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 写运行记录 item(31 格秒数 + 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_INFO(item 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 +} diff --git a/apps/usr_app/app_nvm.h b/apps/usr_app/app_nvm.h new file mode 100644 index 0000000..a793796 --- /dev/null +++ b/apps/usr_app/app_nvm.h @@ -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.0,P0 = 空骨架(BOARD_NVM_ENABLE=0) + * + * @note 存:灯泡开关(DP0)、linkage_config(20B, DP11)、time_en(DP12)、 + * time_config1~7(11B, DP13~19)、run_record(DP20:31×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 + +/** + * @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__ */ diff --git a/apps/usr_app/app_pair.c b/apps/usr_app/app_pair.c new file mode 100644 index 0000000..65d3c26 --- /dev/null +++ b/apps/usr_app/app_pair.c @@ -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; +} diff --git a/apps/usr_app/app_pair.h b/apps/usr_app/app_pair.h new file mode 100644 index 0000000..521d5da --- /dev/null +++ b/apps/usr_app/app_pair.h @@ -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 + +/** 配对结果边沿事件,仅在状态变化那一次返回一次 */ +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__ */ diff --git a/apps/usr_app/app_relay.c b/apps/usr_app/app_relay.c new file mode 100644 index 0000000..fecc5bc --- /dev/null +++ b/apps/usr_app/app_relay.c @@ -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; +} diff --git a/apps/usr_app/app_relay.h b/apps/usr_app/app_relay.h new file mode 100644 index 0000000..4cbf711 --- /dev/null +++ b/apps/usr_app/app_relay.h @@ -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 + +/** + * @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__ */ diff --git a/apps/usr_app/app_runrec.c b/apps/usr_app/app_runrec.c new file mode 100644 index 0000000..e316bcb --- /dev/null +++ b/apps/usr_app/app_runrec.c @@ -0,0 +1,317 @@ +/****************************************************************************** + * @file app_runrec.c + * @brief CBR0 31 天运行记录(规格书 13.1 节,DP20 RO,124B) + * @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_tick(20ms 中断):只把在吸合的毫秒折成 pending 秒, + * 不碰 s_rr.rec[]、不写 flash。 + * - app_runrec_rotate_if_needed(500ms 任务):收下 pending 加到 + * slot[0]、每拍灌 DP20、跨天旋转、满 600s / 跨天 / 首次对时才 + * 写 VM。拔电没有软关机钩子,接受最多丢不满 10 分钟的秒数。 + ******************************************************************************/ + +#include + +#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] 对应本地日期 yyyymmdd;0=未对时 */ + 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 yyyymmdd;0=未对时 + */ +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 yyyymmdd;0=未对时 + * @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 +} diff --git a/apps/usr_app/app_runrec.h b/apps/usr_app/app_runrec.h new file mode 100644 index 0000000..62f6be8 --- /dev/null +++ b/apps/usr_app/app_runrec.h @@ -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.0,P0 = 骨架(BOARD_RUNREC_ENABLE=0;P2 填充) + * + * @note 什么叫在运行:过零后继电器**实际已吸合**(app_relay_get_actual()==1)。 + * 日期轴:未对时只往 slot[0] 加不旋转;对时后按本地日历左移。 + * 落盘:每累计 600s 写一次 VM;跨天立刻写。 + ******************************************************************************/ + +#ifndef __APP_RUNREC_H__ +#define __APP_RUNREC_H__ + +#include +#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 解包 124B(uint32 大端) + * @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__ */ diff --git a/apps/usr_app/app_th_scan.c b/apps/usr_app/app_th_scan.c new file mode 100644 index 0000000..32183ca --- /dev/null +++ b/apps/usr_app/app_th_scan.c @@ -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 + +#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_MS(8.2)。 + * 不去重:风扇同一地址先发回连再发 JBao-。单位 0.625ms → ms*8/5(500ms→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 +} diff --git a/apps/usr_app/app_th_scan.h b/apps/usr_app/app_th_scan.h new file mode 100644 index 0000000..d50ca53 --- /dev/null +++ b/apps/usr_app/app_th_scan.h @@ -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 + +/** + * @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__ */ diff --git a/apps/usr_app/app_timer_task.c b/apps/usr_app/app_timer_task.c new file mode 100644 index 0000000..34b8b42 --- /dev/null +++ b/apps/usr_app/app_timer_task.c @@ -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_loop(500ms,任务上下文) + * 调用——窗判断分钟级精度足够,且单次自毁要写 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.2:time_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); +} diff --git a/apps/usr_app/app_timer_task.h b/apps/usr_app/app_timer_task.h new file mode 100644 index 0000000..0a29017 --- /dev/null +++ b/apps/usr_app/app_timer_task.h @@ -0,0 +1,41 @@ +/****************************************************************************** + * @file app_timer_task.h + * @brief CBR0 定时引擎(规格书第 11 章,P2:7 组 11B 配置 + time_en + RTC 门闩) + * @author cyWu <1917507415@qq.com> + * @date 2026.09.04 + * @version V1.1.0,P0 = 骨架(BOARD_TIMER_ENABLE=0) + ******************************************************************************/ + +#ifndef __APP_TIMER_TASK_H__ +#define __APP_TIMER_TASK_H__ + +#include + +/** + * @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__ */ diff --git a/apps/usr_app/app_zc.c b/apps/usr_app/app_zc.c new file mode 100644 index 0000000..0c75cbe --- /dev/null +++ b/apps/usr_app/app_zc.c @@ -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; +} diff --git a/apps/usr_app/app_zc.h b/apps/usr_app/app_zc.h new file mode 100644 index 0000000..bd802db --- /dev/null +++ b/apps/usr_app/app_zc.h @@ -0,0 +1,33 @@ +/****************************************************************************** + * @file app_zc.h + * @brief 过零业务:装配 bsp_zc,20ms 节拍判 100ms 超时 + * @author cyWu <1917507415@qq.com> + * @date 2026.09.04 + * @version V1.1.0 + ******************************************************************************/ + +#ifndef __APP_ZC_H__ +#define __APP_ZC_H__ + +#include + +/** + * @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__ */ diff --git a/apps/usr_app/key.c b/apps/usr_app/key.c new file mode 100644 index 0000000..701b744 --- /dev/null +++ b/apps/usr_app/key.c @@ -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 + +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; +} diff --git a/apps/usr_app/key.h b/apps/usr_app/key.h new file mode 100644 index 0000000..693ddec --- /dev/null +++ b/apps/usr_app/key.h @@ -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 +#include + +/*============================================================================*/ +/* 宏定义 */ +/*============================================================================*/ + +#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__ */ diff --git a/apps/usr_app/key_manager.c b/apps/usr_app/key_manager.c new file mode 100644 index 0000000..6b2fc00 --- /dev/null +++ b/apps/usr_app/key_manager.c @@ -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(); +} diff --git a/apps/usr_app/key_manager.h b/apps/usr_app/key_manager.h new file mode 100644 index 0000000..e28a630 --- /dev/null +++ b/apps/usr_app/key_manager.h @@ -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__ */ diff --git a/apps/usr_app/led.c b/apps/usr_app/led.c new file mode 100644 index 0000000..8fc5823 --- /dev/null +++ b/apps/usr_app/led.c @@ -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 + +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 diff --git a/apps/usr_app/led.h b/apps/usr_app/led.h new file mode 100644 index 0000000..63343e6 --- /dev/null +++ b/apps/usr_app/led.h @@ -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 +#include + +/*============================================================================*/ +/* 宏定义 */ +/*============================================================================*/ + +#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__ */ diff --git a/apps/usr_app/led_manager.c b/apps/usr_app/led_manager.c new file mode 100644 index 0000000..96edfb4 --- /dev/null +++ b/apps/usr_app/led_manager.c @@ -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.0,P0 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(); +} diff --git a/apps/usr_app/led_manager.h b/apps/usr_app/led_manager.h new file mode 100644 index 0000000..7c10d09 --- /dev/null +++ b/apps/usr_app/led_manager.h @@ -0,0 +1,32 @@ +/****************************************************************************** + * @file led_manager.h + * @brief CBR0 红绿 LED 通道与事件表(跟继电器/配对/OTA) + * @author cyWu <1917507415@qq.com> + * @date 2026.09.04 + * @version V1.0.0,P0 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__ */ diff --git a/apps/usr_jb_proto/Protocol/jb_product.c b/apps/usr_jb_proto/Protocol/jb_product.c index cdad178..356dd20 100644 --- a/apps/usr_jb_proto/Protocol/jb_product.c +++ b/apps/usr_jb_proto/Protocol/jb_product.c @@ -1,18 +1,36 @@ /* ================================================================ - * jb_product.c — 用户层实现(请填补空白处) - * 设备: 电子开关-加热灯罩 | 生成时间: 2026-09-08 09:59:54 + * jb_product.c — 用户层实现(薄适配:只写 DP,不写业务算法) + * 设备: 电子开关-加热灯罩 | 生成: 2026-09-08(V1.0.0 → P1 接线) * * ══ 开发流程 ══ * 1. 实现硬件抽象: jbGetTimerCount(定时器由 SOC SDK 提供) - * 发送由 SOC SDK 的 ble_send_data 承担,协议层已直接调用,无需 uartWrite - * 2. userInit() 设置 DP 默认值与外设初始化 - * 3. userHandle() 用户数据采集 - * 4. jbEventProcess() 处理 APP 下发的控制命令 - * 5. 主动上报 / OTA 的网关应答在收到 REPORTCMD_V2 后调用 jbReportAckCheck 清除等待 - * ================================================================ - */ + * 发送由 SOC SDK 的 ble_send_data 承担,协议层已直接调用 + * 2. userInit() 设置 DP 默认值 + * 3. userHandle() 用户数据采集(本机无传感器,DP 由 app_function_poll 同步) + * 4. jbEventProcess() APP 下行:只写 gDevData 并转调 app_*,不在这里写业务 + * + * ⚠ 规格书第 0 章第 2 条:**业务算法一律不进本文件**。 + * 继电器 → app_relay / app_arbiter;定时存储 → app_timer_task; + * 灯泡寿命 → app_light_life;持久化 → app_nvm;出厂重置 → app_function。 + * ⚠ DP0 上报**实际吸合态**:本文件的赋值只表达"手动请求",app_function_poll + * (20ms)会把它覆盖为过零后的真值,协议层差分上报读到的即实际态。 + * ⚠ DP3 剩余寿命 / DP25 告警由 app_light_life 维护并写 gDevData,本文件只做 + * APP 写 DP3/DP4 时的入口转发。 + * ================================================================ */ #include "jb_product.h" +#include "app_relay.h" +#include "app_arbiter.h" +#include "app_linkage.h" +#include "app_timer_task.h" +#include "app_nvm.h" +#include "app_function.h" +#include "app_light_life.h" + +#define LOG_TAG_CONST APP +#define LOG_TAG "[JB_PRD]" +#define LOG_INFO_ENABLE +#include "debug.h" /* ════════════════════════════════════════════════════════════════ * 全局变量 @@ -22,86 +40,61 @@ jb_data_point_t gDevData; static volatile uint32_t timerMs; /* ════════════════════════════════════════════════════════════════ - * userInit — DP 默认值 + * userInit — DP 默认值(剩余寿命/告警在 app_light_life_init 与 + * app_nvm_load 恢复,这里先置 0 与首上电语义一致) * ════════════════════════════════════════════════════════════════ */ void userInit(void) { memset(&gDevData, 0, sizeof(jb_data_point_t)); - gDevData.heat_switch = 0; /* DP0 灯泡开关 0=关 1=开 */ - gDevData.temp_control_switch = 0; /* DP1 温控开关 0=关 1=开 */ - gDevData.light_work_time = 0; /* DP2 灯泡总工作时长 */ - gDevData.lighe_remain_time = 0; /* DP3 灯泡剩余寿命 */ - gDevData.light_reset = 0; /* DP4 灯泡复位 0=关 1=开 */ - memset(gDevData.temp_control_time, 0, 4); /* DP5 温控时间 */ - gDevData.mode = 0; /* MODE_0 (??) */ - gDevData.min_temp = 0.0f; /* DP7 最低温度 (float) */ - gDevData.max_temp = 0.0f; /* DP8 最高温度 (float) */ - gDevData.current_temp = 0.0f; /* DP9 当前温度 (float) */ - gDevData.linkage_delete = 0; /* DP10 联动删除 0=关 1=开 */ - memset(gDevData.linkage_config, 0, 20); /* DP11 联动配置 */ - gDevData.time_en = 0; /* DP12 定时使能 0=关 1=开 */ - memset(gDevData.time_config1, 0, 11); /* DP13 定时配置1 */ + gDevData.heat_switch = 0; /* DP0 灯泡开关 0=关 1=开(由 VM 恢复) */ + gDevData.temp_control_switch = 0; /* DP1 温控开关(本版写忽略) */ + gDevData.light_work_time = 0; /* DP2 灯泡总工作时长(写忽略,上报恒 0) */ + gDevData.lighe_remain_time = 0; /* DP3 灯泡剩余寿命(app_light_life 维护) */ + gDevData.light_reset = 0; /* DP4 灯泡复位(WO,脉冲) */ + memset(gDevData.temp_control_time, 0, 4); /* DP5 温控时间(本版写忽略) */ + gDevData.mode = 0; /* DP6 模式(本版写忽略) */ + gDevData.min_temp = 0.0f; /* DP7 最低温度(本版写忽略) */ + gDevData.max_temp = 0.0f; /* DP8 最高温度(本版写忽略) */ + gDevData.current_temp = 0.0f; /* DP9 当前温度(WO,只收不下发) */ + gDevData.linkage_delete = 0; /* DP10 联动删除(脉冲;P2 接) */ + memset(gDevData.linkage_config, 0, 20); /* DP11 联动配置(P2 接) */ + gDevData.time_en = 0; /* DP12 定时使能(由 VM 恢复) */ + memset(gDevData.time_config1, 0, 11); /* DP13 定时配置1(11B) */ memset(gDevData.time_config2, 0, 11); /* DP14 定时配置2 */ memset(gDevData.time_config3, 0, 11); /* DP15 定时配置3 */ memset(gDevData.time_config4, 0, 11); /* DP16 定时配置4 */ memset(gDevData.time_config5, 0, 11); /* DP17 定时配置5 */ memset(gDevData.time_config6, 0, 11); /* DP18 定时配置6 */ memset(gDevData.time_config7, 0, 11); /* DP19 定时配置7 */ - memset(gDevData.run_record, 0, 124); /* DP20 运行记录 */ - gDevData.heartbeat = 0; /* DP21 心跳 0=关 1=开 */ + memset(gDevData.run_record, 0, 124); /* DP20 运行记录(RO,app_runrec 填) */ + gDevData.heartbeat = 0; /* DP21 心跳(本版忽略) */ gDevData.fault_en = 0; /* DP22 设备故障使能 */ - gDevData.fault_code = 0; /* DP23 设备故障 */ + gDevData.fault_code = 0; /* DP23 设备故障(RO,过零超时=2) */ gDevData.alarm_en = 0; /* DP24 设备告警使能 */ - gDevData.alarm_code = 0; /* DP25 设备告警 */ - gDevData.factory_reset = 0; /* DP26 恢复出厂设置 0=关 1=开 */ + gDevData.alarm_code = 0; /* DP25 设备告警(RO,剩余寿命推导) */ + gDevData.factory_reset = 0; /* DP26 恢复出厂设置(WO,写 1 触发) */ gDevData.reserva_dp1 = 0; /* DP27 预留DP1 */ gDevData.reserva_dp2 = 0; /* DP28 预留DP2 */ gDevData.reserva_dp3 = 0; /* DP29 预留DP3 */ } /* ════════════════════════════════════════════════════════════════ - * userHandle — 用户数据采集 - * 协议层自动检测变化并上报(注意:此处是上报,不是下发控制) + * userHandle — 用户数据采集(500ms 由 usr_timer_loop 调用) + * 本机无传感器:DP0 由 app_function_poll(20ms)同步实际态; + * DP20 运行记录由 app_runrec、DP3/DP25 由 app_light_life 在任务上下文灌; + * 断连扫描温度(P2)只进联动引擎,不填 DP9 * ════════════════════════════════════════════════════════════════ */ void userHandle(void) { - /* ── 可上报 DP (RO / RW): 用户数据采集 ── */ - // gDevData.heat_switch = read_sensor(); /* DP0 灯泡开关 */ - // gDevData.temp_control_switch = read_sensor(); /* DP1 温控开关 */ - // gDevData.light_work_time = read_sensor(); /* DP2 灯泡总工作时长 */ - // gDevData.lighe_remain_time = read_sensor(); /* DP3 灯泡剩余寿命 */ - // memcpy(gDevData.temp_control_time, sensor_read(), 4); /* DP5 温控时间 */ - // gDevData.mode = read_sensor(); /* DP6 模式 */ - // gDevData.min_temp = 23.5f; /* DP7 最低温度 (float, scale=0.1), 直接填物理量 */ - // gDevData.max_temp = 23.5f; /* DP8 最高温度 (float, scale=0.1), 直接填物理量 */ - // gDevData.current_temp = 23.5f; /* DP9 当前温度 (float, scale=0.1), 直接填物理量 */ - // gDevData.linkage_delete = read_sensor(); /* DP10 联动删除 */ - // memcpy(gDevData.linkage_config, sensor_read(), 20); /* DP11 联动配置 */ - // gDevData.time_en = read_sensor(); /* DP12 定时使能 */ - // memcpy(gDevData.time_config1, sensor_read(), 11); /* DP13 定时配置1 */ - // memcpy(gDevData.time_config2, sensor_read(), 11); /* DP14 定时配置2 */ - // memcpy(gDevData.time_config3, sensor_read(), 11); /* DP15 定时配置3 */ - // memcpy(gDevData.time_config4, sensor_read(), 11); /* DP16 定时配置4 */ - // memcpy(gDevData.time_config5, sensor_read(), 11); /* DP17 定时配置5 */ - // memcpy(gDevData.time_config6, sensor_read(), 11); /* DP18 定时配置6 */ - // memcpy(gDevData.time_config7, sensor_read(), 11); /* DP19 定时配置7 */ - // memcpy(gDevData.run_record, sensor_read(), 124); /* DP20 运行记录 */ - // gDevData.fault_en = read_sensor(); /* DP22 设备故障使能 */ - // gDevData.fault_code = read_sensor(); /* DP23 设备故障 */ - // gDevData.alarm_en = read_sensor(); /* DP24 设备告警使能 */ - // gDevData.alarm_code = read_sensor(); /* DP25 设备告警 */ - // gDevData.reserva_dp1 = read_sensor(); /* DP27 预留DP1 */ - // gDevData.reserva_dp2 = read_sensor(); /* DP28 预留DP2 */ - // gDevData.reserva_dp3 = read_sensor(); /* DP29 预留DP3 */ + /* 无本机温度/湿度采集 */ } /* ════════════════════════════════════════════════════════════════ * jbEventProcess — 0x03 APP 控制事件处理(下行 / DP 赋值方向) * - * 协议层自动解包 0x03 帧后调用本函数。 - * info->dp_ids[] = 被控制的 DP ID 列表 - * ctrl 已填入下发的值;复制到 gDevData 并执行动作 + * ⚠ 只做两件事:DP 写入 gDevData + 转调 app_* 模块。业务算法一律 + * 不进本文件(规格书第 0 章第 2 条 / 15 章)。 * ════════════════════════════════════════════════════════════════ */ int8_t jbEventProcess(jb_event_info_t *info, jb_data_point_t *ctrl) { @@ -114,217 +107,141 @@ int8_t jbEventProcess(jb_event_info_t *info, jb_data_point_t *ctrl) { switch (info->dp_ids[i]) { - - /* ── DP0 灯泡开关 (bool) ── */ + /* ── DP0 灯泡开关:写 = 手动抢权,请求开/关(等过零)。 + * gDevData 值会被 app_function_poll 同步为实际吸合态再上报 ── */ case DP_ID_HEAT_SWITCH: - if (ctrl->heat_switch) - { - gDevData.heat_switch = 1; - // TODO: 开 — 执行动作 - } - else - { - gDevData.heat_switch = 0; - // TODO: 关 — 执行动作 - } + gDevData.heat_switch = ctrl->heat_switch ? 1 : 0; + app_arbiter_manual_intervene(); /* 手动动作:联动/定时让位 */ + app_relay_request(gDevData.heat_switch); + log_info("dp0 light request=%d\n", gDevData.heat_switch); break; - /* ── DP1 温控开关 (bool) ── */ - case DP_ID_TEMP_CONTROL_SWITCH: - if (ctrl->temp_control_switch) - { - gDevData.temp_control_switch = 1; - // TODO: 开 — 执行动作 - } - else - { - gDevData.temp_control_switch = 0; - // TODO: 关 — 执行动作 - } + /* ── DP1/DP2/DP5~8:本版写忽略(规格书 9 章 / 15 章)。 + * 不更新 gDevData → 不动作、不回显、不落盘 ── */ + case DP_ID_TEMP_CONTROL_SWITCH: /* DP1 温控开关 */ + case DP_ID_LIGHT_WORK_TIME: /* DP2 灯泡总工作时长(恒 0) */ + case DP_ID_TEMP_CONTROL_TIME: /* DP5 温控时间 */ + case DP_ID_MODE: /* DP6 模式 */ + case DP_ID_MIN_TEMP: /* DP7 最低温度 */ + case DP_ID_MAX_TEMP: /* DP8 最高温度 */ break; - /* ── DP2 灯泡总工作时长 (uint32) ── */ - case DP_ID_LIGHT_WORK_TIME: - gDevData.light_work_time = ctrl->light_work_time; - // TODO: 根据 ctrl->light_work_time 执行动作 - break; - - /* ── DP3 灯泡剩余寿命 (uint32) ── */ + /* ── DP3 灯泡剩余寿命(13.2):APP 写立刻生效,0~0xFFFFFFFF 不封顶。 + * set_remain 内部同步 gDevData + 推导 alarm_code + 立刻落盘 ── */ case DP_ID_LIGHE_REMAIN_TIME: - gDevData.lighe_remain_time = ctrl->lighe_remain_time; - // TODO: 根据 ctrl->lighe_remain_time 执行动作 + app_light_life_set_remain(ctrl->lighe_remain_time); break; - /* ── DP4 灯泡复位 (bool) ── */ + /* ── DP4 灯泡复位(WO,13.2):写 1 → 回额定值 2,592,000、告警 0、 + * 立刻落盘;写 0 无动作;WO 不上报,值清回 0 ── */ case DP_ID_LIGHT_RESET: - if (ctrl->light_reset) - { - gDevData.light_reset = 1; - // TODO: 开 — 执行动作 - } - else - { - gDevData.light_reset = 0; - // TODO: 关 — 执行动作 + if (ctrl->light_reset) { + app_light_life_reset(); } + gDevData.light_reset = 0; break; - /* ── DP5 温控时间 (raw) ── */ - case DP_ID_TEMP_CONTROL_TIME: - memcpy(gDevData.temp_control_time, ctrl->temp_control_time, 4); - // TODO: 根据 gDevData.temp_control_time 执行动作 (长度 4 字节) + /* ── DP9 当前温度(WO):服务器下发 → 联动用温度(10.2/8 章)。 + * 协议层不打包不上报;联动引擎关时也只存 RAM ── */ + case DP_ID_CURRENT_TEMP: + gDevData.current_temp = ctrl->current_temp; + app_linkage_set_connected_temp(ctrl->current_temp); break; - /* ── DP6 模式 (enum) ── */ - case DP_ID_MODE: - gDevData.mode = ctrl->mode; - // TODO: 根据 ctrl->mode 执行动作 - break; - - /* ── DP7 最低温度 (float, scale=0.1) ── */ - case DP_ID_MIN_TEMP: - gDevData.min_temp = ctrl->min_temp; - // TODO: 根据 ctrl->min_temp 执行动作 (已是浮点物理量) - break; - - /* ── DP8 最高温度 (float, scale=0.1) ── */ - case DP_ID_MAX_TEMP: - gDevData.max_temp = ctrl->max_temp; - // TODO: 根据 ctrl->max_temp 执行动作 (已是浮点物理量) - break; - - /* ── DP10 联动删除 (bool) ── */ + /* ── DP10 联动删除(脉冲,10.4):写 1 清配置并让出灯, + * 然后清回 0 再上报;写 0 = 无操作 ── */ case DP_ID_LINKAGE_DELETE: - if (ctrl->linkage_delete) - { - gDevData.linkage_delete = 1; - // TODO: 开 — 执行动作 - } - else - { - gDevData.linkage_delete = 0; - // TODO: 关 — 执行动作 + if (ctrl->linkage_delete) { + app_linkage_delete(); + gDevData.linkage_delete = 0; /* 脉冲:清回 0 供上报 */ } break; - /* ── DP11 联动配置 (raw) ── */ + /* ── DP11 联动配置:20B memcpy + 存 VM + 清手动作废 + 立刻重判(10.5)── */ case DP_ID_LINKAGE_CONFIG: memcpy(gDevData.linkage_config, ctrl->linkage_config, 20); - // TODO: 根据 gDevData.linkage_config 执行动作 (长度 20 字节) + app_linkage_set_config(); break; - /* ── DP12 定时使能 (bool) ── */ + /* ── DP12 定时使能:写 gDevData;引擎每 500ms 读 time_en(11.2)── */ case DP_ID_TIME_EN: - if (ctrl->time_en) - { - gDevData.time_en = 1; - // TODO: 开 — 执行动作 - } - else - { - gDevData.time_en = 0; - // TODO: 关 — 执行动作 - } + gDevData.time_en = ctrl->time_en ? 1 : 0; + app_timer_set_en(gDevData.time_en); break; - /* ── DP13 定时配置1 (raw) ── */ + /* ── DP13~19 定时配置:11B 原样存 gDevData(规格书 11.1); + * 引擎按窗口起止/星期直接读这些字段 ── */ case DP_ID_TIME_CONFIG1: memcpy(gDevData.time_config1, ctrl->time_config1, 11); - // TODO: 根据 gDevData.time_config1 执行动作 (长度 11 字节) + app_timer_set_config(1); break; - - /* ── DP14 定时配置2 (raw) ── */ case DP_ID_TIME_CONFIG2: memcpy(gDevData.time_config2, ctrl->time_config2, 11); - // TODO: 根据 gDevData.time_config2 执行动作 (长度 11 字节) + app_timer_set_config(2); break; - - /* ── DP15 定时配置3 (raw) ── */ case DP_ID_TIME_CONFIG3: memcpy(gDevData.time_config3, ctrl->time_config3, 11); - // TODO: 根据 gDevData.time_config3 执行动作 (长度 11 字节) + app_timer_set_config(3); break; - - /* ── DP16 定时配置4 (raw) ── */ case DP_ID_TIME_CONFIG4: memcpy(gDevData.time_config4, ctrl->time_config4, 11); - // TODO: 根据 gDevData.time_config4 执行动作 (长度 11 字节) + app_timer_set_config(4); break; - - /* ── DP17 定时配置5 (raw) ── */ case DP_ID_TIME_CONFIG5: memcpy(gDevData.time_config5, ctrl->time_config5, 11); - // TODO: 根据 gDevData.time_config5 执行动作 (长度 11 字节) + app_timer_set_config(5); break; - - /* ── DP18 定时配置6 (raw) ── */ case DP_ID_TIME_CONFIG6: memcpy(gDevData.time_config6, ctrl->time_config6, 11); - // TODO: 根据 gDevData.time_config6 执行动作 (长度 11 字节) + app_timer_set_config(6); break; - - /* ── DP19 定时配置7 (raw) ── */ case DP_ID_TIME_CONFIG7: memcpy(gDevData.time_config7, ctrl->time_config7, 11); - // TODO: 根据 gDevData.time_config7 执行动作 (长度 11 字节) + app_timer_set_config(7); break; - /* ── DP21 心跳 (bool) ── */ + /* ── DP20 运行记录:RO,禁止 APP 写(规格书 15 章)── */ + case DP_ID_RUN_RECORD: + break; + + /* ── DP21 心跳:本版忽略 ── */ case DP_ID_HEARTBEAT: - if (ctrl->heartbeat) - { - gDevData.heartbeat = 1; - // TODO: 开 — 执行动作 - } - else - { - gDevData.heartbeat = 0; - // TODO: 关 — 执行动作 - } break; - /* ── DP22 设备故障使能 (uint8) ── */ + /* ── DP22/DP24 使能:原样存(本版不做位级过滤,13 章"若有写入")── */ case DP_ID_FAULT_EN: gDevData.fault_en = ctrl->fault_en; - // TODO: 根据 ctrl->fault_en 执行动作 + app_nvm_mark_dirty(); /* 使能要持久化 */ break; - - /* ── DP24 设备告警使能 (uint8) ── */ case DP_ID_ALARM_EN: gDevData.alarm_en = ctrl->alarm_en; - // TODO: 根据 ctrl->alarm_en 执行动作 + app_nvm_mark_dirty(); break; - /* ── DP26 恢复出厂设置 (bool) ── */ + /* ── DP23/DP25 故障/告警:RO,禁止 APP 写 ── */ + case DP_ID_FAULT_CODE: + case DP_ID_ALARM_CODE: + break; + + /* ── DP26 恢复出厂设置:清业务 VM(留配对)+ RAM 复位(含寿命回 + * 额定 2,592,000、告警 0),不断开不软复位(规格书第 14 章)── */ case DP_ID_FACTORY_RESET: - if (ctrl->factory_reset) - { - gDevData.factory_reset = 1; - // TODO: 开 — 执行动作 - } - else - { - gDevData.factory_reset = 0; - // TODO: 关 — 执行动作 + if (ctrl->factory_reset) { + app_function_factory_reset(); + gDevData.factory_reset = 0; /* WO:清回 0 */ + log_info("dp26 factory reset\n"); } break; - /* ── DP27 预留DP1 (uint32) ── */ + /* ── DP27~29 预留:存 RAM,不实现动作 ── */ case DP_ID_RESERVA_DP1: gDevData.reserva_dp1 = ctrl->reserva_dp1; - // TODO: 根据 ctrl->reserva_dp1 执行动作 break; - - /* ── DP28 预留DP2 (uint32) ── */ case DP_ID_RESERVA_DP2: gDevData.reserva_dp2 = ctrl->reserva_dp2; - // TODO: 根据 ctrl->reserva_dp2 执行动作 break; - - /* ── DP29 预留DP3 (uint32) ── */ case DP_ID_RESERVA_DP3: gDevData.reserva_dp3 = ctrl->reserva_dp3; - // TODO: 根据 ctrl->reserva_dp3 执行动作 break; default: @@ -338,14 +255,13 @@ int8_t jbEventProcess(jb_event_info_t *info, jb_data_point_t *ctrl) * 返回 0=接受升级(协议层应答 JB_OK + MaxDataLen) * 非 0=拒绝,返回值即错误码(JB_OTA_*) * - * ⚠ SOC 侧 OTA 尚未实现:当前仅应答,不执行任何升级动作。 */ + * ⚠ SOC 侧协议 OTA 未实现:拒绝接收,避免 APP 继续推数据。 + * 按键 12s OTA 仍走 usr_app_ota_init(规格书 15 章:协议 OTA 可 stub, + * 禁止假装升级成功后擦用户区)。 */ int8_t jbOnOtaNotify(uint32_t fwVersion, uint32_t fwLength, uint32_t fwCrc32) { (void)fwVersion; (void)fwLength; (void)fwCrc32; - // TODO: 检查 Flash 空间与版本,决定是否接收升级 - // 允许: 返回 JB_OK(协议层应答 JB_OK + MaxDataLen) - // 拒绝: 返回对应 JB_OTA_* 错误码(如 JB_OTA_VER_LOW / JB_OTA_SPACE_ERR) - return JB_OK; + return JB_OTA_STATE_ERR; } /* 0x32 OTA 数据(含分片序号)*/ @@ -353,35 +269,32 @@ void jbOnOtaData(const uint8_t *data, uint16_t len, uint16_t shardIdx, uint16_t totalShards) { (void)data; (void)len; (void)shardIdx; (void)totalShards; - // TODO: 写入 Flash,累计 CRC32 - // 全部数据接收完成后调用 jbSendOtaResult(JB_OK, mcuVersion) + /* TODO(P2): 写入 Flash,累计 CRC32;全部接收后调用 jbSendOtaResult */ } /* 0x36 / 0x38 停止 OTA */ void jbOnOtaStop(void) { - // TODO: 终止 OTA,释放资源 + /* TODO(P2): 终止 OTA,释放资源 */ } -/* 任意 MCU→BLE 请求的 ACK 回调(cmd 标识是哪条请求的应答,result 为结果码) - - SOC 仅保留 OTA 主动请求应答: OTA完成 0x35 / 停止 0x39 - - 可在本回调中设置"XX 完成/失败"标志,供主循环判断"确认成功才继续" */ +/* 任意 MCU→BLE 请求的 ACK 回调 */ void jbOnAck(uint8_t cmd, uint8_t result) { (void)cmd; (void)result; - // TODO: switch(cmd){ case CMD_OTA_COMPLETE_ACK: gOtaDone = (result==JB_OK); break; case CMD_OTA_STOP_MCU_ACK: ... } + /* TODO(P2): switch(cmd){ case CMD_OTA_COMPLETE_ACK: ... } */ } /* ════════════════════════════════════════════════════════════════ * 硬件抽象(按实际平台实现) * ════════════════════════════════════════════════════════════════ */ -/* ── 定时器 ── */ +/* ── 定时器:由 usr_jb_main 的 20ms 节拍按 1ms 步进调用 jbTimerIrq() ── */ void jbTimerIrq(void) { timerMs++; -} /* 在 1ms 中断中调用本函数 */ +} uint32_t jbGetTimerCount(void) { diff --git a/apps/usr_jb_proto/Protocol/jb_protocol.c b/apps/usr_jb_proto/Protocol/jb_protocol.c index b35e35a..41ae2be 100644 --- a/apps/usr_jb_proto/Protocol/jb_protocol.c +++ b/apps/usr_jb_proto/Protocol/jb_protocol.c @@ -211,14 +211,7 @@ static uint16_t jbPackDps(const jb_data_point_t *dp, jbWriteU16BE(&out[valIdx], (uint16_t)_raw); valIdx += 2; } - /* DP9 当前温度 (float, 2B, RO (float, scale=0.1)) */ - if (mask[DP_ID_CURRENT_TEMP / 8] & (1 << (DP_ID_CURRENT_TEMP % 8))) - { - bitmap[DP_ID_CURRENT_TEMP / 8] |= (1 << (DP_ID_CURRENT_TEMP % 8)); - int16_t _raw = (int16_t)(dp->current_temp * 10.0f); - jbWriteU16BE(&out[valIdx], (uint16_t)_raw); - valIdx += 2; - } + /* DP9 当前温度:WO,不上报,不打包 */ /* DP10 联动删除 (bool, 1B, RW) */ if (mask[DP_ID_LINKAGE_DELETE / 8] & (1 << (DP_ID_LINKAGE_DELETE % 8))) { @@ -472,6 +465,20 @@ static void jbUnpackDps(const uint8_t *bitmap, const uint8_t *values, info->dp_ids[info->count++] = DP_ID_MAX_TEMP; } } + /* DP9 当前温度 (float, 2B, WO (float, scale=0.1)) */ + if (bitmap[DP_ID_CURRENT_TEMP / 8] & (1 << (DP_ID_CURRENT_TEMP % 8))) + { + if (valIdx + 2 <= valLen) + { + int16_t _raw = (int16_t)jbReadU16BE(&values[valIdx]); + dp->current_temp = (float)_raw * 0.1f; + valIdx += 2; + } + if (info && info->count < DP_ID_MAX) + { + info->dp_ids[info->count++] = DP_ID_CURRENT_TEMP; + } + } /* DP10 联动删除 (bool, 1B, RW) */ if (bitmap[DP_ID_LINKAGE_DELETE / 8] & (1 << (DP_ID_LINKAGE_DELETE % 8))) { @@ -748,12 +755,7 @@ static uint8_t jbBuildDiffMask(const jb_data_point_t *last, mask[DP_ID_MAX_TEMP / 8] |= (1 << (DP_ID_MAX_TEMP % 8)); hasChange = 1; } - /* DP9 当前温度 */ - if ((int16_t)(last->current_temp * 10.0f) != (int16_t)(cur->current_temp * 10.0f)) - { - mask[DP_ID_CURRENT_TEMP / 8] |= (1 << (DP_ID_CURRENT_TEMP % 8)); - hasChange = 1; - } + /* DP9 当前温度:WO,变化不上报 */ /* DP10 联动删除 */ if (last->linkage_delete != cur->linkage_delete) { @@ -814,12 +816,7 @@ static uint8_t jbBuildDiffMask(const jb_data_point_t *last, mask[DP_ID_TIME_CONFIG7 / 8] |= (1 << (DP_ID_TIME_CONFIG7 % 8)); hasChange = 1; } - /* DP20 运行记录 */ - if (memcmp(last->run_record, cur->run_record, 124)) - { - mask[DP_ID_RUN_RECORD / 8] |= (1 << (DP_ID_RUN_RECORD % 8)); - hasChange = 1; - } + /* DP20 运行记录:不进差分(秒累加会刷 0x07);0x05 读 / 10min 全量仍带 */ /* DP22 设备故障使能 */ if (last->fault_en != cur->fault_en) { diff --git a/apps/usr_jb_proto/Protocol/jb_protocol.h b/apps/usr_jb_proto/Protocol/jb_protocol.h index 9d162c0..39125c8 100644 --- a/apps/usr_jb_proto/Protocol/jb_protocol.h +++ b/apps/usr_jb_proto/Protocol/jb_protocol.h @@ -23,7 +23,7 @@ * ════════════════════════════════════════════════════════════════ */ #define PKT_HEAD_0 0x55 #define PKT_HEAD_1 0xAA -#define MAX_PACKAGE_LEN 128 +#define MAX_PACKAGE_LEN 512 #define JB_OTA_MAX_PAYLOAD 96 /* OTA 单包 Payload 上限(Byte); 须 <= MAX_PACKAGE_LEN - 11(0x32 帧开销) */ #define DP_COUNT 30 @@ -127,7 +127,7 @@ typedef enum DP_ID_MODE = 6, /* 模式 (enum, RW) */ DP_ID_MIN_TEMP = 7, /* 最低温度 (float, RW) */ DP_ID_MAX_TEMP = 8, /* 最高温度 (float, RW) */ - DP_ID_CURRENT_TEMP = 9, /* 当前温度 (float, RO) */ + DP_ID_CURRENT_TEMP = 9, /* 当前温度 (float, WO) */ DP_ID_LINKAGE_DELETE = 10, /* 联动删除 (bool, RW) */ DP_ID_LINKAGE_CONFIG = 11, /* 联动配置 (raw, RW) */ DP_ID_TIME_EN = 12, /* 定时使能 (bool, RW) */ @@ -199,7 +199,7 @@ typedef struct uint8_t mode; /* DP6 模式 */ float min_temp; /* DP7 最低温度 (float, scale=0.1) */ float max_temp; /* DP8 最高温度 (float, scale=0.1) */ - float current_temp; /* DP9 当前温度 (float, scale=0.1) */ + float current_temp; /* DP9 当前温度 (float, scale=0.1, WO) */ uint8_t linkage_delete;/* DP10 联动删除 */ uint8_t linkage_config[20];/* DP11 联动配置 */ uint8_t time_en; /* DP12 定时使能 */ diff --git a/apps/usr_jb_proto/usr_jb_main.c b/apps/usr_jb_proto/usr_jb_main.c index ebf80b1..3422e77 100644 --- a/apps/usr_jb_proto/usr_jb_main.c +++ b/apps/usr_jb_proto/usr_jb_main.c @@ -1,18 +1,53 @@ +/****************************************************************************** + * @file usr_jb_main.c + * @brief CBR0 业务组合根(加热灯罩):只编排调用顺序,不写业务算法 + * @author cyWu <1917507415@qq.com> + * @date 2026.09.04 + * @version V1.2.0 + * @history + * - V1.2.0, 2026.09.08, cyWu, 灯罩适配:接入灯泡寿命模块(规格书 13.2) + * - V1.1.0, 2026.09.04, cyWu, 精简注释 + * - V1.0.0, 2026.09.04, cyWu, P0 组合根 + * + * @note jb_product.c 不写业务。20ms tick 禁止写 VM / 切 BLE; + * 配对、OTA、落盘在 500ms usr_timer_loop(任务上下文)。 + * 灯泡寿命:tick 在 ISR 扣秒,loop 做 60s 节流同步与边沿立刻落盘。 + ******************************************************************************/ + #include "system/includes.h" #include "app_config.h" #include "app_main.h" #include "app_task.h" #include "user_cfg.h" -#include "vm.h" -#include "sys_time.h" #include "jb_protocol.h" #include "jb_product.h" #include "jb_ringbuffer.h" #include "jb_common.h" #include "usr_le_product.h" +#include "usr_le_api.h" +#include "bsp_hw.h" +#include "bsp_zc.h" +#include "app_pair.h" +#include "app_function.h" +#include "app_relay.h" +#include "app_zc.h" +#include "app_led.h" +#include "app_arbiter.h" +#include "app_linkage.h" +#include "app_runrec.h" +#include "app_timer_task.h" +#include "app_th_scan.h" +#include "app_light_life.h" +#include "app_nvm.h" +#include "key_manager.h" -/* 向 usr_le_code 库提供产品身份(换产品只改 jb_protocol.h 宏即可) */ +#define LOG_TAG_CONST APP +#define LOG_TAG "[USR_JB]" +#define LOG_INFO_ENABLE +#include "debug.h" + +/* 向 usr_le_code 提供产品身份(换产品只改 jb_protocol.h 宏即可) */ const uint8_t g_usr_le_prodcode[USR_LE_PRODCODE_LEN] = { JB_CATEGORY_CODE[0], JB_CATEGORY_CODE[1], JB_CATEGORY_CODE[2], JB_CATEGORY_CODE[3] @@ -22,28 +57,144 @@ const uint8_t g_usr_le_pcode[USR_LE_PCODE_LEN] = { JB_PRODUCT_CODE[3], JB_PRODUCT_CODE[4], JB_PRODUCT_CODE[5] }; -void usr_timer_1_ms(); -static int jb_timer_id=0; +#define USR_TIMER_IDLE_MS 20 /**< 全局统一节拍(规格书第 3 章) */ -void usr_jb_init() +static void usr_timer_tick(void *priv); +static void usr_log_boot_version(void); + +/** + * @brief 20ms 业务节拍(硬件定时器中断) + * @param priv 未使用 + * @note 禁止写 VM / 切 BLE / printf + * @return 无 + */ +static void usr_timer_tick(void *priv) { + uint16_t dt = USR_TIMER_IDLE_MS; + uint16_t i; + + (void)priv; + for (i = 0; i < dt; i++) { + jbTimerIrq(); + } + + user_key_scan(dt); /* 内部完成 5s/12s 长按分级,只置标志 */ + user_key_handle(); /* 派发短按:切灯 */ + app_zc_poll(dt); /* 只做 100ms 无沿超时兜底(切变在过零 ISR) */ + app_function_poll(); /* 同步 DP0 = 实际继电器态 */ + app_runrec_tick(dt); /* 实际吸合才累计;满 10 分钟只打脏,不写 flash */ + app_light_life_tick(dt); /* 实际吸合才扣寿命秒;60s 节流只置标志(13.2) */ + app_nvm_tick(dt); /* 变化检测 + 500ms 防抖计时(不写 flash!) */ + app_led_run(dt); /* 灯跟继电器实际态;内部推进配对会话 */ +} + +#if (BOARD_ZC_ENABLE && BOARD_ZC_SW_POLL_1MS) +/** + * @brief 过零 1ms 软件采样兜底 + * @param priv 未使用 + * @return 无 + */ +static void usr_timer_1ms(void *priv) +{ + (void)priv; + bsp_zc_scan_1ms(); +} +#endif + +/** + * @brief 上电打印固件版本与编译时间,方便串口对应当前是哪一包 + * @return 无 + */ +static void usr_log_boot_version(void) +{ + log_info("sw V%d.%d.%d hw V%d.%d.%d build %s %s\n", + JB_MCU_SW_VERSION_MAJOR, JB_MCU_SW_VERSION_MINOR, JB_MCU_SW_VERSION_PATCH, + JB_MCU_HW_VERSION_MAJOR, JB_MCU_HW_VERSION_MINOR, JB_MCU_HW_VERSION_PATCH, + __DATE__, __TIME__); +} + +/** + * @brief 业务初始化(上电即开机) + * @return 无 + */ +void usr_jb_init(void) +{ + usr_log_boot_version(); /* UART 已在 app_main 起来,最先能看到版本 */ + + bsp_hw_init(); + app_led_init(); /* 先于一切业务:灯效事件表要最先装载 */ + jbInit(); userInit(); - if(jb_timer_id==0)jb_timer_id = sys_s_hi_timer_add(NULL, usr_timer_1_ms, TIMER_PERIOD_MS); + + app_relay_init(); /* 执行器(请求/实际态)先于过零与仲裁 */ + app_zc_init(); /* PA1 双沿 → 继电器切变(app_relay 回调) */ + app_arbiter_init(); /* 占用仲裁(手动>定时>联动)先于联动/定时引擎 */ + app_linkage_init(); + app_runrec_init(); + app_timer_init(); + app_th_scan_init(); + app_light_life_init(); /* 剩余寿命先置额定值(VM 有效时下方 nvm_load 覆盖) */ + + app_nvm_init(); + app_nvm_load(); /* 回填灯泡请求/联动/定时/31 天记录/剩余寿命(13.2) */ + + app_function_init(); + + user_key_init(); + /* 上电时键若还按着:只锁短按(避免一上电就切灯),长按 5s/12s 分级照常生效, + * 所以"插电时就按住 5s 进配对"这一用法仍然成立 */ + user_key_lock_if_held(); + + /* priority=1 常驻注册,永不重挂(220V 常电不需要低功耗) */ + usr_timer_add(NULL, usr_timer_tick, USR_TIMER_IDLE_MS, 1); + +#if (BOARD_ZC_ENABLE && BOARD_ZC_SW_POLL_1MS) + /* 过零软件兜底采样:真实 1ms 节拍(P33 中断之外的第二道保险) */ + usr_timer_add(NULL, usr_timer_1ms, 1, 1); +#endif + log_info("timer started, period=%ums, zc_irq=%d, zc_poll_1ms=%d\n", + (unsigned)USR_TIMER_IDLE_MS, BOARD_ZC_USE_IRQ, + (BOARD_ZC_ENABLE && BOARD_ZC_SW_POLL_1MS) ? 1 : 0); } -void usr_timer_1_ms() +/** + * @brief BLE 收到见宝载荷,送入协议环形缓冲 + * @param data 载荷指针 + * @param len 载荷长度,单位字节 + * @return 无 + */ +void usr_jb_le_recieve_data(u8 *data, u16 len) { - jbTimerIrq(); + jbPutData(data, len); } -void usr_jb_le_recieve_data(u8* data,u16 len) -{ - jbPutData(data,len); -} - -void usr_timer_loop()///500ms +/** + * @brief 约 500ms:协议、自动化引擎、配对/OTA 落地(任务上下文) + * @return 无 + */ +void usr_timer_loop(void) { userHandle(); jbProtocolHandle(&gDevData); + app_th_scan_tick(); /* P2:断连时 500ms 重连广播/500ms 扫描交替 */ + app_linkage_run(500); /* P2:联动引擎(任务上下文;仲裁见 app_arbiter) */ + app_timer_run(500); /* P2:定时引擎(RTC 门闩 + 窗判断) */ + app_runrec_rotate_if_needed(); /* P2:跨天旋转/落盘/灌 DP20 */ + app_light_life_loop(); /* 灯泡寿命:60s 节流同步 / 边沿立刻落盘(13.2) */ + app_function_loop(); /* VM 防抖落盘 + 配对/OTA 动作落地 */ + +#if (BOARD_ZC_ENABLE && BOARD_ZC_DIAG_LOG) + /* 台上诊断:确认过零 FPIN 中断真的在跑(edge 持续不涨 = 前端没信号/中断没起来) */ + { + static uint32_t s_zc_cnt_last; + uint32_t cnt = bsp_zc_edge_count(); + uint32_t last = bsp_zc_last_edge_ms(); + uint32_t ago = last ? ((uint32_t)timer_get_ms() - last) : 0xFFFFFFFFu; + log_info("zc diag: edge=%u(+%u) ago=%ums lvl=%d req=%d act=%d\n", + cnt, (uint32_t)(cnt - s_zc_cnt_last), ago, (int)bsp_zc_level(), + (int)app_relay_get_request(), (int)app_relay_get_actual()); + s_zc_cnt_last = cnt; + } +#endif } diff --git a/apps/usr_le_code/bleproto_packer.c b/apps/usr_le_code/bleproto_packer.c index b1c6159..7f08bb2 100644 --- a/apps/usr_le_code/bleproto_packer.c +++ b/apps/usr_le_code/bleproto_packer.c @@ -673,51 +673,89 @@ int bleproto_appdata_serialize(bleproto_appdata_t *appdata, int bleproto_appdata_wrap_raw(uint8_t *txbuf, uint16_t txbuf_size, + uint8_t packetlen, const bleproto_appdata_header_t *header, const uint8_t *payload, uint16_t payload_len) { - uint16_t total; + uint8_t frame_max; + uint16_t totalframe; + uint16_t need; + uint16_t offset = 0; + uint16_t data_offset = 0; + uint16_t i; - if (!txbuf || !header) { - BLEPROTO_T_E("Invalid parameters"); + if (!txbuf || !header || packetlen <= BLEPROTO_APPDATA_HEADR_LEN) { + printf("Invalid parameters"); return -1; } if (payload_len > 0 && !payload) { - BLEPROTO_T_E("Invalid payload"); - return -1; - } - /* 单帧 this_frame_len 为 uint8,超过 255 需走分包 serialize */ - if (payload_len > 255u) { - BLEPROTO_T_E("payload too long for single frame"); + printf("Invalid payload"); return -1; } - total = (uint16_t)(BLEPROTO_APPDATA_HEADR_LEN + payload_len); - if (txbuf_size < total) { - BLEPROTO_T_E("txbuf too short"); + /* 与 serialize 相同:一帧总长 packetlen,其中 8B 头,其余为 payload */ + frame_max = (uint8_t)(packetlen - BLEPROTO_APPDATA_HEADR_LEN); + + if (payload_len == 0) { + totalframe = 1; + } else { + totalframe = (uint16_t)((payload_len + frame_max - 1u) / frame_max); + } + if (totalframe > 255u) { + printf("payload too long, frames %u>255\n", (unsigned)totalframe); return -1; } - /* buf[0]: version[2:0] | datafmt[3:3] | msgtype[7:4] */ - txbuf[0] = UTILS_BITFIELD_SET(header->version, 2, 0) | - UTILS_BITFIELD_SET(header->datafmt, 3, 3) | - UTILS_BITFIELD_SET(header->msgtype, 7, 4); - /* buf[1]: msgid[3:0] | encrypt[4:4] | reserve[7:5] */ - txbuf[1] = UTILS_BITFIELD_SET(header->msgid, 3, 0) | - UTILS_BITFIELD_SET(header->encrypt, 4, 4) | - UTILS_BITFIELD_SET(header->reserve, 7, 5); - txbuf[2] = 1; /* totalframe */ - txbuf[3] = 0; /* frameseq:单帧为 0 */ - txbuf[4] = (uint8_t)payload_len; /* this frame payload len */ - txbuf[5] = header->serviceid; - proto_bytes_put_le16(&txbuf[6], payload_len); - - if (payload_len > 0) { - memcpy(&txbuf[BLEPROTO_APPDATA_HEADR_LEN], payload, payload_len); + need = (uint16_t)((uint16_t)totalframe * BLEPROTO_APPDATA_HEADR_LEN + payload_len); + if (txbuf_size < need) { + printf("txbuf too short %u<%u\n", (unsigned)txbuf_size, (unsigned)need); + return -1; } - return (int)total; + for (i = 0; i < totalframe; i++) { + uint8_t this_len; + uint8_t frameseq; + + if (totalframe == 1u) { + this_len = (uint8_t)payload_len; + frameseq = 0; /* 单帧序号为 0 */ + } else if ((uint16_t)(i + 1u) < totalframe) { + this_len = frame_max; + frameseq = (uint8_t)(i + 1u); /* 分包从 1 起 */ + } else { + this_len = (uint8_t)(payload_len - data_offset); + frameseq = (uint8_t)(i + 1u); + } + + /* buf[0]: version[2:0] | datafmt[3:3] | msgtype[7:4] */ + txbuf[offset + 0] = UTILS_BITFIELD_SET(header->version, 2, 0) | + UTILS_BITFIELD_SET(header->datafmt, 3, 3) | + UTILS_BITFIELD_SET(header->msgtype, 7, 4); + /* buf[1]: msgid[3:0] | encrypt[4:4] | reserve[7:5] */ + txbuf[offset + 1] = UTILS_BITFIELD_SET(header->msgid, 3, 0) | + UTILS_BITFIELD_SET(header->encrypt, 4, 4) | + UTILS_BITFIELD_SET(header->reserve, 7, 5); + txbuf[offset + 2] = (uint8_t)totalframe; + txbuf[offset + 3] = frameseq; + txbuf[offset + 4] = this_len; + txbuf[offset + 5] = header->serviceid; + proto_bytes_put_le16(&txbuf[offset + 6], payload_len); + + offset = (uint16_t)(offset + BLEPROTO_APPDATA_HEADR_LEN); + if (this_len != 0) { + memcpy(&txbuf[offset], &payload[data_offset], this_len); + offset = (uint16_t)(offset + this_len); + data_offset = (uint16_t)(data_offset + this_len); + } + } + + if (totalframe > 1u) { + printf("wrap_raw split %uB -> %u frames, out %uB\n", + (unsigned)payload_len, (unsigned)totalframe, (unsigned)offset); + } + + return (int)offset; } int bleproto_appdata_deserialize(uint8_t * buf, diff --git a/apps/usr_le_code/bleproto_packer.h b/apps/usr_le_code/bleproto_packer.h index 8ec82c1..e4c1d9c 100644 --- a/apps/usr_le_code/bleproto_packer.h +++ b/apps/usr_le_code/bleproto_packer.h @@ -586,17 +586,21 @@ int bleproto_appdata_serialize(bleproto_appdata_t *appdata, uint16_t size); /** - * @brief 将已打包好的 payload 包装成单帧 appdata(不分包) - * @param txbuf 输出缓冲 + * @brief 将已打包好的 payload 包装成 appdata(超过单帧则按 packetlen 分包) + * @param txbuf 输出缓冲(须能放下 每帧8B头 × 帧数 + payload) * @param txbuf_size 输出缓冲大小 + * @param packetlen BLE GATT 单包有效长度,与 serialize 相同(本板 247) * @param header 应用层头(datalen/totalframe/frameseq 由本函数填写) * @param payload 已序列化的 service 数据;可为 NULL(payload_len==0) - * @param payload_len payload 长度,须 <=255(单帧 pthis_frame_len 为 u8) + * @param payload_len 应用数据总长度 * @return >0 总字节数;<0 失败 - * @note 小包场景(JB 55AA 透传等)用此接口,避免 bleproto_appdata_t 大缓冲 + * @note 切帧规则与 serialize 一致:单帧 payload = packetlen - 8; + * 不超过则 totalframe=1、frameseq=0;超过则中间帧填满、末帧剩余, + * frameseq 从 1 递增。避免走 bleproto_appdata_t 大缓冲。 */ int bleproto_appdata_wrap_raw(uint8_t *txbuf, uint16_t txbuf_size, + uint8_t packetlen, const bleproto_appdata_header_t *header, const uint8_t *payload, uint16_t payload_len); diff --git a/apps/usr_le_code/usr_le_adv.c b/apps/usr_le_code/usr_le_adv.c index 9faa88c..dd5337b 100644 --- a/apps/usr_le_code/usr_le_adv.c +++ b/apps/usr_le_code/usr_le_adv.c @@ -2,9 +2,10 @@ * @file usr_le_adv.c * @brief BLE 广播 / 配对 / OTA 广播切换 * @author cyWu <1917507415@qq.com> - * @date 2026.08.19 - * @version V1.1.0 + * @date 2026.09.07 + * @version V1.2.0 * @history + * - V1.2.0, 2026.09.07, cyWu, 切慢广播由 BOARD_BLE_SLOW_ADV_ENABLE / _SEC 控制 * - V1.0.0, 2026.08.03, cyWu, 首次发布 * - V1.1.0, 2026.08.19, cyWu, 对齐模组注释与助手;保留 SOC 上电绑广播策略 ******************************************************************************/ @@ -16,6 +17,7 @@ #include "bleproto.h" #include "bleproto_api.h" #include "usr_le_product.h" +#include "board_pin.h" #define LOG_TAG_CONST APP #define LOG_TAG "[LE_ADV]" @@ -67,6 +69,9 @@ void usr_ble_adv_updata(void); static int le_timer_handle = 0; +/* 广播门控:扫描分时期间暂停广播发送(见 app_th_scan),仅门控不断连 */ +static u8 s_adv_paused = 0; + /** * @brief BLE 是否已连接 * @return 1=已连接,0=未连接 @@ -216,13 +221,53 @@ u8 usr_get_pair_flag(void) return usr_auth_data.paired_flag; } +/** + * @brief 暂停/恢复广播发送(扫描分时射频仲裁,见 app_th_scan) + * @note adv data 仍注册在协议栈,恢复时续发无需重新编码;仅门控发送,不断连接。 + * 只动广播(bt_ble_adv_enable 内部有 con_handle 保护),不动模块使能位, + * 因此已连接时误调不会断开已有链路。 + * @param pause 1=暂停(扫描期让出射频),0=恢复 + */ +void usr_le_set_adv_pause(u8 pause) +{ + extern void bt_ble_adv_enable(u8 enable); + + if (pause == s_adv_paused) { + return; + } + if (pause && usr_ble_is_connected()) { + return; /* 扫描只允许断连态;已连接保持广播 */ + } + s_adv_paused = pause; + bt_ble_adv_enable(pause ? 0 : 1); +} + +/** + * @brief 查询广播是否处于暂停态 + * @return 1=暂停中(扫描期),0=正常广播 + */ +u8 usr_le_adv_is_paused(void) +{ + return s_adv_paused; +} + /** * @brief 周期处理广播刷新(由 500ms 定时器调用) * @note 仅当 usr_adv_flag==1 时:停广播 → 写入 muti_adv_data → 再开广播 */ void usr_ble_adv_updata(void) { +#if BOARD_BLE_SLOW_ADV_ENABLE static u16 usr_goto_slow_adv = 0; + /* usr_ble_adv_updata 周期 500ms → 秒数换拍 */ + const u16 slow_ticks = (u16)((BOARD_BLE_SLOW_ADV_SEC) * (1000u / 500u)); +#endif + + if (usr_le_adv_is_paused()) { + /* 扫描期(app_th_scan 分时):不刷广播、不切快慢,只跑协议周期 */ + usr_timer_loop(); + return; + } if (usr_var.usr_adv_flag) { @@ -234,9 +279,10 @@ void usr_ble_adv_updata(void) ble_trans_module_enable(1); } +#if BOARD_BLE_SLOW_ADV_ENABLE if (!usr_ble_is_connected()) { - /* 未连接:回连快广播(80)持续约 60s(500ms×120)后切慢广播 */ + /* 已配对断连:快广播 80 持续 BOARD_BLE_SLOW_ADV_SEC 后切 800 */ if (usr_get_adv_interval() == 80) { if ((usr_var.usr_goto_pair == 0) && usr_auth_data.paired_flag) @@ -244,13 +290,14 @@ void usr_ble_adv_updata(void) if (usr_var.usr_ota_succ_flag == 0) { usr_goto_slow_adv++; - printf("usr_goto_slow_adv = %d\n", usr_goto_slow_adv); - } - if (usr_goto_slow_adv >= 120) - { - usr_goto_slow_adv = 0; - usr_set_adv_interval(80 * 10); - usr_var.usr_adv_flag = 1; + if ((slow_ticks != 0) && (usr_goto_slow_adv >= slow_ticks)) + { + usr_goto_slow_adv = 0; + usr_set_adv_interval(80 * 10); + usr_var.usr_adv_flag = 1; + log_info("ble adv -> slow after %us\n", + (unsigned)BOARD_BLE_SLOW_ADV_SEC); + } } } } @@ -263,6 +310,7 @@ void usr_ble_adv_updata(void) { usr_goto_slow_adv = 0; } +#endif if (usr_get_ota_status() == 0) { diff --git a/apps/usr_le_code/usr_le_api.h b/apps/usr_le_code/usr_le_api.h index 4ccb3ba..4fe4a6a 100644 --- a/apps/usr_le_code/usr_le_api.h +++ b/apps/usr_le_code/usr_le_api.h @@ -43,12 +43,31 @@ void usr_clear_pair_info(void); */ uint8_t usr_get_pair_flag(void); +/** + * @brief 查询 BLE 是否已连接 + * @return 1=已连接,0=未连接 + */ +uint8_t usr_ble_is_connected(void); + /** * @brief 启动应用侧 OTA 流程 * @note 置位 usr_goto_updata,并创建 1s 周期定时器驱动断连与 OTA 广播切换 */ void usr_app_ota_init(void); +/** + * @brief 暂停/恢复广播发送(扫描分时射频仲裁,见 app_th_scan) + * @note 仅门控发送,不断开连接;恢复后按注册数据续发,无需重新编码 + * @param pause 1=暂停(扫描期让出射频),0=恢复 + */ +void usr_le_set_adv_pause(uint8_t pause); + +/** + * @brief 查询广播是否处于暂停态 + * @return 1=暂停中(扫描期),0=正常广播 + */ +uint8_t usr_le_adv_is_paused(void); + #ifdef __cplusplus } #endif diff --git a/apps/usr_le_code/usr_le_recieve.c b/apps/usr_le_code/usr_le_recieve.c index 4e0ddfe..074e605 100644 --- a/apps/usr_le_code/usr_le_recieve.c +++ b/apps/usr_le_code/usr_le_recieve.c @@ -2,11 +2,12 @@ * @file usr_le_recieve.c * @brief BLE 收包入口:bleproto 解析后分发配网 / 对时 / JB 协议 / 组应答 * @author cyWu <1917507415@qq.com> - * @date 2026.08.19 - * @version V1.1.0 + * @date 2026.09.07 + * @version V1.2.0 * @history * - V1.0.0, 2026.08.03, cyWu, 首次发布 * - V1.1.0, 2026.08.19, cyWu, 对齐模组结构;废弃 JSON OTA;修 AUTHDELETE serviceid + * - V1.2.0, 2026.09.07, cyWu, JB 上报超 255 走 wrap_raw 自动分包;加大 txbuf ******************************************************************************/ #include "system/includes.h" @@ -27,15 +28,21 @@ #define LOG_INFO_ENABLE #include "debug.h" -/* 拼 GATT 分包:业务包 ≈ bleproto头 + V2 开销 + JB 帧(<=128),1024 足够 */ +/* 收:GATT 拼包缓冲。发:V2 + bleproto 多帧(单帧总长 247,与 ATT 对齐) */ #define USR_LE_RX_BUF_LEN 1024 -#define USR_LE_V2_WRAP_OVERHEAD 32u /* V2 头开销,JB 帧最大 128 → 约 160 */ +#define USR_LE_V2_WRAP_OVERHEAD 32u /* V2 头开销 */ +#define USR_LE_V2_MAX_LEN (512u + USR_LE_V2_WRAP_OVERHEAD) +#define USR_LE_APPDATA_PACKETLEN 247u /* 对齐 ATT_LOCAL_PAYLOAD_SIZE:8B头+239B数据 */ +#define USR_LE_APPDATA_FRAME_PAYLOAD (USR_LE_APPDATA_PACKETLEN - 8u) +#define USR_LE_TX_BUF_LEN (USR_LE_V2_MAX_LEN + \ + 8u * ((USR_LE_V2_MAX_LEN + USR_LE_APPDATA_FRAME_PAYLOAD - 1u) / \ + USR_LE_APPDATA_FRAME_PAYLOAD)) #define USR_LE_V2_CMD_REPORT 100u /* REPORTCMD_V2 内层 cmd */ #define USR_LE_V2_CMD_RUN 110u /* RUNCMD_V2 内层 cmd */ #define USR_LE_JB_CMD_OFF 4u /* 55 AA | LEN_H LEN_L | CMD */ static int encode_len = 0; -static u8 txbuf[384] = {0}; +static u8 txbuf[USR_LE_TX_BUF_LEN]; static u8 rxbuf[USR_LE_RX_BUF_LEN] = {0}; static u8 s_last_runcmd_msgid = 0; /* RUNCMD_V2 应答须回填最近 msgid */ @@ -75,7 +82,7 @@ static void usr_le_fill_rsp_header(bleproto_appdata_desc_t *out, */ static void usr_le_encode_and_send(bleproto_appdata_desc_t *desc) { - encode_len = bleproto_appdata_encode(txbuf, sizeof(txbuf), sizeof(txbuf), desc); + encode_len = bleproto_appdata_encode(txbuf, sizeof(txbuf), USR_LE_APPDATA_PACKETLEN, desc); if (encode_len > 0) { printf("encode_len = %d\n", encode_len); @@ -365,7 +372,8 @@ void ble_send_data(u8 *data, u16 len) return; } - elen = bleproto_appdata_wrap_raw(txbuf, sizeof(txbuf), &hdr, v2_buf, (uint16_t)plen); + elen = bleproto_appdata_wrap_raw(txbuf, sizeof(txbuf), USR_LE_APPDATA_PACKETLEN, + &hdr, v2_buf, (uint16_t)plen); printf("ble_send_data jb_cmd=0x%02X active=%d encode_len=%d msgid=%d\n", jb_cmd, is_active_send, elen, hdr.msgid); if (elen <= 0) diff --git a/apps/usr_periph/board_pin.h b/apps/usr_periph/board_pin.h new file mode 100644 index 0000000..1287938 --- /dev/null +++ b/apps/usr_periph/board_pin.h @@ -0,0 +1,137 @@ +/****************************************************************************** + * @file board_pin.h + * @brief CBR0 板级引脚、电平与时序(电子开关-加热灯罩;换板只改本文件) + * @author cyWu <1917507415@qq.com> + * @date 2026.09.08 + * @version V1.0.0 + * + * @note 与加热垫同一块电子开关板、同一套引脚和灯效,负载换成灯泡。 + * UART0_TX=PC5 在 board_jl701n_demo_cfg.h。 + * 过零用 MCPWM FPIN 双沿(CH0 下降 / CH1 上升),勿占这两个 PWM 通道。 + * 软件采样兜底必须挂真实 1ms 定时器,禁止在 20ms 节拍里空循环读沿。 + ******************************************************************************/ + +#ifndef __BOARD_PIN_H__ +#define __BOARD_PIN_H__ + +#include "asm/gpio.h" + +/*============================================================================*/ +/* 功能开关:某路硬件异常时可单独关掉,不影响配网 */ +/* 0 时对应模块 init/run 直接 return,GPIO 不配,日志打一次 xxx disabled */ +/*============================================================================*/ + +#define BOARD_LED_ENABLE 1 /**< 1=红绿状态 LED(PA7 红 / PA8 绿,低亮) */ +#define BOARD_KEY_ENABLE 1 /**< 1=按键 PB1(低按下) */ +#define BOARD_RELAY_ENABLE 1 /**< 1=继电器 PC3(高导通=灯泡开) */ +#define BOARD_ZC_ENABLE 1 /**< 1=过零 PA1;=0 时开请求禁吸合、关请求强制断开 */ +#define BOARD_LINKAGE_ENABLE 1 /* 联动引擎(规格书 10 章);P2 开 */ +#define BOARD_TIMER_ENABLE 1 /* 定时引擎(规格书 11 章);P2 开 */ +#define BOARD_TH_SCAN_ENABLE 1 /* 断连扫描风扇广播(规格书 8 章);P2 开 */ +#define BOARD_RUNREC_ENABLE 1 /* 31 天运行记录(规格书 13.1 章);P2 开 */ +#define BOARD_LIGHT_LIFE_ENABLE 1 /* 灯泡剩余寿命(规格书 13.2 章,灯罩新增);P2 开 */ +#define BOARD_NVM_ENABLE 1 /* VM 持久化(规格书 13 章) */ + +/*============================================================================*/ +/* GPIO */ +/*============================================================================*/ + +#define BOARD_UART_TX_PIN IO_PORTC_05 /**< 调试 UART TX(cfg: TCFG_UART0_TX_PORT) */ +#define BOARD_KEY_PIN IO_PORTB_01 /**< 按键,低电平按下 */ +#define BOARD_ZC_PIN IO_PORTA_01 /**< 过零检测,双沿都算一次过零 */ +#define BOARD_LED_GREEN_PIN IO_PORTA_08 /**< 绿灯,低电平亮 */ +#define BOARD_LED_RED_PIN IO_PORTA_07 /**< 红灯,低电平亮 */ +#define BOARD_RELAY_PIN IO_PORTC_03 /**< 继电器,高电平导通(灯泡开) */ + +#define BOARD_LED_ON_LEVEL 0 /**< LED 点亮电平:0=低亮 */ +#define BOARD_RELAY_ON_LEVEL 1 /**< 继电器有效电平:1=高导通 */ +#define BOARD_KEY_ACTIVE_LEVEL 0 /**< 按键有效电平:0=按下为低 */ + +/*============================================================================*/ +/* 按键 / 配对 / OTA */ +/*============================================================================*/ + +/** + * 直接作为 key.c 状态机的 keyLongTime/keyLastTime 使用: + * 松手时刻 + * @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 "asm/gpio.h" +#include "bsp_hw.h" + +#define LOG_TAG_CONST APP +#define LOG_TAG "[BSP_HW]" +#define LOG_ERROR_ENABLE +#define LOG_INFO_ENABLE +#include "debug.h" + +static uint8_t s_initialized; + +/** + * @brief 配置 GPIO 为推挽输出 + * @param pin GPIO 编号 + * @param value 输出初值(电平) + * @return 无 + */ +static void bsp_pin_output(u32 pin, int value) +{ + gpio_set_pull_up(pin, 0); + gpio_set_pull_down(pin, 0); + gpio_set_die(pin, 1); + gpio_direction_output(pin, value); +} + +/** + * @brief 配置 GPIO 为数字输入(可内部上拉) + * @param pin GPIO 编号 + * @param pull_up 1=内部上拉,0=不上拉 + * @return 无 + */ +static void bsp_pin_input(u32 pin, int pull_up) +{ + gpio_direction_output(pin, 1); + gpio_set_direction(pin, 1); + gpio_set_pull_down(pin, 0); + gpio_set_pull_up(pin, pull_up ? 1 : 0); + gpio_set_die(pin, 1); +} + +/** + * @brief 初始化 GPIO + * @note 上电顺序:继电器先保持关(PC3=0),灯全灭(LED_ENABLE 由事件表接管)。 + * @return BSP_HW_OK=成功 + */ +BspHw_Ret_t bsp_hw_init(void) +{ + if (s_initialized) { + return BSP_HW_OK; + } + +#if BOARD_LED_ENABLE + bsp_pin_output(BOARD_LED_RED_PIN, BOARD_LED_ON_LEVEL ? 0 : 1); /* 红灯灭 */ + bsp_pin_output(BOARD_LED_GREEN_PIN, BOARD_LED_ON_LEVEL ? 0 : 1); /* 绿灯灭 */ +#else + log_info("hw led disabled\n"); +#endif + +#if BOARD_KEY_ENABLE + bsp_pin_input(BOARD_KEY_PIN, 1); /* 上拉输入,低按下 */ +#else + log_info("hw key disabled\n"); +#endif + +#if BOARD_RELAY_ENABLE + bsp_pin_output(BOARD_RELAY_PIN, 0); /* 继电器初始关(PC3=0),开请求等过零 */ +#else + log_info("hw relay disabled\n"); +#endif + + s_initialized = 1; + log_info("hw init led=%d key=%d relay=%d\n", + BOARD_LED_ENABLE, BOARD_KEY_ENABLE, BOARD_RELAY_ENABLE); + return BSP_HW_OK; +} + +/** + * @brief 查询板级硬件是否已初始化 + * @return BSP_HW_OK=已初始化,BSP_HW_ERR_NOT_INIT=未初始化 + */ +BspHw_Ret_t bsp_hw_get_status(void) +{ + return s_initialized ? BSP_HW_OK : BSP_HW_ERR_NOT_INIT; +} + +/** + * @brief 红灯开关(PA7,低电平亮) + * @param on 1=亮,0=灭 + * @return 无 + */ +void bsp_led_red_set(uint8_t on) +{ +#if BOARD_LED_ENABLE + gpio_direction_output(BOARD_LED_RED_PIN, + on ? BOARD_LED_ON_LEVEL : (BOARD_LED_ON_LEVEL ? 0 : 1)); +#else + (void)on; +#endif +} + +/** + * @brief 绿灯开关(PA8,低电平亮) + * @param on 1=亮,0=灭 + * @return 无 + */ +void bsp_led_green_set(uint8_t on) +{ +#if BOARD_LED_ENABLE + gpio_direction_output(BOARD_LED_GREEN_PIN, + on ? BOARD_LED_ON_LEVEL : (BOARD_LED_ON_LEVEL ? 0 : 1)); +#else + (void)on; +#endif +} + +/** + * @brief 红绿全灭 + * @return 无 + */ +void bsp_led_all_off(void) +{ + bsp_led_red_set(0); + bsp_led_green_set(0); +} + +/** + * @brief 读取按键(PB1) + * @return 1=按下,0=未按下 + */ +uint8_t bsp_key_is_pressed(void) +{ +#if BOARD_KEY_ENABLE + return (uint8_t)(gpio_read(BOARD_KEY_PIN) == BOARD_KEY_ACTIVE_LEVEL); +#else + return 0; +#endif +} + +/** + * @brief 直接写继电器 GPIO(只准 app_relay 在过零/超时路径调用) + * @param on 1=吸合,0=断开 + * @return 无 + */ +void bsp_relay_set(uint8_t on) +{ +#if BOARD_RELAY_ENABLE + gpio_direction_output(BOARD_RELAY_PIN, + on ? BOARD_RELAY_ON_LEVEL : (BOARD_RELAY_ON_LEVEL ? 0 : 1)); +#else + (void)on; +#endif +} + +/** + * @brief 读继电器 GPIO 当前电平 + * @return 1=吸合,0=断开 + */ +uint8_t bsp_relay_get(void) +{ +#if BOARD_RELAY_ENABLE + return (uint8_t)(gpio_read(BOARD_RELAY_PIN) == BOARD_RELAY_ON_LEVEL); +#else + return 0; +#endif +} diff --git a/apps/usr_periph/bsp_hw.h b/apps/usr_periph/bsp_hw.h new file mode 100644 index 0000000..0d12b47 --- /dev/null +++ b/apps/usr_periph/bsp_hw.h @@ -0,0 +1,77 @@ +/****************************************************************************** + * @file bsp_hw.h + * @brief 板级 GPIO:红绿 LED、按键、继电器(过零见 bsp_zc) + * @author cyWu <1917507415@qq.com> + * @date 2026.09.04 + * @version V1.1.0 + ******************************************************************************/ + +#ifndef __BSP_HW_H__ +#define __BSP_HW_H__ + +#include +#include "board_pin.h" + +typedef enum { + BSP_HW_OK = 0, + BSP_HW_ERR_NOT_INIT +} BspHw_Ret_t; + +/** + * @brief 初始化 GPIO:LED 双通道(初始灭)、按键(上拉输入)、继电器(初始关) + * @return BSP_HW_OK=成功 + */ +BspHw_Ret_t bsp_hw_init(void); + +/** + * @brief 查询板级硬件是否已初始化 + * @return BSP_HW_OK=已初始化,BSP_HW_ERR_NOT_INIT=未初始化 + */ +BspHw_Ret_t bsp_hw_get_status(void); + +/* ═══ 红绿 LED(低电平亮;on 传"亮"语义,内部按 BOARD_LED_ON_LEVEL 取反)═══ */ + +/** + * @brief 红灯开关(PA7) + * @param on 1=亮,0=灭 + * @return 无 + */ +void bsp_led_red_set(uint8_t on); + +/** + * @brief 绿灯开关(PA8) + * @param on 1=亮,0=灭 + * @return 无 + */ +void bsp_led_green_set(uint8_t on); + +/** + * @brief 红绿全灭 + * @return 无 + */ +void bsp_led_all_off(void); + +/* ═══ 按键 ═══ */ + +/** + * @brief 读取按键 + * @return 1=按下,0=未按下 + */ +uint8_t bsp_key_is_pressed(void); + +/* ═══ 继电器(PC3,高电平导通)═══ */ + +/** + * @brief 直接写继电器 GPIO(**只准由 app_relay 在过零时刻/超时路径调用**) + * @param on 1=吸合(加热开),0=断开(加热关) + * @return 无 + */ +void bsp_relay_set(uint8_t on); + +/** + * @brief 读继电器 GPIO 当前电平 + * @return 1=吸合,0=断开 + */ +uint8_t bsp_relay_get(void); + +#endif /* __BSP_HW_H__ */ diff --git a/apps/usr_periph/bsp_zc.c b/apps/usr_periph/bsp_zc.c new file mode 100644 index 0000000..a745d3d --- /dev/null +++ b/apps/usr_periph/bsp_zc.c @@ -0,0 +1,192 @@ +/****************************************************************************** + * @file bsp_zc.c + * @brief PA1 过零:MCPWM FPIN 双沿中断,沿到则回调切继电器 + * @author cyWu <1917507415@qq.com> + * @date 2026.09.04 + * @version V3.1.0 + * @history + * - V3.1.0, 2026.09.04, cyWu, 状态收进 BspZcCtx_t,去掉无用计数与排障史注释 + * - V3.0.0, 2026.09.04, cyWu, MCPWM FPIN 硬件边沿中断 + ******************************************************************************/ + +#include "system/includes.h" +#include "asm/gpio.h" +#include "asm/mcpwm.h" +#include "asm/hwi.h" +#include "asm/irq.h" +#include "asm/timer.h" +#include "bsp_zc.h" +#include "board_pin.h" +#include + +#define LOG_TAG_CONST APP +#define LOG_TAG "[BSP_ZC]" +#define LOG_INFO_ENABLE +#include "debug.h" + +extern PWM_CH_REG *get_pwm_ch_reg(pwm_ch_num_type index); + +typedef struct { + BspZcEdgeCb_t edge_cb; + volatile uint32_t edge_cnt; + volatile uint32_t last_edge_ms; + volatile uint8_t last_level; + uint8_t initialized; +} BspZcCtx_t; + +static BspZcCtx_t s_zc; + +/** + * @brief FPIN 边沿 ISR:清 pending、记沿、执行切变回调 + * @note 中断上下文:禁止 printf / 写 VM / 切 BLE + * @return 无 + */ +___interrupt +static void bsp_zc_interrupt(void) +{ + if (JL_MCPWM->CH0_CON1 & BIT(15)) { + JL_MCPWM->CH0_CON1 |= BIT(14); + } else if (JL_MCPWM->CH1_CON1 & BIT(15)) { + JL_MCPWM->CH1_CON1 |= BIT(14); + } else if (JL_MCPWM->CH2_CON1 & BIT(15)) { + JL_MCPWM->CH2_CON1 |= BIT(14); + } else if (JL_MCPWM->CH3_CON1 & BIT(15)) { + JL_MCPWM->CH3_CON1 |= BIT(14); + } else { + return; + } + + s_zc.edge_cnt++; + s_zc.last_edge_ms = (uint32_t)timer_get_ms(); + s_zc.last_level = (uint8_t)gpio_read(BOARD_ZC_PIN); + + if (s_zc.edge_cb) { + s_zc.edge_cb(); + } +} + +/** + * @brief 把 PA1 接到一个 FPIN 通道(上升沿或下降沿) + * @param index FPIN 通道 0~3 + * @param rising 1=上升沿,0=下降沿 + * @return 无 + */ +static void bsp_zc_fpin_channel_init(u8 index, u8 rising) +{ + PWM_CH_REG *pwm_reg; + + if (index > 3) { + return; + } + + if (rising) { + JL_MCPWM->FPIN_CON |= BIT(16 + index); + } else { + JL_MCPWM->FPIN_CON &= ~BIT(16 + index); + } + +#if BOARD_ZC_FPIN_FILTER_EN + JL_MCPWM->FPIN_CON |= BIT(8 + index); + JL_MCPWM->FPIN_CON |= ((u32)BOARD_ZC_FPIN_FILTER & 0x3FUL); +#else + JL_MCPWM->FPIN_CON &= ~BIT(8 + index); +#endif + + gpio_ich_sel_iutput_signal(BOARD_ZC_PIN, + (enum INPUT_CH_SIGNAL)(INPUT_CH_SIGNAL_MC_FPIN0 + index), + INPUT_CH_TYPE_GP_ICH); + + request_irq(IRQ_MCPWM_CHX_IDX, 3, bsp_zc_interrupt, 0); + + pwm_reg = get_pwm_ch_reg((pwm_ch_num_type)index); + pwm_reg->ch_con1 = BIT(14) | BIT(11) | BIT(4) | (index << 0); + JL_MCPWM->MCPWM_CON0 |= BIT(index); +} + +/** + * @brief 初始化 PA1 输入并挂双沿 FPIN 中断 + * @param cb 沿回调,NULL 则只计数 + * @return 无 + */ +void bsp_zc_init(BspZcEdgeCb_t cb) +{ + if (s_zc.initialized) { + return; + } + + memset(&s_zc, 0, sizeof(s_zc)); + s_zc.edge_cb = cb; + + gpio_set_die(BOARD_ZC_PIN, 1); + gpio_set_direction(BOARD_ZC_PIN, 1); + gpio_set_pull_up(BOARD_ZC_PIN, BOARD_ZC_PULL_UP); + gpio_set_pull_down(BOARD_ZC_PIN, BOARD_ZC_PULL_DOWN); + s_zc.last_level = (uint8_t)gpio_read(BOARD_ZC_PIN); + +#if BOARD_ZC_USE_IRQ + bsp_zc_fpin_channel_init((u8)BOARD_ZC_FPIN_FALL_CH, 0); +#if BOARD_ZC_FPIN_BOTH_EDGE + bsp_zc_fpin_channel_init((u8)BOARD_ZC_FPIN_RISE_CH, 1); +#endif +#endif + + s_zc.initialized = 1; + log_info("zc init irq=%d fall=%d rise=%d filt=%d lvl=%d\n", + BOARD_ZC_USE_IRQ, BOARD_ZC_FPIN_FALL_CH, +#if BOARD_ZC_FPIN_BOTH_EDGE + BOARD_ZC_FPIN_RISE_CH, +#else + -1, +#endif + BOARD_ZC_FPIN_FILTER_EN, (int)s_zc.last_level); +} + +/** + * @brief 1ms 软件采样兜底(须由真实 1ms 定时器调用) + * @return 无 + */ +void bsp_zc_scan_1ms(void) +{ + uint8_t lvl; + + if (!s_zc.initialized) { + return; + } + + lvl = (uint8_t)gpio_read(BOARD_ZC_PIN); + if (lvl != s_zc.last_level) { + s_zc.last_level = lvl; + s_zc.edge_cnt++; + s_zc.last_edge_ms = (uint32_t)timer_get_ms(); + if (s_zc.edge_cb) { + s_zc.edge_cb(); + } + } +} + +/** + * @brief 累计过零沿次数 + * @return 沿计数 + */ +uint32_t bsp_zc_edge_count(void) +{ + return s_zc.edge_cnt; +} + +/** + * @brief 最近一次沿的时间戳 + * @return timer_get_ms(),0=从未检测到 + */ +uint32_t bsp_zc_last_edge_ms(void) +{ + return s_zc.last_edge_ms; +} + +/** + * @brief 当前 PA1 电平 + * @return 0 或 1 + */ +uint8_t bsp_zc_level(void) +{ + return (uint8_t)gpio_read(BOARD_ZC_PIN); +} diff --git a/apps/usr_periph/bsp_zc.h b/apps/usr_periph/bsp_zc.h new file mode 100644 index 0000000..9b22fa6 --- /dev/null +++ b/apps/usr_periph/bsp_zc.h @@ -0,0 +1,55 @@ +/****************************************************************************** + * @file bsp_zc.h + * @brief PA1 过零检测(MCPWM FPIN 双沿中断) + * @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.09.04, cyWu, FPIN 硬件边沿中断 + * + * @note BR28 无通用 GPIO 中断,沿检测复用 MCPWM FPIN(例程 usr_hw_gpio_int.c)。 + * PA1 占两个通道实现双沿。ISR 只准清 pending、计数、回调切 GPIO。 + * 软件兜底必须用真实 1ms 定时器,禁止在 20ms 节拍里空循环采样。 + ******************************************************************************/ + +#ifndef __BSP_ZC_H__ +#define __BSP_ZC_H__ + +#include + +typedef void (*BspZcEdgeCb_t)(void); + +/** + * @brief 初始化 PA1 并挂 FPIN 双沿中断 + * @param cb 沿回调(app_relay 提供),NULL 则只计数 + * @return 无 + */ +void bsp_zc_init(BspZcEdgeCb_t cb); + +/** + * @brief 1ms 软件采样兜底 + * @note 须由真实 1ms 定时器调用 + * @return 无 + */ +void bsp_zc_scan_1ms(void); + +/** + * @brief 累计过零沿次数 + * @return 沿计数 + */ +uint32_t bsp_zc_edge_count(void); + +/** + * @brief 最近一次沿的时间戳 + * @return timer_get_ms(),0=从未检测到 + */ +uint32_t bsp_zc_last_edge_ms(void); + +/** + * @brief 当前 PA1 电平 + * @return 0 或 1 + */ +uint8_t bsp_zc_level(void); + +#endif /* __BSP_ZC_H__ */ diff --git a/dp_definition.json b/dp_definition.json index c6b7184..67dd0ee 100644 --- a/dp_definition.json +++ b/dp_definition.json @@ -131,7 +131,7 @@ "type": "float", "len": 2, "scale": 0.1, - "dir": "ro", + "dir": "wo", "description": "2个字节的float按照int16传输,可以传输的温度范围为:-3276.8 ~ 3276.7", "values": {} },