fix:修补蓝牙配网bug

This commit is contained in:
2026-08-12 09:22:28 +08:00
parent 93112dcdfd
commit 08a055b611
32 changed files with 13266 additions and 327 deletions
+65 -27
View File
@@ -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
View File
@@ -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());
}
// ============================================================
// 回调注册
// ============================================================
+8 -9
View File
@@ -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
View File
@@ -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");