Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78458361fa | ||
|
|
03129c9ed8 | ||
|
|
b960682d73 | ||
|
|
9a4b5cfbeb | ||
|
|
577eafd0ee | ||
|
|
8facb8b719 | ||
|
|
cc3ba5a2f8 | ||
|
|
36f5a285f4 | ||
|
|
08a055b611 | ||
|
|
93112dcdfd |
@@ -19,7 +19,12 @@
|
|||||||
"Bash(sed -i 's/#define BTN_PAGE_SWITCH_PIN 10/#define BTN_PAGE_SWITCH_PIN 5/' include/config.h)",
|
"Bash(sed -i 's/#define BTN_PAGE_SWITCH_PIN 10/#define BTN_PAGE_SWITCH_PIN 5/' include/config.h)",
|
||||||
"Bash(sed -i 's/#define BTN_LONG_PRESS_MS 3000/#define BTN_LONG_PRESS_MS 1500/' include/config.h)",
|
"Bash(sed -i 's/#define BTN_LONG_PRESS_MS 3000/#define BTN_LONG_PRESS_MS 1500/' include/config.h)",
|
||||||
"Bash(sed -i 's/#define WATER_SENSOR_PIN 6 /#define WATER_SENSOR_PIN 45/' include/config.h)",
|
"Bash(sed -i 's/#define WATER_SENSOR_PIN 6 /#define WATER_SENSOR_PIN 45/' include/config.h)",
|
||||||
"Bash(sed -i 's/#define BUZZER_PIN 7 /#define BUZZER_PIN 21/' include/config.h)"
|
"Bash(sed -i 's/#define BUZZER_PIN 7 /#define BUZZER_PIN 21/' include/config.h)",
|
||||||
|
"PowerShell(Get-ChildItem \"$env:USERPROFILE\\\\.claude\" -ErrorAction SilentlyContinue; if \\(-not $?\\) { Write-Host \"No .claude at USERPROFILE\" })",
|
||||||
|
"Read(//c/Users/杨志佳/.claude/**)",
|
||||||
|
"Read(//d/ClaudeData/.claude/**)",
|
||||||
|
"Read(//c/Users/杨志佳/.claude/projects/d--PlatformIO1-Projects-Healthv2-0/memory/**)",
|
||||||
|
"Read(//d/PlatformIO1/Projects/Healthv2.0/ai/sessions/memory/**)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,3 +3,6 @@
|
|||||||
.vscode/c_cpp_properties.json
|
.vscode/c_cpp_properties.json
|
||||||
.vscode/launch.json
|
.vscode/launch.json
|
||||||
.vscode/ipch
|
.vscode/ipch
|
||||||
|
|
||||||
|
# AI 会话日志——不入库
|
||||||
|
ai/sessions/
|
||||||
|
|||||||
+21
-11
@@ -16,10 +16,15 @@
|
|||||||
// 1. 硬件引脚定义 (ESP32-S3)
|
// 1. 硬件引脚定义 (ESP32-S3)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
// --- R60ABD1 毫米波雷达 ---
|
// --- 雷达型号选择 ---
|
||||||
|
#define RADAR_TYPE_ABD1 0 // R60ABD1: 心率/呼吸精准, 跌倒一般
|
||||||
|
#define RADAR_TYPE_AFD1 1 // R60AFD1: 跌倒/姿态精准, 无心率呼吸
|
||||||
|
#define RADAR_TYPE RADAR_TYPE_AFD1 // AFD1 (跌倒/姿态精准, 卫生间用)
|
||||||
|
|
||||||
|
// --- 毫米波雷达 ---
|
||||||
#define RADAR_UART_PORT Serial2
|
#define RADAR_UART_PORT Serial2
|
||||||
#define RADAR_RX_PIN 17
|
#define RADAR_RX_PIN 17
|
||||||
#define RADAR_TX_PIN 1 // UART 发送 (从18挪到1)
|
#define RADAR_TX_PIN 1
|
||||||
#define RADAR_BAUD_RATE 115200
|
#define RADAR_BAUD_RATE 115200
|
||||||
|
|
||||||
// --- 雷达备用 GPIO ---
|
// --- 雷达备用 GPIO ---
|
||||||
@@ -71,9 +76,9 @@
|
|||||||
// 3. WiFi 默认配置 (配网使用)
|
// 3. WiFi 默认配置 (配网使用)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
#define WIFI_AP_SSID "HealthMonitor-Setup"
|
#define WIFI_AP_SSID_PREFIX "HealthMonitor-" // 末尾拼 MAC 末4位, 保证每台设备唯一
|
||||||
#define WIFI_AP_PASSWORD "12345678"
|
#define WIFI_AP_PASSWORD "1234567890"
|
||||||
#define WIFI_CONFIG_TIMEOUT 300
|
#define WIFI_CONFIG_TIMEOUT 120 // 配网超时(秒), 超时回连上一个 WiFi
|
||||||
#define WIFI_CONNECT_TIMEOUT 15
|
#define WIFI_CONNECT_TIMEOUT 15
|
||||||
#define WIFI_RETRY_INTERVAL 10000
|
#define WIFI_RETRY_INTERVAL 10000
|
||||||
|
|
||||||
@@ -115,8 +120,9 @@
|
|||||||
// 6. 上报配置 (协议规范 §4.1, §7.2)
|
// 6. 上报配置 (协议规范 §4.1, §7.2)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
#define REPORT_INTERVAL_MS 1000 // 遥测上报周期 1秒
|
#define REPORT_INTERVAL_MS 1000 // 遥测上报周期 1秒 (有人时)
|
||||||
#define REPORT_QOS 0 // 遥测 QoS 0
|
#define REPORT_QOS 0 // 遥测 QoS 0
|
||||||
|
#define PRESENCE_HEARTBEAT_MS 60000 // 无人心跳间隔 60秒 (4G省流量)
|
||||||
#define STATUS_QOS 1 // 状态/响应 QoS 1
|
#define STATUS_QOS 1 // 状态/响应 QoS 1
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -125,11 +131,12 @@
|
|||||||
|
|
||||||
#define HEART_RATE_MIN 50
|
#define HEART_RATE_MIN 50
|
||||||
#define HEART_RATE_MAX 120
|
#define HEART_RATE_MAX 120
|
||||||
#define BREATH_RATE_MIN 8
|
#define BREATH_RATE_MIN 10
|
||||||
#define BREATH_RATE_MAX 24
|
#define BREATH_RATE_MAX 26
|
||||||
#define TEMP_LOW 5.0f
|
#define TEMP_LOW 5.0f
|
||||||
#define TEMP_HIGH 45.0f
|
#define TEMP_HIGH 45.0f
|
||||||
#define HUMI_LOW 15.0f
|
#define HUMI_LOW 15.0f
|
||||||
|
#define PRESENCE_HYSTERESIS 8 // 有人无人防抖 (PCB信号抖动时过滤)
|
||||||
#define HUMI_HIGH 90.0f
|
#define HUMI_HIGH 90.0f
|
||||||
#define WATER_DEBOUNCE_MS 50
|
#define WATER_DEBOUNCE_MS 50
|
||||||
|
|
||||||
@@ -167,10 +174,11 @@
|
|||||||
|
|
||||||
struct TelemetryData {
|
struct TelemetryData {
|
||||||
// --- 设备标识 ---
|
// --- 设备标识 ---
|
||||||
char saleid[12]; // 站点编号 "10100001"
|
char saleid[12]; // 站点编号 (已弃用)
|
||||||
char gateid[16]; // 网关编号 "10100001001"
|
char gateid[16]; // 网关编号
|
||||||
char meterId[8]; // 节点ID "ENV001"
|
char meterId[8]; // 节点ID (已弃用, 新用 gateid+后缀)
|
||||||
char meterName[16]; // 设备型号 "RadarDHT11"
|
char meterName[16]; // 设备型号 "RadarDHT11"
|
||||||
|
char deviceType[16]; // 设备类型枚举 bedroom/bathroom/...
|
||||||
|
|
||||||
// --- 环境数据 (DHT11) ---
|
// --- 环境数据 (DHT11) ---
|
||||||
float temp; // 温度 ℃
|
float temp; // 温度 ℃
|
||||||
@@ -187,6 +195,8 @@ struct TelemetryData {
|
|||||||
uint8_t heartRate; // 心率 bpm (无效=0)
|
uint8_t heartRate; // 心率 bpm (无效=0)
|
||||||
uint8_t breathRate; // 呼吸 次/分 (无效=0)
|
uint8_t breathRate; // 呼吸 次/分 (无效=0)
|
||||||
uint8_t body; // 人体能量/体征值
|
uint8_t body; // 人体能量/体征值
|
||||||
|
uint8_t sleepState; // 睡眠状态 0=清醒 1=浅睡 2=深睡
|
||||||
|
uint8_t sleepScore; // 睡眠评分 0~100
|
||||||
bool radarValid; // 雷达数据有效性
|
bool radarValid; // 雷达数据有效性
|
||||||
bool vitalValid; // 心率呼吸有效性
|
bool vitalValid; // 心率呼吸有效性
|
||||||
|
|
||||||
|
|||||||
@@ -83,6 +83,9 @@ public:
|
|||||||
/** 本地报警上报 status/{gateid}/alarm (§4.7 扩展) */
|
/** 本地报警上报 status/{gateid}/alarm (§4.7 扩展) */
|
||||||
void publishLocalAlarm(uint8_t alarmType, bool triggered, const char* desc);
|
void publishLocalAlarm(uint8_t alarmType, bool triggered, const char* desc);
|
||||||
|
|
||||||
|
/** 无人极简报文 (仅 Presence+RadarValid, 省4G流量) */
|
||||||
|
void publishPresenceOnly(bool presence, bool radarValid, uint32_t sequence);
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 回调注册 (协议规范 §5)
|
// 回调注册 (协议规范 §5)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ struct RadarRawData {
|
|||||||
uint8_t breathRate; // 呼吸
|
uint8_t breathRate; // 呼吸
|
||||||
uint8_t body; // 体动/能量值
|
uint8_t body; // 体动/能量值
|
||||||
uint16_t distance; // 距离 cm
|
uint16_t distance; // 距离 cm
|
||||||
|
uint8_t sleepState; // 睡眠状态: 0=清醒 1=浅睡 2=深睡
|
||||||
|
uint8_t sleepScore; // 睡眠评分: 0~100
|
||||||
|
|
||||||
bool radarValid; // 雷达有效性
|
bool radarValid; // 雷达有效性
|
||||||
bool vitalValid; // 生命体征有效性
|
bool vitalValid; // 生命体征有效性
|
||||||
@@ -68,9 +70,12 @@ private:
|
|||||||
void _extractPresence(const RadarFrame& frame);
|
void _extractPresence(const RadarFrame& frame);
|
||||||
void _extractBreathing(const RadarFrame& frame);
|
void _extractBreathing(const RadarFrame& frame);
|
||||||
void _extractHeartRate(const RadarFrame& frame);
|
void _extractHeartRate(const RadarFrame& frame);
|
||||||
|
void _extractFall(const RadarFrame& frame); // AFD1 跌倒
|
||||||
void _extractSleep(const RadarFrame& frame);
|
void _extractSleep(const RadarFrame& frame);
|
||||||
void _extractWorkStatus(const RadarFrame& frame);
|
void _extractWorkStatus(const RadarFrame& frame);
|
||||||
void _extractDetectRange(const RadarFrame& frame);
|
void _extractDetectRange(const RadarFrame& frame);
|
||||||
|
void _filterMotion(bool rawMotion); // 运动状态 3 帧去抖
|
||||||
|
void _filterFall(bool rawFall); // 跌倒状态 2 帧去抖 (防 AFD1 自检期噪声)
|
||||||
|
|
||||||
uint8_t _calcChecksum(const uint8_t* data, uint16_t len);
|
uint8_t _calcChecksum(const uint8_t* data, uint16_t len);
|
||||||
void _notifyUpdate();
|
void _notifyUpdate();
|
||||||
@@ -85,6 +90,21 @@ private:
|
|||||||
RadarCallback _callback;
|
RadarCallback _callback;
|
||||||
uint32_t _frameCount;
|
uint32_t _frameCount;
|
||||||
uint32_t _errorCount;
|
uint32_t _errorCount;
|
||||||
|
uint32_t _lastHeartBeat; // 上次心率数据时间
|
||||||
|
uint32_t _lastBreathBeat; // 上次呼吸数据时间
|
||||||
|
|
||||||
|
// 有人无人防抖 (PCB 信号不稳定时过滤跳变)
|
||||||
|
bool _stablePresence; // 防抖后的稳定有人/无人
|
||||||
|
uint8_t _presenceCnt; // 连续一致计数
|
||||||
|
bool _lastRawPresence; // 上一次原始值
|
||||||
|
|
||||||
|
// 运动状态防抖 (体动信号常因小动作抖动, 加 3 帧持续才确认)
|
||||||
|
uint8_t _motionCnt; // 连续一致计数
|
||||||
|
bool _lastRawMotion; // 上一次原始值
|
||||||
|
|
||||||
|
// 跌倒状态防抖 (AFD1 自检期 fall 位可能为随机值, 2 帧持续才确认)
|
||||||
|
uint8_t _fallCnt; // 连续一致计数
|
||||||
|
bool _lastRawFall; // 上一次原始值
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // RADAR_MANAGER_H
|
#endif // RADAR_MANAGER_H
|
||||||
|
|||||||
@@ -33,9 +33,10 @@ enum RadarControl : uint8_t {
|
|||||||
CTRL_WORK_STATUS = 0x05, // 工作状态
|
CTRL_WORK_STATUS = 0x05, // 工作状态
|
||||||
CTRL_DETECT_RANGE = 0x07, // 探测范围
|
CTRL_DETECT_RANGE = 0x07, // 探测范围
|
||||||
CTRL_PRESENCE = 0x80, // 人体存在
|
CTRL_PRESENCE = 0x80, // 人体存在
|
||||||
CTRL_BREATHING = 0x81, // 呼吸检测
|
CTRL_BREATHING = 0x81, // 呼吸检测 (仅 ABD1)
|
||||||
CTRL_SLEEP = 0x84, // 睡眠监测
|
CTRL_FALL = 0x83, // 跌倒检测 (仅 AFD1)
|
||||||
CTRL_HEART_RATE = 0x85, // 心率监测
|
CTRL_SLEEP = 0x84, // 睡眠监测 (仅 ABD1)
|
||||||
|
CTRL_HEART_RATE = 0x85, // 心率监测 (仅 ABD1)
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -73,6 +74,7 @@ enum SleepStatus : uint8_t {
|
|||||||
SLEEP_AWAKE = 0, // 清醒
|
SLEEP_AWAKE = 0, // 清醒
|
||||||
SLEEP_LIGHT = 1, // 浅睡
|
SLEEP_LIGHT = 1, // 浅睡
|
||||||
SLEEP_DEEP = 2, // 深睡
|
SLEEP_DEEP = 2, // 深睡
|
||||||
|
SLEEP_ABSENT = 3, // 离床/无人/无效
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
+59
-75
@@ -1,10 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* @file wifi_manager.h
|
* @file wifi_manager.h
|
||||||
* @brief WiFi 配网管理器
|
* @brief WiFi 配网管理器 (AP + WebServer)
|
||||||
*
|
*
|
||||||
* 功能:
|
* 功能:
|
||||||
* - 自动连接已保存的 WiFi
|
* - 自动连接已保存的 WiFi
|
||||||
* - 配网失败/首次启动 → AP 模式 + 强制门户页面
|
* - 配网失败/首次启动 → AP 模式 + Captive Portal
|
||||||
* - 自定义参数: MQTT 服务器/端口/用户/密码、设备名称、环境备注
|
* - 自定义参数: MQTT 服务器/端口/用户/密码、设备名称、环境备注
|
||||||
* - 长按按钮重置配置
|
* - 长按按钮重置配置
|
||||||
* - LED 状态指示
|
* - LED 状态指示
|
||||||
@@ -15,8 +15,14 @@
|
|||||||
#define WIFI_MANAGER_H
|
#define WIFI_MANAGER_H
|
||||||
|
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <WiFiManager.h>
|
#include <WiFi.h>
|
||||||
|
#include <WebServer.h>
|
||||||
|
#include <DNSServer.h>
|
||||||
#include <LittleFS.h>
|
#include <LittleFS.h>
|
||||||
|
#include <BLEDevice.h>
|
||||||
|
#include <BLEServer.h>
|
||||||
|
#include <BLEUtils.h>
|
||||||
|
#include <BLE2902.h>
|
||||||
#include <ArduinoJson.h>
|
#include <ArduinoJson.h>
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
|
||||||
@@ -27,7 +33,10 @@ struct DeviceConfig {
|
|||||||
char mqtt_user[20];
|
char mqtt_user[20];
|
||||||
char mqtt_pass[20];
|
char mqtt_pass[20];
|
||||||
char device_id[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 {
|
class WiFiMgmt {
|
||||||
@@ -38,100 +47,75 @@ public:
|
|||||||
// 单例指针 (用于静态回调)
|
// 单例指针 (用于静态回调)
|
||||||
static WiFiMgmt* _instance;
|
static WiFiMgmt* _instance;
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 初始化 WiFi 管理器
|
|
||||||
* @return true=成功
|
|
||||||
*/
|
|
||||||
bool begin();
|
bool begin();
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 主循环, 处理配网流程和按钮检测
|
|
||||||
* @return true=WiFi 已连接
|
|
||||||
*/
|
|
||||||
bool loop();
|
bool loop();
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief WiFi 是否已连接
|
|
||||||
*/
|
|
||||||
bool isConnected() const;
|
bool isConnected() const;
|
||||||
|
bool justProvisioned(); // 刚配网成功 (读取后自动清除)
|
||||||
/**
|
|
||||||
* @brief 获取设备配置
|
|
||||||
*/
|
|
||||||
DeviceConfig getConfig() const;
|
DeviceConfig getConfig() const;
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 获取设备 ID (用于 MQTT client_id)
|
|
||||||
*/
|
|
||||||
const char* getDeviceId() const;
|
const char* getDeviceId() const;
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 获取环境备注
|
|
||||||
*/
|
|
||||||
const char* getEnvNote() const;
|
const char* getEnvNote() const;
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 强制进入配网模式
|
|
||||||
*/
|
|
||||||
void startConfigPortal();
|
void startConfigPortal();
|
||||||
|
void handleBLEProvision(const char* json); // BLE 配网回调
|
||||||
/**
|
void enqueueBLEJson(const char* json); // BLE 线程安全入队 (仅暂存)
|
||||||
* @brief 清除所有配置并重启
|
|
||||||
*/
|
|
||||||
void resetSettings();
|
void resetSettings();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
/**
|
bool _connectWiFi(); // 仅尝试已保存凭据
|
||||||
* @brief 尝试连接 WiFi
|
bool _tryConnect(const char* ssid, const char* pass); // 尝试连接指定 WiFi
|
||||||
*/
|
|
||||||
bool _connectWiFi();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 设置 LED 模式 (线程安全, 无阻塞)
|
|
||||||
* @param mode 0=灭 1=慢闪(断连1s) 2=快闪(配网300ms) 3=常亮(已连接)
|
|
||||||
*/
|
|
||||||
void _setLEDMode(uint8_t mode);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief LED 闪烁独立任务 (FreeRTOS, 不受主循环阻塞影响)
|
|
||||||
*/
|
|
||||||
static void _ledTaskFunc(void* param);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 检测按钮长按
|
|
||||||
*/
|
|
||||||
bool _checkButtonLongPress();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 加载/保存配置到 LittleFS
|
|
||||||
*/
|
|
||||||
bool _loadConfig();
|
bool _loadConfig();
|
||||||
bool _saveConfig();
|
bool _saveConfig();
|
||||||
|
void _setLEDMode(uint8_t mode);
|
||||||
|
static void _ledTaskFunc(void* param);
|
||||||
|
bool _checkButtonLongPress();
|
||||||
|
|
||||||
/**
|
// Web 服务器
|
||||||
* @brief 保存 WiFi 参数回调 (WiFiManager 的回调)
|
void _startPortal();
|
||||||
*/
|
void _stopPortal();
|
||||||
static void _saveParamsCallback();
|
void _handleRoot();
|
||||||
static void _apCallback(WiFiManager* wm);
|
void _handleSave();
|
||||||
|
void _handleReset();
|
||||||
|
void _updateWifiScan(); // WiFi 扫描缓存
|
||||||
|
String _htmlPage(); // 返回配网 HTML 页面
|
||||||
|
|
||||||
WiFiManager _wm;
|
// BLE 蓝牙配网
|
||||||
|
void _startBLE();
|
||||||
|
void _stopBLE();
|
||||||
|
|
||||||
|
WebServer* _server;
|
||||||
|
DNSServer* _dns;
|
||||||
DeviceConfig _config;
|
DeviceConfig _config;
|
||||||
bool _connected;
|
bool _connected;
|
||||||
|
bool _portalActive;
|
||||||
|
bool _portalStopFlag; // 延迟关闭标记
|
||||||
|
uint32_t _portalStartTime;
|
||||||
|
String _pendingSSID;
|
||||||
|
String _pendingPass;
|
||||||
|
bool _provisionSuccess; // 刚配网成功标记
|
||||||
uint32_t _lastCheckTime;
|
uint32_t _lastCheckTime;
|
||||||
uint32_t _buttonPressTime;
|
uint32_t _buttonPressTime;
|
||||||
bool _buttonPressed;
|
bool _buttonPressed;
|
||||||
|
|
||||||
// LED 独立任务 (不被 startConfigPortal 阻塞影响)
|
// WiFi 扫描缓存 (避免每次打开页面都扫描3-8秒)
|
||||||
|
String _wifiScanCache;
|
||||||
|
uint32_t _lastScanTime;
|
||||||
|
|
||||||
|
// LED 独立任务
|
||||||
TaskHandle_t _ledTaskHandle;
|
TaskHandle_t _ledTaskHandle;
|
||||||
volatile uint8_t _ledMode; // 0=灭 1=慢闪 2=快闪 3=常亮
|
volatile uint8_t _ledMode; // 0=灭 1=慢闪 2=快闪 3=常亮
|
||||||
|
|
||||||
// WiFiManager 自定义参数
|
// BLE
|
||||||
WiFiManagerParameter* _param_mqtt_server;
|
BLEServer* _bleServer;
|
||||||
WiFiManagerParameter* _param_mqtt_port;
|
BLECharacteristic* _bleInfoChar; // READ: 设备信息
|
||||||
WiFiManagerParameter* _param_mqtt_user;
|
BLECharacteristic* _bleChar; // WRITE+NOTIFY: 配网
|
||||||
WiFiManagerParameter* _param_mqtt_pass;
|
BLEAdvertising* _bleAdv;
|
||||||
WiFiManagerParameter* _param_device_id;
|
|
||||||
WiFiManagerParameter* _param_env_note;
|
// BLE 回调保护: 不在 BTC_TASK 栈里做 Flash 写操作
|
||||||
|
String _blePendingJson; // 待处理的 BLE 配网 JSON
|
||||||
|
bool _blePendingFlag; // 有未处理的 BLE 数据
|
||||||
|
void _processBLEPending(); // 在 loop() 上下文处理
|
||||||
|
|
||||||
|
// AP SSID (前缀+MAC后4位, BLE 命名复用)
|
||||||
|
String _apSsid;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // WIFI_MANAGER_H
|
#endif // WIFI_MANAGER_H
|
||||||
|
|||||||
@@ -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 = "<!DOCTYPE html>"
|
|
||||||
"<html lang='zh-CN'><head>"
|
|
||||||
"<meta name='format-detection' content='telephone=no'>"
|
|
||||||
"<meta charset='UTF-8'>"
|
|
||||||
"<meta name='viewport' content='width=device-width,initial-scale=1,user-scalable=no'/>"
|
|
||||||
"<title>{v}</title>";
|
|
||||||
|
|
||||||
const char HTTP_SCRIPT[] PROGMEM = "<script>function c(l){"
|
|
||||||
"document.getElementById('s').value=l.getAttribute('data-ssid')||l.innerText||l.textContent;"
|
|
||||||
"p = l.nextElementSibling.classList.contains('l');"
|
|
||||||
"document.getElementById('p').disabled = !p;"
|
|
||||||
"if(p)document.getElementById('p').focus();};"
|
|
||||||
"function f() {var x = document.getElementById('p');x.type==='password'?x.type='text':x.type='password';}"
|
|
||||||
"</script>";
|
|
||||||
|
|
||||||
const char HTTP_HEAD_END[] PROGMEM = "</head><body class='{c}'><div class='wrap'>"; // {c} = _bodyclass
|
|
||||||
|
|
||||||
const char HTTP_ROOT_MAIN[] PROGMEM = "<h1>{t}</h1><h3>{v}</h3>";
|
|
||||||
|
|
||||||
const char * const HTTP_PORTAL_MENU[] PROGMEM = {
|
|
||||||
"<form action='/wifi' method='get'><button>配置 WiFi</button></form><br/>\n", // MENU_WIFI
|
|
||||||
"<form action='/0wifi' method='get'><button>配置 WiFi (不扫描)</button></form><br/>\n", // MENU_WIFINOSCAN
|
|
||||||
"<form action='/info' method='get'><button>系统信息</button></form><br/>\n", // MENU_INFO
|
|
||||||
"<form action='/param' method='get'><button>参数设置</button></form><br/>\n", // MENU_PARAM
|
|
||||||
"<form action='/close' method='get'><button>关闭页面</button></form><br/>\n", // MENU_CLOSE
|
|
||||||
"<form action='/restart' method='get'><button>重启设备</button></form><br/>\n", // MENU_RESTART
|
|
||||||
"<form action='/exit' method='get'><button>退出配网</button></form><br/>\n", // MENU_EXIT
|
|
||||||
"<form action='/erase' method='get'><button class='D'>清除配置</button></form><br/>\n", // MENU_ERASE
|
|
||||||
"<form action='/update' method='get'><button>固件升级</button></form><br/>\n", // MENU_UPDATE
|
|
||||||
"<hr><br/>" // MENU_SEP
|
|
||||||
};
|
|
||||||
|
|
||||||
const char HTTP_PORTAL_OPTIONS[] PROGMEM = "";
|
|
||||||
const char HTTP_ITEM_QI[] PROGMEM = "<div role='img' aria-label='{r}%' title='{r}%' class='q q-{q} {i} {h}'></div>";
|
|
||||||
const char HTTP_ITEM_QP[] PROGMEM = "<div class='q {h}'>{r}%</div>";
|
|
||||||
const char HTTP_ITEM[] PROGMEM = "<div><a href='#p' onclick='c(this)' data-ssid='{V}'>{v}</a>{qi}{qp}</div>";
|
|
||||||
|
|
||||||
const char HTTP_FORM_START[] PROGMEM = "<form method='POST' action='{v}'>";
|
|
||||||
const char HTTP_FORM_WIFI[] PROGMEM = "<label for='s'>WiFi 名称 (SSID)</label><input id='s' name='s' maxlength='32' autocorrect='off' autocapitalize='none' placeholder='{v}'><br/><label for='p'>WiFi 密码</label><input id='p' name='p' maxlength='64' type='password' placeholder='{p}'><input type='checkbox' id='showpass' onclick='f()'> <label for='showpass'>显示密码</label><br/>";
|
|
||||||
const char HTTP_FORM_WIFI_END[] PROGMEM = "";
|
|
||||||
const char HTTP_FORM_STATIC_HEAD[] PROGMEM = "<hr><br/>";
|
|
||||||
const char HTTP_FORM_END[] PROGMEM = "<br/><br/><button type='submit'>保存配置</button></form>";
|
|
||||||
const char HTTP_FORM_LABEL[] PROGMEM = "<label for='{i}'>{t}</label>";
|
|
||||||
const char HTTP_FORM_PARAM_HEAD[] PROGMEM = "<hr><br/>";
|
|
||||||
const char HTTP_FORM_PARAM[] PROGMEM = "<br/><input id='{i}' name='{n}' maxlength='{l}' value='{v}' {c}>\n";
|
|
||||||
|
|
||||||
const char HTTP_SCAN_LINK[] PROGMEM = "<br/><form action='/wifi?refresh=1' method='POST'><button name='refresh' value='1'>重新扫描</button></form>";
|
|
||||||
const char HTTP_SAVED[] PROGMEM = "<div class='msg'>正在保存配置<br/>尝试连接 WiFi...<br/>如果连接失败,请重新连接 AP 再试一次</div>";
|
|
||||||
const char HTTP_PARAMSAVED[] PROGMEM = "<div class='msg S'>配置已保存<br/></div>";
|
|
||||||
const char HTTP_END[] PROGMEM = "</div></body></html>";
|
|
||||||
const char HTTP_ERASEBTN[] PROGMEM = "<br/><form action='/erase' method='get'><button class='D'>清除 WiFi 配置</button></form>";
|
|
||||||
const char HTTP_UPDATEBTN[] PROGMEM = "<br/><form action='/update' method='get'><button>固件升级</button></form>";
|
|
||||||
const char HTTP_BACKBTN[] PROGMEM = "<hr><br/><form action='/' method='get'><button>返回首页</button></form>";
|
|
||||||
|
|
||||||
const char HTTP_STATUS_ON[] PROGMEM = "<div class='msg S'><strong>已连接</strong> {v}<br/><em><small>IP 地址: {i}</small></em></div>";
|
|
||||||
const char HTTP_STATUS_OFF[] PROGMEM = "<div class='msg {c}'><strong>未连接</strong> {v}{r}</div>";
|
|
||||||
const char HTTP_STATUS_OFFPW[] PROGMEM = "<br/>WiFi 密码错误";
|
|
||||||
const char HTTP_STATUS_OFFNOAP[] PROGMEM = "<br/>未找到该 WiFi";
|
|
||||||
const char HTTP_STATUS_OFFFAIL[] PROGMEM = "<br/>无法连接";
|
|
||||||
const char HTTP_STATUS_NONE[] PROGMEM = "<div class='msg'>未设置 WiFi</div>";
|
|
||||||
const char HTTP_BR[] PROGMEM = "<br/>";
|
|
||||||
|
|
||||||
const char HTTP_STYLE[] PROGMEM = "<style>"
|
|
||||||
".c,body{text-align:center;font-family:'PingFang SC','Microsoft YaHei','Helvetica Neue',sans-serif}div,input,select{padding:5px;font-size:1em;margin:5px 0;box-sizing:border-box}"
|
|
||||||
"input,button,select,.msg{border-radius:.3rem;width: 100%}input[type=radio],input[type=checkbox]{width:auto}"
|
|
||||||
"button,input[type='button'],input[type='submit']{cursor:pointer;border:0;background-color:#1fa3ec;color:#fff;line-height:2.4rem;font-size:1.2rem;width:100%}"
|
|
||||||
"input[type='file']{border:1px solid #1fa3ec}"
|
|
||||||
".wrap {text-align:left;display:inline-block;min-width:260px;max-width:500px}"
|
|
||||||
"a{color:#000;font-weight:700;text-decoration:none}a:hover{color:#1fa3ec;text-decoration:underline}"
|
|
||||||
".q{height:16px;margin:0;padding:0 5px;text-align:right;min-width:38px;float:right}.q.q-0:after{background-position-x:0}.q.q-1:after{background-position-x:-16px}.q.q-2:after{background-position-x:-32px}.q.q-3:after{background-position-x:-48px}.q.q-4:after{background-position-x:-64px}.q.l:before{background-position-x:-80px;padding-right:5px}.ql .q{float:left}.q:after,.q:before{content:'';width:16px;height:16px;display:inline-block;background-repeat:no-repeat;background-position: 16px 0;"
|
|
||||||
"background-image:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAAAQCAMAAADeZIrLAAAAJFBMVEX///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADHJj5lAAAAC3RSTlMAIjN3iJmqu8zd7vF8pzcAAABsSURBVHja7Y1BCsAwCASNSVo3/v+/BUEiXnIoXkoX5jAQMxTHzK9cVSnvDxwD8bFx8PhZ9q8FmghXBhqA1faxk92PsxvRc2CCCFdhQCbRkLoAQ3q/wWUBqG35ZxtVzW4Ed6LngPyBU2CobdIDQ5oPWI5nCUwAAAAASUVORK5CYII=');}"
|
|
||||||
"@media (-webkit-min-device-pixel-ratio: 2),(min-resolution: 192dpi){.q:before,.q:after {"
|
|
||||||
"background-image:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALwAAAAgCAMAAACfM+KhAAAALVBMVEX///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAOrOgAAAADnRSTlMAESIzRGZ3iJmqu8zd7gKjCLQAAACmSURBVHgB7dDBCoMwEEXRmKlVY3L//3NLhyzqIqSUggy8uxnhCR5Mo8xLt+14aZ7wwgsvvPA/ofv9+44334UXXngvb6XsFhO/VoC2RsSv9J7x8BnYLW+AjT56ud/uePMdb7IP8Bsc/e7h8Cfk912ghsNXWPpDC4hvN+D1560A1QPORyh84VKLjjdvfPFm++i9EWq0348XXnjhhT+4dIbCW+WjZim9AKk4UZMnnCEuAAAAAElFTkSuQmCC');"
|
|
||||||
"background-size: 95px 16px;}}"
|
|
||||||
".msg{padding:20px;margin:20px 0;border:1px solid #eee;border-left-width:5px;border-left-color:#777}.msg h4{margin-top:0;margin-bottom:5px}.msg.P{border-left-color:#1fa3ec}.msg.P h4{color:#1fa3ec}.msg.D{border-left-color:#dc3630}.msg.D h4{color:#dc3630}.msg.S{border-left-color: #5cb85c}.msg.S h4{color: #5cb85c}"
|
|
||||||
"dt{font-weight:bold}dd{margin:0;padding:0 0 0.5em 0;min-height:12px}"
|
|
||||||
"td{vertical-align: top;}"
|
|
||||||
".h{display:none}"
|
|
||||||
"button{transition: 0s opacity;transition-delay: 3s;transition-duration: 0s;cursor: pointer}"
|
|
||||||
"button.D{background-color:#dc3630}"
|
|
||||||
"button:active{opacity:50% !important;cursor:wait;transition-delay: 0s}"
|
|
||||||
"body.invert,body.invert a,body.invert h1 {background-color:#060606;color:#fff;}"
|
|
||||||
"body.invert .msg{color:#fff;background-color:#282828;border-top:1px solid #555;border-right:1px solid #555;border-bottom:1px solid #555;}"
|
|
||||||
"body.invert .q[role=img]{-webkit-filter:invert(1);filter:invert(1);}"
|
|
||||||
":disabled {opacity: 0.5;}"
|
|
||||||
"</style>";
|
|
||||||
|
|
||||||
#ifndef WM_NOHELP
|
|
||||||
const char HTTP_HELP[] PROGMEM =
|
|
||||||
"<br/><h3>可用页面</h3><hr>"
|
|
||||||
"<table class='table'>"
|
|
||||||
"<thead><tr><th>页面</th><th>功能</th></tr></thead><tbody>"
|
|
||||||
"<tr><td><a href='/'>/</a></td>"
|
|
||||||
"<td>主菜单页面</td></tr>"
|
|
||||||
"<tr><td><a href='/wifi'>/wifi</a></td>"
|
|
||||||
"<td>显示 WiFi 扫描结果并配置连接 (/0wifi 不扫描)</td></tr>"
|
|
||||||
"<tr><td><a href='/wifisave'>/wifisave</a></td>"
|
|
||||||
"<td>保存 WiFi 配置并连接设备</td></tr>"
|
|
||||||
"<tr><td><a href='/param'>/param</a></td>"
|
|
||||||
"<td>参数设置页面</td></tr>"
|
|
||||||
"<tr><td><a href='/info'>/info</a></td>"
|
|
||||||
"<td>系统信息页面</td></tr>"
|
|
||||||
"<tr><td><a href='/u'>/u</a></td>"
|
|
||||||
"<td>固件 OTA 升级</td></tr>"
|
|
||||||
"<tr><td><a href='/close'>/close</a></td>"
|
|
||||||
"<td>关闭弹出窗口,配网服务保持运行</td></tr>"
|
|
||||||
"<tr><td>/exit</td>"
|
|
||||||
"<td>退出配网模式,配网服务将关闭</td></tr>"
|
|
||||||
"<tr><td>/restart</td>"
|
|
||||||
"<td>重启设备</td></tr>"
|
|
||||||
"<tr><td>/erase</td>"
|
|
||||||
"<td>清除 WiFi 配置并重启。清除后设备不会自动连接,需重新配网</td></tr>"
|
|
||||||
"</table>"
|
|
||||||
"<p/>GitHub <a href='https://github.com/tzapu/WiFiManager'>https://github.com/tzapu/WiFiManager</a>.";
|
|
||||||
#else
|
|
||||||
const char HTTP_HELP[] PROGMEM = "";
|
|
||||||
#endif
|
|
||||||
|
|
||||||
const char HTTP_UPDATE[] PROGMEM = "上传新固件<br/><form method='POST' action='u' enctype='multipart/form-data' onchange=\"(function(el){document.getElementById('uploadbin').style.display = el.value=='' ? 'none' : 'initial';})(this)\"><input type='file' name='update' accept='.bin,application/octet-stream'><button id='uploadbin' type='submit' class='h D'>开始升级</button></form><small><a href='http://192.168.4.1/update' target='_blank'>* 如果在配网页面内无法使用,请在浏览器中打开 http://192.168.4.1</a><small>";
|
|
||||||
const char HTTP_UPDATE_FAIL[] PROGMEM = "<div class='msg D'><strong>升级失败!</strong><Br/>请重启设备后重试</div>";
|
|
||||||
const char HTTP_UPDATE_SUCCESS[] PROGMEM = "<div class='msg S'><strong>升级成功!</strong><Br/>设备正在重启...</div>";
|
|
||||||
|
|
||||||
#ifdef WM_JSTEST
|
|
||||||
const char HTTP_JS[] PROGMEM =
|
|
||||||
"<script>function postAjax(url, data, success) {"
|
|
||||||
" var params = typeof data == 'string' ? data : Object.keys(data).map("
|
|
||||||
" function(k){ return encodeURIComponent(k) + '=' + encodeURIComponent(data[k]) }"
|
|
||||||
" ).join('&');"
|
|
||||||
" var xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject(\"Microsoft.XMLHTTP\");"
|
|
||||||
" xhr.open('POST', url);"
|
|
||||||
" xhr.onreadystatechange = function() {"
|
|
||||||
" if (xhr.readyState>3 && xhr.status==200) { success(xhr.responseText); }"
|
|
||||||
" };"
|
|
||||||
" xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');"
|
|
||||||
" xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');"
|
|
||||||
" xhr.send(params);"
|
|
||||||
" return xhr;}"
|
|
||||||
"postAjax('/status', 'p1=1&p2=Hello+World', function(data){ console.log(data); });"
|
|
||||||
"postAjax('/status', { p1: 1, p2: 'Hello World' }, function(data){ console.log(data); });"
|
|
||||||
"</script>";
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Info html
|
|
||||||
#ifdef ESP32
|
|
||||||
const char HTTP_INFO_esphead[] PROGMEM = "<h3>ESP32 信息</h3><hr><dl>";
|
|
||||||
const char HTTP_INFO_chiprev[] PROGMEM = "<dt>芯片版本</dt><dd>{1}</dd>";
|
|
||||||
const char HTTP_INFO_lastreset[] PROGMEM = "<dt>上次复位原因</dt><dd>CPU0: {1}<br/>CPU1: {2}</dd>";
|
|
||||||
const char HTTP_INFO_aphost[] PROGMEM = "<dt>AP 主机名</dt><dd>{1}</dd>";
|
|
||||||
const char HTTP_INFO_psrsize[] PROGMEM = "<dt>PSRAM 大小</dt><dd>{1} 字节</dd>";
|
|
||||||
const char HTTP_INFO_temp[] PROGMEM = "<dt>芯片温度</dt><dd>{1}°C / {2}°F</dd>";
|
|
||||||
const char HTTP_INFO_hall[] PROGMEM = "<dt>霍尔传感器</dt><dd>{1}</dd>";
|
|
||||||
#else
|
|
||||||
const char HTTP_INFO_esphead[] PROGMEM = "<h3>ESP8266 信息</h3><hr><dl>";
|
|
||||||
const char HTTP_INFO_fchipid[] PROGMEM = "<dt>Flash 芯片 ID</dt><dd>{1}</dd>";
|
|
||||||
const char HTTP_INFO_corever[] PROGMEM = "<dt>Core 版本</dt><dd>{1}</dd>";
|
|
||||||
const char HTTP_INFO_bootver[] PROGMEM = "<dt>Boot 版本</dt><dd>{1}</dd>";
|
|
||||||
const char HTTP_INFO_lastreset[] PROGMEM = "<dt>上次复位原因</dt><dd>{1}</dd>";
|
|
||||||
const char HTTP_INFO_flashsize[] PROGMEM = "<dt>实际 Flash 大小</dt><dd>{1} 字节</dd>";
|
|
||||||
#endif
|
|
||||||
|
|
||||||
const char HTTP_INFO_memsmeter[] PROGMEM = "<br/><progress value='{1}' max='{2}'></progress></dd>";
|
|
||||||
const char HTTP_INFO_memsketch[] PROGMEM = "<dt>内存 - 程序大小</dt><dd>已用 / 总计<br/>{1} / {2} 字节";
|
|
||||||
const char HTTP_INFO_freeheap[] PROGMEM = "<dt>内存 - 可用堆</dt><dd>{1} 字节</dd>";
|
|
||||||
const char HTTP_INFO_wifihead[] PROGMEM = "<br/><h3>WiFi 信息</h3><hr>";
|
|
||||||
const char HTTP_INFO_uptime[] PROGMEM = "<dt>运行时间</dt><dd>{1} 分 {2} 秒</dd>";
|
|
||||||
const char HTTP_INFO_chipid[] PROGMEM = "<dt>芯片 ID</dt><dd>{1}</dd>";
|
|
||||||
const char HTTP_INFO_idesize[] PROGMEM = "<dt>Flash 大小</dt><dd>{1} 字节</dd>";
|
|
||||||
const char HTTP_INFO_sdkver[] PROGMEM = "<dt>SDK 版本</dt><dd>{1}</dd>";
|
|
||||||
const char HTTP_INFO_cpufreq[] PROGMEM = "<dt>CPU 频率</dt><dd>{1}MHz</dd>";
|
|
||||||
const char HTTP_INFO_apip[] PROGMEM = "<dt>AP IP 地址</dt><dd>{1}</dd>";
|
|
||||||
const char HTTP_INFO_apmac[] PROGMEM = "<dt>AP MAC 地址</dt><dd>{1}</dd>";
|
|
||||||
const char HTTP_INFO_apssid[] PROGMEM = "<dt>AP SSID</dt><dd>{1}</dd>";
|
|
||||||
const char HTTP_INFO_apbssid[] PROGMEM = "<dt>BSSID</dt><dd>{1}</dd>";
|
|
||||||
const char HTTP_INFO_stassid[] PROGMEM = "<dt>Station SSID</dt><dd>{1}</dd>";
|
|
||||||
const char HTTP_INFO_staip[] PROGMEM = "<dt>Station IP</dt><dd>{1}</dd>";
|
|
||||||
const char HTTP_INFO_stagw[] PROGMEM = "<dt>网关地址</dt><dd>{1}</dd>";
|
|
||||||
const char HTTP_INFO_stasub[] PROGMEM = "<dt>子网掩码</dt><dd>{1}</dd>";
|
|
||||||
const char HTTP_INFO_dnss[] PROGMEM = "<dt>DNS 服务器</dt><dd>{1}</dd>";
|
|
||||||
const char HTTP_INFO_host[] PROGMEM = "<dt>主机名</dt><dd>{1}</dd>";
|
|
||||||
const char HTTP_INFO_stamac[] PROGMEM = "<dt>Station MAC</dt><dd>{1}</dd>";
|
|
||||||
const char HTTP_INFO_conx[] PROGMEM = "<dt>连接状态</dt><dd>{1}</dd>";
|
|
||||||
const char HTTP_INFO_autoconx[] PROGMEM = "<dt>自动连接</dt><dd>{1}</dd>";
|
|
||||||
|
|
||||||
const char HTTP_INFO_aboutver[] PROGMEM = "<dt>WiFiManager 版本</dt><dd>{1}</dd>";
|
|
||||||
const char HTTP_INFO_aboutarduino[] PROGMEM = "<dt>Arduino 版本</dt><dd>{1}</dd>";
|
|
||||||
const char HTTP_INFO_aboutsdk[] PROGMEM = "<dt>ESP-SDK/IDF 版本</dt><dd>{1}</dd>";
|
|
||||||
const char HTTP_INFO_aboutdate[] PROGMEM = "<dt>构建日期</dt><dd>{1}</dd>";
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// 文本常量
|
|
||||||
// ============================================================
|
|
||||||
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
|
|
||||||
+3
-4
@@ -9,6 +9,8 @@ framework = arduino
|
|||||||
; 串口监视器
|
; 串口监视器
|
||||||
monitor_speed = 115200
|
monitor_speed = 115200
|
||||||
monitor_filters = direct
|
monitor_filters = direct
|
||||||
|
monitor_rts = 0
|
||||||
|
monitor_dtr = 0
|
||||||
|
|
||||||
; Flash 配置
|
; Flash 配置
|
||||||
board_build.flash_mode = qio
|
board_build.flash_mode = qio
|
||||||
@@ -25,9 +27,6 @@ build_flags =
|
|||||||
|
|
||||||
; 库依赖
|
; 库依赖
|
||||||
lib_deps =
|
lib_deps =
|
||||||
; WiFi 配网 (自带 captive portal)
|
|
||||||
tzapu/WiFiManager@^2.0.17
|
|
||||||
|
|
||||||
; MQTT 客户端
|
; MQTT 客户端
|
||||||
knolleary/PubSubClient@^2.8
|
knolleary/PubSubClient@^2.8
|
||||||
|
|
||||||
@@ -43,4 +42,4 @@ lib_deps =
|
|||||||
|
|
||||||
; 上传配置
|
; 上传配置
|
||||||
upload_speed = 921600
|
upload_speed = 921600
|
||||||
upload_port = COM16
|
upload_port = COM19
|
||||||
|
|||||||
+33
-19
@@ -64,9 +64,9 @@ AlarmManager::~AlarmManager() {}
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
bool AlarmManager::begin() {
|
bool AlarmManager::begin() {
|
||||||
ledcSetup(BUZZER_PWM_CHANNEL, BUZZER_PWM_FREQ, BUZZER_PWM_RESOLUTION);
|
// 有源蜂鸣器: 用 digitalWrite, 不用 PWM
|
||||||
ledcAttachPin(BUZZER_PIN, BUZZER_PWM_CHANNEL);
|
pinMode(BUZZER_PIN, OUTPUT);
|
||||||
ledcWrite(BUZZER_PWM_CHANNEL, 0);
|
digitalWrite(BUZZER_PIN, LOW);
|
||||||
|
|
||||||
pinMode(LED_ALARM_PIN, OUTPUT);
|
pinMode(LED_ALARM_PIN, OUTPUT);
|
||||||
digitalWrite(LED_ALARM_PIN, LOW);
|
digitalWrite(LED_ALARM_PIN, LOW);
|
||||||
@@ -148,26 +148,26 @@ static void _calcBuzzerParams(AlarmManager::AlarmType t,
|
|||||||
uint8_t& beeps, uint32_t& onMs,
|
uint8_t& beeps, uint32_t& onMs,
|
||||||
uint32_t& offMs, uint32_t& pauseMs) {
|
uint32_t& offMs, uint32_t& pauseMs) {
|
||||||
switch (t) {
|
switch (t) {
|
||||||
// 人身安全 — 连速急哔 (最刺耳)
|
// 人身安全 — 连速急哔
|
||||||
case AlarmManager::ALARM_FALL:
|
case AlarmManager::ALARM_FALL:
|
||||||
beeps = 0; onMs = 60; offMs = 60; pauseMs = 0; // 跌倒: 连速哔
|
beeps = 0; onMs = 200; offMs = 200; pauseMs = 0; // 跌倒: 急促哔哔
|
||||||
break;
|
break;
|
||||||
case AlarmManager::ALARM_HEART_ABNORMAL:
|
case AlarmManager::ALARM_HEART_ABNORMAL:
|
||||||
case AlarmManager::ALARM_BREATH_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;
|
break;
|
||||||
// 环境安全 — 次急
|
// 环境安全
|
||||||
case AlarmManager::ALARM_WATER_LEAK:
|
case AlarmManager::ALARM_WATER_LEAK:
|
||||||
beeps = 3; onMs = 120; offMs = 100; pauseMs = 500; // 水浸: 3连哔
|
beeps = 3; onMs = 250; offMs = 200; pauseMs = 600; // 水浸: 3连哔
|
||||||
break;
|
break;
|
||||||
// 环境舒适 — 温和
|
// 环境舒适
|
||||||
case AlarmManager::ALARM_TEMP_ABNORMAL:
|
case AlarmManager::ALARM_TEMP_ABNORMAL:
|
||||||
case AlarmManager::ALARM_HUMI_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;
|
break;
|
||||||
// 设备状态 — 仅提示
|
// 设备状态
|
||||||
case AlarmManager::ALARM_SENSOR_FAULT:
|
case AlarmManager::ALARM_SENSOR_FAULT:
|
||||||
beeps = 1; onMs = 300; offMs = 3000; pauseMs = 0; // 传感器: 单声提醒
|
beeps = 1; onMs = 500; offMs = 3000; pauseMs = 0; // 传感器: 单声提醒
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
beeps = 0; onMs = 500; offMs = 500; pauseMs = 0; // 持续
|
beeps = 0; onMs = 500; offMs = 500; pauseMs = 0; // 持续
|
||||||
@@ -258,6 +258,11 @@ void AlarmManager::executeAlarm(const AlarmCommand& cmd) {
|
|||||||
AlarmType localType = ALARM_SERVER;
|
AlarmType localType = ALARM_SERVER;
|
||||||
if (strcmp(cmd.alarmType, "fall") == 0) localType = ALARM_FALL;
|
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, "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;
|
||||||
|
|
||||||
if (cmd.buzzerOn) {
|
if (cmd.buzzerOn) {
|
||||||
triggerLocal(localType, cmd.alarmDesc);
|
triggerLocal(localType, cmd.alarmDesc);
|
||||||
@@ -279,6 +284,11 @@ void AlarmManager::executeRestore(const AlarmCommand& cmd) {
|
|||||||
AlarmType localType = ALARM_SERVER;
|
AlarmType localType = ALARM_SERVER;
|
||||||
if (strcmp(cmd.alarmType, "fall") == 0) localType = ALARM_FALL;
|
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, "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);
|
clearLocal(localType);
|
||||||
|
|
||||||
@@ -308,7 +318,7 @@ void AlarmManager::_setBuzzer(bool on) {
|
|||||||
if (on) {
|
if (on) {
|
||||||
_buzzerOffTime = 0;
|
_buzzerOffTime = 0;
|
||||||
} else {
|
} else {
|
||||||
ledcWrite(BUZZER_PWM_CHANNEL, 0);
|
digitalWrite(BUZZER_PIN, LOW);
|
||||||
_buzzerOffTime = 0;
|
_buzzerOffTime = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -328,16 +338,20 @@ void AlarmManager::_updateBuzzer() {
|
|||||||
// 定时关闭
|
// 定时关闭
|
||||||
if (_buzzerOffTime > 0 && millis() >= _buzzerOffTime) {
|
if (_buzzerOffTime > 0 && millis() >= _buzzerOffTime) {
|
||||||
_buzzerOn = false;
|
_buzzerOn = false;
|
||||||
ledcWrite(BUZZER_PWM_CHANNEL, 0);
|
digitalWrite(BUZZER_PIN, LOW);
|
||||||
Serial.println(F("[告警] ⏰ 蜂鸣器定时关闭"));
|
Serial.println(F("[告警] ⏰ 蜂鸣器定时关闭"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t now = millis();
|
uint32_t now = millis();
|
||||||
|
|
||||||
// 云端定时: 持续响
|
// 云端定时: 哔哔声 (500ms响/500ms停), 不是长鸣
|
||||||
if (_buzzerOffTime > 0) {
|
if (_buzzerOffTime > 0) {
|
||||||
ledcWrite(BUZZER_PWM_CHANNEL, 128);
|
if (now - _buzzerToggleTime >= 500) {
|
||||||
|
_buzzerToggleTime = now;
|
||||||
|
_buzzerBeepState = !_buzzerBeepState;
|
||||||
|
digitalWrite(BUZZER_PIN, _buzzerBeepState ? HIGH : LOW);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,7 +361,7 @@ void AlarmManager::_updateBuzzer() {
|
|||||||
if (now - _buzzerToggleTime >= (_buzzerBeepState ? _beepOnMs : _beepOffMs)) {
|
if (now - _buzzerToggleTime >= (_buzzerBeepState ? _beepOnMs : _beepOffMs)) {
|
||||||
_buzzerToggleTime = now;
|
_buzzerToggleTime = now;
|
||||||
_buzzerBeepState = !_buzzerBeepState;
|
_buzzerBeepState = !_buzzerBeepState;
|
||||||
ledcWrite(BUZZER_PWM_CHANNEL, _buzzerBeepState ? 128 : 0);
|
digitalWrite(BUZZER_PIN, _buzzerBeepState ? HIGH : LOW);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 多声一组: N声哔 + 长停顿
|
// 多声一组: N声哔 + 长停顿
|
||||||
@@ -358,7 +372,7 @@ void AlarmManager::_updateBuzzer() {
|
|||||||
_buzzerToggleTime = now;
|
_buzzerToggleTime = now;
|
||||||
if (_beepCount >= _beepsPerCycle) _beepCount = 0;
|
if (_beepCount >= _beepsPerCycle) _beepCount = 0;
|
||||||
_buzzerBeepState = true;
|
_buzzerBeepState = true;
|
||||||
ledcWrite(BUZZER_PWM_CHANNEL, 128);
|
digitalWrite(BUZZER_PIN, HIGH);
|
||||||
_beepCount++;
|
_beepCount++;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -366,7 +380,7 @@ void AlarmManager::_updateBuzzer() {
|
|||||||
if (now - _buzzerToggleTime >= _beepOnMs) {
|
if (now - _buzzerToggleTime >= _beepOnMs) {
|
||||||
_buzzerToggleTime = now;
|
_buzzerToggleTime = now;
|
||||||
_buzzerBeepState = false;
|
_buzzerBeepState = false;
|
||||||
ledcWrite(BUZZER_PWM_CHANNEL, 0);
|
digitalWrite(BUZZER_PIN, LOW);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-7
@@ -44,13 +44,13 @@ void ButtonManager::begin() {
|
|||||||
pinMode(BTN_ALARM_DISMISS_PIN, INPUT_PULLUP);
|
pinMode(BTN_ALARM_DISMISS_PIN, INPUT_PULLUP);
|
||||||
pinMode(BTN_PAGE_SWITCH_PIN, INPUT_PULLUP);
|
pinMode(BTN_PAGE_SWITCH_PIN, INPUT_PULLUP);
|
||||||
|
|
||||||
// 读取初始状态, 避免启动时的虚假边沿
|
// 强制初始化为未按下 (PCB 上电 GPIO 不稳, 忽略初始状态)
|
||||||
_btnProvision.rawState = _readPin(BTN_PROVISION_PIN);
|
_btnProvision.rawState = false;
|
||||||
_btnProvision.stableState = _btnProvision.rawState;
|
_btnProvision.stableState = false;
|
||||||
_btnDismiss.rawState = _readPin(BTN_ALARM_DISMISS_PIN);
|
_btnDismiss.rawState = false;
|
||||||
_btnDismiss.stableState = _btnDismiss.rawState;
|
_btnDismiss.stableState = false;
|
||||||
_btnPage.rawState = _readPin(BTN_PAGE_SWITCH_PIN);
|
_btnPage.rawState = false;
|
||||||
_btnPage.stableState = _btnPage.rawState;
|
_btnPage.stableState = false;
|
||||||
|
|
||||||
Serial.println(F("[按钮] ✅ 三按钮初始化完成 (双向去抖+超时自愈)"));
|
Serial.println(F("[按钮] ✅ 三按钮初始化完成 (双向去抖+超时自愈)"));
|
||||||
Serial.printf(" 配网: GPIO%d | 解除报警: GPIO%d | 翻页: GPIO%d\n",
|
Serial.printf(" 配网: GPIO%d | 解除报警: GPIO%d | 翻页: GPIO%d\n",
|
||||||
@@ -64,6 +64,9 @@ void ButtonManager::begin() {
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
void ButtonManager::loop() {
|
void ButtonManager::loop() {
|
||||||
|
// 开机 10 秒静默 (PCB 上电 GPIO 不稳 + 强制初始未按下)
|
||||||
|
if (millis() < 10000) return;
|
||||||
|
|
||||||
// 按钮1: 配网 (仅长按)
|
// 按钮1: 配网 (仅长按)
|
||||||
_processButton(_btnProvision, false, true);
|
_processButton(_btnProvision, false, true);
|
||||||
|
|
||||||
|
|||||||
+300
-43
@@ -45,11 +45,35 @@ bool g_localHumiAbnActive = false;
|
|||||||
bool g_localRadarFaultActive = false;
|
bool g_localRadarFaultActive = false;
|
||||||
bool g_localDhtFaultActive = false;
|
bool g_localDhtFaultActive = false;
|
||||||
|
|
||||||
|
// 手动解除抑制 — dismiss 后不重复报警, 条件恢复正常才重新武装
|
||||||
|
bool g_waterDismissed = false;
|
||||||
|
bool g_heartDismissed = false;
|
||||||
|
bool g_breathDismissed = false;
|
||||||
|
bool g_tempDismissed = false;
|
||||||
|
bool g_humiDismissed = false;
|
||||||
|
|
||||||
// 报警防抖计数器 (连续N次确认才触发)
|
// 报警防抖计数器 (连续N次确认才触发)
|
||||||
uint8_t g_tempAbnCnt = 0;
|
uint8_t g_tempAbnCnt = 0;
|
||||||
uint8_t g_humiAbnCnt = 0;
|
uint8_t g_humiAbnCnt = 0;
|
||||||
uint8_t g_radarTimeoutCnt = 0;
|
uint8_t g_radarTimeoutCnt = 0;
|
||||||
|
uint32_t g_heartAbnMs = 0; // 心率异常持续计时 (ms)
|
||||||
|
uint32_t g_heartNormalMs = 0; // 心率正常持续计时 (ms)
|
||||||
|
uint32_t g_breathAbnMs = 0; // 呼吸异常持续计时 (ms)
|
||||||
|
uint32_t g_breathNormalMs = 0; // 呼吸正常持续计时 (ms)
|
||||||
|
uint32_t g_fallDetectMs = 0; // 跌倒检测计时 (ms)
|
||||||
|
uint32_t g_fallClearMs = 0; // 跌倒清除计时 (ms)
|
||||||
|
bool g_fallDismissed = false; // 已手动解除, 不重复报警
|
||||||
|
uint32_t g_vitalLostMs = 0; // 生命体征丢失持续计时 (ms)
|
||||||
|
bool g_vitalLostActive = false; // 生命体征丢失报警激活
|
||||||
|
bool g_vitalLostDismissed = false; // 生命体征丢失解除抑制
|
||||||
|
uint32_t g_stillMs = 0; // 持续静默计时 (清醒时即使雷达误判, 持续有体动会重置)
|
||||||
#define ALARM_HYSTERESIS 3 // 连续3次确认才触发
|
#define ALARM_HYSTERESIS 3 // 连续3次确认才触发
|
||||||
|
#define FALL_DETECT_MS 10000 // 跌倒持续10秒才触发 (房顶安装, 过滤坐下)
|
||||||
|
#define FALL_CLEAR_MS 18000 // 跌倒消失18秒后才清除 (防误消)
|
||||||
|
#define VITAL_DETECT_MS 20000 // 心率/呼吸持续异常20秒才触发
|
||||||
|
#define VITAL_CLEAR_MS 15000 // 心率/呼吸持续正常15秒才恢复
|
||||||
|
#define VITAL_LOST_MS 60000 // 在床但心率呼吸持续为0达60秒 → 疑似骤停
|
||||||
|
#define SLEEP_QUIET_MS 300000 // 需持续5分钟静默才上报睡眠评分 (清醒坐着会被雷达误判, 靠这个过滤)
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 时间同步
|
// 时间同步
|
||||||
@@ -84,14 +108,41 @@ void syncTime() {
|
|||||||
|
|
||||||
void handleAlarmCommand(const AlarmCommand& cmd) {
|
void handleAlarmCommand(const AlarmCommand& cmd) {
|
||||||
if (strcmp(cmd.type, "alarm_restore") == 0) {
|
if (strcmp(cmd.type, "alarm_restore") == 0) {
|
||||||
// 告警恢复
|
// 告警恢复 — 远程指令与手动按钮同等优先级 (镜像 onAlarmDismiss 逻辑)
|
||||||
g_alarm.executeRestore(cmd);
|
g_alarm.clearAllLocal();
|
||||||
|
|
||||||
// 清除对应本地告警状态
|
// 清除全部本地告警状态 (远程恢复=手动解除)
|
||||||
if (strcmp(cmd.alarmType, "fall") == 0) g_localFallActive = false;
|
g_localFallActive = false;
|
||||||
else if (strcmp(cmd.alarmType, "water_leak") == 0) g_localWaterActive = false;
|
g_localWaterActive = false;
|
||||||
else if (strcmp(cmd.alarmType, "heart_abnormal") == 0) g_localHeartAbnActive = false;
|
g_localHeartAbnActive = false;
|
||||||
else if (strcmp(cmd.alarmType, "breath_abnormal") == 0) g_localBreathAbnActive = 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_heartAbnMs = 0;
|
||||||
|
g_heartNormalMs = 0;
|
||||||
|
g_breathAbnMs = 0;
|
||||||
|
g_breathNormalMs = 0;
|
||||||
|
g_fallDetectMs = 0;
|
||||||
|
g_fallClearMs = 0;
|
||||||
|
g_vitalLostMs = 0;
|
||||||
|
g_vitalLostActive = false;
|
||||||
|
|
||||||
|
// 抑制标志置 true — 与按钮一致 (原 BUG: 置 false 导致报警立即重新触发)
|
||||||
|
// 等报警源信号真正恢复正常后才由 checkLocalAlarms() 重新武装
|
||||||
|
g_fallDismissed = true;
|
||||||
|
g_waterDismissed = true;
|
||||||
|
g_heartDismissed = true;
|
||||||
|
g_breathDismissed = true;
|
||||||
|
g_tempDismissed = true;
|
||||||
|
g_humiDismissed = true;
|
||||||
|
g_vitalLostDismissed = true;
|
||||||
} else {
|
} else {
|
||||||
// 告警触发
|
// 告警触发
|
||||||
g_alarm.executeAlarm(cmd);
|
g_alarm.executeAlarm(cmd);
|
||||||
@@ -201,11 +252,13 @@ TelemetryData buildTelemetry(const RadarRawData& radar) {
|
|||||||
memset(&t, 0, sizeof(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.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);
|
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
|
// DHT11
|
||||||
t.temp = g_sensor.getTemp();
|
t.temp = g_sensor.getTemp();
|
||||||
t.humi = g_sensor.getHumi();
|
t.humi = g_sensor.getHumi();
|
||||||
@@ -219,6 +272,25 @@ TelemetryData buildTelemetry(const RadarRawData& radar) {
|
|||||||
t.heartRate = radar.heartRate;
|
t.heartRate = radar.heartRate;
|
||||||
t.breathRate = radar.breathRate;
|
t.breathRate = radar.breathRate;
|
||||||
t.body = radar.body;
|
t.body = radar.body;
|
||||||
|
|
||||||
|
// ─── 睡眠状态判定: 设备端二次校验 ───
|
||||||
|
// 雷达常把"清醒静坐"误判为深睡 (只看体动/呼吸/心率不看眼睛)
|
||||||
|
// 设备端用"持续静默"过滤: 5 分钟内必须无任何体动才算真睡
|
||||||
|
// - 持续微动 (手掌点击/键盘敲击/呼吸起伏) → 必然判清醒
|
||||||
|
// - 完全无体动持续 5 分钟 → 才是真睡
|
||||||
|
if (radar.body > 0) {
|
||||||
|
g_stillMs = 0; // 任何体动立即重置, 表明此刻清醒
|
||||||
|
} else if (g_stillMs == 0) {
|
||||||
|
g_stillMs = millis();
|
||||||
|
}
|
||||||
|
bool realSleep = (radar.resident && radar.vitalValid &&
|
||||||
|
millis() - g_stillMs >= SLEEP_QUIET_MS);
|
||||||
|
|
||||||
|
// sleepState 二次校验: 雷达说睡 + 静默达标 → 上报; 否则强制清醒
|
||||||
|
t.sleepState = realSleep ? radar.sleepState : 0;
|
||||||
|
// 评分与 sleepState 保持一致 (同步显示)
|
||||||
|
t.sleepScore = realSleep ? radar.sleepScore : 0;
|
||||||
|
|
||||||
t.radarValid = radar.radarValid;
|
t.radarValid = radar.radarValid;
|
||||||
t.vitalValid = radar.vitalValid;
|
t.vitalValid = radar.vitalValid;
|
||||||
|
|
||||||
@@ -250,69 +322,190 @@ void onLocalAlarmChange(uint8_t type, bool triggered, const char* desc) {
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
void checkLocalAlarms(const RadarRawData& radar) {
|
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;
|
bool humanPresent = radar.presence && radar.radarValid;
|
||||||
|
|
||||||
// ─── ★★★★★ 人身安全: 跌倒 (立即触发, 无防抖) ───
|
// ─── ★★★★★ 人身安全: 跌倒 (仅浴室/走廊/客厅) ───
|
||||||
if (radar.fall && humanPresent && !g_localFallActive) {
|
bool fallNow = enFall && radar.fall && humanPresent; // 当前是否处于跌倒状态
|
||||||
|
|
||||||
|
if (fallNow && !g_fallDismissed && millis() > 30000) { // AFD1 自检约15-30s, 30s 跳过 + 2 帧去抖兜底
|
||||||
|
// 跌倒中且未解除 → 累积时长, 满 FALL_DETECT_MS 触发
|
||||||
|
if (!g_localFallActive) {
|
||||||
|
if (g_fallDetectMs == 0) {
|
||||||
|
g_fallDetectMs = millis();
|
||||||
|
} else if (millis() - g_fallDetectMs >= FALL_DETECT_MS) {
|
||||||
g_localFallActive = true;
|
g_localFallActive = true;
|
||||||
|
g_fallDetectMs = 0;
|
||||||
g_alarm.triggerLocal(AlarmManager::ALARM_FALL, "雷达检测到跌倒");
|
g_alarm.triggerLocal(AlarmManager::ALARM_FALL, "雷达检测到跌倒");
|
||||||
} else if (!radar.fall && g_localFallActive) {
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
g_fallDetectMs = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跌倒信号真正消失后: 延迟清除报警 + 重新武装解除抑制 (防 AFD1 频繁跳变)
|
||||||
|
if (!fallNow) {
|
||||||
|
if (g_fallClearMs == 0) {
|
||||||
|
g_fallClearMs = millis();
|
||||||
|
}
|
||||||
|
if (millis() - g_fallClearMs >= FALL_CLEAR_MS) {
|
||||||
|
if (g_localFallActive) {
|
||||||
g_localFallActive = false;
|
g_localFallActive = false;
|
||||||
g_alarm.clearLocal(AlarmManager::ALARM_FALL);
|
g_alarm.clearLocal(AlarmManager::ALARM_FALL);
|
||||||
}
|
}
|
||||||
|
if (g_fallDismissed) {
|
||||||
|
g_fallDismissed = false; // 跌倒真正消失才重新武装
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
g_fallClearMs = 0; // 仍在跌倒, 重置消失计时
|
||||||
|
}
|
||||||
|
|
||||||
// ─── ★★★★★ 人身安全: 心率异常 (有人才检测) ───
|
// ─── ★★★★★ 人身安全: 心率异常 (仅卧室+ABD1, 持续异常20s触发/正常15s恢复, 30s) ───
|
||||||
if (humanPresent && radar.vitalValid && radar.heartRate > 0) {
|
#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);
|
bool heartAbn = (radar.heartRate < HEART_RATE_MIN || radar.heartRate > HEART_RATE_MAX);
|
||||||
if (heartAbn && !g_localHeartAbnActive) {
|
if (heartAbn) {
|
||||||
|
// 持续异常: 累积时长, 满 VITAL_DETECT_MS 触发
|
||||||
|
if (!g_heartDismissed && !g_localHeartAbnActive) {
|
||||||
|
if (g_heartAbnMs == 0) {
|
||||||
|
g_heartAbnMs = millis();
|
||||||
|
} else if (millis() - g_heartAbnMs >= VITAL_DETECT_MS) {
|
||||||
g_localHeartAbnActive = true;
|
g_localHeartAbnActive = true;
|
||||||
char buf[32];
|
char buf[32];
|
||||||
snprintf(buf, sizeof(buf), "心率异常: %d bpm", radar.heartRate);
|
snprintf(buf, sizeof(buf), "心率异常: %d bpm", radar.heartRate);
|
||||||
g_alarm.triggerLocal(AlarmManager::ALARM_HEART_ABNORMAL, buf);
|
g_alarm.triggerLocal(AlarmManager::ALARM_HEART_ABNORMAL, buf);
|
||||||
} else if (!heartAbn && g_localHeartAbnActive) {
|
}
|
||||||
|
}
|
||||||
|
g_heartNormalMs = 0; // 仍异常, 重置正常计时
|
||||||
|
} else {
|
||||||
|
// 持续正常: 累积时长, 满 VITAL_CLEAR_MS 清除 + 重新武装
|
||||||
|
g_heartAbnMs = 0; // 正常了, 重置异常计时
|
||||||
|
if (g_heartNormalMs == 0) {
|
||||||
|
g_heartNormalMs = millis();
|
||||||
|
}
|
||||||
|
if (millis() - g_heartNormalMs >= VITAL_CLEAR_MS) {
|
||||||
|
if (g_localHeartAbnActive) {
|
||||||
g_localHeartAbnActive = false;
|
g_localHeartAbnActive = false;
|
||||||
g_alarm.clearLocal(AlarmManager::ALARM_HEART_ABNORMAL);
|
g_alarm.clearLocal(AlarmManager::ALARM_HEART_ABNORMAL);
|
||||||
}
|
}
|
||||||
|
if (g_heartDismissed) {
|
||||||
|
g_heartDismissed = false; // 持续正常后重新武装
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (!humanPresent && g_localHeartAbnActive) {
|
} else if (!humanPresent && g_localHeartAbnActive) {
|
||||||
// 人走了 — 清除之前的报警
|
|
||||||
g_localHeartAbnActive = false;
|
g_localHeartAbnActive = false;
|
||||||
|
g_heartAbnMs = 0;
|
||||||
|
g_heartNormalMs = 0;
|
||||||
|
g_heartDismissed = false;
|
||||||
g_alarm.clearLocal(AlarmManager::ALARM_HEART_ABNORMAL);
|
g_alarm.clearLocal(AlarmManager::ALARM_HEART_ABNORMAL);
|
||||||
}
|
}
|
||||||
|
#endif // RADAR_TYPE != AFD1
|
||||||
|
|
||||||
// ─── ★★★★★ 人身安全: 呼吸异常 (有人才检测) ───
|
// ─── ★★★★★ 人身安全: 呼吸异常 (仅卧室+ABD1, 持续异常20s触发/正常15s恢复, 30秒后生效) ───
|
||||||
if (humanPresent && radar.vitalValid && radar.breathRate > 0) {
|
#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);
|
bool breathAbn = (radar.breathRate < BREATH_RATE_MIN || radar.breathRate > BREATH_RATE_MAX);
|
||||||
if (breathAbn && !g_localBreathAbnActive) {
|
if (breathAbn) {
|
||||||
|
// 持续异常: 累积时长, 满 VITAL_DETECT_MS 触发
|
||||||
|
if (!g_breathDismissed && !g_localBreathAbnActive) {
|
||||||
|
if (g_breathAbnMs == 0) {
|
||||||
|
g_breathAbnMs = millis();
|
||||||
|
} else if (millis() - g_breathAbnMs >= VITAL_DETECT_MS) {
|
||||||
g_localBreathAbnActive = true;
|
g_localBreathAbnActive = true;
|
||||||
char buf[32];
|
char buf[32];
|
||||||
snprintf(buf, sizeof(buf), "呼吸异常: %d 次/分", radar.breathRate);
|
snprintf(buf, sizeof(buf), "呼吸异常: %d 次/分", radar.breathRate);
|
||||||
g_alarm.triggerLocal(AlarmManager::ALARM_BREATH_ABNORMAL, buf);
|
g_alarm.triggerLocal(AlarmManager::ALARM_BREATH_ABNORMAL, buf);
|
||||||
} else if (!breathAbn && g_localBreathAbnActive) {
|
}
|
||||||
|
}
|
||||||
|
g_breathNormalMs = 0; // 仍异常, 重置正常计时
|
||||||
|
} else {
|
||||||
|
// 持续正常: 累积时长, 满 VITAL_CLEAR_MS 清除 + 重新武装
|
||||||
|
g_breathAbnMs = 0; // 正常了, 重置异常计时
|
||||||
|
if (g_breathNormalMs == 0) {
|
||||||
|
g_breathNormalMs = millis();
|
||||||
|
}
|
||||||
|
if (millis() - g_breathNormalMs >= VITAL_CLEAR_MS) {
|
||||||
|
if (g_localBreathAbnActive) {
|
||||||
g_localBreathAbnActive = false;
|
g_localBreathAbnActive = false;
|
||||||
g_alarm.clearLocal(AlarmManager::ALARM_BREATH_ABNORMAL);
|
g_alarm.clearLocal(AlarmManager::ALARM_BREATH_ABNORMAL);
|
||||||
}
|
}
|
||||||
|
if (g_breathDismissed) {
|
||||||
|
g_breathDismissed = false; // 持续正常后重新武装
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (!humanPresent && g_localBreathAbnActive) {
|
} else if (!humanPresent && g_localBreathAbnActive) {
|
||||||
g_localBreathAbnActive = false;
|
g_localBreathAbnActive = false;
|
||||||
|
g_breathAbnMs = 0;
|
||||||
|
g_breathNormalMs = 0;
|
||||||
|
g_breathDismissed = false;
|
||||||
g_alarm.clearLocal(AlarmManager::ALARM_BREATH_ABNORMAL);
|
g_alarm.clearLocal(AlarmManager::ALARM_BREATH_ABNORMAL);
|
||||||
}
|
}
|
||||||
|
#endif // RADAR_TYPE != AFD1
|
||||||
|
|
||||||
// ─── ★★★ 环境安全: 水浸 ───
|
// ─── ★★★★★ 人身安全: 生命体征丢失 (在床但心率呼吸持续为0 → 疑似骤停/死亡) ───
|
||||||
|
// 用 resident(在床) 而非 presence(有人): 人离房时 presence 有保持时间会误报,
|
||||||
|
// 而 resident 离床即清, 精准区分"人离开"和"人死在床上"。复用 心率+呼吸异常 类型。
|
||||||
|
#if RADAR_TYPE != RADAR_TYPE_AFD1
|
||||||
|
if (enVital && millis() > 30000) {
|
||||||
|
bool vitalLost = radar.resident && radar.heartRate == 0 && radar.breathRate == 0;
|
||||||
|
if (vitalLost) {
|
||||||
|
if (!g_vitalLostActive && !g_vitalLostDismissed) {
|
||||||
|
if (g_vitalLostMs == 0) {
|
||||||
|
g_vitalLostMs = millis();
|
||||||
|
} else if (millis() - g_vitalLostMs >= VITAL_LOST_MS) {
|
||||||
|
g_vitalLostActive = true;
|
||||||
|
g_vitalLostMs = 0;
|
||||||
|
g_alarm.triggerLocal(AlarmManager::ALARM_HEART_ABNORMAL, "生命体征丢失(疑似骤停)");
|
||||||
|
g_alarm.triggerLocal(AlarmManager::ALARM_BREATH_ABNORMAL, "生命体征丢失(疑似骤停)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
g_vitalLostMs = 0;
|
||||||
|
if (g_vitalLostActive) {
|
||||||
|
g_vitalLostActive = false;
|
||||||
|
g_alarm.clearLocal(AlarmManager::ALARM_HEART_ABNORMAL);
|
||||||
|
g_alarm.clearLocal(AlarmManager::ALARM_BREATH_ABNORMAL);
|
||||||
|
}
|
||||||
|
if (g_vitalLostDismissed) {
|
||||||
|
g_vitalLostDismissed = false; // 体征恢复或离床后重新武装
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif // RADAR_TYPE != AFD1
|
||||||
|
|
||||||
|
// ─── ★★★ 环境安全: 水浸 (仅浴室) ───
|
||||||
|
if (enWater) {
|
||||||
bool waterLeak = !g_sensor.getRainDO() && g_sensor.isRainValid();
|
bool waterLeak = !g_sensor.getRainDO() && g_sensor.isRainValid();
|
||||||
if (waterLeak && !g_localWaterActive) {
|
if (waterLeak) {
|
||||||
|
if (!g_waterDismissed && !g_localWaterActive) {
|
||||||
g_localWaterActive = true;
|
g_localWaterActive = true;
|
||||||
g_alarm.triggerLocal(AlarmManager::ALARM_WATER_LEAK, "水浸传感器触发");
|
g_alarm.triggerLocal(AlarmManager::ALARM_WATER_LEAK, "水浸传感器触发");
|
||||||
} else if (!waterLeak && g_localWaterActive) {
|
}
|
||||||
|
} else {
|
||||||
|
g_waterDismissed = false;
|
||||||
|
if (g_localWaterActive) {
|
||||||
g_localWaterActive = false;
|
g_localWaterActive = false;
|
||||||
g_alarm.clearLocal(AlarmManager::ALARM_WATER_LEAK);
|
g_alarm.clearLocal(AlarmManager::ALARM_WATER_LEAK);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
} // enWater
|
||||||
|
|
||||||
// ─── ★★ 环境舒适: 温度异常 (防抖3次) ───
|
// ─── ★★ 环境舒适: 温湿度异常 (仅卧室/阳台/厨房, 防抖3次, 30s) ───
|
||||||
if (g_sensor.isDhtValid()) {
|
if (enEnv && millis() > 30000 && g_sensor.isDhtValid()) {
|
||||||
float temp = g_sensor.getTemp();
|
float temp = g_sensor.getTemp();
|
||||||
bool tempAbn = (temp < TEMP_LOW || temp > TEMP_HIGH);
|
bool tempAbn = (temp < TEMP_LOW || temp > TEMP_HIGH);
|
||||||
if (tempAbn) {
|
if (tempAbn) {
|
||||||
if (++g_tempAbnCnt >= ALARM_HYSTERESIS && !g_localTempAbnActive) {
|
if (!g_tempDismissed && ++g_tempAbnCnt >= ALARM_HYSTERESIS && !g_localTempAbnActive) {
|
||||||
g_localTempAbnActive = true;
|
g_localTempAbnActive = true;
|
||||||
char buf[32];
|
char buf[32];
|
||||||
snprintf(buf, sizeof(buf), "温度%.1f°C 超出范围", temp);
|
snprintf(buf, sizeof(buf), "温度%.1f°C 超出范围", temp);
|
||||||
@@ -320,6 +513,7 @@ void checkLocalAlarms(const RadarRawData& radar) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
g_tempAbnCnt = 0;
|
g_tempAbnCnt = 0;
|
||||||
|
g_tempDismissed = false;
|
||||||
if (g_localTempAbnActive) {
|
if (g_localTempAbnActive) {
|
||||||
g_localTempAbnActive = false;
|
g_localTempAbnActive = false;
|
||||||
g_alarm.clearLocal(AlarmManager::ALARM_TEMP_ABNORMAL);
|
g_alarm.clearLocal(AlarmManager::ALARM_TEMP_ABNORMAL);
|
||||||
@@ -330,7 +524,7 @@ void checkLocalAlarms(const RadarRawData& radar) {
|
|||||||
float humi = g_sensor.getHumi();
|
float humi = g_sensor.getHumi();
|
||||||
bool humiAbn = (humi < HUMI_LOW || humi > HUMI_HIGH);
|
bool humiAbn = (humi < HUMI_LOW || humi > HUMI_HIGH);
|
||||||
if (humiAbn) {
|
if (humiAbn) {
|
||||||
if (++g_humiAbnCnt >= ALARM_HYSTERESIS && !g_localHumiAbnActive) {
|
if (!g_humiDismissed && ++g_humiAbnCnt >= ALARM_HYSTERESIS && !g_localHumiAbnActive) {
|
||||||
g_localHumiAbnActive = true;
|
g_localHumiAbnActive = true;
|
||||||
char buf[32];
|
char buf[32];
|
||||||
snprintf(buf, sizeof(buf), "湿度%.1f%% 超出范围", humi);
|
snprintf(buf, sizeof(buf), "湿度%.1f%% 超出范围", humi);
|
||||||
@@ -338,6 +532,7 @@ void checkLocalAlarms(const RadarRawData& radar) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
g_humiAbnCnt = 0;
|
g_humiAbnCnt = 0;
|
||||||
|
g_humiDismissed = false;
|
||||||
if (g_localHumiAbnActive) {
|
if (g_localHumiAbnActive) {
|
||||||
g_localHumiAbnActive = false;
|
g_localHumiAbnActive = false;
|
||||||
g_alarm.clearLocal(AlarmManager::ALARM_HUMI_ABNORMAL);
|
g_alarm.clearLocal(AlarmManager::ALARM_HUMI_ABNORMAL);
|
||||||
@@ -345,27 +540,27 @@ void checkLocalAlarms(const RadarRawData& radar) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── ★ 设备:雷达超时 (连续5秒无数据) ───
|
// ─── 设备:雷达超时 (连续5秒无数据, 仅标记, 不报警) ───
|
||||||
if (g_radar.isTimeout()) {
|
if (g_radar.isTimeout()) {
|
||||||
if (++g_radarTimeoutCnt >= 5 && !g_localRadarFaultActive) {
|
if (++g_radarTimeoutCnt >= 5 && !g_localRadarFaultActive) {
|
||||||
g_localRadarFaultActive = true;
|
g_localRadarFaultActive = true;
|
||||||
g_alarm.triggerLocal(AlarmManager::ALARM_SENSOR_FAULT, "雷达超时无数据");
|
Serial.println(F("[系统] ⚠️ 雷达超时无数据"));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
g_radarTimeoutCnt = 0;
|
g_radarTimeoutCnt = 0;
|
||||||
if (g_localRadarFaultActive) {
|
if (g_localRadarFaultActive) {
|
||||||
g_localRadarFaultActive = false;
|
g_localRadarFaultActive = false;
|
||||||
g_alarm.clearLocal(AlarmManager::ALARM_SENSOR_FAULT);
|
Serial.println(F("[系统] ✅ 雷达数据恢复"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── ★ 设备:DHT 持续故障 (已由 SensorManager 内部计数) ───
|
// ─── 设备:DHT 持续故障 (仅标记, 不报警) ───
|
||||||
if (g_sensor.isDhtFault() && !g_localDhtFaultActive && millis() > 60000) {
|
if (g_sensor.isDhtFault() && !g_localDhtFaultActive && millis() > 60000) {
|
||||||
g_localDhtFaultActive = true;
|
g_localDhtFaultActive = true;
|
||||||
g_alarm.triggerLocal(AlarmManager::ALARM_SENSOR_FAULT, "DHT11温湿度传感器故障");
|
Serial.println(F("[系统] ⚠️ DHT11 温湿度传感器故障"));
|
||||||
} else if (!g_sensor.isDhtFault() && g_localDhtFaultActive) {
|
} else if (!g_sensor.isDhtFault() && g_localDhtFaultActive) {
|
||||||
g_localDhtFaultActive = false;
|
g_localDhtFaultActive = false;
|
||||||
g_alarm.clearLocal(AlarmManager::ALARM_SENSOR_FAULT);
|
Serial.println(F("[系统] ✅ DHT11 传感器恢复"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -439,6 +634,33 @@ void setup() {
|
|||||||
});
|
});
|
||||||
g_buttons.onAlarmDismiss([]() {
|
g_buttons.onAlarmDismiss([]() {
|
||||||
g_alarm.clearAllLocal();
|
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_heartAbnMs = 0;
|
||||||
|
g_heartNormalMs = 0;
|
||||||
|
g_breathAbnMs = 0;
|
||||||
|
g_breathNormalMs = 0;
|
||||||
|
g_radarTimeoutCnt = 0;
|
||||||
|
g_fallClearMs = 0;
|
||||||
|
g_fallDismissed = true; // 抑制重复报警, 等跌倒信号消了再恢复
|
||||||
|
g_vitalLostMs = 0;
|
||||||
|
g_vitalLostActive = false;
|
||||||
|
// 其余类型同样抑制, 条件恢复正常后才重新武装
|
||||||
|
g_waterDismissed = true;
|
||||||
|
g_heartDismissed = true;
|
||||||
|
g_breathDismissed = true;
|
||||||
|
g_tempDismissed = true;
|
||||||
|
g_humiDismissed = true;
|
||||||
|
g_vitalLostDismissed = true;
|
||||||
});
|
});
|
||||||
g_buttons.onProvisionLongPress([]() {
|
g_buttons.onProvisionLongPress([]() {
|
||||||
g_wifi.startConfigPortal();
|
g_wifi.startConfigPortal();
|
||||||
@@ -474,6 +696,9 @@ void setup() {
|
|||||||
// 8. 传感器
|
// 8. 传感器
|
||||||
g_sensor.begin();
|
g_sensor.begin();
|
||||||
|
|
||||||
|
// 8. 等雷达 + 电源稳定
|
||||||
|
delay(8000);
|
||||||
|
|
||||||
// 9. 雷达任务 (核心0)
|
// 9. 雷达任务 (核心0)
|
||||||
xTaskCreatePinnedToCore(
|
xTaskCreatePinnedToCore(
|
||||||
radarTask, "RadarTask",
|
radarTask, "RadarTask",
|
||||||
@@ -507,13 +732,22 @@ void loop() {
|
|||||||
static bool wasDisconnected = true;
|
static bool wasDisconnected = true;
|
||||||
if (g_mqtt.isConnected()) {
|
if (g_mqtt.isConnected()) {
|
||||||
if (wasDisconnected) {
|
if (wasDisconnected) {
|
||||||
// 刚恢复连接, 同步时间
|
|
||||||
syncTime();
|
syncTime();
|
||||||
wasDisconnected = false;
|
wasDisconnected = false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
wasDisconnected = true;
|
wasDisconnected = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 配网成功后: 先更新 MQTT 配置, 再强制重连
|
||||||
|
if (g_wifi.justProvisioned()) {
|
||||||
|
DeviceConfig cfg = g_wifi.getConfig();
|
||||||
|
g_mqtt.disconnect();
|
||||||
|
g_mqtt.setServer(cfg.mqtt_server, atoi(cfg.mqtt_port),
|
||||||
|
cfg.mqtt_user, cfg.mqtt_pass);
|
||||||
|
g_mqtt.connect();
|
||||||
|
g_mqtt.publishProvisionStatus("success", "");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 3. 传感器 (独立,任何传感器故障不影响其他) ---
|
// --- 3. 传感器 (独立,任何传感器故障不影响其他) ---
|
||||||
@@ -523,34 +757,57 @@ void loop() {
|
|||||||
RadarRawData radar = g_radar.getData();
|
RadarRawData radar = g_radar.getData();
|
||||||
checkLocalAlarms(radar);
|
checkLocalAlarms(radar);
|
||||||
|
|
||||||
// --- 传感器故障日志 (报警系统统一管理触发, 此处仅记录) ---
|
// --- 传感器状态日志 (仅记录, 不报警) ---
|
||||||
static uint32_t lastFaultLog = 0;
|
static uint32_t lastFaultLog = 0;
|
||||||
if (now - lastFaultLog > 60000) {
|
if (now - lastFaultLog > 60000) {
|
||||||
lastFaultLog = now;
|
lastFaultLog = now;
|
||||||
if (!g_sensor.isDhtValid() && millis() > 60000 && !g_localDhtFaultActive) {
|
if (!g_sensor.isDhtValid() && millis() > 60000) {
|
||||||
Serial.println(F("[系统] ⚠️ DHT11 读数异常"));
|
Serial.println(F("[系统] ⚠️ DHT11 读数失效"));
|
||||||
}
|
}
|
||||||
if (g_radar.isTimeout() && !g_localRadarFaultActive) {
|
if (g_radar.isTimeout()) {
|
||||||
Serial.println(F("[系统] ⚠️ 雷达超时无数据"));
|
Serial.println(F("[系统] ⚠️ 雷达数据超时"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 5. 报警模块更新 (定时器/闪烁) ---
|
// --- 5. 报警模块更新 (定时器/闪烁) ---
|
||||||
g_alarm.loop();
|
g_alarm.loop();
|
||||||
|
|
||||||
// --- 6. 遥测上报 (1秒周期) ---
|
// --- 6. 遥测上报 ---
|
||||||
|
// 有人时: 每秒全量上报
|
||||||
|
// 无人时: 仅状态变化时发极简报文 + 60秒心跳 (省4G流量)
|
||||||
|
static bool lastPresence = false;
|
||||||
|
static uint32_t lastHeartbeat = 0; // 无人心跳计时
|
||||||
|
|
||||||
|
if (radar.presence) {
|
||||||
|
// ── 有人: 每秒完整上报 ──
|
||||||
if (now - g_lastReportTime >= REPORT_INTERVAL_MS) {
|
if (now - g_lastReportTime >= REPORT_INTERVAL_MS) {
|
||||||
g_lastReportTime = now;
|
g_lastReportTime = now;
|
||||||
|
|
||||||
TelemetryData tele = buildTelemetry(radar);
|
TelemetryData tele = buildTelemetry(radar);
|
||||||
g_mqtt.publishAllPoints(tele, g_sequence);
|
g_mqtt.publishAllPoints(tele, g_sequence);
|
||||||
g_sequence++;
|
g_sequence++;
|
||||||
|
lastHeartbeat = now; // 有人时重置心跳计时
|
||||||
|
|
||||||
// 调试输出 + OLED 显示
|
|
||||||
printDebug(tele);
|
printDebug(tele);
|
||||||
g_display.update(tele, wifiOk, g_alarm.isBuzzerOn(),
|
g_display.update(tele, wifiOk, g_alarm.isBuzzerOn(),
|
||||||
g_alarm.hasActiveAlarm(), g_alarm.getActiveAlarmStr());
|
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_display.update(tele, wifiOk, g_alarm.isBuzzerOn(),
|
||||||
|
g_alarm.hasActiveAlarm(), g_alarm.getActiveAlarmStr());
|
||||||
|
}
|
||||||
|
lastPresence = radar.presence;
|
||||||
|
|
||||||
// --- 7. MQTT 配网状态 (WiFiManager 触发时) ---
|
// --- 7. MQTT 配网状态 (WiFiManager 触发时) ---
|
||||||
// WiFiManager 的 captive portal 启动/关闭时更新状态
|
// WiFiManager 的 captive portal 启动/关闭时更新状态
|
||||||
|
|||||||
+59
-3
@@ -145,6 +145,16 @@ bool MQTTManager::isConnected() {
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
void MQTTManager::loop() {
|
void MQTTManager::loop() {
|
||||||
|
// 连接实际已断开 (网络抖动/broker重启/心跳超时), 复位标志触发重连
|
||||||
|
// 否则 _connected 永远为 true, 下面 else 分支只调 _mqtt.loop() 却不重连,
|
||||||
|
// 设备会卡在"WiFi通/MQTT死"的僵尸状态 (小程序显示离线)
|
||||||
|
if (_connected && !_mqtt.connected()) {
|
||||||
|
Serial.println("[MQTT] ⚠️ 连接断开, 触发重连...");
|
||||||
|
_connected = false;
|
||||||
|
_reconnectInterval = MQTT_RECONNECT_MIN;
|
||||||
|
_lastReconnectAttempt = 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (!_connected) {
|
if (!_connected) {
|
||||||
uint32_t now = millis();
|
uint32_t now = millis();
|
||||||
if (now - _lastReconnectAttempt >= _reconnectInterval) {
|
if (now - _lastReconnectAttempt >= _reconnectInterval) {
|
||||||
@@ -178,20 +188,23 @@ void MQTTManager::publishAllPoints(const TelemetryData& d, uint32_t sequence) {
|
|||||||
snprintf(timeStr, sizeof(timeStr), "2026-01-01 00:00:00");
|
snprintf(timeStr, sizeof(timeStr), "2026-01-01 00:00:00");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建 JSON (协议规范 §4.1)
|
// 构建 JSON (协议规范 V0.08)
|
||||||
JsonDocument doc;
|
JsonDocument doc;
|
||||||
doc["saleid"] = d.saleid;
|
|
||||||
doc["gateid"] = d.gateid;
|
doc["gateid"] = d.gateid;
|
||||||
doc["type"] = "report";
|
doc["type"] = "report";
|
||||||
doc["time"] = timeStr;
|
doc["time"] = timeStr;
|
||||||
doc["sequence"] = String(sequence);
|
doc["sequence"] = String(sequence);
|
||||||
doc["source"] = "da";
|
doc["source"] = "da";
|
||||||
|
|
||||||
|
// meter.id = gateid + 后缀 (如 10100001001 → 10100001001001)
|
||||||
|
String meterId = String(d.gateid) + "001";
|
||||||
|
|
||||||
JsonArray meter = doc["meter"].to<JsonArray>();
|
JsonArray meter = doc["meter"].to<JsonArray>();
|
||||||
JsonObject m = meter.add<JsonObject>();
|
JsonObject m = meter.add<JsonObject>();
|
||||||
m["id"] = d.meterId;
|
m["id"] = meterId;
|
||||||
m["status"] = "1";
|
m["status"] = "1";
|
||||||
m["name"] = d.meterName;
|
m["name"] = d.meterName;
|
||||||
|
m["type"] = d.deviceType; // 设备类型枚举
|
||||||
|
|
||||||
JsonObject v = m["values"].to<JsonObject>();
|
JsonObject v = m["values"].to<JsonObject>();
|
||||||
// 环境数据
|
// 环境数据
|
||||||
@@ -205,6 +218,8 @@ void MQTTManager::publishAllPoints(const TelemetryData& d, uint32_t sequence) {
|
|||||||
v["Resident"] = d.resident ? "1" : "0";
|
v["Resident"] = d.resident ? "1" : "0";
|
||||||
// 生命体征
|
// 生命体征
|
||||||
v["Body"] = String(d.body);
|
v["Body"] = String(d.body);
|
||||||
|
v["SleepState"] = String(d.sleepState);
|
||||||
|
v["SleepScore"] = String(d.sleepScore);
|
||||||
v["HeartRate"] = String(d.heartRate);
|
v["HeartRate"] = String(d.heartRate);
|
||||||
v["BreathRate"] = String(d.breathRate);
|
v["BreathRate"] = String(d.breathRate);
|
||||||
v["RadarValid"] = d.radarValid ? "1" : "0";
|
v["RadarValid"] = d.radarValid ? "1" : "0";
|
||||||
@@ -416,6 +431,47 @@ void MQTTManager::publishLocalAlarm(uint8_t alarmType, bool triggered, const cha
|
|||||||
_mqtt.publish(topic.c_str(), payload.c_str());
|
_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<JsonArray>();
|
||||||
|
JsonObject m = meter.add<JsonObject>();
|
||||||
|
m["id"] = meterId;
|
||||||
|
m["status"] = "1";
|
||||||
|
m["name"] = METER_NAME_DEFAULT;
|
||||||
|
|
||||||
|
JsonObject v = m["values"].to<JsonObject>();
|
||||||
|
v["Presence"] = presence ? "1" : "0";
|
||||||
|
v["RadarValid"] = radarValid ? "1" : "0";
|
||||||
|
|
||||||
|
String payload;
|
||||||
|
serializeJson(doc, payload);
|
||||||
|
|
||||||
|
_mqtt.publish("report/allpoints", payload.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 回调注册
|
// 回调注册
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
+121
-24
@@ -19,6 +19,15 @@ RadarManager::RadarManager()
|
|||||||
, _callback(nullptr)
|
, _callback(nullptr)
|
||||||
, _frameCount(0)
|
, _frameCount(0)
|
||||||
, _errorCount(0)
|
, _errorCount(0)
|
||||||
|
, _lastHeartBeat(0)
|
||||||
|
, _lastBreathBeat(0)
|
||||||
|
, _stablePresence(false)
|
||||||
|
, _presenceCnt(0)
|
||||||
|
, _lastRawPresence(false)
|
||||||
|
, _motionCnt(0)
|
||||||
|
, _lastRawMotion(false)
|
||||||
|
, _fallCnt(0)
|
||||||
|
, _lastRawFall(false)
|
||||||
{
|
{
|
||||||
memset(&_currentFrame, 0, sizeof(_currentFrame));
|
memset(&_currentFrame, 0, sizeof(_currentFrame));
|
||||||
memset(&_data, 0, sizeof(_data));
|
memset(&_data, 0, sizeof(_data));
|
||||||
@@ -43,6 +52,9 @@ bool RadarManager::begin() {
|
|||||||
_resetParser();
|
_resetParser();
|
||||||
memset(&_data, 0, sizeof(_data));
|
memset(&_data, 0, sizeof(_data));
|
||||||
_data.lastUpdate = millis();
|
_data.lastUpdate = millis();
|
||||||
|
_stablePresence = false;
|
||||||
|
_presenceCnt = 0;
|
||||||
|
_lastRawPresence = false;
|
||||||
|
|
||||||
// 不给雷达发任何命令,让它自己初始化并开始自动上报
|
// 不给雷达发任何命令,让它自己初始化并开始自动上报
|
||||||
// R60ABD1 上电后通常需要 15-30 秒自检,之后自动输出数据
|
// R60ABD1 上电后通常需要 15-30 秒自检,之后自动输出数据
|
||||||
@@ -68,6 +80,16 @@ void RadarManager::loop() {
|
|||||||
Serial.printf("[雷达] ✅ 开始接收数据 (首帧)\n");
|
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 秒还没收到任何数据,打印一次
|
// 超时检测:如果 10 秒还没收到任何数据,打印一次
|
||||||
static uint32_t noDataWarn = 0;
|
static uint32_t noDataWarn = 0;
|
||||||
if (_frameCount == 0 && millis() - _data.lastUpdate > 10000) {
|
if (_frameCount == 0 && millis() - _data.lastUpdate > 10000) {
|
||||||
@@ -222,6 +244,15 @@ void RadarManager::_processCompleteFrame() {
|
|||||||
case CTRL_HEART_RATE:
|
case CTRL_HEART_RATE:
|
||||||
_extractHeartRate(_currentFrame);
|
_extractHeartRate(_currentFrame);
|
||||||
break;
|
break;
|
||||||
|
case CTRL_FALL:
|
||||||
|
#if RADAR_TYPE == RADAR_TYPE_AFD1
|
||||||
|
_extractFall(_currentFrame); // AFD1 专用跌倒
|
||||||
|
#else
|
||||||
|
// ABD1 不会发 0x83 帧, 若收到可能是干扰, 直接丢弃
|
||||||
|
(void)_currentFrame;
|
||||||
|
#endif
|
||||||
|
break;
|
||||||
|
break;
|
||||||
case CTRL_SLEEP:
|
case CTRL_SLEEP:
|
||||||
_extractSleep(_currentFrame);
|
_extractSleep(_currentFrame);
|
||||||
break;
|
break;
|
||||||
@@ -250,47 +281,63 @@ void RadarManager::_processCompleteFrame() {
|
|||||||
|
|
||||||
void RadarManager::_extractPresence(const RadarFrame& frame) {
|
void RadarManager::_extractPresence(const RadarFrame& frame) {
|
||||||
uint8_t raw = frame.data_length > 0 ? frame.data[0] : 0;
|
uint8_t raw = frame.data_length > 0 ? frame.data[0] : 0;
|
||||||
|
bool rawPresence = false;
|
||||||
|
|
||||||
// cmd 0x00 (len=1): 有人状态 (1=有人 0=无人)
|
// cmd 0x00 (len=1): 有人状态 (1=有人 0=无人)
|
||||||
if (frame.command == 0x00 && frame.data_length >= 1) {
|
if (frame.command == 0x00 && frame.data_length >= 1) {
|
||||||
_data.presence = (raw == 1);
|
rawPresence = (raw == 1);
|
||||||
_data.radarValid = true;
|
_data.radarValid = true;
|
||||||
_data.lastUpdate = millis();
|
_data.lastUpdate = millis();
|
||||||
_notifyUpdate();
|
|
||||||
}
|
}
|
||||||
// cmd 0x01 (len=1): 运动状态/体动 (值越大越活跃)
|
// cmd 0x01 (len=1): 运动状态/体动 (值越大越活跃)
|
||||||
else if (frame.command == 0x01 && frame.data_length >= 1) {
|
else if (frame.command == 0x01 && frame.data_length >= 1) {
|
||||||
_data.body = raw; // 0=静止, 1+=有体动
|
_data.body = raw;
|
||||||
_data.motion = (raw >= 1); // >=1 有运动
|
bool rawMotion = (raw >= 1);
|
||||||
|
_filterMotion(rawMotion); // 3 帧防抖
|
||||||
_data.radarValid = true;
|
_data.radarValid = true;
|
||||||
_data.lastUpdate = millis();
|
_data.lastUpdate = millis();
|
||||||
_notifyUpdate();
|
_notifyUpdate();
|
||||||
|
return; // 运动帧不含有人信息, 跳过防抖
|
||||||
}
|
}
|
||||||
// cmd 0x03 (len=1): 有人状态备用 (非0=有人)
|
// cmd 0x03 (len=1): 有人状态备用 (非0=有人)
|
||||||
else if (frame.command == 0x03 && frame.data_length >= 1) {
|
else if (frame.command == 0x03 && frame.data_length >= 1) {
|
||||||
_data.presence = (raw != 0);
|
rawPresence = (raw != 0);
|
||||||
_data.radarValid = true;
|
_data.radarValid = true;
|
||||||
_data.lastUpdate = millis();
|
_data.lastUpdate = millis();
|
||||||
_notifyUpdate();
|
|
||||||
}
|
}
|
||||||
// cmd 0x04 (len=2): [0]=presence [1]=motion
|
// cmd 0x04 (len=2): [0]=presence [1]=motion
|
||||||
else if (frame.command == 0x04 && frame.data_length >= 2) {
|
else if (frame.command == 0x04 && frame.data_length >= 2) {
|
||||||
_data.presence = (frame.data[0] != 0);
|
rawPresence = (frame.data[0] != 0);
|
||||||
_data.motion = (frame.data[1] == 0);
|
bool rawMotion = (frame.data[1] == 0);
|
||||||
|
_filterMotion(rawMotion); // 3 帧防抖
|
||||||
_data.radarValid = true;
|
_data.radarValid = true;
|
||||||
_data.lastUpdate = millis();
|
_data.lastUpdate = millis();
|
||||||
_notifyUpdate();
|
|
||||||
}
|
}
|
||||||
// cmd 0x05 (len=6): 完整包
|
// cmd 0x05 (len=6): 完整包
|
||||||
else if (frame.command == 0x05 && frame.data_length >= 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);
|
bool rawMotion = (frame.data[1] == 0);
|
||||||
|
_filterMotion(rawMotion); // 3 帧防抖
|
||||||
_data.distance = ((uint16_t)frame.data[2] << 8) | frame.data[3];
|
_data.distance = ((uint16_t)frame.data[2] << 8) | frame.data[3];
|
||||||
_data.body = frame.data[4];
|
_data.body = frame.data[4];
|
||||||
_data.radarValid = true;
|
_data.radarValid = true;
|
||||||
_data.lastUpdate = millis();
|
_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) {
|
void RadarManager::_extractBreathing(const RadarFrame& frame) {
|
||||||
@@ -299,8 +346,9 @@ void RadarManager::_extractBreathing(const RadarFrame& frame) {
|
|||||||
if (rate != 0x80 && rate != 0x00 && rate <= 40) {
|
if (rate != 0x80 && rate != 0x00 && rate <= 40) {
|
||||||
_data.breathRate = rate;
|
_data.breathRate = rate;
|
||||||
_data.vitalValid = true;
|
_data.vitalValid = true;
|
||||||
_data.presence = true; // 有呼吸=有人
|
_data.presence = true;
|
||||||
_data.radarValid = true;
|
_data.radarValid = true;
|
||||||
|
_lastBreathBeat = millis();
|
||||||
}
|
}
|
||||||
_data.lastUpdate = millis();
|
_data.lastUpdate = millis();
|
||||||
_notifyUpdate();
|
_notifyUpdate();
|
||||||
@@ -313,14 +361,40 @@ void RadarManager::_extractHeartRate(const RadarFrame& frame) {
|
|||||||
if (rate != 0x80 && rate != 0x00 && rate >= 40) {
|
if (rate != 0x80 && rate != 0x00 && rate >= 40) {
|
||||||
_data.heartRate = rate;
|
_data.heartRate = rate;
|
||||||
_data.vitalValid = true;
|
_data.vitalValid = true;
|
||||||
_data.presence = true; // 有心跳=有人
|
_data.presence = true;
|
||||||
_data.radarValid = true;
|
_data.radarValid = true;
|
||||||
|
_lastHeartBeat = millis();
|
||||||
}
|
}
|
||||||
_data.lastUpdate = millis();
|
_data.lastUpdate = millis();
|
||||||
_notifyUpdate();
|
_notifyUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AFD1 专用: 跌倒检测 (控制字 0x83)
|
||||||
|
void RadarManager::_extractFall(const RadarFrame& frame) {
|
||||||
|
if (frame.data_length >= 1) {
|
||||||
|
bool rawFall = (frame.data[0] != 0);
|
||||||
|
_filterFall(rawFall); // 2 帧去抖, 防 AFD1 自检期误报
|
||||||
|
_data.radarValid = true;
|
||||||
|
_data.lastUpdate = millis();
|
||||||
|
_notifyUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跌倒状态防抖: 连续 2 帧一致才更新 _data.fall
|
||||||
|
// AFD1 自检期 fall 位可能是随机值, 单帧误报会被过滤
|
||||||
|
void RadarManager::_filterFall(bool rawFall) {
|
||||||
|
if (rawFall == _lastRawFall) {
|
||||||
|
if (_fallCnt < 2) _fallCnt++;
|
||||||
|
} else {
|
||||||
|
_fallCnt = 0;
|
||||||
|
_lastRawFall = rawFall;
|
||||||
|
}
|
||||||
|
if (_fallCnt >= 2 && rawFall != _data.fall) {
|
||||||
|
_data.fall = rawFall;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void RadarManager::_extractSleep(const RadarFrame& frame) {
|
void RadarManager::_extractSleep(const RadarFrame& frame) {
|
||||||
// 从睡眠数据提取: cmd=0x00/0x01 为入床离床状态
|
// 从睡眠数据提取: cmd=0x00/0x01 为入床离床状态
|
||||||
if (frame.data_length >= 1) {
|
if (frame.data_length >= 1) {
|
||||||
@@ -333,19 +407,42 @@ void RadarManager::_extractSleep(const RadarFrame& frame) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (frame.data_length >= 3) {
|
if (frame.data_length >= 3) {
|
||||||
// data[0]: 入床/离床
|
_data.sleepState = frame.data[1]; // 0=清醒 1=浅睡 2=深睡
|
||||||
// data[1]: 睡眠状态
|
_data.sleepScore = frame.data[2]; // 0~100
|
||||||
// data[2]: 评分
|
|
||||||
|
|
||||||
// 跌倒检测: 可能在睡眠异常标志中
|
|
||||||
if (frame.data_length >= 4) {
|
|
||||||
_data.fall = (frame.data[3] & 0x01) != 0;
|
|
||||||
_data.resident = (frame.data[3] & 0x02) != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
_data.lastUpdate = millis();
|
_data.lastUpdate = millis();
|
||||||
_notifyUpdate();
|
_notifyUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (frame.data_length >= 4) {
|
||||||
|
#if RADAR_TYPE == RADAR_TYPE_AFD1
|
||||||
|
// AFD1 协议: data[3] 低 2 位 = 跌倒/驻留
|
||||||
|
bool rawFallFromSleep = (frame.data[3] & 0x01) != 0;
|
||||||
|
_filterFall(rawFallFromSleep); // 走防抖, 0x83 和 0x84 帧任一报 fall=true 都计数
|
||||||
|
_data.resident = (frame.data[3] & 0x02) != 0;
|
||||||
|
_data.lastUpdate = millis();
|
||||||
|
_notifyUpdate();
|
||||||
|
#else
|
||||||
|
// ABD1 协议: data[3] bit0 是睡眠状态标志 (不是跌倒), bit1 才是驻留
|
||||||
|
// 之前误把 bit0 当 fall 触发误报, 现只取 resident
|
||||||
|
_data.resident = (frame.data[3] & 0x02) != 0;
|
||||||
|
_data.lastUpdate = millis();
|
||||||
|
_notifyUpdate();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 运动状态防抖: 连续 3 帧一致才更新 _data.motion
|
||||||
|
// 雷达体动信号容易因小幅抖动/呼吸胸腔起伏/电磁干扰在 0/1 之间跳变
|
||||||
|
void RadarManager::_filterMotion(bool rawMotion) {
|
||||||
|
if (rawMotion == _lastRawMotion) {
|
||||||
|
if (_motionCnt < 3) _motionCnt++;
|
||||||
|
} else {
|
||||||
|
_motionCnt = 0;
|
||||||
|
_lastRawMotion = rawMotion;
|
||||||
|
}
|
||||||
|
if (_motionCnt >= 3 && rawMotion != _data.motion) {
|
||||||
|
_data.motion = rawMotion;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void RadarManager::_extractWorkStatus(const RadarFrame& frame) {
|
void RadarManager::_extractWorkStatus(const RadarFrame& frame) {
|
||||||
|
|||||||
+540
-375
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user