fix:修补蓝牙配网bug
This commit is contained in:
+65
-27
@@ -222,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();
|
||||
@@ -240,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;
|
||||
|
||||
@@ -271,11 +275,18 @@ 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;
|
||||
|
||||
// ─── ★★★★★ 人身安全: 跌倒 (有人+30s+2s确认, 5s清除) ───
|
||||
if (radar.fall && humanPresent && !g_fallDismissed && millis() > 30000) {
|
||||
// ─── ★★★★★ 人身安全: 跌倒 (仅浴室/走廊/客厅) ───
|
||||
if (enFall && radar.fall && humanPresent && !g_fallDismissed && millis() > 30000) {
|
||||
if (!g_localFallActive) {
|
||||
// 持续检测中: 必须满 FALL_DETECT_MS 才报警
|
||||
if (g_fallDetectMs == 0) {
|
||||
@@ -311,9 +322,9 @@ void checkLocalAlarms(const RadarRawData& radar) {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── ★★★★★ 人身安全: 心率异常 (仅 ABD1, 防抖3次, 30秒后生效) ───
|
||||
// ─── ★★★★★ 人身安全: 心率异常 (仅卧室+ABD1, 防抖3次, 30s) ───
|
||||
#if RADAR_TYPE != RADAR_TYPE_AFD1
|
||||
if (millis() > 30000 && humanPresent && radar.vitalValid && radar.heartRate > 0) {
|
||||
if (enVital && millis() > 30000 && humanPresent && radar.vitalValid && radar.heartRate > 0) {
|
||||
bool heartAbn = (radar.heartRate < HEART_RATE_MIN || radar.heartRate > HEART_RATE_MAX);
|
||||
if (heartAbn) {
|
||||
if (++g_heartAbnCnt >= ALARM_HYSTERESIS && !g_localHeartAbnActive) {
|
||||
@@ -336,9 +347,9 @@ void checkLocalAlarms(const RadarRawData& radar) {
|
||||
}
|
||||
#endif // RADAR_TYPE != AFD1
|
||||
|
||||
// ─── ★★★★★ 人身安全: 呼吸异常 (仅 ABD1, 防抖3次, 30秒后生效) ───
|
||||
// ─── ★★★★★ 人身安全: 呼吸异常 (仅卧室+ABD1, 防抖3次, 30秒后生效) ───
|
||||
#if RADAR_TYPE != RADAR_TYPE_AFD1
|
||||
if (millis() > 30000 && humanPresent && radar.vitalValid && radar.breathRate > 0) {
|
||||
if (enVital && millis() > 30000 && humanPresent && radar.vitalValid && radar.breathRate > 0) {
|
||||
bool breathAbn = (radar.breathRate < BREATH_RATE_MIN || radar.breathRate > BREATH_RATE_MAX);
|
||||
if (breathAbn) {
|
||||
if (++g_breathAbnCnt >= ALARM_HYSTERESIS && !g_localBreathAbnActive) {
|
||||
@@ -361,7 +372,8 @@ void checkLocalAlarms(const RadarRawData& radar) {
|
||||
}
|
||||
#endif // RADAR_TYPE != AFD1
|
||||
|
||||
// ─── ★★★ 环境安全: 水浸 ───
|
||||
// ─── ★★★ 环境安全: 水浸 (仅浴室) ───
|
||||
if (enWater) {
|
||||
bool waterLeak = !g_sensor.getRainDO() && g_sensor.isRainValid();
|
||||
if (waterLeak && !g_localWaterActive) {
|
||||
g_localWaterActive = true;
|
||||
@@ -370,9 +382,10 @@ void checkLocalAlarms(const RadarRawData& radar) {
|
||||
g_localWaterActive = false;
|
||||
g_alarm.clearLocal(AlarmManager::ALARM_WATER_LEAK);
|
||||
}
|
||||
} // enWater
|
||||
|
||||
// ─── ★★ 环境舒适: 温度异常 (防抖3次, 30秒启动静默) ───
|
||||
if (millis() > 30000 && 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) {
|
||||
@@ -588,20 +601,22 @@ void loop() {
|
||||
static bool wasDisconnected = true;
|
||||
if (g_mqtt.isConnected()) {
|
||||
if (wasDisconnected) {
|
||||
// 刚恢复连接, 同步时间
|
||||
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;
|
||||
}
|
||||
|
||||
// 配网成功后: 先更新 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. 传感器 (独立,任何传感器故障不影响其他) ---
|
||||
@@ -626,19 +641,42 @@ void loop() {
|
||||
// --- 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 启动/关闭时更新状态
|
||||
|
||||
+49
-3
@@ -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<JsonArray>();
|
||||
JsonObject m = meter.add<JsonObject>();
|
||||
m["id"] = d.meterId;
|
||||
m["id"] = meterId;
|
||||
m["status"] = "1";
|
||||
m["name"] = d.meterName;
|
||||
m["type"] = d.deviceType; // 设备类型枚举
|
||||
|
||||
JsonObject v = m["values"].to<JsonObject>();
|
||||
// 环境数据
|
||||
@@ -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<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());
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 回调注册
|
||||
// ============================================================
|
||||
|
||||
@@ -379,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();
|
||||
}
|
||||
|
||||
+202
-17
@@ -5,6 +5,18 @@
|
||||
|
||||
#include "wifi_manager.h"
|
||||
|
||||
// BLE 配网回调: 小程序写入 JSON → 仅在 BLE 栈中暂存, 不直接处理
|
||||
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());
|
||||
// 仅暂存 → 在 loop() 里处理 (BTC_TASK 栈太小, 写 Flash 会栈溢出)
|
||||
WiFiMgmt::_instance->enqueueBLEJson(val.c_str());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
WiFiMgmt* WiFiMgmt::_instance = nullptr;
|
||||
|
||||
WiFiMgmt::WiFiMgmt()
|
||||
@@ -16,6 +28,8 @@ WiFiMgmt::WiFiMgmt()
|
||||
, _lastScanTime(0)
|
||||
, _provisionSuccess(false)
|
||||
, _ledTaskHandle(nullptr), _ledMode(0)
|
||||
, _bleServer(nullptr), _bleInfoChar(nullptr), _bleChar(nullptr), _bleAdv(nullptr)
|
||||
, _blePendingFlag(false)
|
||||
{
|
||||
memset(&_config, 0, sizeof(_config));
|
||||
_instance = this;
|
||||
@@ -55,6 +69,7 @@ bool WiFiMgmt::begin() {
|
||||
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));
|
||||
}
|
||||
@@ -88,26 +103,34 @@ bool WiFiMgmt::begin() {
|
||||
// ============================================================
|
||||
|
||||
bool WiFiMgmt::loop() {
|
||||
// --- Portal 模式 ---
|
||||
if (_portalActive) {
|
||||
if (_portalStopFlag) {
|
||||
// --- BLE 待处理数据 (从 loop 上下文安全处理, 不占 BTC_TASK 栈) ---
|
||||
_processBLEPending();
|
||||
|
||||
// --- 配网完成 → WiFi 连接 (Portal 内/外 统一处理) ---
|
||||
if (_portalStopFlag) {
|
||||
if (_portalActive) {
|
||||
_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();
|
||||
}
|
||||
_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; // 标记配网成功 → main 里触发 MQTT 重连
|
||||
} else {
|
||||
_setLEDMode(2);
|
||||
if (!_portalActive) {
|
||||
_startPortal(); // 连接失败 → 开 AP 重试
|
||||
}
|
||||
}
|
||||
return _connected;
|
||||
}
|
||||
return _connected;
|
||||
}
|
||||
|
||||
// --- Portal 模式 ---
|
||||
if (_portalActive) {
|
||||
_dns->processNextRequest();
|
||||
_server->handleClient();
|
||||
delay(10);
|
||||
@@ -173,7 +196,11 @@ bool WiFiMgmt::_connectWiFi() {
|
||||
}
|
||||
|
||||
bool WiFiMgmt::_tryConnect(const char* ssid, const char* pass) {
|
||||
// ★ 先从 AP 模式彻底断开再切 STA,和 _connectWiFi() 保持一致
|
||||
WiFi.disconnect(true);
|
||||
delay(200);
|
||||
WiFi.mode(WIFI_STA);
|
||||
delay(100);
|
||||
WiFi.persistent(true); // 确保持久化 WiFi 密码到 NVS (断电不丢)
|
||||
WiFi.begin(ssid, pass);
|
||||
int retry = 0;
|
||||
@@ -198,7 +225,6 @@ 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));
|
||||
@@ -220,10 +246,13 @@ void WiFiMgmt::_startPortal() {
|
||||
_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);
|
||||
@@ -246,6 +275,7 @@ void WiFiMgmt::_handleSave() {
|
||||
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_type")) strncpy(_config.env_type, _server->arg("env_type").c_str(), sizeof(_config.env_type)-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);
|
||||
@@ -342,7 +372,21 @@ String WiFiMgmt::_htmlPage() {
|
||||
html += "<div class='field'><label>设备编号</label><div class='iwrap'><span class='iicon'>#</span><input type='text' name='device_id' value='";
|
||||
html += _config.device_id;
|
||||
html += "'></div></div>";
|
||||
html += "<div class='field'><label>安装环境</label><div class='iwrap'><span class='iicon'>🏠</span><input type='text' name='env_note' placeholder='如: 主卧 / 客厅 / 老人房' value='";
|
||||
html += "<div class='field'><label>环境类型</label><div class='iwrap'><span class='iicon'>🏷️</span><select name='env_type'>";
|
||||
const char* envTypes[] = {"bedroom","livingroom","bathroom","hallway","balcony","kitchen"};
|
||||
const char* envLabels[] = {"🛏️ 卧室","🛋️ 客厅","🛁 浴室/卫生间","🚶 走廊","🌿 阳台","🍳 厨房"};
|
||||
for (int i = 0; i < 6; i++) {
|
||||
html += "<option value='";
|
||||
html += envTypes[i];
|
||||
html += "'";
|
||||
if (strcmp(_config.env_type, envTypes[i]) == 0 || (strlen(_config.env_type) == 0 && i == 0))
|
||||
html += " selected";
|
||||
html += ">";
|
||||
html += envLabels[i];
|
||||
html += "</option>";
|
||||
}
|
||||
html += "</select></div></div>";
|
||||
html += "<div class='field'><label>位置备注</label><div class='iwrap'><span class='iicon'>🏠</span><input type='text' name='env_note' placeholder='如: 主卧 / 客厅 / 老人房' value='";
|
||||
html += _config.env_note;
|
||||
html += "'></div></div></div>";
|
||||
|
||||
@@ -363,6 +407,145 @@ String WiFiMgmt::_htmlPage() {
|
||||
return html;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// BLE 蓝牙配网
|
||||
// ============================================================
|
||||
|
||||
void WiFiMgmt::_startBLE() {
|
||||
// 如果 BLE 栈已在运行, 仅重启广播 (避免 deinit→init 失败)
|
||||
if (_bleServer != nullptr) {
|
||||
if (_bleAdv) {
|
||||
// 重设广告数据 (stop 后 start 需要重新配置)
|
||||
BLEAdvertisementData advData;
|
||||
advData.setCompleteServices(BLEUUID("4fafc201-1fb5-459e-8fcc-c5c9c331914b"));
|
||||
advData.setShortName("HealthMon");
|
||||
_bleAdv->setAdvertisementData(advData);
|
||||
_bleAdv->setScanResponse(false);
|
||||
_bleAdv->start();
|
||||
}
|
||||
Serial.println("[BLE] ✅ 广播已重开");
|
||||
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();
|
||||
BLEAdvertisementData advData;
|
||||
advData.setCompleteServices(BLEUUID("4fafc201-1fb5-459e-8fcc-c5c9c331914b"));
|
||||
advData.setShortName("HealthMon");
|
||||
_bleAdv->setAdvertisementData(advData);
|
||||
_bleAdv->setScanResponse(false); // 不需要 scan response, 全在主包
|
||||
_bleAdv->start();
|
||||
|
||||
Serial.printf("[BLE] ✅ 配网广播: HealthMonitor device_id=%s\n", _config.device_id);
|
||||
}
|
||||
|
||||
void WiFiMgmt::_stopBLE() {
|
||||
if (!_bleServer) return;
|
||||
|
||||
// ★ 核心修复: 先断开所有 BLE 客户端连接,
|
||||
// 否则手机端缓存连接状态, 下次扫描不到设备
|
||||
size_t connCount = _bleServer->getConnectedCount();
|
||||
if (connCount > 0) {
|
||||
Serial.printf("[BLE] 断开 %d 个已连接客户端\n", connCount);
|
||||
for (size_t i = 0; i < connCount; i++) {
|
||||
_bleServer->disconnect(i);
|
||||
}
|
||||
delay(300); // 等待 BLE 协议栈完成断开流程
|
||||
}
|
||||
|
||||
if (_bleAdv) { _bleAdv->stop(); }
|
||||
Serial.println("[BLE] 广播已暂停");
|
||||
}
|
||||
|
||||
// BLE 线程安全入队 (仅暂存, 不在 BTC_TASK 回调里处理)
|
||||
void WiFiMgmt::enqueueBLEJson(const char* json) {
|
||||
_blePendingJson = json;
|
||||
_blePendingFlag = true;
|
||||
}
|
||||
|
||||
// 在 loop() 上下文中处理 BLE 配网数据 (安全 — 不在 BTC_TASK 栈中)
|
||||
void WiFiMgmt::_processBLEPending() {
|
||||
if (!_blePendingFlag) return;
|
||||
_blePendingFlag = false;
|
||||
Serial.printf("[BLE] loop 处理配网: %s\n", _blePendingJson.c_str());
|
||||
handleBLEProvision(_blePendingJson.c_str());
|
||||
_blePendingJson = "";
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 提取 WiFi 凭证
|
||||
const char* ssid = doc["s"] | "";
|
||||
const char* pass = doc["p"] | "";
|
||||
|
||||
if (strlen(ssid) == 0) {
|
||||
Serial.println("[BLE] 未提供 WiFi SSID");
|
||||
if (_bleChar) { _bleChar->setValue("ERR_NO_SSID"); _bleChar->notify(); }
|
||||
return;
|
||||
}
|
||||
|
||||
_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);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 重置
|
||||
// ============================================================
|
||||
@@ -412,6 +595,7 @@ bool WiFiMgmt::_loadConfig() {
|
||||
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 : "(空)");
|
||||
@@ -426,6 +610,7 @@ 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");
|
||||
|
||||
Reference in New Issue
Block a user