1	/**
2	 * @file mqtt_manager.h
3	 * @brief MQTT 通信管理器 (协议规范 V0.08)
4	 *
5	 * 上行: report/allpoints, status/{gateid}/online, status/{gateid}/offline,
6	 *       status/{gateid}/provision, response/{gateid}/cmd, response/{gateid}/provision
7	 * 下行: cmd/{gateid}/alarm, cmd/{gateid}/alarm/restore,
8	 *       cmd/{gateid}/control, cmd/{gateid}/provision/wifi
9	 */
10
11	#ifndef MQTT_MANAGER_H
12	#define MQTT_MANAGER_H
13
14	#include <Arduino.h>
15	#include <WiFi.h>
16	#include <PubSubClient.h>
17	#include <ArduinoJson.h>
18	#include "config.h"
19
20	// ============================================================
21	// 回调函数类型
22	// ============================================================
23
24	// 告警指令回调 (alarm / alarm_restore)
25	typedef void (*MQTTAlarmCallback)(const AlarmCommand& cmd);
26
27	// 控制指令回调
28	typedef void (*MQTTControlCallback)(const ControlCommand& cmd);
29
30	// 配网指令回调 (WiFi 凭证下发)
31	typedef void (*MQTTProvisionCallback)(const ProvisionCommand& cmd);
32
33	// ============================================================
34	// MQTTManager 类
35	// ============================================================
36
37	class MQTTManager {
38	public:
39	    MQTTManager();
40	    ~MQTTManager();
41
42	    /** 初始化 */
43	    bool begin(const char* gateid);
44
45	    /** 主循环 */
46	    void loop();
47
48	    /** 连接/断开 */
49	    bool connect();
50	    void disconnect();
51	    bool isConnected();
52
53	    /** 设置服务器 */
54	    void setServer(const char* host, uint16_t port,
55	                   const char* user, const char* pass);
56
57	    // ============================================================
58	    // 上行: 发布方法 (协议规范 §4)
59	    // ============================================================
60
61	    /** 全量遥测上报 report/allpoints (§4.1) */
62	    void publishAllPoints(const TelemetryData& data, uint32_t sequence);
63
64	    /** 设备上线通知 status/{gateid}/online (§4.2) */
65	    void publishOnline();
66
67	    /** 设备离线通知 status/{gateid}/offline (§4.3) — 主动调用 */
68	    void publishOffline();
69
70	    /** 配网状态通知 status/{gateid}/provision (§4.4) */
71	    void publishProvisionStatus(const char* state, const char* bleName);
72
73	    /** 指令执行响应 response/{gateid}/cmd (§4.5) */
74	    void publishCmdResponse(const char* cmdId, const char* cmdType,
75	                            const char* alarmType, bool success,
76	                            const char* errorCode, const char* errorMsg);
77
78	    /** 配网指令响应 response/{gateid}/provision (§4.6) */
79	    void publishProvisionResponse(const char* cmdId, bool success,
80	                                  const char* errorCode, const char* errorMsg,
81	                                  const char* wifiSignal = "");
82
83	    // ============================================================
84	    // 回调注册 (协议规范 §5)
85	    // ============================================================
86
87	    void onAlarmCommand(MQTTAlarmCallback cb);
88	    void onControlCommand(MQTTControlCallback cb);
89	    void onProvisionCommand(MQTTProvisionCallback cb);
90
91	    /** 获取 gateid */
92	    const char* getGateId() const { return _gateid; }
93
94	private:
95	    static void _mqttCallback(char* topic, byte* payload, unsigned int length);
96	    static MQTTManager* _instance;
97
98	    // 主题构建
99	    String _buildTopic(const char* tmpl) const;
100
101	    // 下行报文解析
102	    void _handleMessage(const char* topic, const char* payload);
103	    void _parseAlarm(const char* payload, bool isRestore);
104	    void _parseControl(const char* payload);
105	    void _parseProvision(const char* payload);
106
107	    // 发布辅助
108	    bool _publish(const char* topicTmpl, const char* payload, uint8_t qos, bool retain = false);
109
110	    WiFiClient      _wifiClient;
111	    PubSubClient    _mqtt;
112	    char            _gateid[16];
113
114	    // 服务器配置
115	    char     _server[64];
116	    uint16_t _port;
117	    char     _user[32];
118	    char     _pass[32];
119
120	    // 重连
121	    uint32_t _reconnectInterval;
122	    uint32_t _lastReconnectAttempt;
123	    bool     _connected;
124
125	    // 回调
126	    MQTTAlarmCallback     _alarmCb;
127	    MQTTControlCallback   _controlCb;
128	    MQTTProvisionCallback _provisionCb;
129	};
130
131	#endif // MQTT_MANAGER_H
132

/**
 * @file mqtt_manager.h
 * @brief MQTT 通信管理器 (协议规范 V0.08)
 *
 * 上行: report/allpoints, status/{gateid}/online, status/{gateid}/offline,
 *       status/{gateid}/provision, response/{gateid}/cmd, response/{gateid}/provision
 * 下行: cmd/{gateid}/alarm, cmd/{gateid}/alarm/restore,
 *       cmd/{gateid}/control, cmd/{gateid}/provision/wifi
 */

#ifndef MQTT_MANAGER_H
#define MQTT_MANAGER_H

#include <Arduino.h>
#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include "config.h"

// ============================================================
// 回调函数类型
// ============================================================

// 告警指令回调 (alarm / alarm_restore)
typedef void (*MQTTAlarmCallback)(const AlarmCommand& cmd);

// 控制指令回调
typedef void (*MQTTControlCallback)(const ControlCommand& cmd);

// 配网指令回调 (WiFi 凭证下发)
typedef void (*MQTTProvisionCallback)(const ProvisionCommand& cmd);

// ============================================================
// MQTTManager 类
// ============================================================

class MQTTManager {
public:
    MQTTManager();
    ~MQTTManager();

    /** 初始化 */
    bool begin(const char* gateid);

    /** 主循环 */
    void loop();

    /** 连接/断开 */
    bool connect();
    void disconnect();
    bool isConnected();

    /** 设置服务器 */
    void setServer(const char* host, uint16_t port,
                   const char* user, const char* pass);

    // ============================================================
    // 上行: 发布方法 (协议规范 §4)
    // ============================================================

    /** 全量遥测上报 report/allpoints (§4.1) */
    void publishAllPoints(const TelemetryData& data, uint32_t sequence);

    /** 设备上线通知 status/{gateid}/online (§4.2) */
    void publishOnline();

    /** 设备离线通知 status/{gateid}/offline (§4.3) — 主动调用 */
    void publishOffline();

    /** 配网状态通知 status/{gateid}/provision (§4.4) */
    void publishProvisionStatus(const char* state, const char* bleName);

    /** 指令执行响应 response/{gateid}/cmd (§4.5) */
    void publishCmdResponse(const char* cmdId, const char* cmdType,
                            const char* alarmType, bool success,
                            const char* errorCode, const char* errorMsg);

    /** 配网指令响应 response/{gateid}/provision (§4.6) */
    void publishProvisionResponse(const char* cmdId, bool success,
                                  const char* errorCode, const char* errorMsg,
                                  const char* wifiSignal = "");

    // ============================================================
    // 回调注册 (协议规范 §5)
    // ============================================================

    void onAlarmCommand(MQTTAlarmCallback cb);
    void onControlCommand(MQTTControlCallback cb);
    void onProvisionCommand(MQTTProvisionCallback cb);

    /** 获取 gateid */
    const char* getGateId() const { return _gateid; }

private:
    static void _mqttCallback(char* topic, byte* payload, unsigned int length);
    static MQTTManager* _instance;

    // 主题构建
    String _buildTopic(const char* tmpl) const;

    // 下行报文解析
    void _handleMessage(const char* topic, const char* payload);
    void _parseAlarm(const char* payload, bool isRestore);
    void _parseControl(const char* payload);
    void _parseProvision(const char* payload);

    // 发布辅助
    bool _publish(const char* topicTmpl, const char* payload, uint8_t qos, bool retain = false);

    WiFiClient      _wifiClient;
    PubSubClient    _mqtt;
    char            _gateid[16];

    // 服务器配置
    char     _server[64];
    uint16_t _port;
    char     _user[32];
    char     _pass[32];

    // 重连
    uint32_t _reconnectInterval;
    uint32_t _lastReconnectAttempt;
    bool     _connected;

    // 回调
    MQTTAlarmCallback     _alarmCb;
    MQTTControlCallback   _controlCb;
    MQTTProvisionCallback _provisionCb;
};

#endif // MQTT_MANAGER_H


I see that MQTTManager doesn't have a `publishAlarmEvent` method. I need to add one. Let me also look at the report/allpoints format to understand how alarms should be reported - they should probably be embedded in the telemetry data or sent as a separate topic.

Actually, looking at the protocol, the best approach is to add an alarm field to the telemetry data or publish it as a separate message. Let me add a simple method `publishLocalAlarm` to MQTTManager that publishes a local alarm event.

Wait, but the user might not want me to change too many files. Let me think about the simplest approach.

Looking at the protocol, `report/allpoints` already includes telemetry data. The alarm status is implicit in the data (fall=1, rainDO=0, heartRate outside range, etc.). The server should be able to detect alarms from the telemetry data itself.

But for local alarms to be properly reported, I can add the alarm info to the telemetry or simply publish a status message. Let me add a `publishAlarmEvent` method to MQTTManager.

Actually, let me keep it simpler. I can:
1. Add `publishLocalAlarm` to MQTTManager
2. In main.cpp, register the alarm callback that calls `g_mqtt.publishLocalAlarm()`

Let me add the method to mqtt_manager.h and mqtt_manager.cpp.

