avatar

蜗牛札记

记录技术、生活与一点点折腾

  • 首页
  • PetBoxX
  • 工具软件
  • NAS 折腾
  • Linux 运维
  • VPS & 网络
  • 关于
主页 PetBoxX ESP32 安全冗余机制设计与测试手册
文章

PetBoxX ESP32 安全冗余机制设计与测试手册

发表于 最近 更新于 最近
作者 Snailszzy
51~66 分钟 阅读

适用固件版本:v1.1.14+
平台:ESP32-S3 / ESP-IDF
场景:孵化保温箱,PTC 加热 + 顶部风扇 + SHT30/40 温湿度传感器


背景与动机

2025 年 5 月 17 日,设备 80B54EE7B22C 在 OTA 升级后出现以下故障:

  1. LEDC PWM 驱动进入不一致状态 — 顶部风扇 PWM 信号丢失,转速归零
  2. I2C 驱动进入不一致状态 — SHT30 在 OTA 重启时处于测量中途,SDA 被拉低,所有后续读取超时,s_sht_data 温湿度冻结在 39.80 °C / 42.70 %

由于温度数据冻结,MQTT 的变化抑制逻辑(相同数据不上报)导致云端看不到任何异常,同时顶部风扇已停转,加热器却因控制环仍以旧数据运算而继续工作。

这次改动的目标:任何单点故障都不能导致 PTC 加热器在无人干预的情况下持续工作。


整体架构

┌─────────────────────────────────────────────────────────────┐
│                      ESP32 任务层                            │
│                                                             │
│  sensor_sht_task (优先级 5)                                  │
│  ├─ 每 1s 读 SHT30/40                                       │
│  ├─ 成功 → set_temperature() → device_control_mark_alive()  │
│  ├─ 失败 5 次 → I2C 完整重初始化 + SHT 软复位               │
│  └─ esp_task_wdt_reset() (每循环)                           │
│                                                             │
│  safety_monitor_task (优先级 6)          ← 比 sensor 高一级  │
│  ├─ 每 1s 检查 device_control_get_last_alive_ms()           │
│  ├─ > 10s 未心跳 → device_control_force_heater_off()       │
│  ├─ 每 10s MQTT 上报 safety_status                          │
│  └─ esp_task_wdt_reset() (每循环)                           │
│                                                             │
│  main_task (优先级 10)                                       │
│  ├─ WiFi/MQTT 状态机                                         │
│  ├─ 未注册 WDT(可合法长阻塞处理网络事件)                    │
│  └─ 每 50ms 检查异步 NTP 完成标志                            │
│                                                             │
│  ntp_sync_task (一次性, 优先级 4)                            │
│  └─ obtain_time() 后台运行,不阻塞 main_task                 │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│                    ESP-IDF Task WDT                          │
│  超时: 60s  触发: 打日志(不 panic)                          │
│  注册任务: sensor_sht_task, safety_monitor_task              │
│  排除任务: main_task, MQTT task, NTP task(网络可合法阻塞)   │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│                  硬件安全联锁(已有)                          │
│  fan_rpm_meas < 200 RPM → heat_cmd = 0(control_loop.c)    │
└─────────────────────────────────────────────────────────────┘

实现细节

1. 控制循环心跳 API

文件:main/devices/device_control.c / device_control.h

// 每次温控成功 tick 调用(sensor_sht_task 成功读取后)
void device_control_mark_alive(void);

// 返回距上次 mark_alive 的毫秒数(从未调用则返回 0)
int64_t device_control_get_last_alive_ms(void);

实现要点:

  • 使用 esp_timer_get_time()(微秒精度,不依赖 RTOS tick)
  • mark_alive() 同时会自动清除因 ctrl_stall 导致的 forced-off 锁存,实现故障自恢复
void device_control_mark_alive(void) {
    s_last_alive_us = esp_timer_get_time();
    // 自动清除 ctrl_stall 类 forced-off(传感器恢复后自动解锁)
    if (s_heater_forced_off &&
        s_heater_off_reason[0] == 'c' &&
        s_heater_off_reason[1] == 't') {
        s_heater_forced_off = false;
        s_heater_off_reason[0] = '\0';
    }
}

2. 强制断热 API

文件:main/devices/device_control.c / device_control.h

// 立即切断 PTC,并阻止 apply_control_from_loop() 重新启用加热
void device_control_force_heater_off(const char *reason);

// 清除锁存(手动恢复或传感器自恢复后调用)
void device_control_clear_heater_forced_off(void);

