PeriphExec Business Flow
PeriphExec Business Flow
Synchronized Device Documentation
This page corresponds to the synchronized Chinese source. Commands, JSON examples, API paths, field names, and screenshots are kept aligned with the Chinese device-side source documentation.
What This Page Covers
- PeriphExec Business Flow context and expected reader workflow.
- Configuration, verification, and release-readiness details.
- Source-aligned implementation notes, screenshots, and troubleshooting references.
Source Reference
The detailed operational source is preserved below so implementation details stay exact while the English navigation, titles, and reading path remain available.
外设执行业务流程
当前版本提示
- 精简版默认启用 PeriphExec,但关闭 RuleScript;脚本相关流程只适用于完整版或定制构建。
- 传感器采集动作会把最近一次读数写入本地缓存,并产生
sensor_cache数据事件;事件触发可使用ds:<外设ID>_<字段>作为数据源,例如ds:dht_01_temperature。 /api/periph-exec会返回sensorSources,前端“本地传感器”列表只展示已经由外设执行规则配置过的传感器采集来源。- 默认配置不再内置专用蜂鸣器外设或蜂鸣器预设动作;
actionType=20仅作为历史保留位。
前端的外设执行页面展示规则启用状态、触发器数量、动作数量和手动执行入口;后端业务流程与该页面的增删改查、启用禁用和执行一次操作对应。
链路图用于快速建立阅读顺序:先看触发源如何命中规则,再看动作队列如何执行,最后看执行结果如何反馈到日志、状态和协议上报。
生命周期图用于理解规则从禁用草稿、配置校验、手动执行、启用运行到异常回滚的状态变化;后文的 CRUD、调度和 WorkerPool 细节都可以映射到这条链路。
内部结构图用于把本文后续章节串起来:Manager 负责规则和配置,Scheduler 负责触发命中,Executor 负责动作分发,WorkerPool 承接慢动作和脚本任务。
目录
- 1. 模块概述
- 2. 数据模型
- 3. 规则生命周期 (CRUD)
- 4. 触发器类型详解
- 5. 动作类型详解
- 6. 触发-动作执行流程
- 7. 核心方法分析
- 8. 异步执行引擎
- 9. 数据转换管道与上报控制
- 10. 按钮事件子系统
- 11. 配置持久化与版本迁移
- 12. API 路由层
- 13. 已知问题与优化建议
1. 模块概述
PeriphExec (Peripheral Execution) 是 FastBee-Arduino 物联网设备的规则引擎模块,实现"当条件满足时执行动作"的自动化逻辑。
核心架构
┌─────────────────────────────────────────────────────────────┐
│ 触发源 (Trigger Sources) │
│ │
│ MQTT消息 │ 定时器 │ 系统事件 │ 轮询数据 │ 按钮事件 │
└─────┬──────┴────┬─────┴─────┬──────┴─────┬──────┴─────┬─────┘
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ PeriphExecManager (单例) │
│ │
│ ┌──────────┐ ┌──────────────┐ ┌────────────────────┐ │
│ │ 规则存储 │ │ 条件评估引擎 │ │ 动作执行/调度引擎 │ │
│ │ map<id, │ │ evaluateCond │ │ sync / async │ │
│ │ rule> │ │ ition() │ │ dispatch │ │
│ └──────────┘ └──────────────┘ └────────────────────┘ │
│ │
│ ┌──────────────────┐ ┌──────────────────────────────┐ │
│ │ 持久化 (LittleFS) │ │ 数据上报 (MQTT/TCP/HTTP/CoAP)│ │
│ └──────────────────┘ └──────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘关键设计约束
| 约束项 | 值 | 说明 |
|---|---|---|
| 每规则最大触发器数 | 3 | MAX_TRIGGERS_PER_RULE |
| 每规则最大动作数 | 4 | MAX_ACTIONS_PER_RULE |
| 最大并发异步任务 | 3 | MAX_ASYNC_TASKS |
| 异步任务最小堆内存 | 30000 bytes | MIN_HEAP_FOR_ASYNC |
| 脚本任务栈大小 | 8192 bytes | SCRIPT_TASK_STACK |
| 普通异步任务栈大小 | 4096 bytes | SIMPLE_TASK_STACK |
| 异步任务优先级 | 0 (最低) | ASYNC_TASK_PRIORITY |
| 同一规则触发器关系 | OR (任一匹配即触发) | — |
| 同一规则动作关系 | 顺序执行 (按数组顺序) | — |
2. 数据模型
规则数据模型可以作为阅读本章的总索引:PeriphExecRule 是持久化和页面展示的核心对象,触发器决定数据来源,动作决定执行目标,运行态负责互斥、队列、结果和日志。
2.1 枚举定义
ExecTriggerType - 触发器类型
| 值 | 名称 | 说明 |
|---|---|---|
| 0 | PLATFORM_TRIGGER | 平台数据触发 - MQTT 下发数据满足条件时触发 |
| 1 | TIMER_TRIGGER | 定时触发 - 按时间间隔或每日定点触发 |
| 4 | EVENT_TRIGGER | 事件触发 - 系统事件 (WiFi/MQTT/按钮等) 触发 |
| 5 | POLL_TRIGGER | 轮询触发 - 外设轮询数据满足条件时触发 |
ExecActionType - 动作类型
动作枚举定义于 include/core/PeripheralExecution.h。注意枚举值 11、12 已弃用保留。
| 值 | 枚举名称 | 分类 | actionValue 含义 |
|---|---|---|---|
| 0 | ACTION_HIGH | GPIO | 忽略,设置目标引脚高电平 |
| 1 | ACTION_LOW | GPIO | 忽略,设置目标引脚低电平 |
| 2 | ACTION_BLINK | GPIO | 闪烁间隔毫秒(默认 500) |
| 3 | ACTION_BREATHE | GPIO | 呼吸灯周期毫秒(默认 2000) |
| 4 | ACTION_SET_PWM | GPIO | PWM 占空比 0-255 |
| 5 | ACTION_SET_DAC | GPIO | DAC 输出值 0-255 |
| 6 | ACTION_SYS_RESTART | 系统 | 忽略,延时 500ms 后重启设备 |
| 7 | ACTION_SYS_FACTORY_RESET | 系统 | 忽略,格式化 LittleFS 后重启 |
| 8 | ACTION_SYS_NTP_SYNC | 系统 | 忽略,触发 NTP 时间同步 |
| 9 | ACTION_SYS_OTA | 系统 | OTA 固件更新 URL(空则使用已配置的默认 URL) |
| 10 | ACTION_CALL_PERIPHERAL | 联动 | 调用其他外设,actionValue 为可选参数 |
| 13 | ACTION_HIGH_INVERTED | GPIO | 忽略,逐位反转后输出高(逻辑高 = 物理低) |
| 14 | ACTION_LOW_INVERTED | GPIO | 忽略,逐位反转后输出低(逻辑低 = 物理高) |
| 15 | ACTION_SCRIPT | 脚本 | 命令序列脚本文本,多行命令逐行执行 |
| 16 | ACTION_MODBUS_COIL_WRITE | Modbus | 线圈写入(FC05),格式 slave,addr,value |
| 17 | ACTION_MODBUS_REG_WRITE | Modbus | 寄存器写入(FC06),格式 slave,addr,value |
| 18 | ACTION_MODBUS_POLL | Modbus | 轮询子设备采集 + 可选控制指令,JSON 或逗号列表 |
| 19 | ACTION_SENSOR_READ | 采集 | JSON,采集传感器数据并上报 |
| 20 | ACTION_RESERVED_20 | 保留 | 旧版蜂鸣器预设动作占位 |
| 21 | ACTION_TRIGGER_EVENT | 事件 | 触发设备事件,targetPeriphId 为事件 ID,actionValue 为额外数据 |
| 22 | ACTION_ENABLE_EXEC_RULE | 规则 | 启用指定外设执行规则,targetPeriphId 为规则 ID |
| 23 | ACTION_DISABLE_EXEC_RULE | 规则 | 禁用指定外设执行规则,targetPeriphId 为规则 ID |
| 24 | ACTION_DISPLAY_NUMBER | 显示屏 | 数码管/显示屏显示数字,如 12.34 / 12:34 / 1234,支持 ${id.field} 模板 |
| 25 | ACTION_DISPLAY_TEXT | 显示屏 | 数码管/显示屏显示文本,如 PLAY、ON |
| 26 | ACTION_DISPLAY_CLEAR | 显示屏 | 数码管/显示屏清屏 |
| 27 | ACTION_OLED_DISPLAY | 显示屏 | OLED 自定义多行显示,支持 ${id.field} 和 $value 模板,首行 # 开头为居中标题 |
ExecOperator - 条件运算符
| 值 | 名称 | 说明 | 数据类型 |
|---|---|---|---|
| 0 | OP_EQ | 等于 | 数值/字符串 |
| 1 | OP_NEQ | 不等于 | 数值/字符串 |
| 2 | OP_GT | 大于 | 数值 |
| 3 | OP_LT | 小于 | 数值 |
| 4 | OP_GTE | 大于等于 | 数值 |
| 5 | OP_LTE | 小于等于 | 数值 |
| 6 | OP_BETWEEN | 区间内 | 数值 (compareValue="min,max") |
| 7 | OP_NOT_BETWEEN | 区间外 | 数值 (compareValue="min,max") |
| 8 | OP_CONTAIN | 包含子串 | 字符串 |
| 9 | OP_NOT_CONTAIN | 不包含子串 | 字符串 |
2.2 核心数据结构
ExecTrigger - 触发器
struct ExecTrigger {
int triggerType; // ExecTriggerType 枚举值
String triggerPeriphId; // 关联外设ID (PLATFORM/POLL触发) 或空
int operatorType; // ExecOperator 枚举值
String compareValue; // 比较值 (数值字符串/区间/子串)
int timerMode; // 0=间隔模式, 1=每日定点模式
int intervalSec; // 间隔秒数 (timerMode=0)
String timePoint; // 定点时间 "HH:MM" (timerMode=1)
String eventId; // 事件ID (EVENT_TRIGGER 专用)
int pollResponseTimeout; // 轮询响应超时 ms (POLL触发)
int pollMaxRetries; // 轮询最大重试次数
int pollInterPollDelay; // 轮询间隔延迟 ms
unsigned long lastTriggerTime; // 运行时: 上次触发时间戳
int triggerCount; // 运行时: 累计触发次数
};ExecAction - 动作
struct ExecAction {
String targetPeriphId; // 目标外设ID
int actionType; // ExecActionType 枚举值
String actionValue; // 动作参数值
bool useReceivedValue; // true=使用触发时接收到的值替代actionValue
int syncDelayMs; // 执行后延迟 ms (用于动作间间隔, 最大10000)
};PeriphExecRule - 规则
struct PeriphExecRule {
String id; // 唯一ID "exec_<millis>"
String name; // 规则名称
bool enabled; // 是否启用
int execMode; // 0=异步优先, 1=强制同步
std::vector<ExecTrigger> triggers; // 触发器列表 (OR关系, 最多3个)
std::vector<ExecAction> actions; // 动作列表 (顺序执行, 最多4个)
int protocolType; // 数据转换协议类型
String scriptContent; // 脚本内容
bool reportAfterExec; // 执行后是否上报设备数据
};2.3 事件系统
系统预定义了 37 个静态事件 (STATIC_EVENTS[]),分为以下类别:
| 分类 | 事件ID范围 | 示例事件 |
|---|---|---|
| WiFi | 1-3 | CONNECTED, DISCONNECTED, RECONNECTING |
| MQTT | 10-13 | CONNECTED, DISCONNECTED, RECONNECTING, MESSAGE_RECEIVED |
| Network | 20-22 | IP_OBTAINED, DNS_RESOLVED, SIGNAL_CHANGED |
| Protocol | 30-34 | MODBUS_OK/ERROR/TIMEOUT, TCP_CONNECTED/DISCONNECTED |
| System | 40-43, 70-73 | BOOT_COMPLETE, LOW_MEMORY, WATCHDOG_TRIGGERED, TIME_SYNCED, HEAP_LOW/CRITICAL, TASK_WATCHDOG, STACK_OVERFLOW |
| Provision | 50-53 | STARTED, COMPLETED, FAILED, RESET |
| Rule | 60-61 | RULE_TRIGGERED, RULE_ERROR |
| Button | 80-86 | SINGLE_CLICK, DOUBLE_CLICK, LONG_PRESS_{2,5,10}S, RELEASED, STATE_CHANGED |
| PeriphExec | 90 | EXEC_COMPLETED |
| Data | 100-101 | DATA_REPORTED, DATA_REPORT_FAILED |
3. 规则生命周期 (CRUD)
3.1 创建规则 (addRule)
请求 → 参数验证 → ID生成 → 唯一性检查 → 限制检查 → 运行时字段重置 → 入库 → 持久化详细流程:
- 互斥锁保护:获取
rulesMutex确保线程安全 - ID 生成:若未提供 ID,自动生成
"exec_" + String(millis()) - 唯一性检查:在
rulesmap 中查找是否已存在该 ID - 触发器数量限制:最多
MAX_TRIGGERS_PER_RULE(3) 个,超出则截断 - 动作数量限制:最多
MAX_ACTIONS_PER_RULE(4) 个,超出则截断 - 运行时字段重置:每个触发器的
lastTriggerTime = 0,triggerCount = 0 - 写入 map:
rules[rule.id] = rule - 持久化:API 层调用
saveConfiguration()写入 LittleFS
3.2 更新规则 (updateRule)
请求 → ID验证 → 存在性检查 → 字段合并 → 运行时状态保留 → 替换 → 持久化关键设计 - 运行时状态保留:
更新规则时,按索引匹配保留每个触发器的 lastTriggerTime 和 triggerCount:
for (size_t i = 0; i < rule.triggers.size() && i < existingTriggers.size(); i++) {
rule.triggers[i].lastTriggerTime = existingTriggers[i].lastTriggerTime;
rule.triggers[i].triggerCount = existingTriggers[i].triggerCount;
}这确保了更新规则配置后,定时器不会立即重新触发,触发计数也不会丢失。
3.3 删除规则 (removeRule)
请求 → ID验证 → 存在性检查 → 从map移除 → 持久化3.4 启用/禁用规则 (enableRule / disableRule)
直接修改 rule.enabled 标志位,随后持久化。禁用的规则不会参与任何触发匹配。
3.5 手动执行 (runOnce)
请求 → ID验证 → 获取规则 → 跳过触发匹配 → 直接执行所有动作 → 触发设备状态上报手动执行不检查规则是否启用,也不检查触发条件,直接执行动作列表。API 层额外触发 MQTT publishDeviceInfo() 进行状态上报。
4. 触发器类型详解
4.1 PLATFORM_TRIGGER (平台数据触发, type=0)
触发源: MQTT 下行消息 (平台下发指令)
匹配流程:
MQTT消息到达 → handleMqttMessage(periphId, value)
│
├─ Phase 1 (持锁): 遍历所有 enabled 规则
│ ├─ 遍历规则的每个触发器
│ │ ├─ 触发器类型 == PLATFORM_TRIGGER?
│ │ ├─ triggerPeriphId 匹配 periphId? (空ID则匹配任意)
│ │ ├─ evaluateCondition(value, operatorType, compareValue)?
│ │ └─ 防抖检查: 距上次触发 > 1000ms?
│ │ → 匹配成功: 更新 lastTriggerTime, triggerCount++
│ └─ 收集到匹配列表 matchedRules[]
│
└─ Phase 2 (释锁): 遍历 matchedRules[]
└─ dispatchAsync(rule, receivedValue) 或 executeAllActions(rule, value)关键细节:
triggerPeriphId为空时匹配任意外设的消息- 同一规则多个 PLATFORM_TRIGGER 是 OR 关系,任一匹配即触发
- 1 秒防抖:同一触发器在 1 秒内不会重复触发
4.2 TIMER_TRIGGER (定时触发, type=1)
触发源: checkTimers() 函数,由主循环每秒调用
两种定时模式:
间隔模式 (timerMode=0)
checkTimers() 每秒执行
│
├─ 规则已启用 && 触发器类型 == TIMER_TRIGGER && timerMode == 0?
├─ lastTriggerTime == 0? → 立即触发 (首次运行)
├─ (当前时间 - lastTriggerTime) >= intervalSec * 1000?
│ → 匹配成功: lastTriggerTime = now, triggerCount++
└─ 收集后 Phase 2 执行动作- 首次立即触发:
lastTriggerTime == 0时不等待间隔,立即执行 - 最小间隔由
intervalSec控制(默认 60 秒)
每日定点模式 (timerMode=1)
checkTimers() 每秒执行
│
├─ 规则已启用 && 触发器类型 == TIMER_TRIGGER && timerMode == 1?
├─ 解析 timePoint "HH:MM" → 目标 hour, minute
├─ 当前 hour:minute == 目标?
│ ├─ 距上次触发 > 60秒? (60秒防重复窗口)
│ │ → 匹配成功: lastTriggerTime = now, triggerCount++
│ └─ 否则跳过 (已在本分钟内触发)
└─ 收集后 Phase 2 执行动作- 60 秒防重复:每日定点模式使用 60 秒窗口防止在同一分钟内重复触发
- 需要 NTP 时间同步才能正常工作
4.3 EVENT_TRIGGER (事件触发, type=4)
触发源: 系统内部事件(WiFi/MQTT/按钮/协议等)
事件触发链路:
系统事件发生 (如 WiFi 连接)
│
├─ triggerEvent(EventType type)
│ ├─ 查找 STATIC_EVENTS[] 中匹配的 eventId
│ └─ 调用 triggerEventById(eventId)
│
├─ triggerEventById(eventId)
│ ├─ Phase 1 (持锁): 遍历所有 enabled 规则
│ │ ├─ 遍历触发器: type == EVENT_TRIGGER && eventId 匹配?
│ │ ├─ 防抖: 距上次触发 > 1000ms?
│ │ └─ 匹配则加入 matchedRules[]
│ │
│ └─ Phase 2 (释锁): 执行匹配规则的动作
│
└─ triggerPeriphExecEvent(eventId) // 为外设执行规则触发的事件
└─ 同上,用于规则完成/错误时的链式触发特殊事件处理:
- 按钮事件使用专门的
triggerButtonEvent(),防抖窗口缩短为 100ms EVENT_PERIPH_EXEC_COMPLETED(ID=90) 在异步任务完成时自动触发,支持规则链式执行
4.4 POLL_TRIGGER (轮询触发, type=5)
触发源: 外设轮询数据回调
匹配流程:
外设轮询完成 → handlePollData(periphId, value)
│
├─ Phase 1 (持锁): 遍历所有 enabled 规则
│ ├─ 遍历触发器: type == POLL_TRIGGER?
│ ├─ triggerPeriphId 匹配? (空ID匹配任意)
│ ├─ evaluateCondition(value, operatorType, compareValue)?
│ ├─ 防抖: 距上次触发 > 1000ms?
│ └─ 匹配则更新计数并加入列表
│
└─ Phase 2 (释锁): 执行匹配规则的动作轮询触发特有参数:
pollResponseTimeout:轮询响应超时 (默认 1000ms)pollMaxRetries:最大重试次数 (默认 2)pollInterPollDelay:轮询间隔延迟 (默认 100ms)
定时轮询启动:
checkTimers() 也会检查 POLL_TRIGGER 类型的触发器,按 intervalSec 间隔主动发起轮询请求,轮询完成后通过回调进入 handlePollData() 流程。
5. 动作类型详解
动作实际分发逻辑位于 PeriphExecExecutor::executeAllActions(),按 ExecActionType 枚举值路由到对应的内部处理函数。以下按功能分组说明。
5.1 GPIO 直接控制(0-5、13、14)
通过 PeripheralManager 操作硬件引脚(含反转模式),记录结果用于后续上报。
| 动作 | 实现方式 | actionValue |
|---|---|---|
ACTION_HIGH (0) | pm.setPeripheralState(id, true) | 忽略 |
ACTION_LOW (1) | pm.setPeripheralState(id, false) | 忽略 |
ACTION_BLINK (2) | pm.blinkPeripheral(id, intervalMs) | 闪烁间隔毫秒(默认 500) |
ACTION_BREATHE (3) | pm.breathePeripheral(id, periodMs) | 呼吸周期毫秒(默认 2000) |
ACTION_SET_PWM (4) | pm.setPWMValue(id, duty) | 占空比 0-255 |
ACTION_SET_DAC (5) | pm.setDACValue(id, value) | DAC 值 0-255(GPIO25/26) |
ACTION_HIGH_INVERTED (13) | 反转后 setPeripheralState(true) | 逻辑高对应物理低,用于低电平有效的继电器等负载 |
ACTION_LOW_INVERTED (14) | 反转后 setPeripheralState(false) | 逻辑低对应物理高 |
5.2 系统动作(6-9)
通过 executeSystemAction() 执行,影响整机状态:
| 动作 | 实现 | 异步策略 |
|---|---|---|
ACTION_SYS_RESTART (6) | delay(500); ESP.restart() | 强制同步(异步任务无法在重启后完成) |
ACTION_SYS_FACTORY_RESET (7) | 格式化 LittleFS + 重启 | 强制同步 |
ACTION_SYS_NTP_SYNC (8) | configTime(gmtOffset, daylightOffset, ntpServer) 重新同步 | 可异步 |
ACTION_SYS_OTA (9) | 触发 OTAManager 启动固件升级,actionValue 可选作为 URL | 可异步 |
重要: 在 dispatchAsync() 中,ACTION_SYS_RESTART 与 ACTION_SYS_FACTORY_RESET 会被自动降级为同步执行。
说明: 枚举值 11、12 原预留给 ACTION_AP_MODE 和 ACTION_BLE_TOGGLE,项目统一采用 AP+STA 双模自动切换后移除,这两个值现保留不可用。
5.3 外设联动(10)
ACTION_CALL_PERIPHERAL (10)
调用其他外设的上层行为方法,targetPeriphId 为被调外设 ID,actionValue 为可选参数字符串或 JSON。该动作不直接操作 GPIO,而是触发外设驱动内部定义的行为(如触发 LCD 重绘、控制步进电机等)。
ULN2003 步进电机(STEPPER_MOTOR)支持以下 JSON 命令:
| actionValue | 行为 |
|---|---|
{"periphId":"stepper","action":"forward"} | 正转 |
{"periphId":"stepper","action":"reverse"} | 反转 |
{"periphId":"stepper","action":"stop"} | 停止并释放线圈 |
{"periphId":"stepper","action":"faster","value":"2"} | 每次增加 2 RPM |
{"periphId":"stepper","action":"slower","value":"2"} | 每次降低 2 RPM |
{"periphId":"stepper","action":"setSpeed","value":"12"} | 设置为 12 RPM |
5.4 脚本动作(15)
ACTION_SCRIPT (15)
通过 executeScriptAction() 执行多行命令脚本。actionValue 为脚本文本(而非 scriptContent),脚本语法详见 script-guide.md 命令脚本部分:
GPIO 5 HIGH
DELAY 500
PERIPH relay_1 HIGH
MQTT 0 [{"id":"alarm","value":"1"}]脚本在异步任务中执行(SCRIPT_TASK_STACK=8192),支持 PERIPH/GPIO/DELAY/PWM/DAC/LOG/MQTT 命令及 RANDOM()/RANDOMF() 表达式。
5.5 Modbus 通信动作(16-18)
ACTION_MODBUS_COIL_WRITE (16)
写单个 Modbus 线圈(功能码 FC05),用于布尔类型输出。
actionValue 格式:slave,addr,value
slave: 从机地址 1-247
addr: 线圈地址 0-65535
value: 0 或 1ACTION_MODBUS_REG_WRITE (17)
写单个保持寄存器(功能码 FC06),用于数值类型输出。
actionValue 格式:slave,addr,value
slave: 从机地址
addr: 寄存器地址
value: 0-65535ACTION_MODBUS_POLL (18)
由 executeModbusPollAction() 执行,支持批量轮询和可选控制指令,两种数据格式:
JSON 格式(推荐):
{
"poll": [0, 1],
"ctrl": [
{"type":"relay","idx":0,"val":1},
{"type":"pwm","idx":0,"val":128},
{"type":"pid","idx":0,"sp":25.0}
]
}旧版逗号分隔格式(只轮询,无控制):
0,1,2轮询参数 pollResponseTimeout、pollMaxRetries、pollInterPollDelay 在触发器结构中配置。
5.6 传感器采集(19)
ACTION_SENSOR_READ (19)
由 executeSensorReadAction() 执行,actionValue 为 JSON 配置字符串。
模拟/数字传感器:
{
"periphId": "sensor_01",
"sensorCategory": "analog",
"scaleFactor": 1.0,
"offset": 0.0,
"decimalPlaces": 2,
"sensorLabel": "光照",
"unit": "lux"
}DHT11/DHT22:
{
"periphId": "dht_01",
"sensorCategory": "dht11",
"dataField": "temperature",
"scaleFactor": 1.0,
"offset": 0.0,
"decimalPlaces": 1,
"sensorLabel": "温度",
"unit": "°C"
}DS18B20:
{
"periphId": "ds18b20_01",
"sensorCategory": "ds18b20",
"deviceIndex": 0,
"scaleFactor": 1.0,
"offset": 0.0,
"decimalPlaces": 2,
"sensorLabel": "水温",
"unit": "°C"
}支持的 sensorCategory:
| 值 | 传感器类型 | 读取方式 | 特点 |
|---|---|---|---|
analog | 模拟输入 (ADC) | analogRead() | 0-4095 原始值 |
digital | 数字输入 (GPIO) | digitalRead() | 0/1 |
pulse | 脉冲/频率 | 预留 | 未实现 |
dht11 | DHT11 温湿度 | 单总线时序 | 温度 0-50°C, 湿度 20-90% |
dht22 | DHT22/AM2302 | 单总线时序 | 温度 -40~80°C, 湿度 0-100% |
ds18b20 | DS18B20 数字温度 | OneWire 协议 | -55~125°C, 12 位精度 |
ultrasonic | HC-SR04 超声波 | Trig+Echo | 输出 distance,单位 cm |
current | 电流型传感器 | ADC+线性校准 | 输出 current,支持 ACS712 等 |
voltage | 电压型传感器 | ADC+分压比还原 | 输出 voltage |
SHT31 | SHT31 温湿度 | I2C 轻量驱动 | 输出 temperature、humidity |
AHT20 | AHT20 温湿度 | I2C 轻量驱动 | 输出 temperature、humidity |
BH1750 | BH1750 光照 | I2C 轻量驱动 | 输出 illuminance,单位 lx |
BMP280 | BMP280 气压 | I2C 高级驱动 | 输出 temperature、pressure、altitude,建议 S3-full |
MPU6050 | MPU6050 姿态 | I2C 高级驱动 | 输出加速度/温度/角速度,建议 S3-full |
非阻塞保障: DHT11 读取约 25ms、DS18B20 转换约 750ms,由于始终在独立异步任务中执行,不阻塞主循环;驱动内置缓存(DHT 2s / DS18B20 1s)避免过频读取。
5.7 外设动作(20)
ACTION_RESERVED_20 (20)
历史保留位。旧版专用蜂鸣器预设动作已移除,新配置不应再使用 actionType=20。
5.8 事件与规则控制(21-23)
ACTION_TRIGGER_EVENT (21)
主动触发设备事件,可用于:
- 触发系统内置事件(
targetPeriphId为事件 ID,如sys_breakdown、sys_alarm、low_power) - 触发
DEVICE_EVENT外设定义的自定义事件
actionValue 作为事件额外数据随事件传递,可被其他 EVENT_TRIGGER 规则匹配或经 MQTT 上报平台。
ACTION_ENABLE_EXEC_RULE (22) / ACTION_DISABLE_EXEC_RULE (23)
在运行时启用或禁用指定的外设执行规则,targetPeriphId 为目标规则的 ID(形如 exec_xxxxx)。可实现"一条规则临时关闭另一条规则"的编排,例如:
- 进入维护模式时禁用全部定时规则
- 白天启用采集规则,夜间禁用
- 连锁保护:触发告警规则后自动禁用会扰动现场的动作规则
5.9 显示屏动作(24-27)
显示屏动作统一由显示控制器(TM1637 七段数码管 / OLED)驱动消化。所有动作均支持 ${periphId.field} 占位符读取传感器缓存,部分支持 $value 表示触发接收到的值。
ACTION_DISPLAY_NUMBER (24)
在数码管或 OLED 上显示数字,actionValue 格式示例:
12.34 # 显示 12.34
12:34 # 显示 12:34(冒号点亮)
1234 # 显示 1234
${dht_01.temperature} # 显示 DHT11 实时温度
$value # 显示触发接收值(配合 useReceivedValue)ACTION_DISPLAY_TEXT (25)
在数码管/OLED 上显示短文本(数码管最多 4 字符),受段码表限制仅部分字母可识别(A/b/C/d/E/F/H/L/o/P/U 等)。
ON # 打开
OFF # 关闭
PLAY # 播放
${dht_01.temperature.unit} # 显示温度单位ACTION_DISPLAY_CLEAR (26)
清空显示内容,熄灭所有段。无需 actionValue。
ACTION_OLED_DISPLAY (27)
OLED 专用多行自定义显示,通过 LCDManager::showCustomText() 渲染:
- 内容按
\n切分为多行 - 首行以
#开头时识别为居中标题并绘制分隔线 - 其余行默认左对齐
- 支持
${periphId.field}与$value模板 - 超过最大行数后自动截断
actionValue 示例:
## 环境监测
温度: ${dht_01.temperature}°C
湿度: ${dht_01.humidity}%
水温: ${ds18b20_01.temperature}°C
状态: 正常6. 触发-动作执行流程
6.1 两阶段锁模式 (Two-Phase Lock Pattern)
这是 PeriphExec 最核心的设计模式,所有触发入口 (handleMqttMessage, handlePollData, triggerEvent, triggerButtonEvent) 都采用此模式:
两阶段锁的关键是“持锁只做匹配和快照,释锁后再执行动作”。排查并发问题时,要先区分规则集合锁和外设资源锁,避免把耗时外设操作放进规则锁保护范围。
Phase 1 - 持锁阶段 (规则匹配)
┌──────────────────────────────────────┐
│ RecursiveMutexGuard lock(mutex) │
│ │
│ for (rule : rules) { │
│ if (!rule.enabled) continue; │
│ for (trigger : rule.triggers) { │
│ if (matchCondition()) { │
│ matchedRules.push_back(rule); │
│ break; // OR关系,一个匹配即可 │
│ } │
│ } │
│ } │
│ │
│ // 自动释放锁 │
└──────────────────────────────────────┘
│
▼
Phase 2 - 释锁阶段 (动作执行)
┌──────────────────────────────────────┐
│ // 此时无锁,动作执行可能耗时较长 │
│ │
│ for (rule : matchedRules) { │
│ dispatchAsync(rule, value); │
│ } │
└──────────────────────────────────────┘设计目的: 持锁期间只做轻量的条件匹配,动作执行在释锁后进行,避免长时间持锁导致其他线程阻塞。
6.2 条件评估 (evaluateCondition)
bool evaluateCondition(String& receivedValue, int operatorType, String& compareValue)评估逻辑:
receivedValue 和 compareValue
│
├─ CONTAIN (8): receivedValue.indexOf(compareValue) >= 0
├─ NOT_CONTAIN (9): receivedValue.indexOf(compareValue) < 0
│
├─ 转换为 float: recvFloat, compFloat
│
├─ EQ (0): recvFloat == compFloat
├─ NEQ (1): recvFloat != compFloat
├─ GT (2): recvFloat > compFloat
├─ LT (3): recvFloat < compFloat
├─ GTE (4): recvFloat >= compFloat
├─ LTE (5): recvFloat <= compFloat
│
├─ BETWEEN (6): 解析 "min,max" → recvFloat >= min && recvFloat <= max
└─ NOT_BETWEEN (7): 解析 "min,max" → recvFloat < min || recvFloat > max6.3 动作顺序执行 (executeAllActions)
executeAllActions(rule, receivedValue)
│
for (action : rule.actions) {
│
├─ useReceivedValue == true?
│ └─ 将 actionValue 替换为 receivedValue (数据透传)
│
├─ 根据 actionType 分发到对应执行函数:
│ ├─ 0-5, 13, 14 → executePeripheralAction() // GPIO 与反转输出
│ ├─ 6-9 → executeSystemAction() // 系统动作
│ ├─ 10 → 外设联动 (CALL_PERIPHERAL)
│ ├─ 15 → executeScriptAction() // 命令序列脚本
│ ├─ 16, 17 → executeModbusAction() // FC05/FC06 写入
│ ├─ 18 → executeModbusPollAction() // 轮询+控制
│ ├─ 19 → executeSensorReadAction() // 传感器采集
│ ├─ 20 → reserved // 历史保留位
│ ├─ 21 → triggerEvent() // 触发设备事件
│ ├─ 22, 23 → enable/disableRule() // 规则启禁
│ └─ 24-27 → executeDisplayAction() // 数码管/OLED 显示
│
├─ syncDelayMs > 0? (最大 10000ms)
│ └─ delay(syncDelayMs) // 动作间延迟
│
} // 下一个动作
│
├─ reportAfterExec == true?
│ └─ reportActionResults() → 上报执行结果
│
└─ triggerPeriphExecEvent(EVENT_PERIPH_EXEC_COMPLETED)
└─ 触发链式规则6.4 完整规则执行生命周期
规则创建/更新
└─ addRule() / updateRule() → saveConfiguration() → LittleFS 持久化
│
▼
设备运行期间
│
├─ MQTT消息到达 → handleMqttMessage()
│ └─ PLATFORM_TRIGGER 匹配 → 条件评估 → 防抖 → 动作执行
│
├─ 每秒 checkTimers()
│ ├─ TIMER_TRIGGER: 间隔/定点检查 → 动作执行
│ └─ POLL_TRIGGER: 按间隔发起轮询
│
├─ 外设数据回调 → handlePollData()
│ └─ POLL_TRIGGER 匹配 → 条件评估 → 防抖 → 动作执行
│
├─ 系统事件 → triggerEvent()
│ └─ EVENT_TRIGGER 匹配 → 防抖 → 动作执行
│
└─ 按钮状态机 → triggerButtonEvent()
└─ EVENT_TRIGGER 匹配 → 100ms防抖 → 动作执行
│
▼
动作执行链
├─ 同步执行: 直接在调用线程中顺序执行
└─ 异步执行: FreeRTOS 新建任务在 Core 1 上执行
│
▼
执行后处理
├─ reportAfterExec → 上报动作结果 (MQTT)
├─ tryReportDeviceData → 上报设备整体状态 (多协议)
└─ 触发 EVENT_PERIPH_EXEC_COMPLETED → 可能引发链式规则7. 核心方法分析
7.1 handleDataCommand()
位置: PeriphExecManager.cpp:500-638
功能: 处理平台下发的数据命令,同步执行匹配规则并构建响应。
与 handleMqttMessage 的区别:
handleMqttMessage用于一般的 MQTT 数据,异步执行handleDataCommand用于需要同步响应的命令场景,调用方需要立即获取执行结果
详细流程:
handleDataCommand(items[], itemCount, response)
│
├─ Step 1: 对 "modbus_read" 类型数据进行预处理
│ ├─ 解析外设注册表中的 modbus 节点
│ ├─ 将原始 modbus 寄存器值映射到业务外设ID
│ └─ 展开为独立的 (periphId, value) 对
│
├─ Step 2: 遍历所有数据项
│ ├─ 持锁: 遍历 enabled 规则的 PLATFORM_TRIGGER
│ │ ├─ triggerPeriphId 匹配当前 periphId?
│ │ ├─ evaluateCondition(value)?
│ │ └─ 匹配 → 同步执行 executeAllActions()
│ │
│ ├─ 释锁后: 跟踪哪些数据项被规则消费
│ └─ 未匹配的数据项保留为 "unmatched"
│
└─ Step 3: 构建响应 JSON
├─ 已执行的动作结果
└─ 未匹配的数据项 (原样返回给调用方)关键特点:
- 同步执行:不使用
dispatchAsync(),直接调用executeAllActions() - Modbus 预处理:将底层 modbus 寄存器地址自动映射为高层外设 ID
- 未匹配跟踪:记录哪些下发数据没有被任何规则处理
7.2 checkTimers()
位置: PeriphExecManager.cpp:685-750
功能: 定时触发检查,由主循环每秒调用一次。
详细流程:
checkTimers() // 每秒调用
│
├─ Phase 1 (持锁):
│ for (rule : rules) {
│ if (!rule.enabled) continue;
│ for (trigger : rule.triggers) {
│ │
│ ├─ TIMER_TRIGGER (type=1):
│ │ ├─ timerMode == 0 (间隔):
│ │ │ ├─ lastTriggerTime == 0 → 立即匹配 (首次)
│ │ │ └─ now - lastTriggerTime >= intervalSec * 1000 → 匹配
│ │ │
│ │ └─ timerMode == 1 (每日定点):
│ │ ├─ 解析 timePoint "HH:MM"
│ │ ├─ 当前时刻 == 目标时刻?
│ │ └─ now - lastTriggerTime > 60000 → 匹配 (60s防重复)
│ │
│ └─ POLL_TRIGGER (type=5):
│ └─ now - lastTriggerTime >= intervalSec * 1000
│ → 标记需要发起轮询 (不直接执行动作)
│ }
│ }
│
└─ Phase 2 (释锁):
├─ TIMER_TRIGGER 匹配 → dispatchAsync() / executeAllActions()
└─ POLL_TRIGGER 到期 → 发起外设轮询请求 (结果通过回调进入 handlePollData)7.3 executeAllActions() (核心执行器)
位置: PeriphExecManager.cpp:771-815
功能: 按顺序执行规则的所有动作。
executeAllActions(rule, receivedValue)
│
├─ 初始化结果收集器 actionResults[]
│
├─ for (i = 0; i < rule.actions.size(); i++) {
│ │
│ ├─ action = rule.actions[i]
│ │
│ ├─ useReceivedValue?
│ │ └─ effectiveValue = receivedValue (数据透传模式)
│ │ └─ else: effectiveValue = action.actionValue
│ │
│ ├─ switch (action.actionType):
│ │ ├─ 0-5, 13, 14 → executePeripheralAction(action, effectiveValue, results)
│ │ ├─ 6-9 → executeSystemAction(action, effectiveValue)
│ │ ├─ 10 → callPeripheralAction(action)
│ │ ├─ 15 → executeScriptAction(action, effectiveValue)
│ │ ├─ 16, 17 → executeModbusAction(action, effectiveValue, results)
│ │ ├─ 18 → executeModbusPollAction(action, effectiveValue, results)
│ │ ├─ 19 → executeSensorReadAction(action, effectiveValue, results)
│ │ ├─ 20 → reserved
│ │ ├─ 21 → triggerEvent(action.targetPeriphId, effectiveValue)
│ │ ├─ 22/23 → enableRule/disableRule(action.targetPeriphId)
│ │ └─ 24-27 → executeDisplayAction(action, effectiveValue)
│ │
│ └─ syncDelayMs > 0 && syncDelayMs <= 10000?
│ └─ delay(min(syncDelayMs, 10000))
│ }
│
├─ rule.reportAfterExec && results 不为空?
│ └─ reportActionResults(results) → MQTT 上报
│
└─ triggerPeriphExecEvent(EVENT_PERIPH_EXEC_COMPLETED)7.4 evaluateCondition()
位置: PeriphExecManager.cpp:648-681
功能: 评估触发条件是否满足。
算法:
evaluateCondition(receivedValue, operatorType, compareValue)
│
├─ OP_CONTAIN (8):
│ return receivedValue.indexOf(compareValue) >= 0
│
├─ OP_NOT_CONTAIN (9):
│ return receivedValue.indexOf(compareValue) < 0
│
├─ 数值转换: recv = receivedValue.toFloat(), comp = compareValue.toFloat()
│
├─ OP_EQ (0): return recv == comp
├─ OP_NEQ (1): return recv != comp
├─ OP_GT (2): return recv > comp
├─ OP_LT (3): return recv < comp
├─ OP_GTE (4): return recv >= comp
├─ OP_LTE (5): return recv <= comp
│
├─ OP_BETWEEN (6):
│ 解析 compareValue 按逗号分割为 min, max
│ return recv >= min && recv <= max
│
└─ OP_NOT_BETWEEN (7):
解析 compareValue 按逗号分割为 min, max
return recv < min || recv > max注意事项:
- 数值比较使用
float,存在浮点精度问题(如 EQ 比较) - CONTAIN/NOT_CONTAIN 操作直接使用字符串的
indexOf - BETWEEN 使用逗号分隔的格式
"min,max"
8. 异步执行引擎
8.1 调度决策 (dispatchAsync)
位置: PeriphExecManager.cpp:1270-1354
dispatchAsync(rule, receivedValue)
│
├─ 检查 1: 包含系统动作 (RESTART/FACTORY_RESET)?
│ └─ YES → 强制同步执行 (异步任务会在重启时被终止)
│
├─ 检查 2: 用户配置 execMode == 1 (强制同步)?
│ └─ YES → 同步执行
│
├─ 检查 3: 可用堆内存 < MIN_HEAP_FOR_ASYNC (30000)?
│ └─ YES → 降级为同步执行 (内存不足以创建任务)
│
├─ 检查 4: 信号量 taskSlotSemaphore 可用? (最大3个并发)
│ └─ NO → 降级为同步执行 (任务槽已满)
│
└─ 所有检查通过:
├─ 创建 AsyncExecContext (deep copy rule + value)
├─ 计算栈大小: 包含脚本? SCRIPT_TASK_STACK(8192) : SIMPLE_TASK_STACK(4096)
└─ xTaskCreatePinnedToCore(asyncExecTaskFunc, ..., Core 1)8.2 异步任务生命周期
asyncExecTaskFunc(context) // 运行在 FreeRTOS 任务中 (Core 1)
│
├─ 执行所有动作: executeAllActions(context.ruleCopy, context.receivedValue)
│
├─ 记录执行结果: recordResult(ruleId, success, ...)
│ └─ 存入最近结果列表 (用于查询)
│
├─ 触发完成事件: triggerPeriphExecEvent(EVENT_PERIPH_EXEC_COMPLETED)
│ └─ 可能触发其他规则的 EVENT_TRIGGER
│
├─ 释放任务槽: xSemaphoreGive(taskSlotSemaphore)
│
└─ 删除任务: vTaskDelete(NULL)8.3 RAII 锁保护
项目使用 RAII 模式管理 FreeRTOS 互斥锁:
class MutexGuard {
SemaphoreHandle_t _mutex;
bool _locked;
public:
explicit MutexGuard(SemaphoreHandle_t m, TickType_t timeout = portMAX_DELAY);
~MutexGuard(); // 析构时自动释放
bool locked() const;
};
class RecursiveMutexGuard {
SemaphoreHandle_t _mutex;
bool _locked;
public:
explicit RecursiveMutexGuard(SemaphoreHandle_t m, TickType_t timeout = portMAX_DELAY);
~RecursiveMutexGuard();
bool locked() const;
};使用递归互斥锁是因为某些调用路径可能嵌套(如规则执行触发事件,事件触发其他规则)。
9. 数据转换管道与上报控制
9.1 数据转换管道
外设原始数据
│
├─ useReceivedValue == true?
│ └─ 直接将触发数据传递给动作 (数据透传)
│
├─ ACTION_SENSOR_READ:
│ └─ rawValue → value * scaleFactor + offset → 格式化(decimals)
│
├─ ACTION_SCRIPT (15):
│ └─ ScriptEngine 执行命令序列脚本
│ action.actionValue → 多行命令文本
│ 支持 GPIO/PWM/DAC/PERIPH/MQTT/RANDOM 等命令
│
└─ Modbus 数据预处理 (handleDataCommand):
└─ 寄存器地址 → 外设ID 映射 → 业务值9.2 执行结果上报 (reportActionResults)
位置: PeriphExecManager.cpp:1609-1638
reportActionResults(results[])
│
├─ 构建 JSON 数组:
│ [{
│ "id": "periph_01", // 外设ID
│ "value": "1", // 执行结果值
│ "remark": "GPIO HIGH" // 执行备注
│ }, ...]
│
└─ 通过 MQTT 发布到设备上报 topic9.3 设备状态上报 (tryReportDeviceData)
位置: PeriphExecManager.cpp:1758-1839
tryReportDeviceData()
│
├─ 检查可用协议:
│ ├─ MQTT 已连接?
│ ├─ TCP 已连接?
│ ├─ HTTP 可用?
│ └─ CoAP 可用?
│
├─ 收集设备数据: collectPeripheralData()
│ ├─ 遍历所有已启用的 GPIO 外设
│ └─ 获取当前状态 (HIGH/LOW/PWM值等)
│
└─ 按优先级尝试上报:
MQTT (优先) → TCP → HTTP → CoAP (降级)协议降级链: 当高优先级协议不可用时,自动尝试下一个协议,确保数据尽可能上报。
9.4 数据收集 (collectPeripheralData)
位置: PeriphExecManager.cpp:1649-1694
collectPeripheralData()
│
├─ 获取 PeripheralManager 实例
├─ 遍历所有已注册外设
│ ├─ 外设已启用?
│ ├─ 外设类型为 GPIO?
│ └─ 读取当前状态 → 加入数据列表
│
└─ 返回 [{periphId, value}] 列表10. 按钮事件子系统
10.1 按钮状态机
位置: PeriphExecManager.cpp:1843-1961
按钮事件通过硬件轮询和状态机实现,不依赖中断。
状态机配置:
struct ButtonEventConfig {
String periphId; // 关联的GPIO外设ID
uint8_t pin; // GPIO引脚号
bool activeLow; // 是否低电平有效
unsigned long debounceMs; // 消抖时间 (默认20ms)
};
struct ButtonRuntimeState {
bool lastStableState; // 上次稳定状态
bool lastRawState; // 上次原始读取值
unsigned long lastChangeTime; // 上次状态变化时间
unsigned long pressStartTime; // 按下开始时间
uint8_t clickCount; // 连击计数
unsigned long lastClickTime; // 上次点击时间
bool longPress2sTriggered; // 2秒长按已触发
bool longPress5sTriggered; // 5秒长按已触发
bool longPress10sTriggered; // 10秒长按已触发
};状态机流程 (checkButtonEvents, 每20ms调用):
读取GPIO引脚状态 (digitalRead)
│
├─ 状态有变化?
│ └─ 重置消抖计时器 lastChangeTime = now
│
├─ 消抖完成? (now - lastChangeTime >= debounceMs)
│ │
│ ├─ 从 未按下 → 按下:
│ │ ├─ pressStartTime = now
│ │ ├─ 重置长按标志
│ │ └─ 触发 EVENT_BUTTON_STATE_CHANGED (86)
│ │
│ ├─ 保持按下中:
│ │ ├─ 按下时长 >= 2s && !longPress2sTriggered?
│ │ │ └─ 触发 EVENT_BUTTON_LONG_PRESS_2S (83)
│ │ ├─ 按下时长 >= 5s && !longPress5sTriggered?
│ │ │ └─ 触发 EVENT_BUTTON_LONG_PRESS_5S (84)
│ │ └─ 按下时长 >= 10s && !longPress10sTriggered?
│ │ └─ 触发 EVENT_BUTTON_LONG_PRESS_10S (85)
│ │
│ └─ 从 按下 → 松开:
│ ├─ 触发 EVENT_BUTTON_RELEASED (85)
│ ├─ 触发 EVENT_BUTTON_STATE_CHANGED (86)
│ ├─ clickCount++
│ ├─ lastClickTime = now
│ └─ 延迟判断:
│ ├─ now - lastClickTime > 300ms? (双击超时)
│ │ ├─ clickCount == 1 → 触发 EVENT_BUTTON_SINGLE_CLICK (80)
│ │ └─ clickCount >= 2 → 触发 EVENT_BUTTON_DOUBLE_CLICK (81)
│ └─ 等待下一次点击...10.2 按钮事件防抖
按钮事件触发规则时使用 100ms 防抖(区别于一般事件的 1000ms),因为按钮操作通常需要更快的响应。
11. 配置持久化与版本迁移
11.1 存储格式 (v3)
位置: PeriphExecManager.cpp:140-203
{
"version": 3,
"rules": [
{
"id": "exec_12345",
"name": "温度告警",
"enabled": true,
"execMode": 0,
"protocolType": 0,
"scriptContent": "",
"reportAfterExec": true,
"triggers": [
{
"triggerType": 0,
"triggerPeriphId": "temp_01",
"operatorType": 2,
"compareValue": "35",
"timerMode": 0,
"intervalSec": 60,
"timePoint": "",
"eventId": "",
"pollResponseTimeout": 1000,
"pollMaxRetries": 2,
"pollInterPollDelay": 100
}
],
"actions": [
{
"targetPeriphId": "relay_01",
"actionType": 0,
"actionValue": "",
"useReceivedValue": false,
"syncDelayMs": 0
}
]
}
]
}11.2 版本迁移
loadConfiguration() 支持三个版本的配置格式:
v1 → v3 迁移
v1 格式特征:无 version 字段,使用扁平结构
v1 扁平字段:
triggerType, triggerPeriphId, operatorType, compareValue,
timerMode, intervalSec, timePoint, eventId,
targetPeriphId, actionType, actionValue, useReceivedValue, syncDelayMs
→ 转换为 triggers[单元素] + actions[单元素]额外迁移处理:
inverted字段 → 根据原始动作映射为ACTION_HIGH_INVERTED(13) 或ACTION_LOW_INVERTED(14)- 旧
eventId格式迁移到新的事件编号系统
v2 → v3 迁移
v2 格式特征:version: 2,同样使用扁平结构
v2 同 v1 的扁平字段 → 转换为 triggers[]/actions[] 数组v3 原生加载
直接解析 triggers[] 和 actions[] JSON 数组。
11.3 持久化时机
以下操作会触发 saveConfiguration():
addRule()→ API 层调用updateRule()→ API 层调用removeRule()→ API 层调用enableRule()/disableRule()→ API 层调用
注意: 运行时状态 (lastTriggerTime, triggerCount) 不会持久化到配置文件,设备重启后这些值重置为 0。
12. API 路由层
API 路由可以按“页面操作 -> REST API -> Manager 方法 -> 配置文件/运行态”的顺序阅读。遇到页面显示成功但规则未落盘时,先看 API 响应,再看 Manager 日志,最后确认 /config/periph_exec.json 是否更新。
12.1 路由注册
位置: PeriphExecRouteHandler.cpp:14-72
| 方法 | 路径 | 处理函数 | 权限 |
|---|---|---|---|
| GET | /api/periph-exec | handleGetRules | system.view |
| POST | /api/periph-exec (JSON) | handleAddRuleJson | config.edit |
| POST | /api/periph-exec (form) | handleAddRule | config.edit |
| POST | /api/periph-exec/update (JSON) | handleUpdateRuleJson | config.edit |
| POST | /api/periph-exec/update (form) | handleUpdateRule | config.edit |
| DELETE | /api/periph-exec/ | handleDeleteRule | config.edit |
| POST | /api/periph-exec/enable | handleEnableRule | config.edit |
| POST | /api/periph-exec/disable | handleDisableRule | config.edit |
| POST | /api/periph-exec/run | handleRunOnce | config.edit |
| GET | /api/periph-exec/events/static | handleGetStaticEvents | system.view |
| GET | /api/periph-exec/events/dynamic | handleGetDynamicEvents | system.view |
| GET | /api/periph-exec/events/categories | handleGetEventCategories | system.view |
| GET | /api/periph-exec/trigger-types | handleGetTriggerTypes | system.view |
| GET | /api/periph-exec/results | handleGetRecentResults | system.view |
12.2 路由注册顺序
路由注册顺序非常重要,因为 AsyncCallbackJsonWebHandler 使用前缀匹配:
- 先注册具体路径:
/events/static,/events/dynamic,/events/categories,/trigger-types - JSON handler 先注册 update:
/api/periph-exec/update必须在/api/periph-exec之前 - 最后注册通用路径:
/api/periph-exec(GET/POST)
如果顺序颠倒,/api/periph-exec/update 的 POST 请求会被 /api/periph-exec 的 POST handler 截获。
12.3 双格式支持
每个写入操作同时支持两种请求格式:
| 格式 | Content-Type | Handler | 说明 |
|---|---|---|---|
| JSON | application/json | handleAddRuleJson / handleUpdateRuleJson | 支持完整的 triggers[]/actions[] 数组 |
| Form | application/x-www-form-urlencoded | handleAddRule / handleUpdateRule | 向后兼容,只支持单个触发器/动作 |
JSON handler 通过 AsyncCallbackJsonWebHandler 注册,优先级高于 form handler。
12.4 JSON 解析辅助
parseRuleFromJson() 统一处理 JSON 到 PeriphExecRule 的解析,包括:
- 字符串安全提取:使用
| ""防止as<String>()返回"null"字符串 - 整数兼容解析:
jsonInt()同时支持 JSON 数值和字符串格式 - 运行时字段重置:解析时
lastTriggerTime = 0,triggerCount = 0
12.5 响应格式
成功响应:
{"success": true, "message": "Rule added"}列表响应:
{
"success": true,
"data": [
{
"id": "exec_12345",
"name": "...",
"triggers": [{...}],
"actions": [{...}],
"triggerPeriphName": "温度传感器", // 关联外设名称 (额外字段)
"targetPeriphName": "继电器", // 关联外设名称 (额外字段)
"targetPeriphType": 1 // 外设类型 (额外字段)
}
]
}错误响应:
{"success": false, "message": "Rule not found"}13. 已知问题与优化建议
13.1 浮点精度问题
位置: evaluateCondition() (PeriphExecManager.cpp:648-681)
问题: 使用 float 进行 OP_EQ 比较时,浮点精度可能导致误判。例如 0.1 + 0.2 != 0.3。
建议: 对于 EQ/NEQ 操作,增加 epsilon 容差比较:
bool floatEq(float a, float b, float eps = 0.001) {
return fabs(a - b) < eps;
}13.2 防抖时间硬编码
问题: 事件防抖时间在代码中硬编码:
- 一般事件: 1000ms (
handleMqttMessage,handlePollData,triggerEvent) - 按钮事件: 100ms (
triggerButtonEvent) - 每日定时: 60000ms (
checkTimers每日定点模式)
建议: 将防抖时间提取为可配置参数,或在触发器结构中增加 debounceMs 字段,允许每个触发器独立设置。
13.3 TIMER_TRIGGER 首次立即触发
问题: 间隔模式下 lastTriggerTime == 0 导致设备重启后立即触发所有定时规则,这在某些场景下可能不是期望行为(如重启后立即触发告警)。
建议: 增加配置项 skipFirstTrigger,允许用户选择重启后是否跳过首次触发。或者在 addRule() 时将 lastTriggerTime 初始化为 millis()。
13.4 异步任务失败无重试
问题: asyncExecTaskFunc 中如果动作执行失败,只记录结果和触发完成事件,没有重试机制。
建议: 对于通信类动作 (Modbus/MQTT/HTTP),增加可配置的重试次数和退避策略。
13.5 规则数量无上限
问题: rules map 没有设置最大容量限制。在 ESP32 的有限内存下,大量规则可能导致内存耗尽。
建议: 增加 MAX_RULES 限制(如 20-50),在 addRule() 中检查。
13.6 配置保存频率
问题: 每次 CRUD 操作都会触发 saveConfiguration(),对 LittleFS 闪存进行写入。频繁的写入操作会缩短闪存寿命。
建议: 实现延迟写入(dirty flag + 定时保存),或在批量操作时合并保存。
13.7 两阶段锁的时序风险
问题: Phase 1 收集匹配规则后释放锁,Phase 2 执行时规则可能已被删除或修改。当前通过复制规则数据缓解,但在高频触发场景下仍有边界情况。
现状: 代码已通过在 Phase 1 中复制规则数据到匹配列表来解决此问题,但对于非异步路径中的 executeAllActions 调用,仍然使用了规则引用,存在潜在的竞态条件。
13.8 系统动作的安全性
问题: ACTION_FACTORY_RESET 可通过规则自动触发,如果条件配置不当(如事件链循环触发),可能导致意外的出厂重置。
建议: 对破坏性系统动作 (RESTART/FACTORY_RESET) 增加二次确认机制或执行次数限制。
13.9 按钮双击检测的延迟
问题: 双击判定需要等待 300ms 超时后才能确定是单击还是双击,这意味着单击响应永远有 300ms 延迟。
建议: 可提供配置项让用户在"单击响应速度"和"双击支持"之间选择。不需要双击的场景可禁用双击检测以获得更快的单击响应。
13.10 旧版预留枚举与版本兼容
ExecActionType 中的空缺位 11、12 是早期版本预留的 ACTION_AP_MODE 和 ACTION_BLE_TOGGLE,在采用 AP+STA 双模自动切换后移除,定义同时移除以避免被新动作覆用。从旧版本升级的配置文件中如果残留 actionType=11 或 12,执行时会在 switch 默认分支被警告跳过。
建议: 更新 UI 和 API 下拉选项时确保不暴露 11、12 这两个值,更新时强制映射到替代动作(如 EVENT_TRIGGER + WiFi 相关事件)。
附录 A: 源码文件索引
| 文件 | 行数 | 功能 |
|---|---|---|
include/core/PeripheralExecution.h | ~200 | 枚举定义、数据结构、事件常量 |
include/core/AsyncExecTypes.h | ~60 | FreeRTOS 异步执行类型、RAII 锁 |
include/core/PeriphExecManager.h | ~120 | 管理器类接口声明 |
src/core/PeriphExecManager.cpp | 2162 | 完整业务逻辑实现 |
include/network/handlers/PeriphExecRouteHandler.h | ~45 | API 路由处理器声明 |
src/network/handlers/PeriphExecRouteHandler.cpp | 566 | API 路由处理实现 |
附录 B: 关键方法速查表
| 方法 | 文件位置 (行号) | 功能 |
|---|---|---|
initialize() | PeriphExecManager.cpp:15-25 | 初始化互斥锁和信号量,加载配置 |
addRule() | PeriphExecManager.cpp:29-58 | 创建新规则 |
updateRule() | PeriphExecManager.cpp:60-89 | 更新规则(保留运行时状态) |
saveConfiguration() | PeriphExecManager.cpp:140-203 | v3 格式持久化到 LittleFS |
loadConfiguration() | PeriphExecManager.cpp:205-349 | 加载配置(支持 v1/v2/v3) |
handleMqttMessage() | PeriphExecManager.cpp:353-422 | MQTT 消息触发 (PLATFORM_TRIGGER) |
handlePollData() | PeriphExecManager.cpp:426-496 | 轮询数据触发 (POLL_TRIGGER) |
handleDataCommand() | PeriphExecManager.cpp:500-638 | 同步数据命令处理 |
evaluateCondition() | PeriphExecManager.cpp:648-681 | 条件表达式评估 |
checkTimers() | PeriphExecManager.cpp:685-750 | 定时触发检查 (每秒) |
executeAllActions() | PeriphExecManager.cpp:771-815 | 顺序执行规则动作 |
executePeripheralAction() | PeriphExecManager.cpp:817-891 | GPIO 动作执行 |
executeModbusAction() | PeriphExecManager.cpp:893-952 | Modbus 写入 |
executeModbusPollAction() | PeriphExecManager.cpp:954-1119 | Modbus 轮询+控制 |
executeSensorReadAction() | PeriphExecManager.cpp:1121-1174 | 传感器读取 |
executeSystemAction() | PeriphExecManager.cpp:1176-1227 | 系统动作 |
executeScriptAction() | PeriphExecManager.cpp:1231-1253 | 脚本执行 |
dispatchAsync() | PeriphExecManager.cpp:1270-1354 | 异步/同步调度决策 |
asyncExecTaskFunc() | PeriphExecManager.cpp:1357-1403 | FreeRTOS 异步任务函数 |
triggerEvent() | PeriphExecManager.cpp:1458-1511 | 事件触发入口 |
triggerButtonEvent() | PeriphExecManager.cpp:1964-2009 | 按钮事件触发 |
checkButtonEvents() | PeriphExecManager.cpp:1843-1961 | 按钮状态机 (每20ms) |
reportActionResults() | PeriphExecManager.cpp:1609-1638 | 上报执行结果 |
tryReportDeviceData() | PeriphExecManager.cpp:1758-1839 | 设备状态上报 |
collectPeripheralData() | PeriphExecManager.cpp:1649-1694 | 收集外设状态数据 |
