warning: in the working copy of 'include/config.h', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'include/mqtt_manager.h', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'include/radar_protocol.h', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'include/wifi_manager.h', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'src/wifi_manager.cpp', LF will be replaced by CRLF the next time Git touches it diff --git a/include/config.h b/include/config.h index a7207c1..6d2b281 100644 --- a/include/config.h +++ b/include/config.h @@ -16,10 +16,15 @@ // 1. 硬件引脚定义 (ESP32-S3) // ============================================================ -// --- R60ABD1 毫米波雷达 --- +// --- 雷达型号选择 --- +#define RADAR_TYPE_ABD1 0 // R60ABD1: 心率/呼吸精准, 跌倒一般 +#define RADAR_TYPE_AFD1 1 // R60AFD1: 跌倒/姿态精准, 无心率呼吸 +#define RADAR_TYPE RADAR_TYPE_ABD1 // ABD1 (心率/呼吸/睡眠) + +// --- 毫米波雷达 --- #define RADAR_UART_PORT Serial2 #define RADAR_RX_PIN 17 -#define RADAR_TX_PIN 1 // UART 发送 (从18挪到1) +#define RADAR_TX_PIN 1 #define RADAR_BAUD_RATE 115200 // --- 雷达备用 GPIO --- @@ -72,8 +77,8 @@ // ============================================================ #define WIFI_AP_SSID "HealthMonitor-Setup" -#define WIFI_AP_PASSWORD "12345678" -#define WIFI_CONFIG_TIMEOUT 300 +#define WIFI_AP_PASSWORD "1234567890" +#define WIFI_CONFIG_TIMEOUT 0 // 0=不超时, 一直等待配置 #define WIFI_CONNECT_TIMEOUT 15 #define WIFI_RETRY_INTERVAL 10000 @@ -115,8 +120,9 @@ // 6. 上报配置 (协议规范 §4.1, §7.2) // ============================================================ -#define REPORT_INTERVAL_MS 1000 // 遥测上报周期 1秒 +#define REPORT_INTERVAL_MS 1000 // 遥测上报周期 1秒 (有人时) #define REPORT_QOS 0 // 遥测 QoS 0 +#define PRESENCE_HEARTBEAT_MS 60000 // 无人心跳间隔 60秒 (4G省流量) #define STATUS_QOS 1 // 状态/响应 QoS 1 // ============================================================ @@ -130,6 +136,7 @@ #define TEMP_LOW 5.0f #define TEMP_HIGH 45.0f #define HUMI_LOW 15.0f +#define PRESENCE_HYSTERESIS 8 // 有人无人防抖 (PCB信号抖动时过滤) #define HUMI_HIGH 90.0f #define WATER_DEBOUNCE_MS 50 @@ -167,10 +174,11 @@ struct TelemetryData { // --- 设备标识 --- - char saleid[12]; // 站点编号 "10100001" - char gateid[16]; // 网关编号 "10100001001" - char meterId[8]; // 节点ID "ENV001" + char saleid[12]; // 站点编号 (已弃用) + char gateid[16]; // 网关编号 + char meterId[8]; // 节点ID (已弃用, 新用 gateid+后缀) char meterName[16]; // 设备型号 "RadarDHT11" + char deviceType[16]; // 设备类型枚举 bedroom/bathroom/... // --- 环境数据 (DHT11) --- float temp; // 温度 ℃ @@ -187,6 +195,8 @@ struct TelemetryData { uint8_t heartRate; // 心率 bpm (无效=0) uint8_t breathRate; // 呼吸 次/分 (无效=0) uint8_t body; // 人体能量/体征值 + uint8_t sleepState; // 睡眠状态 0=清醒 1=浅睡 2=深睡 + uint8_t sleepScore; // 睡眠评分 0~100 bool radarValid; // 雷达数据有效性 bool vitalValid; // 心率呼吸有效性 diff --git a/include/mqtt_manager.h b/include/mqtt_manager.h index e830043..fb51036 100644 --- a/include/mqtt_manager.h +++ b/include/mqtt_manager.h @@ -83,6 +83,9 @@ public: /** 本地报警上报 status/{gateid}/alarm (§4.7 扩展) */ void publishLocalAlarm(uint8_t alarmType, bool triggered, const char* desc); + /** 无人极简报文 (仅 Presence+RadarValid, 省4G流量) */ + void publishPresenceOnly(bool presence, bool radarValid, uint32_t sequence); + // ============================================================ // 回调注册 (协议规范 §5) // ============================================================ diff --git a/include/radar_manager.h b/include/radar_manager.h index 30935fd..adbdb08 100644 --- a/include/radar_manager.h +++ b/include/radar_manager.h @@ -21,6 +21,8 @@ struct RadarRawData { uint8_t breathRate; // 呼吸 uint8_t body; // 体动/能量值 uint16_t distance; // 距离 cm + uint8_t sleepState; // 睡眠状态: 0=清醒 1=浅睡 2=深睡 + uint8_t sleepScore; // 睡眠评分: 0~100 bool radarValid; // 雷达有效性 bool vitalValid; // 生命体征有效性 @@ -68,6 +70,7 @@ private: void _extractPresence(const RadarFrame& frame); void _extractBreathing(const RadarFrame& frame); void _extractHeartRate(const RadarFrame& frame); + void _extractFall(const RadarFrame& frame); // AFD1 跌倒 void _extractSleep(const RadarFrame& frame); void _extractWorkStatus(const RadarFrame& frame); void _extractDetectRange(const RadarFrame& frame); @@ -85,6 +88,13 @@ private: RadarCallback _callback; uint32_t _frameCount; uint32_t _errorCount; + uint32_t _lastHeartBeat; // 上次心率数据时间 + uint32_t _lastBreathBeat; // 上次呼吸数据时间 + + // 有人无人防抖 (PCB 信号不稳定时过滤跳变) + bool _stablePresence; // 防抖后的稳定有人/无人 + uint8_t _presenceCnt; // 连续一致计数 + bool _lastRawPresence; // 上一次原始值 }; #endif // RADAR_MANAGER_H diff --git a/include/radar_protocol.h b/include/radar_protocol.h index 205fc8e..302d625 100644 --- a/include/radar_protocol.h +++ b/include/radar_protocol.h @@ -33,9 +33,10 @@ enum RadarControl : uint8_t { CTRL_WORK_STATUS = 0x05, // 工作状态 CTRL_DETECT_RANGE = 0x07, // 探测范围 CTRL_PRESENCE = 0x80, // 人体存在 - CTRL_BREATHING = 0x81, // 呼吸检测 - CTRL_SLEEP = 0x84, // 睡眠监测 - CTRL_HEART_RATE = 0x85, // 心率监测 + CTRL_BREATHING = 0x81, // 呼吸检测 (仅 ABD1) + CTRL_FALL = 0x83, // 跌倒检测 (仅 AFD1) + CTRL_SLEEP = 0x84, // 睡眠监测 (仅 ABD1) + CTRL_HEART_RATE = 0x85, // 心率监测 (仅 ABD1) }; // ============================================================ @@ -73,6 +74,7 @@ enum SleepStatus : uint8_t { SLEEP_AWAKE = 0, // 清醒 SLEEP_LIGHT = 1, // 浅睡 SLEEP_DEEP = 2, // 深睡 + SLEEP_ABSENT = 3, // 离床/无人/无效 }; // ============================================================ diff --git a/include/wifi_manager.h b/include/wifi_manager.h index 3e3a904..b6c327e 100644 --- a/include/wifi_manager.h +++ b/include/wifi_manager.h @@ -1,10 +1,10 @@ /** * @file wifi_manager.h - * @brief WiFi 配网管理器 + * @brief WiFi 配网管理器 (AP + WebServer) * * 功能: * - 自动连接已保存的 WiFi - * - 配网失败/首次启动 → AP 模式 + 强制门户页面 + * - 配网失败/首次启动 → AP 模式 + Captive Portal * - 自定义参数: MQTT 服务器/端口/用户/密码、设备名称、环境备注 * - 长按按钮重置配置 * - LED 状态指示 @@ -15,8 +15,14 @@ #define WIFI_MANAGER_H #include -#include +#include +#include +#include #include +#include +#include +#include +#include #include #include "config.h" @@ -27,7 +33,10 @@ struct DeviceConfig { char mqtt_user[20]; char mqtt_pass[20]; char device_id[20]; - char env_note[20]; // 环境备注 (如: "主卧", "客厅", "老人房") + char env_note[20]; // 环境备注 (房间名) + char env_type[16]; // 环境类型 bedroom/bathroom/... + char wifi_ssid[33]; // WiFi SSID + char wifi_pass[65]; // WiFi 密码 }; class WiFiMgmt { @@ -38,100 +47,66 @@ public: // 单例指针 (用于静态回调) static WiFiMgmt* _instance; - /** - * @brief 初始化 WiFi 管理器 - * @return true=成功 - */ bool begin(); - - /** - * @brief 主循环, 处理配网流程和按钮检测 - * @return true=WiFi 已连接 - */ bool loop(); - - /** - * @brief WiFi 是否已连接 - */ bool isConnected() const; - - /** - * @brief 获取设备配置 - */ + bool justProvisioned(); // 刚配网成功 (读取后自动清除) DeviceConfig getConfig() const; - - /** - * @brief 获取设备 ID (用于 MQTT client_id) - */ const char* getDeviceId() const; - - /** - * @brief 获取环境备注 - */ const char* getEnvNote() const; - - /** - * @brief 强制进入配网模式 - */ void startConfigPortal(); - - /** - * @brief 清除所有配置并重启 - */ + void handleBLEProvision(const char* json); // BLE 配网回调 void resetSettings(); private: - /** - * @brief 尝试连接 WiFi - */ - bool _connectWiFi(); - - /** - * @brief 设置 LED 模式 (线程安全, 无阻塞) - * @param mode 0=灭 1=慢闪(断连1s) 2=快闪(配网300ms) 3=常亮(已连接) - */ + bool _connectWiFi(); // 仅尝试已保存凭据 + bool _tryConnect(const char* ssid, const char* pass); // 尝试连接指定 WiFi + bool _loadConfig(); + bool _saveConfig(); void _setLEDMode(uint8_t mode); - - /** - * @brief LED 闪烁独立任务 (FreeRTOS, 不受主循环阻塞影响) - */ static void _ledTaskFunc(void* param); - - /** - * @brief 检测按钮长按 - */ bool _checkButtonLongPress(); - /** - * @brief 加载/保存配置到 LittleFS - */ - bool _loadConfig(); - bool _saveConfig(); - - /** - * @brief 保存 WiFi 参数回调 (WiFiManager 的回调) - */ - static void _saveParamsCallback(); - static void _apCallback(WiFiManager* wm); - - WiFiManager _wm; - DeviceConfig _config; - bool _connected; - uint32_t _lastCheckTime; - uint32_t _buttonPressTime; - bool _buttonPressed; - - // LED 独立任务 (不被 startConfigPortal 阻塞影响) + // Web 服务器 + void _startPortal(); + void _stopPortal(); + void _handleRoot(); + void _handleSave(); + void _handleReset(); + void _updateWifiScan(); // WiFi 扫描缓存 + String _htmlPage(); // 返回配网 HTML 页面 + + // BLE 蓝牙配网 + void _startBLE(); + void _stopBLE(); + + WebServer* _server; + DNSServer* _dns; + DeviceConfig _config; + bool _connected; + bool _portalActive; + bool _portalStopFlag; // 延迟关闭标记 + uint32_t _portalStartTime; + String _pendingSSID; + String _pendingPass; + bool _provisionSuccess; // 刚配网成功标记 + uint32_t _lastCheckTime; + uint32_t _buttonPressTime; + bool _buttonPressed; + + // WiFi 扫描缓存 (避免每次打开页面都扫描3-8秒) + String _wifiScanCache; + uint32_t _lastScanTime; + + // LED 独立任务 TaskHandle_t _ledTaskHandle; volatile uint8_t _ledMode; // 0=灭 1=慢闪 2=快闪 3=常亮 - // WiFiManager 自定义参数 - WiFiManagerParameter* _param_mqtt_server; - WiFiManagerParameter* _param_mqtt_port; - WiFiManagerParameter* _param_mqtt_user; - WiFiManagerParameter* _param_mqtt_pass; - WiFiManagerParameter* _param_device_id; - WiFiManagerParameter* _param_env_note; + // BLE + BLEServer* _bleServer; + BLECharacteristic* _bleInfoChar; // READ: 设备信息 + BLECharacteristic* _bleChar; // WRITE+NOTIFY: 配网 + BLEAdvertising* _bleAdv; }; #endif // WIFI_MANAGER_H diff --git a/include/wm_strings_zh.h b/include/wm_strings_zh.h deleted file mode 100644 index e3398b2..0000000 --- a/include/wm_strings_zh.h +++ /dev/null @@ -1,262 +0,0 @@ -/** - * wm_strings_zh.h - * 中文语言包 for WiFiManager - * 基于 wm_strings_en.h 翻译 - * - * WiFiManager, a library for the ESP8266/Arduino platform - * for configuration of WiFi credentials using a Captive Portal - * - * @author Creator tzapu - * @author tablatronix - * @version 0.0.0 - * @license MIT - */ - -#ifndef _WM_STRINGS_ZH_H_ -#define _WM_STRINGS_ZH_H_ - -// strings files must include a consts file! -#include "wm_consts_en.h" // include constants, tokens, routes - -const char WM_LANGUAGE[] PROGMEM = "zh-CN"; // i18n lang code - -const char HTTP_HEAD_START[] PROGMEM = "" -"" -"" -"" -"" -"{v}"; - -const char HTTP_SCRIPT[] PROGMEM = ""; - -const char HTTP_HEAD_END[] PROGMEM = "
"; // {c} = _bodyclass - -const char HTTP_ROOT_MAIN[] PROGMEM = "

