From 49e3abbb34c889660ae066b2d9b8179e584dcf50 Mon Sep 17 00:00:00 2001 From: yangzhijia <547486824@qq.com> Date: Wed, 22 Jul 2026 11:27:37 +0800 Subject: [PATCH] feat: init --- .claude/settings.local.json | 13 + .gitignore | 5 + .vscode/extensions.json | 10 + data/index.html | 461 ++++++++++++++++++++++++++++++ include/alarm_manager.h | 85 ++++++ include/button_manager.h | 54 ++++ include/config.h | 246 ++++++++++++++++ include/display_manager.h | 58 ++++ include/mqtt_manager.h | 131 +++++++++ include/radar_manager.h | 90 ++++++ include/radar_protocol.h | 114 ++++++++ include/sensor_manager.h | 61 ++++ include/wifi_manager.h | 132 +++++++++ include/wm_strings_zh.h | 262 +++++++++++++++++ platformio.ini | 46 +++ src/alarm_manager.cpp | 266 +++++++++++++++++ src/button_manager.cpp | 88 ++++++ src/display_manager.cpp | 248 ++++++++++++++++ src/main.cpp | 463 ++++++++++++++++++++++++++++++ src/mqtt_manager.cpp | 552 ++++++++++++++++++++++++++++++++++++ src/radar_manager.cpp | 473 ++++++++++++++++++++++++++++++ src/sensor_manager.cpp | 118 ++++++++ src/wifi_manager.cpp | 412 +++++++++++++++++++++++++++ 23 files changed, 4388 insertions(+) create mode 100644 .claude/settings.local.json create mode 100644 .gitignore create mode 100644 .vscode/extensions.json create mode 100644 data/index.html create mode 100644 include/alarm_manager.h create mode 100644 include/button_manager.h create mode 100644 include/config.h create mode 100644 include/display_manager.h create mode 100644 include/mqtt_manager.h create mode 100644 include/radar_manager.h create mode 100644 include/radar_protocol.h create mode 100644 include/sensor_manager.h create mode 100644 include/wifi_manager.h create mode 100644 include/wm_strings_zh.h create mode 100644 platformio.ini create mode 100644 src/alarm_manager.cpp create mode 100644 src/button_manager.cpp create mode 100644 src/display_manager.cpp create mode 100644 src/main.cpp create mode 100644 src/mqtt_manager.cpp create mode 100644 src/radar_manager.cpp create mode 100644 src/sensor_manager.cpp create mode 100644 src/wifi_manager.cpp diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..666f113 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,13 @@ +{ + "permissions": { + "allow": [ + "Bash(\"D:/PlatformIO/penv/Scripts/platformio.exe\" run *)", + "WebFetch(domain:www.micradar.cn)", + "WebFetch(domain:m.elecfans.com)", + "WebSearch", + "WebFetch(domain:boardor.com)", + "Read(//d//**)", + "PowerShell(Get-Content \"D:\\\\R60AFD1_数据手册_V1.2.pdf\" -Raw -Encoding Byte | Out-Null; Write-Host \"File exists, size:\" \\(\\(Get-Item \"D:\\\\R60AFD1_数据手册_V1.2.pdf\"\\).Length\\);)" + ] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..89cc49c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..080e70d --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "platformio.platformio-ide" + ], + "unwantedRecommendations": [ + "ms-vscode.cpptools-extension-pack" + ] +} diff --git a/data/index.html b/data/index.html new file mode 100644 index 0000000..f174f00 --- /dev/null +++ b/data/index.html @@ -0,0 +1,461 @@ + + + + + + 健康监测系统 - 配网设置 + + + +
+ +
+
🏥
+

健康监测系统

+

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

