# 电子开关-加热垫固件需求规格书 | 项 | 内容 | |---|---| | 产品 | 电子开关-加热垫(爬宠) | | 芯片 | JL7016 / AC7016C(BR28) | | 工程 | `电子开关-加热垫_soc_JieLi-JL7016` | | 产品码 | ProdtCode=`CBR1`,productCode=`PRCBR1` | | 协议 | 见宝 SOC 55AA,V1.0.0 | | 文档版本 | V1.0.1 | | 日期 | 2026.09.04 | | 作者 | cyWu | | 读者 | WorkBuddy / 固件开发 | | 参考工程 | `D:\Jianbao\Project\产品\爬宠\温湿度风扇\温湿度风扇_soc_JieLi-JL7016`(框架 / 按键分级 / 联动仲裁 / 定时 / 运行记录) | | 灯效参考 | `D:\Jianbao\Project\产品\宠物\门夹弹力绳\门夹弹力绳_soc_JieLi-JL7016`(红绿双通道、OTA 红绿交替) | > **V1.0.1(2026-09-04):** 增补第 0.1 节编码约束:模块状态收进一个 `s_xxx` 上下文结构体;注释写「为什么」不写排障史;禁止只写不读的诊断变量。P0 过零/继电器代码已按此整理。 > > **V1.0.0(2026-09-04):** 首版共同理解。220V 常电继电器加热垫;过零切继电器;短按开关;开红关绿;5s 配对 / 12s OTA;DP0 加热开关;DP1–5 本版忽略;DP6 由 RO 改为 **WO**(连着时服务器下发当前温度);断连 500ms 重连广播 / 500ms 扫描风扇 `JBao-{缸体号}` 温湿度广播;联动只看温度回差;定时看使能/类型/星期/时分(Byte1 模式忽略);运行记录计实际吸合秒。加热垫本体恒温,闭合后不会过热。 本文是 **已拍板的共同理解**,不是草稿。实现时以本文为准;DP 表以 `dp_definition.json` + 本文第 10.2 / 11.1 / 13.1 / 15 章为准。 --- ## 0. WorkBuddy 开干前必读(硬约束) 1. **程序框架必须和温湿度风扇一致。** 组合根 `usr_jb_main.c`:20ms `usr_timer_tick`(硬件定时器中断)+ 500ms `usr_timer_loop`(BLE 任务上下文)。配对/OTA/写 VM 只准在任务上下文落地(`app_defer` / `app_function_loop`)。禁止在 20ms ISR 里 `syscfg_write`、切 BLE 广播、跑联动/定时引擎。 2. **不要把业务算法写进 `jb_product.c`。** 只把 DP 写入 `gDevData` 并转调 `app_*`。 3. **不要抄门夹的电池 / 低电 / 深睡开机判定。** 本机 220V 常电、无电池。**上电即开机**;拔电即关,无软关机。 4. **不要抄风扇的 PWM 风扇 / 数码管 / AHT20 / 通风强力。** 执行器是继电器 + 过零;温度来自扫描或 DP6,不是本机传感器。 5. **`MAX_PACKAGE_LEN` 必须为 256。** 生成值 128 装不下 DP17 `run_record`(124B)。已在 `jb_protocol.h` 改过,禁止再生成回 128。 6. **DP6 `current_temp` 为 WO。** 已连接时服务器下发;设备不上报;断连扫描到的温度不填 DP6。已在 `dp_definition.json` / `jb_protocol.c` / `jb_product.c` 改过。 7. **DP1–5 本版写忽略、不实现动作**(温控开关 / 温控时间 / 模式 / 最低最高温度)。DP18 心跳本版忽略。 8. 每个模块必须能用 `board_pin.h` 的 `BOARD_xxx_ENABLE` **单独关掉** 仍能编过、能配网上报。 9. 日志全部 **英文**。中文只出现在注释和本文。 10. 编码风格对齐风扇 + 工程师规范:文件小写+下划线,`g_` / `s_` 前缀,枚举 `Pascal_t`,接口 `xxx_init` / `xxx_run` 或 tick / `xxx_get_status`。 11. **现有工程是协议骨架。** 没有 `apps/usr_app`、没有 `board_pin.h`。`app_main.c` 里上电强制 `usr_goto_pair=1` **必须删掉**,改为上电进工作态(已配对走重连广播)。 12. 调试 UART TX = **PC5**(本机无 AHT20,PC5 空闲)。关掉会与 PA1 / PA7 / PA8 / PB1 / PC3 冲突的 demo 外设。 13. **遵守第 0.1 节编码与注释。** 散落 static、把排障过程复制进每个函数、只写不读的计数,一律视为未完成。 --- ## 0.1 编码与注释(禁止再犯) 新产品模块(`apps/usr_app/*`、`apps/usr_periph/*`、`usr_jb_main.c`)必须遵守。从风扇/门夹 **原样拷贝** 的 `key.c` / `led.c` 框架不要借机大改。 1. **一个模块一个上下文。** 两个及以上的模块状态变量必须收进 `XxxCtx_t`,文件内只留 `static XxxCtx_t s_xxx`。ISR 与主循环共享的成员在结构体里标 `volatile`。单个标志可以单独 `static`(如 `app_defer` 的位图)。 2. **禁止一排裸 static。** 反例:`s_edge_cb`、`s_edge_cnt`、`s_rise_cnt`、`s_fall_cnt`、`s_last_edge_ms`、`s_last_level`、`s_initialized` 并列。正例:`static BspZcCtx_t s_zc`。 3. **不要只写不读的诊断变量。** 需要看沿是否在跑,用已有 getter + 一条日志(`bsp_zc_edge_count`)。不要 `s_rise_cnt` / `s_fall_cnt` 这种从来没人读的计数。 4. **文件头三件事:做什么、一两行硬约束、history。** 同一条约束只写一次。禁止把「V1.0.0 空循环漏沿」复制到 `.c` / `.h` / `usr_jb_main.c` / `board_pin.h` / 每个 `@note`。 5. **函数注释短。** `@brief` 一行;`@param` / `@return` 按需;`@note` 只写本函数独特约束(例如「FPIN ISR,禁止 printf」)。不要复述代码(`s_zc.edge_cnt++` 旁边不要写「累计沿次数」)。 6. **注释解释为什么,不写排障日记。** 「必须等过零再切继电器」是为什么;「我改了三次 ISR 才好」不要进源码。 7. **P0 路径不堆死字段。** 没有 getter、没有日志、P2 也没用上的 `zc_ok_cnt` / `last_change_ms` 不要先占着。需要时再加。 --- ## 1. 产品一句话 220V 常供电的爬宠加热垫:过零点吸合/断开继电器(避免打火),本地短按开关,BLE 连 APP。APP 可下发加热开关、1 条缸体联动配置、7 组定时。连着时用服务器下发的当前温度做联动;断连时扫描风扇的 `JBao-{缸体号}` 广播取温度。加热实际吸合的秒数记 31 天运行记录。垫体自身恒温,软件仍按 Tmin/Tmax 回差开停。 --- ## 2. 硬件资源 ### 2.1 引脚(以用户原理图说明 + 本文为准) | 功能 | MCU | 电平 / 说明 | |---|---|---| | 按键 | **PB1** `IO_PORTB_01` | 低电平按下(与风扇/门夹相同;若实测相反只改 `BOARD_KEY_ACTIVE_LEVEL`) | | 过零检测 | **PA1** `IO_PORTA_01` | 过零时跳变;**上升沿和下降沿都算**一次过零 | | 绿灯 | **PA8** `IO_PORTA_08` | **低电平亮** | | 红灯 | **PA7** `IO_PORTA_07` | **低电平亮** | | 继电器 | **PC3** `IO_PORTC_03` | **高电平导通**(加热开) | | 调试 UART TX | **PC5** `IO_PORTC_05` | 只发打印 | | 供电 | 220V 市电 → 隔离电源 3.3V | 无电池、无充电管理 | 板级文件仍指向 `CONFIG_BOARD_JL701N_DEMO`。产品 GPIO **全部集中到** `apps/usr_periph/board_pin.h`,并改 `board_jl701n_demo_cfg.h`: ```c #define TCFG_UART0_TX_PORT IO_PORTC_05 ``` 关掉与产品脚冲突的 demo 外设(IR 键占 PA8、PWM LED 等)。产品灯走 `app_led`,保持 `TCFG_PWMLED_ENABLE=DISABLE`。 ### 2.2 过零与继电器安全 - 市电 50Hz:半波 10ms,双沿即约每 10ms 一次过零。 - **开和关都必须等下一次过零沿再改 PC3**,禁止在过零窗口外切继电器(打火)。 - GPIO 上电默认:**继电器关(PC3=0)**,再按 VM 恢复请求;若要开,等过零再吸合。 - 过零 ISR 只允许:置沿标志 +(若有 pending)改继电器 GPIO。禁止写 VM / BLE / printf。 - 超时见第 7.2 节。 --- ## 3. 软件架构(对齐风扇,删风扇/显示/传感,加继电器/过零/扫描) ``` apps/earphone/app_main.c └─ usr_rtc_init() └─ usr_jb_init() ← 唯一组合根 bsp_hw_init() app_led_init() ← 灯效事件表最先装载 jbInit() + userInit() (上电即开机:无按键等待) app_relay_init() / app_zc_init() app_arbiter_init() app_linkage_init() / app_timer_init() / app_runrec_init() app_th_scan_init() app_nvm_init() + app_nvm_load() app_function_init() user_key_init() + lock_if_held usr_timer_add(usr_timer_tick, 20ms, priority=1) ← 常驻,永不重挂 ``` **20ms 业务节拍**(`usr_timer_tick`,硬件定时器中断上下文,禁止再开 1ms ISR): 1. `jbTimerIrq()` × dt 2. `user_key_scan` + `user_key_handle` 3. `app_zc_poll`(过零超时 100ms 判定;真正切 GPIO 在过零 ISR) 4. `app_function_poll`(同步 DP0 = 实际继电器态) 5. `app_runrec_tick`(继电器已吸合则累加秒;满 10 分钟只打脏,不写 flash) 6. `app_nvm_tick`(变化检测 + 500ms 防抖计时,不写 flash) 7. `app_led_run` **priority 固定 1、只注册一次,不动态重挂**:220V 常电,不需要低功耗。 `usr_timer_loop()`(BLE 500ms 任务上下文):`userHandle()`、`jbProtocolHandle()`、 `app_th_scan_tick()`(断连时 500ms 重连广播 / 500ms 扫描交替)、 `app_linkage_run()` / `app_timer_run()`、`app_runrec_rotate_if_needed()`、 `app_function_loop()`(VM 落盘 + 配对/OTA 落地)。 可直接复用风扇源码(拷贝后改引脚/事件表,不要改框架语义): | 复用 | 路径 | 改什么 | |---|---|---| | 按键 FSM | `key.c` / `key.h` / `key_manager.c` | 短按切加热;**按住 5s 配对 / 12s OTA** | | 配对会话 | `app_pair.c` | 120s 超时;灯效改双灯 | | 延后执行 | `app_defer.c` | 原样 | | NVM / 仲裁 / 运行记录 / 定时 | `app_nvm` / `app_arbiter` / `app_runrec` / `app_timer_task` | owner 从风扇档位改为继电器;定时动作改为开/关 | | 联动 | `app_linkage` | 开停条件改温度回差(第 10.3);温度源改扫描/DP6 | | RTC | `usr_rtc.c` | 原样 | | 协议骨架 | `usr_jb_proto` | 已生成;`MAX_PACKAGE_LEN=256`,DP6=WO | LED 底层 **拷门夹双通道**(`led.c` 打开 `ALTERNATING_BLINK_ENABLE`),不要用风扇单灯裁剪版。事件表按第 6 章。 **不要拷贝:** `app_battery.c`、`app_boot.c`、`app_power.c`、`app_fan.c`、`app_disp.c`、`app_th.c`、`app_mode.c`、`app_th_adv.c`、充电 GPIO、CT1642、AHT20。 --- ## 4. 模块开关(独立使能 / 屏蔽调试) 全部放 `apps/usr_periph/board_pin.h`。`0` 时:对应 `init/run` 直接 return,GPIO 不配,日志打一次 `xxx disabled`。 ```c #define BOARD_LED_ENABLE 1 #define BOARD_KEY_ENABLE 1 #define BOARD_RELAY_ENABLE 1 #define BOARD_ZC_ENABLE 1 /* 过零;=0 时继电器请求仍超时路径强制关,禁止无过零硬开 */ #define BOARD_LINKAGE_ENABLE 1 #define BOARD_TIMER_ENABLE 1 /* P0/P1 可先 0 */ #define BOARD_TH_SCAN_ENABLE 1 /* 断连扫描;P0/P1 可 0 */ #define BOARD_RUNREC_ENABLE 1 #define BOARD_NVM_ENABLE 1 /* P0 可 0 */ ``` P0 建议先 `LINKAGE/TIMER/TH_SCAN/RUNREC/NVM=0`,台上只验按键、双灯、过零继电器。 --- ## 5. 电源与按键 ### 5.1 上电 / 断电 - **上电直接进入工作态**,无需按键。流程对齐风扇 5.1:`bsp_hw_init` → `app_led_init` → `jbInit/userInit` → 各 `app_*_init` → `app_nvm_load` → `user_key_init` + `user_key_lock_if_held` → 20ms 节拍。 - **关机 = 拔电。** 无软关机、无深睡开机判定。 - 上电若键仍按着:只锁**短按**(防一上电就切加热),不锁 5s/12s。 - 设备工作态恒开机。DP0 是 **加热开关**(不是风扇那种预留 power)。 - 无电池、无充电逻辑。 - **冷启动继电器:** GPIO 先保持关;加载 VM 后按上次 `heat_switch` 恢复请求;若为开则等过零再吸合。温度没到之前联动不改它(第 13 章)。 ### 5.2 按住分级(运行态,与风扇同一套时序) | 按住时长 | 行为 | |---|---| | < 5000 ms 松开 | 短按:切换加热(见 5.3) | | 满 5000 ms(仍按住) | **只**切配对提示灯(绿灯 100ms 快闪),**不清绑定** | | 满 5s 后、12s 前松开 | 提交配对:`usr_goto_pair_mode()` + `app_pair_start()` 120s | | 满 12000 ms(到点即触发) | 进 OTA,不等松手;同时撤销本次配对 | 配对刻意不在 5s 到点提交 —— 用户可能继续按到 12s 进 OTA。 ⚠ 20ms ISR 只准置标志 + 切灯效。清绑定与 `usr_app_ota_init` 延后到 `app_function_loop()`。 ### 5.3 开机态按键 | 事件 | 行为 | |---|---| | 短按(<5s 松开) | 切换加热请求(开↔关),走手动抢权(第 12 章),同步 DP0 | | 按住 5s 后松开 | 清绑定 + 进配对 | | 按住 12s | 进 OTA | OTA 进行中:忽略短按和再次 5s/12s。配对进行中:短按仍可切加热。 --- ## 6. LED(红绿双灯,低亮) 加热开 = 红灯亮绿灯灭;加热关 = 红灯灭绿灯亮。灯表示 **继电器实际状态**(过零完成之后),不是 pending 请求。 | 事件 | 图案 | 优先级(高者覆盖) | |---|---|---| | OTA | 红绿交替,200ms 半周期,直到升级结束/复位 | 最高 | | 按住满 5s / 配对中 | 绿灯 100ms 亮 / 100ms 灭(红灯灭) | 高 | | 配对成功 | 可保持绿灯常亮 3s,再回到「跟继电器」 | 高 | | 配对失败 | 红绿交替 3s,然后回到「跟继电器」 | 高 | | 加热开(实际吸合) | 红灯常亮,绿灯灭 | 低 | | 加热关 | 绿灯常亮,红灯灭 | 低 | 优先级高→低:**OTA → 配对/5s 将配网/配对结果 → 平时跟继电器**。 --- ## 7. 继电器 + 过零 ### 7.1 接口 - `app_relay_init` / `app_relay_request(uint8_t on)` / `app_relay_get_actual()` / `app_relay_get_request()`。 - `on=1`:pending 开,等过零后 PC3=1。 - `on=0`:pending 关,等过零后 PC3=0。 - DP0 上报 **实际吸合状态**(`app_relay_get_actual()`),让 APP 看见过零完成。 ### 7.2 过零超时(100ms) 双沿都算过零。从 pending 置位起: | 请求 | 100ms 内无过零 | 行为 | |---|---|---| | 关 | 强制 PC3=0 | `fault_code=2`(设备故障) | | 开 | **不吸合**,保持关 | 清 pending 开,`fault_code=2` | `BOARD_ZC_ENABLE=0`:禁止走「无过零硬开」;关请求仍允许强制断开(安全)。台上可用示波器看 PA1,再用宏强制超时路径。 过零恢复后:新的请求仍按第 7.1 走;`fault_code` 在下一次成功过零切变后清 0(P2 实现)。P0 可先打日志。 --- ## 8. 温度来源(无本机传感器) 内部统一用 `temp_x10`(0.1℃,有符号)。联动比较见 10.3。 | 场景 | 温度从哪来 | 扫描 | DP6 | |---|---|---|---| | BLE 已连接 | 服务器写 DP6(int16×0.1 → float) | **停** | WO 收下,不存 VM,不上报 | | BLE 断连且已配对 | 扫描风扇自定义广播 | **500ms 扫描 / 500ms 重连广播** 交替 | 扫描值 **不写** DP6 | | 未配对 / 配对中 / OTA | 不扫描 | 重连或绑定/OTA 广播 | — | 湿度:广播里有,本版 **不参与开停**,只可打日志。 ### 8.1 风扇侧广播格式(只读约定,加热垫不发这种包) 风扇 `app_th_adv.c`: - Complete Local Name:`JBao-{tank_id}`(十进制,无前导零),例如缸体 1 → `JBao-1` - Manufacturer `0xFFFF` 后 3 字节:`[配置版本 uint8][温度 int8 ℃][湿度 uint8 %]` 加热垫匹配规则: 1. 名字等于 `JBao-` + 本机 DP8 缸体编号(uint16 十进制)。 2. 配置版本等于 DP8 Byte6。 3. 两者都满足才更新 `temp_x10 = 温度int8 * 10`。 4. 名字不对、版本不对、解析失败:**当没收到**,保持上一温度和上一加热态(第 10.3 / Q6)。 缸体编号为 0:视为无有效联动,不扫描匹配。 ### 8.2 500ms 时隙 断连已配对: ``` tick 偶数:见宝重连广播(encode4reconn) tick 奇数:关广播、开扫描 ≤500ms,回调里滤 JBao- 前缀 已连接 / 配对 / OTA:退出该交替,回到风扇同款 adv 策略 ``` 实现时若协议栈不能真正双角色并发,**必须时分**,不允许「边广播边扫」把时序写死成阻塞。 温度丢失(长时间无匹配包):**保持上一态**,不因丢包关加热(垫体恒温,闭合不会过热)。 --- ## 9. 本版不用的模式 / 温控 DP | DP | 本版 | |---|---| | 1 温控开关 | 写忽略 | | 2 温控时间 | 写忽略 | | 3 模式 | 写忽略;定时 Byte1 同样忽略 | | 4 / 5 最低最高温度 | 写忽略(阈值只在 DP8 联动 20B 里) | 无通风/强力状态机,无 `app_mode`。 --- ## 10. 联动引擎 设备只有 **1 条** 20 字节联动配置(DP8)。不改 DP3。 ### 10.1 有效配置 同时满足才跑联动开停: - 缸体编号 ≠ 0(Byte0–1,uint16 大端) - `Tmin ≤ Tmax` - `BOARD_LINKAGE_ENABLE=1` 否则视为无联动:不自动开;若原先是联动占着加热则关掉。 湿度 min/max、配置版本:整包 20B 原样落盘。湿度 **不参与开停**。版本只用于扫描匹配(8.1)。Byte7–19 预留,逻辑忽略。 ### 10.2 20 字节布局(DP8 `linkage_config`) | 偏移 | 字段 | 编码 | |---|---|---| | Byte0–1 | 缸体编号 | uint16 **大端** | | Byte2 | 最低温度 `Tmin` | 有符号 int8,单位 ℃ | | Byte3 | 最高温度 `Tmax` | 有符号 int8,单位 ℃ | | Byte4 | 最低湿度 `Hmin` | uint8 %,只存不用 | | Byte5 | 最高湿度 `Hmax` | uint8 %,只存不用 | | Byte6 | 配置版本 | uint8,扫描匹配用 | | Byte7–19 | 预留 | 只存不用 | 比较用 `temp_x10`:`T < Tmin` ⇔ `temp_x10 < Tmin * 10`;`T > Tmax` ⇔ `temp_x10 > Tmax * 10`。**等于不算越界**,走「中间维持上一态」。 已连接:`temp_x10 = (int16_t)(gDevData.current_temp * 10)`。断连扫描:int8 ℃ × 10。尚无任何有效温度:联动不改继电器(保持上电恢复值 / 上一态)。 ### 10.3 开停(只看温度) 在有效配置、未被手动作废、仲裁允许联动、且已有有效温度时,每个 500ms 任务 tick 判一次: | 条件 | 动作 | |---|---| | `T < Tmin` | 请求开加热,**不改其它 DP** | | `T > Tmax` | 请求关加热 | | `Tmin ≤ T ≤ Tmax`(含等于) | **保持上一态** | 过热关、过冷开。本产品恒温垫,软件仍执行 `T > Tmax` 关。 ### 10.4 联动删除(DP7) | 写入 | 行为 | |---|---| | 0 | 无操作 | | 1 | 清 RAM + VM 里 20 字节;若当前是联动占着加热 → 关;然后把 `linkage_delete` **清回 0** 再上报(脉冲) | ### 10.5 APP 重写 DP8 `memcpy` 20 字节 → 存 VM → **清手动作废标志** → 立刻按新阈值重判(有有效温度时)。缸体编号写成 0:按无效配置,联动占着则关。 --- ## 11. 定时引擎(P2,7 组) ### 11.1 8 字节 | 偏移 | 字段 | |---|---| | Byte0 | 使能 | | Byte1 | 模式 | **本版忽略**(仍原样存 VM) | | Byte2 | 类型:0=单次,1=每天,2=每周 | | Byte3 | 星期位图 Bit0=周一 … Bit6=周日(仅类型 2) | | Byte4 | 开始小时 0~23 | | Byte5 | 开始分钟 0~59 | | Byte6 | 结束小时 | | Byte7 | 结束分钟 | - 窗开始沿:请求 **开** 继电器。 - 窗结束沿:请求 **关** 继电器,然后立刻按第 10.3 判一次联动。 - 窗内保持开(直到被手动打断,见第 12 章)。 - 支持跨午夜:22:00→06:00 = 当晚 22 点到次日 6 点。 - 类型 1:每天该窗。 - 类型 2:仅位图置位的星期;跨午夜以 **开始时刻所在的本地星期** 判断。 - 类型 0 单次:结束时刻过后把 **Byte0 清 0 并写 VM**。 - 多组重叠:任一窗内即开;所有窗都结束才走结束关。 ### 11.2 RTC 门闩(强制,与风扇相同) ``` usr_rtc_get_time() 失败 → 全部定时不执行(等同 time_en=0) 仅 BLE 已连接但从未对时 → 同上 对时成功瞬间 → 立刻补判:若当前落在某条有效窗内,进入该窗(开加热) ``` 无电池,拔电后 RTC 丢失。开机后必须等 APP/网关 `usr_rtc_set_from_unix` 成功。 `time_en=0`:所有窗立刻视为结束(关加热),再按 10.3 补判联动。 --- ## 12. 仲裁(唯一决策表) 优先级 **高 → 低**: 1. **手动**:短按、APP 写 DP0 2. **定时窗**(RTC 有效且 `time_en=1` 且当前落在窗内) 3. **联动**(有效配置且本 tick 满足开/关条件) | 冲突 | 胜者 | 副作用 | |---|---|---| | 手动 vs 正在跑的定时 / 联动 | 手动 | 若原是联动占着 → **本轮联动作废**(RAM,不写 VM) | | 手动 vs 定时窗 | 手动 | 本窗剩余时间不再自动抢回;等到下一窗开始沿或下次对时补窗 | | 定时窗 vs 联动 | 定时 | 窗内联动不开、不关;窗结束收口后立刻按 10.3 重判 | | 联动 vs 空闲 | 联动 | 开则请求开,关则请求关 | 作废标志不存 VM。 **手动作废后的再武装:** 必须先走到第 10.3 的「关」条件(`T > Tmax`),再重新出现 `T < Tmin`,才允许联动再开。作废期间丢广播仍保持上一态。 **冷启动:** 作废标志是 RAM,上电为空。VM 恢复的加热态先执行;有有效温度且无定时窗占用时,允许联动按 10.3 立刻改。 `time_en` 写成 0:立刻停定时(窗结束语义)。无联动总开关;无效配置 / DP7 删除即停联动。 --- ## 13. 持久化(VM) 存(冷启动保留): - `heat_switch`(上次加热请求;上电按此恢复) - `linkage_config`(20B) - `time_en`、`time_config1~7`(各 8B,含被忽略的 Byte1) - `fault_en`、`alarm_en`(若有写入) - `run_record`:31 × uint32 秒 + slot[0] 对应的本地日期(对时后才有意义) 不存: - DP1–5、DP6 当前温度、联动作废标志、过零 pending - `linkage_delete`(脉冲) - BLE 三元组(`CFG_USER_PAIR_INFO`,出厂重置 **不清**) 上电:加载上述字段,继电器按上次 `heat_switch` 恢复(开则等过零),再让联动按第 12 章冷启动规则判(无温度则不动)。 首次上电(VM 无效):加热关,联动 20B 全 0,定时全 0,`time_en=0`,31 天运行记录全 0。 写 VM 时机: - APP 改联动/定时、单次定时自毁、加热稳定变化:500ms 防抖 - 运行记录:累计吸合满 **10 分钟** 写一次;**跨天旋转时立刻写** ### 13.1 运行记录(DP17 `run_record`,RO,124B) 31 天 × 4 字节,每格 uint32 **大端**,单位秒: | 偏移 | 含义 | |---|---| | Byte0–3 | 今天加热垫运行总秒数 | | Byte4–7 | 昨天 | | … | … | | Byte120–123 | 前 30 天 | **什么叫在运行:** 过零后继电器 **实际已吸合**(`app_relay_get_actual()==1`)。pending 开但还没过零 **不算**。手动 / 定时 / 联动只要真正吸合都算。 **日期轴:** 与风扇相同 —— 未对时只往 slot[0] 加、不旋转;对时后按本地日历左移;掉电再上电先灌 VM。 **上报:** 今天累计秒数相对上次 0x07 上报满 **60 秒** 才进差分(`JB_RUNREC_DIFF_SEC`);跨天旋转立刻带 DP17。APP 0x05 读、10 分钟全量仍带完整 124B。其它 DP 上报时不刷新运行记录基准。 **落盘:** 每累计运行满 600 秒写一次 VM。跨天立刻写。拔电接受最多丢不满 10 分钟的秒数。 `BOARD_RUNREC_ENABLE=0`:不累加、DP17 报全 0。 --- ## 14. 出厂重置(DP23 写 1) - **清全部业务 VM**(第 13 章列表,含联动 20B 与 31 天运行记录)。 - **保留 BLE 配对,不断开、不软复位。** - RAM 中加热关,联动/定时停止,作废标志清掉。 - 换机:运行中 **按住 5s 后松手** 清配对进绑定广播(见 5.2)。 --- ## 15. BLE / DP 行为摘要 身份:`CBR1` / `PRCBR1`。帧格式见 `jb_protocol.h`(`MAX_PACKAGE_LEN=256`)。 | DP | 处理 | |---|---| | 0 heat_switch | 写则手动抢权,请求开/关(等过零)。上报实际吸合 | | 1–5 | 本版写忽略 | | 6 current_temp | **WO**。已连接时服务器下发 → 更新联动用温度。不上报。断连扫描不填 | | 7 linkage_delete | 第 10.4 节,脉冲 | | 8 linkage_config | 20 字节 memcpy + 存 VM + 第 10.5 节立刻重判 | | 9 time_en | 见第 11 章 | | 10–16 time_config | 8 字节 + 存 VM;Byte1 忽略 | | 17 run_record | RO,由 `app_runrec` 填 124B,禁止 APP 写 | | 18 heartbeat | 本版忽略 | | 19 fault_en | 原样存,P0 不做位级过滤 | | 20 fault_code | 0 无 / 1 温度异常(本版不上报) / **2 过零超时等设备故障** | | 21 alarm_en | 原样存 | | 22 alarm_code | 保持 0 | | 23 factory_reset | 第 14 章 | | 24–26 reserva | 存 RAM,可不实现动作 | OTA:运行态按住 12s 走 `usr_app_ota_init`(任务上下文,红绿交替)。协议 OTA(`jbOnOtaNotify`)**P2 可继续 stub**,禁止假装升级成功后擦用户区。 配对超时 120s,失败保持开机,灯回「跟继电器」。 断连已配对:500ms 见宝重连广播 ↔ 500ms 扫描(第 8.2)。绑定广播仍用 `CBR1` / `PRCBR1`,不要改成 `JBao-` 名(那是风扇温湿度窗)。 --- ## 16. 建议文件清单(新建,风扇已有的拷贝改) ``` apps/usr_periph/ board_pin.h # 引脚、时序、BOARD_xxx_ENABLE bsp_hw.c/.h # LED 双通道 / KEY / 继电器 GPIO bsp_zc.c/.h # PA1 双沿中断,过零回调 usr_rtc.c/.h # 已有则保留 apps/usr_app/ key.c/.h, key_manager.c/.h # 拷风扇;短按切加热 + 5s/12s led.c/.h, led_manager.c/.h # 拷门夹双通道 + 红绿交替 app_led.c/.h # 第 6 章事件表;含 5s 将配网 app_defer.c/.h # 拷风扇 app_pair.c/.h # 拷风扇,灯效改双灯 app_relay.c/.h # 请求 / 实际 / pending app_th_scan.c/.h # 500ms 交替扫描 JBao-{tank} app_linkage.c/.h # 第 10 章温度回差 app_runrec.c/.h # 实际吸合计秒 app_timer_task.c/.h # 第 11 章;动作改为开/关 app_nvm.c/.h app_function.c/.h app_arbiter.c/.h ``` `usr_jb_main.c` 只编排,不写算法。禁止引入 `app_fan` / `app_disp` / `app_th` / `app_mode`。 --- ## 17. 分阶段交付与验收 ### P0 — 台上 Bring-up(不依赖 APP) **目标:** 按键、双灯、过零继电器可独立验证。 - [ ] `board_pin.h` + UART TX=PC5,打印正常 - [ ] 插电即开机(不按键,绿灯常亮,继电器关) - [ ] 短按:继电器在过零沿才变;开红关绿 - [ ] 示波器:PA1 过零沿与 PC3 边沿对齐(差应在亚毫秒级,禁止随便 delay 后切) - [ ] 模拟过零丢失:关请求 100ms 强关;开请求 100ms 保持关(可宏强制) - [ ] 按住满 5s → 绿灯 100ms 快闪;松手 → 清绑定进配对 - [ ] 按住满 5s 不松手到 12s → 不进配对,红绿交替进 OTA - [ ] 长按进配对/OTA 后不复位(动作走任务上下文) - [ ] 上电强制 `usr_goto_pair=1` 已删除 **P0 关闭:** `BOARD_LINKAGE/TIMER/TH_SCAN/RUNREC/NVM_ENABLE=0` ### P1 — 最小产品闭环 - [ ] 5s 配对,APP 能写 DP0 开关加热(过零后灯和继电器一致,DP0 报实际态) - [ ] 短按同步上报 DP0 - [ ] VM:改加热后断电,重新上电按上次开关恢复(开则等过零再吸合) - [ ] DP1–5 写忽略;设备不关、继电器不跟这五个 DP - [ ] DP6 WO:APP/服务器能写下当前温度;设备全量上报 **不含** DP6 - [ ] 定时 7 组 + `time_en` 能存;`BOARD_TIMER_ENABLE=0` 时不执行 - [ ] `MAX_PACKAGE_LEN=256`,APP 能读完整 124B 运行记录(本阶段记录可仍为 0) ### P2 — 自动化 - [ ] 断连:500ms 重连广播 / 500ms 扫描;已连接停扫描 - [ ] 匹配 `JBao-{缸体号}` 且版本相同才更新温度;湿度不参与开停 - [ ] 联动:`TTmax` 关,中间保持;无匹配包保持上一态 - [ ] 缸体号 0 / min>max 不跑联动 - [ ] 手动作废后须先 `T>Tmax` 再 `TTmax`,再回到 `TTmax` → 关。 --- ## 18. `board_pin.h` 时序宏(实现时抄这份数) ```c #define BOARD_UART_TX_PIN IO_PORTC_05 #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 #define BOARD_RELAY_ON_LEVEL 1 #define BOARD_KEY_ACTIVE_LEVEL 0 #define BOARD_KEY_PAIR_MS 5000 #define BOARD_KEY_OTA_MS 12000 #define BOARD_PAIR_TIMEOUT_MS (120u * 1000u) #define BOARD_LED_FAST_ON_MS 100 #define BOARD_LED_FAST_OFF_MS 100 #define BOARD_LED_OTA_ON_MS 200 #define BOARD_LED_OTA_OFF_MS 200 #define BOARD_LED_PAIR_FAIL_MS 200 /* 红绿交替半周期 */ #define BOARD_LED_PAIR_FAIL_HOLD_MS 3000 #define BOARD_ZC_TIMEOUT_MS 100 #define BOARD_ADV_SCAN_SLOT_MS 500 /* 重连广播 / 扫描各占一拍 */ #define BOARD_RUN_RECORD_DAYS 31 #define BOARD_RUN_RECORD_SAVE_SEC 600 ``` --- ## 19. 风险与不要擅自发挥的点 | 风险 | 要求 | |---|---| | 无过零硬切继电器 | 禁止。打火会烧触点 | | 开请求过零丢失仍吸合 | 禁止。只允许关路径超时强关 | | 抄风扇「过热开吹」 | 禁止。本机是过冷开、过热关 | | 湿度参与开停 | 本版禁止 | | 丢广播就关加热 | 禁止(已拍板保持上一态) | | 生成协议再导回 DP6=RO / LEN=128 | 禁止;以本文 + 当前源码为准 | | `app_main` 上电清配对进绑定 | 必须删 | | 20ms ISR 写 VM / 切 BLE | 禁止(风扇已踩过栈溢出) | | 一排裸 static / 排障史注释 | 禁止。≥2 个状态收进一个 `s_xxx`;单标志可单独 static。详见第 0.1 节 | | 协议 OTA stub 返回 OK 当已升级 | 注释标明未升级 | | 扫描时改 GATT 名 | 不要改 0x2A00;只解析对方 adv | | 门夹 USB 插入直通开机 / 电池逻辑 | 禁止移植 | | 把加热开理解成「设备开机灯」 | 开机后关加热是绿灯,不是灭灯 | --- ## 20. 给 WorkBuddy 的推荐开工顺序 1. 确认 `MAX_PACKAGE_LEN=256`、DP6=WO(本节交付已改协议文件)。`jb_product.c` 只保留 DP 赋值,业务迁 `app_*`。 2. `board_pin.h` + `bsp_hw` + `bsp_zc`;删 `app_main` 上电强制配对。 3. 从风扇拷 `usr_jb_main` 框架、key、defer、pair、nvm 骨架;LED 拷门夹双通道。 4. `app_relay`:短按 + DP0 + 过零;P0 验收灯和沿。 5. P1:VM 恢复加热、DP1–5 忽略、定时只存。 6. P2:`app_th_scan`、`app_linkage`、`app_timer`、`app_runrec`、仲裁用例。 每一步都保持对应 `BOARD_xxx_ENABLE` 可关。提交前对照第 0.1 节:一个模块一个 `s_xxx`,注释写为什么,不要排障日记。 --- **文档结束。** 行为争议以本文第 5~13 章为准;字节布局以第 10.2 / 11.1 / 13.1 节为准。