{t}

{v}

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

\n", // MENU_WIFI -"

\n", // MENU_WIFINOSCAN -"

\n", // MENU_INFO -"

\n", // MENU_PARAM -"

\n", // MENU_CLOSE -"

\n", // MENU_RESTART -"

\n", // MENU_EXIT -"

\n", // MENU_ERASE -"

\n", // MENU_UPDATE -"

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

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

"; -const char HTTP_FORM_END[] PROGMEM = "

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

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

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

可用页面


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

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

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

ESP32 信息


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

ESP8266 信息


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

WiFi 信息


"; -const char HTTP_INFO_uptime[] PROGMEM = "
运行时间
{1} 分 {2} 秒
"; -const char HTTP_INFO_chipid[] PROGMEM = "
芯片 ID
{1}
"; -const char HTTP_INFO_idesize[] PROGMEM = "
Flash 大小
{1} 字节
"; -const char HTTP_INFO_sdkver[] PROGMEM = "
SDK 版本
{1}
"; -const char HTTP_INFO_cpufreq[] PROGMEM = "
CPU 频率
{1}MHz
"; -const char HTTP_INFO_apip[] PROGMEM = "
AP IP 地址
{1}
"; -const char HTTP_INFO_apmac[] PROGMEM = "
AP MAC 地址
{1}
"; -const char HTTP_INFO_apssid[] PROGMEM = "
AP SSID
{1}
"; -const char HTTP_INFO_apbssid[] PROGMEM = "
BSSID
{1}
"; -const char HTTP_INFO_stassid[] PROGMEM = "
Station SSID
{1}
"; -const char HTTP_INFO_staip[] PROGMEM = "
Station IP
{1}
"; -const char HTTP_INFO_stagw[] PROGMEM = "
网关地址
{1}
"; -const char HTTP_INFO_stasub[] PROGMEM = "
子网掩码
{1}
"; -const char HTTP_INFO_dnss[] PROGMEM = "
DNS 服务器
{1}
"; -const char HTTP_INFO_host[] PROGMEM = "
主机名
{1}
"; -const char HTTP_INFO_stamac[] PROGMEM = "
Station MAC
{1}
"; -const char HTTP_INFO_conx[] PROGMEM = "
连接状态
{1}
"; -const char HTTP_INFO_autoconx[] PROGMEM = "
自动连接
{1}
"; - -const char HTTP_INFO_aboutver[] PROGMEM = "
WiFiManager 版本
{1}
"; -const char HTTP_INFO_aboutarduino[] PROGMEM = "
Arduino 版本
{1}
"; -const char HTTP_INFO_aboutsdk[] PROGMEM = "
ESP-SDK/IDF 版本
{1}
"; -const char HTTP_INFO_aboutdate[] PROGMEM = "
构建日期
{1}
"; - -// ============================================================ -// 文本常量 -// ============================================================ -const char S_brand[] PROGMEM = "健康监测配网"; -const char S_debugPrefix[] PROGMEM = "*wm:"; -const char S_y[] PROGMEM = "是"; -const char S_n[] PROGMEM = "否"; -const char S_enable[] PROGMEM = "已启用"; -const char S_disable[] PROGMEM = "已禁用"; -const char S_GET[] PROGMEM = "GET"; -const char S_POST[] PROGMEM = "POST"; -const char S_NA[] PROGMEM = "未知"; -const char S_passph[] PROGMEM = "********"; -const char S_titlewifisaved[] PROGMEM = "WiFi 已保存"; -const char S_titlewifisettings[] PROGMEM = "设置已保存"; -const char S_titlewifi[] PROGMEM = "配置 WiFi"; -const char S_titleinfo[] PROGMEM = "系统信息"; -const char S_titleparam[] PROGMEM = "参数设置"; -const char S_titleparamsaved[] PROGMEM = "设置已保存"; -const char S_titleexit[] PROGMEM = "退出配网"; -const char S_titlereset[] PROGMEM = "重置"; -const char S_titleerase[] PROGMEM = "清除配置"; -const char S_titleclose[] PROGMEM = "关闭页面"; -const char S_options[] PROGMEM = "选项"; -const char S_nonetworks[] PROGMEM = "未发现 WiFi 网络,请点击刷新重新扫描。"; -const char S_staticip[] PROGMEM = "静态 IP"; -const char S_staticgw[] PROGMEM = "静态网关"; -const char S_staticdns[] PROGMEM = "静态 DNS"; -const char S_subnet[] PROGMEM = "子网掩码"; -const char S_exiting[] PROGMEM = "正在退出配网模式"; -const char S_resetting[] PROGMEM = "设备将在几秒后重启"; -const char S_closing[] PROGMEM = "可以关闭此页面,配网服务将继续运行"; -const char S_error[] PROGMEM = "发生错误"; -const char S_notfound[] PROGMEM = "页面未找到\n\n"; -const char S_uri[] PROGMEM = "URI: "; -const char S_method[] PROGMEM = "\n请求方法: "; -const char S_args[] PROGMEM = "\n参数: "; -const char S_parampre[] PROGMEM = "param_"; - -// debug strings -const char D_HR[] PROGMEM = "--------------------"; - -// softap ssid default prefix -#ifdef ESP8266 - const char S_ssidpre[] PROGMEM = "ESP"; -#elif defined(ESP32) - const char S_ssidpre[] PROGMEM = "ESP32"; -#else - const char S_ssidpre[] PROGMEM = "WM"; -#endif - -#endif diff --git a/platformio.ini b/platformio.ini index 1c12f78..250f8dd 100644 --- a/platformio.ini +++ b/platformio.ini @@ -9,6 +9,8 @@ framework = arduino ; 串口监视器 monitor_speed = 115200 monitor_filters = direct +monitor_rts = 0 +monitor_dtr = 0 ; Flash 配置 board_build.flash_mode = qio @@ -25,9 +27,6 @@ build_flags = ; 库依赖 lib_deps = - ; WiFi 配网 (自带 captive portal) - tzapu/WiFiManager@^2.0.17 - ; MQTT 客户端 knolleary/PubSubClient@^2.8 @@ -43,4 +42,4 @@ lib_deps = ; 上传配置 upload_speed = 921600 -upload_port = COM16 +upload_port = COM18 diff --git a/src/alarm_manager.cpp b/src/alarm_manager.cpp index 7bae9d0..fbeb265 100644 --- a/src/alarm_manager.cpp +++ b/src/alarm_manager.cpp @@ -64,9 +64,9 @@ 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); + // 有源蜂鸣器: 用 digitalWrite, 不用 PWM + pinMode(BUZZER_PIN, OUTPUT); + digitalWrite(BUZZER_PIN, LOW); pinMode(LED_ALARM_PIN, OUTPUT); digitalWrite(LED_ALARM_PIN, LOW); @@ -148,29 +148,29 @@ static void _calcBuzzerParams(AlarmManager::AlarmType t, uint8_t& beeps, uint32_t& onMs, uint32_t& offMs, uint32_t& pauseMs) { switch (t) { - // 人身安全 — 连速急哔 (最刺耳) + // 人身安全 — 连速急哔 case AlarmManager::ALARM_FALL: - beeps = 0; onMs = 60; offMs = 60; pauseMs = 0; // 跌倒: 连速哔 + beeps = 0; onMs = 200; offMs = 200; pauseMs = 0; // 跌倒: 急促哔哔 break; case AlarmManager::ALARM_HEART_ABNORMAL: case AlarmManager::ALARM_BREATH_ABNORMAL: - beeps = 5; onMs = 80; offMs = 80; pauseMs = 400; // 心率/呼吸: 5连急哔 + beeps = 5; onMs = 200; offMs = 150; pauseMs = 500; // 心率/呼吸: 5连哔 break; - // 环境安全 — 次急 + // 环境安全 case AlarmManager::ALARM_WATER_LEAK: - beeps = 3; onMs = 120; offMs = 100; pauseMs = 500; // 水浸: 3连哔 + beeps = 3; onMs = 250; offMs = 200; pauseMs = 600; // 水浸: 3连哔 break; - // 环境舒适 — 温和 + // 环境舒适 case AlarmManager::ALARM_TEMP_ABNORMAL: case AlarmManager::ALARM_HUMI_ABNORMAL: - beeps = 2; onMs = 200; offMs = 400; pauseMs = 1200; // 温湿度: 2声长停 + beeps = 2; onMs = 300; offMs = 500; pauseMs = 1500; // 温湿度: 2声长停 break; - // 设备状态 — 仅提示 + // 设备状态 case AlarmManager::ALARM_SENSOR_FAULT: - beeps = 1; onMs = 300; offMs = 3000; pauseMs = 0; // 传感器: 单声提醒 + beeps = 1; onMs = 500; offMs = 3000; pauseMs = 0; // 传感器: 单声提醒 break; default: - beeps = 0; onMs = 500; offMs = 500; pauseMs = 0; // 持续 + beeps = 0; onMs = 500; offMs = 500; pauseMs = 0; // 持续 break; } } @@ -277,8 +277,13 @@ 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; + if (strcmp(cmd.alarmType, "fall") == 0) localType = ALARM_FALL; + else if (strcmp(cmd.alarmType, "water_leak") == 0) localType = ALARM_WATER_LEAK; + else if (strcmp(cmd.alarmType, "heart_abnormal") == 0) localType = ALARM_HEART_ABNORMAL; + else if (strcmp(cmd.alarmType, "breath_abnormal") == 0) localType = ALARM_BREATH_ABNORMAL; + else if (strcmp(cmd.alarmType, "temp_abnormal") == 0) localType = ALARM_TEMP_ABNORMAL; + else if (strcmp(cmd.alarmType, "humi_abnormal") == 0) localType = ALARM_HUMI_ABNORMAL; + else if (strcmp(cmd.alarmType, "sensor_fault") == 0) localType = ALARM_SENSOR_FAULT; clearLocal(localType); @@ -308,7 +313,7 @@ void AlarmManager::_setBuzzer(bool on) { if (on) { _buzzerOffTime = 0; } else { - ledcWrite(BUZZER_PWM_CHANNEL, 0); + digitalWrite(BUZZER_PIN, LOW); _buzzerOffTime = 0; } } @@ -328,16 +333,20 @@ void AlarmManager::_updateBuzzer() { // 定时关闭 if (_buzzerOffTime > 0 && millis() >= _buzzerOffTime) { _buzzerOn = false; - ledcWrite(BUZZER_PWM_CHANNEL, 0); + digitalWrite(BUZZER_PIN, LOW); Serial.println(F("[告警] ⏰ 蜂鸣器定时关闭")); return; } uint32_t now = millis(); - // 云端定时: 持续响 + // 云端定时: 哔哔声 (500ms响/500ms停), 不是长鸣 if (_buzzerOffTime > 0) { - ledcWrite(BUZZER_PWM_CHANNEL, 128); + if (now - _buzzerToggleTime >= 500) { + _buzzerToggleTime = now; + _buzzerBeepState = !_buzzerBeepState; + digitalWrite(BUZZER_PIN, _buzzerBeepState ? HIGH : LOW); + } return; } @@ -347,7 +356,7 @@ void AlarmManager::_updateBuzzer() { if (now - _buzzerToggleTime >= (_buzzerBeepState ? _beepOnMs : _beepOffMs)) { _buzzerToggleTime = now; _buzzerBeepState = !_buzzerBeepState; - ledcWrite(BUZZER_PWM_CHANNEL, _buzzerBeepState ? 128 : 0); + digitalWrite(BUZZER_PIN, _buzzerBeepState ? HIGH : LOW); } } else { // 多声一组: N声哔 + 长停顿 @@ -358,7 +367,7 @@ void AlarmManager::_updateBuzzer() { _buzzerToggleTime = now; if (_beepCount >= _beepsPerCycle) _beepCount = 0; _buzzerBeepState = true; - ledcWrite(BUZZER_PWM_CHANNEL, 128); + digitalWrite(BUZZER_PIN, HIGH); _beepCount++; } } else { @@ -366,7 +375,7 @@ void AlarmManager::_updateBuzzer() { if (now - _buzzerToggleTime >= _beepOnMs) { _buzzerToggleTime = now; _buzzerBeepState = false; - ledcWrite(BUZZER_PWM_CHANNEL, 0); + digitalWrite(BUZZER_PIN, LOW); } } } diff --git a/src/button_manager.cpp b/src/button_manager.cpp index 8b9fdce..bda20e7 100644 --- a/src/button_manager.cpp +++ b/src/button_manager.cpp @@ -44,13 +44,13 @@ void ButtonManager::begin() { pinMode(BTN_ALARM_DISMISS_PIN, INPUT_PULLUP); pinMode(BTN_PAGE_SWITCH_PIN, INPUT_PULLUP); - // 读取初始状态, 避免启动时的虚假边沿 - _btnProvision.rawState = _readPin(BTN_PROVISION_PIN); - _btnProvision.stableState = _btnProvision.rawState; - _btnDismiss.rawState = _readPin(BTN_ALARM_DISMISS_PIN); - _btnDismiss.stableState = _btnDismiss.rawState; - _btnPage.rawState = _readPin(BTN_PAGE_SWITCH_PIN); - _btnPage.stableState = _btnPage.rawState; + // 强制初始化为未按下 (PCB 上电 GPIO 不稳, 忽略初始状态) + _btnProvision.rawState = false; + _btnProvision.stableState = false; + _btnDismiss.rawState = false; + _btnDismiss.stableState = false; + _btnPage.rawState = false; + _btnPage.stableState = false; Serial.println(F("[按钮] ✅ 三按钮初始化完成 (双向去抖+超时自愈)")); Serial.printf(" 配网: GPIO%d | 解除报警: GPIO%d | 翻页: GPIO%d\n", @@ -64,6 +64,9 @@ void ButtonManager::begin() { // ============================================================ void ButtonManager::loop() { + // 开机 10 秒静默 (PCB 上电 GPIO 不稳 + 强制初始未按下) + if (millis() < 10000) return; + // 按钮1: 配网 (仅长按) _processButton(_btnProvision, false, true); diff --git a/src/main.cpp b/src/main.cpp index d85963b..42d9d3e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -49,7 +49,14 @@ bool g_localDhtFaultActive = false; uint8_t g_tempAbnCnt = 0; uint8_t g_humiAbnCnt = 0; uint8_t g_radarTimeoutCnt = 0; -#define ALARM_HYSTERESIS 3 // 连续3次确认才触发 +uint8_t g_heartAbnCnt = 0; // 心率异常防抖 +uint8_t g_breathAbnCnt = 0; // 呼吸异常防抖 +uint32_t g_fallDetectMs = 0; // 跌倒检测计时 (ms) +uint32_t g_fallClearMs = 0; // 跌倒清除计时 (ms) +bool g_fallDismissed = false; // 已手动解除, 不重复报警 +#define ALARM_HYSTERESIS 3 // 连续3次确认才触发 +#define FALL_DETECT_MS 2000 // 跌倒持续2秒才触发 (房顶安装, 过滤坐下) +#define FALL_CLEAR_MS 5000 // 跌倒消失5秒后才清除 (防误消) // ============================================================ // 时间同步 @@ -84,14 +91,28 @@ void syncTime() { 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; + // 清除全部本地告警状态 (远程恢复=手动解除) + g_localFallActive = false; + g_localWaterActive = false; + g_localHeartAbnActive = false; + g_localBreathAbnActive = false; + g_localTempAbnActive = false; + g_localHumiAbnActive = false; + g_localRadarFaultActive = false; + g_localDhtFaultActive = false; + + // 防抖计数器也复位 + g_tempAbnCnt = 0; + g_humiAbnCnt = 0; + g_radarTimeoutCnt = 0; + g_heartAbnCnt = 0; + g_breathAbnCnt = 0; + g_fallDetectMs = 0; + g_fallClearMs = 0; + g_fallDismissed = false; } else { // 告警触发 g_alarm.executeAlarm(cmd); @@ -201,11 +222,13 @@ TelemetryData buildTelemetry(const RadarRawData& radar) { 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); + // 环境类型: 从配网配置读取 (默认 bedroom) + DeviceConfig cfg = g_wifi.getConfig(); + strncpy(t.deviceType, cfg.env_type[0] ? cfg.env_type : "bedroom", sizeof(t.deviceType)-1); + // DHT11 t.temp = g_sensor.getTemp(); t.humi = g_sensor.getHumi(); @@ -219,6 +242,8 @@ TelemetryData buildTelemetry(const RadarRawData& radar) { t.heartRate = radar.heartRate; t.breathRate = radar.breathRate; t.body = radar.body; + t.sleepState = radar.sleepState; + t.sleepScore = radar.sleepScore; t.radarValid = radar.radarValid; t.vitalValid = radar.vitalValid; @@ -250,54 +275,105 @@ void onLocalAlarmChange(uint8_t type, bool triggered, const char* desc) { // ============================================================ void checkLocalAlarms(const RadarRawData& radar) { + // 环境类型 → 报警开关 + const char* env = g_wifi.getConfig().env_type; + bool enFall = (strcmp(env, "bathroom") == 0 || strcmp(env, "hallway") == 0 || strcmp(env, "livingroom") == 0); + bool enVital = (strcmp(env, "bedroom") == 0); + bool enWater = (strcmp(env, "bathroom") == 0); + bool enEnv = (strcmp(env, "bedroom") == 0 || strcmp(env, "balcony") == 0 || strcmp(env, "kitchen") == 0); + // ─── 前置条件: 有人才检测人体生命体征 ─── bool humanPresent = radar.presence && radar.radarValid; - // ─── ★★★★★ 人身安全: 跌倒 (立即触发, 无防抖) ─── - if (radar.fall && humanPresent && !g_localFallActive) { - g_localFallActive = true; - g_alarm.triggerLocal(AlarmManager::ALARM_FALL, "雷达检测到跌倒"); - } else if (!radar.fall && g_localFallActive) { - g_localFallActive = false; - g_alarm.clearLocal(AlarmManager::ALARM_FALL); + // ─── ★★★★★ 人身安全: 跌倒 (仅浴室/走廊/客厅) ─── + if (enFall && radar.fall && humanPresent && !g_fallDismissed && millis() > 30000) { + if (!g_localFallActive) { + // 持续检测中: 必须满 FALL_DETECT_MS 才报警 + if (g_fallDetectMs == 0) { + g_fallDetectMs = millis(); + } else if (millis() - g_fallDetectMs >= FALL_DETECT_MS) { + g_localFallActive = true; + g_fallDetectMs = 0; + g_fallClearMs = 0; + g_alarm.triggerLocal(AlarmManager::ALARM_FALL, "雷达检测到跌倒"); + } + } else { + g_fallClearMs = 0; // 还在跌倒, 重置清除计时 + } + } else { + g_fallDetectMs = 0; + // 解除抑制需要 fall 持续消失 FALL_CLEAR_MS (防 AFD1 频繁跳变) + if (g_fallDismissed && g_fallClearMs == 0) { + g_fallClearMs = millis(); + } + if (!g_fallDismissed || millis() - g_fallClearMs >= FALL_CLEAR_MS) { + g_fallDismissed = false; + g_fallClearMs = 0; + } + if (g_localFallActive) { + // 跌倒已消失 → 延迟清除 + if (g_fallClearMs == 0) { + g_fallClearMs = millis(); + } else if (millis() - g_fallClearMs >= FALL_CLEAR_MS) { + g_localFallActive = false; + g_fallClearMs = 0; + g_alarm.clearLocal(AlarmManager::ALARM_FALL); + } + } } - // ─── ★★★★★ 人身安全: 心率异常 (有人才检测) ─── - if (humanPresent && radar.vitalValid && radar.heartRate > 0) { + // ─── ★★★★★ 人身安全: 心率异常 (仅卧室+ABD1, 防抖3次, 30s) ─── +#if RADAR_TYPE != RADAR_TYPE_AFD1 + if (enVital && millis() > 30000 && humanPresent && radar.vitalValid && radar.heartRate > 0) { bool heartAbn = (radar.heartRate < HEART_RATE_MIN || radar.heartRate > HEART_RATE_MAX); - if (heartAbn && !g_localHeartAbnActive) { - g_localHeartAbnActive = true; - char buf[32]; - snprintf(buf, sizeof(buf), "心率异常: %d bpm", radar.heartRate); - g_alarm.triggerLocal(AlarmManager::ALARM_HEART_ABNORMAL, buf); - } else if (!heartAbn && g_localHeartAbnActive) { - g_localHeartAbnActive = false; - g_alarm.clearLocal(AlarmManager::ALARM_HEART_ABNORMAL); + if (heartAbn) { + if (++g_heartAbnCnt >= ALARM_HYSTERESIS && !g_localHeartAbnActive) { + g_localHeartAbnActive = true; + char buf[32]; + snprintf(buf, sizeof(buf), "心率异常: %d bpm", radar.heartRate); + g_alarm.triggerLocal(AlarmManager::ALARM_HEART_ABNORMAL, buf); + } + } else { + g_heartAbnCnt = 0; + if (g_localHeartAbnActive) { + g_localHeartAbnActive = false; + g_alarm.clearLocal(AlarmManager::ALARM_HEART_ABNORMAL); + } } } else if (!humanPresent && g_localHeartAbnActive) { - // 人走了 — 清除之前的报警 g_localHeartAbnActive = false; + g_heartAbnCnt = 0; g_alarm.clearLocal(AlarmManager::ALARM_HEART_ABNORMAL); } +#endif // RADAR_TYPE != AFD1 - // ─── ★★★★★ 人身安全: 呼吸异常 (有人才检测) ─── - if (humanPresent && radar.vitalValid && radar.breathRate > 0) { + // ─── ★★★★★ 人身安全: 呼吸异常 (仅卧室+ABD1, 防抖3次, 30秒后生效) ─── +#if RADAR_TYPE != RADAR_TYPE_AFD1 + if (enVital && millis() > 30000 && humanPresent && radar.vitalValid && radar.breathRate > 0) { bool breathAbn = (radar.breathRate < BREATH_RATE_MIN || radar.breathRate > BREATH_RATE_MAX); - if (breathAbn && !g_localBreathAbnActive) { - g_localBreathAbnActive = true; - char buf[32]; - snprintf(buf, sizeof(buf), "呼吸异常: %d 次/分", radar.breathRate); - g_alarm.triggerLocal(AlarmManager::ALARM_BREATH_ABNORMAL, buf); - } else if (!breathAbn && g_localBreathAbnActive) { - g_localBreathAbnActive = false; - g_alarm.clearLocal(AlarmManager::ALARM_BREATH_ABNORMAL); + if (breathAbn) { + if (++g_breathAbnCnt >= ALARM_HYSTERESIS && !g_localBreathAbnActive) { + g_localBreathAbnActive = true; + char buf[32]; + snprintf(buf, sizeof(buf), "呼吸异常: %d 次/分", radar.breathRate); + g_alarm.triggerLocal(AlarmManager::ALARM_BREATH_ABNORMAL, buf); + } + } else { + g_breathAbnCnt = 0; + if (g_localBreathAbnActive) { + g_localBreathAbnActive = false; + g_alarm.clearLocal(AlarmManager::ALARM_BREATH_ABNORMAL); + } } } else if (!humanPresent && g_localBreathAbnActive) { g_localBreathAbnActive = false; + g_breathAbnCnt = 0; g_alarm.clearLocal(AlarmManager::ALARM_BREATH_ABNORMAL); } +#endif // RADAR_TYPE != AFD1 - // ─── ★★★ 环境安全: 水浸 ─── + // ─── ★★★ 环境安全: 水浸 (仅浴室) ─── + if (enWater) { bool waterLeak = !g_sensor.getRainDO() && g_sensor.isRainValid(); if (waterLeak && !g_localWaterActive) { g_localWaterActive = true; @@ -306,9 +382,10 @@ void checkLocalAlarms(const RadarRawData& radar) { g_localWaterActive = false; g_alarm.clearLocal(AlarmManager::ALARM_WATER_LEAK); } + } // enWater - // ─── ★★ 环境舒适: 温度异常 (防抖3次) ─── - if (g_sensor.isDhtValid()) { + // ─── ★★ 环境舒适: 温湿度异常 (仅卧室/阳台/厨房, 防抖3次, 30s) ─── + if (enEnv && millis() > 30000 && g_sensor.isDhtValid()) { float temp = g_sensor.getTemp(); bool tempAbn = (temp < TEMP_LOW || temp > TEMP_HIGH); if (tempAbn) { @@ -345,27 +422,27 @@ void checkLocalAlarms(const RadarRawData& radar) { } } - // ─── ★ 设备:雷达超时 (连续5秒无数据) ─── + // ─── 设备:雷达超时 (连续5秒无数据, 仅标记, 不报警) ─── if (g_radar.isTimeout()) { if (++g_radarTimeoutCnt >= 5 && !g_localRadarFaultActive) { g_localRadarFaultActive = true; - g_alarm.triggerLocal(AlarmManager::ALARM_SENSOR_FAULT, "雷达超时无数据"); + Serial.println(F("[系统] ⚠️ 雷达超时无数据")); } } else { g_radarTimeoutCnt = 0; if (g_localRadarFaultActive) { g_localRadarFaultActive = false; - g_alarm.clearLocal(AlarmManager::ALARM_SENSOR_FAULT); + Serial.println(F("[系统] ✅ 雷达数据恢复")); } } - // ─── ★ 设备:DHT 持续故障 (已由 SensorManager 内部计数) ─── + // ─── 设备:DHT 持续故障 (仅标记, 不报警) ─── if (g_sensor.isDhtFault() && !g_localDhtFaultActive && millis() > 60000) { g_localDhtFaultActive = true; - g_alarm.triggerLocal(AlarmManager::ALARM_SENSOR_FAULT, "DHT11温湿度传感器故障"); + Serial.println(F("[系统] ⚠️ DHT11 温湿度传感器故障")); } else if (!g_sensor.isDhtFault() && g_localDhtFaultActive) { g_localDhtFaultActive = false; - g_alarm.clearLocal(AlarmManager::ALARM_SENSOR_FAULT); + Serial.println(F("[系统] ✅ DHT11 传感器恢复")); } } @@ -439,6 +516,20 @@ void setup() { }); g_buttons.onAlarmDismiss([]() { g_alarm.clearAllLocal(); + // 同步清除所有本地状态标记 + g_localFallActive = false; + g_localWaterActive = false; + g_localHeartAbnActive = false; + g_localBreathAbnActive = false; + g_localTempAbnActive = false; + g_localHumiAbnActive = false; + g_localRadarFaultActive = false; + g_localDhtFaultActive = false; + g_tempAbnCnt = 0; + g_humiAbnCnt = 0; + g_radarTimeoutCnt = 0; + g_fallClearMs = 0; + g_fallDismissed = true; // 抑制重复报警, 等跌倒信号消了再恢复 }); g_buttons.onProvisionLongPress([]() { g_wifi.startConfigPortal(); @@ -474,6 +565,9 @@ void setup() { // 8. 传感器 g_sensor.begin(); + // 8. 等雷达 + 电源稳定 + delay(8000); + // 9. 雷达任务 (核心0) xTaskCreatePinnedToCore( radarTask, "RadarTask", @@ -511,6 +605,13 @@ void loop() { syncTime(); wasDisconnected = false; } + // 配网成功后更新 MQTT 配置 (如果有改动) 并上报 + if (g_wifi.justProvisioned()) { + DeviceConfig cfg = g_wifi.getConfig(); + g_mqtt.setServer(cfg.mqtt_server, atoi(cfg.mqtt_port), + cfg.mqtt_user, cfg.mqtt_pass); + g_mqtt.publishProvisionStatus("success", ""); + } } else { wasDisconnected = true; } @@ -523,34 +624,57 @@ void loop() { RadarRawData radar = g_radar.getData(); checkLocalAlarms(radar); - // --- 传感器故障日志 (报警系统统一管理触发, 此处仅记录) --- + // --- 传感器状态日志 (仅记录, 不报警) --- static uint32_t lastFaultLog = 0; if (now - lastFaultLog > 60000) { lastFaultLog = now; - if (!g_sensor.isDhtValid() && millis() > 60000 && !g_localDhtFaultActive) { - Serial.println(F("[系统] ⚠️ DHT11 读数异常")); + if (!g_sensor.isDhtValid() && millis() > 60000) { + Serial.println(F("[系统] ⚠️ DHT11 读数失效")); } - if (g_radar.isTimeout() && !g_localRadarFaultActive) { - Serial.println(F("[系统] ⚠️ 雷达超时无数据")); + if (g_radar.isTimeout()) { + Serial.println(F("[系统] ⚠️ 雷达数据超时")); } } // --- 5. 报警模块更新 (定时器/闪烁) --- g_alarm.loop(); - // --- 6. 遥测上报 (1秒周期) --- - if (now - g_lastReportTime >= REPORT_INTERVAL_MS) { - g_lastReportTime = now; + // --- 6. 遥测上报 --- + // 有人时: 每秒全量上报 + // 无人时: 仅状态变化时发极简报文 + 60秒心跳 (省4G流量) + static bool lastPresence = false; + static uint32_t lastHeartbeat = 0; // 无人心跳计时 + + if (radar.presence) { + // ── 有人: 每秒完整上报 ── + if (now - g_lastReportTime >= REPORT_INTERVAL_MS) { + g_lastReportTime = now; + TelemetryData tele = buildTelemetry(radar); + g_mqtt.publishAllPoints(tele, g_sequence); + g_sequence++; + lastHeartbeat = now; // 有人时重置心跳计时 + + printDebug(tele); + g_display.update(tele, wifiOk, g_alarm.isBuzzerOn(), + g_alarm.hasActiveAlarm(), g_alarm.getActiveAlarmStr()); + } + } else { + // ── 无人: 状态翻转发一次极简报文, 之后每60秒心跳 ── + if (lastPresence != radar.presence || now - lastHeartbeat >= PRESENCE_HEARTBEAT_MS) { + g_mqtt.publishPresenceOnly(radar.presence, radar.radarValid, g_sequence); + g_sequence++; + lastHeartbeat = now; + + // 调试输出 + Serial.printf("[上报] 无人极简报文 (Presence=0) 序号:%d\n", g_sequence - 1); + } + // OLED 还是照常刷新 (显示无人状态) TelemetryData tele = buildTelemetry(radar); - g_mqtt.publishAllPoints(tele, g_sequence); - g_sequence++; - - // 调试输出 + OLED 显示 - printDebug(tele); g_display.update(tele, wifiOk, g_alarm.isBuzzerOn(), g_alarm.hasActiveAlarm(), g_alarm.getActiveAlarmStr()); } + lastPresence = radar.presence; // --- 7. MQTT 配网状态 (WiFiManager 触发时) --- // WiFiManager 的 captive portal 启动/关闭时更新状态 diff --git a/src/mqtt_manager.cpp b/src/mqtt_manager.cpp index 01afb19..e307c16 100644 --- a/src/mqtt_manager.cpp +++ b/src/mqtt_manager.cpp @@ -178,20 +178,23 @@ void MQTTManager::publishAllPoints(const TelemetryData& d, uint32_t sequence) { snprintf(timeStr, sizeof(timeStr), "2026-01-01 00:00:00"); } - // 构建 JSON (协议规范 §4.1) + // 构建 JSON (协议规范 V0.08) JsonDocument doc; - doc["saleid"] = d.saleid; doc["gateid"] = d.gateid; doc["type"] = "report"; doc["time"] = timeStr; doc["sequence"] = String(sequence); doc["source"] = "da"; + // meter.id = gateid + 后缀 (如 10100001001 → 10100001001001) + String meterId = String(d.gateid) + "001"; + JsonArray meter = doc["meter"].to(); JsonObject m = meter.add(); - m["id"] = d.meterId; + m["id"] = meterId; m["status"] = "1"; m["name"] = d.meterName; + m["type"] = d.deviceType; // 设备类型枚举 JsonObject v = m["values"].to(); // 环境数据 @@ -205,6 +208,8 @@ void MQTTManager::publishAllPoints(const TelemetryData& d, uint32_t sequence) { v["Resident"] = d.resident ? "1" : "0"; // 生命体征 v["Body"] = String(d.body); + v["SleepState"] = String(d.sleepState); + v["SleepScore"] = String(d.sleepScore); v["HeartRate"] = String(d.heartRate); v["BreathRate"] = String(d.breathRate); v["RadarValid"] = d.radarValid ? "1" : "0"; @@ -416,6 +421,47 @@ void MQTTManager::publishLocalAlarm(uint8_t alarmType, bool triggered, const cha _mqtt.publish(topic.c_str(), payload.c_str()); } +// ============================================================ +// 上行: 无人极简报文 (省4G流量, 仅 Presence + RadarValid) +// ============================================================ + +void MQTTManager::publishPresenceOnly(bool presence, bool radarValid, 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: ~120 字节 vs 完整报文 ~500 字节 + JsonDocument doc; + doc["gateid"] = _gateid; + doc["type"] = "report"; + doc["time"] = timeStr; + doc["sequence"] = String(sequence); + doc["source"] = "da"; + + String meterId = String(_gateid) + "001"; + + JsonArray meter = doc["meter"].to(); + JsonObject m = meter.add(); + m["id"] = meterId; + m["status"] = "1"; + m["name"] = METER_NAME_DEFAULT; + + JsonObject v = m["values"].to(); + v["Presence"] = presence ? "1" : "0"; + v["RadarValid"] = radarValid ? "1" : "0"; + + String payload; + serializeJson(doc, payload); + + _mqtt.publish("report/allpoints", payload.c_str()); +} + // ============================================================ // 回调注册 // ============================================================ diff --git a/src/radar_manager.cpp b/src/radar_manager.cpp index ccd534f..1884d23 100644 --- a/src/radar_manager.cpp +++ b/src/radar_manager.cpp @@ -19,6 +19,11 @@ RadarManager::RadarManager() , _callback(nullptr) , _frameCount(0) , _errorCount(0) + , _lastHeartBeat(0) + , _lastBreathBeat(0) + , _stablePresence(false) + , _presenceCnt(0) + , _lastRawPresence(false) { memset(&_currentFrame, 0, sizeof(_currentFrame)); memset(&_data, 0, sizeof(_data)); @@ -43,6 +48,9 @@ bool RadarManager::begin() { _resetParser(); memset(&_data, 0, sizeof(_data)); _data.lastUpdate = millis(); + _stablePresence = false; + _presenceCnt = 0; + _lastRawPresence = false; // 不给雷达发任何命令,让它自己初始化并开始自动上报 // R60ABD1 上电后通常需要 15-30 秒自检,之后自动输出数据 @@ -68,6 +76,16 @@ void RadarManager::loop() { Serial.printf("[雷达] ✅ 开始接收数据 (首帧)\n"); } + // 心率/呼吸数据过期 (>10秒无更新 → 标记无效) + if (millis() - _lastHeartBeat > 10000) { + _data.vitalValid = false; + _data.heartRate = 0; + } + if (millis() - _lastBreathBeat > 10000) { + _data.vitalValid = false; + _data.breathRate = 0; + } + // 超时检测:如果 10 秒还没收到任何数据,打印一次 static uint32_t noDataWarn = 0; if (_frameCount == 0 && millis() - _data.lastUpdate > 10000) { @@ -222,6 +240,9 @@ void RadarManager::_processCompleteFrame() { case CTRL_HEART_RATE: _extractHeartRate(_currentFrame); break; + case CTRL_FALL: + _extractFall(_currentFrame); // AFD1 专用跌倒 + break; case CTRL_SLEEP: _extractSleep(_currentFrame); break; @@ -250,47 +271,60 @@ void RadarManager::_processCompleteFrame() { void RadarManager::_extractPresence(const RadarFrame& frame) { uint8_t raw = frame.data_length > 0 ? frame.data[0] : 0; + bool rawPresence = false; // cmd 0x00 (len=1): 有人状态 (1=有人 0=无人) if (frame.command == 0x00 && frame.data_length >= 1) { - _data.presence = (raw == 1); + rawPresence = (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.body = raw; + _data.motion = (raw >= 1); _data.radarValid = true; _data.lastUpdate = millis(); _notifyUpdate(); + return; // 运动帧不含有人信息, 跳过防抖 } // cmd 0x03 (len=1): 有人状态备用 (非0=有人) else if (frame.command == 0x03 && frame.data_length >= 1) { - _data.presence = (raw != 0); + rawPresence = (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); + rawPresence = (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); + rawPresence = (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(); + } else { + return; // 未知命令 + } + + // 防抖: 连续 PRESENCE_HYSTERESIS 次一致才更新 + if (rawPresence == _lastRawPresence) { + if (_presenceCnt < PRESENCE_HYSTERESIS) _presenceCnt++; + } else { + _presenceCnt = 0; + _lastRawPresence = rawPresence; + } + if (_presenceCnt >= PRESENCE_HYSTERESIS && rawPresence != _data.presence) { + _data.presence = rawPresence; + _stablePresence = rawPresence; } + _notifyUpdate(); } void RadarManager::_extractBreathing(const RadarFrame& frame) { @@ -299,8 +333,9 @@ void RadarManager::_extractBreathing(const RadarFrame& frame) { if (rate != 0x80 && rate != 0x00 && rate <= 40) { _data.breathRate = rate; _data.vitalValid = true; - _data.presence = true; // 有呼吸=有人 + _data.presence = true; _data.radarValid = true; + _lastBreathBeat = millis(); } _data.lastUpdate = millis(); _notifyUpdate(); @@ -313,14 +348,25 @@ void RadarManager::_extractHeartRate(const RadarFrame& frame) { if (rate != 0x80 && rate != 0x00 && rate >= 40) { _data.heartRate = rate; _data.vitalValid = true; - _data.presence = true; // 有心跳=有人 + _data.presence = true; _data.radarValid = true; + _lastHeartBeat = millis(); } _data.lastUpdate = millis(); _notifyUpdate(); } } +// AFD1 专用: 跌倒检测 (控制字 0x83) +void RadarManager::_extractFall(const RadarFrame& frame) { + if (frame.data_length >= 1) { + _data.fall = (frame.data[0] != 0); + _data.radarValid = true; + _data.lastUpdate = millis(); + _notifyUpdate(); + } +} + void RadarManager::_extractSleep(const RadarFrame& frame) { // 从睡眠数据提取: cmd=0x00/0x01 为入床离床状态 if (frame.data_length >= 1) { @@ -333,16 +379,15 @@ void RadarManager::_extractSleep(const RadarFrame& frame) { } 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.sleepState = frame.data[1]; // 0=清醒 1=浅睡 2=深睡 + _data.sleepScore = frame.data[2]; // 0~100 + _data.lastUpdate = millis(); + _notifyUpdate(); + } + if (frame.data_length >= 4) { + _data.fall = (frame.data[3] & 0x01) != 0; + _data.resident = (frame.data[3] & 0x02) != 0; _data.lastUpdate = millis(); _notifyUpdate(); } diff --git a/src/wifi_manager.cpp b/src/wifi_manager.cpp index f33eecd..d9da352 100644 --- a/src/wifi_manager.cpp +++ b/src/wifi_manager.cpp @@ -1,36 +1,40 @@ /** * @file wifi_manager.cpp - * @brief WiFi 配网管理器实现 + * @brief WiFi 配网管理器 — AP + WebServer */ #include "wifi_manager.h" -// 单例指针 -WiFiMgmt* WiFiMgmt::_instance = nullptr; +// BLE 配网回调: 小程序写入 JSON → 触发配网 +class BLEProvisionCB : public BLECharacteristicCallbacks { + void onWrite(BLECharacteristic* ch) override { + std::string val = ch->getValue(); + if (val.length() > 0 && WiFiMgmt::_instance) { + Serial.printf("[BLE] 收到配网数据 (%d 字节)\n", val.length()); + WiFiMgmt::_instance->handleBLEProvision(val.c_str()); + } + } +}; -// ============================================================ -// 构造函数 & 析构函数 -// ============================================================ +WiFiMgmt* WiFiMgmt::_instance = nullptr; WiFiMgmt::WiFiMgmt() - : _connected(false) - , _lastCheckTime(0) - , _buttonPressTime(0) - , _buttonPressed(false) - , _ledTaskHandle(nullptr) - , _ledMode(0) - , _param_mqtt_server(nullptr) - , _param_mqtt_port(nullptr) - , _param_mqtt_user(nullptr) - , _param_mqtt_pass(nullptr) - , _param_device_id(nullptr) - , _param_env_note(nullptr) + : _server(nullptr), _dns(nullptr) + , _connected(false) + , _portalActive(false), _portalStopFlag(false) + , _portalStartTime(0) + , _lastCheckTime(0), _buttonPressTime(0), _buttonPressed(false) + , _lastScanTime(0) + , _provisionSuccess(false) + , _ledTaskHandle(nullptr), _ledMode(0) + , _bleServer(nullptr), _bleInfoChar(nullptr), _bleChar(nullptr), _bleAdv(nullptr) { memset(&_config, 0, sizeof(_config)); - _instance = this; // 设置单例 + _instance = this; } WiFiMgmt::~WiFiMgmt() { + _stopPortal(); _instance = nullptr; } @@ -39,176 +43,53 @@ WiFiMgmt::~WiFiMgmt() { // ============================================================ bool WiFiMgmt::begin() { - Serial.println(F("\n========================================")); - Serial.println(F(" WiFi 配网管理器 初始化")); - Serial.println(F("========================================")); + Serial.println(("\n=== WiFi 配网管理器 (SmartConfig) ===")); - // 初始化引脚 pinMode(CONFIG_BUTTON_PIN, INPUT_PULLUP); pinMode(LED_WIFI_PIN, OUTPUT); digitalWrite(LED_WIFI_PIN, LOW); - // 启动 LED 独立任务 (避免 startConfigPortal 阻塞影响闪烁) xTaskCreate(_ledTaskFunc, "WiFiLed", 1024, nullptr, 1, &_ledTaskHandle); - // 初始化 LittleFS + // 冷启动延迟, 等 Flash 稳定 + delay(500); if (!LittleFS.begin(true)) { - Serial.println(F("[WiFi] ❌ LittleFS 挂载失败!")); + Serial.println(("[WiFi] LittleFS 挂载失败!")); return false; } - Serial.println(F("[WiFi] ✅ LittleFS 已挂载")); + Serial.println(("[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); + Serial.println(("[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); + strncpy(_config.env_type, "bedroom", sizeof(_config.env_type)-1); + memset(_config.wifi_ssid, 0, sizeof(_config.wifi_ssid)); + memset(_config.wifi_pass, 0, sizeof(_config.wifi_pass)); } - // 配置 WiFiManager - _wm.setDebugOutput(false); // 关闭调试输出 (避免干扰) - _wm.setConfigPortalTimeout(WIFI_CONFIG_TIMEOUT); - _wm.setConnectTimeout(WIFI_CONNECT_TIMEOUT); - _wm.setTitle("健康监测系统 - 设备配网"); - _wm.setCustomHeadElement( - "" - ); - WiFi.setAutoReconnect(true); // ESP32 自动重连 - - // 注册保存回调 (必须使用静态函数, 不能使用 lambda) - _wm.setSaveConfigCallback(_saveParamsCallback); - - // 创建自定义参数 - _param_mqtt_server = new WiFiManagerParameter( - "mqtt_server", "MQTT 服务器地址", - _config.mqtt_server, 40); - _param_mqtt_port = new WiFiManagerParameter( - "mqtt_port", "MQTT 端口", - _config.mqtt_port, 6); - _param_mqtt_user = new WiFiManagerParameter( - "mqtt_user", "MQTT 用户名", - _config.mqtt_user, 20); - _param_mqtt_pass = new WiFiManagerParameter( - "mqtt_pass", "MQTT 密码", - _config.mqtt_pass, 20); - _param_device_id = new WiFiManagerParameter( - "device_id", "设备名称", - _config.device_id, 20); - _param_env_note = new WiFiManagerParameter( - "env_note", "🏠 环境备注 (如: 主卧/客厅/老人房)", - _config.env_note, 20); - - // 注册自定义参数 - _wm.addParameter(_param_mqtt_server); - _wm.addParameter(_param_mqtt_port); - _wm.addParameter(_param_mqtt_user); - _wm.addParameter(_param_mqtt_pass); - _wm.addParameter(_param_device_id); - _wm.addParameter(_param_env_note); - - // 设置 AP 模式回调 (静态函数) - _wm.setAPCallback(_apCallback); - - // 尝试连接 WiFi - _connected = _connectWiFi(); + WiFi.setAutoReconnect(true); + + // 有配置 → 直接连 + bool hasConfig = LittleFS.exists(CONFIG_FILE_DEVICE); + Serial.printf("[WiFi] 配置文件: %s\n", hasConfig ? "存在" : "不存在"); + if (hasConfig) { + _connected = _connectWiFi(); + Serial.printf("[WiFi] 自动连接: %s\n", _connected ? "成功" : "失败"); + } else { + Serial.println(("[WiFi] 无配置, 跳过连接")); + _connected = false; + } if (!_connected) { - // 如果自动连接失败, 启动配网页面 - Serial.println(F("[WiFi] 自动连接失败, 启动配网模式...")); _setLEDMode(2); // 快闪 = 配网模式 - _connected = _connectWiFi(); // 内部会启动配网页面 - } - - if (_connected) { - _setLEDMode(3); // 常亮 = 已连接 + _startPortal(); // 只开 AP + } else { + _setLEDMode(3); // 常亮 } _lastCheckTime = millis(); @@ -220,29 +101,59 @@ bool WiFiMgmt::begin() { // ============================================================ bool WiFiMgmt::loop() { - // 检测按钮长按 + // --- Portal 模式 --- + if (_portalActive) { + if (_portalStopFlag) { + _stopPortal(); + _portalStopFlag = false; + if (_pendingSSID.length() > 0) { + Serial.printf("[WiFi] 尝试连接: %s\n", _pendingSSID.c_str()); + _connected = _tryConnect(_pendingSSID.c_str(), _pendingPass.c_str()); + _pendingSSID = ""; _pendingPass = ""; + if (_connected) { + _setLEDMode(3); + _provisionSuccess = true; // 标记配网成功 + } else { + _setLEDMode(2); + _startPortal(); + } + } + return _connected; + } + + _dns->processNextRequest(); + _server->handleClient(); + delay(10); + return _connected; + } + + // --- 正常运行 --- if (_checkButtonLongPress()) { - Serial.println(F("[WiFi] ⚠️ 检测到长按, 清除配置并重启...")); - resetSettings(); + Serial.println(("[WiFi] 长按, 进配网模式")); + _setLEDMode(2); + WiFi.disconnect(); + _startPortal(); + _connected = false; return false; } - // 检查 WiFi 状态 if (millis() - _lastCheckTime > WIFI_RETRY_INTERVAL) { _lastCheckTime = millis(); - - if (WiFi.status() != WL_CONNECTED) { - _connected = false; - _setLEDMode(1); // 慢闪 = 未连接 - Serial.println(F("[WiFi] ⚠️ WiFi 断开, 尝试重连...")); - _connectWiFi(); - } else { + wl_status_t s = WiFi.status(); + if (s != WL_CONNECTED) { + if (_connected) { + _connected = false; + _setLEDMode(1); + Serial.printf("[WiFi] 断开! status=%d RSSI=%d\n", s, WiFi.RSSI()); + } + // 不手动 reconnect(), 让 WiFi.setAutoReconnect(true) 自动处理 + } else if (!_connected) { _connected = true; - _setLEDMode(3); // 常亮 = 已连接 + _setLEDMode(3); + Serial.printf("[WiFi] 已连接 RSSI=%d\n", WiFi.RSSI()); } } - // LED 由独立 FreeRTOS 任务处理 (不受 startConfigPortal 阻塞影响) return _connected; } @@ -251,128 +162,375 @@ bool WiFiMgmt::loop() { // ============================================================ 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); + if (_config.wifi_ssid[0] == '\0') { + Serial.println(("[WiFi] 无已存WiFi密码")); + return false; } - + Serial.printf("[WiFi] 连接: %s\n", _config.wifi_ssid); + + // 崩溃重启后 WiFi 模块可能残留旧状态, 彻底重置 + WiFi.disconnect(true); + delay(200); + WiFi.mode(WIFI_STA); + delay(100); + WiFi.begin(_config.wifi_ssid, _config.wifi_pass); + int retry = 0; + while (WiFi.status() != WL_CONNECTED && retry < 40) { // 20s + delay(500); Serial.print("."); yield(); retry++; + } + Serial.println(); + _connected = (WiFi.status() == WL_CONNECTED); + Serial.printf("[WiFi] 连接结果: %s status=%d\n", _connected ? "成功" : "失败", WiFi.status()); + if (_connected) Serial.printf("[WiFi] SSID=%s IP=%s\n", WiFi.SSID().c_str(), WiFi.localIP().toString().c_str()); return _connected; } +bool WiFiMgmt::_tryConnect(const char* ssid, const char* pass) { + WiFi.mode(WIFI_STA); + WiFi.persistent(true); // 确保持久化 WiFi 密码到 NVS (断电不丢) + WiFi.begin(ssid, pass); + int retry = 0; + while (WiFi.status() != WL_CONNECTED && retry < 40) { // 20s + delay(500); Serial.print("."); retry++; + } + bool ok = (WiFi.status() == WL_CONNECTED); + Serial.printf("\n[WiFi] 连接结果: %s status=%d\n", ok ? "成功!" : "失败", WiFi.status()); + return ok; +} + // ============================================================ -// 配网页面 +// 配网 Portal (AP + WebServer) // ============================================================ void WiFiMgmt::startConfigPortal() { - Serial.println(F("[WiFi] 🔧 手动启动配网页面...")); - _setLEDMode(2); // 快闪 = 配网模式 - _wm.startConfigPortal(WIFI_AP_SSID, WIFI_AP_PASSWORD); - _connected = (WiFi.status() == WL_CONNECTED); - if (_connected) { - _setLEDMode(3); // 常亮 = 已连接 + _setLEDMode(2); + _startPortal(); +} + +void WiFiMgmt::_startPortal() { + if (_portalActive) return; + + WiFi.disconnect(true); + WiFi.mode(WIFI_OFF); + delay(200); + WiFi.mode(WIFI_AP); + WiFi.softAPConfig(IPAddress(192,168,4,1), IPAddress(192,168,4,1), IPAddress(255,255,255,0)); + WiFi.softAP(WIFI_AP_SSID, WIFI_AP_PASSWORD); + Serial.printf("[WiFi] AP: %s IP: 192.168.4.1\n", WIFI_AP_SSID); + + // DNS 劫持: 所有域名 → 192.168.4.1 → 触发手机强制门户弹窗 + _dns = new DNSServer(); + _dns->start(53, "*", WiFi.softAPIP()); + + _server = new WebServer(80); + _server->on("/", HTTP_GET, [this]() { _handleRoot(); }); + _server->on("/save", HTTP_POST, [this]() { _handleSave(); }); + _server->on("/reset", HTTP_GET, [this]() { _handleReset(); }); + _server->onNotFound( [this]() { _handleRoot(); }); + _server->begin(); + + _portalActive = true; + _portalStartTime = millis(); + + Serial.println(("[WiFi] 手机连 AP 后浏览器打开 http://192.168.4.1")); + + _startBLE(); // 同时启动 BLE 配网广播 +} + +void WiFiMgmt::_stopPortal() { + if (!_portalActive) return; + _stopBLE(); + if (_server) { _server->stop(); delete _server; _server = nullptr; } + if (_dns) { _dns->stop(); delete _dns; _dns = nullptr; } + WiFi.softAPdisconnect(true); + _portalActive = false; +} + +// ============================================================ +// Web 处理器 +// ============================================================ + +void WiFiMgmt::_handleRoot() { + _server->send(200, "text/html; charset=utf-8", _htmlPage()); +} + +void WiFiMgmt::_handleSave() { + if (_server->hasArg("s")) { + _pendingSSID = _server->arg("s"); + _pendingPass = _server->arg("p"); + // 持久化 WiFi 密码到 LittleFS + strncpy(_config.wifi_ssid, _pendingSSID.c_str(), sizeof(_config.wifi_ssid)-1); + strncpy(_config.wifi_pass, _pendingPass.c_str(), sizeof(_config.wifi_pass)-1); + if (_server->hasArg("device_id")) strncpy(_config.device_id, _server->arg("device_id").c_str(), sizeof(_config.device_id)-1); + if (_server->hasArg("env_note")) strncpy(_config.env_note, _server->arg("env_note").c_str(), sizeof(_config.env_note)-1); + if (_server->hasArg("mqtt_server")) strncpy(_config.mqtt_server, _server->arg("mqtt_server").c_str(), sizeof(_config.mqtt_server)-1); + if (_server->hasArg("mqtt_port")) strncpy(_config.mqtt_port, _server->arg("mqtt_port").c_str(), sizeof(_config.mqtt_port)-1); + if (_server->hasArg("mqtt_user")) strncpy(_config.mqtt_user, _server->arg("mqtt_user").c_str(), sizeof(_config.mqtt_user)-1); + if (_server->hasArg("mqtt_pass")) strncpy(_config.mqtt_pass, _server->arg("mqtt_pass").c_str(), sizeof(_config.mqtt_pass)-1); + _saveConfig(); + + _server->send(200, "text/html; charset=utf-8", + "" + "配置完成
" + "

配置已保存

设备正在连接 WiFi...
等待指示灯常亮

" + "
"); + + _portalStopFlag = true; + return; } + _handleRoot(); +} + +void WiFiMgmt::_handleReset() { + _server->send(200, "text/html; charset=utf-8", + "已重置" + "

配置已清除

设备正在重启...

"); + delay(500); + _stopPortal(); + resetSettings(); } // ============================================================ -// 重置设置 +// 配网 HTML 页面 (精简版) // ============================================================ -void WiFiMgmt::resetSettings() { - Serial.println(F("[WiFi] 清除所有配置...")); +void WiFiMgmt::_updateWifiScan() { + _wifiScanCache = ""; + int n = WiFi.scanNetworks(false, true); + for (int i = 0; i < n; i++) { + _wifiScanCache += ""; + } + if (n == 0) _wifiScanCache = ""; + WiFi.scanDelete(); + _lastScanTime = millis(); +} - // 删除 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); +String WiFiMgmt::_htmlPage() { + if (_wifiScanCache.length() == 0) _updateWifiScan(); + + String html; + html.reserve(4096); + html += ""; + html += "设备配网
"; + + // Header + 提示 + html += "
📡

观照-五劳无感 设备配网

WiFi 联网与参数配置

"; + html += "
💡 也可用 ESP-Touch App 一键配网
"; + + html += "
"; + html += "
📶
无线网络
选择 WiFi 或使用 SmartConfig
"; + html += "
📡
"; + html += "
🔒
"; + + html += "
⚙️
设备基础信息
唯一标识与安装位置
"; + html += "
#
"; + html += "
🏠
"; + + html += ""; + + html += "
"; + html += ""; + html += "
Health Monitor · Device Provisioning
"; + + return html; +} + +// ============================================================ +// BLE 蓝牙配网 +// ============================================================ + +void WiFiMgmt::_startBLE() { + if (_bleServer != nullptr) return; // 已启动 + + BLEDevice::init("HealthMonitor"); + _bleServer = BLEDevice::createServer(); + + BLEService* svc = _bleServer->createService("4fafc201-1fb5-459e-8fcc-c5c9c331914b"); + + // 特征1: 设备信息 (READ) — 小程序连接后读取设备编号等 + _bleInfoChar = svc->createCharacteristic( + "4fafc202-1fb5-459e-8fcc-c5c9c331914b", + BLECharacteristic::PROPERTY_READ + ); + // 设备信息 JSON (最多 512 字节) + JsonDocument info; + info["device_id"] = _config.device_id; + info["mqtt_server"] = _config.mqtt_server; + info["mqtt_port"] = _config.mqtt_port; + info["fw_version"] = FW_VERSION; + String infoJson; + serializeJson(info, infoJson); + _bleInfoChar->setValue(infoJson.c_str()); + + // 特征2: 配网写入 (WRITE + NOTIFY) — 小程序发配网数据, 设备回复 + _bleChar = svc->createCharacteristic( + "4fafc203-1fb5-459e-8fcc-c5c9c331914b", + BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_NOTIFY + ); + _bleChar->setCallbacks(new BLEProvisionCB()); + _bleChar->addDescriptor(new BLE2902()); // 启用 notify + _bleChar->setValue(""); // 初始为空 + + svc->start(); + + _bleAdv = _bleServer->getAdvertising(); + _bleAdv->addServiceUUID("4fafc201-1fb5-459e-8fcc-c5c9c331914b"); + _bleAdv->setScanResponse(true); + _bleAdv->start(); + + Serial.printf("[BLE] ✅ 配网广播: HealthMonitor device_id=%s\n", _config.device_id); +} + +void WiFiMgmt::_stopBLE() { + if (!_bleServer) return; + if (_bleAdv) { _bleAdv->stop(); _bleAdv = nullptr; } + BLEDevice::deinit(false); + _bleServer = nullptr; + _bleChar = nullptr; + Serial.println("[BLE] 已停止"); +} + +void WiFiMgmt::handleBLEProvision(const char* json) { + JsonDocument doc; + DeserializationError err = deserializeJson(doc, json); + if (err) { + Serial.printf("[BLE] JSON 解析失败: %s\n", err.c_str()); + if (_bleChar) { _bleChar->setValue("ERR_JSON"); _bleChar->notify(); } + return; + } - // 清除 WiFiManager 的配置 - _wm.resetSettings(); + // 提取 WiFi 凭证 + const char* ssid = doc["s"] | ""; + const char* pass = doc["p"] | ""; - // LED 快闪提示 - for (int i = 0; i < 10; i++) { - digitalWrite(LED_WIFI_PIN, !digitalRead(LED_WIFI_PIN)); - delay(100); + if (strlen(ssid) == 0) { + Serial.println("[BLE] 未提供 WiFi SSID"); + if (_bleChar) { _bleChar->setValue("ERR_NO_SSID"); _bleChar->notify(); } + return; } - Serial.println(F("[WiFi] 🔄 正在重启...")); + _pendingSSID = ssid; + _pendingPass = pass; + + // 保存到 LittleFS (和 Web 配网完全相同的逻辑) + strncpy(_config.wifi_ssid, ssid, sizeof(_config.wifi_ssid) - 1); + strncpy(_config.wifi_pass, pass, sizeof(_config.wifi_pass) - 1); + + if (doc.containsKey("device_id")) strncpy(_config.device_id, doc["device_id"] | "", sizeof(_config.device_id) - 1); + if (doc.containsKey("env_note")) strncpy(_config.env_note, doc["env_note"] | "", sizeof(_config.env_note) - 1); + if (doc.containsKey("env_type")) strncpy(_config.env_type, doc["env_type"] | "", sizeof(_config.env_type) - 1); + if (doc.containsKey("mqtt_server")) strncpy(_config.mqtt_server, doc["mqtt_server"] | "", sizeof(_config.mqtt_server) - 1); + if (doc.containsKey("mqtt_port")) strncpy(_config.mqtt_port, doc["mqtt_port"] | "", sizeof(_config.mqtt_port) - 1); + if (doc.containsKey("mqtt_user")) strncpy(_config.mqtt_user, doc["mqtt_user"] | "", sizeof(_config.mqtt_user) - 1); + if (doc.containsKey("mqtt_pass")) strncpy(_config.mqtt_pass, doc["mqtt_pass"] | "", sizeof(_config.mqtt_pass) - 1); + + _saveConfig(); + _portalStopFlag = true; // 触发延迟关闭 AP+BLE → 连接 WiFi + + Serial.printf("[BLE] ✅ 配网完成, SSID=%s\n", ssid); + + // 回复 OK + if (_bleChar) { _bleChar->setValue("OK"); _bleChar->notify(); } + delay(100); +} + +// ============================================================ +// 重置 +// ============================================================ + +void WiFiMgmt::resetSettings() { + Serial.println(("[WiFi] 清除所有配置...")); + 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); + WiFi.disconnect(true, true); + for (int i = 0; i < 10; i++) { digitalWrite(LED_WIFI_PIN, !digitalRead(LED_WIFI_PIN)); delay(100); } + Serial.println(("[WiFi] 正在重启...")); delay(500); ESP.restart(); } // ============================================================ -// 按钮长按检测 +// 按钮 // ============================================================ bool WiFiMgmt::_checkButtonLongPress() { bool pressed = (digitalRead(CONFIG_BUTTON_PIN) == LOW); - if (pressed && !_buttonPressed) { - // 按下 - _buttonPressed = true; - _buttonPressTime = millis(); + _buttonPressed = true; _buttonPressTime = millis(); } else if (!pressed && _buttonPressed) { - // 松开 _buttonPressed = false; - if (millis() - _buttonPressTime >= BUTTON_LONG_PRESS_MS) { - return true; // 长按确认 - } + if (millis() - _buttonPressTime >= BUTTON_LONG_PRESS_MS) return true; } - return false; } // ============================================================ -// LittleFS 配置持久化 +// LittleFS // ============================================================ bool WiFiMgmt::_loadConfig() { - if (!LittleFS.exists(CONFIG_FILE_DEVICE)) { - return false; - } - + 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); + if (err) 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); + strncpy(_config.env_type, doc["env_type"] | "bedroom", sizeof(_config.env_type)-1); + strncpy(_config.wifi_ssid, doc["wifi_ssid"] | "", sizeof(_config.wifi_ssid)-1); + strncpy(_config.wifi_pass, doc["wifi_pass"] | "", sizeof(_config.wifi_pass)-1); + Serial.printf("[WiFi] 配置加载, 已存WiFi: %s\n", _config.wifi_ssid[0] ? _config.wifi_ssid : "(空)"); return true; } @@ -384,134 +542,45 @@ bool WiFiMgmt::_saveConfig() { doc["mqtt_pass"] = _config.mqtt_pass; doc["device_id"] = _config.device_id; doc["env_note"] = _config.env_note; - + doc["env_type"] = _config.env_type; + doc["wifi_ssid"] = _config.wifi_ssid; + doc["wifi_pass"] = _config.wifi_pass; 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; - } - + if (!file) return false; + if (serializeJson(doc, file) == 0) { file.close(); return false; } file.close(); - Serial.println(F("[WiFi] ✅ 设备配置已保存")); return true; } // ============================================================ -// WiFiManager 保存参数回调 -// ============================================================ - -// ============================================================ -// 静态回调函数 (WiFiManager 要求 C 函数指针) +// Getter // ============================================================ -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); -} +bool WiFiMgmt::isConnected() const { return _connected && (WiFi.status() == WL_CONNECTED); } +bool WiFiMgmt::justProvisioned() { bool v = _provisionSuccess; _provisionSuccess = false; return v; } +DeviceConfig WiFiMgmt::getConfig() const { return _config; } +const char* WiFiMgmt::getDeviceId() const { return _config.device_id; } +const char* WiFiMgmt::getEnvNote() const { return _config.env_note; } // ============================================================ -// Getter 方法 +// LED // ============================================================ -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) { - _ledMode = mode; // uint8_t 赋值在 ESP32 上是原子的 -} - -// ============================================================ -// LED 闪烁独立任务 (FreeRTOS) -// 独立于主循环,startConfigPortal 阻塞期间也能正常闪烁 -// ============================================================ +void WiFiMgmt::_setLEDMode(uint8_t mode) { _ledMode = mode; } void WiFiMgmt::_ledTaskFunc(void* param) { - const TickType_t fastMs = pdMS_TO_TICKS(300); // 快闪 = 配网模式 - const TickType_t slowMs = pdMS_TO_TICKS(1000); // 慢闪 = 未连接 - + const TickType_t fastMs = pdMS_TO_TICKS(300); + const TickType_t slowMs = pdMS_TO_TICKS(1000); while (true) { - if (!_instance) { - vTaskDelay(slowMs); - continue; - } - + if (!_instance) { vTaskDelay(slowMs); continue; } uint8_t mode = _instance->_ledMode; - switch (mode) { - case 0: // 灭 - digitalWrite(LED_WIFI_PIN, LOW); - vTaskDelay(slowMs); - break; - case 1: // 慢闪 — WiFi 断连 - digitalWrite(LED_WIFI_PIN, HIGH); - vTaskDelay(slowMs); - digitalWrite(LED_WIFI_PIN, LOW); - vTaskDelay(slowMs); - break; - case 2: // 快闪 — 配网模式 - digitalWrite(LED_WIFI_PIN, HIGH); - vTaskDelay(fastMs); - digitalWrite(LED_WIFI_PIN, LOW); - vTaskDelay(fastMs); - break; - case 3: // 常亮 — 已连接 - digitalWrite(LED_WIFI_PIN, HIGH); - vTaskDelay(slowMs); - break; + case 0: digitalWrite(LED_WIFI_PIN, LOW); vTaskDelay(slowMs); break; + case 1: digitalWrite(LED_WIFI_PIN, HIGH); vTaskDelay(slowMs); + digitalWrite(LED_WIFI_PIN, LOW); vTaskDelay(slowMs); break; + case 2: digitalWrite(LED_WIFI_PIN, HIGH); vTaskDelay(fastMs); + digitalWrite(LED_WIFI_PIN, LOW); vTaskDelay(fastMs); break; + case 3: digitalWrite(LED_WIFI_PIN, HIGH); vTaskDelay(slowMs); break; } } }