diff --git a/3.Software/wl_pro_robot/basic_web.h b/3.Software/wl_pro_robot/basic_web.h index c265a8a..b40d47c 100644 --- a/3.Software/wl_pro_robot/basic_web.h +++ b/3.Software/wl_pro_robot/basic_web.h @@ -280,6 +280,9 @@ const char basic_web[] PROGMEM = R"=====(
固件构建:"); + page += WIFI_CFG_BUILD; + page += F("
"); + 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( + "" + ""); + if (refresh) { + page += F(""); + } + page += F( + ""); + page += message; + page += F("
返回 WiFi 设置 " + "遥控页面
"); + 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(); + } +} diff --git a/3.Software/wl_pro_robot/wifi.h b/3.Software/wl_pro_robot/wifi_manager.h similarity index 77% rename from 3.Software/wl_pro_robot/wifi.h rename to 3.Software/wl_pro_robot/wifi_manager.h index a006f97..e062112 100644 --- a/3.Software/wl_pro_robot/wifi.h +++ b/3.Software/wl_pro_robot/wifi_manager.h @@ -1,16 +1,16 @@ // ----------------------------------------------------------------------------- // 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 @@ -23,6 +23,13 @@ #pragma once #include