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