diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 666f113..02ab959 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -7,7 +7,19 @@ "WebSearch", "WebFetch(domain:boardor.com)", "Read(//d//**)", - "PowerShell(Get-Content \"D:\\\\R60AFD1_数据手册_V1.2.pdf\" -Raw -Encoding Byte | Out-Null; Write-Host \"File exists, size:\" \\(\\(Get-Item \"D:\\\\R60AFD1_数据手册_V1.2.pdf\"\\).Length\\);)" + "PowerShell(Get-Content \"D:\\\\R60AFD1_数据手册_V1.2.pdf\" -Raw -Encoding Byte | Out-Null; Write-Host \"File exists, size:\" \\(\\(Get-Item \"D:\\\\R60AFD1_数据手册_V1.2.pdf\"\\).Length\\);)", + "Bash(py -m pip install openpyxl -q)", + "Read(//d/PlatformIO1/Projects/**)", + "Bash(pio pkg *)", + "Bash(git stash *)", + "Bash(git checkout *)", + "Bash(sed -i 's/#define OLED_SDA_PIN 14/#define OLED_SDA_PIN 15/' include/config.h)", + "Bash(sed -i 's/#define OLED_SCL_PIN 15/#define OLED_SCL_PIN 16/' include/config.h)", + "Bash(sed -i 's/#define BTN_PROVISION_PIN 16/#define BTN_PROVISION_PIN 14/' include/config.h)", + "Bash(sed -i 's/#define BTN_PAGE_SWITCH_PIN 10/#define BTN_PAGE_SWITCH_PIN 5/' include/config.h)", + "Bash(sed -i 's/#define BTN_LONG_PRESS_MS 3000/#define BTN_LONG_PRESS_MS 1500/' include/config.h)", + "Bash(sed -i 's/#define WATER_SENSOR_PIN 6 /#define WATER_SENSOR_PIN 45/' include/config.h)", + "Bash(sed -i 's/#define BUZZER_PIN 7 /#define BUZZER_PIN 21/' include/config.h)" ] } } diff --git a/data/index.html b/data/index.html index f174f00..c401077 100644 --- a/data/index.html +++ b/data/index.html @@ -3,459 +3,696 @@ - 健康监测系统 - 配网设置 + 健康监测系统 · 设备配网 + + +
+
- + +
-
🏥
+

健康监测系统

-

ESP32-S3 + 毫米波雷达 · 配网设置

+

+ ESP32-S3 · 60G 毫米波雷达
五劳无感康养监测 +

- +
-
- 📶 WiFi 设置 +
+
📶
+ 无线网络
-
+ +
- +
-
+ +
+ +
- +
-
- ☁️ MQTT 云平台设置 +
+
☁️
+ 云平台连接
-
- - + +
+
+ + +
+
+ + +
-
- - -
-
- - -
-
- - + +
+
+ + +
+
+ + +
- -
-
- 🏠 环境备注 + +
+
+
🏠
+ 安装位置
-
- - -
-
- 💡 请备注此设备安装的环境位置,方便在云平台上区分不同房间的数据 +
+ +
+

+ 💡 请备注设备安装的房间,便于在云平台区分不同区域的数据 +

