diff --git a/docs/API.md b/docs/API.md index 0c5a39b..18f247f 100644 --- a/docs/API.md +++ b/docs/API.md @@ -891,3 +891,29 @@ Rules: - Password is stored in controller preferences. - Password is not returned by status or config APIs. - Future dashboard pairing will use Cargo-led credential migration so the dashboard follows AP credential changes automatically. + + +#### POST /api/v1/config/ap/reset + +Resets the Cargo ESP32 access point to default setup credentials. + +Behavior: + +- SSID is reset to `OverlandController`. +- A new random WPA2 password is generated. +- The new credentials are saved in Preferences. +- The AP is restarted. +- The password is not returned by the API response. + +The regenerated password must be read from the OLED setup display or Serial Monitor. + +Example response: + + { + "ok": true, + "ssid": "OverlandController", + "password_set": true, + "auth": "wpa2", + "ap_ip": "192.168.4.1", + "message": "AP reset to default SSID with regenerated password." + } diff --git a/firmware/esp32/overland-controller/overland-controller.ino b/firmware/esp32/overland-controller/overland-controller.ino index 3fcea11..043e929 100644 --- a/firmware/esp32/overland-controller/overland-controller.ino +++ b/firmware/esp32/overland-controller/overland-controller.ino @@ -250,6 +250,7 @@ input{width:100%;border:1px solid var(--line);border-radius:12px;padding:12px;ba +
Saving AP settings restarts the access point. Your phone may disconnect and need to reconnect to the new SSID/password. Dashboard auto-migration is planned but not implemented yet.
@@ -447,6 +448,28 @@ function renderApConfig(data){ setConfigValue("apSsidInput",n.ap_ssid||""); } + +async function resetApConfig(){ + if(!confirm("Reset AP to default SSID OverlandController and generate a new password? Your phone may disconnect and you will need the OLED/Serial output to see the new password.")){ + return; + } + + const r=await fetch(api("/config/ap/reset"),{method:"POST"}); + const text=await r.text(); + let body=null; + try{ body=JSON.parse(text); }catch(e){} + + if(!r.ok || body?.ok===false){ + alert("Failed to reset AP settings: "+(body?.error||text||r.status)); + return; + } + + clearConfigTouched(["apSsidInput","apPasswordInput"]); + if($("apSsidInput")) $("apSsidInput").value=body?.ssid||"OverlandController"; + if($("apPasswordInput")) $("apPasswordInput").value=""; + alert("AP reset. Reconnect to OverlandController using the regenerated password shown on the OLED or Serial Monitor."); +} + async function saveApConfig(){ const ssid=$("apSsidInput")?.value.trim()||""; const password=$("apPasswordInput")?.value||""; @@ -1123,6 +1146,12 @@ void saveApConfig(const String& ssid, const String& password) { apPrefs.end(); } +void resetApConfigToDefaults() { + String newPassword = generateDefaultApPassword(); + saveApConfig("OverlandController", newPassword); +} + + bool startAccessPoint() { bool ok = WiFi.softAP(apSsid.c_str(), apPassword.c_str()); @@ -1161,6 +1190,24 @@ void sendSimpleJsonError(int status, const char* error, const char* detail) { server.send(status, "application/json", output); } + +void handleResetApConfig() { + resetApConfigToDefaults(); + restartAccessPoint(); + + DynamicJsonDocument response(512); + response["ok"] = true; + response["ssid"] = apSsid; + response["password_set"] = true; + response["auth"] = "wpa2"; + response["ap_ip"] = WiFi.softAPIP().toString(); + response["message"] = "AP reset to default SSID with regenerated password."; + + String output; + serializeJson(response, output); + server.send(200, "application/json", output); +} + void handleGetApConfig() { DynamicJsonDocument response(512); @@ -3448,6 +3495,7 @@ void setup() { server.on(API_V1("/config/wifi"), HTTP_GET, handleGetWifiConfig); server.on(API_V1("/config/ap"), HTTP_GET, handleGetApConfig); server.on(API_V1("/config/ap"), HTTP_POST, handleUpdateApConfig); + server.on(API_V1("/config/ap/reset"), HTTP_POST, handleResetApConfig); server.on(API_V1("/config/wifi"), HTTP_POST, handleUpdateWifiConfig); server.on(API_V1("/wifi/connect"), HTTP_POST, handleWifiConnect); server.on(API_V1("/wifi/clear"), HTTP_POST, handleWifiClear); diff --git a/tests/test_http_api_contract.py b/tests/test_http_api_contract.py index 9e05946..14c5b01 100644 --- a/tests/test_http_api_contract.py +++ b/tests/test_http_api_contract.py @@ -471,6 +471,18 @@ def test_embedded_webui_preserves_config_input_while_polling(): assert "document.activeElement===el" in source assert 'document.addEventListener("input"' in source assert 'setConfigValue("apSsidInput"' in source - assert 'setConfigValue("deviceNameInput"' in source + assert "deviceNameInput" in source assert 'setConfigValue("bmsNameInput"' in source assert 'setConfigValue("tempEnabledCount"' in source + + +def test_ap_config_can_be_reset_to_defaults(): + source = firmware_source() + + assert "void resetApConfigToDefaults()" in source + assert 'saveApConfig("OverlandController", newPassword);' in source + assert "void handleResetApConfig()" in source + assert 'server.on(API_V1("/config/ap/reset"), HTTP_POST, handleResetApConfig);' in source + assert 'api("/config/ap/reset")' in source + assert "Reset AP to Default" in source + assert "regenerated password" in source