+
+ + +
+
+ 📶 WiFi 设置 +
+
+ + +
+
+ + +
+
+ + +
+
+ ☁️ MQTT 云平台设置 +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ 🏠 环境备注 +
+
+ + +
+
+ 💡 请备注此设备安装的环境位置,方便在云平台上区分不同房间的数据 +
+
+ + +
+
+ 📱 设备设置 +
+
+ + +
+
+ + + + + + +
+ 等待配置... +
+
+ + +
+ + + + diff --git a/include/alarm_manager.h b/include/alarm_manager.h new file mode 100644 index 0000000..527a4dd --- /dev/null +++ b/include/alarm_manager.h @@ -0,0 +1,85 @@ +/** + * @file alarm_manager.h + * @brief 报警管理器 - 双模式 (本地判定 + 服务端指令) + * + * 本地模式: 离线也能用, 跌倒/水浸/心率异常/呼吸异常 本地判定触发 + * 服务端模式: 收到 cmd/{gateid}/alarm, alarm/restore 执行联动 + */ + +#ifndef ALARM_MANAGER_H +#define ALARM_MANAGER_H + +#include +#include "config.h" + +class AlarmManager { +public: + // 告警类型 (本地 + 服务端共用) + enum AlarmType { + ALARM_NONE = 0, + ALARM_FALL, // 跌倒 (本地 + 服务端) + ALARM_WATER_LEAK, // 水浸 (本地 + 服务端) + ALARM_HEART_ABNORMAL, // 心率异常 (本地) + ALARM_BREATH_ABNORMAL, // 呼吸异常 (本地) + ALARM_SERVER, // 服务端下发 (通用) + }; + + AlarmManager(); + ~AlarmManager(); + + bool begin(); + void loop(); + + // ============================================================ + // 本地报警接口 (离线可用) + // ============================================================ + + /** 触发本地报警 */ + void triggerLocal(AlarmType type); + /** 清除本地报警 */ + void clearLocal(AlarmType type); + /** 清除所有本地报警 */ + void clearAllLocal(); + + // ============================================================ + // 服务端指令接口 (协议规范 §5.1, §5.2) + // ============================================================ + + /** 执行服务端告警指令 */ + void executeAlarm(const AlarmCommand& cmd); + /** 执行服务端恢复指令 */ + void executeRestore(const AlarmCommand& cmd); + /** 蜂鸣器测试 */ + void buzzerTest(uint32_t durationSec); + + // ============================================================ + // 状态查询 (上报用) + // ============================================================ + + bool isBuzzerOn() const { return _buzzerOn; } + bool hasActiveAlarm() const { return _activeCount > 0; } + const char* getActiveAlarmType() const; + +private: + void _setBuzzer(bool on); + void _setLED(bool on); + void _updateBuzzer(); + void _updateLED(); + + // 蜂鸣器/LED 状态 + bool _buzzerOn; + bool _ledOn; + uint32_t _buzzerOffTime; // 定时关闭时刻 (0=手动关) + uint32_t _buzzerToggleTime; // 间歇切换时刻 + bool _buzzerBeepState; // 间歇状态 (响/停) + uint8_t _beepCount; // 当前周期哔了几声 + + // 活动报警列表 (最多 4 个同时) + AlarmType _activeAlarms[4]; + uint8_t _activeCount; + + // LED 闪烁 + uint32_t _lastLEDToggle; +}; + +#endif // ALARM_MANAGER_H diff --git a/include/button_manager.h b/include/button_manager.h new file mode 100644 index 0000000..812e3d9 --- /dev/null +++ b/include/button_manager.h @@ -0,0 +1,54 @@ +/** + * @file button_manager.h + * @brief 三按钮管理器 - 去抖 + 长按/短按识别 + * + * 按钮1 (GPIO14): 长按3秒 → 配网 + * 按钮2 (GPIO15): 短按 → 解除报警 + * 按钮3 (GPIO16): 短按 → 翻页 + */ + +#ifndef BUTTON_MANAGER_H +#define BUTTON_MANAGER_H + +#include +#include "config.h" + +typedef void (*ButtonCallback)(void); + +class ButtonManager { +public: + ButtonManager(); + + /** 初始化三个按钮引脚 */ + void begin(); + + /** 主循环轮询 (每 10ms 调用) */ + void loop(); + + /** 注册回调 */ + void onProvisionLongPress(ButtonCallback cb) { _cbProvision = cb; } + void onAlarmDismiss(ButtonCallback cb) { _cbDismiss = cb; } + void onPageSwitch(ButtonCallback cb) { _cbPage = cb; } + +private: + struct ButtonState { + uint8_t pin; + bool pressed; // 当前是否在按下状态 + uint32_t pressTime; // 按下时刻 (ms) + bool longTriggered; // 防止长按时重复触发短按 + }; + + /** 处理单个按钮的状态机 */ + void _processButton(ButtonState& btn, bool hasShort, bool hasLong); + bool _readPin(uint8_t pin); + + ButtonState _btnProvision; + ButtonState _btnDismiss; + ButtonState _btnPage; + + ButtonCallback _cbProvision; + ButtonCallback _cbDismiss; + ButtonCallback _cbPage; +}; + +#endif // BUTTON_MANAGER_H diff --git a/include/config.h b/include/config.h new file mode 100644 index 0000000..9e4ef7d --- /dev/null +++ b/include/config.h @@ -0,0 +1,246 @@ +/** + * @file config.h + * @brief 全局配置文件 - 五劳无感康养监测系统 + * + * 协议版本: V0.08 (2026-07-17) + * 包含: 硬件引脚定义、WiFi/MQTT 默认参数、 + * 雷达协议常量、告警阈值、任务配置等 + */ + +#ifndef CONFIG_H +#define CONFIG_H + +#include + +// ============================================================ +// 1. 硬件引脚定义 (ESP32-S3) +// ============================================================ + +// --- R60ABD1 毫米波雷达 --- +#define RADAR_UART_PORT Serial2 +#define RADAR_RX_PIN 17 +#define RADAR_TX_PIN 1 // UART 发送 (从18挪到1) +#define RADAR_BAUD_RATE 115200 + +// --- 雷达备用 GPIO --- +#define RADAR_GPIO1_PIN 2 // 活跃/静止指示 (从10挪到2) +#define RADAR_GPIO2_PIN 11 // 有人/无人指示 + +// --- DHT11 温湿度传感器 --- +#define DHT11_PIN 4 // 温湿度传感器 + +// --- 水浸传感器 --- +#define WATER_SENSOR_PIN 6 // 数字输入, 低电平=有水 + +// --- 蜂鸣器 (PWM) --- +#define BUZZER_PIN 7 +#define BUZZER_PWM_CHANNEL 0 +#define BUZZER_PWM_FREQ 2000 +#define BUZZER_PWM_RESOLUTION 8 + +// --- 配网按钮 --- +#define CONFIG_BUTTON_PIN 0 // 长按 3 秒复位 WiFi +#define BUTTON_LONG_PRESS_MS 3000 + +// --- 状态指示灯 --- +#define LED_WIFI_PIN 8 // 蓝色 - WiFi 状态 +#define LED_ALARM_PIN 9 // 红色 - 报警指示 + +// --- OLED 显示屏 (SSD1306 128x64 I2C) --- +#define OLED_SDA_PIN 14 // I2C 数据线 +#define OLED_SCL_PIN 15 // I2C 时钟线 + +// --- 交互按钮 --- +#define BTN_PROVISION_PIN 16 // 擦除WiFi配网按钮 (长按3秒) +#define BTN_ALARM_DISMISS_PIN 13 // 解除报警按钮 +#define BTN_PAGE_SWITCH_PIN 10 // 翻页按钮 +#define BTN_DEBOUNCE_MS 50 // 去抖时间 +#define BTN_LONG_PRESS_MS 3000 // 长按判定 + +// ============================================================ +// 2. 设备标识 (协议规范 §2) +// ============================================================ + +#define SALEID_DEFAULT "10100001" // 站点/系统编号 +#define GATEID_DEFAULT "10100001001" // 网关唯一编号 +#define METER_ID_DEFAULT "ENV001" // 感知节点ID (卧室=ENV001, 卫生间=ENV002, 客厅=ENV003) +#define METER_NAME_DEFAULT "RadarDHT11" // 设备型号名称 +#define FW_VERSION "V1.0.0" + +// ============================================================ +// 3. WiFi 默认配置 (配网使用) +// ============================================================ + +#define WIFI_AP_SSID "HealthMonitor-Setup" +#define WIFI_AP_PASSWORD "12345678" +#define WIFI_CONFIG_TIMEOUT 300 +#define WIFI_CONNECT_TIMEOUT 15 +#define WIFI_RETRY_INTERVAL 10000 + +// ============================================================ +// 4. MQTT 默认配置 (协议规范 §2) +// ============================================================ + +#define MQTT_DEFAULT_SERVER "202.110.234.102" +#define MQTT_DEFAULT_PORT 8000 +#define MQTT_DEFAULT_PORT_STR "8000" // WiFiManager 参数用字符串 +#define MQTT_DEFAULT_USER "test" +#define MQTT_DEFAULT_PASS "testtest" +#define MQTT_DEFAULT_DEVICE "10100001001" // 默认网关ID = gateid +#define MQTT_DEFAULT_ENV "主卧" // 默认环境备注 +#undef MQTT_KEEPALIVE +#define MQTT_KEEPALIVE 30 // 30秒 (协议规范) +#define MQTT_RECONNECT_MIN 1000 +#define MQTT_RECONNECT_MAX 60000 + +// ============================================================ +// 5. MQTT 主题定义 (协议规范 §3) +// ============================================================ + +// --- 上行主题 (设备 → 服务端) --- +#define MQTT_TOPIC_REPORT_ALLPOINTS "report/allpoints" // 全量遥测上报 QoS0 +#define MQTT_TOPIC_STATUS_ONLINE "status/%s/online" // 上线通知 QoS1 +#define MQTT_TOPIC_STATUS_OFFLINE "status/%s/offline" // 离线遗嘱 QoS1 +#define MQTT_TOPIC_STATUS_PROVISION "status/%s/provision" // 配网状态 QoS1 +#define MQTT_TOPIC_RESPONSE_CMD "response/%s/cmd" // 指令响应 QoS1 +#define MQTT_TOPIC_RESPONSE_PROVISION "response/%s/provision" // 配网响应 QoS1 + +// --- 下行主题 (服务端 → 设备) --- +#define MQTT_TOPIC_CMD_ALARM "cmd/%s/alarm" // 告警指令 QoS1 +#define MQTT_TOPIC_CMD_ALARM_RESTORE "cmd/%s/alarm/restore" // 告警恢复 QoS1 +#define MQTT_TOPIC_CMD_CONTROL "cmd/%s/control" // 控制指令 QoS1 +#define MQTT_TOPIC_CMD_PROVISION_WIFI "cmd/%s/provision/wifi" // WiFi配网 QoS1 + +// ============================================================ +// 6. 上报配置 (协议规范 §4.1, §7.2) +// ============================================================ + +#define REPORT_INTERVAL_MS 1000 // 遥测上报周期 1秒 +#define REPORT_QOS 0 // 遥测 QoS 0 +#define STATUS_QOS 1 // 状态/响应 QoS 1 + +// ============================================================ +// 7. 报警阈值 (协议规范 §6, 服务端判定用) +// ============================================================ + +#define HEART_RATE_MIN 50 +#define HEART_RATE_MAX 120 +#define BREATH_RATE_MIN 8 +#define BREATH_RATE_MAX 24 +#define TEMP_LOW 5.0f +#define TEMP_HIGH 45.0f +#define HUMI_LOW 15.0f +#define HUMI_HIGH 90.0f +#define WATER_DEBOUNCE_MS 50 + +// ============================================================ +// 8. 任务配置 +// ============================================================ + +#define RADAR_TASK_STACK 4096 +#define RADAR_TASK_PRIORITY 5 +#define RADAR_TASK_CORE 0 + +#define MQTT_TASK_STACK 8192 +#define MQTT_TASK_PRIORITY 4 +#define MQTT_TASK_CORE 1 + +// ============================================================ +// 9. NTP 时间同步 +// ============================================================ + +#define NTP_SERVER "ntp.aliyun.com" +#define NTP_GMT_OFFSET_SEC 28800 // UTC+8 (北京时间) +#define NTP_DAYLIGHT_OFFSET 0 + +// ============================================================ +// 10. LittleFS 配置文件名 +// ============================================================ + +#define CONFIG_FILE_MQTT "/mqtt_config.json" +#define CONFIG_FILE_WIFI "/wifi_config.json" +#define CONFIG_FILE_DEVICE "/device_config.json" + +// ============================================================ +// 11. 结构体: 全量遥测数据 (协议规范 §4.1) +// ============================================================ + +struct TelemetryData { + // --- 设备标识 --- + char saleid[12]; // 站点编号 "10100001" + char gateid[16]; // 网关编号 "10100001001" + char meterId[8]; // 节点ID "ENV001" + char meterName[16]; // 设备型号 "RadarDHT11" + + // --- 环境数据 (DHT11) --- + float temp; // 温度 ℃ + float humi; // 湿度 %RH + bool dhtValid; // 温湿度有效性 + + // --- 雷达: 存在/运动 --- + bool presence; // 有人=1 无人=0 + bool motion; // 有运动=1 无运动=0 + bool fall; // 跌倒检测 1=触发 + bool resident; // 驻留状态 1=驻留 + + // --- 雷达: 生命体征 --- + uint8_t heartRate; // 心率 bpm (无效=0) + uint8_t breathRate; // 呼吸 次/分 (无效=0) + uint8_t body; // 人体能量/体征值 + bool radarValid; // 雷达数据有效性 + bool vitalValid; // 心率呼吸有效性 + + // --- 水浸 --- + bool rainDO; // 1=正常 0=水浸触发 + bool rainValid; // 水浸传感器有效性 + + // --- 设备状态 --- + bool wifiReady; // WiFi/MQTT 连接状态 + uint32_t uptimeSec; // 设备累计运行秒数 + bool buzzerState; // 蜂鸣器状态 1=开启 + + // --- 时间戳 --- + uint32_t lastUpdate; // 最后更新 ms +}; + +// 结构体: 下行报警指令 (协议规范 §5.1, §5.2) +struct AlarmCommand { + char cmdId[32]; // 指令唯一ID + char type[20]; // alarm / alarm_restore + char alarmType[24]; // fall/water_leak/heart_abnormal/breath_abnormal + char alarmLevel[8]; // low/medium/high + char alarmDesc[64]; // 告警描述 + char meterId[8]; // 触发节点ID + char relatedCmdId[32]; // 关联原始告警ID (恢复用) + + // 联动动作 + bool buzzerOn; // 蜂鸣器开关 + uint16_t buzzerDuration; // 蜂鸣持续秒数 (0=持续到恢复) + bool nightLight; // 夜灯 + + uint32_t timestamp; +}; + +// 结构体: 下行控制指令 (协议规范 §5.3) +struct ControlCommand { + char cmdId[32]; + char type[20]; // control + char controlType[24]; // buzzer_test/device_restart/report_interval_set/factory_reset + uint32_t params_duration; // 参数: 持续秒数 + uint32_t params_interval; // 参数: 上报间隔 + uint32_t timestamp; +}; + +// 结构体: 下行配网指令 (协议规范 §5.4) +struct ProvisionCommand { + char cmdId[32]; + char wifi_ssid[33]; + char wifi_password[65]; + char wifi_encrypt[8]; // OPEN/WPA2/WPA3 + char mqtt_broker[64]; + uint16_t mqtt_port; + bool mqtt_useTls; + uint32_t timestamp; +}; + +#endif // CONFIG_H diff --git a/include/display_manager.h b/include/display_manager.h new file mode 100644 index 0000000..96da8d0 --- /dev/null +++ b/include/display_manager.h @@ -0,0 +1,58 @@ +/** + * @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 +#include +#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 diff --git a/include/mqtt_manager.h b/include/mqtt_manager.h new file mode 100644 index 0000000..fb5e58d --- /dev/null +++ b/include/mqtt_manager.h @@ -0,0 +1,131 @@ +/** + * @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 +#include +#include +#include +#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 diff --git a/include/radar_manager.h b/include/radar_manager.h new file mode 100644 index 0000000..30935fd --- /dev/null +++ b/include/radar_manager.h @@ -0,0 +1,90 @@ +/** + * @file radar_manager.h + * @brief R60ABD1 毫米波雷达管理模块 + */ + +#ifndef RADAR_MANAGER_H +#define RADAR_MANAGER_H + +#include +#include "config.h" +#include "radar_protocol.h" + +// 雷达原始数据结构 +struct RadarRawData { + bool presence; // 有人/无人 + bool motion; // 运动 (false=静止) + bool fall; // 跌倒 + bool resident; // 驻留 + + uint8_t heartRate; // 心率 + uint8_t breathRate; // 呼吸 + uint8_t body; // 体动/能量值 + uint16_t distance; // 距离 cm + + bool radarValid; // 雷达有效性 + bool vitalValid; // 生命体征有效性 + + uint32_t lastUpdate; +}; + +typedef void (*RadarCallback)(const RadarRawData& data); + +class RadarManager { +public: + RadarManager(); + ~RadarManager(); + + bool begin(); + void loop(); + + /** 获取最新数据 */ + RadarRawData getData() const; + + /** 数据更新回调 */ + void onDataUpdate(RadarCallback cb); + + /** 发送命令 */ + void sendCommand(uint8_t control, uint8_t command, + const uint8_t* data = nullptr, uint16_t len = 0); + void queryInitStatus(); + void queryDetectRange(); + + bool isInitialized() const { return _initialized; } + + /** 检查雷达是否超时未收到数据 (>5秒无有效帧) */ + bool isTimeout() const { + return (_frameCount > 0) && (millis() - _data.lastUpdate > 5000); + } + /** 雷达数据是否有效 */ + bool isDataValid() const { return _data.radarValid; } + +private: + void _parseByte(uint8_t byte); + void _resetParser(); + bool _validateChecksum(); + void _processCompleteFrame(); + + void _extractPresence(const RadarFrame& frame); + void _extractBreathing(const RadarFrame& frame); + void _extractHeartRate(const RadarFrame& frame); + void _extractSleep(const RadarFrame& frame); + void _extractWorkStatus(const RadarFrame& frame); + void _extractDetectRange(const RadarFrame& frame); + + uint8_t _calcChecksum(const uint8_t* data, uint16_t len); + void _notifyUpdate(); + + HardwareSerial* _serial; + FrameState _state; + RadarFrame _currentFrame; + uint16_t _dataIndex; + RadarRawData _data; + bool _initialized; + + RadarCallback _callback; + uint32_t _frameCount; + uint32_t _errorCount; +}; + +#endif // RADAR_MANAGER_H diff --git a/include/radar_protocol.h b/include/radar_protocol.h new file mode 100644 index 0000000..205fc8e --- /dev/null +++ b/include/radar_protocol.h @@ -0,0 +1,114 @@ +/** + * @file radar_protocol.h + * @brief R60ABD1 60G 毫米波雷达通信协议定义 + * + * 协议: SIP-S v1.0 串口协议 (云帆瑞达 Micradar) + * 帧格式: 帧头(2B) + 控制字(1B) + 命令字(1B) + 长度(2B) + 数据(NB) + 校验(1B) + 帧尾(2B) + */ + +#ifndef RADAR_PROTOCOL_H +#define RADAR_PROTOCOL_H + +#include + +// ============================================================ +// 帧定界符 +// ============================================================ +#define FRAME_HEADER_1 0x53 // 'S' +#define FRAME_HEADER_2 0x59 // 'Y' +#define FRAME_TAIL_1 0x54 // 'T' +#define FRAME_TAIL_2 0x43 // 'C' + +#define FRAME_HEADER_SIZE 2 +#define FRAME_TAIL_SIZE 2 +#define FRAME_MIN_SIZE 8 // 最小帧: 头2+控制1+命令1+长度2+校验1+尾2 = 9 字节 + +// ============================================================ +// 控制字 (Control Word) +// ============================================================ +enum RadarControl : uint8_t { + CTRL_HEARTBEAT = 0x01, // 心跳包 + CTRL_PRODUCT_INFO = 0x02, // 产品信息 + CTRL_OTA = 0x03, // OTA 升级 + CTRL_WORK_STATUS = 0x05, // 工作状态 + CTRL_DETECT_RANGE = 0x07, // 探测范围 + CTRL_PRESENCE = 0x80, // 人体存在 + CTRL_BREATHING = 0x81, // 呼吸检测 + CTRL_SLEEP = 0x84, // 睡眠监测 + CTRL_HEART_RATE = 0x85, // 心率监测 +}; + +// ============================================================ +// 命令字 (Command Word) +// ============================================================ +enum RadarCommand : uint8_t { + CMD_QUERY = 0x01, // 查询命令 + CMD_REPORT = 0x02, // 主动上报 + CMD_SET = 0x03, // 设置命令 + CMD_ACK = 0x04, // 应答 +}; + +// ============================================================ +// 工作状态命令 (CTRL_WORK_STATUS 下的子命令) +// ============================================================ +enum WorkStatusCmd : uint8_t { + WS_INIT_COMPLETE = 0x01, // 初始化完成 + WS_INIT_QUERY = 0x81, // 查询初始化状态 + WS_POS_OUT_BOUNDS = 0x07, // 位置超出范围 +}; + +// ============================================================ +// 呼吸状态 +// ============================================================ +enum RespirationStatus : uint8_t { + RESPI_NORMAL = 0, // 正常 (10~25 次/分) + RESPI_HIGH = 1, // 过高 (>25) + RESPI_LOW = 2, // 过低 (<10) +}; + +// ============================================================ +// 睡眠状态 +// ============================================================ +enum SleepStatus : uint8_t { + SLEEP_AWAKE = 0, // 清醒 + SLEEP_LIGHT = 1, // 浅睡 + SLEEP_DEEP = 2, // 深睡 +}; + +// ============================================================ +// 帧解析状态机 +// ============================================================ +enum FrameState { + STATE_IDLE, // 等待帧头 0x53 + STATE_GOT_HEADER1, // 已收到 0x53, 等待 0x59 + STATE_GOT_HEADER2, // 已收到帧头, 等待控制字 + STATE_GOT_CONTROL, // 已收到控制字, 等待命令字 + STATE_GOT_COMMAND, // 已收到命令字, 等待长度高字节 + STATE_GOT_LEN_H, // 已收到长度高字节, 等待长度低字节 + STATE_GOT_LEN_L, // 已收到长度低字节, 读取数据 + STATE_GOT_DATA, // 数据接收完成, 等待校验 + STATE_GOT_CHECKSUM, // 已收到校验, 等待帧尾 0x54 + STATE_GOT_TAIL1, // 已收到 0x54, 等待 0x43 + STATE_FRAME_COMPLETE, // 帧接收完成 +}; + +// ============================================================ +// 帧缓冲区大小 +// ============================================================ +#define RADAR_FRAME_BUF_SIZE 128 // 单帧最大缓冲区 +#define RADAR_RING_BUF_SIZE 512 // 环形接收缓冲区 + +// ============================================================ +// 解析后的雷达帧数据 (原始) +// ============================================================ +struct RadarFrame { + uint8_t control; + uint8_t command; + uint16_t data_length; + uint8_t data[RADAR_FRAME_BUF_SIZE]; + uint8_t checksum; + bool valid; // 校验通过 + uint32_t timestamp; +}; + +#endif // RADAR_PROTOCOL_H diff --git a/include/sensor_manager.h b/include/sensor_manager.h new file mode 100644 index 0000000..f9bdee2 --- /dev/null +++ b/include/sensor_manager.h @@ -0,0 +1,61 @@ +/** + * @file sensor_manager.h + * @brief 传感器管理器 - DHT11温湿度 + 水浸检测 + */ + +#ifndef SENSOR_MANAGER_H +#define SENSOR_MANAGER_H + +#include +#include +#include "config.h" + +#define DHT_TYPE DHT11 + +typedef void (*SensorCallback)(void); // 数据变化通知 + +class SensorManager { +public: + SensorManager(); + ~SensorManager(); + + /** 初始化 */ + bool begin(); + + /** 读取所有传感器 (定时调用) */ + void read(); + + /** 获取温度 */ + float getTemp() const { return _temp; } + /** 获取湿度 */ + float getHumi() const { return _humi; } + /** DHT11 有效性 */ + bool isDhtValid() const { return _dhtValid; } + /** DHT 是否持续故障 */ + bool isDhtFault() const { return _dhtErrorCnt >= 10; } + + /** 获取水浸状态 */ + bool getRainDO() const { return _rainDO; } + /** 水浸传感器有效性 */ + bool isRainValid() const { return _rainValid; } + + /** 设置数据变化回调 */ + void onChange(SensorCallback cb); + +private: + DHT* _dht; + float _temp; + float _humi; + bool _dhtValid; + uint8_t _dhtErrorCnt; // 连续失败计数 + + bool _rainDO; // 1=正常 0=水浸 + bool _rainValid; + bool _lastRainRaw; + uint32_t _rainChangeTime; + + SensorCallback _callback; + uint32_t _lastReadTime; +}; + +#endif // SENSOR_MANAGER_H diff --git a/include/wifi_manager.h b/include/wifi_manager.h new file mode 100644 index 0000000..c0c6990 --- /dev/null +++ b/include/wifi_manager.h @@ -0,0 +1,132 @@ +/** + * @file wifi_manager.h + * @brief WiFi 配网管理器 + * + * 功能: + * - 自动连接已保存的 WiFi + * - 配网失败/首次启动 → AP 模式 + 强制门户页面 + * - 自定义参数: MQTT 服务器/端口/用户/密码、设备名称、环境备注 + * - 长按按钮重置配置 + * - LED 状态指示 + * - LittleFS 持久化存储 + */ + +#ifndef WIFI_MANAGER_H +#define WIFI_MANAGER_H + +#include +#include +#include +#include +#include "config.h" + +// 设备配置结构体 +struct DeviceConfig { + char mqtt_server[40]; + char mqtt_port[6]; + char mqtt_user[20]; + char mqtt_pass[20]; + char device_id[20]; + char env_note[20]; // 环境备注 (如: "主卧", "客厅", "老人房") +}; + +class WiFiMgmt { +public: + WiFiMgmt(); + ~WiFiMgmt(); + + // 单例指针 (用于静态回调) + static WiFiMgmt* _instance; + + /** + * @brief 初始化 WiFi 管理器 + * @return true=成功 + */ + bool begin(); + + /** + * @brief 主循环, 处理配网流程和按钮检测 + * @return true=WiFi 已连接 + */ + bool loop(); + + /** + * @brief WiFi 是否已连接 + */ + bool isConnected() const; + + /** + * @brief 获取设备配置 + */ + DeviceConfig getConfig() const; + + /** + * @brief 获取设备 ID (用于 MQTT client_id) + */ + const char* getDeviceId() const; + + /** + * @brief 获取环境备注 + */ + const char* getEnvNote() const; + + /** + * @brief 强制进入配网模式 + */ + void startConfigPortal(); + + /** + * @brief 清除所有配置并重启 + */ + void resetSettings(); + +private: + /** + * @brief 尝试连接 WiFi + */ + bool _connectWiFi(); + + /** + * @brief 设置 LED 状态 + * @param mode 0=慢闪(未连接) 1=快闪(配网) 2=常亮(已连接) + */ + void _setLEDMode(uint8_t mode); + + /** + * @brief 检测按钮长按 + */ + bool _checkButtonLongPress(); + + /** + * @brief 加载/保存配置到 LittleFS + */ + bool _loadConfig(); + bool _saveConfig(); + + /** + * @brief 保存 WiFi 参数回调 (WiFiManager 的回调) + */ + static void _saveParamsCallback(); + static void _apCallback(WiFiManager* wm); + + WiFiManager _wm; + DeviceConfig _config; + bool _connected; + uint32_t _lastCheckTime; + uint32_t _buttonPressTime; + bool _buttonPressed; + + // LED 闪烁状态 + uint32_t _ledLastToggle; + bool _ledState; + + // WiFiManager 自定义参数 + WiFiManagerParameter* _param_mqtt_server; + WiFiManagerParameter* _param_mqtt_port; + WiFiManagerParameter* _param_mqtt_user; + WiFiManagerParameter* _param_mqtt_pass; + WiFiManagerParameter* _param_device_id; + WiFiManagerParameter* _param_env_note; +}; + +#endif // WIFI_MANAGER_H diff --git a/include/wm_strings_zh.h b/include/wm_strings_zh.h new file mode 100644 index 0000000..e3398b2 --- /dev/null +++ b/include/wm_strings_zh.h @@ -0,0 +1,262 @@ +/** + * wm_strings_zh.h + * 中文语言包 for WiFiManager + * 基于 wm_strings_en.h 翻译 + * + * WiFiManager, a library for the ESP8266/Arduino platform + * for configuration of WiFi credentials using a Captive Portal + * + * @author Creator tzapu + * @author tablatronix + * @version 0.0.0 + * @license MIT + */ + +#ifndef _WM_STRINGS_ZH_H_ +#define _WM_STRINGS_ZH_H_ + +// strings files must include a consts file! +#include "wm_consts_en.h" // include constants, tokens, routes + +const char WM_LANGUAGE[] PROGMEM = "zh-CN"; // i18n lang code + +const char HTTP_HEAD_START[] PROGMEM = "" +"" +"" +"" +"" +"{v}"; + +const char HTTP_SCRIPT[] PROGMEM = ""; + +const char HTTP_HEAD_END[] PROGMEM = "
"; // {c} = _bodyclass + +const char HTTP_ROOT_MAIN[] PROGMEM = "