Looking at the existing publish methods, I'll add something like:
```cpp
void publishLocalAlarm(uint8_t alarmType, bool triggered, const char* desc);
```

This will publish to a topic like `status/{gateid}/alarm` with JSON payload.

OK let me now do all the changes. I'll update:
1. mqtt_manager.h - add publishLocalAlarm declaration
2. mqtt_manager.cpp - add publishLocalAlarm implementation
3. main.cpp - expand tracking variables, checkLocalAlarms, register alarm callback

Let me start.


MQTT 管理器缺少本地报警上报接口，先补上。


Edit

    /** 配网指令响应 response/{gateid}/provision (§4.6) */
    void publishProvisionResponse(const char* cmdId, bool success,
                                  const char* errorCode, const char* errorMsg,
                                  const char* wifiSignal = "");

    /** 配网指令响应 response/{gateid}/provision (§4.6) */
    void publishProvisionResponse(const char* cmdId, bool success,
                                  const char* errorCode, const char* errorMsg,
                                  const char* wifiSignal = "");

    /** 本地报警上报 status/{gateid}/alarm (§4.7 扩展) */
    void publishLocalAlarm(uint8_t alarmType, bool triggered, const char* desc);


The file d:\PlatformIO1\Projects\Healthv2.0\include\mqtt_manager.h has been updated successfully. (file state is current in your context — no need to Read it back)

    /** 配网指令响应 response/{gateid}/provision (§4.6) */
    void publishProvisionResponse(const char* cmdId, bool success,
                                  const char* errorCode, const char* errorMsg,
                                  const char* wifiSignal = "");

    /** 本地报警上报 status/{gateid}/alarm (§4.7 扩展) */
    void publishLocalAlarm(uint8_t alarmType, bool triggered, const char* desc);

/**
 * @file mqtt_manager.h
 * @brief MQTT 通信管理器 (协议规范 V0.08)
 *
 * 上行: report/allpoints, status/{gateid}/online, status/{gateid}/offline,
 *       status/{gateid}/provision, response/{gateid}/cmd, response/{gateid}/provision
 * 下行: cmd/{gateid}/alarm, cmd/{gateid}/alarm/restore,
 *       cmd/{gateid}/control, cmd/{gateid}/provision/wifi
 */

#ifndef MQTT_MANAGER_H
#define MQTT_MANAGER_H

#include <Arduino.h>
#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include "config.h"

// ============================================================
// 回调函数类型
// ============================================================

// 告警指令回调 (alarm / alarm_restore)
typedef void (*MQTTAlarmCallback)(const AlarmCommand& cmd);

// 控制指令回调
typedef void (*MQTTControlCallback)(const ControlCommand& cmd);

// 配网指令回调 (WiFi 凭证下发)
typedef void (*MQTTProvisionCallback)(const ProvisionCommand& cmd);

// ============================================================
// MQTTManager 类
// ============================================================

class MQTTManager {
public:
    MQTTManager();
    ~MQTTManager();

    /** 初始化 */
    bool begin(const char* gateid);

    /** 主循环 */
    void loop();

    /** 连接/断开 */
    bool connect();
    void disconnect();
    bool isConnected();

    /** 设置服务器 */
    void setServer(const char* host, uint16_t port,
                   const char* user, const char* pass);

    // ============================================================
    // 上行: 发布方法 (协议规范 §4)
    // ============================================================

    /** 全量遥测上报 report/allpoints (§4.1) */
    void publishAllPoints(const TelemetryData& data, uint32_t sequence);

    /** 设备上线通知 status/{gateid}/online (§4.2) */
    void publishOnline();

    /** 设备离线通知 status/{gateid}/offline (§4.3) — 主动调用 */
    void publishOffline();

    /** 配网状态通知 status/{gateid}/provision (§4.4) */
    void publishProvisionStatus(const char* state, const char* bleName);

    /** 指令执行响应 response/{gateid}/cmd (§4.5) */
    void publishCmdResponse(const char* cmdId, const char* cmdType,
                            const char* alarmType, bool success,
                            const char* errorCode, const char* errorMsg);

    /** 配网指令响应 response/{gateid}/provision (§4.6) */
    void publishProvisionResponse(const char* cmdId, bool success,
                                  const char* errorCode, const char* errorMsg,
                                  const char* wifiSignal = "");

    // ============================================================
    // 回调注册 (协议规范 §5)
    // ============================================================

    void onAlarmCommand(MQTTAlarmCallback cb);
    void onControlCommand(MQTTControlCallback cb);
    void onProvisionCommand(MQTTProvisionCallback cb);

    /** 获取 gateid */
    const char* getGateId() const { return _gateid; }

private:
    static void _mqttCallback(char* topic, byte* payload, unsigned int length);
    static MQTTManager* _instance;

    // 主题构建
    String _buildTopic(const char* tmpl) const;

    // 下行报文解析
    void _handleMessage(const char* topic, const char* payload);
    void _parseAlarm(const char* payload, bool isRestore);
    void _parseControl(const char* payload);
    void _parseProvision(const char* payload);

    // 发布辅助
    bool _publish(const char* topicTmpl, const char* payload, uint8_t qos, bool retain = false);

    WiFiClient      _wifiClient;
    PubSubClient    _mqtt;
    char            _gateid[16];

    // 服务器配置
    char     _server[64];
    uint16_t _port;
    char     _user[32];
    char     _pass[32];

    // 重连
    uint32_t _reconnectInterval;
    uint32_t _lastReconnectAttempt;
    bool     _connected;

    // 回调
    MQTTAlarmCallback     _alarmCb;
    MQTTControlCallback   _controlCb;
    MQTTProvisionCallback _provisionCb;
};

#endif // MQTT_MANAGER_H

+    /** 本地报警上报 status/{gateid}/alarm (§4.7 扩展) */

+    void publishLocalAlarm(uint8_t alarmType, bool triggered, const char* desc);

// 回调注册 (协议规范 §5)


不，报警逻辑以人体的危险为先


The user said "不，报警逻辑以人体的危险为先" — "No, alarm logic should prioritize human danger first".

This means:
- **Human-related alarms (highest priority)**: Fall, heart rate abnormal, breath rate abnormal — these directly threaten human life
- **Environmental alarms (medium priority)**: Water leak, temperature abnormal, humidity abnormal — these are environment issues
- **Equipment alarms (lowest priority)**: Sensor faults, radar timeout — these are equipment issues

So the priority should be reordered:
1. ALARM_FALL — life-threatening ★★★★★
2. ALARM_HEART_ABNORMAL — life-threatening ★★★★★
3. ALARM_BREATH_ABNORMAL — life-threatening ★★★★★
4. ALARM_WATER_LEAK — safety hazard ★★★
5. ALARM_TEMP_ABNORMAL — comfort/environment ★★
6. ALARM_HUMI_ABNORMAL — comfort/environment ★★
7. ALARM_SENSOR_FAULT — equipment ★

The user is telling me to prioritize human vital signs and fall over environmental/equipment issues. Let me adjust the priority in alarm_manager.cpp.

Also, the user hasn't seen my MQTT and main.cpp changes yet since they sent this message mid-turn. Let me first fix the priority, then continue with the rest of the changes.

Let me update the priority function and also reorder the enum.


明白——报警优先级应该以人身安全为先：**跌倒/心率/呼吸 > 环境 > 设备故障**。我来调整优先级，然后继续完成整个扩展。


// ============================================================
// 优先级定义 (数值越小越紧急)
// ============================================================

uint8_t AlarmManager::_priority(AlarmType t) {
    switch (t) {
    case ALARM_FALL:             return 0;
    case ALARM_WATER_LEAK:       return 1;
    case ALARM_HEART_ABNORMAL:   return 2;
    case ALARM_BREATH_ABNORMAL:  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;
    }
}

// ============================================================
// 优先级定义 — 人身安全 > 环境安全 > 设备故障
// ============================================================

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;
    }
}


现在说拓展所有的报警逻辑


The file d:\PlatformIO1\Projects\Healthv2.0\src\alarm_manager.cpp has been updated successfully. (file state is current in your context — no need to Read it back)

// ============================================================
// 优先级定义 (数值越小越紧急)
// ============================================================

uint8_t AlarmManager::_priority(AlarmType t) {
    switch (t) {
    case ALARM_FALL:             return 0;
    case ALARM_WATER_LEAK:       return 1;
    case ALARM_HEART_ABNORMAL:   return 2;
    case ALARM_BREATH_ABNORMAL:  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;
    }
}

// ============================================================
// 优先级定义 — 人身安全 > 环境安全 > 设备故障
// ============================================================

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;
    }
}

-// 优先级定义 (数值越小越紧急)

+// 优先级定义 — 人身安全 > 环境安全 > 设备故障

+    // ★★★★★ 人身安全 — 直接威胁生命

-    case ALARM_WATER_LEAK:       return 1;

-    case ALARM_HEART_ABNORMAL:   return 2;

-    case ALARM_BREATH_ABNORMAL:  return 2;

+    case ALARM_HEART_ABNORMAL:   return 1;

+    case ALARM_BREATH_ABNORMAL:  return 1;

+    // ★★★ 环境安全 — 潜在危险

+    case ALARM_WATER_LEAK:       return 2;

+    // ★★ 环境舒适 — 非紧急

+    // ★ 设备状态 — 仅提示


Good, now let me also update the header enum order to reflect the human-first priority. And update the _calcBuzzerParams to match — heart/breath should be at the same urgency as fall, water leak a bit less, etc.

Actually the header enum doesn't need to be in priority order — it's just a list. But the buzzer params should reflect the urgency. Let me update the buzzer params.

