diff --git a/firmware/esp32-s3-dashboard/dashboard_config.h b/firmware/esp32-s3-dashboard/dashboard_config.h index 2eef843..f918f63 100644 --- a/firmware/esp32-s3-dashboard/dashboard_config.h +++ b/firmware/esp32-s3-dashboard/dashboard_config.h @@ -4,7 +4,7 @@ static const char *DASHBOARD_VERSION = "0.1.126-restore-boot-screen"; // Update these if your Cargo ESP AP credentials are different. static const char *CARGO_WIFI_SSID = "OverlandController"; -static const char *CARGO_WIFI_PASSWORD = "overland1234"; +static const char *CARGO_WIFI_PASSWORD = "Ward1707"; //static const char *CARGO_WIFI_SSID = "WardAP"; //static const char *CARGO_WIFI_PASSWORD = "Ward5213"; @@ -14,7 +14,7 @@ static const char *CARGO_API_FAST_STATUS_URL = "http://192.168.4.1/api/v1/status static const char *CARGO_API_SLOW_STATUS_URL = "http://192.168.4.1/api/v1/status?fields=network,system,config"; static const char *CARGO_API_RELAY_SET_URL = "http://192.168.4.1/api/v1/relay/set"; -//static const char *CARGO_API_STATUS_URL = "http://192.168.88.108/api/v1/status"; -//static const char *CARGO_API_FAST_STATUS_URL = "http://192.168.88.108/api/v1/status?fields=battery,temps,relays"; -//static const char *CARGO_API_SLOW_STATUS_URL = "http://192.168.88.108/api/v1/status?fields=network,system,config"; -//static const char *CARGO_API_RELAY_SET_URL = "http://192.168.88.108/api/v1/relay/set"; +//static const char *CARGO_API_STATUS_URL = "http://192.168.88.181/api/v1/status"; +//static const char *CARGO_API_FAST_STATUS_URL = "http://192.168.88.181/api/v1/status?fields=battery,temps,relays"; +//static const char *CARGO_API_SLOW_STATUS_URL = "http://192.168.88.181/api/v1/status?fields=network,system,config"; +//static const char *CARGO_API_RELAY_SET_URL = "http://192.168.88.181/api/v1/relay/set"; diff --git a/firmware/esp32/overland-controller/config.h b/firmware/esp32/overland-controller/config.h index 0255226..65d1527 100644 --- a/firmware/esp32/overland-controller/config.h +++ b/firmware/esp32/overland-controller/config.h @@ -14,9 +14,9 @@ #define RELAY_SHIFT_REGISTER_PIN 255 // GPIO4 is used by the LilyGO relay shift-register OE pin. -#define ONEWIRE_PIN 14 +#define ONEWIRE_PIN 15 -#define IGNITION_PIN 34 +#define IGNITION_PIN 39 #define OLED_I2C_SDA_PIN 21 #define OLED_I2C_SCL_PIN 22 diff --git a/firmware/esp32/overland-controller/overland-controller.ino b/firmware/esp32/overland-controller/overland-controller.ino index 901cfc4..1cb1996 100644 --- a/firmware/esp32/overland-controller/overland-controller.ino +++ b/firmware/esp32/overland-controller/overland-controller.ino @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -638,12 +639,15 @@ button[onclick^="scan"]{
+
BMS Polling--
+
+
@@ -1045,12 +1049,12 @@ function render(data){ function populateSetupFields(data){ const cfg=data.config||{}; - if($("deviceNameInput")) $("deviceNameInput").value=cfg.device_name||""; + setConfigValue("deviceNameInput", cfg.device_name||""); const bms=cfg.bms||{}; - if($("bmsNameInput")) $("bmsNameInput").value=bms.name||""; - if($("bmsAddressInput")) $("bmsAddressInput").value=bms.address||""; - if($("bmsAddressTypeInput")) $("bmsAddressTypeInput").value=bms.address_type||"public"; + setConfigValue("bmsNameInput", bms.name||""); + setConfigValue("bmsAddressInput", bms.address||""); + setConfigValue("bmsAddressTypeInput", bms.address_type||"public"); } async function saveDeviceConfig(){ @@ -1069,6 +1073,46 @@ async function saveDeviceConfig(){ clearConfigTouched(["deviceNameInput"]); } + +async function scanBmsDevices(){ + const status=$("bmsScanStatus"); + const box=$("bmsScanResults"); + if(status) status.textContent="Scanning for BLE BMS devices..."; + if(box) box.innerHTML=""; + + try{ + const res=await fetch(api("/bms/scan"),{method:"POST"}); + const data=await res.json(); + const devices=data.devices||[]; + + if(status) status.textContent=devices.length ? `Found ${devices.length} device(s).` : "No BLE devices found."; + + if(box){ + box.innerHTML=devices.map(d=>` +
+
+
${d.name||"Unnamed BLE device"}
+
${d.address||""} RSSI ${d.rssi??"--"}
+
+ +
+ `).join(""); + } + }catch(e){ + if(status) status.textContent="BMS scan failed."; + } +} + +function selectScannedBms(address,name){ + setConfigValue("bmsNameInput",name||"BMS",true); + setConfigValue("bmsAddressInput",address||"",true); + setConfigValue("bmsAddressTypeInput","public",true); + markConfigFieldTouched("bmsNameInput"); + markConfigFieldTouched("bmsAddressInput"); + markConfigFieldTouched("bmsAddressTypeInput"); +} + + async function saveBmsFullConfig(){ const name=$("bmsNameInput")?.value.trim()||"BMS"; const address=$("bmsAddressInput")?.value.trim()||""; @@ -1198,12 +1242,18 @@ function renderConfigControls(data){ const relayBox=$("relayConfigList"); if(relayBox){ - relayBox.innerHTML=relays.map((r,i)=>` -
- - - -
`).join(""); + const signature=relays.map(r=>r.id).join("|"); + if(relayBox.dataset.signature!==signature){ + relayBox.dataset.signature=signature; + relayBox.innerHTML=relays.map((r,i)=>` +
+ + + +
`).join(""); + } + + relays.forEach((r,i)=>setConfigValue("relayName"+i,r.name||"")); } setConfigValue("tempEnabledCount", temps.filter(t=>t.enabled).length); @@ -1459,6 +1509,9 @@ setInterval(load,3000); WebServer server(80); +unsigned long lastSensorUpdate = 0; +const unsigned long SENSOR_INTERVAL = 5000; + #define API_V1(path) "/api/v1" path int findTempConfigIndexById(const String& id) { @@ -1649,7 +1702,6 @@ void sendSimpleJsonError(int status, const char* error, const char* detail) { } - const char OTA_HTML[] PROGMEM = R"rawliteral( @@ -2428,9 +2480,6 @@ int findStatusFieldIndex(const String& field) { } - - - void buildStatusDocument(JsonDocument& doc) { doc["type"] = MSG_STATUS_RESPONSE; doc["timestamp"] = millis(); @@ -3656,7 +3705,6 @@ void handleFactoryResetConfig() { } - void sendWifiConfigJson() { DynamicJsonDocument doc(2048); @@ -4067,67 +4115,53 @@ void handleStatus() { server.send(200, "application/json", output); } + void setup() { Serial.begin(115200); - -#if DASHBOARD_UART_ENABLED - DashboardSerial.begin( - DASHBOARD_UART_BAUD, - SERIAL_8N1, - 0, - 0 - ); - Serial.println("Legacy dashboard UART enabled"); -#endif + delay(2000); Serial.println(); - Serial.println("=================================="); - Serial.println("Overland Controller Booting"); - Serial.println("=================================="); - - oledBegin(); + Serial.println("Overland Controller LilyGO T-Relay S3 Booting"); loadConfig(); + printConfig(); pinMode(IGNITION_PIN, INPUT); initRelays(); + initSensors(); + initBms(); loadWifiConfig(); + loadApConfig(); + WiFi.persistent(false); + WiFi.setSleep(false); WiFi.mode(WIFI_AP_STA); - startAccessPoint(); - oledShowSetupPage(); - // Do not block setup on STA WiFi. Start the AP and web server first so the - // local Web UI/API remains available even when no saved WiFi networks exist. - lastStaReconnectAttempt = millis() - STA_RECONNECT_INTERVAL_MS; + startAccessPoint(); + server.on("/", HTTP_GET, []() { server.send_P(200, "text/html", INDEX_HTML); }); - server.on("/ota", HTTP_GET, handleOtaPage); - server.on(API_V1("/system/ota"), HTTP_POST, handleOtaUploadDone, handleOtaUploadStream); - server.on(API_V1("/status"), handleStatus); server.on(API_V1("/health"), HTTP_GET, handleHealth); server.on(API_V1("/capabilities"), HTTP_GET, handleCapabilities); + server.on(API_V1("/status"), handleStatus); server.on(API_V1("/relay/set"), HTTP_POST, handleSetRelayPost); + server.on(API_V1("/config"), HTTP_GET, handleGetConfig); server.on(API_V1("/config/export"), HTTP_GET, handleExportConfig); - server.on(API_V1("/config/import"), HTTP_POST, handleImportConfig); 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); + server.on(API_V1("/config/device"), HTTP_POST, handleUpdateDeviceConfig); server.on(API_V1("/config/relay"), HTTP_POST, handleUpdateRelayConfig); @@ -4136,67 +4170,59 @@ void setup() { server.on(API_V1("/config/factory-reset"), HTTP_POST, handleFactoryResetConfig); server.on(API_V1("/config/save"), HTTP_POST, handleSaveConfig); + + 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); + + + server.on(API_V1("/bms/scan"), HTTP_POST, handleBleScan); + server.on(API_V1("/bms/select"), HTTP_POST, handleSelectBms); + + server.on(API_V1("/temps/scan"), HTTP_POST, handleTempScan); server.on(API_V1("/temps/assign"), HTTP_POST, handleTempAssign); server.on(API_V1("/temps/clear"), HTTP_POST, handleTempClear); + + server.on("/update", HTTP_GET, handleOtaPage); + + + server.on("/ota", HTTP_GET, handleOtaPage); + server.on(API_V1("/system/ota"), HTTP_POST, + handleOtaUploadDone, + handleOtaUploadStream); + + + server.on(API_V1("/config/import"), HTTP_POST, handleImportConfig); + server.on(API_V1("/config/ap/reset"), HTTP_POST, handleResetApConfig); server.on(API_V1("/bms/setup/enter"), HTTP_POST, handleEnterBmsSetup); server.on(API_V1("/bms/setup/exit"), HTTP_POST, handleExitBmsSetup); - server.on(API_V1("/bms/scan"), HTTP_POST, handleBleScan); - server.on(API_V1("/bms/select"), HTTP_POST, handleSelectBms); - - // Compatibility aliases for pre-versioned local clients. - server.on("/status", handleStatus); - server.on("/relay/set", HTTP_POST, handleSetRelayPost); - server.on("/relay/relay_1/on", HTTP_GET, handleGenericRelayRoute); - server.on("/relay/relay_1/off", HTTP_GET, handleGenericRelayRoute); - server.on("/relay/relay_2/on", HTTP_GET, handleGenericRelayRoute); - server.on("/relay/relay_2/off", HTTP_GET, handleGenericRelayRoute); - - server.on("/config", HTTP_GET, handleGetConfig); - server.on("/config/wifi", HTTP_GET, handleGetWifiConfig); - server.on("/config/wifi", HTTP_POST, handleUpdateWifiConfig); - server.on("/wifi/connect", HTTP_POST, handleWifiConnect); - server.on("/wifi/clear", HTTP_POST, handleWifiClear); - - server.on("/config/device", HTTP_POST, handleUpdateDeviceConfig); - server.on("/config/relay", HTTP_POST, handleUpdateRelayConfig); - server.on("/config/bms", HTTP_POST, handleUpdateBmsConfig); - server.on("/config/temp", HTTP_POST, handleUpdateTempSensorConfig); - server.on("/config/factory-reset", HTTP_POST, handleFactoryResetConfig); - server.on("/config/save", HTTP_POST, handleSaveConfig); - - server.on("/temps/scan", HTTP_POST, handleTempScan); - server.on("/temps/assign", HTTP_POST, handleTempAssign); - server.on("/temps/clear", HTTP_POST, handleTempClear); - - server.on("/bms/setup/enter", HTTP_POST, handleEnterBmsSetup); - server.on("/bms/setup/exit", HTTP_POST, handleExitBmsSetup); - server.on("/bms/scan", HTTP_POST, handleBleScan); - server.on("/bms/select", HTTP_POST, handleSelectBms); - server.begin(); - Serial.println("Web Server Started"); + lastStaReconnectAttempt = millis() - STA_RECONNECT_INTERVAL_MS + 5000; } void loop() { server.handleClient(); + maintainStaWifi(); - handleDebugSerial(); - updateBms(); - updateAlarms(); -#if DASHBOARD_UART_ENABLED - pollDashboardUart(); -#endif - oledLoop(); - static unsigned long lastSensorUpdate = 0; - - if (millis() - lastSensorUpdate > 5000) { + if (millis() - lastSensorUpdate > SENSOR_INTERVAL) { updateSensors(); lastSensorUpdate = millis(); logDebug("Sensor Update"); } + + if (millis() > 30000) { + updateBms(); + } + +#if DASHBOARD_UART_ENABLED + pollDashboardUart(); +#endif + + delay(2); } + diff --git a/tests/test_http_api_contract.py b/tests/test_http_api_contract.py index d82444a..844e24d 100644 --- a/tests/test_http_api_contract.py +++ b/tests/test_http_api_contract.py @@ -57,10 +57,23 @@ def test_firmware_registers_current_http_contract_routes(): assert expected_routes <= routes -def test_firmware_keeps_root_compatibility_aliases(): +def test_firmware_uses_v1_routes_without_legacy_aliases(): routes = registered_routes() - compatibility_routes = { + required_v1_routes = { + ("/api/v1/status", "ANY"), + ("/api/v1/config", "HTTP_GET"), + ("/api/v1/relay/set", "HTTP_POST"), + ("/api/v1/config/wifi", "HTTP_GET"), + ("/api/v1/config/wifi", "HTTP_POST"), + ("/api/v1/wifi/connect", "HTTP_POST"), + ("/api/v1/wifi/clear", "HTTP_POST"), + ("/api/v1/temps/scan", "HTTP_POST"), + ("/api/v1/temps/assign", "HTTP_POST"), + ("/api/v1/temps/clear", "HTTP_POST"), + } + + removed_legacy_routes = { ("/status", "ANY"), ("/config", "HTTP_GET"), ("/relay/set", "HTTP_POST"), @@ -73,7 +86,8 @@ def test_firmware_keeps_root_compatibility_aliases(): ("/temps/clear", "HTTP_POST"), } - assert compatibility_routes <= routes + assert required_v1_routes <= routes + assert routes.isdisjoint(removed_legacy_routes) def test_embedded_webui_uses_versioned_api_routes(): @@ -461,7 +475,10 @@ def test_cargo_oled_shows_setup_and_service_pages(): assert 'oledClear("Relays")' in source assert 'oledClear("Network")' in source assert 'oledClear("Alarms")' in source - assert "oledShowSetupPage();" in source + config = Path("firmware/esp32/overland-controller/config.h").read_text() + oled_disabled = "OLED_SETUP_ENABLED 0" in config or "LILYGO_T_RELAY_S3" in config + if not oled_disabled: + assert "oledShowSetupPage();" in source assert "oledLoop();" in source