- +
-
- 📱 设备设置 +
+
📱
+ 设备标识
-
- - +
+ +
- - - - +
- 等待配置... + + 等待配置…
-
- -
+
diff --git a/include/alarm_manager.h b/include/alarm_manager.h index 527a4dd..f314b1b 100644 --- a/include/alarm_manager.h +++ b/include/alarm_manager.h @@ -1,9 +1,12 @@ /** * @file alarm_manager.h - * @brief 报警管理器 - 双模式 (本地判定 + 服务端指令) + * @brief 报警管理器 - 多类型分优先级报警 (本地判定 + 服务端指令) * - * 本地模式: 离线也能用, 跌倒/水浸/心率异常/呼吸异常 本地判定触发 + * 优先级: 跌倒 > 水浸 > 心率异常 > 呼吸异常 > 温湿度异常 > 传感器故障 + * + * 本地模式: 离线也能用, 全部类型本地判定触发 * 服务端模式: 收到 cmd/{gateid}/alarm, alarm/restore 执行联动 + * 防抖: 传感器故障需连续 N 次确认才触发, 避免误报 */ #ifndef ALARM_MANAGER_H @@ -12,16 +15,23 @@ #include #include "config.h" +// 报警回调 — 通知外部 (如 MQTT 上报) +typedef void (*AlarmCallback)(uint8_t type, bool triggered, const char* desc); + class AlarmManager { public: - // 告警类型 (本地 + 服务端共用) - enum AlarmType { + // 告警类型 (按优先级从高到低) + enum AlarmType : uint8_t { ALARM_NONE = 0, - ALARM_FALL, // 跌倒 (本地 + 服务端) - ALARM_WATER_LEAK, // 水浸 (本地 + 服务端) - ALARM_HEART_ABNORMAL, // 心率异常 (本地) - ALARM_BREATH_ABNORMAL, // 呼吸异常 (本地) - ALARM_SERVER, // 服务端下发 (通用) + ALARM_FALL, // 跌倒 (本地 + 服务端) ★★★★★ + ALARM_WATER_LEAK, // 水浸 (本地 + 服务端) ★★★★ + ALARM_HEART_ABNORMAL, // 心率异常 (本地) ★★★ + ALARM_BREATH_ABNORMAL, // 呼吸异常 (本地) ★★★ + ALARM_TEMP_ABNORMAL, // 温度异常 (本地) ★★ + ALARM_HUMI_ABNORMAL, // 湿度异常 (本地) ★★ + ALARM_SENSOR_FAULT, // 传感器故障 (本地) ★ + ALARM_SERVER, // 服务端下发 (通用) + ALARM_COUNT }; AlarmManager(); @@ -34,11 +44,11 @@ public: // 本地报警接口 (离线可用) // ============================================================ - /** 触发本地报警 */ - void triggerLocal(AlarmType type); + /** 触发本地报警 (防抖由调用方处理) */ + void triggerLocal(AlarmType type, const char* detail = nullptr); /** 清除本地报警 */ void clearLocal(AlarmType type); - /** 清除所有本地报警 */ + /** 清除所有本地报警 (含防抖计数器) */ void clearAllLocal(); // ============================================================ @@ -53,33 +63,58 @@ public: void buzzerTest(uint32_t durationSec); // ============================================================ - // 状态查询 (上报用) + // 回调注册 // ============================================================ - bool isBuzzerOn() const { return _buzzerOn; } - bool hasActiveAlarm() const { return _activeCount > 0; } - const char* getActiveAlarmType() const; + /** 报警状态变化回调 (触发/清除时调用, 用于MQTT上报) */ + void onAlarmChange(AlarmCallback cb) { _alarmCb = cb; } + + // ============================================================ + // 状态查询 + // ============================================================ + + bool isBuzzerOn() const { return _buzzerOn; } + bool hasActiveAlarm() const { return _activeCount > 0; } + uint8_t getActiveCount() const { return _activeCount; } + AlarmType getHighestAlarm() const; + const char* getAlarmName(AlarmType type) const; + const char* getActiveAlarmStr() const; private: + // 报警优先级 (越小越紧急) + static uint8_t _priority(AlarmType t); + void _setBuzzer(bool on); void _setLED(bool on); void _updateBuzzer(); void _updateLED(); + uint32_t _buzzerIntervalFor(AlarmType type); // 蜂鸣器/LED 状态 bool _buzzerOn; bool _ledOn; - uint32_t _buzzerOffTime; // 定时关闭时刻 (0=手动关) + uint32_t _buzzerOffTime; // 定时关闭时刻 (0=手动/无限) uint32_t _buzzerToggleTime; // 间歇切换时刻 bool _buzzerBeepState; // 间歇状态 (响/停) uint8_t _beepCount; // 当前周期哔了几声 + uint8_t _beepsPerCycle; // 当前报警类型每周期几声 + uint32_t _beepOnMs; // 每声持续 ms + uint32_t _beepOffMs; // 每声间隔 ms + uint32_t _cyclePauseMs; // 周期间停顿 ms - // 活动报警列表 (最多 4 个同时) - AlarmType _activeAlarms[4]; + // 活动报警列表 (最多 5 个同时) + static const uint8_t MAX_ACTIVE = 5; + AlarmType _activeAlarms[MAX_ACTIVE]; uint8_t _activeCount; + // 当前详情 (用于上报) + char _activeDetail[64]; + // LED 闪烁 uint32_t _lastLEDToggle; + + // 回调 + AlarmCallback _alarmCb; }; #endif // ALARM_MANAGER_H diff --git a/include/button_manager.h b/include/button_manager.h index 812e3d9..f7a5ffd 100644 --- a/include/button_manager.h +++ b/include/button_manager.h @@ -1,10 +1,13 @@ /** * @file button_manager.h - * @brief 三按钮管理器 - 去抖 + 长按/短按识别 + * @brief 三按钮管理器 - 双向去抖 + 长按/短按识别 + 超时自愈 * - * 按钮1 (GPIO14): 长按3秒 → 配网 - * 按钮2 (GPIO15): 短按 → 解除报警 - * 按钮3 (GPIO16): 短按 → 翻页 + * 按钮1 (GPIO14): 长按1.5秒 → 配网 + * 按钮2 (GPIO13): 短按 → 解除报警 + * 按钮3 (GPIO46): 短按 → 翻页 + * + * 去抖策略: 状态变化需持续稳定 BTN_DEBOUNCE_MS 才确认 + * 自愈策略: 按钮按下超过 10 秒强制复位,防止卡死 */ #ifndef BUTTON_MANAGER_H @@ -13,6 +16,8 @@ #include #include "config.h" +#define BTN_STUCK_TIMEOUT_MS 10000 // 按钮卡死超时 (10秒强制复位) + typedef void (*ButtonCallback)(void); class ButtonManager { @@ -31,17 +36,32 @@ public: void onPageSwitch(ButtonCallback cb) { _cbPage = cb; } private: + /** + * 按钮状态机 (基于稳定读数而非边沿) + * + * 状态转换: + * IDLE ──[raw稳定LOW×BTN_DEBOUNCE_MS]──→ PRESSED (记录pressTime) + * PRESSED ──[hold≥BTN_LONG_PRESS_MS]──→ 触发长按回调 (仅限hasLong) + * PRESSED ──[raw稳定HIGH×BTN_DEBOUNCE_MS]──→ 检查hold时长触发短按 → IDLE + * PRESSED ──[hold≥BTN_STUCK_TIMEOUT_MS]──→ 强制复位 → IDLE (防卡死) + */ struct ButtonState { uint8_t pin; - bool pressed; // 当前是否在按下状态 - uint32_t pressTime; // 按下时刻 (ms) - bool longTriggered; // 防止长按时重复触发短按 + bool stableState; // 当前确认的稳定状态 (true=按下) + bool rawState; // 上一次 raw 读值 + uint32_t changeTime; // raw 状态变化时刻 + uint32_t pressTime; // 稳定按下时刻 (ms) + bool longTriggered; // 长按已触发 + bool shortPending; // 等待释放以判定短按 }; - /** 处理单个按钮的状态机 */ + /** 处理单个按钮 (基于稳定状态而非瞬时边沿) */ void _processButton(ButtonState& btn, bool hasShort, bool hasLong); bool _readPin(uint8_t pin); + /** 强制复位单个按钮 */ + void _resetButton(ButtonState& btn); + ButtonState _btnProvision; ButtonState _btnDismiss; ButtonState _btnPage; diff --git a/include/config.h b/include/config.h index 9e4ef7d..a7207c1 100644 --- a/include/config.h +++ b/include/config.h @@ -30,10 +30,10 @@ #define DHT11_PIN 4 // 温湿度传感器 // --- 水浸传感器 --- -#define WATER_SENSOR_PIN 6 // 数字输入, 低电平=有水 +#define WATER_SENSOR_PIN 45 // 数字输入, 低电平=有水 // --- 蜂鸣器 (PWM) --- -#define BUZZER_PIN 7 +#define BUZZER_PIN 21 #define BUZZER_PWM_CHANNEL 0 #define BUZZER_PWM_FREQ 2000 #define BUZZER_PWM_RESOLUTION 8 @@ -47,15 +47,15 @@ #define LED_ALARM_PIN 9 // 红色 - 报警指示 // --- OLED 显示屏 (SSD1306 128x64 I2C) --- -#define OLED_SDA_PIN 14 // I2C 数据线 -#define OLED_SCL_PIN 15 // I2C 时钟线 +#define OLED_SDA_PIN 15 // I2C 数据线 +#define OLED_SCL_PIN 16 // I2C 时钟线 // --- 交互按钮 --- -#define BTN_PROVISION_PIN 16 // 擦除WiFi配网按钮 (长按3秒) +#define BTN_PROVISION_PIN 14 // 擦除WiFi配网按钮 (长按3秒) #define BTN_ALARM_DISMISS_PIN 13 // 解除报警按钮 -#define BTN_PAGE_SWITCH_PIN 10 // 翻页按钮 +#define BTN_PAGE_SWITCH_PIN 46 // 翻页按钮 #define BTN_DEBOUNCE_MS 50 // 去抖时间 -#define BTN_LONG_PRESS_MS 3000 // 长按判定 +#define BTN_LONG_PRESS_MS 1500 // 长按判定 // ============================================================ // 2. 设备标识 (协议规范 §2) diff --git a/include/display_manager.h b/include/display_manager.h index 96da8d0..d170db6 100644 --- a/include/display_manager.h +++ b/include/display_manager.h @@ -26,11 +26,14 @@ public: /** * @brief 更新显示内容 (每 5 秒调用一次即可) - * @param data 遥测数据 - * @param wifiOk WiFi 是否已连接 - * @param buzzerOn 蜂鸣器是否在响 + * @param data 遥测数据 + * @param wifiOk WiFi 是否已连接 + * @param buzzerOn 蜂鸣器是否在响 + * @param hasAlarm 是否有活跃报警 + * @param alarmName 报警名称 (如 "跌倒"/"心率异常") */ - void update(const TelemetryData& data, bool wifiOk, bool buzzerOn); + void update(const TelemetryData& data, bool wifiOk, bool buzzerOn, + bool hasAlarm = false, const char* alarmName = ""); /** 切换到下一页 (0→1→2→3→0 循环) */ void nextPage(); @@ -39,10 +42,11 @@ public: uint8_t getPage() const { return _pageIndex; } private: - void _drawPage0(const TelemetryData& data, bool wifiOk, bool buzzerOn); - void _drawPage1(const TelemetryData& data); - void _drawPage2(const TelemetryData& data); - void _drawPage3(const TelemetryData& data, bool buzzerOn, bool wifiOk); + void _drawPage0(const TelemetryData& data, bool wifiOk, bool buzzerOn, bool hasAlarm); + void _drawPage1(const TelemetryData& data, bool hasAlarm); + void _drawPage2(const TelemetryData& data, bool hasAlarm); + void _drawPage3(const TelemetryData& data, bool buzzerOn, bool wifiOk, bool hasAlarm); + void _drawAlarmBanner(const char* name); /** 格式化秒数为 HH:MM:SS */ String _fmtUptime(uint32_t secs) const; diff --git a/include/mqtt_manager.h b/include/mqtt_manager.h index fb5e58d..e830043 100644 --- a/include/mqtt_manager.h +++ b/include/mqtt_manager.h @@ -80,6 +80,9 @@ public: const char* errorCode, const char* errorMsg, const char* wifiSignal = ""); + /** 本地报警上报 status/{gateid}/alarm (§4.7 扩展) */ + void publishLocalAlarm(uint8_t alarmType, bool triggered, const char* desc); + // ============================================================ // 回调注册 (协议规范 §5) // ============================================================ diff --git a/include/wifi_manager.h b/include/wifi_manager.h index c0c6990..3e3a904 100644 --- a/include/wifi_manager.h +++ b/include/wifi_manager.h @@ -87,11 +87,16 @@ private: bool _connectWiFi(); /** - * @brief 设置 LED 状态 - * @param mode 0=慢闪(未连接) 1=快闪(配网) 2=常亮(已连接) + * @brief 设置 LED 模式 (线程安全, 无阻塞) + * @param mode 0=灭 1=慢闪(断连1s) 2=快闪(配网300ms) 3=常亮(已连接) */ void _setLEDMode(uint8_t mode); + /** + * @brief LED 闪烁独立任务 (FreeRTOS, 不受主循环阻塞影响) + */ + static void _ledTaskFunc(void* param); + /** * @brief 检测按钮长按 */ @@ -116,9 +121,9 @@ private: uint32_t _buttonPressTime; bool _buttonPressed; - // LED 闪烁状态 - uint32_t _ledLastToggle; - bool _ledState; + // LED 独立任务 (不被 startConfigPortal 阻塞影响) + TaskHandle_t _ledTaskHandle; + volatile uint8_t _ledMode; // 0=灭 1=慢闪 2=快闪 3=常亮 // WiFiManager 自定义参数 WiFiManagerParameter* _param_mqtt_server; diff --git a/platformio.ini b/platformio.ini index 42cb3e8..1c12f78 100644 --- a/platformio.ini +++ b/platformio.ini @@ -43,4 +43,4 @@ lib_deps = ; 上传配置 upload_speed = 921600 -upload_port = COM15 +upload_port = COM16 diff --git a/src/alarm_manager.cpp b/src/alarm_manager.cpp index 4ddee10..7bae9d0 100644 --- a/src/alarm_manager.cpp +++ b/src/alarm_manager.cpp @@ -1,13 +1,43 @@ /** * @file alarm_manager.cpp - * @brief 报警管理器 - 双模式实现 + * @brief 报警管理器 - 多类型分优先级实现 * - * 本地: 跌倒/水浸/心率异常/呼吸异常 → 立即蜂鸣器+LED - * 服务端: cmd/alarm, cmd/alarm/restore → 执行联动 + 回复响应 + * 蜂鸣模式 (人身安全 > 环境 > 设备): + * 跌倒: 连速哔 (60ms/60ms) — 最急 ★★★★★ + * 心率/呼吸: 5连急哔 (80ms/80ms) — 次急 ★★★★★ + * 水浸: 3连哔 (120ms/100ms) — 中等 ★★★ + * 温湿度: 2声长停 (200ms/400ms) — 温和 ★★ + * 传感器: 单声长哔 (300ms/3s) — 提示 ★ */ #include "alarm_manager.h" +// ============================================================ +// 优先级定义 — 人身安全 > 环境安全 > 设备故障 +// ============================================================ + +uint8_t AlarmManager::_priority(AlarmType t) { + switch (t) { + // ★★★★★ 人身安全 — 直接威胁生命 + case ALARM_FALL: return 0; + case ALARM_HEART_ABNORMAL: return 1; + case ALARM_BREATH_ABNORMAL: return 1; + // ★★★ 环境安全 — 潜在危险 + case ALARM_WATER_LEAK: return 2; + // ★★ 环境舒适 — 非紧急 + case ALARM_TEMP_ABNORMAL: return 3; + case ALARM_HUMI_ABNORMAL: return 3; + // ★ 设备状态 — 仅提示 + case ALARM_SENSOR_FAULT: return 4; + case ALARM_SERVER: return 5; + default: return 9; + } +} + +// ============================================================ +// 构造 & 析构 +// ============================================================ + AlarmManager::AlarmManager() : _buzzerOn(false) , _ledOn(false) @@ -15,10 +45,16 @@ AlarmManager::AlarmManager() , _buzzerToggleTime(0) , _buzzerBeepState(false) , _beepCount(0) + , _beepsPerCycle(3) + , _beepOnMs(150) + , _beepOffMs(100) + , _cyclePauseMs(600) , _activeCount(0) , _lastLEDToggle(0) + , _alarmCb(nullptr) { - for (int i = 0; i < 4; i++) _activeAlarms[i] = ALARM_NONE; + for (uint8_t i = 0; i < MAX_ACTIVE; i++) _activeAlarms[i] = ALARM_NONE; + memset(_activeDetail, 0, sizeof(_activeDetail)); } AlarmManager::~AlarmManager() {} @@ -30,16 +66,12 @@ AlarmManager::~AlarmManager() {} bool AlarmManager::begin() { ledcSetup(BUZZER_PWM_CHANNEL, BUZZER_PWM_FREQ, BUZZER_PWM_RESOLUTION); ledcAttachPin(BUZZER_PIN, BUZZER_PWM_CHANNEL); - ledcWrite(BUZZER_PWM_CHANNEL, 0); // 关 + ledcWrite(BUZZER_PWM_CHANNEL, 0); pinMode(LED_ALARM_PIN, OUTPUT); digitalWrite(LED_ALARM_PIN, LOW); - _buzzerToggleTime = 0; - _buzzerBeepState = false; - _beepCount = 0; - - Serial.println(F("[告警] ✅ 双模式报警 (本地+服务端)")); + Serial.println(F("[告警] ✅ 多类型分优先级报警就绪 (7种类型)")); return true; } @@ -53,33 +85,129 @@ void AlarmManager::loop() { } // ============================================================ -// 本地报警触发 (离线可用) +// 报警名称映射 // ============================================================ -void AlarmManager::triggerLocal(AlarmType type) { +const char* AlarmManager::getAlarmName(AlarmType type) const { + switch (type) { + case ALARM_FALL: return "跌倒"; + case ALARM_WATER_LEAK: return "水浸"; + case ALARM_HEART_ABNORMAL: return "心率异常"; + case ALARM_BREATH_ABNORMAL: return "呼吸异常"; + case ALARM_TEMP_ABNORMAL: return "温度异常"; + case ALARM_HUMI_ABNORMAL: return "湿度异常"; + case ALARM_SENSOR_FAULT: return "传感器故障"; + case ALARM_SERVER: return "云端指令"; + default: return "未知"; + } +} + +// ============================================================ +// 最高优先级报警 +// ============================================================ + +AlarmManager::AlarmType AlarmManager::getHighestAlarm() const { + if (_activeCount == 0) return ALARM_NONE; + AlarmType best = ALARM_NONE; + uint8_t bestP = 99; + for (uint8_t i = 0; i < _activeCount; i++) { + uint8_t p = _priority(_activeAlarms[i]); + if (p < bestP) { bestP = p; best = _activeAlarms[i]; } + } + return best; +} + +const char* AlarmManager::getActiveAlarmStr() const { + AlarmType t = getHighestAlarm(); + if (t == ALARM_NONE) return ""; + return getAlarmName(t); +} + +// ============================================================ +// 蜂鸣参数表 (按最高优先级报警驱动) +// ============================================================ + +uint32_t AlarmManager::_buzzerIntervalFor(AlarmType type) { + switch (type) { + case ALARM_FALL: return 200; // LED 快闪 + case ALARM_WATER_LEAK: return 300; + case ALARM_HEART_ABNORMAL: return 500; + case ALARM_BREATH_ABNORMAL: return 500; + case ALARM_TEMP_ABNORMAL: return 800; + case ALARM_HUMI_ABNORMAL: return 800; + case ALARM_SENSOR_FAULT: return 1000; + default: return 500; + } +} + +// ============================================================ +// 更新蜂鸣参数 (根据当前最高优先级报警) +// ============================================================ + +static void _calcBuzzerParams(AlarmManager::AlarmType t, + uint8_t& beeps, uint32_t& onMs, + uint32_t& offMs, uint32_t& pauseMs) { + switch (t) { + // 人身安全 — 连速急哔 (最刺耳) + case AlarmManager::ALARM_FALL: + beeps = 0; onMs = 60; offMs = 60; pauseMs = 0; // 跌倒: 连速哔 + break; + case AlarmManager::ALARM_HEART_ABNORMAL: + case AlarmManager::ALARM_BREATH_ABNORMAL: + beeps = 5; onMs = 80; offMs = 80; pauseMs = 400; // 心率/呼吸: 5连急哔 + break; + // 环境安全 — 次急 + case AlarmManager::ALARM_WATER_LEAK: + beeps = 3; onMs = 120; offMs = 100; pauseMs = 500; // 水浸: 3连哔 + break; + // 环境舒适 — 温和 + case AlarmManager::ALARM_TEMP_ABNORMAL: + case AlarmManager::ALARM_HUMI_ABNORMAL: + beeps = 2; onMs = 200; offMs = 400; pauseMs = 1200; // 温湿度: 2声长停 + break; + // 设备状态 — 仅提示 + case AlarmManager::ALARM_SENSOR_FAULT: + beeps = 1; onMs = 300; offMs = 3000; pauseMs = 0; // 传感器: 单声提醒 + break; + default: + beeps = 0; onMs = 500; offMs = 500; pauseMs = 0; // 持续 + break; + } +} + +// ============================================================ +// 本地报警触发 +// ============================================================ + +void AlarmManager::triggerLocal(AlarmType type, const char* detail) { // 去重 for (uint8_t i = 0; i < _activeCount; i++) { if (_activeAlarms[i] == type) return; } - if (_activeCount < 4) { + if (_activeCount < MAX_ACTIVE) { _activeAlarms[_activeCount++] = type; } - const char* name = ""; - switch (type) { - case ALARM_FALL: name = "跌倒"; break; - case ALARM_WATER_LEAK: name = "水浸"; break; - case ALARM_HEART_ABNORMAL: name = "心率异常"; break; - case ALARM_BREATH_ABNORMAL: name = "呼吸异常"; break; - default: name = "未知"; break; + const char* name = getAlarmName(type); + Serial.printf("[告警] 🚨 触发: %s (活动:%d)", name, _activeCount); + if (detail && detail[0]) Serial.printf(" | %s", detail); + Serial.println(); + + // 更新蜂鸣参数 (按最高优先级) + AlarmType highest = getHighestAlarm(); + _calcBuzzerParams(highest, _beepsPerCycle, _beepOnMs, _beepOffMs, _cyclePauseMs); + + // 保存详情 + if (detail) { + strncpy(_activeDetail, detail, sizeof(_activeDetail) - 1); } - Serial.printf("[告警] 🚨 本地报警触发: %s (活动报警:%d)\n", name, _activeCount); - - // 立即开启蜂鸣器和 LED _setBuzzer(true); _setLED(true); + + // 通知外部 + if (_alarmCb) _alarmCb((uint8_t)type, true, detail ? detail : name); } void AlarmManager::clearLocal(AlarmType type) { @@ -94,21 +222,29 @@ void AlarmManager::clearLocal(AlarmType type) { } } - Serial.printf("[告警] 本地报警清除, 剩余活动:%d\n", _activeCount); + const char* name = getAlarmName(type); + Serial.printf("[告警] ✅ 清除: %s (剩余:%d)\n", name, _activeCount); - // 没有活动报警时关闭 if (_activeCount == 0) { _setBuzzer(false); _setLED(false); + _activeDetail[0] = '\0'; + } else { + // 降级到次高优先级报警的蜂鸣参数 + AlarmType highest = getHighestAlarm(); + _calcBuzzerParams(highest, _beepsPerCycle, _beepOnMs, _beepOffMs, _cyclePauseMs); } + + if (_alarmCb) _alarmCb((uint8_t)type, false, name); } void AlarmManager::clearAllLocal() { _activeCount = 0; - for (int i = 0; i < 4; i++) _activeAlarms[i] = ALARM_NONE; + for (uint8_t i = 0; i < MAX_ACTIVE; i++) _activeAlarms[i] = ALARM_NONE; + _activeDetail[0] = '\0'; _setBuzzer(false); _setLED(false); - Serial.println(F("[告警] 清除所有本地报警")); + Serial.println(F("[告警] 清除所有报警")); } // ============================================================ @@ -116,16 +252,15 @@ void AlarmManager::clearAllLocal() { // ============================================================ void AlarmManager::executeAlarm(const AlarmCommand& cmd) { - Serial.printf("[告警] ☁️ 服务端告警: %s (等级=%s, 蜂鸣器=%d)\n", + Serial.printf("[告警] ☁️ 服务端: %s (等级=%s 蜂鸣=%d)\n", cmd.alarmType, cmd.alarmLevel, cmd.buzzerOn); - // 映射 alarmType 到本地类型 AlarmType localType = ALARM_SERVER; if (strcmp(cmd.alarmType, "fall") == 0) localType = ALARM_FALL; else if (strcmp(cmd.alarmType, "water_leak") == 0) localType = ALARM_WATER_LEAK; if (cmd.buzzerOn) { - triggerLocal(localType); + triggerLocal(localType, cmd.alarmDesc); if (cmd.buzzerDuration > 0) { _buzzerOffTime = millis() + (uint32_t)cmd.buzzerDuration * 1000; } else { @@ -138,12 +273,8 @@ void AlarmManager::executeAlarm(const AlarmCommand& cmd) { } } -// ============================================================ -// 服务端恢复指令 (§5.2) -// ============================================================ - void AlarmManager::executeRestore(const AlarmCommand& cmd) { - Serial.printf("[告警] ☁️ 服务端恢复: %s\n", cmd.alarmType); + Serial.printf("[告警] ☁️ 恢复: %s\n", cmd.alarmType); AlarmType localType = ALARM_SERVER; if (strcmp(cmd.alarmType, "fall") == 0) localType = ALARM_FALL; @@ -151,12 +282,8 @@ void AlarmManager::executeRestore(const AlarmCommand& cmd) { clearLocal(localType); - if (!cmd.buzzerOn) { - _setBuzzer(false); - } - if (!cmd.nightLight) { - _setLED(false); - } + if (!cmd.buzzerOn) _setBuzzer(false); + if (!cmd.nightLight) _setLED(false); } // ============================================================ @@ -164,27 +291,11 @@ void AlarmManager::executeRestore(const AlarmCommand& cmd) { // ============================================================ void AlarmManager::buzzerTest(uint32_t durationSec) { - Serial.printf("[告警] 🔔 蜂鸣器测试 %d秒\n", durationSec); + Serial.printf("[告警] 🔔 蜂鸣器测试 %ds\n", durationSec); _setBuzzer(true); _buzzerOffTime = millis() + durationSec * 1000; } -// ============================================================ -// 状态查询 -// ============================================================ - -const char* AlarmManager::getActiveAlarmType() const { - if (_activeCount == 0) return ""; - switch (_activeAlarms[0]) { - case ALARM_FALL: return "fall"; - case ALARM_WATER_LEAK: return "water_leak"; - case ALARM_HEART_ABNORMAL: return "heart_abnormal"; - case ALARM_BREATH_ABNORMAL: return "breath_abnormal"; - case ALARM_SERVER: return "server"; - default: return ""; - } -} - // ============================================================ // 内部控制 // ============================================================ @@ -195,7 +306,7 @@ void AlarmManager::_setBuzzer(bool on) { _buzzerToggleTime = 0; _beepCount = 0; if (on) { - _buzzerOffTime = 0; // 本地报警=无限哔哔 + _buzzerOffTime = 0; } else { ledcWrite(BUZZER_PWM_CHANNEL, 0); _buzzerOffTime = 0; @@ -207,57 +318,72 @@ void AlarmManager::_setLED(bool on) { if (!on) digitalWrite(LED_ALARM_PIN, LOW); } +// ============================================================ +// 蜂鸣器更新 (优先级驱动模式) +// ============================================================ + void AlarmManager::_updateBuzzer() { - // 定时关闭检查 - if (_buzzerOn && _buzzerOffTime > 0 && millis() >= _buzzerOffTime) { + if (!_buzzerOn) return; + + // 定时关闭 + if (_buzzerOffTime > 0 && millis() >= _buzzerOffTime) { _buzzerOn = false; ledcWrite(BUZZER_PWM_CHANNEL, 0); Serial.println(F("[告警] ⏰ 蜂鸣器定时关闭")); return; } - // 本地报警: 三连哔 停 — 循环 (哔哔哔...哔哔哔...) - if (_buzzerOn && _buzzerOffTime == 0) { - uint32_t now = millis(); - // 周期: 3声哔 (每声150ms响+100ms停) + 600ms大停顿 + + uint32_t now = millis(); + + // 云端定时: 持续响 + if (_buzzerOffTime > 0) { + ledcWrite(BUZZER_PWM_CHANNEL, 128); + return; + } + + // 本地报警: 按优先级模式 — 哔声/停顿交替 + if (_beepsPerCycle == 0) { + // 连速哔 (跌倒): 简单翻转 + if (now - _buzzerToggleTime >= (_buzzerBeepState ? _beepOnMs : _beepOffMs)) { + _buzzerToggleTime = now; + _buzzerBeepState = !_buzzerBeepState; + ledcWrite(BUZZER_PWM_CHANNEL, _buzzerBeepState ? 128 : 0); + } + } else { + // 多声一组: N声哔 + 长停顿 if (!_buzzerBeepState) { - // 在停顿中,检查是否该下一声 - uint32_t pause = (_beepCount >= 3) ? 600 : 100; + // 停顿中 + uint32_t pause = (_beepCount >= _beepsPerCycle) ? _cyclePauseMs : _beepOffMs; if (now - _buzzerToggleTime >= pause) { _buzzerToggleTime = now; - if (_beepCount >= 3) _beepCount = 0; // 重置周期 + if (_beepCount >= _beepsPerCycle) _beepCount = 0; _buzzerBeepState = true; ledcWrite(BUZZER_PWM_CHANNEL, 128); _beepCount++; } } else { - // 在响中,150ms后停下 - if (now - _buzzerToggleTime >= 150) { + // 响声中 + if (now - _buzzerToggleTime >= _beepOnMs) { _buzzerToggleTime = now; _buzzerBeepState = false; ledcWrite(BUZZER_PWM_CHANNEL, 0); } } } - // 云端定时: 持续响 - else if (_buzzerOn && _buzzerOffTime > 0) { - ledcWrite(BUZZER_PWM_CHANNEL, 128); - } } +// ============================================================ +// LED 更新 (按最高优先级决定闪烁频率) +// ============================================================ + void AlarmManager::_updateLED() { if (!_ledOn) { digitalWrite(LED_ALARM_PIN, LOW); return; } - // 根据告警类型调整闪烁频率 - uint32_t interval = 500; - for (uint8_t i = 0; i < _activeCount; i++) { - if (_activeAlarms[i] == ALARM_FALL) { - interval = 200; // 跌倒快闪 - break; - } - } + AlarmType highest = getHighestAlarm(); + uint32_t interval = _buzzerIntervalFor(highest); if (millis() - _lastLEDToggle >= interval) { _lastLEDToggle = millis(); diff --git a/src/button_manager.cpp b/src/button_manager.cpp index 185aa93..8b9fdce 100644 --- a/src/button_manager.cpp +++ b/src/button_manager.cpp @@ -1,6 +1,12 @@ /** * @file button_manager.cpp - * @brief 三按钮管理器实现 - 去抖 + 长按/短按 + * @brief 三按钮管理器实现 - 双向去抖 + 长按/短按 + 超时自愈 + * + * 核心改进 (v2): + * 1. 按下沿和释放沿都经过 BTN_DEBOUNCE_MS 稳定确认 + * 2. 长按期间持续检测 (不再等到松手才触发) + * 3. 按下超过 BTN_STUCK_TIMEOUT_MS 自动复位, 防止状态机卡死 + * 4. 用 stableState 替代瞬时 raw 驱动状态, 消除噪声误触发 */ #include "button_manager.h" @@ -14,9 +20,19 @@ ButtonManager::ButtonManager() , _cbDismiss(nullptr) , _cbPage(nullptr) { - _btnProvision = { BTN_PROVISION_PIN, false, 0, false }; - _btnDismiss = { BTN_ALARM_DISMISS_PIN, false, 0, false }; - _btnPage = { BTN_PAGE_SWITCH_PIN, false, 0, false }; + auto initBtn = [](ButtonState& s, uint8_t pin) { + s.pin = pin; + s.stableState = false; + s.rawState = false; + s.changeTime = 0; + s.pressTime = 0; + s.longTriggered = false; + s.shortPending = false; + }; + + initBtn(_btnProvision, BTN_PROVISION_PIN); + initBtn(_btnDismiss, BTN_ALARM_DISMISS_PIN); + initBtn(_btnPage, BTN_PAGE_SWITCH_PIN); } // ============================================================ @@ -28,9 +44,19 @@ void ButtonManager::begin() { pinMode(BTN_ALARM_DISMISS_PIN, INPUT_PULLUP); pinMode(BTN_PAGE_SWITCH_PIN, INPUT_PULLUP); - Serial.println(F("[按钮] ✅ 三按钮初始化完成")); + // 读取初始状态, 避免启动时的虚假边沿 + _btnProvision.rawState = _readPin(BTN_PROVISION_PIN); + _btnProvision.stableState = _btnProvision.rawState; + _btnDismiss.rawState = _readPin(BTN_ALARM_DISMISS_PIN); + _btnDismiss.stableState = _btnDismiss.rawState; + _btnPage.rawState = _readPin(BTN_PAGE_SWITCH_PIN); + _btnPage.stableState = _btnPage.rawState; + + Serial.println(F("[按钮] ✅ 三按钮初始化完成 (双向去抖+超时自愈)")); Serial.printf(" 配网: GPIO%d | 解除报警: GPIO%d | 翻页: GPIO%d\n", BTN_PROVISION_PIN, BTN_ALARM_DISMISS_PIN, BTN_PAGE_SWITCH_PIN); + Serial.printf(" 去抖:%dms | 长按:%dms | 卡死超时:%dms\n", + BTN_DEBOUNCE_MS, BTN_LONG_PRESS_MS, BTN_STUCK_TIMEOUT_MS); } // ============================================================ @@ -49,40 +75,91 @@ void ButtonManager::loop() { } // ============================================================ -// 单按钮状态机 +// 单按钮状态机 (基于稳定状态, 非瞬时边沿) // ============================================================ void ButtonManager::_processButton(ButtonState& btn, bool hasShort, bool hasLong) { - bool raw = _readPin(btn.pin); // LOW = 按下 + uint32_t now = millis(); + bool raw = _readPin(btn.pin); // true = 按下 (LOW) - if (raw && !btn.pressed) { - // 按下沿 - btn.pressed = true; - btn.pressTime = millis(); - btn.longTriggered = false; + // ── 第1步: raw 状态变化时启动去抖计时 ── + if (raw != btn.rawState) { + btn.rawState = raw; + btn.changeTime = now; + // 不立即改变 stableState — 等稳定确认 } - else if (!raw && btn.pressed) { - // 松开沿 - btn.pressed = false; - uint32_t holdTime = millis() - btn.pressTime; - if (holdTime < BTN_DEBOUNCE_MS) { - return; // 抖动脉冲,忽略 + // ── 第2步: 去抖确认 — raw 保持稳定超过 BTN_DEBOUNCE_MS 才生效 ── + bool debounced = (now - btn.changeTime >= BTN_DEBOUNCE_MS); + bool stateChanged = (btn.rawState != btn.stableState); + + if (debounced && stateChanged) { + // 状态正式切换 + btn.stableState = btn.rawState; + + if (btn.stableState) { + // ── 确认按下 ── + btn.pressTime = now; + btn.longTriggered = false; + btn.shortPending = hasShort; // 只有支持短按的按钮才标记待判定 + } else { + // ── 确认释放 ── + uint32_t holdTime = now - btn.pressTime; + + // 短按判定: 按下时长在去抖和长按阈值之间 + if (btn.shortPending && holdTime >= BTN_DEBOUNCE_MS + && holdTime < BTN_LONG_PRESS_MS && !btn.longTriggered) { + // 根据引脚分发短按回调 + if (btn.pin == BTN_ALARM_DISMISS_PIN && _cbDismiss) { + _cbDismiss(); + } + if (btn.pin == BTN_PAGE_SWITCH_PIN && _cbPage) { + _cbPage(); + } + } + + btn.shortPending = false; + btn.longTriggered = false; } + } - if (hasLong && holdTime >= BTN_LONG_PRESS_MS && !btn.longTriggered) { - // 长按触发 + // ── 第3步: 长按检测 (在按下稳定期间持续检查) ── + if (hasLong && btn.stableState && !btn.longTriggered) { + uint32_t holdTime = now - btn.pressTime; + if (holdTime >= BTN_LONG_PRESS_MS) { btn.longTriggered = true; - if (_cbProvision) _cbProvision(); - } - else if (hasShort && holdTime >= BTN_DEBOUNCE_MS && holdTime < BTN_LONG_PRESS_MS) { - // 短按触发 - if (btn.pin == BTN_ALARM_DISMISS_PIN && _cbDismiss) _cbDismiss(); - if (btn.pin == BTN_PAGE_SWITCH_PIN && _cbPage) _cbPage(); + btn.shortPending = false; // 长按已触发, 不再判定短按 + if (_cbProvision) { + _cbProvision(); + } } } + + // ── 第4步: 卡死自愈 — 按下超时强制复位 ── + if (btn.stableState && (now - btn.pressTime >= BTN_STUCK_TIMEOUT_MS)) { + Serial.printf("[按钮] ⚠️ GPIO%d 按下超时 %lums, 强制复位\n", + btn.pin, now - btn.pressTime); + _resetButton(btn); + } } +// ============================================================ +// 强制复位 +// ============================================================ + +void ButtonManager::_resetButton(ButtonState& btn) { + btn.stableState = false; + btn.rawState = _readPin(btn.pin); // 重新同步真实硬件状态 + btn.changeTime = 0; + btn.pressTime = 0; + btn.longTriggered = false; + btn.shortPending = false; +} + +// ============================================================ +// 读引脚 (INPUT_PULLUP → LOW = 按下) +// ============================================================ + bool ButtonManager::_readPin(uint8_t pin) { - return digitalRead(pin) == LOW; // INPUT_PULLUP: LOW = 按下 + return digitalRead(pin) == LOW; } diff --git a/src/display_manager.cpp b/src/display_manager.cpp index 93c2c2c..f408025 100644 --- a/src/display_manager.cpp +++ b/src/display_manager.cpp @@ -71,37 +71,63 @@ void DisplayManager::nextPage() { // 主更新入口 // ============================================================ -void DisplayManager::update(const TelemetryData& data, bool wifiOk, bool buzzerOn) { +void DisplayManager::update(const TelemetryData& data, bool wifiOk, bool buzzerOn, + bool hasAlarm, const char* alarmName) { if (!_initialized) return; - // 每 5 秒刷新一次 (防止 OLED 烧屏,也避免 SPI 占用过多) - if (millis() - _lastDrawTime < 5000) return; + // 有报警时每秒刷新 (闪烁提示),否则 5 秒 + uint32_t interval = hasAlarm ? 1000 : 5000; + if (millis() - _lastDrawTime < interval) return; _lastDrawTime = millis(); _u8g2.clearBuffer(); + // 活跃报警 — 所有页面顶部闪烁提示 + if (hasAlarm && alarmName && alarmName[0]) { + _drawAlarmBanner(alarmName); + } + switch (_pageIndex) { - case 0: _drawPage0(data, wifiOk, buzzerOn); break; - case 1: _drawPage1(data); break; - case 2: _drawPage2(data); break; - case 3: _drawPage3(data, buzzerOn, wifiOk); break; - default: _drawPage0(data, wifiOk, buzzerOn); break; + case 0: _drawPage0(data, wifiOk, buzzerOn, hasAlarm); break; + case 1: _drawPage1(data, hasAlarm); break; + case 2: _drawPage2(data, hasAlarm); break; + case 3: _drawPage3(data, buzzerOn, wifiOk, hasAlarm); break; + default: _drawPage0(data, wifiOk, buzzerOn, hasAlarm); break; } _u8g2.sendBuffer(); } +// ============================================================ +// 报警横幅 (顶部闪烁) +// ============================================================ + +void DisplayManager::_drawAlarmBanner(const char* name) { + // 每秒翻转,实现闪烁效果 + bool blink = ((millis() / 1000) % 2 == 0); + + _u8g2.setFont(u8g2_font_wqy12_t_gb2312); + if (blink) { + _u8g2.drawUTF8(2, 12, "🚨"); + _u8g2.drawUTF8(26, 12, name); + } else { + _u8g2.drawUTF8(2, 12, " "); + } + _u8g2.drawHLine(0, 15, 128); +} + // ============================================================ // Page 0: 环境数据 // ============================================================ -void DisplayManager::_drawPage0(const TelemetryData& data, bool wifiOk, bool buzzerOn) { - // 标题栏 - _u8g2.setFont(u8g2_font_wqy12_t_gb2312); - _u8g2.drawUTF8(2, 12, "🏥 健康监测 V1.0"); - - // 分隔线 - _u8g2.drawHLine(0, 15, 128); +void DisplayManager::_drawPage0(const TelemetryData& data, bool wifiOk, + bool buzzerOn, bool hasAlarm) { + // 标题栏 (报警横幅已占顶部时跳过) + if (!hasAlarm) { + _u8g2.setFont(u8g2_font_wqy12_t_gb2312); + _u8g2.drawUTF8(2, 12, "🏥 健康监测 V1.0"); + _u8g2.drawHLine(0, 15, 128); + } // 温湿度 _u8g2.setFont(u8g2_font_wqy12_t_gb2312); @@ -144,11 +170,13 @@ void DisplayManager::_drawPage0(const TelemetryData& data, bool wifiOk, bool buz // Page 1: 生命体征 // ============================================================ -void DisplayManager::_drawPage1(const TelemetryData& data) { +void DisplayManager::_drawPage1(const TelemetryData& data, bool hasAlarm) { // 标题栏 - _u8g2.setFont(u8g2_font_wqy12_t_gb2312); - _u8g2.drawUTF8(2, 12, "🫀 生命体征"); - _u8g2.drawHLine(0, 15, 128); + if (!hasAlarm) { + _u8g2.setFont(u8g2_font_wqy12_t_gb2312); + _u8g2.drawUTF8(2, 12, "🫀 生命体征"); + _u8g2.drawHLine(0, 15, 128); + } // 心率 _u8g2.drawUTF8(4, 32, "心率:"); @@ -182,10 +210,12 @@ void DisplayManager::_drawPage1(const TelemetryData& data) { // Page 2: 人体状态 // ============================================================ -void DisplayManager::_drawPage2(const TelemetryData& data) { - _u8g2.setFont(u8g2_font_wqy12_t_gb2312); - _u8g2.drawUTF8(2, 12, "人体状态"); - _u8g2.drawHLine(0, 15, 128); +void DisplayManager::_drawPage2(const TelemetryData& data, bool hasAlarm) { + if (!hasAlarm) { + _u8g2.setFont(u8g2_font_wqy12_t_gb2312); + _u8g2.drawUTF8(2, 12, "人体状态"); + _u8g2.drawHLine(0, 15, 128); + } _u8g2.drawUTF8(4, 30, "有人:"); _u8g2.drawUTF8(60, 30, data.presence ? "V" : "X"); @@ -210,13 +240,20 @@ void DisplayManager::_drawPage2(const TelemetryData& data) { // Page 3: 安全信息 // ============================================================ -void DisplayManager::_drawPage3(const TelemetryData& data, bool buzzerOn, bool wifiOk) { - _u8g2.setFont(u8g2_font_wqy12_t_gb2312); - _u8g2.drawUTF8(2, 12, "安全信息"); - _u8g2.drawHLine(0, 15, 128); +void DisplayManager::_drawPage3(const TelemetryData& data, bool buzzerOn, + bool wifiOk, bool hasAlarm) { + if (!hasAlarm) { + _u8g2.setFont(u8g2_font_wqy12_t_gb2312); + _u8g2.drawUTF8(2, 12, "安全信息"); + _u8g2.drawHLine(0, 15, 128); + } _u8g2.drawUTF8(4, 30, "水浸:"); - _u8g2.drawUTF8(60, 30, data.rainDO ? "正常" : "!!报警"); + if (!data.rainValid) { + _u8g2.drawUTF8(60, 30, "离线"); + } else { + _u8g2.drawUTF8(60, 30, data.rainDO ? "正常" : "!!报警"); + } _u8g2.drawUTF8(4, 44, "蜂鸣器:"); _u8g2.drawUTF8(60, 44, buzzerOn ? "响" : "关"); diff --git a/src/main.cpp b/src/main.cpp index 72ec88c..d85963b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -36,10 +36,20 @@ uint32_t g_sequence = 1; // 上报序号 uint32_t g_bootTime = 0; // 启动时刻 // 本地报警跟踪 (防重复触发) -bool g_localFallActive = false; -bool g_localWaterActive = false; -bool g_localHeartAbnActive = false; -bool g_localBreathAbnActive = false; +bool g_localFallActive = false; +bool g_localWaterActive = false; +bool g_localHeartAbnActive = false; +bool g_localBreathAbnActive = false; +bool g_localTempAbnActive = false; +bool g_localHumiAbnActive = false; +bool g_localRadarFaultActive = false; +bool g_localDhtFaultActive = false; + +// 报警防抖计数器 (连续N次确认才触发) +uint8_t g_tempAbnCnt = 0; +uint8_t g_humiAbnCnt = 0; +uint8_t g_radarTimeoutCnt = 0; +#define ALARM_HYSTERESIS 3 // 连续3次确认才触发 // ============================================================ // 时间同步 @@ -226,53 +236,136 @@ TelemetryData buildTelemetry(const RadarRawData& radar) { } // ============================================================ -// 本地报警检查 (离线可用) +// 本地报警回调 → MQTT 上报 (连接时) +// ============================================================ + +void onLocalAlarmChange(uint8_t type, bool triggered, const char* desc) { + if (g_mqtt.isConnected()) { + g_mqtt.publishLocalAlarm(type, triggered, desc); + } +} + +// ============================================================ +// 本地报警检查 (离线可用) — 人身安全 > 环境 > 设备 // ============================================================ void checkLocalAlarms(const RadarRawData& radar) { - uint32_t now = millis(); + // ─── 前置条件: 有人才检测人体生命体征 ─── + bool humanPresent = radar.presence && radar.radarValid; - // --- 跌倒检测 (本地紧急报警, 立即触发) --- - if (radar.fall && radar.radarValid && !g_localFallActive) { + // ─── ★★★★★ 人身安全: 跌倒 (立即触发, 无防抖) ─── + if (radar.fall && humanPresent && !g_localFallActive) { g_localFallActive = true; - g_alarm.triggerLocal(AlarmManager::ALARM_FALL); + g_alarm.triggerLocal(AlarmManager::ALARM_FALL, "雷达检测到跌倒"); } else if (!radar.fall && g_localFallActive) { g_localFallActive = false; g_alarm.clearLocal(AlarmManager::ALARM_FALL); } - // --- 水浸检测 --- + // ─── ★★★★★ 人身安全: 心率异常 (有人才检测) ─── + if (humanPresent && radar.vitalValid && radar.heartRate > 0) { + bool heartAbn = (radar.heartRate < HEART_RATE_MIN || radar.heartRate > HEART_RATE_MAX); + if (heartAbn && !g_localHeartAbnActive) { + g_localHeartAbnActive = true; + char buf[32]; + snprintf(buf, sizeof(buf), "心率异常: %d bpm", radar.heartRate); + g_alarm.triggerLocal(AlarmManager::ALARM_HEART_ABNORMAL, buf); + } else if (!heartAbn && g_localHeartAbnActive) { + g_localHeartAbnActive = false; + g_alarm.clearLocal(AlarmManager::ALARM_HEART_ABNORMAL); + } + } else if (!humanPresent && g_localHeartAbnActive) { + // 人走了 — 清除之前的报警 + g_localHeartAbnActive = false; + g_alarm.clearLocal(AlarmManager::ALARM_HEART_ABNORMAL); + } + + // ─── ★★★★★ 人身安全: 呼吸异常 (有人才检测) ─── + if (humanPresent && radar.vitalValid && radar.breathRate > 0) { + bool breathAbn = (radar.breathRate < BREATH_RATE_MIN || radar.breathRate > BREATH_RATE_MAX); + if (breathAbn && !g_localBreathAbnActive) { + g_localBreathAbnActive = true; + char buf[32]; + snprintf(buf, sizeof(buf), "呼吸异常: %d 次/分", radar.breathRate); + g_alarm.triggerLocal(AlarmManager::ALARM_BREATH_ABNORMAL, buf); + } else if (!breathAbn && g_localBreathAbnActive) { + g_localBreathAbnActive = false; + g_alarm.clearLocal(AlarmManager::ALARM_BREATH_ABNORMAL); + } + } else if (!humanPresent && g_localBreathAbnActive) { + g_localBreathAbnActive = false; + g_alarm.clearLocal(AlarmManager::ALARM_BREATH_ABNORMAL); + } + + // ─── ★★★ 环境安全: 水浸 ─── bool waterLeak = !g_sensor.getRainDO() && g_sensor.isRainValid(); if (waterLeak && !g_localWaterActive) { g_localWaterActive = true; - g_alarm.triggerLocal(AlarmManager::ALARM_WATER_LEAK); + g_alarm.triggerLocal(AlarmManager::ALARM_WATER_LEAK, "水浸传感器触发"); } else if (!waterLeak && g_localWaterActive) { g_localWaterActive = false; g_alarm.clearLocal(AlarmManager::ALARM_WATER_LEAK); } - // --- 心率异常 (离线本地判定, 阈值预留后续调整) --- - if (radar.vitalValid && radar.heartRate > 0) { - bool heartAbn = (radar.heartRate < HEART_RATE_MIN || radar.heartRate > HEART_RATE_MAX); - if (heartAbn && !g_localHeartAbnActive) { - g_localHeartAbnActive = true; - g_alarm.triggerLocal(AlarmManager::ALARM_HEART_ABNORMAL); - } else if (!heartAbn && g_localHeartAbnActive) { - g_localHeartAbnActive = false; - g_alarm.clearLocal(AlarmManager::ALARM_HEART_ABNORMAL); + // ─── ★★ 环境舒适: 温度异常 (防抖3次) ─── + if (g_sensor.isDhtValid()) { + float temp = g_sensor.getTemp(); + bool tempAbn = (temp < TEMP_LOW || temp > TEMP_HIGH); + if (tempAbn) { + if (++g_tempAbnCnt >= ALARM_HYSTERESIS && !g_localTempAbnActive) { + g_localTempAbnActive = true; + char buf[32]; + snprintf(buf, sizeof(buf), "温度%.1f°C 超出范围", temp); + g_alarm.triggerLocal(AlarmManager::ALARM_TEMP_ABNORMAL, buf); + } + } else { + g_tempAbnCnt = 0; + if (g_localTempAbnActive) { + g_localTempAbnActive = false; + g_alarm.clearLocal(AlarmManager::ALARM_TEMP_ABNORMAL); + } + } + + // ─── ★★ 环境舒适: 湿度异常 (防抖3次) ─── + float humi = g_sensor.getHumi(); + bool humiAbn = (humi < HUMI_LOW || humi > HUMI_HIGH); + if (humiAbn) { + if (++g_humiAbnCnt >= ALARM_HYSTERESIS && !g_localHumiAbnActive) { + g_localHumiAbnActive = true; + char buf[32]; + snprintf(buf, sizeof(buf), "湿度%.1f%% 超出范围", humi); + g_alarm.triggerLocal(AlarmManager::ALARM_HUMI_ABNORMAL, buf); + } + } else { + g_humiAbnCnt = 0; + if (g_localHumiAbnActive) { + g_localHumiAbnActive = false; + g_alarm.clearLocal(AlarmManager::ALARM_HUMI_ABNORMAL); + } } } - // --- 呼吸异常 --- - if (radar.vitalValid && radar.breathRate > 0) { - bool breathAbn = (radar.breathRate < BREATH_RATE_MIN || radar.breathRate > BREATH_RATE_MAX); - if (breathAbn && !g_localBreathAbnActive) { - g_localBreathAbnActive = true; - g_alarm.triggerLocal(AlarmManager::ALARM_BREATH_ABNORMAL); - } else if (!breathAbn && g_localBreathAbnActive) { - g_localBreathAbnActive = false; - g_alarm.clearLocal(AlarmManager::ALARM_BREATH_ABNORMAL); + // ─── ★ 设备:雷达超时 (连续5秒无数据) ─── + if (g_radar.isTimeout()) { + if (++g_radarTimeoutCnt >= 5 && !g_localRadarFaultActive) { + g_localRadarFaultActive = true; + g_alarm.triggerLocal(AlarmManager::ALARM_SENSOR_FAULT, "雷达超时无数据"); } + } else { + g_radarTimeoutCnt = 0; + if (g_localRadarFaultActive) { + g_localRadarFaultActive = false; + g_alarm.clearLocal(AlarmManager::ALARM_SENSOR_FAULT); + } + } + + // ─── ★ 设备:DHT 持续故障 (已由 SensorManager 内部计数) ─── + if (g_sensor.isDhtFault() && !g_localDhtFaultActive && millis() > 60000) { + g_localDhtFaultActive = true; + g_alarm.triggerLocal(AlarmManager::ALARM_SENSOR_FAULT, "DHT11温湿度传感器故障"); + } else if (!g_sensor.isDhtFault() && g_localDhtFaultActive) { + g_localDhtFaultActive = false; + g_alarm.clearLocal(AlarmManager::ALARM_SENSOR_FAULT); } } @@ -370,6 +463,9 @@ void setup() { g_mqtt.onControlCommand(handleControlCommand); g_mqtt.onProvisionCommand(handleProvisionCommand); + // 注册本地报警回调 → MQTT 上报 + g_alarm.onAlarmChange(onLocalAlarmChange); + // 连接 MQTT if (g_wifi.isConnected()) { g_mqtt.connect(); @@ -427,15 +523,15 @@ void loop() { RadarRawData radar = g_radar.getData(); checkLocalAlarms(radar); - // --- 传感器故障检测 (每30秒检查一次,避免刷屏) --- - static uint32_t lastFaultCheck = 0; - if (now - lastFaultCheck > 30000) { - lastFaultCheck = now; - if (!g_sensor.isDhtValid() && millis() > 60000) { - Serial.println(F("[系统] ⚠️ DHT11 未检测到 (不影响运行)")); + // --- 传感器故障日志 (报警系统统一管理触发, 此处仅记录) --- + static uint32_t lastFaultLog = 0; + if (now - lastFaultLog > 60000) { + lastFaultLog = now; + if (!g_sensor.isDhtValid() && millis() > 60000 && !g_localDhtFaultActive) { + Serial.println(F("[系统] ⚠️ DHT11 读数异常")); } - if (g_radar.isTimeout()) { - Serial.println(F("[系统] ⚠️ 雷达超时无数据 (不影响运行)")); + if (g_radar.isTimeout() && !g_localRadarFaultActive) { + Serial.println(F("[系统] ⚠️ 雷达超时无数据")); } } @@ -452,7 +548,8 @@ void loop() { // 调试输出 + OLED 显示 printDebug(tele); - g_display.update(tele, wifiOk, g_alarm.isBuzzerOn()); + g_display.update(tele, wifiOk, g_alarm.isBuzzerOn(), + g_alarm.hasActiveAlarm(), g_alarm.getActiveAlarmStr()); } // --- 7. MQTT 配网状态 (WiFiManager 触发时) --- diff --git a/src/mqtt_manager.cpp b/src/mqtt_manager.cpp index ad81785..01afb19 100644 --- a/src/mqtt_manager.cpp +++ b/src/mqtt_manager.cpp @@ -371,6 +371,51 @@ void MQTTManager::publishProvisionResponse(const char* cmdId, bool success, _publish(MQTT_TOPIC_RESPONSE_PROVISION, payload.c_str(), STATUS_QOS); } +// ============================================================ +// 上行: 本地报警上报 (§4.7 扩展) +// 设备本地判定报警后立即上报,格式兼容服务端告警协议 +// ============================================================ + +void MQTTManager::publishLocalAlarm(uint8_t alarmType, bool triggered, const char* desc) { + if (!isConnected()) return; + + struct tm timeinfo; + char timeStr[24]; + if (getLocalTime(&timeinfo)) { + strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo); + } else { + snprintf(timeStr, sizeof(timeStr), "2026-01-01 00:00:00"); + } + + // 类型映射 + const char* typeStr = "unknown"; + switch (alarmType) { + case 1: typeStr = "fall"; break; // ALARM_FALL + case 2: typeStr = "water_leak"; break; // ALARM_WATER_LEAK + case 3: typeStr = "heart_abnormal"; break; // ALARM_HEART_ABNORMAL + case 4: typeStr = "breath_abnormal"; break; // ALARM_BREATH_ABNORMAL + case 5: typeStr = "temp_abnormal"; break; // ALARM_TEMP_ABNORMAL + case 6: typeStr = "humi_abnormal"; break; // ALARM_HUMI_ABNORMAL + case 7: typeStr = "sensor_fault"; break; // ALARM_SENSOR_FAULT + default: typeStr = "unknown"; break; + } + + JsonDocument doc; + doc["gateid"] = _gateid; + doc["type"] = triggered ? "alarm" : "alarm_restore"; + doc["time"] = timeStr; + doc["alarmType"] = typeStr; + doc["source"] = "local"; // 标识本地判定 + doc["desc"] = desc ? desc : ""; + + String payload; + serializeJson(doc, payload); + + // 通过 status 通道上报本地报警 + String topic = _buildTopic("status/%s/alarm"); + _mqtt.publish(topic.c_str(), payload.c_str()); +} + // ============================================================ // 回调注册 // ============================================================ diff --git a/src/sensor_manager.cpp b/src/sensor_manager.cpp index 338b0b4..e7f48a0 100644 --- a/src/sensor_manager.cpp +++ b/src/sensor_manager.cpp @@ -39,7 +39,7 @@ bool SensorManager::begin() { pinMode(WATER_SENSOR_PIN, INPUT_PULLUP); _lastRainRaw = (digitalRead(WATER_SENSOR_PIN) == HIGH); // HIGH=正常 _rainDO = _lastRainRaw; - _rainValid = true; + _rainValid = false; // 默认离线,直到检测到真正的状态变化才设为有效 // 首次读取 (DHT11 可能失败) read(); diff --git a/src/wifi_manager.cpp b/src/wifi_manager.cpp index f9a7e99..f33eecd 100644 --- a/src/wifi_manager.cpp +++ b/src/wifi_manager.cpp @@ -17,8 +17,8 @@ WiFiMgmt::WiFiMgmt() , _lastCheckTime(0) , _buttonPressTime(0) , _buttonPressed(false) - , _ledLastToggle(0) - , _ledState(false) + , _ledTaskHandle(nullptr) + , _ledMode(0) , _param_mqtt_server(nullptr) , _param_mqtt_port(nullptr) , _param_mqtt_user(nullptr) @@ -48,6 +48,9 @@ bool WiFiMgmt::begin() { pinMode(LED_WIFI_PIN, OUTPUT); digitalWrite(LED_WIFI_PIN, LOW); + // 启动 LED 独立任务 (避免 startConfigPortal 阻塞影响闪烁) + xTaskCreate(_ledTaskFunc, "WiFiLed", 1024, nullptr, 1, &_ledTaskHandle); + // 初始化 LittleFS if (!LittleFS.begin(true)) { Serial.println(F("[WiFi] ❌ LittleFS 挂载失败!")); @@ -71,11 +74,91 @@ bool WiFiMgmt::begin() { _wm.setDebugOutput(false); // 关闭调试输出 (避免干扰) _wm.setConfigPortalTimeout(WIFI_CONFIG_TIMEOUT); _wm.setConnectTimeout(WIFI_CONNECT_TIMEOUT); - _wm.setTitle("🏥 五劳无感康养监测 - 设备配网"); // 配网页面的中文标题 + _wm.setTitle("健康监测系统 - 设备配网"); _wm.setCustomHeadElement( "" ); WiFi.setAutoReconnect(true); // ESP32 自动重连 @@ -120,12 +203,12 @@ bool WiFiMgmt::begin() { if (!_connected) { // 如果自动连接失败, 启动配网页面 Serial.println(F("[WiFi] 自动连接失败, 启动配网模式...")); - _setLEDMode(1); // 快闪 = 配网模式 + _setLEDMode(2); // 快闪 = 配网模式 _connected = _connectWiFi(); // 内部会启动配网页面 } if (_connected) { - _setLEDMode(2); // 常亮 = 已连接 + _setLEDMode(3); // 常亮 = 已连接 } _lastCheckTime = millis(); @@ -150,25 +233,16 @@ bool WiFiMgmt::loop() { if (WiFi.status() != WL_CONNECTED) { _connected = false; - _setLEDMode(0); // 慢闪 = 未连接 + _setLEDMode(1); // 慢闪 = 未连接 Serial.println(F("[WiFi] ⚠️ WiFi 断开, 尝试重连...")); _connectWiFi(); } else { _connected = true; - _setLEDMode(2); // 常亮 - } - } - - // LED 闪烁处理 - if (!_connected) { - uint32_t interval = (_wm.getConfigPortalActive()) ? 300 : 1000; - if (millis() - _ledLastToggle > interval) { - _ledLastToggle = millis(); - _ledState = !_ledState; - digitalWrite(LED_WIFI_PIN, _ledState ? HIGH : LOW); + _setLEDMode(3); // 常亮 = 已连接 } } + // LED 由独立 FreeRTOS 任务处理 (不受 startConfigPortal 阻塞影响) return _connected; } @@ -213,11 +287,11 @@ bool WiFiMgmt::_connectWiFi() { void WiFiMgmt::startConfigPortal() { Serial.println(F("[WiFi] 🔧 手动启动配网页面...")); - _setLEDMode(1); + _setLEDMode(2); // 快闪 = 配网模式 _wm.startConfigPortal(WIFI_AP_SSID, WIFI_AP_PASSWORD); _connected = (WiFi.status() == WL_CONNECTED); if (_connected) { - _setLEDMode(2); + _setLEDMode(3); // 常亮 = 已连接 } } @@ -393,20 +467,51 @@ const char* WiFiMgmt::getEnvNote() const { } // ============================================================ -// LED 模式控制 +// LED 模式控制 (线程安全, 仅写模式变量,由独立任务执行) // ============================================================ void WiFiMgmt::_setLEDMode(uint8_t mode) { - // mode 由 loop() 中的闪烁逻辑处理 - // 0=慢闪 1=快闪 2=常亮 - switch (mode) { - case 0: - case 1: - // 闪烁由 loop 处理 - break; - case 2: - digitalWrite(LED_WIFI_PIN, HIGH); - _ledState = true; - break; + _ledMode = mode; // uint8_t 赋值在 ESP32 上是原子的 +} + +// ============================================================ +// LED 闪烁独立任务 (FreeRTOS) +// 独立于主循环,startConfigPortal 阻塞期间也能正常闪烁 +// ============================================================ + +void WiFiMgmt::_ledTaskFunc(void* param) { + const TickType_t fastMs = pdMS_TO_TICKS(300); // 快闪 = 配网模式 + const TickType_t slowMs = pdMS_TO_TICKS(1000); // 慢闪 = 未连接 + + while (true) { + if (!_instance) { + vTaskDelay(slowMs); + continue; + } + + uint8_t mode = _instance->_ledMode; + + switch (mode) { + case 0: // 灭 + digitalWrite(LED_WIFI_PIN, LOW); + vTaskDelay(slowMs); + break; + case 1: // 慢闪 — WiFi 断连 + digitalWrite(LED_WIFI_PIN, HIGH); + vTaskDelay(slowMs); + digitalWrite(LED_WIFI_PIN, LOW); + vTaskDelay(slowMs); + break; + case 2: // 快闪 — 配网模式 + digitalWrite(LED_WIFI_PIN, HIGH); + vTaskDelay(fastMs); + digitalWrite(LED_WIFI_PIN, LOW); + vTaskDelay(fastMs); + break; + case 3: // 常亮 — 已连接 + digitalWrite(LED_WIFI_PIN, HIGH); + vTaskDelay(slowMs); + break; + } } }