For human danger (fall, heart, breath):
- Fall: continuous rapid beep (80ms on, 80ms off) — same
- Heart/breath: rapid beeping, 5 beeps (100ms on, 100ms off) — more urgent than before
- Water leak: 3 beeps (150ms on, 100ms off) — medium
- Temp/humi: 2 slow beeps (200ms on, 400ms off) — gentle
- Sensor fault: 1 long beep (500ms on, 2s off) — notification only

Let me update the buzzer params.


同步更新蜂鸣器模式——人体危险报警也要更急：


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 = 80; offMs = 80; pauseMs = 0;   // 连续急哔
        break;
    case AlarmManager::ALARM_WATER_LEAK:
        beeps = 5; onMs = 100; offMs = 100; pauseMs = 500; // 5连哔
        break;
    case AlarmManager::ALARM_HEART_ABNORMAL:
    case AlarmManager::ALARM_BREATH_ABNORMAL:
        beeps = 3; onMs = 150; offMs = 100; pauseMs = 600; // 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 = 500; offMs = 2000; pauseMs = 0;  // 单声长哔
        break;
    default:
        beeps = 0; onMs = 500; offMs = 500; pauseMs = 0;   // 持续
        break;
    }
}

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;
    }
}


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 = 80; offMs = 80; pauseMs = 0;   // 连续急哔
        break;
    case AlarmManager::ALARM_WATER_LEAK:
        beeps = 5; onMs = 100; offMs = 100; pauseMs = 500; // 5连哔
        break;
    case AlarmManager::ALARM_HEART_ABNORMAL:
    case AlarmManager::ALARM_BREATH_ABNORMAL:
        beeps = 3; onMs = 150; offMs = 100; pauseMs = 600; // 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 = 500; offMs = 2000; pauseMs = 0;  // 单声长哔
        break;
    default:
        beeps = 0; onMs = 500; offMs = 500; pauseMs = 0;   // 持续
        break;
    }
}

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;
    }
}

+    // 人身安全 — 连速急哔 (最刺耳)

-        beeps = 0; onMs = 80; offMs = 80; pauseMs = 0;   // 连续急哔

+        beeps = 0; onMs = 60; offMs = 60; pauseMs = 0;      // 跌倒: 连速哔

-    case AlarmManager::ALARM_WATER_LEAK:

-        beeps = 5; onMs = 100; offMs = 100; pauseMs = 500; // 5连哔

-        beeps = 3; onMs = 150; offMs = 100; pauseMs = 600; // 3连哔

+        beeps = 5; onMs = 80; offMs = 80; pauseMs = 400;    // 心率/呼吸: 5连急哔

+    // 环境安全 — 次急

+    case AlarmManager::ALARM_WATER_LEAK:

+        beeps = 3; onMs = 120; offMs = 100; pauseMs = 500;  // 水浸: 3连哔

+    // 环境舒适 — 温和

-        beeps = 2; onMs = 200; offMs = 400; pauseMs = 1200; // 2声间隔

+        beeps = 2; onMs = 200; offMs = 400; pauseMs = 1200; // 温湿度: 2声长停

+    // 设备状态 — 仅提示

-        beeps = 1; onMs = 500; offMs = 2000; pauseMs = 0;  // 单声长哔

+        beeps = 1; onMs = 300; offMs = 3000; pauseMs = 0;   // 传感器: 单声提醒

-        beeps = 0; onMs = 500; offMs = 500; pauseMs = 0;   // 持续

+        beeps = 0; onMs = 500; offMs = 500; pauseMs = 0;    // 持续


Good. Now let me update the header comment to reflect the priority change, and then update the file header comment to match the new priority.

Actually, the file header comment was already updated when I rewrote it. Let me check...

Looking at alarm_manager.cpp line 5-9:
```
 * 蜂鸣模式 (按最高优先级报警):
 *   跌倒:     连速哔 (80ms响/80ms停) — 最急
 *   水浸:     5连哔停顿 (100ms/100ms) — 次急
 *   心率/呼吸: 3连哔停顿 (150ms/100ms) — 中等
 *   温湿度:    2连哔长停 (200ms/400ms) — 温和
 *   传感器故障: 单声长哔 (500ms/2s) — 仅提示
```

This needs updating to reflect the new values. Let me fix it.


更新文件头注释：


 * 蜂鸣模式 (按最高优先级报警):
 *   跌倒:     连速哔 (80ms响/80ms停) — 最急
 *   水浸:     5连哔停顿 (100ms/100ms) — 次急
 *   心率/呼吸: 3连哔停顿 (150ms/100ms) — 中等
 *   温湿度:    2连哔长停 (200ms/400ms) — 温和
 *   传感器故障: 单声长哔 (500ms/2s) — 仅提示

 * 蜂鸣模式 (人身安全 > 环境 > 设备):
 *   跌倒:     连速哔 (60ms/60ms) — 最急 ★★★★★
 *   心率/呼吸: 5连急哔 (80ms/80ms) — 次急 ★★★★★
 *   水浸:     3连哔 (120ms/100ms) — 中等 ★★★
 *   温湿度:   2声长停 (200ms/400ms) — 温和 ★★
 *   传感器:   单声长哔 (300ms/3s) — 提示 ★


 * 蜂鸣模式 (按最高优先级报警):
 *   跌倒:     连速哔 (80ms响/80ms停) — 最急
 *   水浸:     5连哔停顿 (100ms/100ms) — 次急
 *   心率/呼吸: 3连哔停顿 (150ms/100ms) — 中等
 *   温湿度:    2连哔长停 (200ms/400ms) — 温和
 *   传感器故障: 单声长哔 (500ms/2s) — 仅提示

 * 蜂鸣模式 (人身安全 > 环境 > 设备):
 *   跌倒:     连速哔 (60ms/60ms) — 最急 ★★★★★
 *   心率/呼吸: 5连急哔 (80ms/80ms) — 次急 ★★★★★
 *   水浸:     3连哔 (120ms/100ms) — 中等 ★★★
 *   温湿度:   2声长停 (200ms/400ms) — 温和 ★★
 *   传感器:   单声长哔 (300ms/3s) — 提示 ★

* @brief 报警管理器 - 多类型分优先级实现

- * 蜂鸣模式 (按最高优先级报警):

- *   传感器故障: 单声长哔 (500ms/2s) — 仅提示

+ * 蜂鸣模式 (人身安全 > 环境 > 设备):

+ *   跌倒:     连速哔 (60ms/60ms) — 最急 ★★★★★

+ *   心率/呼吸: 5连急哔 (80ms/80ms) — 次急 ★★★★★

+ *   水浸:     3连哔 (120ms/100ms) — 中等 ★★★


扩展 AlarmManager：新报警类型 + 优先级蜂鸣 + 回调

扩展 main.cpp：温湿度/传感器故障检测 + MQTT上报 + 防抖

OLED 显示活跃报警提示


Good, now I need to continue with the MQTT publishLocalAlarm implementation and the main.cpp expansion. Let me pick up where I left off. Let me implement publishLocalAlarm in mqtt_manager.cpp, then update main.cpp.


继续，先补 MQTT `publishLocalAlarm` 实现：


140	    return _connected && _mqtt.connected();
141	}
142
143	// ============================================================
144	// 主循环
145	// ============================================================
146
147	void MQTTManager::loop() {
148	    if (!_connected) {
149	        uint32_t now = millis();
150	        if (now - _lastReconnectAttempt >= _reconnectInterval) {
151	            _lastReconnectAttempt = now;
152	            if (connect()) {
153	                _reconnectInterval = MQTT_RECONNECT_MIN;
154	                publishOnline();  // 上线通知
155	            } else {
156	                _reconnectInterval = min(_reconnectInterval * 2,
157	                                        (uint32_t)MQTT_RECONNECT_MAX);
158	            }
159	        }
160	    } else {
161	        _mqtt.loop();
162	    }
163	}
164
165	// ============================================================
166	// 上行: 全量遥测上报 (§4.1)
167	// ============================================================
168
169	void MQTTManager::publishAllPoints(const TelemetryData& d, uint32_t sequence) {
170	    if (!isConnected()) return;
171
172	    // 构建时间字符串
173	    struct tm timeinfo;
174	    char timeStr[24];
175	    if (getLocalTime(&timeinfo)) {
176	        strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo);
177	    } else {
178	        snprintf(timeStr, sizeof(timeStr), "2026-01-01 00:00:00");
179	    }
180
181	    // 构建 JSON (协议规范 §4.1)
182	    JsonDocument doc;
183	    doc["saleid"]   = d.saleid;
184	    doc["gateid"]   = d.gateid;
185	    doc["type"]     = "report";
186	    doc["time"]     = timeStr;
187	    doc["sequence"] = String(sequence);
188	    doc["source"]   = "da";
189
190	    JsonArray meter = doc["meter"].to<JsonArray>();
191	    JsonObject m    = meter.add<JsonObject>();
192	    m["id"]     = d.meterId;
193	    m["status"] = "1";
194	    m["name"]   = d.meterName;
195
196	    JsonObject v = m["values"].to<JsonObject>();
197	    // 环境数据
198	    v["Temp"]    = String(d.temp, 1);
199	    v["Humi"]    = String(d.humi, 1);
200	    v["DhtValid"] = d.dhtValid ? "1" : "0";
201	    // 雷达存在/运动
202	    v["Presence"]  = d.presence ? "1" : "0";
203	    v["Motion"]    = d.motion ? "1" : "0";
204	    v["Fall"]      = d.fall ? "1" : "0";
205	    v["Resident"]  = d.resident ? "1" : "0";
206	    // 生命体征
207	    v["Body"]       = String(d.body);
208	    v["HeartRate"]  = String(d.heartRate);
209	    v["BreathRate"] = String(d.breathRate);
210	    v["RadarValid"] = d.radarValid ? "1" : "0";
211	    v["VitalValid"] = d.vitalValid ? "1" : "0";
212	    // 水浸
213	    v["RainDO"]    = d.rainDO ? "1" : "0";
214	    v["RainValid"] = d.rainValid ? "1" : "0";
215	    // 设备状态
216	    v["WifiReady"]   = d.wifiReady ? "1" : "0";
217	    v["UptimeSec"]   = String(d.uptimeSec);
218	    v["BuzzerState"] = d.buzzerState ? "1" : "0";
219

    return _connected && _mqtt.connected();
}