{t}

{v}

"; + +const char * const HTTP_PORTAL_MENU[] PROGMEM = { +"

\n", // MENU_WIFI +"

\n", // MENU_WIFINOSCAN +"

\n", // MENU_INFO +"

\n", // MENU_PARAM +"

\n", // MENU_CLOSE +"

\n", // MENU_RESTART +"

\n", // MENU_EXIT +"

\n", // MENU_ERASE +"

\n", // MENU_UPDATE +"

" // MENU_SEP +}; + +const char HTTP_PORTAL_OPTIONS[] PROGMEM = ""; +const char HTTP_ITEM_QI[] PROGMEM = ""; +const char HTTP_ITEM_QP[] PROGMEM = "
{r}%
"; +const char HTTP_ITEM[] PROGMEM = "
{v}{qi}{qp}
"; + +const char HTTP_FORM_START[] PROGMEM = "
"; +const char HTTP_FORM_WIFI[] PROGMEM = "

"; +const char HTTP_FORM_WIFI_END[] PROGMEM = ""; +const char HTTP_FORM_STATIC_HEAD[] PROGMEM = "

"; +const char HTTP_FORM_END[] PROGMEM = "

"; +const char HTTP_FORM_LABEL[] PROGMEM = ""; +const char HTTP_FORM_PARAM_HEAD[] PROGMEM = "

"; +const char HTTP_FORM_PARAM[] PROGMEM = "
\n"; + +const char HTTP_SCAN_LINK[] PROGMEM = "
"; +const char HTTP_SAVED[] PROGMEM = "
正在保存配置
尝试连接 WiFi...
如果连接失败,请重新连接 AP 再试一次
"; +const char HTTP_PARAMSAVED[] PROGMEM = "
配置已保存
"; +const char HTTP_END[] PROGMEM = "
"; +const char HTTP_ERASEBTN[] PROGMEM = "
"; +const char HTTP_UPDATEBTN[] PROGMEM = "
"; +const char HTTP_BACKBTN[] PROGMEM = "

"; + +const char HTTP_STATUS_ON[] PROGMEM = "
已连接 {v}
IP 地址: {i}
"; +const char HTTP_STATUS_OFF[] PROGMEM = "
未连接 {v}{r}
"; +const char HTTP_STATUS_OFFPW[] PROGMEM = "
WiFi 密码错误"; +const char HTTP_STATUS_OFFNOAP[] PROGMEM = "
未找到该 WiFi"; +const char HTTP_STATUS_OFFFAIL[] PROGMEM = "
无法连接"; +const char HTTP_STATUS_NONE[] PROGMEM = "
未设置 WiFi
"; +const char HTTP_BR[] PROGMEM = "
"; + +const char HTTP_STYLE[] PROGMEM = ""; + +#ifndef WM_NOHELP +const char HTTP_HELP[] PROGMEM = + "

可用页面


" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "
页面功能
/主菜单页面
/wifi显示 WiFi 扫描结果并配置连接 (/0wifi 不扫描)
/wifisave保存 WiFi 配置并连接设备
/param参数设置页面
/info系统信息页面
/u固件 OTA 升级
/close关闭弹出窗口,配网服务保持运行
/exit退出配网模式,配网服务将关闭
/restart重启设备
/erase清除 WiFi 配置并重启。清除后设备不会自动连接,需重新配网
" + "

GitHub https://github.com/tzapu/WiFiManager."; +#else +const char HTTP_HELP[] PROGMEM = ""; +#endif + +const char HTTP_UPDATE[] PROGMEM = "上传新固件

* 如果在配网页面内无法使用,请在浏览器中打开 http://192.168.4.1"; +const char HTTP_UPDATE_FAIL[] PROGMEM = "
升级失败!
请重启设备后重试
"; +const char HTTP_UPDATE_SUCCESS[] PROGMEM = "
升级成功!
设备正在重启...
"; + +#ifdef WM_JSTEST +const char HTTP_JS[] PROGMEM = +""; +#endif + +// Info html +#ifdef ESP32 + const char HTTP_INFO_esphead[] PROGMEM = "

ESP32 信息


"; + const char HTTP_INFO_chiprev[] PROGMEM = "
芯片版本
{1}
"; + const char HTTP_INFO_lastreset[] PROGMEM = "
上次复位原因
CPU0: {1}
CPU1: {2}
"; + const char HTTP_INFO_aphost[] PROGMEM = "
AP 主机名
{1}
"; + const char HTTP_INFO_psrsize[] PROGMEM = "
PSRAM 大小
{1} 字节
"; + const char HTTP_INFO_temp[] PROGMEM = "
芯片温度
{1}°C / {2}°F
"; + const char HTTP_INFO_hall[] PROGMEM = "
霍尔传感器
{1}
"; +#else + const char HTTP_INFO_esphead[] PROGMEM = "

ESP8266 信息


"; + const char HTTP_INFO_fchipid[] PROGMEM = "
Flash 芯片 ID
{1}
"; + const char HTTP_INFO_corever[] PROGMEM = "
Core 版本
{1}
"; + const char HTTP_INFO_bootver[] PROGMEM = "
Boot 版本
{1}
"; + const char HTTP_INFO_lastreset[] PROGMEM = "
上次复位原因
{1}
"; + const char HTTP_INFO_flashsize[] PROGMEM = "
实际 Flash 大小
{1} 字节
"; +#endif + +const char HTTP_INFO_memsmeter[] PROGMEM = "
"; +const char HTTP_INFO_memsketch[] PROGMEM = "
内存 - 程序大小
已用 / 总计
{1} / {2} 字节"; +const char HTTP_INFO_freeheap[] PROGMEM = "
内存 - 可用堆
{1} 字节
"; +const char HTTP_INFO_wifihead[] PROGMEM = "

WiFi 信息


