Files
JBao_Heat_Lamp_FW/Doc/电子开关-加热灯罩_固件需求规格书.md

775 lines
37 KiB
Markdown
Raw Permalink 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=`CBR0`productCode=`PRCBR0` |
| 协议 | 见宝 SOC 55AAV1.0.0 |
| 文档版本 | V1.0.1 |
| 日期 | 2026.09.08 |
| 作者 | cyWu |
| 读者 | WorkBuddy / 固件开发 |
| 参考工程 | `D:\Jianbao\Project\产品\爬宠\电子开关-加热垫\电子开关-加热垫_soc_JieLi-JL7016`(业务层 / 过零继电器 / 联动仲裁 / 定时 / 运行记录 / 按键灯效) |
| 框架参考 | `D:\Jianbao\Project\产品\爬宠\温湿度风扇\温湿度风扇_soc_JieLi-JL7016`20ms tick + 500ms loop |
| 灯效参考 | `D:\Jianbao\Project\产品\宠物\门夹弹力绳\门夹弹力绳_soc_JieLi-JL7016`(红绿双通道、OTA 红绿交替) |
| 加热垫规格 | `电子开关-加热垫_固件需求规格书.md` V1.0.1(同板行为以加热垫为准,本页只写灯罩差量与 DP 重编号) |
> **V1.0.12026-09-08):** DP4 灯泡复位本版写忽略。APP 换灯直接写 DP3(例如 2,592,000);出厂复位 DP26 仍把寿命拉回额定值。
>
> **V1.0.02026-09-08):** 首版共同理解。硬件、过零继电器、短按、联动、定时开停、31 天运行记录、故障码 2、出厂复位 **与加热垫相同**。差量:灯泡剩余寿命(额定 2,592,000 秒,按实际吸合每秒减 1,到 0 报粘性告警 1,灯仍可亮);DP9 当前温度改为 **WO**;定时配置 **11 字节且无模式字节**(使能/类型/星期/开始结束时分,Byte7–10 预留);DP2 总工作时长本版恒 0。
本文是 **已拍板的共同理解**,不是草稿。实现时以本文为准;DP 表以 `dp_definition.json` + 本文第 10.2 / 11.1 / 13.1 / 13.2 / 15 章为准。与加热垫冲突时:**引脚、过零、仲裁、联动回差跟加热垫;DP 号、定时 11 字节、寿命跟本文。**
---
## 0. WorkBuddy 开干前必读(硬约束)
1. **业务层从加热垫迁,不要从零写。**`apps/usr_app``apps/usr_periph``usr_le_code` 桥接、`usr_jb_main.c` 编排,再按本文改 DP 号、定时 11 字节、寿命模块。程序框架必须和加热垫 / 风扇一致:20ms `usr_timer_tick`(硬件定时器中断)+ 500ms `usr_timer_loop`(BLE 任务上下文)。配对/OTA/写 VM 只准在任务上下文落地。禁止在 20ms ISR 里 `syscfg_write`、切 BLE 广播、跑联动/定时/寿命告警上报。
2. **不要把业务算法写进 `jb_product.c`。** 只把 DP 写入 `gDevData` 并转调 `app_*`。寿命扣秒、复位、告警放 `app_light_life`
3. **不要抄门夹的电池 / 低电 / 深睡开机判定。** 本机 220V 常电、无电池。**上电即开机**;拔电即关,无软关机。
4. **不要抄风扇的 PWM 风扇 / 数码管 / AHT20 / 通风强力。** 执行器是继电器 + 过零;温度来自扫描或 DP9,不是本机传感器。
5. **`MAX_PACKAGE_LEN` 必须为 512。** 生成值 128 装不下 DP20 `run_record`(124B),11×7 定时 + 寿命 DP 全量一包也会超过 256。禁止再生成回 128。
6. **DP9 `current_temp` 为 WO。** 生成 JSON 是 RO,必须改成 WO。已连接时服务器下发;设备不上报;断连扫描到的温度不填 DP9。
7. **本版写忽略:** DP1 温控开关、DP2 总工作时长(上报恒 0)、**DP4 灯泡复位**、DP5 温控时间、DP6 模式、DP7/8 最低最高温度。DP21 心跳忽略。APP 换灯请写 DP3。
8. 每个模块必须能用 `board_pin.h``BOARD_xxx_ENABLE` **单独关掉** 仍能编过、能配网上报。寿命用 `BOARD_LIGHT_LIFE_ENABLE`
9. 日志全部 **英文**。中文只出现在注释和本文。
10. 编码风格对齐加热垫 + 工程师规范:文件小写+下划线,`g_` / `s_` 前缀,枚举 `Pascal_t`,接口 `xxx_init` / `xxx_run` 或 tick / `xxx_get_status`
11. **现有工程是协议骨架。** 没有 `apps/usr_app`、没有 `board_pin.h`、没有 `usr_le_code` 实现。`app_main.c` 里若存在上电强制 `usr_goto_pair=1` **必须删掉**,改为上电进工作态(已配对走重连广播)。
12. 调试 UART TX = **PC5**。关掉会与 PA1 / PA7 / PA8 / PB1 / PC3 冲突的 demo 外设。
13. **遵守第 0.1 节编码与注释。** 散落 static、把排障过程复制进每个函数、只写不读的计数,一律视为未完成。
14. **定时 11 字节布局与加热垫 8 字节不兼容。** 禁止把加热垫 `cfg[2]=类型、cfg[4]=开始小时` 原样拿来用。必须按第 11.1 节:Byte1=类型,Byte3=开始小时。
15. 协议字段名 **保持** `lighe_remain_time`(生成拼写),不要改成 `light_remain_time`
---
## 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` 并列。正例:`static LightLifeCtx_t s_life`
3. **不要只写不读的诊断变量。** 需要看沿是否在跑,用已有 getter + 一条日志。
4. **文件头三件事:做什么、一两行硬约束、history。** 同一条约束只写一次。
5. **函数注释短。** `@brief` 一行;`@param` / `@return` 按需;`@note` 只写本函数独特约束。不要复述代码。
6. **注释解释为什么,不写排障日记。** 「必须等过零再切继电器」是为什么;「我改了三次 ISR 才好」不要进源码。
7. **P0 路径不堆死字段。** 没有 getter、没有日志、P2 也没用上的字段不要先占着。
---
## 1. 产品一句话
220V 常供电的爬宠加热灯罩:过零点吸合/断开继电器(避免打火),本地短按开关,BLE 连 APP。APP 可下发灯泡开关、1 条缸体联动配置、7 组定时。连着时用服务器下发的当前温度做联动;断连时扫描风扇的 `JBao-{缸体号}` 广播取温度。灯泡实际吸合的秒数记 31 天运行记录,并从额定 30 天工时里扣除剩余寿命;扣到 0 上报到期告警,灯仍可继续亮。
---
## 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. 软件架构(迁加热垫,加寿命模块,改 DP 号)
```
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_light_life_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_light_life_tick`(已吸合且剩余 >0 则扣秒;到 0 只打脏 + 置告警,不写 flash)
7. `app_nvm_tick`(变化检测 + 500ms 防抖计时,不写 flash
8. `app_led_run`
`usr_timer_loop()`BLE 500ms 任务上下文):`userHandle()``jbProtocolHandle()`
`app_th_scan_tick()``app_linkage_run()` / `app_timer_run()`
`app_runrec_rotate_if_needed()``app_light_life_loop()`(1 分钟节流上报 + 边沿立刻报)、
`app_function_loop()`(VM 落盘 + 配对/OTA 落地)。
从加热垫拷贝后改:
| 复用 | 路径 | 改什么 |
|---|---|---|
| 按键 / 配对 / 延后 / LED | 加热垫 `key*` / `app_pair` / `app_defer` / `led*` / `app_led` | 短按仍切加热(DP0 灯泡开关);灯效不变 |
| 继电器 / 过零 | `app_relay` / `app_zc` / `bsp_zc` | 原样;fault 走 DP23 |
| 联动 / 仲裁 | `app_linkage` / `app_arbiter` | 温度 DP 改为 DP9;联动配置改为 DP11;删除改为 DP10 |
| 定时 | `app_timer_task` | **按第 11.1 重写 11 字节解析**`time_en` 改为 DP12 |
| 运行记录 | `app_runrec` | DP 改为 DP20;口径仍是实际吸合秒 |
| NVM | `app_nvm` | 增加剩余寿命;定时每组 11B;magic 用灯罩自己的 |
| 协议 | `usr_jb_proto` | 已生成 30 DP;改 `MAX_PACKAGE_LEN=512`、DP9=WO、DP3 节流上报 |
| 寿命 | **新建** `app_light_life` | 第 13.2 节 |
**不要拷贝:** `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`
---
## 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
#define BOARD_TH_SCAN_ENABLE 1
#define BOARD_RUNREC_ENABLE 1
#define BOARD_LIGHT_LIFE_ENABLE 1 /* 灯罩新增;=0 时 DP3 仍按 VM/默认上报,不扣秒、不置告警 */
#define BOARD_NVM_ENABLE 1
```
P0 建议先 `LINKAGE/TIMER/TH_SCAN/RUNREC/LIGHT_LIFE/NVM=0`,台上只验按键、双灯、过零继电器。
---
## 5. 电源与按键
### 5.1 上电 / 断电
- **上电直接进入工作态**,无需按键。流程对齐加热垫 5.1。
- **关机 = 拔电。** 无软关机、无深睡开机判定。
- 上电若键仍按着:只锁**短按**(防一上电就切灯泡),不锁 5s/12s。
- 设备工作态恒开机。DP0 是 **灯泡开关**(不是设备电源)。
- 无电池、无充电逻辑。
- **冷启动继电器:** GPIO 先保持关;加载 VM 后按上次 `heat_switch` 恢复请求;若为开则等过零再吸合。温度没到之前联动不改它(第 13 章)。
- **冷启动寿命:** 见第 13.2。VM 无效 → 剩余寿命 2,592,000,告警 0。
### 5.2 按住分级(运行态,与加热垫同一套时序)
| 按住时长 | 行为 |
|---|---|
| < 5000 ms 松开 | 短按:切换灯泡(见 5.3) |
| 满 5000 ms(仍按住) | **只**切配对提示灯(绿灯 100ms 快闪),**不清绑定、不碰寿命** |
| 满 5s 后、12s 前松开 | 提交配对:`usr_goto_pair_mode()` + `app_pair_start()` 120s |
| 满 12000 ms(到点即触发) | 进 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 章相同,仅 DP 号改为故障走 **DP23**
### 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`:禁止走「无过零硬开」;关请求仍允许强制断开(安全)。
过零恢复后:新的请求仍按第 7.1 走;`fault_code` 在下一次成功过零切变后清 0。
寿命扣秒与运行记录都看 **实际吸合**。开请求超时未吸合 → **不扣寿命、不记运行记录**
---
## 8. 温度来源(无本机传感器)
内部统一用 `temp_x10`(0.1℃,有符号)。联动比较见 10.3。
| 场景 | 温度从哪来 | 扫描 | DP9 |
|---|---|---|---|
| BLE 已连接 | 服务器写 DP9int16×0.1 → float | **停** | WO 收下,不存 VM,不上报 |
| BLE 断连且已配对 | 扫描风扇自定义广播 | **500ms 扫描 / 500ms 重连广播** 交替 | 扫描值 **不写** DP9 |
| 未配对 / 配对中 / OTA | 不扫描 | 重连或绑定/OTA 广播 | — |
湿度:广播里有,本版 **不参与开停**,只可打日志。
### 8.1 风扇侧广播格式(只读约定,灯罩不发这种包)
与加热垫 8.1 相同:
- Complete Local Name`JBao-{tank_id}`(十进制,无前导零)
- Manufacturer `0xFFFF` 后 3 字节:`[配置版本 uint8][温度 int8 ℃][湿度 uint8 %]`
匹配规则:
1. 名字等于 `JBao-` + 本机 **DP11** 缸体编号(uint16 十进制)。
2. 配置版本等于 DP11 Byte6。
3. 两者都满足才更新 `temp_x10 = 温度int8 * 10`
4. 名字不对、版本不对、解析失败:**当没收到**,保持上一温度和上一加热态。
缸体编号为 0:视为无有效联动,不扫描匹配。
### 8.2 500ms 时隙
与加热垫 8.2 相同。温度丢失:**保持上一态**,不因丢包关灯。
---
## 9. 本版不用的模式 / 温控 / 总时长 DP
| DP | 本版 |
|---|---|
| 1 温控开关 | 写忽略 |
| 2 灯泡总工作时长 | 写忽略,**上报恒 0**(不累计) |
| 4 灯泡复位 | 写忽略。APP 换灯直接写 DP3 |
| 5 温控时间 | 写忽略 |
| 6 模式 | 写忽略 |
| 7 / 8 最低最高温度 | 写忽略(阈值只在 DP11 联动 20B 里) |
| 21 心跳 | 写忽略 |
无通风/强力状态机,无 `app_mode`。定时配置里 **没有模式字节**(第 11.1)。
---
## 10. 联动引擎
设备只有 **1 条** 20 字节联动配置(**DP11**)。不改 DP6。
### 10.1 有效配置
同时满足才跑联动开停:
- 缸体编号 ≠ 0Byte01uint16 大端)
- `Tmin ≤ Tmax`
- `BOARD_LINKAGE_ENABLE=1`
否则视为无联动:不自动开;若原先是联动占着加热则关掉。
湿度 min/max、配置版本:整包 20B 原样落盘。湿度 **不参与开停**。版本只用于扫描匹配(8.1)。Byte7–19 预留,逻辑忽略。
### 10.2 20 字节布局(DP11 `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`(含等于) | **保持上一态** |
过热关、过冷开。到期告警 **不阻止** 本表。
### 10.4 联动删除(DP10
| 写入 | 行为 |
|---|---|
| 0 | 无操作 |
| 1 | 清 RAM + VM 里 20 字节;若当前是联动占着加热 → 关;然后把 `linkage_delete` **清回 0** 再上报(脉冲) |
### 10.5 APP 重写 DP11
`memcpy` 20 字节 → 存 VM → **清手动作废标志** → 立刻按新阈值重判(有有效温度时)。缸体编号写成 0:按无效配置,联动占着则关。
---
## 11. 定时引擎(P2,7 组,11 字节)
行为与加热垫相同:**窗开始开、窗结束关**。字节布局 **不是** 加热垫的 8 字节(加热垫 Byte1=模式、Byte2=类型)。本产品无模式字段。
### 11.1 11 字节(DP1319
| 偏移 | 字段 | 本版 |
|---|---|---|
| Byte0 | 使能 | 用 |
| Byte1 | 类型:0=单次,1=每天,2=每周 | 用 |
| Byte2 | 星期位图 Bit0=周一 … Bit6=周日(仅类型 2) | 用 |
| Byte3 | 开始小时 0~23 | 用 |
| Byte4 | 开始分钟 0~59 | 用 |
| Byte5 | 结束小时 | 用 |
| Byte6 | 结束分钟 | 用 |
| Byte710 | 预留 | 原样存 VM,不判断 |
- 窗开始沿:请求 **开** 继电器。
- 窗结束沿:请求 **关** 继电器,然后立刻按第 10.3 判一次联动。
- 窗内保持开(直到被手动打断,见第 12 章)。
- 开始==结束:非法窗,不执行。
- 支持跨午夜:22:00→06:00 = 当晚 22 点到次日 6 点。
- 类型 1:每天该窗。
- 类型 2:仅位图置位的星期;跨午夜以 **开始时刻所在的本地星期** 判断。
- 类型 0 单次:结束时刻过后把 **Byte0 清 0 并写 VM**
- 多组重叠:任一窗内即开;所有窗都结束才走结束关。
- 整 11 字节入库、回读(含预留)。
### 11.2 RTC 门闩(强制,与加热垫相同)
```
usr_rtc_get_time() 失败 → 全部定时不执行(等同 time_en=0)
仅 BLE 已连接但从未对时 → 同上
对时成功瞬间 → 立刻补判:若当前落在某条有效窗内,进入该窗(开灯)
```
无电池,拔电后 RTC 丢失。开机后必须等 APP/网关 `usr_rtc_set_from_unix` 成功。
`time_en=0`(DP12):所有窗立刻视为结束(关灯),再按 10.3 补判联动。
---
## 12. 仲裁(唯一决策表)
与加热垫第 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:立刻停定时(窗结束语义)。无联动总开关;无效配置 / DP10 删除即停联动。
**寿命到期不参与仲裁。** 到期只改 `alarm_code`,不强制关灯、不阻止手动/定时/联动再开。
---
## 13. 持久化(VM
存(冷启动保留):
- `heat_switch`(上次加热请求;上电按此恢复)
- `linkage_config`20B
- `time_en``time_config1~7`(各 **11B**,含 Byte710 预留)
- `fault_en``alarm_en`(若有写入)
- `run_record`31 × uint32 秒 + slot[0] 对应的本地日期
- **`lighe_remain_time`(剩余寿命秒)**
不存:
- DP1、DP2(恒 0)、DP5–8、DP9 当前温度、联动作废标志、过零 pending
- `linkage_delete``light_reset`(脉冲 / WO
- `alarm_code`(上电由剩余寿命推导,见 13.2
- BLE 三元组(`CFG_USER_PAIR_INFO`,出厂重置 **不清**
- DP2729 预留(RAM
上电:加载上述字段,继电器按上次 `heat_switch` 恢复(开则等过零);按第 13.2 恢复寿命与告警;再让联动按第 12 章冷启动规则判。
首次上电(VM 无效):加热关,联动 20B 全 0,定时全 0,`time_en=0`,31 天运行记录全 0**剩余寿命 = 2,592,000**,告警 0。
写 VM 时机:
- APP 改联动/定时、单次定时自毁、加热稳定变化:500ms 防抖
- 运行记录:累计吸合满 **10 分钟** 写一次;**跨天旋转时立刻写**
- 剩余寿命:累计吸合满 **10 分钟** 写一次(可与运行记录同一次);**下列边沿立刻写**(第 13.2)
VM magic 必须用灯罩自己的值(建议 `0xCB00`),**禁止**复用加热垫 `0xCB01`,避免两套固件互刷把 8 字节定时当 11 字节解析。
### 13.1 运行记录(DP20 `run_record`RO124B
31 天 × 4 字节,每格 uint32 **大端**,单位秒:
| 偏移 | 含义 |
|---|---|
| Byte0–3 | 今天灯泡运行总秒数 |
| Byte47 | 昨天 |
| … | … |
| Byte120123 | 前 30 天 |
**什么叫在运行:** 过零后继电器 **实际已吸合**`app_relay_get_actual()==1`)。pending 开但还没过零 **不算**。手动 / 定时 / 联动只要真正吸合都算。到期后只要还在亮,**继续累计**。
**日期轴:** 未对时只往 slot[0] 加、不旋转;对时后按本地日历左移;掉电再上电先灌 VM。
**上报:** 今天累计秒数相对上次 0x07 上报满 **60 秒** 才进差分(`JB_RUNREC_DIFF_SEC`);跨天旋转立刻带 DP20。APP 0x05 读、10 分钟全量仍带完整 124B。其它 DP 上报时不刷新运行记录基准。
**落盘:** 每累计运行满 600 秒写一次 VM。跨天立刻写。拔电接受最多丢不满 10 分钟的秒数。
`BOARD_RUNREC_ENABLE=0`:不累加、DP20 报全 0。
### 13.2 灯泡剩余寿命(DP2 / DP3 / DP25DP4 不用)
额定工时,不是日历:
```c
#define BOARD_LIGHT_RATED_SEC 2592000u /* 30 * 24 * 3600 */
```
| 规则 | 约定 |
|---|---|
| 扣秒条件 | `app_relay_get_actual()==1` 且剩余 > 0,与运行记录同一口径 |
| 减到 0 | 停在 0,不回绕;`alarm_code = 1`(灯泡到期告警)粘住 |
| 到期后加热 | **不互锁**,开停仍走第 7 / 10 / 11 / 12 章 |
| 告警推导 | `remain == 0``alarm_code = 1`,否则 `0``alarm_en` 不屏蔽 |
| DP2 | 写忽略,上报恒 0 |
| DP3 写 | **立刻生效**,范围 04,294,967,295,不封顶。**APP 换灯:直接写 DP3=2,592,000** |
| DP4 | 本版 **写忽略**,不上报 |
| 按键 5s 清绑定 | 不碰寿命 |
| 出厂复位 DP26 | 剩余寿命回到 2,592,000,告警 0(与第一次开机相同) |
**边沿副作用(必须同一套):** APP 写 DP3、扣到 0、出厂复位:
1.`gDevData.lighe_remain_time`
2. 按值置/清 `alarm_code`
3. **立刻写 Flash**(任务上下文,禁止 ISR
4. **立刻上报** DP3 与 DP25(不受 1 分钟节流)
**平时写 Flash** 约 600 秒实际点灯写一次(允许丢最后几十秒~几分钟)。边沿必须立刻写,避免掉电把已到期「复活」,或 APP 刚改完寿命就丢。
**DP3 上报节流:**
- 实际吸合且剩余 >0:距上次 DP3 上报满 **墙上 60 秒** 才再走变化上报
- 到期、APP 写 DP3、读请求、10 分钟全量:**立刻带上 DP3**
- 其它 DP 仍 2 秒变化防抖;DP25 随告警边沿立刻报
`BOARD_LIGHT_LIFE_ENABLE=0`:不扣秒、不因寿命置告警;DP3 若 VM 里有值仍上报,否则 0。
上电加载剩余寿命后立刻按上表推导 `alarm_code`(不单独存告警)。
---
## 14. 出厂重置(DP26 写 1
- **清全部业务 VM**(第 13 章列表,含联动 20B、31 天运行记录、剩余寿命)。
- RAM:加热关,联动/定时停止,作废标志清掉,**剩余寿命 = 2,592,000,告警 0**,立刻写 VM 并上报。
- **保留 BLE 配对,不断开、不软复位。**
- 换机:运行中 **按住 5s 后松手** 清配对进绑定广播(见 5.2),**不复位寿命**。
---
## 15. BLE / DP 行为摘要
身份:`CBR0` / `PRCBR0`。帧格式见 `jb_protocol.h``MAX_PACKAGE_LEN=512`)。DP 共 30 个(ID 029)。
| DP | Key | 处理 |
|---|---|---|
| 0 | heat_switch | 写则手动抢权,请求开/关(等过零)。上报实际吸合。显示名「灯泡开关」 |
| 1 | temp_control_switch | 本版写忽略 |
| 2 | light_work_time | 本版写忽略,上报恒 0 |
| 3 | lighe_remain_time | 第 13.2 节。RW;固件扣秒;APP 写立刻生效 |
| 4 | light_reset | **WO,本版写忽略。** APP 换灯直接写 DP3 |
| 5 | temp_control_time | 本版写忽略 |
| 6 | mode | 本版写忽略 |
| 7 / 8 | min_temp / max_temp | 本版写忽略 |
| 9 | current_temp | **WO**。已连接时服务器下发 → 更新联动用温度。不上报。断连扫描不填 |
| 10 | linkage_delete | 第 10.4 节,脉冲 |
| 11 | linkage_config | 20 字节 memcpy + 存 VM + 第 10.5 节立刻重判 |
| 12 | time_en | 见第 11 章 |
| 1319 | time_config1~7 | **11 字节** + 存 VM;按第 11.1 解析 |
| 20 | run_record | RO,由 `app_runrec` 填 124B,禁止 APP 写 |
| 21 | heartbeat | 本版忽略 |
| 22 | fault_en | 原样存,本版不做位级过滤 |
| 23 | fault_code | 0 无 / 1 温度异常(本版不上报) / **2 过零超时等设备故障** |
| 24 | alarm_en | 原样存,不屏蔽告警 |
| 25 | alarm_code | 0 无 / **1 灯泡到期告警**(由剩余寿命推导) |
| 26 | factory_reset | 第 14 章 |
| 2729 | reserva_dp1~3 | 存 RAM,无动作 |
OTA:运行态按住 12s 走 `usr_app_ota_init`(任务上下文,红绿交替)。协议 OTA(`jbOnOtaNotify`)**可继续 stub**,禁止假装升级成功后擦用户区。
配对超时 120s,失败保持开机,灯回「跟继电器」。
断连已配对:500ms 见宝重连广播 ↔ 500ms 扫描(第 8.2)。绑定广播仍用 `CBR0` / `PRCBR0`,不要改成 `JBao-` 名。
生成协议必须改动(相对当前骨架):
1. `MAX_PACKAGE_LEN` 128 → **512**
2. DP9 `current_temp` **RO → WO**pack/diff **排除** DP9
3. DP3 变化上报加 60s 节流(边沿除外)
4. `dp_definition.json` 同步:DP9 `dir=wo`;定时 11 字节描述改成第 11.1 节表
---
## 16. 建议文件清单(从加热垫拷贝改 + 新建寿命)
```
apps/usr_periph/
board_pin.h # 引脚、时序、BOARD_xxx_ENABLE(含 LIGHT_LIFE
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
led.c/.h, led_manager.c/.h
app_led.c/.h
app_defer.c/.h
app_pair.c/.h
app_relay.c/.h
app_th_scan.c/.h # 缸体号改读 DP11
app_linkage.c/.h # 温度改 DP9,配置改 DP11
app_runrec.c/.h # 上报 DP20
app_timer_task.c/.h # 第 11.1 节 11 字节
app_light_life.c/.h # 新建,第 13.2 节
app_nvm.c/.h # magic 0xCB0011B 定时;剩余寿命
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
与加热垫 P0 相同(按键、双灯、过零继电器、5s/12s、删上电强制配对)。
**P0 关闭:** `BOARD_LINKAGE/TIMER/TH_SCAN/RUNREC/LIGHT_LIFE/NVM_ENABLE=0`
### P1 — 最小产品闭环
- [ ] 5s 配对,APP 能写 DP0 开关灯泡(过零后灯和继电器一致,DP0 报实际态)
- [ ] 短按同步上报 DP0
- [ ] VM:改开关后断电,重新上电按上次开关恢复(开则等过零再吸合)
- [ ] DP1 / DP2 / DP4 / DP58 写忽略;DP2 上报 0
- [ ] DP9 WO:APP/服务器能写下当前温度;设备全量上报 **不含** DP9
- [ ] 定时 7 组 11 字节 + `time_en` 能存;`BOARD_TIMER_ENABLE=0` 时不执行
- [ ] `MAX_PACKAGE_LEN=512`,APP 能读完整 124B 运行记录(本阶段记录可仍为 0)
- [ ] 第一次开机 DP3 = 2,592,000DP25 = 0
### P2 — 自动化 + 寿命
- [ ] 断连:500ms 重连广播 / 500ms 扫描;已连接停扫描
- [ ] 匹配 `JBao-{缸体号}` 且版本相同才更新温度;湿度不参与开停
- [ ] 联动:`T<Tmin` 开,`T>Tmax` 关,中间保持;无匹配包保持上一态
- [ ] 缸体号 0 / min>max 不跑联动
- [ ] 手动作废后须先 `T>Tmax``T<Tmin` 才再开
- [ ] 定时 RTC 门闩:未对时绝不跑;对时后补窗
- [ ] 跨午夜、单次自毁、每天/每周、多组重叠
- [ ] 定时窗开始开、结束关,结束立刻把联动判一次;**按 11.1 解析,不是加热垫 8 字节**
- [ ] DP10 脉冲删除;DP11 重写立刻重判
- [ ] 运行记录:实际吸合 10 分钟落盘;对时后跨天左移
- [ ] 出厂重置清业务留配对,寿命回到 2,592,000
- [ ] 过零超时 `fault_code=2`DP23
- [ ] 仲裁表第 12 章逐条用例(与加热垫相同,DP 号按本文)
- [ ] **寿命:** 实际吸合才扣;到 0 停在 0`alarm_code=1`,灯仍可亮
- [ ] **DP4 写忽略:** 写 1 不改寿命
- [ ] **APP 写 DP3** 任意 uint32 生效;写 2,592,000 即换灯;>0 清告警,=0 置告警;立刻落盘
- [ ] **5s 清绑定不改寿命**
- [ ] 点灯时 DP3 最多 1 分钟报一次;到期/复位/写 DP3 立刻报
- [ ] 掉电再上电:未到期接着扣;已到期仍为 0 且告警 1
### P2 仲裁最小用例(必须写进测试笔记)
与加热垫第 17 章用例 1–9 相同,把「加热」读成「灯泡」、DP6 读成 **DP9**。额外:
10. 剩余寿命置 5,实际吸合 5 秒 → DP3=0、DP25=1,继电器保持开(若请求仍为开)。
11. 用例 10 之后 APP 写 DP3=2,592,000 → DP25=0。写 DP4=1 **不得**改寿命。
12. APP 写 DP3=100 后立刻拔电再上电 → 仍约 100(允许未满边沿的秒误差,不允许回到 2,592,000)。
13. 定时 11 字节:Byte1=1(每天)、Byte36=时分,Byte0=1 → 到点开;若误用加热垫偏移(Byte2 当类型)则本条失败。
---
## 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
#define BOARD_LIGHT_RATED_SEC 2592000u
#define BOARD_LIGHT_REPORT_MS 60000u
#define BOARD_LINKAGE_CFG_LEN 20
#define BOARD_TIME_CFG_LEN 11
```
产品码:
```c
#define JB_CATEGORY_CODE "CBR0"
#define JB_PRODUCT_CODE "PRCBR0"
```
---
## 19. 风险与不要擅自发挥的点
| 风险 | 要求 |
|---|---|
| 无过零硬切继电器 | 禁止。打火会烧触点 |
| 开请求过零丢失仍吸合 | 禁止。只允许关路径超时强关 |
| 抄风扇「过热开吹」 | 禁止。本机是过冷开、过热关 |
| 湿度参与开停 | 本版禁止 |
| 丢广播就关灯 | 禁止(保持上一态) |
| 生成协议再导回 DP9=RO / LEN=128 | 禁止;以本文 + 改过的源码为准 |
| 把加热垫 8 字节定时偏移用到灯罩 | 禁止。Byte1 是类型不是模式 |
| 到期强制关灯 | 禁止。只告警 |
| 用 DP4 做换灯 | 禁止。本版 DP4 写忽略;APP 换灯直接写 DP3 |
| 每秒写一次寿命到 Flash | 禁止。平时 600s,边沿立刻写 |
| 点灯时每 2 秒报 DP3 | 禁止。最多 60s 一次(边沿除外) |
| 改 `lighe_remain_time` 拼写 | 禁止 |
| 复用加热垫 VM magic | 禁止 |
| `app_main` 上电清配对进绑定 | 必须删 |
| 20ms ISR 写 VM / 切 BLE | 禁止 |
| 一排裸 static / 排障史注释 | 禁止。详见第 0.1 节 |
| 协议 OTA stub 返回 OK 当已升级 | 注释标明未升级 |
| 扫描时改 GATT 名 | 不要改 0x2A00 |
| 门夹 USB / 电池逻辑 | 禁止移植 |
| 把灯泡开理解成「设备开机灯」 | 开机后关灯是绿灯,不是灭灯 |
| 5s 清绑定顺便复位寿命 | 禁止 |
---
## 20. 给 WorkBuddy 的推荐开工顺序
1. 确认 `MAX_PACKAGE_LEN=512`、DP9=WOpack/diff 排除)。`jb_product.c` 只保留 DP 赋值,业务迁 `app_*`
2. 从加热垫拷 `usr_app` / `usr_periph` / `usr_le_code` / `usr_jb_main`;改产品码 `CBR0`/`PRCBR0`;删 `app_main` 上电强制配对。
3. `board_pin.h` 与加热垫相同引脚;加上 `BOARD_LIGHT_LIFE_ENABLE` 与寿命宏。
4. 改 DP 映射:联动 DP11、删除 DP10、温度 DP9、定时 DP1219、运行记录 DP20、故障 DP23、告警 DP25、出厂 DP26。
5. **重写定时 11 字节解析**(第 11.1),不要沿用 `cfg[2]/cfg[4]`
6. P0:继电器 + 灯 + 按键验收。
7. P1:VM 恢复开关、忽略 DP、定时只存、首次 DP3=2,592,000。
8. P2:扫描/联动/定时引擎/运行记录/仲裁用例。
9. 新建 `app_light_life`:扣秒、写 DP3、告警、60s 节流、边沿立刻落盘。DP4 写忽略。
每一步都保持对应 `BOARD_xxx_ENABLE` 可关。提交前对照第 0.1 节:一个模块一个 `s_xxx`,注释写为什么,不要排障日记。
---
## 附录 A. 与加热垫 DP 号对照
| 加热垫 | 灯罩 | 备注 |
|---|---|---|
| 0 加热开关 | 0 灯泡开关 | 同一字段 `heat_switch` |
| 1 温控开关 | 1 | 忽略 |
| — | 2 总工作时长 | 新增,恒 0 |
| — | 3 剩余寿命 | 新增 |
| — | 4 灯泡复位 | 新增,WO,**本版忽略** |
| 2 温控时间 | 5 | 忽略 |
| 3 模式 | 6 | 忽略 |
| 4 / 5 最低最高温度 | 7 / 8 | 忽略 |
| 6 当前温度 WO | 9 当前温度 WO | 生成物曾误标 RO,必须改 |
| 7 / 8 联动删除/配置 | 10 / 11 | |
| 9 time_en | 12 | |
| 1016 定时 8B | 1319 定时 **11B** | **布局不同** |
| 17 运行记录 | 20 | |
| 18 心跳 | 21 | 忽略 |
| 19 / 20 故障 | 22 / 23 | |
| 21 / 22 告警 | 24 / 25 | 灯罩启用告警 1 |
| 23 出厂 | 26 | 额外复位寿命 |
| 2426 预留 | 2729 | |
---
**文档结束。** 行为争议以本文第 5~13 章为准;字节布局以第 10.2 / 11.1 / 13.1 / 13.2 节为准。