bool        device_control_is_heater_forced_off(void);
const char *device_control_get_heater_off_reason(void);

关键实现:在 apply_control_from_loop() 入口处插入拦截:

static void apply_control_from_loop(float user_sp_c, float temp_meas_c) {
    // 安全联锁:强制断热标志置位时,不允许任何加热
    if (s_heater_forced_off) {
        triac_delay_us = AC_HALF_CYCLE_US;  // 立即关闭 TRIAC
        heating_enabled = false;
        return;  // 跳过所有控制逻辑
    }
    // ... 正常控制逻辑
}

triac_delay_us 由硬件 ISR(零过检测中断)直接读取,设为 AC_HALF_CYCLE_US(10000 µs)意味着 TRIAC 在整个交流半周期都不触发,等效于立即断开加热。

reason 字符串约定:

reason触发场景
ctrl_stall温控心跳超时(safety_monitor 检测到)
ota_start固件 OTA 开始前
lcd_otaLCD OTA 开始前
test手动测试命令

3. 安全监督任务

文件:main/petboxX.c

#define SAFETY_CTRL_STALL_MS   10000  // 10s 无心跳 → 触发断热

static void safety_monitor_task(void *arg) {
    esp_task_wdt_add(NULL);
    vTaskDelay(pdMS_TO_TICKS(15000));  // 等待设备启动稳定

    while (1) {
        esp_task_wdt_reset();
        int64_t alive_ms = device_control_get_last_alive_ms();

        if (!device_control_is_heater_forced_off() &&
            alive_ms > SAFETY_CTRL_STALL_MS) {
            device_control_force_heater_off("ctrl_stall");
            // MQTT 告警上报(best-effort)
        }

        // 每 10s 定期上报安全状态
        if (++s_pub_tick >= 10) {
            // 发布 safety_status MQTT 消息
        }

        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

设计考量:

  • 优先级 6,高于 sensor_sht_task(5),确保即使 sensor 任务挤 CPU 也能被调度
  • 启动延迟 15s:避免设备刚上电、I2C 还在初始化时就误判"心跳超时"
  • 不调用 esp_restart():保温箱场景下,重启会导致 LCD 复位、继电器冲击,等待人工检查更安全

4. I2C 完整重初始化

文件:main/devices/sensor_sht.c

原有逻辑(缺陷):连续 5 次失败仅重置 s_sensor_type,不清理驱动状态。

新逻辑:连续 5 次失败触发完整重初始化:

sensor_sht_i2c_full_restart()
├─ i2c_driver_delete()          ← 清理驱动
├─ 16 个 SCL 脉冲 + STOP 条件   ← 物理总线复位
├─ sensor_sht_i2c_master_init() ← 重装驱动
└─ 发送 SHT30 软复位命令 0x30A2  ← 重置传感器 FSM

SHT30 软复位原理:SHT30 内部状态机在上电或复位后从头开始。OTA 重启时如果 SHT30 正在采样(FSM 处于 measurement state),重启后 ESP32 发出的 I2C START 会与 SHT30 状态机预期的序列不一致,导致 NACK 或数据错误。0x30A2 强制 SHT30 退出任何中间状态回到 idle。


5. Task Watchdog 配置

文件:main/petboxX.c

esp_task_wdt_config_t wdt_cfg = {
    .timeout_ms    = 60 * 1000,  // 60s 超时
    .idle_core_mask = 0,          // 不监控 idle 任务
    .trigger_panic  = false,      // 仅打日志,不触发 panic 重启
};
esp_task_wdt_reconfigure(&wdt_cfg);

为什么 60s 而不是推荐的 8-15s?

Task WDT 是"任务完全冻结"的最后一道防线(如死锁、无限循环)。应用层心跳检测(10s)才是主要的温控异常检测手段。60s 避免 OTA 写 Flash、I2C 总线恢复等合法长操作误触发。

为什么 trigger_panic = false?

保温箱首要目标是安全停止加热,而不是立即重启。safety_monitor_task 在 WDT 触发前(<60s)就已经切断加热器了。WDT 触发时加热器已处于安全状态,此时打日志比 panic 更利于远程诊断。

注册与排除:

任务WDT 注册原因
sensor_sht_task✅ 注册严格 1s 周期,绝不应长阻塞
safety_monitor_task✅ 注册严格 1s 周期
main_task❌ 排除处理 WiFi 事件,可合法长阻塞
MQTT 任务❌ 排除网络 IO 可合法长等待
ntp_sync_task❌ 排除一次性任务,最多等 10s

6. OTA 前强制断热

文件:main/devices/petbox_ota.c / petbox_lcd_ota.c

static void ota_task(void *param) {
    // 第一步:断热,防止 OTA 期间温控任务被抢占导致热失控
    device_control_force_heater_off("ota_start");
    ESP_LOGI(TAG, "OTA: heater forced OFF before download");
    // ...后续下载、写 Flash、重启
}

OTA 重启后 s_heater_forced_off 不持久化(RAM 变量),新固件启动时自动清零,温控正常恢复。


7. NTP 同步异步化

文件:main/petboxX.c

问题:原来 obtain_time() 在 main_task 的 SYS_RUN 状态处理中同步调用,最多阻塞 10s(10 次重试 × 1s)。期间 WiFi 断连等事件积压在队列无法处理。

修复:独立任务后台运行:

原来:main_task → SYS_RUN → obtain_time() [阻塞 0~10s] → http_server_start()
                                ↑ WiFi 断连事件丢失

现在:main_task → SYS_RUN → http_server_start()    ← 立即执行
                           → petbox_mqtt_start()    ← 立即执行
                           → xTaskCreate(ntp_sync_task)  ← 后台
                                       ↓
                          [ntp_sync_task] obtain_time() → s_ntp_done=true
                                       ↓
                          [main_task 主循环每 50ms] s_ntp_done → ntp_time_synced=true

WiFi 重连时 s_ntp_running = false,下次连接自动重新发起 NTP 同步。


8. 启动复位原因上报

文件:main/petboxX.c

// app_main 开头读取
esp_reset_reason_t rr = esp_reset_reason();
// 转换为字符串,存入 s_boot_reset_reason
// 每 10s 通过 safety_status MQTT 消息上报

上报字段 boot_reset_reason 可能的值:

值含义
power_on正常上电
sw_reset软件主动重启(esp_restart())
task_wdtTask Watchdog 触发重启
int_wdtInterrupt Watchdog 触发重启
panic代码异常(assert、null 解引用等)
brownout欠压保护
ext_resetEN 引脚外部复位

MQTT 消息参考

定时上报:safety_status(每 10s)

Topic:petbox/iot/{device_id}/status

{
  "event": "safety_status",
  "ctrl_alive_ms": 1234,
  "heater_forced_off": false,
  "heater_off_reason": "",
  "boot_reset_reason": "power_on"
}
字段含义
ctrl_alive_ms距上次温控心跳的毫秒数,正常应 < 2000
heater_forced_off加热器是否被强制关闭
heater_off_reason关闭原因(ctrl_stall / ota_start / lcd_ota / test)
boot_reset_reason本次启动的复位原因

告警上报:safety_alert(触发时立即发送)

{
  "event": "safety_alert",
  "reason": "ctrl_stall",
  "stall_ms": 11234,
  "heater_forced_off": true
}

命令响应格式

所有命令通过后端 API 下发:

curl -X POST "http://<server>/api/devices/<device_id>/commands/custom" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"action":"<命令名>"}'

测试步骤

前置准备

  1. 固件版本 v1.1.14+ 烧录完成
  2. 设备在线,MQTT 已连接
  3. 订阅设备 status topic 以实时查看响应:
    petbox/iot/{device_id}/status
    
  4. 获取有效 Bearer Token(由后端 /api/auth/login 接口签发)

TEST-01:查询当前安全状态

目的:确认 safety_monitor_task 正常运行,心跳数据正常。

命令:

curl -X POST ".../commands/custom" -d '{"action":"safety_status"}'

预期响应(MQTT):

{
  "event": "safety_status_ack",
  "ok": true,
  "detail": "alive_ms=1200 forced_off=0 reason="
}

验收条件:

  • alive_ms < 2000(温控每秒心跳一次)
  • forced_off = 0
  • reason 为空

TEST-02:模拟温控环卡死(需要 SAFETY_TEST_ENABLED=1)

目的:验证 safety_monitor_task 能在 10s 内检测到心跳停跳并切断加热器。

命令:

curl -X POST ".../commands/custom" -d '{"action":"safety_test_stall"}'

执行后观察:

  1. 立即(0s):

    • 收到 safety_test_stall_ack 响应
    • triac_delay_us 被设为 AC_HALF_CYCLE_US,加热器立刻停止
    • heater_forced_off = true,reason = "ctrl_stall"
  2. 约 1-2s 内:

    • safety_monitor_task 检测到 alive_ms > 10000
    • 发送 MQTT safety_alert:
      {"event":"safety_alert","reason":"ctrl_stall","stall_ms":11000,"heater_forced_off":true}
      
  3. 约 10s 内:

    • 温度曲线(前端)开始缓慢下降(加热停止,散热中)

注意:此时 sensor_sht_task 仍在正常运行(I2C 读取、心跳仍更新),但因 s_heater_forced_off=true,apply_control_from_loop() 入口被拦截,心跳不会自动清除该标志(ctrl_stall 自清除逻辑在 mark_alive() 里,但 force_heater_off() 是从测试命令触发的,不是真实卡死——所以心跳实际上还在更新,mark_alive() 会在下一个 tick 自动清除 forced-off)。

如果测试命令立即被心跳自动恢复,说明 sensor 任务运行正常,这是正确行为。若要保持断热状态更长时间观察,可以先 safety_test_stall 之后再快速查询 safety_status。


TEST-03:手动恢复加热

目的:验证 safety_recover 能清除 forced-off 锁存,恢复正常温控。

命令:

curl -X POST ".../commands/custom" -d '{"action":"safety_recover"}'

预期响应:

{
  "event": "safety_recover_ack",
  "ok": true,
  "detail": "was_forced=1 reason=ctrl_stall"
}

验收条件:

  • was_forced=1 确认原来处于断热状态
  • 之后 safety_status 查询 forced_off=0
  • 温度曲线恢复上升趋势

TEST-04:验证 OTA 期间 PTC 断热

目的:确认 OTA 开始时加热器被立即切断。

步骤:

  1. 下发 OTA 命令(任意 URL,哪怕失败也行,关键是观察初始行为)
  2. 同时订阅 MQTT status topic

预期串口日志(最优先出现):

I (xxx) petbox_ota: OTA: heater forced OFF before download

预期 MQTT(OTA started ACK 之前就应看到 heater_forced_off=true):

  • 下一个 safety_status 上报中 heater_off_reason="ota_start"

验收条件:OTA ACK "stage":"started" 的时间戳 晚于 heater 被切断的时间戳。


TEST-05:验证 I2C 恢复(需要硬件操作)

目的:确认 SHT30 断线后能自动恢复,不需要重启设备。

步骤:

  1. 观察 MQTT 温湿度正常上报
  2. 短暂断开 SHT30 的 SDA 或 SCL 引脚(2-3 秒后恢复)
  3. 观察串口日志

预期日志(断线期间):

E (xxx) SHT: Failed to read temperature and humidity
E (xxx) SHT: Failed to read temperature and humidity
... (共 5 次)
W (xxx) SHT: I2C full restart: deleting driver and reinitialising
I (xxx) SHT: SHT30 soft reset sent

恢复后:

I (xxx) SHT: I2C device found at 0x44
I (xxx) SHT: SHT sensor detected: SHT30
I (xxx) SHT: Current: Temperature=37.xx°C ...

验收条件:

  • 重新接好后 30s 内温湿度恢复正常上报
  • 不需要重启设备
  • MQTT 温度恢复上报(不再冻结)

TEST-06:验证断网不重启

目的:确认 WiFi 断开或 MQTT 断连不会触发 WDT 重启。

步骤:

  1. 关闭 WiFi 路由器或设备的 WiFi AP
  2. 等待 3-5 分钟
  3. 恢复 WiFi 连接

观察:

  • 断网期间:串口日志只有 WiFi 重连重试,无 WDT 触发日志
  • 断网期间:sensor_sht_task 仍在运行,串口显示温湿度读取正常
  • 恢复后:设备重新连接 WiFi → MQTT → 自动继续上报

验收条件:

  • 断网 5 分钟内设备没有重启(通过 boot_reset_reason 确认,恢复连接后仍为 power_on)
  • 断网期间温控继续工作(串口温度数据持续更新)

TEST-07:验证复位原因上报

目的:确认 boot_reset_reason 能正确区分重启原因。

步骤:

操作预期 boot_reset_reason
正常上电(断电重插)power_on
MQTT 发送 esp_restart 命令sw_reset
手动按 EN 复位按钮ext_reset
制造 panic(代码 assert)panic

查看方式:设备连接 MQTT 后,等待最多 10s,safety_status 消息中包含 boot_reset_reason 字段。

也可通过串口开机日志直接查看:

W (xxx) MAIN: Boot reset reason: sw_reset (3)

TEST-08:验证 NTP 异步化(断网下仍可控温)

目的:确认 NTP 同步失败时 MQTT 和温控不受影响。

步骤:

  1. 设备断网启动(或启动后立即断网,在 NTP 同步期间)
  2. 观察 MQTT 连接和温控行为

预期:

  • http_server_start() 和 petbox_mqtt_start() 正常执行(不等待 NTP)
  • NTP 重试 10 次失败后放弃,串口日志:E (xxx) NTP: Failed to synchronize time.
  • 温湿度上报、加热控制正常继续(完全不依赖 NTP)
  • 时间显示为 "Time not set",但功能不受影响

验收条件:NTP 失败不导致 MQTT 断开、不导致温控停止、不导致重启。


量产(MP)编译配置

在 main/CMakeLists.txt 中控制测试命令的开关:

target_compile_definitions(${COMPONENT_LIB} PRIVATE
    PETBOX_USB_HOST_ENABLE=1
    SAFETY_TEST_ENABLED=0   # ← MP 量产时改为 0
)

SAFETY_TEST_ENABLED=0 时:

  • safety_test_stall、safety_test_heater_off 命令被编译器完全移除
  • safety_status、safety_recover 命令保留(运维必要工具)
  • 安全冗余本身(心跳、断热、WDT)不受影响,永远开启

关键常量速查

常量位置默认值说明
SAFETY_CTRL_STALL_MSpetboxX.c10000 ms心跳超时阈值
SAFETY_WDT_TIMEOUT_SpetboxX.c60 sTask WDT 超时
HIGH_TEMP_ALERT_THRESHOLDsensor_sht.c39.0 °C高温告警阈值
MAX_RETRY_COUNTntp_time_sync.c10 次NTP 同步最大重试次数
I2C 全重初始化触发sensor_sht.c5 次连续失败触发完整 I2C 驱动重装

已知局限与后续改进

项目当前状态建议改进
风扇卡死自恢复未实现PWM > 0 但 RPM = 0 超过 N 秒 → 重初始化 LEDC
WDT 触发 panictrigger_panic=false待充分测试后可改为 true,并在 boot_reset_reason=task_wdt 时发 MQTT 告警
forced-off 持久化重启后清除OTA 场景下可接受;其他场景可考虑写 NVS
stall 后主动保持排风未显式设置在 safety_monitor 检测到 stall 后调用 set_back_fan_speed(30)

文档生成日期:2025-05-17 | 固件 commit:待定

PetBoxX
PetBoxX
许可协议:  CC BY 4.0
分享

相关文章

5月 17, 2026

PetBoxX ESP32 安全冗余机制设计与测试手册

适用固件版本:v1.1.14+ 平台:ESP32-S3 / ESP-IDF 场景:孵化保温箱,PTC 加热 + 顶部风扇 + SHT30/40 温湿度传感器 背景与动机 2025 年 5 月 17 日,设备 80B54EE7B22C 在 OTA 升级后出现以下故障: LEDC PWM 驱动进入不一致

5月 14, 2026

PetBoxX 恒温区风扇控制的演变:固定 PWM、hold_ready 重置,与动态微调

本文记录 PetBoxX 孵化箱固件在"恒温维持阶段"风扇控制策略的三次演变,从最初的固定 PWM、引入 hold_ready 累计判断,到最终的按风扇资质归一化的动态微调。最后讨论哪种方案在理论和实测数据上更优。 背景:PTC + 风扇的控温物理模型 PetBoxX 使用 PTC(正温度系数)陶瓷

5月 14, 2026

PetBoxX 育雏系统:温湿度分阶段管理与自动喷雾设计

记录 PetBoxX ESP32-S3 固件中喷雾育雏模块的完整设计思路,方便日后查阅。 涉及两个相互独立但协同工作的子系统:湿度区间状态机(humidity_ctrl)和水位自动补水任务(mist_autofill)。 整体架构 育雏期的湿度管理分两层,职责完全分离: 模块 职责 控制的硬件 hu

下一篇

上一篇

PetBoxX 恒温区风扇控制的演变:固定 PWM、hold_ready 重置,与动态微调

最近更新

  • PetBoxX ESP32 安全冗余机制设计与测试手册
  • PetBoxX 恒温区风扇控制的演变:固定 PWM、hold_ready 重置,与动态微调
  • PetBoxX 育雏系统:温湿度分阶段管理与自动喷雾设计
  • 宠物保温箱恒温控制系统设计笔记
  • AutoTune 分段控制执行范围

热门标签

Halo PetBox PetBoxX

目录

©2026 蜗牛札记. 保留部分权利。

使用 Halo 主题 Chirpy