"; +const char HTTP_INFO_uptime[] PROGMEM = "
运行时间
{1} 分 {2} 秒
"; +const char HTTP_INFO_chipid[] PROGMEM = "
芯片 ID
{1}
"; +const char HTTP_INFO_idesize[] PROGMEM = "
Flash 大小
{1} 字节
"; +const char HTTP_INFO_sdkver[] PROGMEM = "
SDK 版本
{1}
"; +const char HTTP_INFO_cpufreq[] PROGMEM = "
CPU 频率
{1}MHz
"; +const char HTTP_INFO_apip[] PROGMEM = "
AP IP 地址
{1}
"; +const char HTTP_INFO_apmac[] PROGMEM = "
AP MAC 地址
{1}
"; +const char HTTP_INFO_apssid[] PROGMEM = "
AP SSID
{1}
"; +const char HTTP_INFO_apbssid[] PROGMEM = "
BSSID
{1}
"; +const char HTTP_INFO_stassid[] PROGMEM = "
Station SSID
{1}
"; +const char HTTP_INFO_staip[] PROGMEM = "
Station IP
{1}
"; +const char HTTP_INFO_stagw[] PROGMEM = "
网关地址
{1}
"; +const char HTTP_INFO_stasub[] PROGMEM = "
子网掩码
{1}
"; +const char HTTP_INFO_dnss[] PROGMEM = "
DNS 服务器
{1}
"; +const char HTTP_INFO_host[] PROGMEM = "
主机名
{1}
"; +const char HTTP_INFO_stamac[] PROGMEM = "
Station MAC
{1}
"; +const char HTTP_INFO_conx[] PROGMEM = "
连接状态
{1}
"; +const char HTTP_INFO_autoconx[] PROGMEM = "
自动连接
{1}
"; + +const char HTTP_INFO_aboutver[] PROGMEM = "
WiFiManager 版本
{1}
"; +const char HTTP_INFO_aboutarduino[] PROGMEM = "
Arduino 版本
{1}
"; +const char HTTP_INFO_aboutsdk[] PROGMEM = "
ESP-SDK/IDF 版本
{1}
"; +const char HTTP_INFO_aboutdate[] PROGMEM = "
构建日期
{1}
"; + +// ============================================================ +// 文本常量 +// ============================================================ +const char S_brand[] PROGMEM = "健康监测配网"; +const char S_debugPrefix[] PROGMEM = "*wm:"; +const char S_y[] PROGMEM = "是"; +const char S_n[] PROGMEM = "否"; +const char S_enable[] PROGMEM = "已启用"; +const char S_disable[] PROGMEM = "已禁用"; +const char S_GET[] PROGMEM = "GET"; +const char S_POST[] PROGMEM = "POST"; +const char S_NA[] PROGMEM = "未知"; +const char S_passph[] PROGMEM = "********"; +const char S_titlewifisaved[] PROGMEM = "WiFi 已保存"; +const char S_titlewifisettings[] PROGMEM = "设置已保存"; +const char S_titlewifi[] PROGMEM = "配置 WiFi"; +const char S_titleinfo[] PROGMEM = "系统信息"; +const char S_titleparam[] PROGMEM = "参数设置"; +const char S_titleparamsaved[] PROGMEM = "设置已保存"; +const char S_titleexit[] PROGMEM = "退出配网"; +const char S_titlereset[] PROGMEM = "重置"; +const char S_titleerase[] PROGMEM = "清除配置"; +const char S_titleclose[] PROGMEM = "关闭页面"; +const char S_options[] PROGMEM = "选项"; +const char S_nonetworks[] PROGMEM = "未发现 WiFi 网络,请点击刷新重新扫描。"; +const char S_staticip[] PROGMEM = "静态 IP"; +const char S_staticgw[] PROGMEM = "静态网关"; +const char S_staticdns[] PROGMEM = "静态 DNS"; +const char S_subnet[] PROGMEM = "子网掩码"; +const char S_exiting[] PROGMEM = "正在退出配网模式"; +const char S_resetting[] PROGMEM = "设备将在几秒后重启"; +const char S_closing[] PROGMEM = "可以关闭此页面,配网服务将继续运行"; +const char S_error[] PROGMEM = "发生错误"; +const char S_notfound[] PROGMEM = "页面未找到\n\n"; +const char S_uri[] PROGMEM = "URI: "; +const char S_method[] PROGMEM = "\n请求方法: "; +const char S_args[] PROGMEM = "\n参数: "; +const char S_parampre[] PROGMEM = "param_"; + +// debug strings +const char D_HR[] PROGMEM = "--------------------"; + +// softap ssid default prefix +#ifdef ESP8266 + const char S_ssidpre[] PROGMEM = "ESP"; +#elif defined(ESP32) + const char S_ssidpre[] PROGMEM = "ESP32"; +#else + const char S_ssidpre[] PROGMEM = "WM"; +#endif + +#endif diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..42cb3e8 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,46 @@ +; PlatformIO Project Configuration File +; ESP32-S3 + R60ABD1 60G 毫米波雷达 健康监测系统 + +[env:esp32-s3-devkitc-1] +platform = espressif32 +board = esp32-s3-devkitc-1 +framework = arduino + +; 串口监视器 +monitor_speed = 115200 +monitor_filters = direct + +; Flash 配置 +board_build.flash_mode = qio +board_build.filesystem = littlefs +board_build.partitions = default_8MB.csv +board_build.arduino.memory_type = qio_opi + +; 编译选项 - 优化速度 +build_flags = + -Os + -DCORE_DEBUG_LEVEL=0 + -DARDUINO_USB_CDC_ON_BOOT=1 + -DBOARD_HAS_PSRAM + +; 库依赖 +lib_deps = + ; WiFi 配网 (自带 captive portal) + tzapu/WiFiManager@^2.0.17 + + ; MQTT 客户端 + knolleary/PubSubClient@^2.8 + + ; JSON 解析 (更省内存) + bblanchon/ArduinoJson@^7.1.0 + + ; DHT11 温湿度传感器 + adafruit/DHT sensor library@^1.4.6 + adafruit/Adafruit Unified Sensor@^1.1.14 + + ; OLED 显示屏 (SSD1306 128x64 I2C) + olikraus/U8g2@^2.35.30 + +; 上传配置 +upload_speed = 921600 +upload_port = COM15 diff --git a/src/alarm_manager.cpp b/src/alarm_manager.cpp new file mode 100644 index 0000000..4ddee10 --- /dev/null +++ b/src/alarm_manager.cpp @@ -0,0 +1,266 @@ +/** + * @file alarm_manager.cpp + * @brief 报警管理器 - 双模式实现 + * + * 本地: 跌倒/水浸/心率异常/呼吸异常 → 立即蜂鸣器+LED + * 服务端: cmd/alarm, cmd/alarm/restore → 执行联动 + 回复响应 + */ + +#include "alarm_manager.h" + +AlarmManager::AlarmManager() + : _buzzerOn(false) + , _ledOn(false) + , _buzzerOffTime(0) + , _buzzerToggleTime(0) + , _buzzerBeepState(false) + , _beepCount(0) + , _activeCount(0) + , _lastLEDToggle(0) +{ + for (int i = 0; i < 4; i++) _activeAlarms[i] = ALARM_NONE; +} + +AlarmManager::~AlarmManager() {} + +// ============================================================ +// 初始化 +// ============================================================ + +bool AlarmManager::begin() { + ledcSetup(BUZZER_PWM_CHANNEL, BUZZER_PWM_FREQ, BUZZER_PWM_RESOLUTION); + ledcAttachPin(BUZZER_PIN, BUZZER_PWM_CHANNEL); + ledcWrite(BUZZER_PWM_CHANNEL, 0); // 关 + + pinMode(LED_ALARM_PIN, OUTPUT); + digitalWrite(LED_ALARM_PIN, LOW); + + _buzzerToggleTime = 0; + _buzzerBeepState = false; + _beepCount = 0; + + Serial.println(F("[告警] ✅ 双模式报警 (本地+服务端)")); + return true; +} + +// ============================================================ +// 主循环 +// ============================================================ + +void AlarmManager::loop() { + _updateBuzzer(); + _updateLED(); +} + +// ============================================================ +// 本地报警触发 (离线可用) +// ============================================================ + +void AlarmManager::triggerLocal(AlarmType type) { + // 去重 + for (uint8_t i = 0; i < _activeCount; i++) { + if (_activeAlarms[i] == type) return; + } + + if (_activeCount < 4) { + _activeAlarms[_activeCount++] = type; + } + + const char* name = ""; + switch (type) { + case ALARM_FALL: name = "跌倒"; break; + case ALARM_WATER_LEAK: name = "水浸"; break; + case ALARM_HEART_ABNORMAL: name = "心率异常"; break; + case ALARM_BREATH_ABNORMAL: name = "呼吸异常"; break; + default: name = "未知"; break; + } + + Serial.printf("[告警] 🚨 本地报警触发: %s (活动报警:%d)\n", name, _activeCount); + + // 立即开启蜂鸣器和 LED + _setBuzzer(true); + _setLED(true); +} + +void AlarmManager::clearLocal(AlarmType type) { + for (uint8_t i = 0; i < _activeCount; i++) { + if (_activeAlarms[i] == type) { + for (uint8_t j = i; j < _activeCount - 1; j++) { + _activeAlarms[j] = _activeAlarms[j + 1]; + } + _activeAlarms[_activeCount - 1] = ALARM_NONE; + _activeCount--; + break; + } + } + + Serial.printf("[告警] 本地报警清除, 剩余活动:%d\n", _activeCount); + + // 没有活动报警时关闭 + if (_activeCount == 0) { + _setBuzzer(false); + _setLED(false); + } +} + +void AlarmManager::clearAllLocal() { + _activeCount = 0; + for (int i = 0; i < 4; i++) _activeAlarms[i] = ALARM_NONE; + _setBuzzer(false); + _setLED(false); + Serial.println(F("[告警] 清除所有本地报警")); +} + +// ============================================================ +// 服务端告警指令 (§5.1) +// ============================================================ + +void AlarmManager::executeAlarm(const AlarmCommand& cmd) { + Serial.printf("[告警] ☁️ 服务端告警: %s (等级=%s, 蜂鸣器=%d)\n", + cmd.alarmType, cmd.alarmLevel, cmd.buzzerOn); + + // 映射 alarmType 到本地类型 + AlarmType localType = ALARM_SERVER; + if (strcmp(cmd.alarmType, "fall") == 0) localType = ALARM_FALL; + else if (strcmp(cmd.alarmType, "water_leak") == 0) localType = ALARM_WATER_LEAK; + + if (cmd.buzzerOn) { + triggerLocal(localType); + if (cmd.buzzerDuration > 0) { + _buzzerOffTime = millis() + (uint32_t)cmd.buzzerDuration * 1000; + } else { + _buzzerOffTime = 0; + } + } + + if (cmd.nightLight || cmd.buzzerOn) { + _setLED(true); + } +} + +// ============================================================ +// 服务端恢复指令 (§5.2) +// ============================================================ + +void AlarmManager::executeRestore(const AlarmCommand& cmd) { + Serial.printf("[告警] ☁️ 服务端恢复: %s\n", cmd.alarmType); + + AlarmType localType = ALARM_SERVER; + if (strcmp(cmd.alarmType, "fall") == 0) localType = ALARM_FALL; + else if (strcmp(cmd.alarmType, "water_leak") == 0) localType = ALARM_WATER_LEAK; + + clearLocal(localType); + + if (!cmd.buzzerOn) { + _setBuzzer(false); + } + if (!cmd.nightLight) { + _setLED(false); + } +} + +// ============================================================ +// 蜂鸣器测试 +// ============================================================ + +void AlarmManager::buzzerTest(uint32_t durationSec) { + Serial.printf("[告警] 🔔 蜂鸣器测试 %d秒\n", durationSec); + _setBuzzer(true); + _buzzerOffTime = millis() + durationSec * 1000; +} + +// ============================================================ +// 状态查询 +// ============================================================ + +const char* AlarmManager::getActiveAlarmType() const { + if (_activeCount == 0) return ""; + switch (_activeAlarms[0]) { + case ALARM_FALL: return "fall"; + case ALARM_WATER_LEAK: return "water_leak"; + case ALARM_HEART_ABNORMAL: return "heart_abnormal"; + case ALARM_BREATH_ABNORMAL: return "breath_abnormal"; + case ALARM_SERVER: return "server"; + default: return ""; + } +} + +// ============================================================ +// 内部控制 +// ============================================================ + +void AlarmManager::_setBuzzer(bool on) { + _buzzerOn = on; + _buzzerBeepState = false; + _buzzerToggleTime = 0; + _beepCount = 0; + if (on) { + _buzzerOffTime = 0; // 本地报警=无限哔哔 + } else { + ledcWrite(BUZZER_PWM_CHANNEL, 0); + _buzzerOffTime = 0; + } +} + +void AlarmManager::_setLED(bool on) { + _ledOn = on; + if (!on) digitalWrite(LED_ALARM_PIN, LOW); +} + +void AlarmManager::_updateBuzzer() { + // 定时关闭检查 + if (_buzzerOn && _buzzerOffTime > 0 && millis() >= _buzzerOffTime) { + _buzzerOn = false; + ledcWrite(BUZZER_PWM_CHANNEL, 0); + Serial.println(F("[告警] ⏰ 蜂鸣器定时关闭")); + return; + } + // 本地报警: 三连哔 停 — 循环 (哔哔哔...哔哔哔...) + if (_buzzerOn && _buzzerOffTime == 0) { + uint32_t now = millis(); + // 周期: 3声哔 (每声150ms响+100ms停) + 600ms大停顿 + if (!_buzzerBeepState) { + // 在停顿中,检查是否该下一声 + uint32_t pause = (_beepCount >= 3) ? 600 : 100; + if (now - _buzzerToggleTime >= pause) { + _buzzerToggleTime = now; + if (_beepCount >= 3) _beepCount = 0; // 重置周期 + _buzzerBeepState = true; + ledcWrite(BUZZER_PWM_CHANNEL, 128); + _beepCount++; + } + } else { + // 在响中,150ms后停下 + if (now - _buzzerToggleTime >= 150) { + _buzzerToggleTime = now; + _buzzerBeepState = false; + ledcWrite(BUZZER_PWM_CHANNEL, 0); + } + } + } + // 云端定时: 持续响 + else if (_buzzerOn && _buzzerOffTime > 0) { + ledcWrite(BUZZER_PWM_CHANNEL, 128); + } +} + +void AlarmManager::_updateLED() { + if (!_ledOn) { + digitalWrite(LED_ALARM_PIN, LOW); + return; + } + + // 根据告警类型调整闪烁频率 + uint32_t interval = 500; + for (uint8_t i = 0; i < _activeCount; i++) { + if (_activeAlarms[i] == ALARM_FALL) { + interval = 200; // 跌倒快闪 + break; + } + } + + if (millis() - _lastLEDToggle >= interval) { + _lastLEDToggle = millis(); + digitalWrite(LED_ALARM_PIN, !digitalRead(LED_ALARM_PIN)); + } +} diff --git a/src/button_manager.cpp b/src/button_manager.cpp new file mode 100644 index 0000000..185aa93 --- /dev/null +++ b/src/button_manager.cpp @@ -0,0 +1,88 @@ +/** + * @file button_manager.cpp + * @brief 三按钮管理器实现 - 去抖 + 长按/短按 + */ + +#include "button_manager.h" + +// ============================================================ +// 构造 +// ============================================================ + +ButtonManager::ButtonManager() + : _cbProvision(nullptr) + , _cbDismiss(nullptr) + , _cbPage(nullptr) +{ + _btnProvision = { BTN_PROVISION_PIN, false, 0, false }; + _btnDismiss = { BTN_ALARM_DISMISS_PIN, false, 0, false }; + _btnPage = { BTN_PAGE_SWITCH_PIN, false, 0, false }; +} + +// ============================================================ +// 初始化 +// ============================================================ + +void ButtonManager::begin() { + pinMode(BTN_PROVISION_PIN, INPUT_PULLUP); + pinMode(BTN_ALARM_DISMISS_PIN, INPUT_PULLUP); + pinMode(BTN_PAGE_SWITCH_PIN, INPUT_PULLUP); + + Serial.println(F("[按钮] ✅ 三按钮初始化完成")); + Serial.printf(" 配网: GPIO%d | 解除报警: GPIO%d | 翻页: GPIO%d\n", + BTN_PROVISION_PIN, BTN_ALARM_DISMISS_PIN, BTN_PAGE_SWITCH_PIN); +} + +// ============================================================ +// 主循环轮询 +// ============================================================ + +void ButtonManager::loop() { + // 按钮1: 配网 (仅长按) + _processButton(_btnProvision, false, true); + + // 按钮2: 解除报警 (仅短按) + _processButton(_btnDismiss, true, false); + + // 按钮3: 翻页 (仅短按) + _processButton(_btnPage, true, false); +} + +// ============================================================ +// 单按钮状态机 +// ============================================================ + +void ButtonManager::_processButton(ButtonState& btn, bool hasShort, bool hasLong) { + bool raw = _readPin(btn.pin); // LOW = 按下 + + if (raw && !btn.pressed) { + // 按下沿 + btn.pressed = true; + btn.pressTime = millis(); + btn.longTriggered = false; + } + else if (!raw && btn.pressed) { + // 松开沿 + btn.pressed = false; + uint32_t holdTime = millis() - btn.pressTime; + + if (holdTime < BTN_DEBOUNCE_MS) { + return; // 抖动脉冲,忽略 + } + + if (hasLong && holdTime >= BTN_LONG_PRESS_MS && !btn.longTriggered) { + // 长按触发 + btn.longTriggered = true; + if (_cbProvision) _cbProvision(); + } + else if (hasShort && holdTime >= BTN_DEBOUNCE_MS && holdTime < BTN_LONG_PRESS_MS) { + // 短按触发 + if (btn.pin == BTN_ALARM_DISMISS_PIN && _cbDismiss) _cbDismiss(); + if (btn.pin == BTN_PAGE_SWITCH_PIN && _cbPage) _cbPage(); + } + } +} + +bool ButtonManager::_readPin(uint8_t pin) { + return digitalRead(pin) == LOW; // INPUT_PULLUP: LOW = 按下 +} diff --git a/src/display_manager.cpp b/src/display_manager.cpp new file mode 100644 index 0000000..93c2c2c --- /dev/null +++ b/src/display_manager.cpp @@ -0,0 +1,248 @@ +/** + * @file display_manager.cpp + * @brief OLED 显示管理器实现 - SSD1306 128x64 I2C + */ + +#include "display_manager.h" + +// ============================================================ +// 构造 & 析构 +// ============================================================ + +DisplayManager::DisplayManager() + : _u8g2(U8G2_R0, U8X8_PIN_NONE, OLED_SCL_PIN, OLED_SDA_PIN) + , _pageIndex(0) + , _lastDrawTime(0) + , _initialized(false) +{ +} + +DisplayManager::~DisplayManager() { +} + +// ============================================================ +// 初始化 +// ============================================================ + +bool DisplayManager::begin() { + // SSD1306 常见地址: 0x3C 或 0x3D + // 先用 0x3C 尝试,失败再用 0x3D + _u8g2.setI2CAddress(0x3C << 1); // U8g2 需要左移一位 + if (!_u8g2.begin()) { + // 尝试 0x3D + _u8g2.setI2CAddress(0x3D << 1); + if (!_u8g2.begin()) { + Serial.println(F("[显示] ❌ OLED 未检测到! (I2C 0x3C/0x3D)")); + Serial.println(F("[显示] 请检查接线: VCC→3.3V GND→GND SCL→22 SDA→21")); + Serial.println(F("[显示] 系统将继续运行, 不影响其他功能")); + return false; + } + Serial.println(F("[显示] 使用 I2C 地址 0x3D")); + } else { + Serial.println(F("[显示] 使用 I2C 地址 0x3C")); + } + + _u8g2.enableUTF8Print(); + _u8g2.setFont(u8g2_font_wqy12_t_gb2312); + _initialized = true; + + // 显示启动画面 + _u8g2.clearBuffer(); + _u8g2.setFont(u8g2_font_wqy16_t_gb2312); + _u8g2.drawUTF8(10, 30, "健康监测系统"); + _u8g2.setFont(u8g2_font_wqy12_t_gb2312); + _u8g2.drawUTF8(30, 52, "启动中..."); + _u8g2.sendBuffer(); + + Serial.println(F("[显示] ✅ OLED 初始化完成 (SSD1306 128x64 I2C)")); + return true; +} + +// ============================================================ +// 翻页 +// ============================================================ + +void DisplayManager::nextPage() { + _pageIndex = (_pageIndex + 1) % 4; + _lastDrawTime = 0; // 强制立即刷新 +} + +// ============================================================ +// 主更新入口 +// ============================================================ + +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(); +} + +// ============================================================ +// 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); + + // 温湿度 + _u8g2.setFont(u8g2_font_wqy12_t_gb2312); + _u8g2.drawUTF8(4, 30, "温度:"); + _u8g2.setCursor(52, 30); + if (data.dhtValid) { + _u8g2.printf("%s°C", _fmtFloat(data.temp).c_str()); + } else { + _u8g2.print("-- °C"); + } + + _u8g2.drawUTF8(4, 44, "湿度:"); + _u8g2.setCursor(52, 44); + if (data.dhtValid) { + _u8g2.printf("%s%%", _fmtFloat(data.humi).c_str()); + } else { + _u8g2.print("-- %"); + } + + // WiFi 状态 + _u8g2.drawUTF8(4, 58, "WiFi:"); + _u8g2.setCursor(52, 58); + _u8g2.print(wifiOk ? "已连接" : "未连接"); + + // 运行时间 + 蜂鸣器状态 + _u8g2.setFont(u8g2_font_wqy12_t_gb2312); + _u8g2.setCursor(2, 64); + _u8g2.print(_fmtUptime(data.uptimeSec)); + + if (buzzerOn) { + _u8g2.drawUTF8(104, 64, "🔔"); + } + + // 页码 + _u8g2.setCursor(110, 12); + _u8g2.print("1/4"); +} + +// ============================================================ +// Page 1: 生命体征 +// ============================================================ + +void DisplayManager::_drawPage1(const TelemetryData& data) { + // 标题栏 + _u8g2.setFont(u8g2_font_wqy12_t_gb2312); + _u8g2.drawUTF8(2, 12, "🫀 生命体征"); + _u8g2.drawHLine(0, 15, 128); + + // 心率 + _u8g2.drawUTF8(4, 32, "心率:"); + _u8g2.setCursor(52, 32); + if (data.vitalValid && data.heartRate > 0) { + _u8g2.printf("%d bpm", data.heartRate); + } else { + _u8g2.print("-- bpm"); + } + + // 呼吸 + _u8g2.drawUTF8(4, 48, "呼吸:"); + _u8g2.setCursor(52, 48); + if (data.vitalValid && data.breathRate > 0) { + _u8g2.printf("%d 次/分", data.breathRate); + } else { + _u8g2.print("-- 次/分"); + } + + // 体动/能量值 + _u8g2.drawUTF8(4, 64, "体动:"); + _u8g2.setCursor(52, 64); + _u8g2.printf("%d", data.body); + + // 页码 + _u8g2.setCursor(110, 12); + _u8g2.print("2/4"); +} + +// ============================================================ +// Page 2: 人体状态 +// ============================================================ + +void DisplayManager::_drawPage2(const TelemetryData& data) { + _u8g2.setFont(u8g2_font_wqy12_t_gb2312); + _u8g2.drawUTF8(2, 12, "人体状态"); + _u8g2.drawHLine(0, 15, 128); + + _u8g2.drawUTF8(4, 30, "有人:"); + _u8g2.drawUTF8(60, 30, data.presence ? "V" : "X"); + + _u8g2.drawUTF8(90, 30, "运动:"); + _u8g2.drawUTF8(120, 30, data.motion ? "V" : "X"); + + _u8g2.drawUTF8(4, 44, "跌倒:"); + _u8g2.drawUTF8(60, 44, data.fall ? "!!" : "X"); + + _u8g2.drawUTF8(90, 44, "驻留:"); + _u8g2.drawUTF8(120, 44, data.resident ? "V" : "X"); + + _u8g2.drawUTF8(4, 58, "体动:"); + _u8g2.setCursor(60, 58); + _u8g2.print(data.body); + + _u8g2.drawUTF8(110, 12, "3/4"); +} + +// ============================================================ +// 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); + + _u8g2.drawUTF8(4, 30, "水浸:"); + _u8g2.drawUTF8(60, 30, data.rainDO ? "正常" : "!!报警"); + + _u8g2.drawUTF8(4, 44, "蜂鸣器:"); + _u8g2.drawUTF8(60, 44, buzzerOn ? "响" : "关"); + + _u8g2.drawUTF8(4, 58, "WiFi:"); + _u8g2.drawUTF8(60, 58, wifiOk ? "已连接" : "未连接"); + + _u8g2.drawUTF8(2, 64, _fmtUptime(data.uptimeSec).c_str()); + _u8g2.drawUTF8(110, 12, "4/4"); +} + +// ============================================================ +// 辅助方法 +// ============================================================ + +String DisplayManager::_fmtUptime(uint32_t secs) const { + uint32_t h = secs / 3600; + uint32_t m = (secs % 3600) / 60; + uint32_t s = secs % 60; + char buf[12]; + snprintf(buf, sizeof(buf), "%02d:%02d:%02d", (int)h, (int)m, (int)s); + return String(buf); +} + +String DisplayManager::_fmtFloat(float val) const { + char buf[8]; + snprintf(buf, sizeof(buf), "%.1f", val); + return String(buf); +} diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..72ec88c --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,463 @@ +/** + * @file main.cpp + * @brief 五劳无感康养监测系统 - 主程序 (协议 V0.08) + * + * ESP32-S3 + R60ABD1 60G 毫米波雷达 + DHT11 + 水浸 + * 本地紧急报警 (离线可用) + 服务端告警联动 + */ + +#include +#include +#include "config.h" +#include "radar_protocol.h" +#include "radar_manager.h" +#include "wifi_manager.h" +#include "mqtt_manager.h" +#include "sensor_manager.h" +#include "alarm_manager.h" +#include "display_manager.h" +#include "button_manager.h" + +// ============================================================ +// 全局对象 +// ============================================================ + +RadarManager g_radar; +WiFiMgmt g_wifi; +MQTTManager g_mqtt; +SensorManager g_sensor; +AlarmManager g_alarm; +DisplayManager g_display; +ButtonManager g_buttons; + +// 上报控制 +uint32_t g_lastReportTime = 0; +uint32_t g_sequence = 1; // 上报序号 +uint32_t g_bootTime = 0; // 启动时刻 + +// 本地报警跟踪 (防重复触发) +bool g_localFallActive = false; +bool g_localWaterActive = false; +bool g_localHeartAbnActive = false; +bool g_localBreathAbnActive = false; + +// ============================================================ +// 时间同步 +// ============================================================ + +void syncTime() { + Serial.println(F("[时间] 正在同步时间...")); + configTime(NTP_GMT_OFFSET_SEC, NTP_DAYLIGHT_OFFSET, NTP_SERVER, "pool.ntp.org"); + + struct tm timeinfo; + int retry = 0; + while (!getLocalTime(&timeinfo) && retry < 20) { + delay(500); + Serial.print("."); + retry++; + } + + if (retry < 20) { + Serial.println(); + Serial.printf("[时间] ✅ 时间同步成功: %04d-%02d-%02d %02d:%02d:%02d\n", + timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, + timeinfo.tm_mday, timeinfo.tm_hour, + timeinfo.tm_min, timeinfo.tm_sec); + } else { + Serial.println(F("\n[时间] ⚠️ 时间同步失败, 使用默认时间")); + } +} + +// ============================================================ +// MQTT 告警指令回调 (协议规范 §5.1, §5.2) +// ============================================================ + +void handleAlarmCommand(const AlarmCommand& cmd) { + if (strcmp(cmd.type, "alarm_restore") == 0) { + // 告警恢复 + g_alarm.executeRestore(cmd); + + // 清除对应本地告警状态 + if (strcmp(cmd.alarmType, "fall") == 0) g_localFallActive = false; + else if (strcmp(cmd.alarmType, "water_leak") == 0) g_localWaterActive = false; + else if (strcmp(cmd.alarmType, "heart_abnormal") == 0) g_localHeartAbnActive = false; + else if (strcmp(cmd.alarmType, "breath_abnormal") == 0) g_localBreathAbnActive = false; + } else { + // 告警触发 + g_alarm.executeAlarm(cmd); + } + + // 回复响应 (§4.5) + g_mqtt.publishCmdResponse(cmd.cmdId, cmd.type, cmd.alarmType, + true, "0", ""); +} + +// ============================================================ +// MQTT 控制指令回调 (§5.3) +// ============================================================ + +void handleControlCommand(const ControlCommand& cmd) { + Serial.printf("[控制] 收到控制指令: %s\n", cmd.controlType); + + bool success = true; + const char* errCode = "0"; + const char* errMsg = ""; + + if (strcmp(cmd.controlType, "buzzer_test") == 0) { + uint32_t dur = cmd.params_duration > 0 ? cmd.params_duration : 3; + g_alarm.buzzerTest(dur); + } + else if (strcmp(cmd.controlType, "device_restart") == 0) { + Serial.println(F("[控制] 🔄 远程重启...")); + g_mqtt.publishCmdResponse(cmd.cmdId, "control", "", true, "0", ""); + g_mqtt.publishOffline(); + delay(500); + ESP.restart(); + } + else if (strcmp(cmd.controlType, "factory_reset") == 0) { + Serial.println(F("[控制] 🔄 恢复出厂设置...")); + g_wifi.resetSettings(); + } + else if (strcmp(cmd.controlType, "enter_provision") == 0) { + Serial.println(F("[控制] 📶 进入配网模式")); + g_wifi.startConfigPortal(); + } + else { + success = false; + errCode = "2001"; + errMsg = "不支持的指令类型"; + } + + g_mqtt.publishCmdResponse(cmd.cmdId, "control", "", + success, errCode, errMsg); +} + +// ============================================================ +// MQTT 配网指令回调 (§5.4) +// ============================================================ + +void handleProvisionCommand(const ProvisionCommand& cmd) { + Serial.printf("[配网] 📶 收到配网指令: SSID=%s\n", cmd.wifi_ssid); + + if (strlen(cmd.wifi_ssid) == 0) { + g_mqtt.publishProvisionResponse(cmd.cmdId, false, + "1002", "未提供WiFi SSID"); + return; + } + + // 连接新 WiFi + g_mqtt.publishProvisionStatus("wifi_connecting", ""); + + WiFi.begin(cmd.wifi_ssid, cmd.wifi_password); + int retry = 0; + while (WiFi.status() != WL_CONNECTED && retry < 30) { + delay(500); + Serial.print("."); + retry++; + } + + if (WiFi.status() == WL_CONNECTED) { + Serial.printf("\n[配网] ✅ WiFi 已连接, IP=%s\n", + WiFi.localIP().toString().c_str()); + + // 更新 MQTT 服务器配置 + if (strlen(cmd.mqtt_broker) > 0) { + g_mqtt.setServer(cmd.mqtt_broker, cmd.mqtt_port, + GATEID_DEFAULT, GATEID_DEFAULT); // 正式账号 = gateid + } + + char signal[8]; + snprintf(signal, sizeof(signal), "%d", WiFi.RSSI()); + g_mqtt.publishProvisionResponse(cmd.cmdId, true, "0", "", signal); + + // 重连 MQTT + g_mqtt.disconnect(); + g_mqtt.connect(); + g_mqtt.publishOnline(); + } else { + Serial.println(F("\n[配网] ❌ WiFi 连接失败")); + g_mqtt.publishProvisionStatus("wifi_failed", ""); + g_mqtt.publishProvisionResponse(cmd.cmdId, false, + "1003", "WiFi连接超时"); + } +} + +// ============================================================ +// 构建遥测数据 +// ============================================================ + +TelemetryData buildTelemetry(const RadarRawData& radar) { + TelemetryData t; + memset(&t, 0, sizeof(t)); + + // 设备标识 + strncpy(t.saleid, SALEID_DEFAULT, sizeof(t.saleid) - 1); + strncpy(t.gateid, g_mqtt.getGateId(), sizeof(t.gateid) - 1); + strncpy(t.meterId, METER_ID_DEFAULT, sizeof(t.meterId) - 1); + strncpy(t.meterName, METER_NAME_DEFAULT, sizeof(t.meterName) - 1); + + // DHT11 + t.temp = g_sensor.getTemp(); + t.humi = g_sensor.getHumi(); + t.dhtValid = g_sensor.isDhtValid(); + + // 雷达 + t.presence = radar.presence; + t.motion = radar.motion; + t.fall = radar.fall; + t.resident = radar.resident; + t.heartRate = radar.heartRate; + t.breathRate = radar.breathRate; + t.body = radar.body; + t.radarValid = radar.radarValid; + t.vitalValid = radar.vitalValid; + + // 水浸 + t.rainDO = g_sensor.getRainDO(); + t.rainValid = g_sensor.isRainValid(); + + // 设备状态 + t.wifiReady = g_wifi.isConnected() && g_mqtt.isConnected(); + t.uptimeSec = (millis() - g_bootTime) / 1000; + t.buzzerState = g_alarm.isBuzzerOn(); + t.lastUpdate = millis(); + + return t; +} + +// ============================================================ +// 本地报警检查 (离线可用) +// ============================================================ + +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); + } + } +} + +// ============================================================ +// 调试输出 +// ============================================================ + +void printDebug(const TelemetryData& t) { + static uint32_t lastPrint = 0; + if (millis() - lastPrint < 5000) return; + lastPrint = millis(); + + Serial.println(F("─── 遥测数据 ───")); + Serial.printf(" 环境: T=%.1f°C H=%.1f%% (温湿度:%d)\n", + t.temp, t.humi, t.dhtValid); + Serial.printf(" 存在:%d 运动:%d 跌倒:%d 驻留:%d (雷达:%d)\n", + t.presence, t.motion, t.fall, t.resident, t.radarValid); + Serial.printf(" 心率:%d 呼吸:%d (体征:%d)\n", + t.heartRate, t.breathRate, t.vitalValid); + Serial.printf(" 水浸:%d (水浸有效:%d) | WiFi:%d 蜂鸣器:%d\n", + t.rainDO, t.rainValid, t.wifiReady, t.buzzerState); + Serial.printf(" 运行:%ds 序号:%d\n", t.uptimeSec, g_sequence); + Serial.println(F("─────────────────")); +} + +// ============================================================ +// 雷达任务 - 核心0 专用 +// ============================================================ + +void radarTask(void* param) { + Serial.println(F("[任务] 📡 雷达任务启动 (核心0)")); + TickType_t lastWake = xTaskGetTickCount(); + const TickType_t freq = pdMS_TO_TICKS(10); + + while (true) { + g_radar.loop(); + vTaskDelayUntil(&lastWake, freq); + } +} + +// ============================================================ +// Setup +// ============================================================ + +void setup() { + Serial.begin(115200); + delay(500); + + Serial.println(F("\n")); + Serial.println(F("╔══════════════════════════════════════════╗")); + Serial.println(F("║ 🏥 五劳无感康养监测系统 v1.0.0 ║")); + Serial.println(F("║ ESP32-S3 + R60ABD1 + DHT11 + 水浸 ║")); + Serial.println(F("║ MQTT 协议 V0.08 ║")); + Serial.println(F("╚══════════════════════════════════════════╝")); + + g_bootTime = millis(); + + // 1. 报警模块 (最先, 引脚安全) + g_alarm.begin(); + + // 2. 雷达 + g_radar.begin(); + + // 3. OLED 显示 + g_display.begin(); + + // 4. 按钮 + g_buttons.begin(); + g_buttons.onPageSwitch([]() { + g_display.nextPage(); + }); + g_buttons.onAlarmDismiss([]() { + g_alarm.clearAllLocal(); + }); + g_buttons.onProvisionLongPress([]() { + g_wifi.startConfigPortal(); + }); + + // 5. WiFi (WiFiManager 配网) + g_wifi.begin(); + + // 6. 时间同步 + if (g_wifi.isConnected()) { + syncTime(); + } + + // 7. MQTT + DeviceConfig cfg = g_wifi.getConfig(); + g_mqtt.setServer(cfg.mqtt_server, atoi(cfg.mqtt_port), + cfg.mqtt_user, cfg.mqtt_pass); + g_mqtt.begin(cfg.device_id); + + // 注册 MQTT 回调 + g_mqtt.onAlarmCommand(handleAlarmCommand); + g_mqtt.onControlCommand(handleControlCommand); + g_mqtt.onProvisionCommand(handleProvisionCommand); + + // 连接 MQTT + if (g_wifi.isConnected()) { + g_mqtt.connect(); + } + + // 8. 传感器 + g_sensor.begin(); + + // 9. 雷达任务 (核心0) + xTaskCreatePinnedToCore( + radarTask, "RadarTask", + RADAR_TASK_STACK, nullptr, + RADAR_TASK_PRIORITY, nullptr, + RADAR_TASK_CORE + ); + + Serial.println(F("[系统] ✅ 系统就绪")); + Serial.println(F("══════════════════════════════════════════\n")); +} + +// ============================================================ +// 主循环 (核心1) +// ============================================================ + +void loop() { + uint32_t now = millis(); + + // --- 0. 按钮轮询 --- + g_buttons.loop(); + + // --- 1. WiFi --- + bool wifiOk = g_wifi.loop(); + + // --- 2. MQTT (含自动重连) --- + if (wifiOk) { + g_mqtt.loop(); + + // WiFi 刚连上但 MQTT 还没连 + static bool wasDisconnected = true; + if (g_mqtt.isConnected()) { + if (wasDisconnected) { + // 刚恢复连接, 同步时间 + syncTime(); + wasDisconnected = false; + } + } else { + wasDisconnected = true; + } + } + + // --- 3. 传感器 (独立,任何传感器故障不影响其他) --- + g_sensor.read(); + + // --- 4. 本地报警检查 (离线也工作) --- + RadarRawData radar = g_radar.getData(); + checkLocalAlarms(radar); + + // --- 传感器故障检测 (每30秒检查一次,避免刷屏) --- + static uint32_t lastFaultCheck = 0; + if (now - lastFaultCheck > 30000) { + lastFaultCheck = now; + if (!g_sensor.isDhtValid() && millis() > 60000) { + Serial.println(F("[系统] ⚠️ DHT11 未检测到 (不影响运行)")); + } + if (g_radar.isTimeout()) { + Serial.println(F("[系统] ⚠️ 雷达超时无数据 (不影响运行)")); + } + } + + // --- 5. 报警模块更新 (定时器/闪烁) --- + g_alarm.loop(); + + // --- 6. 遥测上报 (1秒周期) --- + if (now - g_lastReportTime >= REPORT_INTERVAL_MS) { + g_lastReportTime = now; + + TelemetryData tele = buildTelemetry(radar); + g_mqtt.publishAllPoints(tele, g_sequence); + g_sequence++; + + // 调试输出 + OLED 显示 + printDebug(tele); + g_display.update(tele, wifiOk, g_alarm.isBuzzerOn()); + } + + // --- 7. MQTT 配网状态 (WiFiManager 触发时) --- + // WiFiManager 的 captive portal 启动/关闭时更新状态 + // 此逻辑已集成在 wifi_manager 中 + + delay(10); +} diff --git a/src/mqtt_manager.cpp b/src/mqtt_manager.cpp new file mode 100644 index 0000000..ad81785 --- /dev/null +++ b/src/mqtt_manager.cpp @@ -0,0 +1,552 @@ +/** + * @file mqtt_manager.cpp + * @brief MQTT 通信管理器实现 (协议规范 V0.08) + */ + +#include "mqtt_manager.h" + +MQTTManager* MQTTManager::_instance = nullptr; + +// ============================================================ +// 构造函数 & 析构函数 +// ============================================================ + +MQTTManager::MQTTManager() + : _mqtt(_wifiClient) + , _port(1883) + , _reconnectInterval(MQTT_RECONNECT_MIN) + , _lastReconnectAttempt(0) + , _connected(false) + , _alarmCb(nullptr) + , _controlCb(nullptr) + , _provisionCb(nullptr) +{ + memset(_gateid, 0, sizeof(_gateid)); + memset(_server, 0, sizeof(_server)); + memset(_user, 0, sizeof(_user)); + memset(_pass, 0, sizeof(_pass)); + _instance = this; +} + +MQTTManager::~MQTTManager() { + disconnect(); + _instance = nullptr; +} + +// ============================================================ +// 初始化 +// ============================================================ + +bool MQTTManager::begin(const char* gateid) { + strncpy(_gateid, gateid, sizeof(_gateid) - 1); + + Serial.println(F("[MQTT] 初始化 (协议 V0.08)")); + Serial.printf(" 网关ID: %s\n", _gateid); + Serial.printf(" 服务器: %s:%d\n", _server, _port); + + _mqtt.setServer(_server, _port); + _mqtt.setCallback(_mqttCallback); + _mqtt.setKeepAlive(MQTT_KEEPALIVE); + _mqtt.setBufferSize(1024); // JSON 报文较大 + + return true; +} + +// ============================================================ +// 设置服务器 +// ============================================================ + +void MQTTManager::setServer(const char* host, uint16_t port, + const char* user, const char* pass) { + strncpy(_server, host, sizeof(_server) - 1); + _port = port; + strncpy(_user, user, sizeof(_user) - 1); + strncpy(_pass, pass, sizeof(_pass) - 1); + _mqtt.setServer(_server, _port); +} + +// ============================================================ +// 连接 & 断开 +// ============================================================ + +bool MQTTManager::connect() { + if (_connected && _mqtt.connected()) return true; + if (!WiFi.isConnected()) return false; + + Serial.printf("[MQTT] 连接 %s:%d ...\n", _server, _port); + + // Client ID = gateid + String clientId = _gateid; + + // 遗嘱消息 (§4.3): status/{gateid}/offline + String willTopic = _buildTopic(MQTT_TOPIC_STATUS_OFFLINE); + JsonDocument willDoc; + willDoc["gateid"] = _gateid; + willDoc["type"] = "offline"; + willDoc["time"] = ""; // Broker 不会填充时间 + String willPayload; + serializeJson(willDoc, willPayload); + + bool hasAuth = (strlen(_user) > 0); + bool ok = false; + + if (hasAuth) { + ok = _mqtt.connect(clientId.c_str(), + _user, _pass, + willTopic.c_str(), 1, true, willPayload.c_str()); + } else { + ok = _mqtt.connect(clientId.c_str(), + willTopic.c_str(), 1, true, willPayload.c_str()); + } + + if (ok) { + _connected = true; + Serial.println(F("[MQTT] ✅ 已连接!")); + + // 订阅下行主题 (§3.2) + String subAlarm = _buildTopic(MQTT_TOPIC_CMD_ALARM); + String subRestore = _buildTopic(MQTT_TOPIC_CMD_ALARM_RESTORE); + String subControl = _buildTopic(MQTT_TOPIC_CMD_CONTROL); + String subProv = _buildTopic(MQTT_TOPIC_CMD_PROVISION_WIFI); + + _mqtt.subscribe(subAlarm.c_str(), 1); + _mqtt.subscribe(subRestore.c_str(), 1); + _mqtt.subscribe(subControl.c_str(), 1); + _mqtt.subscribe(subProv.c_str(), 1); + + Serial.printf(" +订阅: %s\n", subAlarm.c_str()); + Serial.printf(" +订阅: %s\n", subRestore.c_str()); + Serial.printf(" +订阅: %s\n", subControl.c_str()); + Serial.printf(" +订阅: %s\n", subProv.c_str()); + } else { + _connected = false; + Serial.printf("[MQTT] ❌ 连接失败, rc=%d\n", _mqtt.state()); + } + + return _connected; +} + +void MQTTManager::disconnect() { + if (_connected) { + // 主动发布离线 + publishOffline(); + delay(100); + _mqtt.disconnect(); + _connected = false; + } +} + +bool MQTTManager::isConnected() { + 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(); + JsonObject m = meter.add(); + m["id"] = d.meterId; + m["status"] = "1"; + m["name"] = d.meterName; + + JsonObject v = m["values"].to(); + // 环境数据 + 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"; + + // 序列化 + 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(); + 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; + 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; + 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); + } +} + +// ============================================================ +// 下行报文路由 +// ============================================================ + +void MQTTManager::_handleMessage(const char* topic, const char* payload) { + // 判断 topic 属于哪种指令 + String t(topic); + + if (t.indexOf("/alarm/restore") > 0) { + _parseAlarm(payload, true); + } else if (t.indexOf("/alarm") > 0) { + _parseAlarm(payload, false); + } else if (t.indexOf("/control") > 0) { + _parseControl(payload); + } else if (t.indexOf("/provision/wifi") > 0) { + _parseProvision(payload); + } else { + Serial.printf("[MQTT] ⚠️ 未识别的主题: %s\n", topic); + } +} + +// ============================================================ +// 解析告警指令 (§5.1, §5.2) +// ============================================================ + +void MQTTManager::_parseAlarm(const char* payload, bool isRestore) { + JsonDocument doc; + DeserializationError err = deserializeJson(doc, payload); + if (err) { + Serial.printf("[MQTT] 告警指令 JSON 解析失败: %s\n", err.c_str()); + return; + } + + AlarmCommand cmd; + memset(&cmd, 0, sizeof(cmd)); + + strncpy(cmd.cmdId, doc["cmdId"] | "", sizeof(cmd.cmdId) - 1); + strncpy(cmd.type, doc["type"] | (isRestore ? "alarm_restore" : "alarm"), sizeof(cmd.type) - 1); + strncpy(cmd.alarmType, doc["alarmType"] | "", sizeof(cmd.alarmType) - 1); + strncpy(cmd.alarmLevel, doc["alarmLevel"] | "medium", sizeof(cmd.alarmLevel) - 1); + strncpy(cmd.alarmDesc, doc["alarmDesc"] | "", sizeof(cmd.alarmDesc) - 1); + strncpy(cmd.meterId, doc["meterId"] | "", sizeof(cmd.meterId) - 1); + strncpy(cmd.relatedCmdId, doc["relatedCmdId"] | "", sizeof(cmd.relatedCmdId) - 1); + + JsonObject action = doc["action"]; + if (!action.isNull()) { + cmd.buzzerOn = action["buzzer"] | false; + cmd.buzzerDuration = action["buzzerDuration"] | 0; + cmd.nightLight = action["nightLight"] | false; + } + cmd.timestamp = millis(); + + Serial.printf("[MQTT] 🚨 告警指令: type=%s alarmType=%s buzzer=%d\n", + cmd.type, cmd.alarmType, cmd.buzzerOn); + + if (_alarmCb) { + _alarmCb(cmd); + } +} + +// ============================================================ +// 解析控制指令 (§5.3) +// ============================================================ + +void MQTTManager::_parseControl(const char* payload) { + JsonDocument doc; + DeserializationError err = deserializeJson(doc, payload); + if (err) { + Serial.printf("[MQTT] 控制指令 JSON 解析失败: %s\n", err.c_str()); + return; + } + + ControlCommand cmd; + memset(&cmd, 0, sizeof(cmd)); + + strncpy(cmd.cmdId, doc["cmdId"] | "", sizeof(cmd.cmdId) - 1); + strncpy(cmd.type, doc["type"] | "control", sizeof(cmd.type) - 1); + strncpy(cmd.controlType, doc["controlType"] | "", sizeof(cmd.controlType) - 1); + + JsonObject params = doc["params"]; + if (!params.isNull()) { + cmd.params_duration = params["duration"] | 0; + cmd.params_interval = params["interval"] | 0; + } + cmd.timestamp = millis(); + + Serial.printf("[MQTT] 🎛️ 控制指令: %s\n", cmd.controlType); + + if (_controlCb) { + _controlCb(cmd); + } +} + +// ============================================================ +// 解析配网指令 (§5.4) +// ============================================================ + +void MQTTManager::_parseProvision(const char* payload) { + JsonDocument doc; + DeserializationError err = deserializeJson(doc, payload); + if (err) { + Serial.printf("[MQTT] 配网指令 JSON 解析失败: %s\n", err.c_str()); + return; + } + + ProvisionCommand cmd; + memset(&cmd, 0, sizeof(cmd)); + + strncpy(cmd.cmdId, doc["cmdId"] | "", sizeof(cmd.cmdId) - 1); + + JsonObject wifi = doc["wifi"]; + if (!wifi.isNull()) { + strncpy(cmd.wifi_ssid, wifi["ssid"] | "", sizeof(cmd.wifi_ssid) - 1); + strncpy(cmd.wifi_password, wifi["password"] | "", sizeof(cmd.wifi_password) - 1); + strncpy(cmd.wifi_encrypt, wifi["encryptType"] | "WPA2", sizeof(cmd.wifi_encrypt) - 1); + } + + JsonObject mqtt = doc["mqtt"]; + if (!mqtt.isNull()) { + strncpy(cmd.mqtt_broker, mqtt["broker"] | "", sizeof(cmd.mqtt_broker) - 1); + cmd.mqtt_port = mqtt["port"] | 1883; + cmd.mqtt_useTls = mqtt["useTls"] | false; + } + cmd.timestamp = millis(); + + Serial.printf("[MQTT] 📶 配网指令: SSID=%s Broker=%s:%d\n", + cmd.wifi_ssid, cmd.mqtt_broker, cmd.mqtt_port); + + if (_provisionCb) { + _provisionCb(cmd); + } +} + +// ============================================================ +// 辅助方法 +// ============================================================ + +String MQTTManager::_buildTopic(const char* tmpl) const { + char buf[128]; + snprintf(buf, sizeof(buf), tmpl, _gateid); + return String(buf); +} + +bool MQTTManager::_publish(const char* topicTmpl, const char* payload, + uint8_t qos, bool retain) { + if (!isConnected()) return false; + String topic = _buildTopic(topicTmpl); + bool ok = _mqtt.publish(topic.c_str(), payload, retain); + if (!ok) { + Serial.printf("[MQTT] ⚠️ 发布失败: %s\n", topic.c_str()); + } + return ok; +} diff --git a/src/radar_manager.cpp b/src/radar_manager.cpp new file mode 100644 index 0000000..ccd534f --- /dev/null +++ b/src/radar_manager.cpp @@ -0,0 +1,473 @@ +/** + * @file radar_manager.cpp + * @brief R60ABD1 毫米波雷达管理模块实现 + * + * 状态机逐字节解析 UART 帧, 提取生命体征数据 + */ + +#include "radar_manager.h" + +// ============================================================ +// 构造函数 & 析构函数 +// ============================================================ + +RadarManager::RadarManager() + : _serial(nullptr) + , _state(STATE_IDLE) + , _dataIndex(0) + , _initialized(false) + , _callback(nullptr) + , _frameCount(0) + , _errorCount(0) +{ + memset(&_currentFrame, 0, sizeof(_currentFrame)); + memset(&_data, 0, sizeof(_data)); +} + +RadarManager::~RadarManager() { +} + +// ============================================================ +// 初始化 +// ============================================================ + +bool RadarManager::begin() { + _serial = &RADAR_UART_PORT; + _serial->begin(RADAR_BAUD_RATE, SERIAL_8N1, + RADAR_RX_PIN, RADAR_TX_PIN); + + // 可选: 初始化雷达 GPIO 作为辅助 + pinMode(RADAR_GPIO1_PIN, INPUT); + pinMode(RADAR_GPIO2_PIN, INPUT); + + _resetParser(); + memset(&_data, 0, sizeof(_data)); + _data.lastUpdate = millis(); + + // 不给雷达发任何命令,让它自己初始化并开始自动上报 + // R60ABD1 上电后通常需要 15-30 秒自检,之后自动输出数据 + Serial.println(F("[雷达] 雷达模块初始化完成, 等待自动上报...")); + Serial.println(F("[雷达] (R60ABD1 自检约15-30秒,心率和呼吸需检测到静止人体后输出)")); + return true; +} + +// ============================================================ +// 主循环 - 逐字节解析 UART 数据 +// ============================================================ + +void RadarManager::loop() { + while (_serial->available() > 0) { + uint8_t byte = _serial->read(); + _parseByte(byte); + } + + // 首次收到数据时打印提示 + static bool firstData = false; + if (!firstData && _frameCount > 0) { + firstData = true; + Serial.printf("[雷达] ✅ 开始接收数据 (首帧)\n"); + } + + // 超时检测:如果 10 秒还没收到任何数据,打印一次 + static uint32_t noDataWarn = 0; + if (_frameCount == 0 && millis() - _data.lastUpdate > 10000) { + if (millis() - noDataWarn > 10000) { + noDataWarn = millis(); + Serial.println(F("[雷达] ⚠️ 10秒内未收到雷达数据, 请检查:")); + Serial.println(F("[雷达] 1.雷达供电 5V? 2.TX→GPIO17 RX→GPIO1? 3.交叉了吗?")); + } + } +} + +// ============================================================ +// 帧解析状态机 +// ============================================================ + +void RadarManager::_parseByte(uint8_t byte) { + switch (_state) { + + case STATE_IDLE: + // 等待帧头第一个字节 0x53 + if (byte == FRAME_HEADER_1) { + _resetParser(); + _currentFrame.data[0] = byte; + _state = STATE_GOT_HEADER1; + } + break; + + case STATE_GOT_HEADER1: + // 等待帧头第二个字节 0x59 + if (byte == FRAME_HEADER_2) { + _currentFrame.data[1] = byte; + _state = STATE_GOT_HEADER2; + } else { + // 不是 0x59, 重新开始 + _errorCount++; + _state = STATE_IDLE; + if (byte == FRAME_HEADER_1) { + // 连续 0x53, 重新开始 + _currentFrame.data[0] = byte; + _state = STATE_GOT_HEADER1; + } + } + break; + + case STATE_GOT_HEADER2: + // 读取控制字 + _currentFrame.control = byte; + _state = STATE_GOT_CONTROL; + break; + + case STATE_GOT_CONTROL: + // 读取命令字 + _currentFrame.command = byte; + _state = STATE_GOT_COMMAND; + break; + + case STATE_GOT_COMMAND: + // 读取长度高字节 + _currentFrame.data_length = ((uint16_t)byte) << 8; + _state = STATE_GOT_LEN_H; + break; + + case STATE_GOT_LEN_H: + // 读取长度低字节 + _currentFrame.data_length |= byte; + + // 长度安全检查 + if (_currentFrame.data_length > RADAR_FRAME_BUF_SIZE) { + Serial.printf("[雷达] 帧数据长度异常: %d\n", + _currentFrame.data_length); + _errorCount++; + _state = STATE_IDLE; + return; + } + + _dataIndex = 0; + + // 如果数据长度为 0, 直接跳到校验状态 + if (_currentFrame.data_length == 0) { + _state = STATE_GOT_DATA; + } else { + _state = STATE_GOT_LEN_L; + } + break; + + case STATE_GOT_LEN_L: + // 读取数据字节 + _currentFrame.data[_dataIndex++] = byte; + + if (_dataIndex >= _currentFrame.data_length) { + _state = STATE_GOT_DATA; + } + break; + + case STATE_GOT_DATA: + // 读取校验码 + _currentFrame.checksum = byte; + _state = STATE_GOT_CHECKSUM; + break; + + case STATE_GOT_CHECKSUM: + // 等待帧尾第一个字节 0x54 + if (byte == FRAME_TAIL_1) { + _state = STATE_GOT_TAIL1; + } else { + _errorCount++; + _state = STATE_IDLE; + } + break; + + case STATE_GOT_TAIL1: + // 等待帧尾第二个字节 0x43 + if (byte == FRAME_TAIL_2) { + _state = STATE_FRAME_COMPLETE; + _currentFrame.timestamp = millis(); + _processCompleteFrame(); + _state = STATE_IDLE; + } else { + _errorCount++; + _state = STATE_IDLE; + } + break; + + default: + _state = STATE_IDLE; + break; + } +} + +// ============================================================ +// 帧处理 +// ============================================================ + +void RadarManager::_processCompleteFrame() { + _frameCount++; + + // 校验 + if (!_validateChecksum()) { + return; + } + + _currentFrame.valid = true; + + // 根据控制字提取数据 + switch (_currentFrame.control) { + case CTRL_PRESENCE: + _extractPresence(_currentFrame); + break; + case CTRL_BREATHING: + _extractBreathing(_currentFrame); + break; + case CTRL_HEART_RATE: + _extractHeartRate(_currentFrame); + break; + case CTRL_SLEEP: + _extractSleep(_currentFrame); + break; + case CTRL_WORK_STATUS: + _extractWorkStatus(_currentFrame); + break; + case CTRL_DETECT_RANGE: + _extractDetectRange(_currentFrame); + break; + default: + Serial.printf("[雷达] 未识别控制字=0x%02X 长度=%d\n", + _currentFrame.control, _currentFrame.data_length); + // 打印数据内容辅助分析 + Serial.print(" 数据: "); + for (int i = 0; i < _currentFrame.data_length && i < 16; i++) { + Serial.printf("%02X ", _currentFrame.data[i]); + } + Serial.println(); + break; + } +} + +// ============================================================ +// 数据提取函数 +// ============================================================ + +void RadarManager::_extractPresence(const RadarFrame& frame) { + uint8_t raw = frame.data_length > 0 ? frame.data[0] : 0; + + // cmd 0x00 (len=1): 有人状态 (1=有人 0=无人) + if (frame.command == 0x00 && frame.data_length >= 1) { + _data.presence = (raw == 1); + _data.radarValid = true; + _data.lastUpdate = millis(); + _notifyUpdate(); + } + // cmd 0x01 (len=1): 运动状态/体动 (值越大越活跃) + else if (frame.command == 0x01 && frame.data_length >= 1) { + _data.body = raw; // 0=静止, 1+=有体动 + _data.motion = (raw >= 1); // >=1 有运动 + _data.radarValid = true; + _data.lastUpdate = millis(); + _notifyUpdate(); + } + // cmd 0x03 (len=1): 有人状态备用 (非0=有人) + else if (frame.command == 0x03 && frame.data_length >= 1) { + _data.presence = (raw != 0); + _data.radarValid = true; + _data.lastUpdate = millis(); + _notifyUpdate(); + } + // cmd 0x04 (len=2): [0]=presence [1]=motion + else if (frame.command == 0x04 && frame.data_length >= 2) { + _data.presence = (frame.data[0] != 0); + _data.motion = (frame.data[1] == 0); + _data.radarValid = true; + _data.lastUpdate = millis(); + _notifyUpdate(); + } + // cmd 0x05 (len=6): 完整包 + else if (frame.command == 0x05 && frame.data_length >= 6) { + _data.presence = (frame.data[0] != 0); + _data.motion = (frame.data[1] == 0); + _data.distance = ((uint16_t)frame.data[2] << 8) | frame.data[3]; + _data.body = frame.data[4]; + _data.radarValid = true; + _data.lastUpdate = millis(); + _notifyUpdate(); + } +} + +void RadarManager::_extractBreathing(const RadarFrame& frame) { + if (frame.data_length >= 1) { + uint8_t rate = frame.data[0]; + if (rate != 0x80 && rate != 0x00 && rate <= 40) { + _data.breathRate = rate; + _data.vitalValid = true; + _data.presence = true; // 有呼吸=有人 + _data.radarValid = true; + } + _data.lastUpdate = millis(); + _notifyUpdate(); + } +} + +void RadarManager::_extractHeartRate(const RadarFrame& frame) { + if (frame.data_length >= 1) { + uint8_t rate = frame.data[0]; + if (rate != 0x80 && rate != 0x00 && rate >= 40) { + _data.heartRate = rate; + _data.vitalValid = true; + _data.presence = true; // 有心跳=有人 + _data.radarValid = true; + } + _data.lastUpdate = millis(); + _notifyUpdate(); + } +} + +void RadarManager::_extractSleep(const RadarFrame& frame) { + // 从睡眠数据提取: cmd=0x00/0x01 为入床离床状态 + if (frame.data_length >= 1) { + // data[0]: 0=离床/无驻留, 非0=入床/驻留 + if (frame.command == 0x00 || frame.command == 0x01) { + _data.resident = (frame.data[0] != 0); + _data.lastUpdate = millis(); + _notifyUpdate(); + } + } + + if (frame.data_length >= 3) { + // data[0]: 入床/离床 + // data[1]: 睡眠状态 + // data[2]: 评分 + + // 跌倒检测: 可能在睡眠异常标志中 + if (frame.data_length >= 4) { + _data.fall = (frame.data[3] & 0x01) != 0; + _data.resident = (frame.data[3] & 0x02) != 0; + } + + _data.lastUpdate = millis(); + _notifyUpdate(); + } +} + +void RadarManager::_extractWorkStatus(const RadarFrame& frame) { + if (frame.command == WS_INIT_COMPLETE && frame.data_length >= 1) { + if (frame.data[0] == 0x0F) { + _initialized = true; + Serial.println(F("[雷达] ✅ 雷达初始化完成")); + } + } +} + +void RadarManager::_extractDetectRange(const RadarFrame& frame) { + // 探测范围信息 + if (frame.data_length >= 1) { + Serial.printf("[雷达] 探测范围状态: 0x%02X\n", frame.data[0]); + } +} + +// ============================================================ +// 校验码计算 & 验证 +// ============================================================ + +uint8_t RadarManager::_calcChecksum(const uint8_t* data, uint16_t len) { + uint16_t sum = 0; + for (uint16_t i = 0; i < len; i++) { + sum += data[i]; + } + return (uint8_t)(sum & 0xFF); +} + +bool RadarManager::_validateChecksum() { + // 计算: 帧头(2) + 控制字(1) + 命令字(1) + 长度(2) + 数据(N) 求和取低8位 + uint8_t header[6]; + header[0] = FRAME_HEADER_1; + header[1] = FRAME_HEADER_2; + header[2] = _currentFrame.control; + header[3] = _currentFrame.command; + header[4] = (_currentFrame.data_length >> 8) & 0xFF; + header[5] = _currentFrame.data_length & 0xFF; + + uint16_t sum = 0; + for (int i = 0; i < 6; i++) sum += header[i]; + for (uint16_t i = 0; i < _currentFrame.data_length; i++) sum += _currentFrame.data[i]; + + uint8_t expected = (uint8_t)(sum & 0xFF); + return (expected == _currentFrame.checksum); +} + +// ============================================================ +// 解析器重置 +// ============================================================ + +void RadarManager::_resetParser() { + _state = STATE_IDLE; + _dataIndex = 0; + memset(&_currentFrame, 0, sizeof(_currentFrame)); +} + +// ============================================================ +// 获取最新数据 (线程安全版本) +// ============================================================ + +RadarRawData RadarManager::getData() const { + return _data; +} + +// ============================================================ +// 回调注册 +// ============================================================ + +void RadarManager::onDataUpdate(RadarCallback cb) { + _callback = cb; +} + +void RadarManager::_notifyUpdate() { + if (_callback) { + _callback(_data); + } +} + +// ============================================================ +// 发送命令到雷达 +// ============================================================ + +void RadarManager::sendCommand(uint8_t control, uint8_t command, + const uint8_t* data, uint16_t len) { + if (!_serial) return; + + // 构建帧 + uint8_t frame[RADAR_FRAME_BUF_SIZE + 9]; + uint16_t idx = 0; + + frame[idx++] = FRAME_HEADER_1; + frame[idx++] = FRAME_HEADER_2; + frame[idx++] = control; + frame[idx++] = command; + frame[idx++] = (len >> 8) & 0xFF; + frame[idx++] = len & 0xFF; + + if (data && len > 0) { + memcpy(&frame[idx], data, len); + idx += len; + } + + // 校验: 帧头(2) + 控制(1) + 命令(1) + 长度(2) + 数据(N) + uint16_t sum = 0; + for (uint16_t i = 0; i < idx; i++) sum += frame[i]; + frame[idx++] = (uint8_t)(sum & 0xFF); + + frame[idx++] = FRAME_TAIL_1; + frame[idx++] = FRAME_TAIL_2; + + _serial->write(frame, idx); + _serial->flush(); +} + +void RadarManager::queryInitStatus() { + // 查询初始化状态: 控制字 0x05, 命令字 0x81, 数据 0x0F + uint8_t d = 0x0F; + sendCommand(CTRL_WORK_STATUS, WS_INIT_QUERY, &d, 1); +} + +void RadarManager::queryDetectRange() { + sendCommand(CTRL_DETECT_RANGE, CMD_QUERY); +} diff --git a/src/sensor_manager.cpp b/src/sensor_manager.cpp new file mode 100644 index 0000000..338b0b4 --- /dev/null +++ b/src/sensor_manager.cpp @@ -0,0 +1,118 @@ +/** + * @file sensor_manager.cpp + * @brief 传感器管理器实现 - DHT11 + 水浸 + */ + +#include "sensor_manager.h" + +SensorManager::SensorManager() + : _dht(nullptr) + , _temp(0) + , _humi(0) + , _dhtValid(false) + , _dhtErrorCnt(0) + , _rainDO(true) // 1=正常 + , _rainValid(false) + , _lastRainRaw(true) + , _rainChangeTime(0) + , _callback(nullptr) + , _lastReadTime(0) +{ +} + +SensorManager::~SensorManager() { + if (_dht) delete _dht; +} + +// ============================================================ +// 初始化 +// ============================================================ + +bool SensorManager::begin() { + // DHT11 — 允许失败,不影响水浸 + _dht = new DHT(DHT11_PIN, DHT_TYPE); + _dht->begin(); + _dhtValid = false; + _dhtErrorCnt = 0; + + // 水浸传感器 — 独立初始化 + pinMode(WATER_SENSOR_PIN, INPUT_PULLUP); + _lastRainRaw = (digitalRead(WATER_SENSOR_PIN) == HIGH); // HIGH=正常 + _rainDO = _lastRainRaw; + _rainValid = true; + + // 首次读取 (DHT11 可能失败) + read(); + + Serial.println(F("[传感器] 初始化完成")); + Serial.printf(" DHT11: %s\n", + _dhtValid ? "✅ 正常" : "⚠️ 未检测到 (不影响系统运行)"); + Serial.printf(" 水浸: %s\n", + _rainValid ? "✅ 正常" : "⚠️ 未检测到"); + return true; // 始终成功,不阻塞系统启动 +} + +// ============================================================ +// 读取传感器 +// ============================================================ + +void SensorManager::read() { + // --- DHT11 读取 (独立,失败不影响水浸) --- + if (millis() - _lastReadTime >= 2000) { + _lastReadTime = millis(); + + float t = _dht->readTemperature(); + float h = _dht->readHumidity(); + + if (!isnan(t) && !isnan(h)) { + if (t > -20 && t < 80 && h >= 0 && h <= 100) { + _temp = t; + _humi = h; + _dhtValid = true; + _dhtErrorCnt = 0; // 恢复正常,清零计数 + } else { + _dhtValid = false; + _dhtErrorCnt++; + } + } else { + _dhtValid = false; + _dhtErrorCnt++; + } + + // 连续失败 10 次 (~20秒) 后只报一次,避免刷屏 + if (_dhtErrorCnt == 10) { + Serial.println(F("[传感器] ⚠️ DHT11 持续读取失败 (不影响其他传感器)")); + } + } + + // 水浸传感器读取 (防抖) + bool raw = (digitalRead(WATER_SENSOR_PIN) == HIGH); // HIGH=正常 + if (raw != _lastRainRaw) { + _rainChangeTime = millis(); + _lastRainRaw = raw; + } + + if (millis() - _rainChangeTime >= WATER_DEBOUNCE_MS) { + bool newState = _lastRainRaw; + if (newState != _rainDO) { + _rainDO = newState; + _rainValid = true; + + if (!_rainDO) { + Serial.println(F("[传感器] ⚠️ 检测到水浸!")); + } else { + Serial.println(F("[传感器] ✅ 水浸解除")); + } + + if (_callback) _callback(); + } + } +} + +// ============================================================ +// 回调 +// ============================================================ + +void SensorManager::onChange(SensorCallback cb) { + _callback = cb; +} diff --git a/src/wifi_manager.cpp b/src/wifi_manager.cpp new file mode 100644 index 0000000..f9a7e99 --- /dev/null +++ b/src/wifi_manager.cpp @@ -0,0 +1,412 @@ +/** + * @file wifi_manager.cpp + * @brief WiFi 配网管理器实现 + */ + +#include "wifi_manager.h" + +// 单例指针 +WiFiMgmt* WiFiMgmt::_instance = nullptr; + +// ============================================================ +// 构造函数 & 析构函数 +// ============================================================ + +WiFiMgmt::WiFiMgmt() + : _connected(false) + , _lastCheckTime(0) + , _buttonPressTime(0) + , _buttonPressed(false) + , _ledLastToggle(0) + , _ledState(false) + , _param_mqtt_server(nullptr) + , _param_mqtt_port(nullptr) + , _param_mqtt_user(nullptr) + , _param_mqtt_pass(nullptr) + , _param_device_id(nullptr) + , _param_env_note(nullptr) +{ + memset(&_config, 0, sizeof(_config)); + _instance = this; // 设置单例 +} + +WiFiMgmt::~WiFiMgmt() { + _instance = nullptr; +} + +// ============================================================ +// 初始化 +// ============================================================ + +bool WiFiMgmt::begin() { + Serial.println(F("\n========================================")); + Serial.println(F(" WiFi 配网管理器 初始化")); + Serial.println(F("========================================")); + + // 初始化引脚 + pinMode(CONFIG_BUTTON_PIN, INPUT_PULLUP); + pinMode(LED_WIFI_PIN, OUTPUT); + digitalWrite(LED_WIFI_PIN, LOW); + + // 初始化 LittleFS + if (!LittleFS.begin(true)) { + Serial.println(F("[WiFi] ❌ LittleFS 挂载失败!")); + return false; + } + Serial.println(F("[WiFi] ✅ LittleFS 已挂载")); + + // 加载配置 + if (!_loadConfig()) { + Serial.println(F("[WiFi] 使用默认配置")); + // 设置默认值 + strncpy(_config.mqtt_server, MQTT_DEFAULT_SERVER, sizeof(_config.mqtt_server) - 1); + strncpy(_config.mqtt_port, MQTT_DEFAULT_PORT_STR, sizeof(_config.mqtt_port) - 1); + strncpy(_config.mqtt_user, MQTT_DEFAULT_USER, sizeof(_config.mqtt_user) - 1); + strncpy(_config.mqtt_pass, MQTT_DEFAULT_PASS, sizeof(_config.mqtt_pass) - 1); + strncpy(_config.device_id, MQTT_DEFAULT_DEVICE, sizeof(_config.device_id) - 1); + strncpy(_config.env_note, MQTT_DEFAULT_ENV, sizeof(_config.env_note) - 1); + } + + // 配置 WiFiManager + _wm.setDebugOutput(false); // 关闭调试输出 (避免干扰) + _wm.setConfigPortalTimeout(WIFI_CONFIG_TIMEOUT); + _wm.setConnectTimeout(WIFI_CONNECT_TIMEOUT); + _wm.setTitle("🏥 五劳无感康养监测 - 设备配网"); // 配网页面的中文标题 + _wm.setCustomHeadElement( + "" + ); + WiFi.setAutoReconnect(true); // ESP32 自动重连 + + // 注册保存回调 (必须使用静态函数, 不能使用 lambda) + _wm.setSaveConfigCallback(_saveParamsCallback); + + // 创建自定义参数 + _param_mqtt_server = new WiFiManagerParameter( + "mqtt_server", "MQTT 服务器地址", + _config.mqtt_server, 40); + _param_mqtt_port = new WiFiManagerParameter( + "mqtt_port", "MQTT 端口", + _config.mqtt_port, 6); + _param_mqtt_user = new WiFiManagerParameter( + "mqtt_user", "MQTT 用户名", + _config.mqtt_user, 20); + _param_mqtt_pass = new WiFiManagerParameter( + "mqtt_pass", "MQTT 密码", + _config.mqtt_pass, 20); + _param_device_id = new WiFiManagerParameter( + "device_id", "设备名称", + _config.device_id, 20); + _param_env_note = new WiFiManagerParameter( + "env_note", "🏠 环境备注 (如: 主卧/客厅/老人房)", + _config.env_note, 20); + + // 注册自定义参数 + _wm.addParameter(_param_mqtt_server); + _wm.addParameter(_param_mqtt_port); + _wm.addParameter(_param_mqtt_user); + _wm.addParameter(_param_mqtt_pass); + _wm.addParameter(_param_device_id); + _wm.addParameter(_param_env_note); + + // 设置 AP 模式回调 (静态函数) + _wm.setAPCallback(_apCallback); + + // 尝试连接 WiFi + _connected = _connectWiFi(); + + if (!_connected) { + // 如果自动连接失败, 启动配网页面 + Serial.println(F("[WiFi] 自动连接失败, 启动配网模式...")); + _setLEDMode(1); // 快闪 = 配网模式 + _connected = _connectWiFi(); // 内部会启动配网页面 + } + + if (_connected) { + _setLEDMode(2); // 常亮 = 已连接 + } + + _lastCheckTime = millis(); + return _connected; +} + +// ============================================================ +// 主循环 +// ============================================================ + +bool WiFiMgmt::loop() { + // 检测按钮长按 + if (_checkButtonLongPress()) { + Serial.println(F("[WiFi] ⚠️ 检测到长按, 清除配置并重启...")); + resetSettings(); + return false; + } + + // 检查 WiFi 状态 + if (millis() - _lastCheckTime > WIFI_RETRY_INTERVAL) { + _lastCheckTime = millis(); + + if (WiFi.status() != WL_CONNECTED) { + _connected = false; + _setLEDMode(0); // 慢闪 = 未连接 + Serial.println(F("[WiFi] ⚠️ WiFi 断开, 尝试重连...")); + _connectWiFi(); + } else { + _connected = true; + _setLEDMode(2); // 常亮 + } + } + + // LED 闪烁处理 + if (!_connected) { + uint32_t interval = (_wm.getConfigPortalActive()) ? 300 : 1000; + if (millis() - _ledLastToggle > interval) { + _ledLastToggle = millis(); + _ledState = !_ledState; + digitalWrite(LED_WIFI_PIN, _ledState ? HIGH : LOW); + } + } + + return _connected; +} + +// ============================================================ +// WiFi 连接 +// ============================================================ + +bool WiFiMgmt::_connectWiFi() { + // 检查是否有已保存的配置 + // autoConnect 会先尝试连接, 失败后启动 AP 配网页面 + _connected = _wm.autoConnect(WIFI_AP_SSID, WIFI_AP_PASSWORD); + + if (_connected) { + Serial.println(F("[WiFi] ✅ 已连接!")); + Serial.printf(" WiFi名称: %s\n", WiFi.SSID().c_str()); + Serial.printf(" IP地址: %s\n", WiFi.localIP().toString().c_str()); + Serial.printf(" MAC地址: %s\n", WiFi.macAddress().c_str()); + + // 重新加载配置 (确保获取最新值) + // autoConnect 过程中如果有保存, 回调已写入 LittleFS + _loadConfig(); + + // 同步自定义参数 (如果配置是从 LittleFS 加载的) + _param_mqtt_server->setValue(_config.mqtt_server, sizeof(_config.mqtt_server)); + _param_mqtt_port->setValue(_config.mqtt_port, sizeof(_config.mqtt_port)); + _param_mqtt_user->setValue(_config.mqtt_user, sizeof(_config.mqtt_user)); + _param_mqtt_pass->setValue(_config.mqtt_pass, sizeof(_config.mqtt_pass)); + _param_device_id->setValue(_config.device_id, sizeof(_config.device_id)); + _param_env_note->setValue(_config.env_note, sizeof(_config.env_note)); + + Serial.printf(" 设备名: %s\n", _config.device_id); + Serial.printf(" 环境: %s\n", _config.env_note); + Serial.printf(" MQTT服务器: %s:%s\n", _config.mqtt_server, _config.mqtt_port); + } + + return _connected; +} + +// ============================================================ +// 配网页面 +// ============================================================ + +void WiFiMgmt::startConfigPortal() { + Serial.println(F("[WiFi] 🔧 手动启动配网页面...")); + _setLEDMode(1); + _wm.startConfigPortal(WIFI_AP_SSID, WIFI_AP_PASSWORD); + _connected = (WiFi.status() == WL_CONNECTED); + if (_connected) { + _setLEDMode(2); + } +} + +// ============================================================ +// 重置设置 +// ============================================================ + +void WiFiMgmt::resetSettings() { + Serial.println(F("[WiFi] 清除所有配置...")); + + // 删除 LittleFS 中的配置文件 + if (LittleFS.exists(CONFIG_FILE_WIFI)) LittleFS.remove(CONFIG_FILE_WIFI); + if (LittleFS.exists(CONFIG_FILE_MQTT)) LittleFS.remove(CONFIG_FILE_MQTT); + if (LittleFS.exists(CONFIG_FILE_DEVICE)) LittleFS.remove(CONFIG_FILE_DEVICE); + + // 清除 WiFiManager 的配置 + _wm.resetSettings(); + + // LED 快闪提示 + for (int i = 0; i < 10; i++) { + digitalWrite(LED_WIFI_PIN, !digitalRead(LED_WIFI_PIN)); + delay(100); + } + + Serial.println(F("[WiFi] 🔄 正在重启...")); + delay(500); + ESP.restart(); +} + +// ============================================================ +// 按钮长按检测 +// ============================================================ + +bool WiFiMgmt::_checkButtonLongPress() { + bool pressed = (digitalRead(CONFIG_BUTTON_PIN) == LOW); + + if (pressed && !_buttonPressed) { + // 按下 + _buttonPressed = true; + _buttonPressTime = millis(); + } else if (!pressed && _buttonPressed) { + // 松开 + _buttonPressed = false; + if (millis() - _buttonPressTime >= BUTTON_LONG_PRESS_MS) { + return true; // 长按确认 + } + } + + return false; +} + +// ============================================================ +// LittleFS 配置持久化 +// ============================================================ + +bool WiFiMgmt::_loadConfig() { + if (!LittleFS.exists(CONFIG_FILE_DEVICE)) { + return false; + } + + File file = LittleFS.open(CONFIG_FILE_DEVICE, "r"); + if (!file) return false; + + JsonDocument doc; + DeserializationError err = deserializeJson(doc, file); + file.close(); + + if (err) { + Serial.printf("[WiFi] JSON 解析失败: %s\n", err.c_str()); + return false; + } + + strncpy(_config.mqtt_server, doc["mqtt_server"] | MQTT_DEFAULT_SERVER, sizeof(_config.mqtt_server) - 1); + strncpy(_config.mqtt_port, doc["mqtt_port"] | MQTT_DEFAULT_PORT_STR, sizeof(_config.mqtt_port) - 1); + strncpy(_config.mqtt_user, doc["mqtt_user"] | MQTT_DEFAULT_USER, sizeof(_config.mqtt_user) - 1); + strncpy(_config.mqtt_pass, doc["mqtt_pass"] | MQTT_DEFAULT_PASS, sizeof(_config.mqtt_pass) - 1); + strncpy(_config.device_id, doc["device_id"] | MQTT_DEFAULT_DEVICE, sizeof(_config.device_id) - 1); + strncpy(_config.env_note, doc["env_note"] | MQTT_DEFAULT_ENV, sizeof(_config.env_note) - 1); + + Serial.println(F("[WiFi] ✅ 设备配置已加载")); + Serial.printf(" 设备名: %s, 环境: %s\n", _config.device_id, _config.env_note); + return true; +} + +bool WiFiMgmt::_saveConfig() { + JsonDocument doc; + doc["mqtt_server"] = _config.mqtt_server; + doc["mqtt_port"] = _config.mqtt_port; + doc["mqtt_user"] = _config.mqtt_user; + doc["mqtt_pass"] = _config.mqtt_pass; + doc["device_id"] = _config.device_id; + doc["env_note"] = _config.env_note; + + File file = LittleFS.open(CONFIG_FILE_DEVICE, "w"); + if (!file) { + Serial.println(F("[WiFi] ❌ 无法创建配置文件")); + return false; + } + + if (serializeJson(doc, file) == 0) { + Serial.println(F("[WiFi] ❌ 配置写入失败")); + file.close(); + return false; + } + + file.close(); + Serial.println(F("[WiFi] ✅ 设备配置已保存")); + return true; +} + +// ============================================================ +// WiFiManager 保存参数回调 +// ============================================================ + +// ============================================================ +// 静态回调函数 (WiFiManager 要求 C 函数指针) +// ============================================================ + +void WiFiMgmt::_apCallback(WiFiManager* wm) { + Serial.println(F("[WiFi] 🔧 进入配网模式")); + Serial.printf(" AP WiFi名称: %s\n", WIFI_AP_SSID); + Serial.printf(" AP 密码: %s\n", WIFI_AP_PASSWORD); + Serial.println(F(" 手机连接此 WiFi 即可打开配网页面 (自动弹出)")); +} + +void WiFiMgmt::_saveParamsCallback() { + if (!_instance) return; + + Serial.println(F("[WiFi] 💾 参数已保存")); + + // 从 WiFiManager 参数中读取值 + strncpy(_instance->_config.mqtt_server, _instance->_param_mqtt_server->getValue(), + sizeof(_instance->_config.mqtt_server) - 1); + strncpy(_instance->_config.mqtt_port, _instance->_param_mqtt_port->getValue(), + sizeof(_instance->_config.mqtt_port) - 1); + strncpy(_instance->_config.mqtt_user, _instance->_param_mqtt_user->getValue(), + sizeof(_instance->_config.mqtt_user) - 1); + strncpy(_instance->_config.mqtt_pass, _instance->_param_mqtt_pass->getValue(), + sizeof(_instance->_config.mqtt_pass) - 1); + strncpy(_instance->_config.device_id, _instance->_param_device_id->getValue(), + sizeof(_instance->_config.device_id) - 1); + strncpy(_instance->_config.env_note, _instance->_param_env_note->getValue(), + sizeof(_instance->_config.env_note) - 1); + + _instance->_saveConfig(); + + Serial.println(F("[WiFi] 📋 新配置:")); + Serial.printf(" MQTT: %s:%s\n", + _instance->_config.mqtt_server, + _instance->_config.mqtt_port); + Serial.printf(" 设备: %s\n", _instance->_config.device_id); + Serial.printf(" 环境: %s\n", _instance->_config.env_note); +} + +// ============================================================ +// Getter 方法 +// ============================================================ + +bool WiFiMgmt::isConnected() const { + return _connected && (WiFi.status() == WL_CONNECTED); +} + +DeviceConfig WiFiMgmt::getConfig() const { + return _config; +} + +const char* WiFiMgmt::getDeviceId() const { + return _config.device_id; +} + +const char* WiFiMgmt::getEnvNote() const { + return _config.env_note; +} + +// ============================================================ +// LED 模式控制 +// ============================================================ + +void WiFiMgmt::_setLEDMode(uint8_t mode) { + // mode 由 loop() 中的闪烁逻辑处理 + // 0=慢闪 1=快闪 2=常亮 + switch (mode) { + case 0: + case 1: + // 闪烁由 loop 处理 + break; + case 2: + digitalWrite(LED_WIFI_PIN, HIGH); + _ledState = true; + break; + } +}