diff --git a/firmware/esp32/overland-controller/overland-controller.ino b/firmware/esp32/overland-controller/overland-controller.ino index 3153b85..7452610 100644 --- a/firmware/esp32/overland-controller/overland-controller.ino +++ b/firmware/esp32/overland-controller/overland-controller.ino @@ -33,6 +33,7 @@ const char INDEX_HTML[] PROGMEM = R"rawliteral( .v{font-weight:650}.good{color:var(--good)}.warn{color:var(--warn)}.bad{color:var(--bad)}.muted{color:var(--muted)} .bar{height:10px;background:#0e141c;border-radius:999px;overflow:hidden;border:1px solid var(--line)}.fill{height:100%;background:linear-gradient(90deg,var(--good),#9be15d);width:0%} button{width:100%;border:0;border-radius:12px;padding:12px;background:var(--btn);color:var(--text);font-weight:700;font-size:15px} + input{width:100%;border:1px solid var(--line);border-radius:12px;padding:12px;background:#0f151d;color:var(--text);font-size:15px} button.on{background:#17462a;color:#dfffea}button.off{background:#46202a;color:#ffe2e8}.relayBtns{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:12px} .tempList,.alarmList{display:grid;gap:8px;margin-top:12px}.item{display:flex;justify-content:space-between;gap:8px;background:#0f151d;border:1px solid var(--line);border-radius:12px;padding:10px} @media(max-width:760px){.span6,.span4{grid-column:span 12}.big{font-size:36px}.wrap{padding:12px}} @@ -76,6 +77,23 @@ const char INDEX_HTML[] PROGMEM = R"rawliteral(
+
+
WiFi
+
AP stays available at 192.168.4.1. STA networks are tried in order.
+
+ + + + + + + + +
+
STA--
+
STA IP--
+
+
Alarms / System
@@ -136,9 +154,43 @@ function render(data){
${k.replaceAll("_"," ")}${alarms[k]?"ALARM":"OK"}
`).join(""); const s=data.system||{}; + const n=data.network||{}; + $("wifiSta").textContent=n.sta_connected ? (n.sta_ssid||"connected") : "Not connected"; + $("wifiIp").textContent=n.sta_ip||"--"; $("fw").textContent=(s.firmware_name||"--")+" "+(s.firmware_version||""); $("uptime").textContent=(s.uptime_seconds||0)+" sec"; } +async function loadWifiConfig(){ + try{ + const r=await fetch("/config/wifi",{cache:"no-store"}); + const d=await r.json(); + const nets=d.wifi?.networks||[]; + for(let i=0;i<3;i++){ + const n=nets[i]||{}; + const s=$("w"+(i+1)+"s"); + if(s) s.value=n.ssid||""; + } + }catch(e){} +} +async function saveWifi(){ + const networks=[]; + for(let i=1;i<=3;i++){ + const ssid=$("w"+i+"s").value.trim(); + const password=$("w"+i+"p").value; + if(ssid) networks.push({ssid,password}); + } + await fetch("/config/wifi",{ + method:"POST", + headers:{"Content-Type":"application/json"}, + body:JSON.stringify({networks}) + }); + await loadWifiConfig(); + await load(); +} +async function connectWifi(){ + await fetch("/wifi/connect",{method:"POST"}); + await load(); +} async function load(){ try{ const r=await fetch("/status",{cache:"no-store"}); @@ -149,6 +201,7 @@ async function load(){ } } load(); +loadWifiConfig(); setInterval(load,3000); @@ -161,20 +214,55 @@ HardwareSerial DashboardSerial(2); String uartLineBuffer; Preferences wifiPrefs; -String staSsid = ""; -String staPassword = ""; + +const int MAX_WIFI_NETWORKS = 3; +String staSsids[MAX_WIFI_NETWORKS]; +String staPasswords[MAX_WIFI_NETWORKS]; +int wifiNetworkCount = 0; +String activeStaSsid = ""; void loadWifiConfig() { wifiPrefs.begin("wifi", true); - staSsid = wifiPrefs.getString("ssid", ""); - staPassword = wifiPrefs.getString("password", ""); + + wifiNetworkCount = wifiPrefs.getInt("count", 0); + + if (wifiNetworkCount < 0) wifiNetworkCount = 0; + if (wifiNetworkCount > MAX_WIFI_NETWORKS) wifiNetworkCount = MAX_WIFI_NETWORKS; + + for (int i = 0; i < MAX_WIFI_NETWORKS; i++) { + staSsids[i] = wifiPrefs.getString(("ssid" + String(i)).c_str(), ""); + staPasswords[i] = wifiPrefs.getString(("pass" + String(i)).c_str(), ""); + } + + // Migration path from old single-network config + if (wifiNetworkCount == 0) { + String oldSsid = wifiPrefs.getString("ssid", ""); + String oldPassword = wifiPrefs.getString("password", ""); + + if (oldSsid.length() > 0) { + staSsids[0] = oldSsid; + staPasswords[0] = oldPassword; + wifiNetworkCount = 1; + } + } + wifiPrefs.end(); } void saveWifiConfig() { wifiPrefs.begin("wifi", false); - wifiPrefs.putString("ssid", staSsid); - wifiPrefs.putString("password", staPassword); + + wifiPrefs.putInt("count", wifiNetworkCount); + + for (int i = 0; i < MAX_WIFI_NETWORKS; i++) { + wifiPrefs.putString(("ssid" + String(i)).c_str(), staSsids[i]); + wifiPrefs.putString(("pass" + String(i)).c_str(), staPasswords[i]); + } + + // Keep legacy keys updated for easier debugging + wifiPrefs.putString("ssid", wifiNetworkCount > 0 ? staSsids[0] : ""); + wifiPrefs.putString("password", wifiNetworkCount > 0 ? staPasswords[0] : ""); + wifiPrefs.end(); } @@ -182,44 +270,67 @@ void clearWifiConfig() { wifiPrefs.begin("wifi", false); wifiPrefs.clear(); wifiPrefs.end(); - staSsid = ""; - staPassword = ""; + + wifiNetworkCount = 0; + activeStaSsid = ""; + + for (int i = 0; i < MAX_WIFI_NETWORKS; i++) { + staSsids[i] = ""; + staPasswords[i] = ""; + } } void connectStaWifi() { - if (staSsid.length() == 0) { + if (wifiNetworkCount <= 0) { Serial.println("STA WiFi not configured."); return; } - Serial.print("Connecting STA WiFi to: "); - Serial.println(staSsid); + WiFi.disconnect(false); + activeStaSsid = ""; - WiFi.begin(staSsid.c_str(), staPassword.c_str()); + for (int i = 0; i < wifiNetworkCount; i++) { + if (staSsids[i].length() == 0) { + continue; + } - unsigned long start = millis(); - while (WiFi.status() != WL_CONNECTED && millis() - start < 10000) { - delay(500); - Serial.print("."); + Serial.print("Connecting STA WiFi to: "); + Serial.println(staSsids[i]); + + WiFi.begin(staSsids[i].c_str(), staPasswords[i].c_str()); + + unsigned long start = millis(); + while (WiFi.status() != WL_CONNECTED && millis() - start < 10000) { + delay(500); + Serial.print("."); + } + + Serial.println(); + + if (WiFi.status() == WL_CONNECTED) { + activeStaSsid = staSsids[i]; + Serial.println("STA WiFi connected"); + Serial.print("STA SSID: "); + Serial.println(activeStaSsid); + Serial.print("STA IP: "); + Serial.println(WiFi.localIP()); + return; + } + + Serial.println("STA WiFi attempt failed."); } - Serial.println(); - - if (WiFi.status() == WL_CONNECTED) { - Serial.println("STA WiFi connected"); - Serial.print("STA IP: "); - Serial.println(WiFi.localIP()); - } else { - Serial.println("STA WiFi failed. AP remains available."); - } + Serial.println("All STA WiFi attempts failed. AP remains available."); } void printNetworkStatus() { Serial.println("Network:"); Serial.print(" AP IP: "); Serial.println(WiFi.softAPIP()); - Serial.print(" STA SSID: "); - Serial.println(staSsid.length() ? staSsid : "(not configured)"); + Serial.print(" Saved STA Networks: "); + Serial.println(wifiNetworkCount); + Serial.print(" Active STA SSID: "); + Serial.println(activeStaSsid.length() ? activeStaSsid : "(none)"); Serial.print(" STA Connected: "); Serial.println(WiFi.status() == WL_CONNECTED ? "true" : "false"); if (WiFi.status() == WL_CONNECTED) { @@ -323,6 +434,13 @@ void buildStatusDocument(JsonDocument& doc) { JsonObject network = doc.createNestedObject("network"); network["wifi_enabled"] = true; network["uart_connected"] = true; + network["ap_enabled"] = true; + network["ap_ip"] = WiFi.softAPIP().toString(); + network["sta_enabled"] = wifiNetworkCount > 0; + network["sta_connected"] = WiFi.status() == WL_CONNECTED; + network["sta_ssid"] = activeStaSsid; + network["sta_ip"] = WiFi.status() == WL_CONNECTED ? WiFi.localIP().toString() : ""; + network["saved_network_count"] = wifiNetworkCount; JsonObject alarmObj = doc.createNestedObject("alarms"); alarmObj["low_soc"] = alarms.lowSoc; @@ -613,12 +731,20 @@ void handleUartMessage(const String& line) { JsonObject wifi = response.createNestedObject("wifi"); wifi["ap_enabled"] = true; - wifi["sta_enabled"] = staSsid.length() > 0; - wifi["ssid"] = staSsid; - wifi["password_set"] = staPassword.length() > 0; + wifi["sta_enabled"] = wifiNetworkCount > 0; + wifi["network_count"] = wifiNetworkCount; + wifi["active_ssid"] = activeStaSsid; wifi["sta_connected"] = WiFi.status() == WL_CONNECTED; wifi["ap_ip"] = WiFi.softAPIP().toString(); + JsonArray networks = wifi.createNestedArray("networks"); + for (int i = 0; i < wifiNetworkCount; i++) { + JsonObject network = networks.createNestedObject(); + network["index"] = i + 1; + network["ssid"] = staSsids[i]; + network["password_set"] = staPasswords[i].length() > 0; + } + if (WiFi.status() == WL_CONNECTED) { wifi["sta_ip"] = WiFi.localIP().toString(); } else { @@ -631,12 +757,34 @@ void handleUartMessage(const String& line) { } if (strcmp(type, "config_wifi") == 0) { - if (doc["ssid"].is()) { - staSsid = doc["ssid"].as(); - } + JsonArray networks = doc["networks"].as(); - if (doc["password"].is()) { - staPassword = doc["password"].as(); + if (!networks.isNull()) { + wifiNetworkCount = 0; + + for (JsonObject network : networks) { + if (wifiNetworkCount >= MAX_WIFI_NETWORKS) break; + + String ssid = network["ssid"] | ""; + String password = network["password"] | ""; + ssid.trim(); + + if (ssid.length() == 0) continue; + + staSsids[wifiNetworkCount] = ssid; + staPasswords[wifiNetworkCount] = password; + wifiNetworkCount++; + } + } else { + if (doc["ssid"].is()) { + staSsids[0] = doc["ssid"].as(); + if (wifiNetworkCount < 1) wifiNetworkCount = 1; + } + + if (doc["password"].is()) { + staPasswords[0] = doc["password"].as(); + if (wifiNetworkCount < 1) wifiNetworkCount = 1; + } } saveWifiConfig(); @@ -644,8 +792,7 @@ void handleUartMessage(const String& line) { DynamicJsonDocument response(512); response["type"] = "wifi_config_response"; response["ok"] = true; - response["ssid"] = staSsid; - response["password_set"] = staPassword.length() > 0; + response["network_count"] = wifiNetworkCount; serializeJson(response, DashboardSerial); DashboardSerial.println(); @@ -810,15 +957,40 @@ void handleDebugSerial() { } if (command.startsWith("wifi ssid ")) { - staSsid = command.substring(10); - Serial.println("OK WiFi SSID updated"); + staSsids[0] = command.substring(10); + if (wifiNetworkCount < 1) wifiNetworkCount = 1; + Serial.println("OK WiFi SSID 1 updated"); Serial.println("Run: wifi save"); return; } if (command.startsWith("wifi pass ")) { - staPassword = command.substring(10); - Serial.println("OK WiFi password updated"); + staPasswords[0] = command.substring(10); + if (wifiNetworkCount < 1) wifiNetworkCount = 1; + Serial.println("OK WiFi password 1 updated"); + Serial.println("Run: wifi save"); + return; + } + + if (command.startsWith("wifi add ")) { + String rest = command.substring(9); + int split = rest.indexOf('|'); + + if (split < 0) { + Serial.println("Invalid command. Use: wifi add SSID|PASSWORD"); + return; + } + + if (wifiNetworkCount >= MAX_WIFI_NETWORKS) { + Serial.println("WiFi network list full"); + return; + } + + staSsids[wifiNetworkCount] = rest.substring(0, split); + staPasswords[wifiNetworkCount] = rest.substring(split + 1); + wifiNetworkCount++; + + Serial.println("OK WiFi network added"); Serial.println("Run: wifi save"); return; } @@ -1114,16 +1286,16 @@ void handleFactoryResetConfig() { void sendWifiConfigJson() { - DynamicJsonDocument doc(1024); + DynamicJsonDocument doc(2048); doc["type"] = "wifi_config_response"; doc["ok"] = true; JsonObject wifi = doc.createNestedObject("wifi"); wifi["ap_enabled"] = true; - wifi["sta_enabled"] = staSsid.length() > 0; - wifi["ssid"] = staSsid; - wifi["password_set"] = staPassword.length() > 0; + wifi["sta_enabled"] = wifiNetworkCount > 0; + wifi["network_count"] = wifiNetworkCount; + wifi["active_ssid"] = activeStaSsid; wifi["sta_connected"] = WiFi.status() == WL_CONNECTED; wifi["ap_ip"] = WiFi.softAPIP().toString(); @@ -1133,6 +1305,14 @@ void sendWifiConfigJson() { wifi["sta_ip"] = ""; } + JsonArray networks = wifi.createNestedArray("networks"); + for (int i = 0; i < wifiNetworkCount; i++) { + JsonObject network = networks.createNestedObject(); + network["index"] = i + 1; + network["ssid"] = staSsids[i]; + network["password_set"] = staPasswords[i].length() > 0; + } + String output; serializeJson(doc, output); server.send(200, "application/json", output); @@ -1143,7 +1323,7 @@ void handleGetWifiConfig() { } void handleUpdateWifiConfig() { - DynamicJsonDocument doc(1024); + DynamicJsonDocument doc(2048); DeserializationError error = deserializeJson(doc, server.arg("plain")); if (error) { @@ -1151,12 +1331,42 @@ void handleUpdateWifiConfig() { return; } - if (doc["ssid"].is()) { - staSsid = doc["ssid"].as(); - } + JsonArray networks = doc["networks"].as(); - if (doc["password"].is()) { - staPassword = doc["password"].as(); + if (!networks.isNull()) { + wifiNetworkCount = 0; + + for (JsonObject network : networks) { + if (wifiNetworkCount >= MAX_WIFI_NETWORKS) break; + + String ssid = network["ssid"] | ""; + String password = network["password"] | ""; + + ssid.trim(); + + if (ssid.length() == 0) { + continue; + } + + staSsids[wifiNetworkCount] = ssid; + staPasswords[wifiNetworkCount] = password; + wifiNetworkCount++; + } + + for (int i = wifiNetworkCount; i < MAX_WIFI_NETWORKS; i++) { + staSsids[i] = ""; + staPasswords[i] = ""; + } + } else { + if (doc["ssid"].is()) { + staSsids[0] = doc["ssid"].as(); + if (wifiNetworkCount < 1) wifiNetworkCount = 1; + } + + if (doc["password"].is()) { + staPasswords[0] = doc["password"].as(); + if (wifiNetworkCount < 1) wifiNetworkCount = 1; + } } saveWifiConfig();