// ============================================================
// 主循环
// ============================================================

void MQTTManager::loop() {
    if (!_connected) {
        uint32_t now = millis();
        if (now - _lastReconnectAttempt >= _reconnectInterval) {
            _lastReconnectAttempt = now;
            if (connect()) {
                _reconnectInterval = MQTT_RECONNECT_MIN;
                publishOnline();  // 上线通知
            } else {
                _reconnectInterval = min(_reconnectInterval * 2,
                                        (uint32_t)MQTT_RECONNECT_MAX);
            }
        }
    } else {
        _mqtt.loop();
    }
}

// ============================================================
// 上行: 全量遥测上报 (§4.1)
// ============================================================

void MQTTManager::publishAllPoints(const TelemetryData& d, uint32_t sequence) {
    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");
    }

    // 构建 JSON (协议规范 §4.1)
    JsonDocument doc;
    doc["saleid"]   = d.saleid;
    doc["gateid"]   = d.gateid;
    doc["type"]     = "report";
    doc["time"]     = timeStr;
    doc["sequence"] = String(sequence);
    doc["source"]   = "da";

    JsonArray meter = doc["meter"].to<JsonArray>();
    JsonObject m    = meter.add<JsonObject>();
    m["id"]     = d.meterId;
    m["status"] = "1";
    m["name"]   = d.meterName;

    JsonObject v = m["values"].to<JsonObject>();
    // 环境数据
    v["Temp"]    = String(d.temp, 1);
    v["Humi"]    = String(d.humi, 1);
    v["DhtValid"] = d.dhtValid ? "1" : "0";
    // 雷达存在/运动
    v["Presence"]  = d.presence ? "1" : "0";
    v["Motion"]    = d.motion ? "1" : "0";
    v["Fall"]      = d.fall ? "1" : "0";
    v["Resident"]  = d.resident ? "1" : "0";
    // 生命体征
    v["Body"]       = String(d.body);
    v["HeartRate"]  = String(d.heartRate);
    v["BreathRate"] = String(d.breathRate);
    v["RadarValid"] = d.radarValid ? "1" : "0";
    v["VitalValid"] = d.vitalValid ? "1" : "0";
    // 水浸
    v["RainDO"]    = d.rainDO ? "1" : "0";
    v["RainValid"] = d.rainValid ? "1" : "0";
    // 设备状态
    v["WifiReady"]   = d.wifiReady ? "1" : "0";
    v["UptimeSec"]   = String(d.uptimeSec);
    v["BuzzerState"] = d.buzzerState ? "1" : "0";


Let me find a good place to add the publishLocalAlarm method. Let me look further to find where publish methods end.


220	    // 序列化
221	    String payload;
222	    serializeJson(doc, payload);
223
224	    // 发布到 report/allpoints (QoS 0)
225	    _mqtt.publish("report/allpoints", payload.c_str());
226	}
227
228	// ============================================================
229	// 上行: 上线通知 (§4.2)
230	// ============================================================
231
232	void MQTTManager::publishOnline() {
233	    if (!isConnected()) return;
234
235	    struct tm timeinfo;
236	    char timeStr[24];
237	    if (getLocalTime(&timeinfo)) {
238	        strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo);
239	    } else {
240	        snprintf(timeStr, sizeof(timeStr), "2026-01-01 00:00:00");
241	    }
242
243	    JsonDocument doc;
244	    doc["gateid"]          = _gateid;
245	    doc["type"]            = "online";
246	    doc["time"]            = timeStr;
247	    doc["firmwareVersion"] = FW_VERSION;
248	    doc["wifiSignal"]      = String(WiFi.RSSI());
249
250	    JsonArray devList = doc["deviceList"].to<JsonArray>();
251	    devList.add(METER_ID_DEFAULT);
252
253	    String payload;
254	    serializeJson(doc, payload);
255
256	    _publish(MQTT_TOPIC_STATUS_ONLINE, payload.c_str(), STATUS_QOS);
257	}
258
259	// ============================================================
260	// 上行: 离线通知 (§4.3)
261	// ============================================================
262
263	void MQTTManager::publishOffline() {
264	    struct tm timeinfo;
265	    char timeStr[24];
266	    if (getLocalTime(&timeinfo)) {
267	        strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo);
268	    } else {
269	        snprintf(timeStr, sizeof(timeStr), "2026-01-01 00:00:00");
270	    }
271
272	    JsonDocument doc;
273	    doc["gateid"] = _gateid;
274	    doc["type"]   = "offline";
275	    doc["time"]   = timeStr;
276
277	    String payload;
278	    serializeJson(doc, payload);
279
280	    _publish(MQTT_TOPIC_STATUS_OFFLINE, payload.c_str(), STATUS_QOS);
281	}
282
283	// ============================================================
284	// 上行: 配网状态通知 (§4.4)
285	// ============================================================
286
287	void MQTTManager::publishProvisionStatus(const char* state, const char* bleName) {
288	    struct tm timeinfo;
289	    char timeStr[24];
290	    if (getLocalTime(&timeinfo)) {
291	        strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo);
292	    } else {
293	        snprintf(timeStr, sizeof(timeStr), "2026-01-01 00:00:00");
294	    }
295
296	    JsonDocument doc;
297	    doc["gateid"]  = _gateid;
298	    doc["type"]    = "provision_status";
299	    doc["time"]    = timeStr;

    // 序列化
    String payload;
    serializeJson(doc, payload);

    // 发布到 report/allpoints (QoS 0)
    _mqtt.publish("report/allpoints", payload.c_str());
}

// ============================================================
// 上行: 上线通知 (§4.2)
// ============================================================

void MQTTManager::publishOnline() {
    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");
    }

    JsonDocument doc;
    doc["gateid"]          = _gateid;
    doc["type"]            = "online";
    doc["time"]            = timeStr;
    doc["firmwareVersion"] = FW_VERSION;
    doc["wifiSignal"]      = String(WiFi.RSSI());

    JsonArray devList = doc["deviceList"].to<JsonArray>();
    devList.add(METER_ID_DEFAULT);

    String payload;
    serializeJson(doc, payload);

    _publish(MQTT_TOPIC_STATUS_ONLINE, payload.c_str(), STATUS_QOS);
}

// ============================================================
// 上行: 离线通知 (§4.3)
// ============================================================

void MQTTManager::publishOffline() {
    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");
    }

    JsonDocument doc;
    doc["gateid"] = _gateid;
    doc["type"]   = "offline";
    doc["time"]   = timeStr;

    String payload;
    serializeJson(doc, payload);

    _publish(MQTT_TOPIC_STATUS_OFFLINE, payload.c_str(), STATUS_QOS);
}

// ============================================================
// 上行: 配网状态通知 (§4.4)
// ============================================================

void MQTTManager::publishProvisionStatus(const char* state, const char* bleName) {
    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");
    }

    JsonDocument doc;
    doc["gateid"]  = _gateid;
    doc["type"]    = "provision_status";
    doc["time"]    = timeStr;


Let me find the end of the publish methods to add my new method. Let me look at more.


300	    doc["state"]   = state;
301	    doc["bleName"] = bleName ? bleName : "";
302
303	    String payload;
304	    serializeJson(doc, payload);
305
306	    _publish(MQTT_TOPIC_STATUS_PROVISION, payload.c_str(), STATUS_QOS);
307	}
308
309	// ============================================================
310	// 上行: 指令执行响应 (§4.5)
311	// ============================================================
312
313	void MQTTManager::publishCmdResponse(const char* cmdId, const char* cmdType,
314	                                      const char* alarmType, bool success,
315	                                      const char* errorCode, const char* errorMsg) {
316	    struct tm timeinfo;
317	    char timeStr[24];
318	    if (getLocalTime(&timeinfo)) {
319	        strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo);
320	    } else {
321	        snprintf(timeStr, sizeof(timeStr), "2026-01-01 00:00:00");
322	    }
323
324	    JsonDocument doc;
325	    doc["gateid"]    = _gateid;
326	    doc["type"]      = "cmd_response";
327	    doc["time"]      = timeStr;
328	    doc["cmdId"]     = cmdId;
329	    doc["cmdType"]   = cmdType;     // alarm / alarm_restore / control
330	    doc["alarmType"] = alarmType ? alarmType : "";
331	    doc["result"]    = success ? "success" : "fail";
332	    doc["errorCode"] = errorCode;
333	    doc["errorMsg"]  = errorMsg;
334
335	    String payload;
336	    serializeJson(doc, payload);
337
338	    _publish(MQTT_TOPIC_RESPONSE_CMD, payload.c_str(), STATUS_QOS);
339	}
340
341	// ============================================================
342	// 上行: 配网指令响应 (§4.6)
343	// ============================================================
344
345	void MQTTManager::publishProvisionResponse(const char* cmdId, bool success,
346	                                            const char* errorCode, const char* errorMsg,
347	                                            const char* wifiSignal) {
348	    struct tm timeinfo;
349	    char timeStr[24];
350	    if (getLocalTime(&timeinfo)) {
351	        strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo);
352	    } else {
353	        snprintf(timeStr, sizeof(timeStr), "2026-01-01 00:00:00");
354	    }
355
356	    JsonDocument doc;
357	    doc["gateid"]    = _gateid;
358	    doc["type"]      = "provision_response";
359	    doc["time"]      = timeStr;

    doc["state"]   = state;
    doc["bleName"] = bleName ? bleName : "";

    String payload;
    serializeJson(doc, payload);

    _publish(MQTT_TOPIC_STATUS_PROVISION, payload.c_str(), STATUS_QOS);
}

