Files
JBao_Heat_Pad_FW/Doc/电子开关-加热垫_固件需求规格书.md
T

664 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 电子开关-加热垫固件需求规格书
| 项 | 内容 |
|---|---|
| 产品 | 电子开关-加热垫(爬宠) |
| 芯片 | JL7016 / AC7016CBR28 |
| 工程 | `电子开关-加热垫_soc_JieLi-JL7016` |
| 产品码 | ProdtCode=`CBR1`productCode=`PRCBR1` |
| 协议 | 见宝 SOC 55AAV1.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.12026-09-04):** 增补第 0.1 节编码约束:模块状态收进一个 `s_xxx` 上下文结构体;注释写「为什么」不写排障史;禁止只写不读的诊断变量。P0 过零/继电器代码已按此整理。
>
> **V1.0.02026-09-04):** 首版共同理解。220V 常电继电器加热垫;过零切继电器;短按开关;开红关绿;5s 配对 / 12s OTA;DP0 加热开关;DP15 本版忽略;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 已连接 | 服务器写 DP6int16×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 有效配置
同时满足才跑联动开停:
- 缸体编号 ≠ 0Byte01uint16 大端)
- `Tmin ≤ Tmax`
- `BOARD_LINKAGE_ENABLE=1`
否则视为无联动:不自动开;若原先是联动占着加热则关掉。
湿度 min/max、配置版本:整包 20B 原样落盘。湿度 **不参与开停**。版本只用于扫描匹配(8.1)。Byte7–19 预留,逻辑忽略。
### 10.2 20 字节布局(DP8 `linkage_config`
| 偏移 | 字段 | 编码 |
|---|---|---|
| Byte01 | 缸体编号 | uint16 **大端** |
| Byte2 | 最低温度 `Tmin` | 有符号 int8,单位 ℃ |
| Byte3 | 最高温度 `Tmax` | 有符号 int8,单位 ℃ |
| Byte4 | 最低湿度 `Hmin` | uint8 %,只存不用 |
| Byte5 | 最高湿度 `Hmax` | uint8 %,只存不用 |
| Byte6 | 配置版本 | uint8,扫描匹配用 |
| Byte719 | 预留 | 只存不用 |
比较用 `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. 定时引擎(P27 组)
### 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`RO124B
31 天 × 4 字节,每格 uint32 **大端**,单位秒:
| 偏移 | 含义 |
|---|---|
| Byte0–3 | 今天加热垫运行总秒数 |
| Byte47 | 昨天 |
| … | … |
| Byte120123 | 前 30 天 |
**什么叫在运行:** 过零后继电器 **实际已吸合**`app_relay_get_actual()==1`)。pending 开但还没过零 **不算**。手动 / 定时 / 联动只要真正吸合都算。
**日期轴:** 与风扇相同 —— 未对时只往 slot[0] 加、不旋转;对时后按本地日历左移;掉电再上电先灌 VM。
**落盘:** 每累计运行满 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 | 写则手动抢权,请求开/关(等过零)。上报实际吸合 |
| 15 | 本版写忽略 |
| 6 current_temp | **WO**。已连接时服务器下发 → 更新联动用温度。不上报。断连扫描不填 |
| 7 linkage_delete | 第 10.4 节,脉冲 |
| 8 linkage_config | 20 字节 memcpy + 存 VM + 第 10.5 节立刻重判 |
| 9 time_en | 见第 11 章 |
| 1016 time_config | 8 字节 + 存 VMByte1 忽略 |
| 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 章 |
| 2426 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-{缸体号}` 且版本相同才更新温度;湿度不参与开停
- [ ] 联动:`T<Tmin` 开,`T>Tmax` 关,中间保持;无匹配包保持上一态
- [ ] 缸体号 0 / min>max 不跑联动
- [ ] 手动作废后须先 `T>Tmax``T<Tmin` 才再开
- [ ] 定时 RTC 门闩:未对时绝不跑;对时后补窗
- [ ] 跨午夜、单次自毁、每天/每周、多组重叠
- [ ] 定时窗开始开、结束关,结束立刻把联动判一次;Byte1 忽略
- [ ] DP7 脉冲删除;DP8 重写立刻重判
- [ ] 运行记录:实际吸合 10 分钟落盘;对时后跨天左移
- [ ] 出厂重置清业务留配对
- [ ] 过零超时 `fault_code=2`
- [ ] 仲裁表第 12 章逐条用例(见下)
### P2 仲裁最小用例(必须写进测试笔记)
1. 空闲 `T<Tmin` → 加热开;短按关 → 保持关,即使仍低于 Tmin。
2. 用例 1 之后先到 `T>Tmax`,再回到 `T<Tmin`,才再次开。
3. 定时窗开始 → 开;窗未结束前保持开;结束时刻关;若此时 `T<Tmin`**联动再开**
4. 定时窗内再短按关 → 保持关到窗结束也不要被定时抢回;窗结束若已被手动作废且仍 `T<Tmin` → **不要**被联动立刻打开。
5. 未对时,定时配置使能也不动作。对时后若在窗内立即开。
6. 上电 VM 为开、暂无温度 → 保持开(等过零吸合);温度到达后再按 10.3。
7. `T` 在 [Tmin, Tmax] → 保持上一态。
8. 断连扫描丢包 → 不关加热(保持上一态)。
9. 已连接写 DP6 使 `T<Tmin` → 开;断开后扫描到同一缸体更高温度 `T>Tmax` → 关。
---
## 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 节为准。