8 changed files with 485 additions and 96 deletions
+1 -1
View File
@@ -396,7 +396,7 @@ Fields:
|---|---:|---|
| `id` | Yes | `relay_1` or `relay_2` |
| `name` | No | User display name |
| `enabled` | No | Configured enabled state |
| `enabled` | No | Configured enabled state. When set to `false`, the relay output is forced OFF and future relay control requests are rejected with `relay_disabled`. |
Returns the updated config.
+2
View File
@@ -69,6 +69,8 @@ HTTP endpoint:
POST /api/v1/config/relay
Disabled relays are forced OFF immediately, hidden from normal dashboard controls, and protected from `/api/v1/relay/set` commands until re-enabled.
## Temperature Sensor Configuration
Temperature sensors use generic firmware IDs:
+11
View File
@@ -319,3 +319,14 @@ The diagnostics overlay shows dashboard version, uptime, heap, WiFi RSSI/IP, Car
- Sends standard 11-bit functional request ID `0x7DF` and accepts ECU responses from `0x7E8` through `0x7EF`.
- Diagnostics page shows TWAI state, coolant value, TX/RX counts, and last OBD status.
- Overview coolant gauge shows `--°` until a valid OBD response is received.
## Dashboard first-boot WiFi setup
The ESP32-S3 dashboard no longer hardcodes Cargo ESP WiFi credentials or the Cargo API base URL. On first boot, it shows a touchscreen setup page where the user enters:
- Cargo ESP WiFi SSID
- Cargo ESP WiFi password
- Cargo API base URL, for example `http://192.168.4.1`
The dashboard saves these values in ESP32 Preferences/NVS and builds all `/api/v1` endpoint URLs at runtime. The boot screen also provides a Setup button for changing saved settings later.
+5 -17
View File
@@ -1,20 +1,8 @@
#pragma once
static const char *DASHBOARD_VERSION = "0.1.126-restore-boot-screen";
static const char *DASHBOARD_VERSION = "0.1.127-first-boot-wifi";
// Update these if your Cargo ESP AP credentials are different.
static const char *CARGO_WIFI_SSID = "OverlandController";
static const char *CARGO_WIFI_PASSWORD = "Ward1707";
//static const char *CARGO_WIFI_SSID = "WardAP";
//static const char *CARGO_WIFI_PASSWORD = "Ward5213";
static const char *CARGO_API_STATUS_URL = "http://192.168.4.1/api/v1/status";
static const char *CARGO_API_FAST_STATUS_URL = "http://192.168.4.1/api/v1/status?fields=battery,temps,relays";
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.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";
// First-boot defaults. Credentials are stored in Preferences/NVS after setup.
static const char *DASHBOARD_DEFAULT_CARGO_WIFI_SSID = "OverlandController";
static const char *DASHBOARD_DEFAULT_CARGO_WIFI_PASSWORD = "";
static const char *DASHBOARD_DEFAULT_CARGO_API_BASE_URL = "http://192.168.4.1";
@@ -1,5 +1,6 @@
#include <Arduino.h>
#include <WiFi.h>
#include <Preferences.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <esp_display_panel.hpp>
@@ -16,6 +17,23 @@ using namespace esp_panel::drivers;
using namespace esp_panel::board;
static DashboardStatus status_model;
static Preferences dashboard_preferences;
static String cargo_wifi_ssid;
static String cargo_wifi_password;
static String cargo_api_base_url;
static String cargo_api_status_url;
static String cargo_api_fast_status_url;
static String cargo_api_slow_status_url;
static String cargo_api_relay_set_url;
static bool dashboard_configured = false;
static lv_obj_t *setup_ssid_textarea = nullptr;
static lv_obj_t *setup_password_textarea = nullptr;
static lv_obj_t *setup_api_textarea = nullptr;
static lv_obj_t *setup_status_label = nullptr;
static lv_obj_t *setup_keyboard = nullptr;
static constexpr gpio_num_t OBD_CAN_TX_PIN = GPIO_NUM_15;
static constexpr gpio_num_t OBD_CAN_RX_PIN = GPIO_NUM_16;
static constexpr uint32_t OBD_REQUEST_ID = 0x7DF;
@@ -733,6 +751,7 @@ static void reset_battery_detail_widget_cache()
static void update_battery_detail_widgets();
static void show_boot_screen();
static void show_dashboard_setup_screen();
static void show_diagnostics_overlay();
static void close_diagnostics_overlay();
static void update_diagnostics_overlay();
@@ -1142,6 +1161,263 @@ static void update_relay_buttons()
lvgl_port_unlock();
}
static String normalize_cargo_api_base_url(String value)
{
value.trim();
if (value.length() == 0) {
value = DASHBOARD_DEFAULT_CARGO_API_BASE_URL;
}
if (!value.startsWith("http://") && !value.startsWith("https://")) {
value = "http://" + value;
}
while (value.endsWith("/")) {
value.remove(value.length() - 1);
}
return value;
}
static void rebuild_cargo_api_urls()
{
cargo_api_base_url = normalize_cargo_api_base_url(cargo_api_base_url);
cargo_api_status_url = cargo_api_base_url + "/api/v1/status";
cargo_api_fast_status_url = cargo_api_base_url + "/api/v1/status?fields=battery,temps,relays";
cargo_api_slow_status_url = cargo_api_base_url + "/api/v1/status?fields=network,system,config";
cargo_api_relay_set_url = cargo_api_base_url + "/api/v1/relay/set";
}
static void load_dashboard_connection_config()
{
dashboard_preferences.begin("dashcfg", false);
bool has_saved_ssid = dashboard_preferences.isKey("wifi_ssid");
cargo_wifi_ssid = dashboard_preferences.getString("wifi_ssid", DASHBOARD_DEFAULT_CARGO_WIFI_SSID);
cargo_wifi_password = dashboard_preferences.getString("wifi_pass", DASHBOARD_DEFAULT_CARGO_WIFI_PASSWORD);
cargo_api_base_url = dashboard_preferences.getString("api_base", DASHBOARD_DEFAULT_CARGO_API_BASE_URL);
rebuild_cargo_api_urls();
// Only skip setup after the user has explicitly saved settings.
dashboard_configured = has_saved_ssid;
Serial.print("Dashboard cargo SSID configured: ");
Serial.println(dashboard_configured ? "yes" : "no");
Serial.print("Dashboard cargo API base: ");
Serial.println(cargo_api_base_url);
}
static void save_dashboard_connection_config(const String &ssid, const String &password, const String &api_base)
{
cargo_wifi_ssid = ssid;
cargo_wifi_password = password;
cargo_api_base_url = api_base;
rebuild_cargo_api_urls();
dashboard_preferences.putString("wifi_ssid", cargo_wifi_ssid);
dashboard_preferences.putString("wifi_pass", cargo_wifi_password);
dashboard_preferences.putString("api_base", cargo_api_base_url);
dashboard_configured = cargo_wifi_ssid.length() > 0;
}
static void setup_keyboard_event_cb(lv_event_t *event)
{
lv_event_code_t code = lv_event_get_code(event);
if (code == LV_EVENT_READY || code == LV_EVENT_CANCEL || code == LV_EVENT_CLICKED) {
if (setup_keyboard != nullptr) {
lv_keyboard_set_textarea(setup_keyboard, nullptr);
lv_obj_add_flag(setup_keyboard, LV_OBJ_FLAG_HIDDEN);
}
}
}
static void setup_textarea_event_cb(lv_event_t *event)
{
lv_event_code_t code = lv_event_get_code(event);
lv_obj_t *target = lv_event_get_target(event);
if (code == LV_EVENT_FOCUSED || code == LV_EVENT_CLICKED) {
if (setup_keyboard != nullptr) {
lv_keyboard_set_textarea(setup_keyboard, target);
lv_obj_clear_flag(setup_keyboard, LV_OBJ_FLAG_HIDDEN);
}
}
}
static lv_obj_t *create_setup_textarea(lv_obj_t *parent, const char *label_text, const String &value, int y, bool password)
{
lv_obj_t *label = lv_label_create(parent);
lv_label_set_text(label, label_text);
lv_obj_set_style_text_color(label, lv_color_hex(0xDDE3EA), 0);
lv_obj_set_style_text_font(label, &lv_font_montserrat_26, 0);
lv_obj_align(label, LV_ALIGN_TOP_LEFT, 80, y);
lv_obj_t *textarea = lv_textarea_create(parent);
lv_obj_set_size(textarea, 760, 52);
lv_obj_align(textarea, LV_ALIGN_TOP_LEFT, 80, y + 32);
lv_textarea_set_one_line(textarea, true);
lv_textarea_set_text(textarea, value.c_str());
lv_textarea_set_password_mode(textarea, password);
lv_obj_set_style_text_font(textarea, &lv_font_montserrat_26, 0);
lv_obj_add_event_cb(textarea, setup_textarea_event_cb, LV_EVENT_ALL, nullptr);
return textarea;
}
static void setup_save_event_cb(lv_event_t *event)
{
if (lv_event_get_code(event) != LV_EVENT_CLICKED) {
return;
}
if (setup_keyboard != nullptr) {
lv_keyboard_set_textarea(setup_keyboard, nullptr);
lv_obj_add_flag(setup_keyboard, LV_OBJ_FLAG_HIDDEN);
}
String ssid = lv_textarea_get_text(setup_ssid_textarea);
String password = lv_textarea_get_text(setup_password_textarea);
String api_base = lv_textarea_get_text(setup_api_textarea);
ssid.trim();
api_base.trim();
if (ssid.length() == 0) {
if (setup_status_label != nullptr) {
lv_label_set_text(setup_status_label, "SSID is required.");
}
return;
}
save_dashboard_connection_config(ssid, password, api_base);
WiFi.disconnect(true);
was_wifi_connected = false;
overview_screen_created = false;
last_wifi_attempt_ms = 0;
last_status_poll_ms = 0;
last_metadata_poll_ms = 0;
metadata_refresh_requested = true;
lvgl_port_lock(-1);
if (dashboard_configured) {
show_boot_screen();
} else {
show_dashboard_setup_screen();
}
lvgl_port_unlock();
Serial.println("Dashboard connection settings saved.");
}
static void setup_reset_event_cb(lv_event_t *event)
{
if (lv_event_get_code(event) != LV_EVENT_CLICKED) {
return;
}
if (setup_keyboard != nullptr) {
lv_keyboard_set_textarea(setup_keyboard, nullptr);
lv_obj_add_flag(setup_keyboard, LV_OBJ_FLAG_HIDDEN);
}
dashboard_preferences.clear();
cargo_wifi_ssid = "";
cargo_wifi_password = "";
cargo_api_base_url = DASHBOARD_DEFAULT_CARGO_API_BASE_URL;
rebuild_cargo_api_urls();
dashboard_configured = false;
WiFi.disconnect(true);
lv_textarea_set_text(setup_ssid_textarea, "");
lv_textarea_set_text(setup_password_textarea, "");
lv_textarea_set_text(setup_api_textarea, cargo_api_base_url.c_str());
if (setup_status_label != nullptr) {
lv_label_set_text(setup_status_label, "Saved settings cleared.");
}
}
static void show_dashboard_setup_screen()
{
lv_obj_t *screen = lv_scr_act();
lv_obj_clean(screen);
lv_obj_set_style_bg_color(screen, lv_color_hex(0x101418), 0);
lv_obj_t *title = lv_label_create(screen);
lv_label_set_text(title, "Dashboard Setup");
lv_obj_set_style_text_color(title, lv_color_hex(0xFFFFFF), 0);
lv_obj_set_style_text_font(title, &lv_font_montserrat_48, 0);
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 28);
lv_obj_t *subtitle = lv_label_create(screen);
lv_label_set_text(subtitle, "Enter Cargo ESP WiFi and API settings.");
lv_obj_set_style_text_color(subtitle, lv_color_hex(0xB8C0C8), 0);
lv_obj_set_style_text_font(subtitle, &lv_font_montserrat_26, 0);
lv_obj_align(subtitle, LV_ALIGN_TOP_MID, 0, 86);
setup_ssid_textarea = create_setup_textarea(screen, "Cargo WiFi SSID", cargo_wifi_ssid, 130, false);
setup_password_textarea = create_setup_textarea(screen, "Cargo WiFi Password", cargo_wifi_password, 220, true);
setup_api_textarea = create_setup_textarea(screen, "Cargo API Base URL", cargo_api_base_url, 310, false);
lv_obj_t *save_button = lv_btn_create(screen);
lv_obj_set_size(save_button, 210, 58);
lv_obj_align(save_button, LV_ALIGN_TOP_LEFT, 80, 408);
lv_obj_add_event_cb(save_button, setup_save_event_cb, LV_EVENT_CLICKED, nullptr);
lv_obj_t *save_label = lv_label_create(save_button);
lv_label_set_text(save_label, "Save");
lv_obj_center(save_label);
lv_obj_t *clear_button = lv_btn_create(screen);
lv_obj_set_size(clear_button, 210, 58);
lv_obj_align(clear_button, LV_ALIGN_TOP_LEFT, 310, 408);
lv_obj_add_event_cb(clear_button, setup_reset_event_cb, LV_EVENT_CLICKED, nullptr);
lv_obj_t *clear_label = lv_label_create(clear_button);
lv_label_set_text(clear_label, "Clear");
lv_obj_center(clear_label);
setup_status_label = lv_label_create(screen);
lv_label_set_text(setup_status_label, dashboard_configured ? "Settings loaded." : "First boot: setup required.");
lv_obj_set_style_text_color(setup_status_label, lv_color_hex(0xDDE3EA), 0);
lv_obj_set_style_text_font(setup_status_label, &lv_font_montserrat_26, 0);
lv_obj_set_width(setup_status_label, 420);
lv_label_set_long_mode(setup_status_label, LV_LABEL_LONG_WRAP);
lv_obj_align(setup_status_label, LV_ALIGN_TOP_LEFT, 548, 410);
lv_obj_t *done_button = lv_btn_create(screen);
lv_obj_set_size(done_button, 180, 48);
lv_obj_align(done_button, LV_ALIGN_BOTTOM_RIGHT, -32, -188);
lv_obj_add_event_cb(done_button, setup_keyboard_event_cb, LV_EVENT_CLICKED, nullptr);
lv_obj_t *done_label = lv_label_create(done_button);
lv_label_set_text(done_label, "Done");
lv_obj_center(done_label);
setup_keyboard = lv_keyboard_create(screen);
lv_obj_set_size(setup_keyboard, 1024, 180);
lv_obj_align(setup_keyboard, LV_ALIGN_BOTTOM_MID, 0, 0);
lv_obj_add_event_cb(setup_keyboard, setup_keyboard_event_cb, LV_EVENT_ALL, nullptr);
lv_obj_add_flag(setup_keyboard, LV_OBJ_FLAG_HIDDEN);
}
static void setup_button_event_cb(lv_event_t *event)
{
if (lv_event_get_code(event) != LV_EVENT_CLICKED) {
return;
}
lvgl_port_lock(-1);
show_dashboard_setup_screen();
lvgl_port_unlock();
}
static bool post_relay_state(const String &relay_id, bool state)
{
if (api_request_in_progress || WiFi.status() != WL_CONNECTED) {
@@ -1158,12 +1434,12 @@ static bool post_relay_state(const String &relay_id, bool state)
serializeJson(request, payload);
Serial.print("POST ");
Serial.println(CARGO_API_RELAY_SET_URL);
Serial.println(cargo_api_relay_set_url);
Serial.println(payload);
HTTPClient http;
http.setTimeout(1200);
http.begin(CARGO_API_RELAY_SET_URL);
http.begin(cargo_api_relay_set_url);
http.setReuse(false);
http.addHeader("Connection", "close");
http.addHeader("Content-Type", "application/json");
@@ -1280,6 +1556,10 @@ static void process_pending_relay_command()
static void connect_wifi_if_needed()
{
if (!dashboard_configured) {
return;
}
if (WiFi.status() == WL_CONNECTED) {
return;
}
@@ -1291,10 +1571,10 @@ static void connect_wifi_if_needed()
last_wifi_attempt_ms = millis();
Serial.print("Connecting to Cargo ESP AP: ");
Serial.println(CARGO_WIFI_SSID);
Serial.println(cargo_wifi_ssid);
WiFi.mode(WIFI_STA);
WiFi.begin(CARGO_WIFI_SSID, CARGO_WIFI_PASSWORD);
WiFi.begin(cargo_wifi_ssid.c_str(), cargo_wifi_password.c_str());
}
static void update_system_status_label()
@@ -1835,13 +2115,13 @@ static void poll_status_api()
if (fast_status_refresh_requested || millis() - last_status_poll_ms >= 1500) {
fast_status_refresh_requested = false;
last_status_poll_ms = millis();
fetch_status_fields(CARGO_API_FAST_STATUS_URL, "fast status");
fetch_status_fields(cargo_api_fast_status_url.c_str(), "fast status");
}
if (metadata_refresh_requested || millis() - last_metadata_poll_ms >= 30000) {
metadata_refresh_requested = false;
last_metadata_poll_ms = millis();
fetch_status_fields(CARGO_API_SLOW_STATUS_URL, "slow status");
fetch_status_fields(cargo_api_slow_status_url.c_str(), "slow status");
}
}
@@ -1908,6 +2188,15 @@ static void show_boot_screen()
lv_obj_set_width(api, 700);
lv_obj_set_style_text_align(api, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_align(api, LV_ALIGN_CENTER, 0, 118);
lv_obj_t *setup_button = lv_btn_create(screen);
lv_obj_set_size(setup_button, 180, 54);
lv_obj_align(setup_button, LV_ALIGN_BOTTOM_MID, 0, -34);
lv_obj_add_event_cb(setup_button, setup_button_event_cb, LV_EVENT_CLICKED, nullptr);
lv_obj_t *setup_label = lv_label_create(setup_button);
lv_label_set_text(setup_label, "Setup");
lv_obj_center(setup_label);
}
static void create_overland_overview_screen()
@@ -2181,6 +2470,8 @@ void setup()
Serial.println("Display: 1024x600");
Serial.println("API Base: /api/v1");
load_dashboard_connection_config();
Board *board = new Board();
board->init();
@@ -1249,11 +1249,20 @@ function renderConfigControls(data){
<div class="configrow">
<label>${r.id}</label>
<input id="relayName${i}" placeholder="Relay name" oninput="markConfigFieldTouched('relayName${i}')">
<label>${r.enabled?"Enabled":"Disabled"}</label>
<label class="tempCheck">
<input id="relayEnabled${i}" type="checkbox" onchange="markConfigFieldTouched('relayEnabled${i}')">
Enabled
</label>
</div>`).join("");
}
relays.forEach((r,i)=>setConfigValue("relayName"+i,r.name||""));
relays.forEach((r,i)=>{
setConfigValue("relayName"+i,r.name||"");
const enabled=$("relayEnabled"+i);
if(enabled && !configTouched["relayEnabled"+i] && document.activeElement!==enabled){
enabled.checked=!!r.enabled;
}
});
}
setConfigValue("tempEnabledCount", temps.filter(t=>t.enabled).length);
@@ -1359,8 +1368,8 @@ async function saveRelayConfig(){
const relays=data.config?.relays||[];
for(let i=0;i<relays.length;i++){
const name=$("relayName"+i)?.value.trim();
if(!name) continue;
const name=$("relayName"+i)?.value.trim() || relays[i].name || relays[i].id;
const enabled=$("relayEnabled"+i)?.checked ?? relays[i].enabled;
await fetch(api("/config/relay"),{
method:"POST",
@@ -1368,14 +1377,14 @@ async function saveRelayConfig(){
body:JSON.stringify({
id:relays[i].id,
name:name,
enabled:relays[i].enabled
enabled:enabled
})
});
}
await fetch(api("/config/save"),{method:"POST"});
await load();
alert("Relay names saved");
alert("Relay settings saved");
}
async function saveTempConfig(){
@@ -1543,9 +1552,19 @@ String staPasswords[MAX_WIFI_NETWORKS];
int staPriorities[MAX_WIFI_NETWORKS];
int wifiNetworkCount = 0;
String activeStaSsid = "";
bool staAttemptTried[MAX_WIFI_NETWORKS] = {false, false, false};
bool staConnectInProgress = false;
int activeStaAttemptIndex = -1;
unsigned long staConnectAttemptStarted = 0;
unsigned long lastStaReconnectAttempt = 0;
const unsigned long STA_CONNECT_TIMEOUT_MS = 8000;
const unsigned long STA_RECONNECT_INTERVAL_MS = 30000;
void resetStaAttemptRound();
bool beginNextStaWifiAttempt();
void connectStaWifi();
void maintainStaWifi();
void loadWifiConfig() {
wifiPrefs.begin("wifi", true);
@@ -2110,85 +2129,110 @@ int findNextWifiIndexByPriority(bool tried[]) {
return bestIndex;
}
void resetStaAttemptRound() {
for (int i = 0; i < MAX_WIFI_NETWORKS; i++) {
staAttemptTried[i] = false;
}
staConnectInProgress = false;
activeStaAttemptIndex = -1;
}
bool beginNextStaWifiAttempt() {
if (wifiNetworkCount <= 0) {
Serial.println("STA WiFi not configured.");
return false;
}
int i = findNextWifiIndexByPriority(staAttemptTried);
if (i < 0) {
return false;
}
staAttemptTried[i] = true;
activeStaAttemptIndex = i;
activeStaSsid = "";
Serial.print("Starting non-blocking STA WiFi attempt priority ");
Serial.print(staPriorities[i]);
Serial.print(" to: ");
Serial.println(staSsids[i]);
WiFi.disconnect(false, false);
WiFi.mode(WIFI_AP_STA);
WiFi.begin(staSsids[i].c_str(), staPasswords[i].c_str());
staConnectAttemptStarted = millis();
staConnectInProgress = true;
return true;
}
void connectStaWifi() {
if (wifiNetworkCount <= 0) {
Serial.println("STA WiFi not configured.");
return;
}
WiFi.disconnect(false);
activeStaSsid = "";
resetStaAttemptRound();
lastStaReconnectAttempt = millis();
bool tried[MAX_WIFI_NETWORKS] = {false, false, false};
for (int attempt = 0; attempt < wifiNetworkCount; attempt++) {
int i = findNextWifiIndexByPriority(tried);
if (i < 0) {
break;
}
tried[i] = true;
Serial.print("Connecting STA WiFi priority ");
Serial.print(staPriorities[i]);
Serial.print(" to: ");
Serial.println(staSsids[i]);
WiFi.disconnect(false, false);
delay(500);
WiFi.mode(WIFI_AP_STA);
delay(250);
WiFi.begin(staSsids[i].c_str(), staPasswords[i].c_str());
unsigned long start = millis();
while (WiFi.status() != WL_CONNECTED && millis() - start < 8000) {
server.handleClient();
oledLoop();
delay(100);
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;
}
wl_status_t status = WiFi.status();
Serial.print("STA WiFi attempt failed. Status: ");
Serial.println((int)status);
wifi_ap_record_t apInfo;
if (esp_wifi_sta_get_ap_info(&apInfo) == ESP_OK) {
Serial.print("Connected AP RSSI: ");
Serial.println(apInfo.rssi);
} else {
Serial.println("No AP association info available.");
}
if (!beginNextStaWifiAttempt()) {
Serial.println("No saved STA WiFi networks available. AP remains available.");
}
Serial.println("All STA WiFi attempts failed. AP remains available.");
}
void maintainStaWifi() {
if (wifiNetworkCount <= 0) return;
if (WiFi.status() == WL_CONNECTED) return;
if (WiFi.status() == WL_CONNECTED) {
if (activeStaSsid.length() == 0) {
if (activeStaAttemptIndex >= 0 && activeStaAttemptIndex < wifiNetworkCount) {
activeStaSsid = staSsids[activeStaAttemptIndex];
} else {
activeStaSsid = WiFi.SSID();
}
Serial.println("STA WiFi connected");
Serial.print("STA SSID: ");
Serial.println(activeStaSsid);
Serial.print("STA IP: ");
Serial.println(WiFi.localIP());
}
staConnectInProgress = false;
activeStaAttemptIndex = -1;
return;
}
if (staConnectInProgress) {
if (millis() - staConnectAttemptStarted < STA_CONNECT_TIMEOUT_MS) {
return;
}
Serial.print("STA WiFi attempt timed out. Status: ");
Serial.println((int)WiFi.status());
WiFi.disconnect(false, false);
staConnectInProgress = false;
activeStaAttemptIndex = -1;
if (beginNextStaWifiAttempt()) {
return;
}
Serial.println("All STA WiFi attempts failed. AP remains available.");
resetStaAttemptRound();
lastStaReconnectAttempt = millis();
return;
}
activeStaSsid = "";
if (millis() - lastStaReconnectAttempt < STA_RECONNECT_INTERVAL_MS) return;
Serial.println("STA WiFi disconnected. Trying saved networks by priority in background...");
resetStaAttemptRound();
lastStaReconnectAttempt = millis();
Serial.println("STA WiFi disconnected. Trying saved networks by priority...");
connectStaWifi();
beginNextStaWifiAttempt();
}
void printNetworkStatus() {
@@ -2643,9 +2687,19 @@ void sendError(Stream& output, const char* message) {
int findRelayConfigIndex(const String& id);
bool relayExistsAndAvailable(const String& relayId) {
int index = findRelayConfigIndex(relayId);
return index >= 0 && appConfig.relays[index].available;
}
bool relayIsEnabled(const String& relayId) {
int index = findRelayConfigIndex(relayId);
return index >= 0 && appConfig.relays[index].enabled;
}
bool setRelayById(const String& relayId, bool enabled) {
int index = findRelayConfigIndex(relayId);
if (index < 0 || !appConfig.relays[index].available) {
if (index < 0 || !appConfig.relays[index].available || !appConfig.relays[index].enabled) {
return false;
}
@@ -3651,7 +3705,13 @@ void handleUpdateRelayConfig() {
}
if (doc["name"].is<String>()) appConfig.relays[index].name = doc["name"].as<String>();
if (doc["enabled"].is<bool>()) appConfig.relays[index].enabled = doc["enabled"].as<bool>();
if (doc["enabled"].is<bool>()) {
appConfig.relays[index].enabled = doc["enabled"].as<bool>();
if (!appConfig.relays[index].enabled) {
relays.state[index] = false;
updateRelayOutputs();
}
}
sendOkConfig();
}
@@ -3994,10 +4054,18 @@ void handleSetRelayPost() {
bool state = doc["state"] | false;
unsigned long applyStartMs = millis();
if (!setRelayById(relayId, state)) {
if (!relayExistsAndAvailable(relayId)) {
server.send(404, "application/json", "{\"ok\":false,\"error\":\"unknown_relay\"}");
return;
}
if (!relayIsEnabled(relayId)) {
server.send(409, "application/json", "{\"ok\":false,\"error\":\"relay_disabled\"}");
return;
}
if (!setRelayById(relayId, state)) {
server.send(409, "application/json", "{\"ok\":false,\"error\":\"relay_disabled\"}");
return;
}
unsigned long appliedMs = millis() - applyStartMs;
DynamicJsonDocument response(512);
@@ -4052,10 +4120,18 @@ void handleGenericRelayRoute() {
return;
}
if (!setRelayById(relayId, enabled)) {
if (!relayExistsAndAvailable(relayId)) {
server.send(404, "application/json", "{\"ok\":false,\"error\":\"unknown_relay\"}");
return;
}
if (!relayIsEnabled(relayId)) {
server.send(409, "application/json", "{\"ok\":false,\"error\":\"relay_disabled\"}");
return;
}
if (!setRelayById(relayId, enabled)) {
server.send(409, "application/json", "{\"ok\":false,\"error\":\"relay_disabled\"}");
return;
}
DynamicJsonDocument doc(512);
doc["ok"] = true;
@@ -4206,6 +4282,7 @@ void setup() {
void loop() {
server.handleClient();
oledLoop();
maintainStaWifi();
+17
View File
@@ -0,0 +1,17 @@
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
DASHBOARD = ROOT / "firmware" / "esp32-s3-dashboard" / "esp32-s3-dashboard.ino"
def test_setup_done_button_event_is_handled():
source = DASHBOARD.read_text()
callback_start = source.index("static void setup_keyboard_event_cb")
callback_end = source.index("static void setup_textarea_event_cb", callback_start)
callback = source[callback_start:callback_end]
assert "LV_EVENT_CLICKED" in callback
assert "lv_obj_add_flag(setup_keyboard, LV_OBJ_FLAG_HIDDEN);" in callback
assert "lv_obj_add_event_cb(done_button, setup_keyboard_event_cb, LV_EVENT_CLICKED" in source
+3
View File
@@ -248,6 +248,8 @@ def test_relay_set_contract_fixtures_and_firmware_errors():
source = firmware_source()
assert '"invalid_json"' in source
assert '"unknown_relay"' in source
assert "relay_disabled" in source
assert "relayIsEnabled" in source
def test_relay_api_is_output_count_agnostic():
@@ -259,6 +261,7 @@ def test_relay_api_is_output_count_agnostic():
assert 'relay["hardware_channel"] = appConfig.relays[i].hardwareChannel;' in source
assert 'relay["available"] = appConfig.relays[i].available;' in source
assert 'relays.state[index] = enabled;' in source
assert '!appConfig.relays[index].enabled' in source
assert 'relays.relay1' not in source
assert 'relays.relay2' not in source