// ============================================================
// 上行: 指令执行响应 (§4.5)
// ============================================================

void MQTTManager::publishCmdResponse(const char* cmdId, const char* cmdType,
                                      const char* alarmType, bool success,
                                      const char* errorCode, const char* errorMsg) {
    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");
    }

    JsonDocument doc;
    doc["gateid"]    = _gateid;
    doc["type"]      = "cmd_response";
    doc["time"]      = timeStr;
    doc["cmdId"]     = cmdId;
    doc["cmdType"]   = cmdType;     // alarm / alarm_restore / control
    doc["alarmType"] = alarmType ? alarmType : "";
    doc["result"]    = success ? "success" : "fail";
    doc["errorCode"] = errorCode;
    doc["errorMsg"]  = errorMsg;

    String payload;
    serializeJson(doc, payload);

    _publish(MQTT_TOPIC_RESPONSE_CMD, payload.c_str(), STATUS_QOS);
}

// ============================================================
// 上行: 配网指令响应 (§4.6)
// ============================================================

void MQTTManager::publishProvisionResponse(const char* cmdId, bool success,
                                            const char* errorCode, const char* errorMsg,
                                            const char* wifiSignal) {
    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");
    }

    JsonDocument doc;
    doc["gateid"]    = _gateid;
    doc["type"]      = "provision_response";
    doc["time"]      = timeStr;


Let me find the end of the publish methods so I can add my new method after them.


360	    doc["cmdId"]     = cmdId;
361	    doc["result"]    = success ? "success" : "fail";
362	    doc["errorCode"] = errorCode;
363	    doc["errorMsg"]  = errorMsg;
364	    if (success && wifiSignal && strlen(wifiSignal) > 0) {
365	        doc["wifiSignal"] = wifiSignal;
366	    }
367
368	    String payload;
369	    serializeJson(doc, payload);
370
371	    _publish(MQTT_TOPIC_RESPONSE_PROVISION, payload.c_str(), STATUS_QOS);
372	}
373
374	// ============================================================
375	// 回调注册
376	// ============================================================
377
378	void MQTTManager::onAlarmCommand(MQTTAlarmCallback cb)     { _alarmCb = cb; }
379	void MQTTManager::onControlCommand(MQTTControlCallback cb)  { _controlCb = cb; }
380	void MQTTManager::onProvisionCommand(MQTTProvisionCallback cb) { _provisionCb = cb; }
381
382	// ============================================================
383	// 静态 MQTT 回调
384	// ============================================================
385
386	void MQTTManager::_mqttCallback(char* topic, byte* payload, unsigned int length) {
387	    char msg[512];
388	    unsigned int l = min(length, (unsigned int)(sizeof(msg) - 1));
389	    memcpy(msg, payload, l);
390	    msg[l] = '\0';
391
392	    Serial.printf("[MQTT] 📩 [%s]\n", topic);
393	    // Serial.println(msg);  // 完整打印在 debug 时打开
394
395	    if (_instance) {
396	        _instance->_handleMessage(topic, msg);
397	    }
398	}
399

    doc["cmdId"]     = cmdId;
    doc["result"]    = success ? "success" : "fail";
    doc["errorCode"] = errorCode;
    doc["errorMsg"]  = errorMsg;
    if (success && wifiSignal && strlen(wifiSignal) > 0) {
        doc["wifiSignal"] = wifiSignal;
    }

    String payload;
    serializeJson(doc, payload);

    _publish(MQTT_TOPIC_RESPONSE_PROVISION, payload.c_str(), STATUS_QOS);
}

// ============================================================
// 回调注册
// ============================================================

void MQTTManager::onAlarmCommand(MQTTAlarmCallback cb)     { _alarmCb = cb; }
void MQTTManager::onControlCommand(MQTTControlCallback cb)  { _controlCb = cb; }
void MQTTManager::onProvisionCommand(MQTTProvisionCallback cb) { _provisionCb = cb; }

// ============================================================
// 静态 MQTT 回调
// ============================================================

void MQTTManager::_mqttCallback(char* topic, byte* payload, unsigned int length) {
    char msg[512];
    unsigned int l = min(length, (unsigned int)(sizeof(msg) - 1));
    memcpy(msg, payload, l);
    msg[l] = '\0';

    Serial.printf("[MQTT] 📩 [%s]\n", topic);
    // Serial.println(msg);  // 完整打印在 debug 时打开

    if (_instance) {
        _instance->_handleMessage(topic, msg);
    }
}


Good. I'll add `publishLocalAlarm` after `publishProvisionResponse` (line 372) and before the 回调注册 section.


在 `publishProvisionResponse` 后添加 `publishLocalAlarm`：


// ============================================================
// 上行: 本地报警上报 (§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());
}

// ============================================================
// 回调注册
// ============================================================


The file d:\PlatformIO1\Projects\Healthv2.0\src\mqtt_manager.cpp has been updated successfully. (file state is current in your context — no need to Read it back)

// ============================================================
// 上行: 本地报警上报 (§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());
}

// ============================================================
// 回调注册
// ============================================================

+// 上行: 本地报警上报 (§4.7 扩展)

+// 设备本地判定报警后立即上报，格式兼容服务端告警协议

+void MQTTManager::publishLocalAlarm(uint8_t alarmType, bool triggered, const char* desc) {

+        strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo);

+        snprintf(timeStr, sizeof(timeStr), "2026-01-01 00:00:00");

+    // 类型映射

+    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;

+    doc["type"]    = triggered ? "alarm" : "alarm_restore";

+    doc["source"]  = "local";   // 标识本地判定

+    // 通过 status 通道上报本地报警

+    String topic = _buildTopic("status/%s/alarm");

+    _mqtt.publish(topic.c_str(), payload.c_str());


现在说拓展所有的报警逻辑


Good. Now I need to update main.cpp with the expanded alarm logic. The changes needed:

1. Add new tracking variables for temp, humi, sensor faults (with hysteresis counters)
2. Add alarm callback for MQTT reporting
3. Expand checkLocalAlarms() with new checks
4. Register alarm callback in setup()

Let me write these changes step by step.


现在更新 `main.cpp`——扩展报警变量、检测逻辑、MQTT 上报：


// 本地报警跟踪 (防重复触发)
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次确认才触发


The file d:\PlatformIO1\Projects\Healthv2.0\src\main.cpp has been updated successfully. (file state is current in your context — no need to Read it back)

// 本地报警跟踪 (防重复触发)
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次确认才触发

uint32_t g_bootTime        = 0;       // 启动时刻

// 本地报警跟踪 (防重复触发)

+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次确认才触发)

+#define ALARM_HYSTERESIS    3   // 连续3次确认才触发


Now add the alarm callback function and expand checkLocalAlarms. Let me replace the entire checkLocalAlarms function and add the MQTT callback.


替换整个 `checkLocalAlarms()` 函数，扩展所有报警类型：


// ============================================================
// 本地报警检查 (离线可用)
// ============================================================

void checkLocalAlarms(const RadarRawData& radar) {
    uint32_t now = millis();

    // --- 跌倒检测 (本地紧急报警, 立即触发) ---
    if (radar.fall && radar.radarValid && !g_localFallActive) {
        g_localFallActive = true;
        g_alarm.triggerLocal(AlarmManager::ALARM_FALL);
    } else if (!radar.fall && g_localFallActive) {
        g_localFallActive = false;
        g_alarm.clearLocal(AlarmManager::ALARM_FALL);
    }

    // --- 水浸检测 ---
    bool waterLeak = !g_sensor.getRainDO() && g_sensor.isRainValid();
    if (waterLeak && !g_localWaterActive) {
        g_localWaterActive = true;
        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);
        }
    }

    // --- 呼吸异常 ---
    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);
        }
    }
}

// ============================================================
// 本地报警回调 → 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) {
    // ─── ★★★★★ 人身安全: 跌倒 (立即触发, 无防抖) ───
    if (radar.fall && radar.radarValid && !g_localFallActive) {
        g_localFallActive = true;
        g_alarm.triggerLocal(AlarmManager::ALARM_FALL, "雷达检测到跌倒");
    } else if (!radar.fall && g_localFallActive) {
        g_localFallActive = false;
        g_alarm.clearLocal(AlarmManager::ALARM_FALL);
    }

    // ─── ★★★★★ 人身安全: 心率异常 ───
    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;
            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);
        }
    }

    // ─── ★★★★★ 人身安全: 呼吸异常 ───
    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;
            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);
        }
    }

    // ─── ★★★ 环境安全: 水浸 ───
    bool waterLeak = !g_sensor.getRainDO() && g_sensor.isRainValid();
    if (waterLeak && !g_localWaterActive) {
        g_localWaterActive = true;
        g_alarm.triggerLocal(AlarmManager::ALARM_WATER_LEAK, "水浸传感器触发");
    } else if (!waterLeak && g_localWaterActive) {
        g_localWaterActive = false;
        g_alarm.clearLocal(AlarmManager::ALARM_WATER_LEAK);
    }

    // ─── ★★ 环境舒适: 温度异常 (防抖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);
            }
        }
    }

    // ─── ★ 设备：雷达超时 (连续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);
    }
}


// ============================================================
// 本地报警检查 (离线可用)
// ============================================================

