Files
JBao_Sprayer_FW/Doc/喷淋器_固件需求规格书.md
wuchuyuan 0dd65e1552 1、喷淋器 P1/P2功能实现
2、红灯按连接态慢闪
2026-09-09 19:57:00 +08:00

821 lines
39 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=`CBR2`productCode=`PRCBR2` |
| 协议 | 见宝 SOC 55AAV1.0.0 |
| 文档版本 | V1.0.2 |
| 日期 | 2026.09.09 |
| 作者 | cyWu |
| 读者 | WorkBuddy / 固件开发 |
| 参考工程 | `D:\Jianbao\Project\产品\爬宠\电子开关-加热垫\电子开关-加热垫_soc_JieLi-JL7016`(框架 / 仲裁 / 联动 / 运行记录 / 配对 OTA / NVM |
| 框架源头 | `D:\Jianbao\Project\产品\爬宠\温湿度风扇\温湿度风扇_soc_JieLi-JL7016`20ms + 500ms、按键分级、扫描时隙) |
| 灯驱参考 | `D:\Jianbao\Project\产品\宠物\门夹弹力绳\门夹弹力绳_soc_JieLi-JL7016`(红绿双通道硬件;**事件表不要抄加热垫互斥灯**) |
> **V1.0.22026-09-09):** 红灯跟 **BLE 连接态**:已连接常亮;已配对未连接(上电重连 / 断连)500ms 慢闪。避免上电把「已绑定」当成「已联网」。
>
> **V1.0.12026-09-09):** 灯脚改为与加热垫相同:**PA8 绿 / PA7 红**(低亮)。红绿通道独立语义不变。
>
> **V1.0.02026-09-09):** 首版共同理解。5V 常电双脚水泵;两键两灯;红绿通道独立;单击按上次配置跑「时长 × 次数 + 间隔」;APP 写 DP7 立刻开喷;湿度联动回差;7 条计划到点触发一轮(不是时间窗);运行记录计水泵实际转动秒;`MAX_PACKAGE_LEN=512`。
本文是 **已拍板的共同理解**,不是草稿。实现时以本文为准;DP 表以 `dp_definition.json` + 本文第 10.2 / 11.2 / 12.1 / 14.1 / 16 章为准。
---
## 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. **不要抄门夹的电池 / 低电 / 深睡开机判定。** 本机 **5V 常电、无电池**。上电即开机;拔电即关,无软关机。
4. **不要抄加热垫的过零 / 继电器,也不要抄风扇的 PWM / 数码管 / AHT20。** 执行器是 **PC2 + PA6 双脚水泵**;湿度来自扫描或 DP6,不是本机传感器。
5. **`MAX_PACKAGE_LEN` 必须为 512。** 生成值是 128,装不下 DP18 `run_record`(124B)。改完禁止再生成回 128。OTA 单包 `JB_OTA_MAX_PAYLOAD` 可保持 96。
6. **DP6 `current_humi` 为 WO。** 已连接时服务器下发;设备不上报;断连扫描到的湿度不填 DP6。
7. **DP5 `current_temp` 本版忽略。** 联动只看湿度。
8. **本版写忽略、不实现动作:** DP0 总开关、DP2 模式、DP3 单次时长、DP4 连续时长、DP5 当前温度、DP19 心跳。DP21 / DP23 保持 0(无缺水检测)。DP20 / DP22 原样存、不做位过滤。
9. 每个模块必须能用 `board_pin.h``BOARD_xxx_ENABLE` **单独关掉** 仍能编过、能配网上报。
10. 日志全部 **英文**。中文只出现在注释和本文。
11. 编码风格对齐风扇 + 工程师规范:文件小写+下划线,`g_` / `s_` 前缀,枚举 `Pascal_t`,接口 `xxx_init` / `xxx_run` 或 tick / `xxx_get_status`
12. **现有工程是协议骨架。** 没有 `apps/usr_app`、没有 `board_pin.h``app_main.c` 里上电强制 `usr_goto_pair=1` **必须删掉**,改为上电进工作态(已配对走重连广播)。
13. 调试 UART TX = **PC5**。关掉会与 PA8 / PA7 / PB1 / PA0 / PC2 / PA6 冲突的 demo 外设(IR 键、PWM LED、demo I2C 等)。
14. **红灯 PA7、绿灯 PA8。** 与加热垫灯脚相同。禁止再把两灯脚写反。
15. **灯是两路独立通道。** 喷淋 / 空闲只改绿灯,不要去关红灯;配网闪红灯时不要去关绿灯。只有 OTA 和配对失败动画临时占用双灯。禁止抄加热垫「开红关绿、互斥」事件表。
16. **上电绝不自动喷。** 不从 VM 恢复 DP1。泵两脚上电默认低。
17. **遵守第 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_last_level``s_initialized` 并列。正例:`static PumpCtx_t s_pump`
3. **不要只写不读的诊断变量。** 需要看泵是否在转,用已有 getter + 一条日志。
4. **文件头三件事:做什么、一两行硬约束、history。** 同一条约束只写一次。禁止把排障日记复制到每个 `@note`
5. **函数注释短。** `@brief` 一行;`@param` / `@return` 按需;`@note` 只写本函数独特约束。不要复述代码。
6. **注释解释为什么,不写排障日记。** 「两脚必须同时高,泵才会转」是为什么;「我改了三次 GPIO 才好」不要进源码。
7. **P0 路径不堆死字段。** 没有 getter、没有日志、P2 也没用上的计数不要先占着。
---
## 1. 产品一句话
5V 常供电的爬宠喷淋器:双 GPIO 同时导通才开泵。本地单击喷淋键,按上次 APP 配置跑完「单次时长 × 次数 + 间隔」一轮;APP 可写配置立刻开喷,也可写开关停喷。连着时用服务器下发的湿度做联动;断连时扫描风扇 `JBao-{缸体号}` 广播取湿度。过干则循环喷、过湿则停、中间维持上一态。7 条喷淋计划到开始时:分各跑一轮(不是加热垫那种时间窗)。水泵实际转动的秒数记 31 天运行记录。
---
## 2. 硬件资源
### 2.1 引脚(以用户原理图说明 + 本文为准)
| 功能 | MCU | 电平 / 说明 |
|---|---|---|
| 红灯 | **PA7** `IO_PORTA_07` | **低电平亮**(与加热垫相同) |
| 绿灯 | **PA8** `IO_PORTA_08` | **低电平亮**(与加热垫相同) |
| 喷淋键 | **PB1** `IO_PORTB_01` | 低电平按下 |
| 配网键 | **PA0** `IO_PORTA_00` | 低电平按下 |
| 水泵总供电 | **PC2** `IO_PORTC_02` | **高电平**把 5V 导到泵 VCC |
| 水泵开关 | **PA6** `IO_PORTA_06` | **高电平**导通泵;必须与 PC2 同时开,泵才工作 |
| 调试 UART TX | **PC5** `IO_PORTC_05` | 只发打印 |
| 供电 | 5V 常电 → 板内 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 键、PWM LED、demo I2C / RDEC 等)。产品灯走 `app_led`,保持 `TCFG_PWMLED_ENABLE=DISABLE`
### 2.2 水泵安全
- GPIO 上电默认:**PC2=0 且 PA6=0**。
- 喷水时段:两脚同时置高。
- 间隔等待、空闲、停止、故障收口:两脚同时拉低。
- **禁止只开其中一只脚。** 写泵接口必须成对操作。
- 无过零、无继电器模块。切 GPIO 可在 20ms 节拍里直接改(5V 小泵,不是市电触点)。
- 上电绝不按 VM 自动开泵。
---
## 3. 软件架构(对齐加热垫,删过零/继电器,加双键/双脚泵/喷淋作业)
```
apps/earphone/app_main.c
└─ usr_rtc_init()
└─ usr_jb_init() ← 唯一组合根
bsp_hw_init()
app_led_init() ← 灯效通道表最先装载
jbInit() + userInit()
(上电即开机:无按键等待)
app_pump_init()
app_spray_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_spray_poll`(作业节拍:时长 / 间隔倒数;到点切泵)
4. `app_function_poll`(同步 DP1 = 作业态,不是某一只脚的瞬时电平)
5. `app_runrec_tick`(两脚都已导通则累加秒;满 10 分钟只打脏,不写 flash)
6. `app_nvm_tick`(变化检测 + 500ms 防抖计时,不写 flash
7. `app_led_run`
**priority 固定 1、只注册一次,不动态重挂**5V 常电,不需要低功耗。
`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 |
| 配对会话 | `app_pair.c` | 120s 超时;红灯通道跟配对态 |
| 延后执行 | `app_defer.c` | 原样 |
| NVM / 仲裁 / 运行记录 | `app_nvm` / `app_arbiter` / `app_runrec` | owner 从继电器改为喷淋作业;记录改计泵实际导通秒 |
| 联动 | `app_linkage` | 开停条件改湿度回差(第 11.3);湿度源改扫描 / DP6 |
| 定时 | `app_timer_task` | **改成到点触发一轮**,不要抄加热垫时间窗 |
| RTC | `usr_rtc.c` | 原样 |
| 扫描 | `app_th_scan` | 取广播湿度,不取温度 |
| 协议骨架 | `usr_jb_proto` | 已生成;`MAX_PACKAGE_LEN` 改 512DP6=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_relay.c``app_zc.c``bsp_zc.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_PUMP_ENABLE 1
#define BOARD_SPRAY_ENABLE 1 /* 喷淋作业引擎;=0 时键/DP 不转泵 */
#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 上电 / 断电
- **上电直接进入工作态**,无需按键。流程:`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 本版忽略。DP1 是 **喷淋作业开关**
- 无电池、无充电逻辑。
- **冷启动水泵:** GPIO 先保持关。VM 恢复配置 / 联动 / 计划 / 运行记录,**不恢复 DP1,不开泵**。湿度没到之前联动不改它(第 13 章)。
- 上电灯:绿灯常亮;未配对红灯灭;已配对未连接红灯 500ms 慢闪;BLE 连上后红灯常亮。
### 5.2 两键分工(运行态)
加热垫是一颗键兼短按 / 配网 / OTA。本机必须拆开,禁止把 5s / 12s 做在喷淋键上。
#### PB1 喷淋键
| 事件 | 行为 |
|---|---|
| 单击(松开且按下时长 < 5000 ms) | 空闲 → 用上次有效配置开一轮(第 10 章);作业中 → 立刻停(第 10.4) |
| 按住 ≥ 5s | **无动作**(不配网、不进 OTA |
#### PA0 配网键
| 按住时长 | 行为 |
|---|---|
| < 5000 ms 松开 | 短按:无动作 |
| 满 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 会话中的按键
| 场景 | PB1 | PA0 |
|---|---|---|
| 配对进行中 | 仍可启停喷淋 | 忽略再次 5s / 12s |
| OTA 进行中 | 忽略 | 忽略 |
---
## 6. LED(红绿独立通道,低亮)
灯脚:**PA7 红、PA8 绿**(与加热垫相同)。两路独立,固件改其中一个 GPIO 时禁止顺手改另一个,除非命中下面的「双灯例外」。
### 6.1 绿灯(喷淋通道)
| 状态 | 绿灯 |
|---|---|
| 空闲(DP1=关) | 常亮 |
| 喷淋作业中(含间隔等待,DP1=开) | 500ms 亮 / 500ms 灭 |
| 上电 | 常亮(泵关) |
绿灯跟 **作业态**DP1),不是 PA6 / PC2 的瞬时电平。间隔等待也算作业中,绿灯继续闪。
### 6.2 红灯(配网通道)
| 状态 | 红灯 |
|---|---|
| 未配对 / 配对失败收尾后 | 灭 |
| 已配对、BLE **未连接**(上电重连 / 断连) | **500ms 慢闪**(空闲 / 喷淋都不要关它) |
| 已配对、BLE **已连接** | **常亮** |
| 配网键满 5s 仍按住、以及配对进行中 | 100ms 亮 / 100ms 灭(此时即使已配对也改成快闪) |
| 配对成功 | 若当时已连接则常亮,否则先慢闪,连上再常亮 |
配网闪红灯期间,绿灯继续按 6.1 闪或常亮,不要关绿灯。
### 6.3 双灯例外(临时占用两路,结束后各回自己的通道)
| 事件 | 图案 | 结束后 |
|---|---|---|
| OTA | 红绿交替,200ms 半周期,直到升级结束 / 复位 | — |
| 配对失败 / 超时 | 红绿交替 3s | 红灯灭;绿灯回到空闲或喷淋 |
优先级高→低:**OTA → 配对失败动画 → 各走各的通道(红=连接/配网态,绿=作业态)**。
---
## 7. 水泵(双脚同时切)
### 7.1 接口
- `app_pump_init` / `app_pump_set(uint8_t on)` / `app_pump_get()`
- `on=1`PC2=1 且 PA6=1。
- `on=0`PC2=0 且 PA6=0。
- 禁止导出「只改一只脚」的接口。
- `BOARD_PUMP_ENABLE=0``set` 直接 returnGPIO 不配。
### 7.2 谁可以切泵
只有 `app_spray` 作业引擎调用 `app_pump_set`。键 / DP / 联动 / 计划都只请求「开一轮 / 停」,不直接改 GPIO。
---
## 8. 湿度来源(无本机传感器)
内部统一用 `humi`(uint8,单位 %)。联动比较见 11.3。
| 场景 | 湿度从哪来 | 扫描 | DP6 |
|---|---|---|---|
| BLE 已连接 | 服务器写 DP6uint8 % | **停** | WO 收下,不存 VM,不上报 |
| BLE 断连且已配对 | 扫描风扇自定义广播 | **500ms 扫描 / 500ms 重连广播** 交替 | 扫描值 **不写** DP6 |
| 未配对 / 配对中 / OTA | 不扫描 | 重连或绑定 / OTA 广播 | — |
DP5 当前温度:本版 **写忽略**,不参与开停。
### 8.1 风扇侧广播格式(只读约定,喷淋器不发这种包)
风扇 `app_th_adv.c`
- Complete Local Name`JBao-{tank_id}`(十进制,无前导零),例如缸体 1 → `JBao-1`
- Manufacturer `0xFFFF` 后 3 字节:`[配置版本 uint8][温度 int8 ℃][湿度 uint8 %]`
喷淋器匹配规则:
1. 名字等于 `JBao-` + 本机 DP9 缸体编号(uint16 十进制)。
2. 配置版本等于 DP9 Byte6。
3. 两者都满足才更新 `humi = 湿度uint8`
4. 名字不对、版本不对、解析失败:**当没收到**,保持上一湿度和上一喷淋态(第 11.3)。
缸体编号为 0:视为无有效联动,不扫描匹配。
### 8.2 500ms 时隙
断连已配对:
```
tick 偶数:见宝重连广播(encode4reconn
tick 奇数:关广播、开扫描 ≤500ms,回调里滤 JBao- 前缀
已连接 / 配对 / OTA:退出该交替,回到风扇同款 adv 策略
```
实现时若协议栈不能真正双角色并发,**必须时分**,不允许「边广播边扫」把时序写死成阻塞。
湿度丢失(长时间无匹配包):**保持上一态**,不因丢包停喷。
---
## 9. 本版不用的 DP
| DP | 本版 |
|---|---|
| 0 power 总开关 | 写忽略 |
| 2 mode 模式 | 写忽略(单次 / 连续那套预留语义作废,喷淋参数只走 DP7 / 计划自带字段) |
| 3 once_spray_time | 写忽略 |
| 4 continuous_spray_time | 写忽略 |
| 5 current_temp | 写忽略;不上报 |
| 19 heartbeat | 写忽略 |
| 21 fault_code | 保持 0 |
| 23 alarm_code | 保持 0(缺水告警本版无传感器) |
`app_mode`。DP20 / DP22 有写入则原样存 VM。
---
## 10. 喷淋作业引擎
设备同一时刻最多跑 **一轮** 作业。一轮 = 按「单次时长、次数、间隔」把泵开停若干次,然后结束。
### 10.1 有效配置
来源优先级(只决定 **参数**,不决定谁有权开):
1. 计划触发:用 **该条计划自己的** 时长 / 次数 / 间隔(第 12.1)。
2. 其它开喷(PB1、DP1=1、写 DP7、联动):用 RAM / VM 里的 DP7。
3. DP7 无效或从未收过:默认 **时长 10s、次数 3、间隔 3s**
首次上电 VM 无效:把默认写入 RAM,并作为 DP7 上报,当作已有有效配置。
### 10.2 DP7 `spray_config`8 字节)
| 偏移 | 字段 | 编码 |
|---|---|---|
| Byte01 | 单次喷淋时长 | uint16 **大端**,单位秒,165535 |
| Byte2 | 喷淋次数 | uint81255 |
| Byte34 | 喷淋间隔 | uint16 **大端**,单位秒,065535 |
| Byte57 | 预留 | 只存不用 |
默认上电值(十六进制):`00 0A 03 00 03 00 00 00`
非法包:**时长=0 或 次数=0**。整包丢弃:不改 RAM / VM,不开喷,保持上一份有效配置。
**间隔=0** 喷完一次马上喷下一次,中间 **不停泵**(两脚保持高,直到整轮次数用完)。
APP 写 DP7(合法包):`memcpy` 8 字节 → 存 VM → **立刻按新参数开一轮**。若正在喷:中止旧轮再开新轮。记入手动抢权(第 13 章),DP1 报开。
### 10.3 一轮怎么跑
```
count 次循环:
泵开
等待 duration 秒
若还不是最后一次 且 interval > 0:
泵关
等待 interval 秒
泵关
作业结束,DP1 报关
```
- 整轮期间(含间隔等待)DP1 = 1,绿灯按作业态闪。
- 轮次自然结束:泵关,DP1 = 0。
- 20ms 节拍里倒数;不要用阻塞 `delay`
### 10.4 停止
任一来源请求停:
- 立刻两脚拉低,结束作业,DP1 报关。
- 来源是 PB1 单击(作业中)或 APP 写 DP1=0:手动作废联动(第 13 章)。
- 若当前轮是 **类型 0 单次计划** 占用的:该条 Byte0 清 0 并写 VM(被手动中途停也要清,避免单次卡死)。
### 10.5 其它开喷入口
| 入口 | 参数 | 是否手动抢权 |
|---|---|---|
| PB1 单击(空闲) | 上次有效 DP7 | 是 |
| APP 写 DP1=1 | 上次有效 DP7 | 是 |
| APP 写 DP7 | 新 DP7 | 是 |
| 计划到点 | 该条 Byte5–9 | 否(计划排队,见 12.3) |
| 联动请求开 | 上次有效 DP7 | 否 |
APP 写 DP1=1 时若已在作业中:保持当前轮,不重开(幂等)。写 DP1=0 时若已空闲:保持关。
---
## 11. 联动引擎
设备只有 **1 条** 20 字节联动配置(DP9)。布局与加热垫 DP8 相同,本版只用湿度字段。
### 11.1 有效配置
同时满足才跑联动开停:
- 缸体编号 ≠ 0Byte01uint16 大端)
- `Hmin ≤ Hmax`
- `BOARD_LINKAGE_ENABLE=1`
否则视为无联动:不自动开;若原先是联动占着作业则停泵。
温度 min / max、配置版本:整包 20B 原样落盘。温度 **不参与开停**。版本只用于扫描匹配(8.1)。Byte7–19 预留,逻辑忽略。
无单独「联动总开关」。停联动:DP8 写 1、或 DP9 缸体写成 0 / min>max、或手动作废。
### 11.2 20 字节布局(DP9 `linkage_config`
| 偏移 | 字段 | 编码 |
|---|---|---|
| Byte01 | 缸体编号 | uint16 **大端** |
| Byte2 | 最低温度 `Tmin` | 有符号 int8 ℃,只存不用 |
| Byte3 | 最高温度 `Tmax` | 有符号 int8 ℃,只存不用 |
| Byte4 | 最低湿度 `Hmin` | uint8 % |
| Byte5 | 最高湿度 `Hmax` | uint8 % |
| Byte6 | 配置版本 | uint8,扫描匹配用 |
| Byte719 | 预留 | 只存不用 |
比较用 uint8 湿度:`H < Hmin``H > Hmax`。**等于不算越界**,走「中间维持上一态」。
已连接:`humi = gDevData.current_humi`。断连扫描:广播第 3 字节。尚无任何有效湿度:联动不改泵(保持上电关 / 上一态)。
### 11.3 开停(只看湿度)
在有效配置、未被手动作废、仲裁允许联动、且已有有效湿度时,每个 500ms 任务 tick 判一次:
| 条件 | 动作 |
|---|---|
| `H < Hmin` | 请求进入喷淋:用 DP7 循环开一轮又一轮(一轮正常结束后立刻再开,无额外间隔) |
| `H > Hmax` | 立刻停泵、结束作业 |
| `Hmin ≤ H ≤ Hmax`(含等于) | **保持上一态**(上一态在喷就继续循环,上一态停就保持停) |
过干开、过湿关。无有效湿度 / 丢包:不改。
联动只在 **空闲且未被作废** 时开下一轮。泵被手动 / 计划占着时联动不抢(第 13 章)。
### 11.4 联动删除(DP8
| 写入 | 行为 |
|---|---|
| 0 | 无操作 |
| 1 | 清 RAM + VM 里 20 字节;若当前是联动占着作业 → 停;然后把 `linkage_delete` **清回 0** 再上报(脉冲) |
### 11.5 APP 重写 DP9
`memcpy` 20 字节 → 存 VM → **清手动作废标志** → 立刻按新阈值重判(有有效湿度时)。缸体编号写成 0:按无效配置,联动占着则停。
---
## 12. 喷淋计划引擎(7 组,到点触发一轮)
**不要抄加热垫时间窗。** 加热垫是开始~结束保持开;本机是「到开始时:分,用该条自己的时长 / 次数 / 间隔跑完一轮就结束」。没有结束时:分。
### 12.1 15 字节(DP1117
| 偏移 | 字段 |
|---|---|
| Byte0 | 使能 0/1 |
| Byte1 | 定时类型:0=单次,1=每天,2=每周。其它值该条无效,不触发 |
| Byte2 | 星期位图 Bit0=周一 … Bit6=周日(仅类型 2) |
| Byte3 | 开始小时 023 |
| Byte4 | 开始分钟 059 |
| Byte56 | 单次时长 uint16 **大端** 秒 |
| Byte7 | 次数 uint8 |
| Byte89 | 间隔 uint16 **大端** 秒 |
| Byte1014 | 预留,只存不用 |
本版 **没有** 加热垫那种「Byte1 模式忽略」。Byte1 就是类型。
非法参数(时长=0 或 次数=0):该条本分钟不触发,使能保持原值。
### 12.2 总开关与 RTC 门闩
`DP10 spray_plan_en`:所有计划的总开关。
```
usr_rtc_get_time() 失败 → 全部计划不新触发(等同 plan_en=0 的「不触发」)
仅 BLE 已连接但从未对时 → 同上
plan_en=0 → 不新触发
```
`plan_en=0` 或 RTC 无效:**不中断** 已经在跑的一轮(手动 / APP / 联动仍可停)。
无电池,拔电后 RTC 丢失。开机后必须等 APP / 网关 `usr_rtc_set_from_unix` 成功。
对时成功瞬间:若 **当前时:分刚好等于** 某条有效计划的开始时:分,补触发一次;**过了这一分钟不再补**(避免下午计划在晚上对时被补喷)。
### 12.3 触发与排队
每个 500ms 任务 tick
1. RTC 无效或 `plan_en=0`:不入队。
2. 类型 1:每天该时:分触发一次。
3. 类型 2:仅位图置位的星期,该时:分触发一次。
4. 类型 0:该时:分触发一次;该轮 **正常跑完或被手动停** 后把 Byte0 清 0 并写 VM。
5. 去重:用「上次触发的本地日期 + 时 + 分 + 槽位」防 500ms 任务在同一分钟内连打。
6. 多条同一分钟:**排队**,做完一条再做下一条,不重叠喷。
7. **计划到点时若泵已经在跑**(手动 / 上一计划 / 联动):当前轮先跑完,再跑队列,**不插队中止**。
8. 新触发只入队,真正 `app_spray_start` 发生在作业空闲且轮到该条时。
### 12.4 一轮结束之后
- 计划轮正常结束:出队;立刻按第 11.3 补判联动(湿度仍 `< Hmin` 且未作废 → 联动接着循环)。
- 被手动停:作废联动,不再立刻补开;若是单次计划,仍清该条使能。
---
## 13. 仲裁(唯一决策表)
优先级 **高 → 低**
1. **手动**PB1、APP 写 DP1、APP 写 DP7
2. **计划队列**RTC 有效且 `plan_en=1` 且已到点入队 / 正在跑计划轮)
3. **联动**(有效配置且本 tick 满足开 / 关条件)
| 冲突 | 胜者 | 副作用 |
|---|---|---|
| 手动开(含写 DP7)vs 正在跑的任何轮 | 手动 | 中止当前轮,按新来源参数重开;若原是联动占着 → **本轮联动作废**RAM,不写 VM |
| 手动停 vs 正在跑的任何轮 | 手动 | 立刻停;联动作废;单次计划清使能 |
| 计划到点 vs 正在跑 | 当前轮先跑完 | 计划入队,不插队 |
| 计划轮 vs 联动 | 计划 | 计划占着时联动不开下一轮;计划结束收口后立刻按 11.3 重判 |
| 联动 vs 空闲 | 联动 | 开则开一轮(循环),关则停 |
作废标志不存 VM。
**手动作废后的再武装:** 必须先走到第 11.3 的「关」条件(`H > Hmax`),再重新出现 `H < Hmin`,才允许联动再开。作废期间丢广播仍保持上一态。
**冷启动:** 作废标志是 RAM,上电为空。泵保持关。有有效湿度且无计划占用时,允许联动按 11.3 立刻改(过干则开喷)。
`plan_en` 写成 0:立刻停新触发(不拆当前轮)。无联动总开关;无效配置 / DP8 删除即停联动。
---
## 14. 持久化(VM
存(冷启动保留):
- `spray_config`8B,含首次默认 10 / 3 / 3
- `linkage_config`20B
- `spray_plan_en``spray_plan1` + `time_config2~7`(各 15B
- `fault_en``alarm_en`(若有写入)
- `run_record`31 × uint32 秒 + slot[0] 对应的本地日期(对时后才有意义)
不存:
- DP0 / DP2 / DP3 / DP4 / DP5、DP1 作业开关、DP6 当前湿度、联动作废标志、泵 GPIO
- `linkage_delete`(脉冲)
- BLE 三元组(`CFG_USER_PAIR_INFO`,出厂重置 **不清**
上电:加载上述字段,泵保持关,DP1=0,再让联动按第 13 章冷启动规则判(无湿度则不动)。
首次上电(VM 无效):DP7=默认 10s / 3 次 / 3s,联动 20B 全 0,计划全 0,`spray_plan_en=0`,31 天运行记录全 0,DP1=0。
写 VM 时机:
- APP 改联动 / 计划 / 喷淋配置、单次计划自毁:500ms 防抖
- 运行记录:累计泵转满 **10 分钟** 写一次;**跨天旋转时立刻写**
### 14.1 运行记录(DP18 `run_record`RO124B
31 天 × 4 字节,每格 uint32 **大端**,单位秒:
| 偏移 | 含义 |
|---|---|
| Byte0–3 | 今天喷淋器运行总秒数 |
| Byte47 | 昨天 |
| … | … |
| Byte120123 | 前 30 天 |
**什么叫在运行:** `app_pump_get()==1`PC2 与 PA6 **都已导通**)。间隔等待、只开了一只脚、作业态为开但泵已关,**都不计**。手动 / 计划 / 联动只要泵真正在转都算。
**日期轴:** 与加热垫 / 风扇相同 —— 未对时只往 slot[0] 加、不旋转;对时后按本地日历左移;掉电再上电先灌 VM。
**落盘:** 每累计运行满 600 秒写一次 VM。跨天立刻写。拔电接受最多丢不满 10 分钟的秒数。
`BOARD_RUNREC_ENABLE=0`:不累加、DP18 报全 0。
---
## 15. 出厂重置(DP24 写 1
- **清全部业务 VM**(第 14 章列表,含 DP7 默认恢复、联动 20B、7 条计划、31 天运行记录)。
- **保留 BLE 配对,不断开、不软复位。**
- RAM 中泵停、DP1=0,联动 / 计划停止,作废标志清掉;DP7 回到默认 10 / 3 / 3。
- 换机:运行中 **按住配网键 5s 后松手** 清配对进绑定广播(见 5.2)。
---
## 16. BLE / DP 行为摘要
身份:`CBR2` / `PRCBR2`。帧格式见 `jb_protocol.h``MAX_PACKAGE_LEN=512`)。
| DP | 处理 |
|---|---|
| 0 power | 本版写忽略 |
| 1 spray_switch | 写 1 开一轮(已在喷则保持);写 0 立刻停。上报 **作业态**(整轮含间隔为 1 |
| 24 | 本版写忽略 |
| 5 current_temp | 本版写忽略;不上报 |
| 6 current_humi | **WO**。已连接时服务器下发 → 更新联动用湿度。不上报。断连扫描不填 |
| 7 spray_config | 8 字节;合法则存 VM + 立刻开一轮(第 10.2) |
| 8 linkage_delete | 第 11.4 节,脉冲 |
| 9 linkage_config | 20 字节 memcpy + 存 VM + 第 11.5 节立刻重判 |
| 10 spray_plan_en | 见第 12 章 |
| 1117 计划 | 15 字节 + 存 VM |
| 18 run_record | RO,由 `app_runrec` 填 124B,禁止 APP 写 |
| 19 heartbeat | 本版忽略 |
| 20 fault_en | 原样存,P0 不做位级过滤 |
| 21 fault_code | 保持 0 |
| 22 alarm_en | 原样存 |
| 23 alarm_code | 保持 0 |
| 24 factory_reset | 第 15 章 |
| 2527 reserva | 存 RAM,可不实现动作 |
OTA:运行态配网键按住 12s 走 `usr_app_ota_init`(任务上下文,红绿交替)。协议 OTA(`jbOnOtaNotify`)**本版 stub**,禁止假装升级成功后擦用户区;加热垫同款直接 `JB_OTA_STATE_ERR` 也可。
配对超时 120s,失败保持开机,灯回「红灭 + 绿按作业 / 空闲」。
断连已配对:500ms 见宝重连广播 ↔ 500ms 扫描(第 8.2)。绑定广播仍用 `CBR2` / `PRCBR2`,不要改成 `JBao-` 名(那是风扇温湿度窗)。
---
## 17. 建议文件清单(新建,加热垫 / 风扇已有的拷贝改)
```
apps/usr_periph/
board_pin.h # 引脚、时序、BOARD_xxx_ENABLE
bsp_hw.c/.h # LED 双通道 / KEY×2 / 泵双脚 GPIO
usr_rtc.c/.h # 已有则保留
apps/usr_app/
key.c/.h, key_manager.c/.h # 拷风扇;两实例:喷淋短按 + 配网 5s/12s
led.c/.h, led_manager.c/.h # 拷门夹双通道 + 红绿交替;事件表按第 6 章
app_led.c/.h # 绿=作业,红=配对;OTA / 失败占双灯
app_defer.c/.h # 拷加热垫 / 风扇
app_pair.c/.h # 拷加热垫,红灯通道跟配对态
app_pump.c/.h # 双脚成对切,禁止单脚接口
app_spray.c/.h # 第 10 章作业引擎(唯一切泵者)
app_th_scan.c/.h # 500ms 交替扫描 JBao-{tank},取湿度
app_linkage.c/.h # 第 11 章湿度回差
app_runrec.c/.h # 泵实际导通计秒
app_timer_task.c/.h # 第 12 章到点触发 + 排队
app_nvm.c/.h
app_function.c/.h
app_arbiter.c/.h
```
`usr_jb_main.c` 只编排,不写算法。禁止引入 `app_fan` / `app_disp` / `app_th` / `app_mode` / `app_relay` / `app_zc`
---
## 18. 分阶段交付与验收
### P0 — 台上 Bring-up(不依赖 APP
**目标:** 双键、双灯、双脚泵可独立验证。
- [ ] `board_pin.h` + UART TX=PC5,打印正常
- [ ] 插电即开机(不按键,绿灯常亮,泵两脚低;未配对红灯灭;已配对未连接红灯 500ms 慢闪)
- [ ] 单击 PB1:泵两脚同时高,按默认 10s × 3 次、间隔 3s 跑完;作业中绿灯 500ms 闪;结束后绿灯常亮、泵关
- [ ] 作业中再点 PB1:立刻双脚低、绿灯回常亮
- [ ] 示波器 / 逻辑:禁止出现「只高一只脚」
- [ ] 间隔 3s 期间两脚都低,但绿灯仍闪(作业态未结束)
- [ ] PA0 满 5s → 红灯 100ms 闪,绿灯不被关掉(仍常亮或仍闪)
- [ ] 5s 后松手 → 清绑定进配对(红灯继续闪)
- [ ] 满 5s 不松手到 12s → 不进配对,红绿交替进 OTA
- [ ] 长按进配对 / OTA 后不复位(动作走任务上下文)
- [ ] 上电强制 `usr_goto_pair=1` 已删除
- [ ] 灯脚确认:PA8=绿、PA7=红(与加热垫相同)
**P0 关闭:** `BOARD_LINKAGE/TIMER/TH_SCAN/RUNREC/NVM_ENABLE=0`
### P1 — 最小产品闭环
- [ ] 5s 配对,APP 能写 DP1 开关喷淋;DP1 报作业态(间隔期间仍为 1)
- [ ] APP 写合法 DP7:立刻按新参数开喷;PB1 再点能停
- [ ] 非法 DP7(时长 0 或次数 0):不改配置、不开喷
- [ ] 短按同步上报 DP1
- [ ] VM:改 DP7 后断电,重新上电 **不自动喷**,但单击按上次配置喷
- [ ] 首次上电 DP7 上报默认 `00 0A 03 00 03 00 00 00`
- [ ] DP0 / 2 / 3 / 4 / 5 / 19 写忽略;设备不关、泵不跟这些 DP
- [ ] DP6 WO:APP / 服务器能写下湿度;设备全量上报 **不含** DP6 / DP5
- [ ] 计划 7 组 + `spray_plan_en` 能存;`BOARD_TIMER_ENABLE=0` 时不执行
- [ ] `MAX_PACKAGE_LEN=512`,APP 能读完整 124B 运行记录(本阶段记录可仍为 0)
### P2 — 自动化
- [ ] 断连:500ms 重连广播 / 500ms 扫描;已连接停扫描
- [ ] 匹配 `JBao-{缸体号}` 且版本相同才更新湿度;温度不参与开停
- [ ] 联动:`H<Hmin` 循环喷,`H>Hmax` 停,中间保持;无匹配包保持上一态
- [ ] 缸体号 0 / min>max 不跑联动
- [ ] 手动作废后须先 `H>Hmax``H<Hmin` 才再开
- [ ] 计划 RTC 门闩:未对时绝不新触发;对时后仅当分钟补触发,不补过点计划
- [ ] 单次自毁(跑完或手动停都清使能)、每天 / 每周、同一分钟排队
- [ ] 计划到点不中止当前轮;当前轮结束后才跑队列;结束后立刻把联动判一次
- [ ] DP8 脉冲删除;DP9 重写立刻重判
- [ ] 运行记录:泵实际导通 10 分钟落盘;对时后跨天左移;间隔秒不计入
- [ ] 出厂重置清业务留配对,DP7 回默认
- [ ] 仲裁表第 13 章逐条用例(见下)
### P2 仲裁最小用例(必须写进测试笔记)
1. 空闲 `H<Hmin` → 开始循环喷;PB1 停 → 保持停,即使仍低于 Hmin。
2. 用例 1 之后先到 `H>Hmax`,再回到 `H<Hmin`,才再次开。
3. 计划到点 → 用该条参数开一轮;跑完结束;若此时 `H<Hmin` 且未作废 → **联动再开**
4. 计划轮未结束时再 PB1 停 → 保持停;单次计划使能被清;即使仍 `H<Hmin` → **不要**被联动立刻打开。
5. 手动一轮进行中,另一条计划到点 → 手动轮跑完后再跑计划,不插队中止。
6. 未对时,计划配置使能也不触发。对时瞬间若刚好在开始时:分则补触发;过了该分钟不补。
7. 上电 VM 有有效 DP7、暂无湿度 → **不喷**;湿度到达后再按 11.3。
8. `H` 在 [Hmin, Hmax] → 保持上一态。
9. 断连扫描丢包 → 不停喷(保持上一态)。
10. 已连接写 DP6 使 `H<Hmin` → 开;断开后扫描到同一缸体更高湿度 `H>Hmax` → 停。
11. APP 写合法 DP7 → 中止当前轮并按新参数重开,联动作废。
12. 间隔等待期间 DP1 仍为 1,绿灯仍闪,运行记录 **不加** 间隔秒。
---
## 19. `board_pin.h` 时序宏(实现时抄这份数)
```c
#define BOARD_UART_TX_PIN IO_PORTC_05
#define BOARD_KEY_SPRAY_PIN IO_PORTB_01 /* 喷淋键,只短按 */
#define BOARD_KEY_PAIR_PIN IO_PORTA_00 /* 配网键,5s / 12s */
#define BOARD_LED_RED_PIN IO_PORTA_07 /* 红灯,低亮;与加热垫相同 */
#define BOARD_LED_GREEN_PIN IO_PORTA_08 /* 绿灯,低亮;与加热垫相同 */
#define BOARD_PUMP_PWR_PIN IO_PORTC_02 /* 泵 5V 供电,高导通 */
#define BOARD_PUMP_SW_PIN IO_PORTA_06 /* 泵开关,高导通 */
#define BOARD_LED_ON_LEVEL 0
#define BOARD_PUMP_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_RECONN_ON_MS 500 /* 已配对未连接:红灯慢闪 */
#define BOARD_LED_RECONN_OFF_MS 500
#define BOARD_LED_SPRAY_ON_MS 500 /* 作业绿灯闪 */
#define BOARD_LED_SPRAY_OFF_MS 500
#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_ADV_SCAN_SLOT_MS 500 /* 重连广播 / 扫描各占一拍 */
#define BOARD_SPRAY_DEFAULT_DUR_S 10
#define BOARD_SPRAY_DEFAULT_COUNT 3
#define BOARD_SPRAY_DEFAULT_INTERVAL_S 3
#define BOARD_RUN_RECORD_DAYS 31
#define BOARD_RUN_RECORD_SAVE_SEC 600
```
---
## 20. 风险与不要擅自发挥的点
| 风险 | 要求 |
|---|---|
| 只开 PC2 或只开 PA6 | 禁止。泵接口必须成对 |
| 上电按 VM 自动喷 | 禁止。水会喷出来 |
| 灯脚写反(PA8 红 / PA7 绿) | 禁止。本机与加热垫相同:PA8 绿 / PA7 红 |
| 抄加热垫互斥灯(开红关绿) | 禁止。红绿独立,空闲不要关红灯 |
| 抄加热垫时间窗定时 | 禁止。本机是到点触发一轮 |
| 抄加热垫过零 / 继电器 | 禁止 |
| 湿度当温度、或温度参与开停 | 本版禁止 |
| 丢广播就停喷 | 禁止(已拍板保持上一态) |
| 计划过点后对时补喷 | 禁止;只补「当前这一分钟」 |
| 计划到点插队中止手动轮 | 禁止;排队 |
| 生成协议再导回 LEN=128 | 禁止;必须 512 |
| `app_main` 上电清配对进绑定 | 必须删 |
| 20ms ISR 写 VM / 切 BLE | 禁止(风扇已踩过栈溢出) |
| 一排裸 static / 排障史注释 | 禁止。≥2 个状态收进一个 `s_xxx`。详见第 0.1 节 |
| 协议 OTA stub 返回 OK 当已升级 | 注释标明未升级,或直接 `JB_OTA_STATE_ERR` |
| 扫描时改 GATT 名 | 不要改 0x2A00;只解析对方 adv |
| 把 DP1 理解成「某一只脚的电平」 | DP1 是作业态,间隔期间仍为开 |
| 把 DP3 / DP4 当喷淋参数 | 禁止;参数只走 DP7 和计划自带字段 |
| 喷淋键上做 5s / 12s | 禁止;配网只在 PA0 |
---
## 21. 给 WorkBuddy 的推荐开工顺序
1.`MAX_PACKAGE_LEN=512``jb_product.c` 只保留 DP 赋值,业务迁 `app_*`。删 `app_main` 上电强制配对。
2. `board_pin.h` + `bsp_hw`(双灯双键双脚泵);UART TX=PC5。
3. 从加热垫拷 `usr_jb_main` 框架、defer、pair、nvm 骨架;key 改两实例;LED 拷门夹双通道,事件表按第 6 章重写。
4. `app_pump` + `app_spray`:P0 验收单击默认 10/3/3、中途停、灯独立。
5. P1DP1 / DP7、VM 不恢复开喷、忽略 DP、计划只存。
6. P2`app_th_scan``app_linkage`、到点计划 + 排队、`app_runrec`、仲裁用例。
每一步都保持对应 `BOARD_xxx_ENABLE` 可关。提交前对照第 0.1 节:一个模块一个 `s_xxx`,注释写为什么,不要排障日记。
---
**文档结束。** 行为争议以本文第 5~14 章为准;字节布局以第 10.2 / 11.2 / 12.1 / 14.1 节为准。