feat: init

This commit is contained in:
yangzhijia
2026-07-22 11:27:37 +08:00
commit 49e3abbb34
23 changed files with 4388 additions and 0 deletions

266
src/alarm_manager.cpp Normal file
View File

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

88
src/button_manager.cpp Normal file
View File

@@ -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 = 按下
}

248
src/display_manager.cpp Normal file
View File

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

463
src/main.cpp Normal file
View File

@@ -0,0 +1,463 @@
/**
* @file main.cpp
* @brief 五劳无感康养监测系统 - 主程序 (协议 V0.08)
*
* ESP32-S3 + R60ABD1 60G 毫米波雷达 + DHT11 + 水浸
* 本地紧急报警 (离线可用) + 服务端告警联动
*/
#include <Arduino.h>
#include <WiFi.h>
#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);
}

552
src/mqtt_manager.cpp Normal file
View File

@@ -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<JsonArray>();
JsonObject m = meter.add<JsonObject>();
m["id"] = d.meterId;
m["status"] = "1";
m["name"] = d.meterName;
JsonObject v = m["values"].to<JsonObject>();
// 环境数据
v["Temp"] = String(d.temp, 1);
v["Humi"] = String(d.humi, 1);
v["DhtValid"] = d.dhtValid ? "1" : "0";
// 雷达存在/运动
v["Presence"] = d.presence ? "1" : "0";
v["Motion"] = d.motion ? "1" : "0";
v["Fall"] = d.fall ? "1" : "0";
v["Resident"] = d.resident ? "1" : "0";
// 生命体征
v["Body"] = String(d.body);
v["HeartRate"] = String(d.heartRate);
v["BreathRate"] = String(d.breathRate);
v["RadarValid"] = d.radarValid ? "1" : "0";
v["VitalValid"] = d.vitalValid ? "1" : "0";
// 水浸
v["RainDO"] = d.rainDO ? "1" : "0";
v["RainValid"] = d.rainValid ? "1" : "0";
// 设备状态
v["WifiReady"] = d.wifiReady ? "1" : "0";
v["UptimeSec"] = String(d.uptimeSec);
v["BuzzerState"] = d.buzzerState ? "1" : "0";
// 序列化
String payload;
serializeJson(doc, payload);
// 发布到 report/allpoints (QoS 0)
_mqtt.publish("report/allpoints", payload.c_str());
}
// ============================================================
// 上行: 上线通知 (§4.2)
// ============================================================
void MQTTManager::publishOnline() {
if (!isConnected()) return;
struct tm timeinfo;
char timeStr[24];
if (getLocalTime(&timeinfo)) {
strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo);
} else {
snprintf(timeStr, sizeof(timeStr), "2026-01-01 00:00:00");
}
JsonDocument doc;
doc["gateid"] = _gateid;
doc["type"] = "online";
doc["time"] = timeStr;
doc["firmwareVersion"] = FW_VERSION;
doc["wifiSignal"] = String(WiFi.RSSI());
JsonArray devList = doc["deviceList"].to<JsonArray>();
devList.add(METER_ID_DEFAULT);
String payload;
serializeJson(doc, payload);
_publish(MQTT_TOPIC_STATUS_ONLINE, payload.c_str(), STATUS_QOS);
}
// ============================================================
// 上行: 离线通知 (§4.3)
// ============================================================
void MQTTManager::publishOffline() {
struct tm timeinfo;
char timeStr[24];
if (getLocalTime(&timeinfo)) {
strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo);
} else {
snprintf(timeStr, sizeof(timeStr), "2026-01-01 00:00:00");
}
JsonDocument doc;
doc["gateid"] = _gateid;
doc["type"] = "offline";
doc["time"] = timeStr;
String payload;
serializeJson(doc, payload);
_publish(MQTT_TOPIC_STATUS_OFFLINE, payload.c_str(), STATUS_QOS);
}
// ============================================================
// 上行: 配网状态通知 (§4.4)
// ============================================================
void MQTTManager::publishProvisionStatus(const char* state, const char* bleName) {
struct tm timeinfo;
char timeStr[24];
if (getLocalTime(&timeinfo)) {
strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo);
} else {
snprintf(timeStr, sizeof(timeStr), "2026-01-01 00:00:00");
}
JsonDocument doc;
doc["gateid"] = _gateid;
doc["type"] = "provision_status";
doc["time"] = timeStr;
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;
}

473
src/radar_manager.cpp Normal file
View File

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

118
src/sensor_manager.cpp Normal file
View File

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

412
src/wifi_manager.cpp Normal file
View File

@@ -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(
"<style>"
"body { font-family: 'Microsoft YaHei', 'PingFang SC', sans-serif; }"
".msg .info { font-size: 14px; }"
"</style>"
);
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;
}
}