void checkLocalAlarms(const RadarRawData& radar) {
    uint32_t now = millis();

    // --- 跌倒检测 (本地紧急报警, 立即触发) ---
    if (radar.fall && radar.radarValid && !g_localFallActive) {
        g_localFallActive = true;
        g_alarm.triggerLocal(AlarmManager::ALARM_FALL);
    } else if (!radar.fall && g_localFallActive) {
        g_localFallActive = false;
        g_alarm.clearLocal(AlarmManager::ALARM_FALL);
    }

    // --- 水浸检测 ---
    bool waterLeak = !g_sensor.getRainDO() && g_sensor.isRainValid();
    if (waterLeak && !g_localWaterActive) {
        g_localWaterActive = true;
        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);
        }
    }

    // --- 呼吸异常 ---
    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);
        }
    }
}

// ============================================================
// 本地报警回调 → 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) {
    // ─── ★★★★★ 人身安全: 跌倒 (立即触发, 无防抖) ───
    if (radar.fall && radar.radarValid && !g_localFallActive) {
        g_localFallActive = true;
        g_alarm.triggerLocal(AlarmManager::ALARM_FALL, "雷达检测到跌倒");
    } else if (!radar.fall && g_localFallActive) {
        g_localFallActive = false;
        g_alarm.clearLocal(AlarmManager::ALARM_FALL);
    }

    // ─── ★★★★★ 人身安全: 心率异常 ───
    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;
            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);
        }
    }

    // ─── ★★★★★ 人身安全: 呼吸异常 ───
    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;
            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);
        }
    }

    // ─── ★★★ 环境安全: 水浸 ───
    bool waterLeak = !g_sensor.getRainDO() && g_sensor.isRainValid();
    if (waterLeak && !g_localWaterActive) {
        g_localWaterActive = true;
        g_alarm.triggerLocal(AlarmManager::ALARM_WATER_LEAK, "水浸传感器触发");
    } else if (!waterLeak && g_localWaterActive) {
        g_localWaterActive = false;
        g_alarm.clearLocal(AlarmManager::ALARM_WATER_LEAK);
    }

    // ─── ★★ 环境舒适: 温度异常 (防抖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);
            }
        }
    }

    // ─── ★ 设备：雷达超时 (连续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);
    }
}

-// 本地报警检查 (离线可用)

+// 本地报警回调 → MQTT 上报 (连接时)

