fix:修补蓝牙配网bug
This commit is contained in:
@@ -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