feat: init
This commit is contained in:
266
src/alarm_manager.cpp
Normal file
266
src/alarm_manager.cpp
Normal 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
88
src/button_manager.cpp
Normal 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
248
src/display_manager.cpp
Normal 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
463
src/main.cpp
Normal 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
552
src/mqtt_manager.cpp
Normal 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
473
src/radar_manager.cpp
Normal 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
118
src/sensor_manager.cpp
Normal 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
412
src/wifi_manager.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user