-void checkLocalAlarms(const RadarRawData& radar) {

+void onLocalAlarmChange(uint8_t type, bool triggered, const char* desc) {

+        g_mqtt.publishLocalAlarm(type, triggered, desc);

-    // --- 跌倒检测 (本地紧急报警, 立即触发) ---

+// 本地报警检查 (离线可用) — 人身安全 > 环境 > 设备

+void checkLocalAlarms(const RadarRawData& radar) {

+    // ─── ★★★★★ 人身安全: 跌倒 (立即触发, 无防抖) ───

-        g_alarm.triggerLocal(AlarmManager::ALARM_FALL);

+        g_alarm.triggerLocal(AlarmManager::ALARM_FALL, "雷达检测到跌倒");

-    if (waterLeak && !g_localWaterActive) {

-        g_alarm.triggerLocal(AlarmManager::ALARM_WATER_LEAK);

-    } else if (!waterLeak && g_localWaterActive) {

-        g_alarm.clearLocal(AlarmManager::ALARM_WATER_LEAK);

-    // --- 心率异常 (离线本地判定, 阈值预留后续调整) ---

+    // ─── ★★★★★ 人身安全: 心率异常 ───

-            g_alarm.triggerLocal(AlarmManager::ALARM_HEART_ABNORMAL);

+            snprintf(buf, sizeof(buf), "心率异常: %d bpm", radar.heartRate);

+            g_alarm.triggerLocal(AlarmManager::ALARM_HEART_ABNORMAL, buf);

+    // ─── ★★★★★ 人身安全: 呼吸异常 ───

-            g_alarm.triggerLocal(AlarmManager::ALARM_BREATH_ABNORMAL);

+            snprintf(buf, sizeof(buf), "呼吸异常: %d 次/分", radar.breathRate);

+            g_alarm.triggerLocal(AlarmManager::ALARM_BREATH_ABNORMAL, buf);

+    // ─── ★★★ 环境安全: 水浸 ───

+    bool waterLeak = !g_sensor.getRainDO() && g_sensor.isRainValid();

+    if (waterLeak && !g_localWaterActive) {

+        g_alarm.triggerLocal(AlarmManager::ALARM_WATER_LEAK, "水浸传感器触发");

+    } else if (!waterLeak && g_localWaterActive) {

+        g_alarm.clearLocal(AlarmManager::ALARM_WATER_LEAK);

+    // ─── ★★ 环境舒适: 温度异常 (防抖3次) ───

+        float temp = g_sensor.getTemp();

+        bool tempAbn = (temp < TEMP_LOW || temp > TEMP_HIGH);

+            if (++g_tempAbnCnt >= ALARM_HYSTERESIS && !g_localTempAbnActive) {

+                g_localTempAbnActive = true;

+                snprintf(buf, sizeof(buf), "温度%.1f°C 超出范围", temp);

+                g_alarm.triggerLocal(AlarmManager::ALARM_TEMP_ABNORMAL, buf);

+            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 (++g_humiAbnCnt >= ALARM_HYSTERESIS && !g_localHumiAbnActive) {

+                g_localHumiAbnActive = true;

+                snprintf(buf, sizeof(buf), "湿度%.1f%% 超出范围", humi);

+                g_alarm.triggerLocal(AlarmManager::ALARM_HUMI_ABNORMAL, buf);

+            if (g_localHumiAbnActive) {

+                g_localHumiAbnActive = false;

+                g_alarm.clearLocal(AlarmManager::ALARM_HUMI_ABNORMAL);

+    // ─── ★ 设备：雷达超时 (连续5秒无数据) ───

+        if (++g_radarTimeoutCnt >= 5 && !g_localRadarFaultActive) {

+            g_localRadarFaultActive = true;

+            g_alarm.triggerLocal(AlarmManager::ALARM_SENSOR_FAULT, "雷达超时无数据");

+            g_localRadarFaultActive = false;

+            g_alarm.clearLocal(AlarmManager::ALARM_SENSOR_FAULT);

+    // ─── ★ 设备：DHT 持续故障 (已由 SensorManager 内部计数) ───

+    if (g_sensor.isDhtFault() && !g_localDhtFaultActive && millis() > 60000) {

+        g_alarm.triggerLocal(AlarmManager::ALARM_SENSOR_FAULT, "DHT11温湿度传感器故障");

+    } else if (!g_sensor.isDhtFault() && g_localDhtFaultActive) {


扩展 AlarmManager：新报警类型 + 优先级蜂鸣 + 回调

扩展 main.cpp：温湿度/传感器故障检测 + MQTT上报 + 防抖

OLED 显示活跃报警提示


Now I need to register the alarm callback in setup() and also remove the old sensor fault check (the one at lines 430-440 that just prints to Serial). Let me update setup() and loop().


在 `setup()` 中注册报警回调，并更新 `loop()` 中的旧故障检测：


    // 注册 MQTT 回调
    g_mqtt.onAlarmCommand(handleAlarmCommand);
    g_mqtt.onControlCommand(handleControlCommand);
    g_mqtt.onProvisionCommand(handleProvisionCommand);

    // 注册 MQTT 回调
    g_mqtt.onAlarmCommand(handleAlarmCommand);
    g_mqtt.onControlCommand(handleControlCommand);
    g_mqtt.onProvisionCommand(handleProvisionCommand);

    // 注册本地报警回调 → MQTT 上报
    g_alarm.onAlarmChange(onLocalAlarmChange);


    // 注册 MQTT 回调
    g_mqtt.onAlarmCommand(handleAlarmCommand);
    g_mqtt.onControlCommand(handleControlCommand);
    g_mqtt.onProvisionCommand(handleProvisionCommand);

    // 注册本地报警回调 → MQTT 上报
    g_alarm.onAlarmChange(onLocalAlarmChange);

+    // 注册本地报警回调 → MQTT 上报

+    g_alarm.onAlarmChange(onLocalAlarmChange);


Now update loop() — the old sensor fault check at lines 430-440 should be replaced since the alarm system now handles it. And I should update the OLED call to pass alarm status.


替换 `loop()` 中旧的传感器故障检测（现在由报警系统统一管理）：


    // --- 传感器故障检测 (每30秒检查一次，避免刷屏) ---
    static uint32_t lastFaultCheck = 0;
    if (now - lastFaultCheck > 30000) {
        lastFaultCheck = now;
        if (!g_sensor.isDhtValid() && millis() > 60000) {
            Serial.println(F("[系统] ⚠ DHT11 未检测到 (不影响运行)"));
        }
        if (g_radar.isTimeout()) {
            Serial.println(F("[系统] ⚠ 雷达超时无数据 (不影响运行)"));
        }
    }

    // --- 传感器故障日志 (报警系统统一管理触发, 此处仅记录) ---
    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() && !g_localRadarFaultActive) {
            Serial.println(F("[系统] ⚠ 雷达超时无数据"));
        }
    }


    // --- 传感器故障检测 (每30秒检查一次，避免刷屏) ---
    static uint32_t lastFaultCheck = 0;
    if (now - lastFaultCheck > 30000) {
        lastFaultCheck = now;
        if (!g_sensor.isDhtValid() && millis() > 60000) {
            Serial.println(F("[系统] ⚠ DHT11 未检测到 (不影响运行)"));
        }
        if (g_radar.isTimeout()) {
            Serial.println(F("[系统] ⚠ 雷达超时无数据 (不影响运行)"));
        }
    }

    // --- 传感器故障日志 (报警系统统一管理触发, 此处仅记录) ---
    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() && !g_localRadarFaultActive) {
            Serial.println(F("[系统] ⚠ 雷达超时无数据"));
        }
    }

-    // --- 传感器故障检测 (每30秒检查一次，避免刷屏) ---

-    static uint32_t lastFaultCheck = 0;

-    if (now - lastFaultCheck > 30000) {

-        if (!g_sensor.isDhtValid() && millis() > 60000) {

-            Serial.println(F("[系统] ⚠ DHT11 未检测到 (不影响运行)"));

+    // --- 传感器故障日志 (报警系统统一管理触发, 此处仅记录) ---

+        if (!g_sensor.isDhtValid() && millis() > 60000 && !g_localDhtFaultActive) {

+            Serial.println(F("[系统] ⚠ DHT11 读数异常"));

-            Serial.println(F("[系统] ⚠ 雷达超时无数据 (不影响运行)"));

+        if (g_radar.isTimeout() && !g_localRadarFaultActive) {

+            Serial.println(F("[系统] ⚠ 雷达超时无数据"));


Now update the OLED display. I need to:
1. Pass alarm info (has active alarm, alarm type) to the display
2. Show alarm indicator on all pages when there's an active alarm

Let me update the display_manager.h first, then the display_manager.cpp, then update the call in main.cpp.

Actually, the simplest change that has the most impact: show an alarm indicator on every page when there's an active alarm. I'll pass the alarm status string to `update()`.

Let me look at the current display_manager.h to see what I need to change.


TodoWrite

扩展 AlarmManager：新报警类型 + 优先级蜂鸣 + 回调

扩展 main.cpp：温湿度/传感器故障检测 + MQTT上报 + 防抖

OLED 显示活跃报警提示


Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable

扩展 AlarmManager：新报警类型 + 优先级蜂鸣 + 回调

扩展 main.cpp：温湿度/传感器故障检测 + MQTT上报 + 防抖

OLED 显示活跃报警提示

扩展 AlarmManager：新报警类型 + 优先级蜂鸣 + 回调

扩展 main.cpp：温湿度/传感器故障检测 + MQTT上报 + 防抖

OLED 显示活跃报警提示


Update the OLED - I need to:
1. Update display_manager.h to add alarm status parameter to `update()`
2. Update display_manager.cpp to show alarm indicator on every page
3. Update main.cpp to pass alarm info

Let me keep it simple - just pass whether there's an active alarm and the alarm name.


更新 OLED，在所有页面显示报警状态：


1	/**
2	 * @file display_manager.h
3	 * @brief OLED 显示管理器 - SSD1306 128x64 I2C
4	 *
5	 * 4 个显示页面:
6	 *   Page 0 - 环境数据 (温湿度 + WiFi)
7	 *   Page 1 - 生命体征 (心率 + 呼吸 + 体动)
8	 *   Page 2 - 人体状态 (有人/运动/跌倒/驻留)
9	 *   Page 3 - 安全信息 (水浸 + 蜂鸣器 + 运行时间)
10	 */
11
12	#ifndef DISPLAY_MANAGER_H
13	#define DISPLAY_MANAGER_H
14
15	#include <Arduino.h>
16	#include <U8g2lib.h>
17	#include "config.h"
18
19	class DisplayManager {
20	public:
21	    DisplayManager();
22	    ~DisplayManager();
23
24	    /** 初始化 OLED */
25	    bool begin();
26
27	    /**
28	     * @brief 更新显示内容 (每 5 秒调用一次即可)
29	     * @param data  遥测数据
30	     * @param wifiOk  WiFi 是否已连接
31	     * @param buzzerOn 蜂鸣器是否在响
32	     */
33	    void update(const TelemetryData& data, bool wifiOk, bool buzzerOn);
34
35	    /** 切换到下一页 (0→1→2→3→0 循环) */
36	    void nextPage();
37
38	    /** 获取当前页码 */
39	    uint8_t getPage() const { return _pageIndex; }
40
41	private:
42	    void _drawPage0(const TelemetryData& data, bool wifiOk, bool buzzerOn);
43	    void _drawPage1(const TelemetryData& data);
44	    void _drawPage2(const TelemetryData& data);
45	    void _drawPage3(const TelemetryData& data, bool buzzerOn, bool wifiOk);
46
47	    /** 格式化秒数为 HH:MM:SS */
48	    String _fmtUptime(uint32_t secs) const;
49	    /** 格式化浮点数为一位小数 */
50	    String _fmtFloat(float val) const;
51
52	    U8G2_SSD1306_128X64_NONAME_F_HW_I2C _u8g2;
53	    uint8_t  _pageIndex;
54	    uint32_t _lastDrawTime;
55	    bool     _initialized;
56	};
57
58	#endif // DISPLAY_MANAGER_H
59

/**
 * @file display_manager.h
 * @brief OLED 显示管理器 - SSD1306 128x64 I2C
 *
 * 4 个显示页面:
 *   Page 0 - 环境数据 (温湿度 + WiFi)
 *   Page 1 - 生命体征 (心率 + 呼吸 + 体动)
 *   Page 2 - 人体状态 (有人/运动/跌倒/驻留)
 *   Page 3 - 安全信息 (水浸 + 蜂鸣器 + 运行时间)
 */

#ifndef DISPLAY_MANAGER_H
#define DISPLAY_MANAGER_H

#include <Arduino.h>
#include <U8g2lib.h>
#include "config.h"

class DisplayManager {
public:
    DisplayManager();
    ~DisplayManager();

    /** 初始化 OLED */
    bool begin();

    /**
     * @brief 更新显示内容 (每 5 秒调用一次即可)
     * @param data  遥测数据
     * @param wifiOk  WiFi 是否已连接
     * @param buzzerOn 蜂鸣器是否在响
     */
    void update(const TelemetryData& data, bool wifiOk, bool buzzerOn);

    /** 切换到下一页 (0→1→2→3→0 循环) */
    void nextPage();

    /** 获取当前页码 */
    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);

    /** 格式化秒数为 HH:MM:SS */
    String _fmtUptime(uint32_t secs) const;
    /** 格式化浮点数为一位小数 */
    String _fmtFloat(float val) const;

    U8G2_SSD1306_128X64_NONAME_F_HW_I2C _u8g2;
    uint8_t  _pageIndex;
    uint32_t _lastDrawTime;
    bool     _initialized;
};

#endif // DISPLAY_MANAGER_H


I need to:
1. Update `update()` to accept alarm info — add `bool hasAlarm` and `const char* alarmName`
2. Update each page to show alarm indicator when active
3. Update main.cpp call

Let me keep it clean. I'll add the alarm parameters to the update method and draw methods.

For the alarm indicator, I'll show a red "🚨" + alarm name at the top of each page when there's an active alarm, replacing the normal title.

Let me update the header and implementation.


Edit

    /**
     * @brief 更新显示内容 (每 5 秒调用一次即可)
     * @param data  遥测数据
     * @param wifiOk  WiFi 是否已连接
     * @param buzzerOn 蜂鸣器是否在响
     */
    void update(const TelemetryData& data, bool wifiOk, bool buzzerOn);

    /**
     * @brief 更新显示内容 (每 5 秒调用一次即可)
     * @param data      遥测数据
     * @param wifiOk    WiFi 是否已连接
     * @param buzzerOn  蜂鸣器是否在响
     * @param hasAlarm  是否有活跃报警
     * @param alarmName 报警名称 (如 "跌倒"/"心率异常")
     */
    void update(const TelemetryData& data, bool wifiOk, bool buzzerOn,
                bool hasAlarm = false, const char* alarmName = "");


The file d:\PlatformIO1\Projects\Healthv2.0\include\display_manager.h has been updated successfully. (file state is current in your context — no need to Read it back)

    /**
     * @brief 更新显示内容 (每 5 秒调用一次即可)
     * @param data      遥测数据
     * @param wifiOk    WiFi 是否已连接
     * @param buzzerOn  蜂鸣器是否在响
     * @param hasAlarm  是否有活跃报警
     * @param alarmName 报警名称 (如 "跌倒"/"心率异常")
     */
    void update(const TelemetryData& data, bool wifiOk, bool buzzerOn,
                bool hasAlarm = false, const char* alarmName = "");

/**
 * @file display_manager.h
 * @brief OLED 显示管理器 - SSD1306 128x64 I2C
 *
 * 4 个显示页面:
 *   Page 0 - 环境数据 (温湿度 + WiFi)
 *   Page 1 - 生命体征 (心率 + 呼吸 + 体动)
 *   Page 2 - 人体状态 (有人/运动/跌倒/驻留)
 *   Page 3 - 安全信息 (水浸 + 蜂鸣器 + 运行时间)
 */

#ifndef DISPLAY_MANAGER_H
#define DISPLAY_MANAGER_H

#include <Arduino.h>
#include <U8g2lib.h>
#include "config.h"

class DisplayManager {
public:
    DisplayManager();
    ~DisplayManager();

    /** 初始化 OLED */
    bool begin();

    /**
     * @brief 更新显示内容 (每 5 秒调用一次即可)
     * @param data  遥测数据
     * @param wifiOk  WiFi 是否已连接
     * @param buzzerOn 蜂鸣器是否在响
     */
    void update(const TelemetryData& data, bool wifiOk, bool buzzerOn);

    /** 切换到下一页 (0→1→2→3→0 循环) */
    void nextPage();

    /** 获取当前页码 */
    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);

    /** 格式化秒数为 HH:MM:SS */
    String _fmtUptime(uint32_t secs) const;
    /** 格式化浮点数为一位小数 */
    String _fmtFloat(float val) const;

    U8G2_SSD1306_128X64_NONAME_F_HW_I2C _u8g2;
    uint8_t  _pageIndex;
    uint32_t _lastDrawTime;
    bool     _initialized;
};

#endif // DISPLAY_MANAGER_H

* @brief 更新显示内容 (每 5 秒调用一次即可)

+     * @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 循环) */


现在说拓展所有的报警逻辑


Now update display_manager.cpp. The key changes:
1. `update()` accepts alarm params
2. On every page, if there's an active alarm, replace the title bar with a flashing alarm indicator
3. Show alarm name prominently


更新 OLED 实现——有报警时顶部闪烁提示：


void DisplayManager::update(const TelemetryData& data, bool wifiOk, bool buzzerOn) {
    if (!_initialized) return;

    // 每 5 秒刷新一次 (防止 OLED 烧屏，也避免 SPI 占用过多)
    if (millis() - _lastDrawTime < 5000) return;
    _lastDrawTime = millis();

    _u8g2.clearBuffer();

    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;
    }

    _u8g2.sendBuffer();
}

void DisplayManager::update(const TelemetryData& data, bool wifiOk, bool buzzerOn,
                             bool hasAlarm, const char* alarmName) {
    if (!_initialized) 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;
    }

    _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);
}


The file d:\PlatformIO1\Projects\Healthv2.0\src\display_manager.cpp has been updated successfully. (file state is current in your context — no need to Read it back)

void DisplayManager::update(const TelemetryData& data, bool wifiOk, bool buzzerOn) {
    if (!_initialized) return;

    // 每 5 秒刷新一次 (防止 OLED 烧屏，也避免 SPI 占用过多)
    if (millis() - _lastDrawTime < 5000) return;
    _lastDrawTime = millis();

    _u8g2.clearBuffer();

    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;
    }

    _u8g2.sendBuffer();
}

void DisplayManager::update(const TelemetryData& data, bool wifiOk, bool buzzerOn,
                             bool hasAlarm, const char* alarmName) {
    if (!_initialized) 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;
    }

    _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);
}

-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) {

-    // 每 5 秒刷新一次 (防止 OLED 烧屏，也避免 SPI 占用过多)

-    if (millis() - _lastDrawTime < 5000) return;

+    // 有报警时每秒刷新 (闪烁提示)，否则 5 秒

+    uint32_t interval = hasAlarm ? 1000 : 5000;

+    if (millis() - _lastDrawTime < interval) return;

+    // 活跃报警 — 所有页面顶部闪烁提示

+    if (hasAlarm && alarmName && alarmName[0]) {

+// 报警横幅 (顶部闪烁)

+void DisplayManager::_drawAlarmBanner(const char* name) {

+    // 每秒翻转，实现闪烁效果

+    _u8g2.setFont(u8g2_font_wqy12_t_gb2312);


Now I need to declare `_drawAlarmBanner` in the header and update the page drawing functions to skip the title bar when alarm is active (since the banner already uses the top area). Also update main.cpp to pass alarm info.

Let me add the declaration to display_manager.h.


在头文件添加 `_drawAlarmBanner` 声明：


    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);
    void _drawPage1(const TelemetryData& data);
    void _drawPage2(const TelemetryData& data);
    void _drawPage3(const TelemetryData& data, bool buzzerOn, bool wifiOk);
    void _drawAlarmBanner(const char* name);


    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 _drawAlarmBanner(const char* name);

/**
 * @file display_manager.h
 * @brief OLED 显示管理器 - SSD1306 128x64 I2C
 *
 * 4 个显示页面:
 *   Page 0 - 环境数据 (温湿度 + WiFi)
 *   Page 1 - 生命体征 (心率 + 呼吸 + 体动)
 *   Page 2 - 人体状态 (有人/运动/跌倒/驻留)
 *   Page 3 - 安全信息 (水浸 + 蜂鸣器 + 运行时间)
 */

#ifndef DISPLAY_MANAGER_H
#define DISPLAY_MANAGER_H

#include <Arduino.h>
#include <U8g2lib.h>
#include "config.h"

class DisplayManager {
public:
    DisplayManager();
    ~DisplayManager();

    /** 初始化 OLED */
    bool begin();

    /**
     * @brief 更新显示内容 (每 5 秒调用一次即可)
     * @param data      遥测数据
     * @param wifiOk    WiFi 是否已连接
     * @param buzzerOn  蜂鸣器是否在响
     * @param hasAlarm  是否有活跃报警
     * @param alarmName 报警名称 (如 "跌倒"/"心率异常")
     */
    void update(const TelemetryData& data, bool wifiOk, bool buzzerOn,
                bool hasAlarm = false, const char* alarmName = "");

    /** 切换到下一页 (0→1→2→3→0 循环) */
    void nextPage();

    /** 获取当前页码 */
    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);

    /** 格式化秒数为 HH:MM:SS */
    String _fmtUptime(uint32_t secs) const;
    /** 格式化浮点数为一位小数 */
    String _fmtFloat(float val) const;

    U8G2_SSD1306_128X64_NONAME_F_HW_I2C _u8g2;
    uint8_t  _pageIndex;
    uint32_t _lastDrawTime;
    bool     _initialized;
};

#endif // DISPLAY_MANAGER_H

+    void _drawAlarmBanner(const char* name);


Now update the page draw functions. Since the alarm banner already uses the top area (lines 0-15), the page titles should be adjusted. The banner draws a horizontal line at y=15, so the page content starts at y=16+ which is the same as before. The pages that have a title bar (page 0, 1, 2, 3) all use line 0-15 area for title — when alarm is active, the banner replaces that.

Actually, looking at the code flow: when `hasAlarm` is true, `_drawAlarmBanner` is called first (drawing at y=0-15), then the page function draws its own title at y=0-15 which would overwrite the banner!

I need to adjust the page functions to skip their title bar when alarm is active. But the page functions don't know if alarm is active...

Let me restructure: pass `hasAlarm` to each page function, or make the decision in `update()` — if alarm is active, skip the page's own title drawing.

The simplest approach: pass a `skipTitle` bool to each draw page function, or just pass `hasAlarm`.

Actually, the cleanest approach: the pages should NOT draw their own title if the alarm banner is present. I'll pass `hasAlarm` to the page functions.

Let me update the page functions. For each page:
- Page 0: Skip "🏥 健康监测 V1.0" title and page number at top
- Page 1: Skip "🫀 生命体征" title
- Page 2: Skip "人体状态" title
- Page 3: Skip "安全信息" title

The content below y=15 is the same in both cases.

Let me update all page functions to accept a `bool hasAlarm` parameter and skip the title when true.


让各页面在有报警时不画自己的标题（报警横幅已经占了顶部）：


    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;
    }

    switch (_pageIndex) {
    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;
    }


    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;
    }

    switch (_pageIndex) {
    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;
    }

