Files
deepseek Bot 5d9e5f73b0 Add AP+STA WiFi manager and stabilize balancing
- Replace wifi.cpp/wifi.h with wifi_manager.cpp/wifi_manager.h: AP and STA
  run together, external WiFi credentials are stored in NVS and editable at
  /wifi, with optional static IP, 15s auto-reconnect, disconnect reason
  reporting, and build/version diagnostics
- Fix MPU6050 I2C port to I2C0 to match the board wiring
- Initialize distance_zeropoint at startup and on Robot Go! rising edge
- Clamp angle_zeropoint to +/-10 degrees to prevent balance-point drift
- Add DEBUG_DISABLE_YAW switch and periodic balance debug output
- Update WiFi setup instructions in README.md and README_CN.md
2026-10-25 19:15:17 +08:00

639 lines
23 KiB
C++
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// -----------------------------------------------------------------------------
// Copyright (c) 2024 Mu Shibo
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
// -----------------------------------------------------------------------------
#include "wifi_manager.h"
#include <Arduino.h>
#include <Esp.h>
#include <Preferences.h>
// ---------------------------------------------------------------------------
// 热点(AP)参数:作为外部 WiFi 连不上时的备用入口
// ---------------------------------------------------------------------------
const char *AP_SSID = "WLROBOT";
const char *AP_PSW = "12345678";
// 注意:192.168.4.x 是 ESP32 的常用热点网段,避免和多数家用路由器的
// 192.168.1.x 冲突。如果你的路由器本身就是 192.168.4.x,请改这里。
IPAddress AP_IP(192, 168, 4, 1);
IPAddress AP_GATEWAY(192, 168, 4, 1);
IPAddress AP_SUBNET(255, 255, 255, 0);
// 首次烧录、Flash 里还没有保存过配置时,沿用原代码里的外部 WiFi。
// 如果不希望自动连它,在 /wifi 页面清空 SSID 保存即可。
static const char *DEFAULT_STA_SSID = "MUJITECH";
static const char *DEFAULT_STA_PASS = "mujitech";
// ---------------------------------------------------------------------------
// 配置存储
// ---------------------------------------------------------------------------
struct WiFiConfig {
String ssid;
String password;
bool use_static = false;
String ip;
String gateway;
String subnet;
String dns;
};
static const char *NVS_NAMESPACE = "wifi_cfg";
// 编译时间:打开 http://192.168.4.1/wifi/version 可确认板子里跑的是不是这份代码
static const char *WIFI_CFG_BUILD = __DATE__ " " __TIME__;
static WiFiConfig wifi_cfg;
static bool wifi_cfg_active = false; // SSID 非空,表示需要连接外部 WiFi
static WebServer *wifi_server = nullptr;
static uint32_t restart_at_ms = 0;
static uint32_t last_retry_ms = 0;
static bool sta_was_connected = false;
static int last_disconnect_reason = -1; // 最近一次 STA 断开/连接失败的 reason
static const char *wifiReasonText(uint8_t reason) {
switch (reason) {
case 2: return "AUTH_EXPIRE";
case 3: return "AUTH_LEAVE";
case 4: return "ASSOC_EXPIRE";
case 5: return "ASSOC_TOOMANY";
case 6: return "NOT_AUTHED";
case 7: return "NOT_ASSOCED";
case 8: return "ASSOC_LEAVE";
case 9: return "ASSOC_NOT_AUTHED";
case 15: return "4WAY_HANDSHAKE_TIMEOUT(密码错误/加密不兼容)";
case 16: return "GROUP_KEY_UPDATE_TIMEOUT";
case 17: return "IE_IN_4WAY_DIFFERS";
case 18: return "GROUP_CIPHER_INVALID";
case 19: return "PAIRWISE_CIPHER_INVALID";
case 20: return "AKMP_INVALID";
case 23: return "802_1X_AUTH_FAILED(企业级加密)";
case 24: return "CIPHER_SUITE_REJECTED(加密方式不支持)";
case 200: return "BEACON_TIMEOUT";
case 201: return "NO_AP_FOUND(SSID/2.4GHz/信号)";
case 202: return "AUTH_FAIL(密码)";
case 203: return "ASSOC_FAIL";
case 204: return "HANDSHAKE_TIMEOUT(密码)";
case 205: return "CONNECTION_FAIL(路由器拒绝/信号)";
default: return "UNKNOWN";
}
}
static void onWiFiEvent(arduino_event_id_t event, arduino_event_info_t info) {
if (event == ARDUINO_EVENT_WIFI_STA_CONNECTED) {
Serial.println("[WiFi] STA associated, waiting for IP...");
} else if (event == ARDUINO_EVENT_WIFI_STA_GOT_IP) {
last_disconnect_reason = -1;
Serial.printf("[WiFi] STA got IP: %s\n", WiFi.localIP().toString().c_str());
} else if (event == ARDUINO_EVENT_WIFI_STA_DISCONNECTED) {
last_disconnect_reason = info.wifi_sta_disconnected.reason;
Serial.printf("[WiFi] STA disconnected/failed, reason=%d (%s)\n",
last_disconnect_reason,
wifiReasonText((uint8_t)last_disconnect_reason));
}
}
static bool loadConfig(WiFiConfig &cfg, bool &found) {
found = false;
Preferences prefs;
if (!prefs.begin(NVS_NAMESPACE, true)) {
return false;
}
if (!prefs.isKey("ssid")) {
prefs.end();
return false;
}
found = true;
cfg.ssid = prefs.getString("ssid", "");
cfg.password = prefs.getString("pass", "");
cfg.use_static = prefs.getBool("static", false);
cfg.ip = prefs.getString("ip", "");
cfg.gateway = prefs.getString("gw", "");
cfg.subnet = prefs.getString("sn", "");
cfg.dns = prefs.getString("dns", "");
prefs.end();
return true;
}
static bool saveConfig(const WiFiConfig &cfg) {
Preferences prefs;
if (!prefs.begin(NVS_NAMESPACE, false)) {
return false;
}
prefs.putString("ssid", cfg.ssid);
prefs.putString("pass", cfg.password);
prefs.putBool("static", cfg.use_static);
prefs.putString("ip", cfg.ip);
prefs.putString("gw", cfg.gateway);
prefs.putString("sn", cfg.subnet);
prefs.putString("dns", cfg.dns);
prefs.end();
return true;
}
static bool parseIP(const String &s, IPAddress &out) {
if (s.length() == 0 || s.length() > 15) {
return false;
}
return out.fromString(s.c_str());
}
static bool sameSubnet(const IPAddress &ip, const IPAddress &gateway, const IPAddress &mask) {
const uint32_t m = (uint32_t)mask;
return (((uint32_t)ip & m) == ((uint32_t)gateway & m));
}
// 连接外部 WiFi。WiFi.begin() 本身是非阻塞的,不会卡住 setup。
static void startSTA(void) {
if (!wifi_cfg_active) {
return;
}
if (wifi_cfg.use_static) {
IPAddress ip, gateway, subnet, dns;
if (parseIP(wifi_cfg.ip, ip) && parseIP(wifi_cfg.gateway, gateway) &&
parseIP(wifi_cfg.subnet, subnet)) {
if (!parseIP(wifi_cfg.dns, dns)) {
dns = gateway;
}
if (!WiFi.config(ip, gateway, subnet, dns)) {
Serial.println("[WiFi] Static IP config rejected, using DHCP");
}
} else {
Serial.println("[WiFi] Invalid static IP in Flash, using DHCP");
}
}
Serial.printf("[WiFi] Connecting to \"%s\" (%s)...\n", wifi_cfg.ssid.c_str(),
wifi_cfg.use_static ? "static IP" : "DHCP");
WiFi.begin(wifi_cfg.ssid.c_str(), wifi_cfg.password.c_str());
last_retry_ms = millis();
}
// ---------------------------------------------------------------------------
// 网页配置页面
// ---------------------------------------------------------------------------
static String htmlEscape(const String &in) {
String out;
out.reserve(in.length() + 16);
for (size_t i = 0; i < in.length(); i++) {
const char c = in[i];
switch (c) {
case '&': out += F("&amp;"); break;
case '<': out += F("&lt;"); break;
case '>': out += F("&gt;"); break;
case '"': out += F("&quot;"); break;
case '\'': out += F("&#39;"); break;
default: out += c; break;
}
}
return out;
}
static String buildConfigPage(void) {
String page;
page.reserve(5200);
const bool connected = (WiFi.status() == WL_CONNECTED);
const String ap_ip = WiFi.softAPIP().toString();
page += F(
"<!DOCTYPE html><html><head><meta charset=\"utf-8\">"
"<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">"
"<title>WL-PRO WiFi 设置</title><style>"
"body{font-family:system-ui,-apple-system,\"Segoe UI\",\"Microsoft YaHei\",Arial,sans-serif;"
"max-width:560px;margin:0 auto;padding:14px;background:#f4f6fb;color:#222}"
"h2{background:#6495ed;color:#fff;border-radius:12px;padding:12px;text-align:center;margin:0 0 12px}"
".card{background:#fff;border-radius:12px;padding:14px;margin:10px 0;"
"box-shadow:0 1px 4px rgba(0,0,0,.08)}"
"label{display:block;margin:9px 0 4px;font-size:14px}"
"input[type=text],input[type=password]{width:100%;box-sizing:border-box;height:38px;"
"padding:0 9px;border:1px solid #ccd3e0;border-radius:8px;font-size:15px}"
".row{display:flex;gap:10px}.row>div{flex:1}"
".note{font-size:12px;color:#666;line-height:1.6;margin:8px 0 0}"
".btn{display:block;width:100%;box-sizing:border-box;height:44px;line-height:44px;"
"text-align:center;border:0;border-radius:10px;background:#6495ed;color:#fff;"
"font-size:16px;margin-top:12px;text-decoration:none}"
".btn.sec{background:#e9edf6;color:#334;margin-top:8px}"
".ok{color:#137a37;font-weight:600}.bad{color:#b42318;font-weight:600}"
".status{font-size:14px;line-height:1.8}.status b{font-weight:600}"
"</style></head><body><h2>WiFi 设置</h2>");
// 当前状态
page += F("<div class=\"card status\">");
page += F("<div>热点:<b>");
page += AP_SSID;
page += F("</b> / <b>");
page += AP_PSW;
page += F("</b></div>");
page += F("<div>热点地址:<b>http://");
page += ap_ip;
page += F("</b></div>");
page += F("<div>STA MAC:<b>");
page += WiFi.macAddress();
page += F("</b></div>");
page += F("<div>外部 WiFi:<b>");
page += (wifi_cfg.ssid.length() > 0) ? htmlEscape(wifi_cfg.ssid)
: String("未配置(仅热点)");
page += F("</b></div>");
page += F("<div>连接状态:<span class=\"");
page += connected ? F("ok") : F("bad");
page += F("\">");
if (connected) {
page += F("已连接");
} else if (wifi_cfg_active) {
page += F("未连接(空闲时自动重试)");
} else {
page += F("仅热点");
}
page += F("</span></div>");
if (connected) {
page += F("<div>当前 IP:<b>");
page += WiFi.localIP().toString();
page += F("</b></div>");
page += F("<div>信号强度:");
page += String((int)WiFi.RSSI());
page += F(" dBm</div>");
}
if (wifi_cfg.ssid.length() > 0) {
page += F("<div>IP 方式:<b>");
page += wifi_cfg.use_static ? F("静态 IP") : F("DHCP");
page += F("</b></div>");
}
if (!connected && wifi_cfg_active && last_disconnect_reason >= 0) {
page += F("<div>最近一次失败:<b>reason ");
page += String(last_disconnect_reason);
page += F("</b>(");
page += wifiReasonText((uint8_t)last_disconnect_reason);
page += F(")</div>");
}
page += F("</div>");
// 表单
page += F(
"<form method=\"POST\" action=\"/wifi/save\">"
"<div class=\"card\"><label>外部 WiFi 名称(SSID)</label>"
"<input type=\"text\" name=\"ssid\" maxlength=\"32\" value=\"");
page += htmlEscape(wifi_cfg.ssid);
page += F(
"\" placeholder=\"留空 = 只用热点\">"
"<label>密码</label>"
"<input type=\"password\" name=\"pass\" maxlength=\"63\" "
"placeholder=\"留空 = 不修改已保存密码\">"
"<label style=\"font-weight:normal;margin-top:10px\">"
"<input type=\"checkbox\" name=\"clear_pass\" value=\"1\"> "
"清除密码(连接开放网络)</label>"
"<p class=\"note\">仅支持 2.4GHz、WPA2。密码留空会保留旧密码;"
"换 SSID 时请重新输入密码。</p></div>");
page += F(
"<div class=\"card\"><label style=\"font-weight:normal\">"
"<input type=\"checkbox\" name=\"use_static\" value=\"1\"");
if (wifi_cfg.use_static) {
page += F(" checked");
}
page += F("> 使用静态 IP(不勾选 = DHCP)</label>");
page += F(
"<div class=\"row\"><div><label>IP 地址</label>"
"<input type=\"text\" name=\"ip\" maxlength=\"15\" value=\"");
page += htmlEscape(wifi_cfg.ip);
page += F("\" placeholder=\"192.168.1.50\"></div></div>");
page += F(
"<div class=\"row\"><div><label>网关</label>"
"<input type=\"text\" name=\"gw\" maxlength=\"15\" value=\"");
page += htmlEscape(wifi_cfg.gateway);
page += F(
"\" placeholder=\"192.168.1.1\"></div><div><label>子网掩码</label>"
"<input type=\"text\" name=\"sn\" maxlength=\"15\" value=\"");
page += htmlEscape(wifi_cfg.subnet);
page += F("\" placeholder=\"255.255.255.0\"></div></div>");
page += F(
"<div class=\"row\"><div><label>DNS(留空 = 网关)</label>"
"<input type=\"text\" name=\"dns\" maxlength=\"15\" value=\"");
page += htmlEscape(wifi_cfg.dns);
page += F(
"\" placeholder=\"192.168.1.1\"></div></div>"
"<p class=\"note\">静态 IP 必须和路由器同网段,并避开 DHCP 地址池。"
"热点固定使用 192.168.4.1,请避免路由器也使用 192.168.4.x 网段。</p></div>");
page += F(
"<button class=\"btn\" type=\"submit\">保存并重启</button>"
"<a class=\"btn sec\" href=\"/\">返回遥控页面</a>"
"<p class=\"note\" style=\"text-align:center\">保存后机器人会重启,"
"请先扶稳或放在支架上。</p></form>");
page += F("<p class=\"note\" style=\"text-align:center\">固件构建:");
page += WIFI_CFG_BUILD;
page += F("</p></body></html>");
return page;
}
static void sendResult(int code, const String &title, const String &message,
bool refresh) {
if (wifi_server == nullptr) {
return;
}
String page;
page.reserve(900);
page += F(
"<!DOCTYPE html><html><head><meta charset=\"utf-8\">"
"<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">");
if (refresh) {
page += F("<meta http-equiv=\"refresh\" content=\"10;url=/wifi\">");
}
page += F(
"<style>body{font-family:system-ui,\"Microsoft YaHei\",Arial,sans-serif;"
"max-width:520px;margin:40px auto;padding:0 16px;text-align:center;line-height:1.7}"
"h2{color:#6495ed}a{color:#4169e1}</style></head><body><h2>");
page += title;
page += F("</h2><p>");
page += message;
page += F("</p><p><a href=\"/wifi\">返回 WiFi 设置</a> "
"<a href=\"/\">遥控页面</a></p></body></html>");
wifi_server->send(code, "text/html; charset=utf-8", page);
}
static void handleConfigPage(void) {
if (wifi_server == nullptr) {
return;
}
wifi_server->send(200, "text/html; charset=utf-8", buildConfigPage());
}
// 直接用浏览器打开 /wifi/save(GET)时给出提示,而不是只回一个 404
static void handleSaveGet(void) {
if (wifi_server == nullptr) {
return;
}
sendResult(405, "请求方式不对",
"请返回 WiFi 设置页面,填写后点击“保存并重启”按钮。", false);
}
static void handleVersion(void) {
if (wifi_server == nullptr) {
return;
}
String msg = F("wifi_manager build: ");
msg += WIFI_CFG_BUILD;
wifi_server->send(200, "text/plain; charset=utf-8", msg);
}
static void handleNotFound(void) {
if (wifi_server == nullptr) {
return;
}
const HTTPMethod m = wifi_server->method();
const char *name = (m == HTTP_GET) ? "GET"
: (m == HTTP_POST) ? "POST"
: "OTHER";
Serial.printf("[HTTP] 404 %s %s\n", name, wifi_server->uri().c_str());
String msg = F("Not found: ");
msg += wifi_server->uri();
msg += F(" (method ");
msg += name;
msg += F(")");
wifi_server->send(404, "text/plain; charset=utf-8", msg);
}
static void handleSave(void) {
if (wifi_server == nullptr) {
return;
}
Serial.printf("[WiFi] /wifi/save request, method=%d, args=%d\n",
(int)wifi_server->method(), wifi_server->args());
String ssid = wifi_server->arg("ssid");
ssid.trim();
if (ssid.length() > 32) {
sendResult(400, "保存失败", "SSID 不能超过 32 个字节。", false);
return;
}
const bool clear_pass = wifi_server->hasArg("clear_pass");
String pass = wifi_server->arg("pass");
if (clear_pass) {
pass = "";
} else if (pass.length() == 0) {
// 留空表示沿用 Flash 里的旧密码
pass = wifi_cfg.password;
}
if (pass.length() > 63) {
sendResult(400, "保存失败", "WiFi 密码不能超过 63 个字符。", false);
return;
}
if (ssid.length() > 0 && !clear_pass && pass.length() == 0 &&
ssid != wifi_cfg.ssid) {
sendResult(400, "保存失败",
"更换 SSID 时请重新输入密码;如果是开放网络,请勾选“清除密码”。",
false);
return;
}
if (ssid.length() > 0 && pass.length() > 0 && pass.length() < 8) {
sendResult(400, "保存失败",
"WPA2 密码至少 8 位;开放网络请勾选“清除密码”。", false);
return;
}
const bool use_static = wifi_server->hasArg("use_static");
String ip = wifi_server->arg("ip");
String gateway = wifi_server->arg("gw");
String subnet = wifi_server->arg("sn");
String dns = wifi_server->arg("dns");
ip.trim();
gateway.trim();
subnet.trim();
dns.trim();
IPAddress ip_addr, gateway_addr, subnet_addr, dns_addr;
if (use_static && ssid.length() > 0) {
if (!parseIP(ip, ip_addr) || (uint32_t)ip_addr == 0) {
sendResult(400, "保存失败", "静态 IP 地址无效。", false);
return;
}
if (!parseIP(gateway, gateway_addr) || (uint32_t)gateway_addr == 0) {
sendResult(400, "保存失败", "网关地址无效。", false);
return;
}
if (!parseIP(subnet, subnet_addr) || (uint32_t)subnet_addr == 0) {
sendResult(400, "保存失败", "子网掩码无效。", false);
return;
}
if (dns.length() == 0) {
dns = gateway;
dns_addr = gateway_addr;
} else if (!parseIP(dns, dns_addr) || (uint32_t)dns_addr == 0) {
sendResult(400, "保存失败", "DNS 地址无效。", false);
return;
}
if (!sameSubnet(ip_addr, gateway_addr, subnet_addr)) {
sendResult(400, "保存失败",
"静态 IP 和网关不在同一网段,请检查 IP/网关/子网掩码。", false);
return;
}
if (sameSubnet(ip_addr, AP_IP, AP_SUBNET)) {
sendResult(400, "保存失败",
"静态 IP 不能使用热点的 192.168.4.x 网段。", false);
return;
}
}
WiFiConfig cfg;
cfg.ssid = ssid;
cfg.password = pass;
cfg.use_static = use_static;
cfg.ip = ip;
cfg.gateway = gateway;
cfg.subnet = subnet;
cfg.dns = dns;
if (!saveConfig(cfg)) {
sendResult(500, "保存失败", "写入 Flash 失败,请重试。", false);
return;
}
// 让状态页立即反映新配置(随后会重启)
wifi_cfg = cfg;
wifi_cfg_active = (cfg.ssid.length() > 0);
String message;
if (cfg.ssid.length() == 0) {
message = "已保存。机器人将重启,之后只使用热点 WLROBOT。";
} else if (cfg.use_static) {
message = "已保存。机器人将在约 1 秒后重启,并尝试连接 \"";
message += htmlEscape(cfg.ssid);
message += "\",静态 IP 为 ";
message += cfg.ip;
message += "。";
} else {
message = "已保存。机器人将在约 1 秒后重启,并尝试连接 \"";
message += htmlEscape(cfg.ssid);
message += "\"(DHCP)。";
}
message += " 如果外部 WiFi 连不上,热点 WLROBOT 仍然可用"
"(http://192.168.4.1)。";
sendResult(200, "保存成功", message, true);
restart_at_ms = millis() + 1000;
}
void WiFi_RegisterWebHandlers(WebServer &server) {
wifi_server = &server;
server.on("/wifi", HTTP_GET, handleConfigPage);
server.on("/wifi/", HTTP_GET, handleConfigPage);
server.on("/wifi/version", HTTP_GET, handleVersion);
server.on("/wifi/save", HTTP_POST, handleSave);
server.on("/wifi/save", HTTP_GET, handleSaveGet);
server.onNotFound(handleNotFound);
}
// ---------------------------------------------------------------------------
// 初始化与主循环
// ---------------------------------------------------------------------------
void WiFi_Init(void) {
Serial.printf("[WiFi] wifi_manager build: %s\n", WIFI_CFG_BUILD);
WiFi.mode(WIFI_AP_STA);
WiFi.onEvent(onWiFiEvent);
WiFi.setHostname("wlrobot");
WiFi.setAutoReconnect(true);
WiFi.softAPConfig(AP_IP, AP_GATEWAY, AP_SUBNET);
if (!WiFi.softAP(AP_SSID, AP_PSW)) {
Serial.println("[WiFi] softAP start failed");
}
Serial.printf("\n[WiFi] AP \"%s\" / \"%s\" -> http://%s/\n", AP_SSID, AP_PSW,
WiFi.softAPIP().toString().c_str());
Serial.printf("[WiFi] Settings page: http://%s/wifi\n",
WiFi.softAPIP().toString().c_str());
Serial.printf("[WiFi] STA MAC: %s, AP MAC: %s\n", WiFi.macAddress().c_str(),
WiFi.softAPmacAddress().c_str());
bool found = false;
loadConfig(wifi_cfg, found);
if (!found) {
// 首次烧录:沿用原代码的默认外部 WiFi,之后可在 /wifi 页面修改
wifi_cfg.ssid = DEFAULT_STA_SSID;
wifi_cfg.password = DEFAULT_STA_PASS;
wifi_cfg.use_static = false;
wifi_cfg.ip = "";
wifi_cfg.gateway = "";
wifi_cfg.subnet = "";
wifi_cfg.dns = "";
Serial.println("[WiFi] No saved config, using default STA credentials");
}
wifi_cfg_active = (wifi_cfg.ssid.length() > 0);
if (wifi_cfg_active) {
startSTA();
} else {
// 清掉 WiFi 驱动里缓存的旧 AP,避免“仅热点”模式下又自动连上旧网络
WiFi.disconnect(false, true);
Serial.println("[WiFi] No external WiFi configured, AP only");
}
}
void WiFi_Loop(void) {
// 配置保存后延时重启,避免在 Web 回调里阻塞平衡控制
if (restart_at_ms != 0 && (int32_t)(millis() - restart_at_ms) >= 0) {
Serial.println("[WiFi] Rebooting to apply settings...");
delay(50);
ESP.restart();
}
const wl_status_t status = WiFi.status();
if (status == WL_CONNECTED) {
if (!sta_was_connected) {
sta_was_connected = true;
Serial.printf("[WiFi] STA connected: \"%s\", IP %s\n",
WiFi.SSID().c_str(), WiFi.localIP().toString().c_str());
Serial.printf("[WiFi] Remote UI: http://%s/ Settings: http://%s/wifi\n",
WiFi.localIP().toString().c_str(),
WiFi.localIP().toString().c_str());
}
return;
}
if (sta_was_connected) {
sta_was_connected = false;
Serial.println("[WiFi] STA disconnected; reconnecting in background "
"(AP still available)");
}
// 外部 WiFi 未连接时后台重试。即使手机正连着热点也照常重试,
// 否则用户连上 AP 查看状态时反而永远不会重连。
if (wifi_cfg_active && (millis() - last_retry_ms > 15000)) {
last_retry_ms = millis();
Serial.printf("[WiFi] retrying \"%s\"...\n", wifi_cfg.ssid.c_str());
WiFi.reconnect();
}
}