-    case 0: _drawPage0(data, wifiOk, buzzerOn); 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;


Now update each page function to skip the title when alarm is active. Let me update them one by one.


更新 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);
    }


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);
    }

-void DisplayManager::_drawPage0(const TelemetryData& data, bool wifiOk, bool buzzerOn) {

-    _u8g2.setFont(u8g2_font_wqy12_t_gb2312);

+void DisplayManager::_drawPage0(const TelemetryData& data, bool wifiOk,

+                                 bool buzzerOn, bool hasAlarm) {

+    // 标题栏 (报警横幅已占顶部时跳过)

+        _u8g2.setFont(u8g2_font_wqy12_t_gb2312);


扩展 AlarmManager：新报警类型 + 优先级蜂鸣 + 回调

扩展 main.cpp：温湿度/传感器故障检测 + MQTT上报 + 防抖

OLED 显示活跃报警提示


Let me continue updating the other page functions.


更新 Page 1：


void DisplayManager::_drawPage1(const TelemetryData& data) {
    // 标题栏
    _u8g2.setFont(u8g2_font_wqy12_t_gb2312);
    _u8g2.drawUTF8(2, 12, "🫀 生命体征");
    _u8g2.drawHLine(0, 15, 128);

void DisplayManager::_drawPage1(const TelemetryData& data, bool hasAlarm) {
    // 标题栏
    if (!hasAlarm) {
        _u8g2.setFont(u8g2_font_wqy12_t_gb2312);
        _u8g2.drawUTF8(2, 12, "🫀 生命体征");
        _u8g2.drawHLine(0, 15, 128);
    }


void DisplayManager::_drawPage1(const TelemetryData& data) {
    // 标题栏
    _u8g2.setFont(u8g2_font_wqy12_t_gb2312);
    _u8g2.drawUTF8(2, 12, "🫀 生命体征");
    _u8g2.drawHLine(0, 15, 128);

void DisplayManager::_drawPage1(const TelemetryData& data, bool hasAlarm) {
    // 标题栏
    if (!hasAlarm) {
        _u8g2.setFont(u8g2_font_wqy12_t_gb2312);
        _u8g2.drawUTF8(2, 12, "🫀 生命体征");
        _u8g2.drawHLine(0, 15, 128);
    }

-void DisplayManager::_drawPage1(const TelemetryData& data) {

+void DisplayManager::_drawPage1(const TelemetryData& data, bool hasAlarm) {


更新 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);
    }


现在说拓展所有的报警逻辑


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);
    }

-void DisplayManager::_drawPage2(const TelemetryData& data) {

+void DisplayManager::_drawPage2(const TelemetryData& data, bool hasAlarm) {

_u8g2.drawUTF8(60, 30, data.presence ? "V" : "X");


更新 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);
    }


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);
    }

-void DisplayManager::_drawPage3(const TelemetryData& data, bool buzzerOn, bool wifiOk) {

+void DisplayManager::_drawPage3(const TelemetryData& data, bool buzzerOn,

+                                 bool wifiOk, bool hasAlarm) {


Now update the display_manager.h to match the new function signatures.


更新头文件中各页面函数的签名：


    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 _drawAlarmBanner(const char* name);

    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);