diff --git a/firmware/esp32-s3-dashboard/esp32-s3-dashboard.ino b/firmware/esp32-s3-dashboard/esp32-s3-dashboard.ino index d4a11dc..55734c1 100644 --- a/firmware/esp32-s3-dashboard/esp32-s3-dashboard.ino +++ b/firmware/esp32-s3-dashboard/esp32-s3-dashboard.ino @@ -10,16 +10,22 @@ using namespace esp_panel::drivers; using namespace esp_panel::board; -static const char *DASHBOARD_VERSION = "0.0.4-clean-overview"; +static const char *DASHBOARD_VERSION = "0.0.5-relay-control"; // 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_API_STATUS_URL = "http://192.168.4.1/api/v1/status"; +static const char *CARGO_API_RELAY_SET_URL = "http://192.168.4.1/api/v1/relay/set"; static lv_obj_t *battery_label = nullptr; static lv_obj_t *temps_label = nullptr; static lv_obj_t *outputs_label = nullptr; +static lv_obj_t *relay_buttons[6] = {nullptr}; +static lv_obj_t *relay_button_labels[6] = {nullptr}; +static String relay_ids[6]; +static bool relay_states[6] = {false}; +static int relay_count = 0; static lv_obj_t *system_label = nullptr; static lv_obj_t *wifi_label = nullptr; static lv_obj_t *touch_count_label = nullptr; @@ -66,6 +72,101 @@ static String on_off(bool value) return value ? "ON" : "OFF"; } +static void force_status_poll_soon() +{ + last_status_poll_ms = 0; +} + +static bool post_relay_state(const String &relay_id, bool state) +{ + if (WiFi.status() != WL_CONNECTED) { + Serial.println("Relay command skipped: WiFi not connected"); + return false; + } + + DynamicJsonDocument request(256); + request["id"] = relay_id; + request["state"] = state; + + String payload; + serializeJson(request, payload); + + Serial.print("POST "); + Serial.println(CARGO_API_RELAY_SET_URL); + Serial.println(payload); + + HTTPClient http; + http.setTimeout(5000); + http.begin(CARGO_API_RELAY_SET_URL); + http.addHeader("Content-Type", "application/json"); + + int code = http.POST(payload); + String body = http.getString(); + + Serial.print("Relay HTTP code: "); + Serial.println(code); + Serial.println(body); + + http.end(); + + return code >= 200 && code < 300; +} + +static void relay_button_event_cb(lv_event_t *event) +{ + if (lv_event_get_code(event) != LV_EVENT_CLICKED) { + return; + } + + intptr_t index_value = reinterpret_cast(lv_event_get_user_data(event)); + int index = static_cast(index_value); + + if (index < 0 || index >= relay_count) { + Serial.println("Relay button index out of range"); + return; + } + + String relay_id = relay_ids[index]; + bool next_state = !relay_states[index]; + + String pending_text = "Sending relay command...\n"; + pending_text += relay_id; + pending_text += " -> "; + pending_text += on_off(next_state); + set_label_text_if_changed(system_label, last_system_text, pending_text); + + bool ok = post_relay_state(relay_id, next_state); + + String result_text = ok ? "Relay command sent\nRefreshing status..." : "Relay command failed"; + set_label_text_if_changed(system_label, last_system_text, result_text); + + force_status_poll_soon(); +} + +static void update_relay_buttons() +{ + lvgl_port_lock(-1); + + for (int i = 0; i < 6; i++) { + if (relay_buttons[i] == nullptr || relay_button_labels[i] == nullptr) { + continue; + } + + if (i < relay_count) { + lv_obj_clear_flag(relay_buttons[i], LV_OBJ_FLAG_HIDDEN); + + String label = relay_ids[i]; + label += relay_states[i] ? " OFF" : " ON"; + lv_label_set_text(relay_button_labels[i], label.c_str()); + } else { + lv_obj_add_flag(relay_buttons[i], LV_OBJ_FLAG_HIDDEN); + } + } + + lvgl_port_unlock(); +} + + static void set_label_text_if_changed(lv_obj_t *label, String &last_text, const String &text) { if (label == nullptr || last_text == text) { @@ -219,13 +320,22 @@ static void parse_status_json(const String &body) String outputs = ""; JsonArray relay_array = doc["relays"]; + relay_count = 0; + for (JsonObject relay : relay_array) { + const char *id = relay["id"] | ""; const char *name = relay["name"] | relay["id"] | "Output"; bool enabled = relay["enabled"] | false; bool available = relay["available"] | true; bool state = relay["state"] | false; int channel = relay["hardware_channel"] | 0; + if (relay_count < 6 && enabled && available) { + relay_ids[relay_count] = String(id); + relay_states[relay_count] = state; + relay_count++; + } + outputs += String(channel); outputs += ". "; outputs += name; @@ -249,6 +359,8 @@ static void parse_status_json(const String &body) status_model.temps_text = temps; status_model.outputs_text = outputs; + update_relay_buttons(); + Serial.println("Parsed status summary:"); Serial.print(" SOC: "); Serial.println(status_model.soc); @@ -442,7 +554,7 @@ static void create_overland_status_screen() lv_obj_t *temps_card = create_card(screen, "Temperatures", 300, 230, LV_ALIGN_TOP_MID, 0, 96); lv_obj_t *outputs_card = create_card(screen, "Outputs", 300, 230, LV_ALIGN_TOP_RIGHT, -32, 96); lv_obj_t *system_card = create_card(screen, "System / Network", 620, 190, LV_ALIGN_BOTTOM_LEFT, 32, -50); - lv_obj_t *touch_card = create_card(screen, "Touch", 300, 190, LV_ALIGN_BOTTOM_RIGHT, -32, -50); + lv_obj_t *touch_card = create_card(screen, "Relay Controls", 300, 190, LV_ALIGN_BOTTOM_RIGHT, -32, -50); battery_label = create_body_label(battery_card, 260); temps_label = create_body_label(temps_card, 260); @@ -461,20 +573,44 @@ static void create_overland_status_screen() lv_label_set_long_mode(system_label, LV_LABEL_LONG_WRAP); lv_obj_align(system_label, LV_ALIGN_TOP_LEFT, 305, 32); - lv_obj_t *button = lv_btn_create(touch_card); - lv_obj_set_size(button, 170, 54); - lv_obj_align(button, LV_ALIGN_TOP_LEFT, 0, 36); - lv_obj_add_event_cb(button, touch_test_event_cb, LV_EVENT_CLICKED, nullptr); + for (int i = 0; i < 2; i++) { + relay_buttons[i] = lv_btn_create(touch_card); + lv_obj_set_size(relay_buttons[i], 120, 48); + lv_obj_align(relay_buttons[i], LV_ALIGN_TOP_LEFT, i * 132, 34); + lv_obj_add_event_cb( + relay_buttons[i], + relay_button_event_cb, + LV_EVENT_CLICKED, + reinterpret_cast(static_cast(i)) + ); - lv_obj_t *button_label = lv_label_create(button); - lv_label_set_text(button_label, "Touch Test"); - lv_obj_center(button_label); + relay_button_labels[i] = lv_label_create(relay_buttons[i]); + lv_label_set_text(relay_button_labels[i], "Relay"); + lv_obj_center(relay_button_labels[i]); + } + + for (int i = 2; i < 6; i++) { + relay_buttons[i] = lv_btn_create(touch_card); + lv_obj_set_size(relay_buttons[i], 120, 42); + lv_obj_align(relay_buttons[i], LV_ALIGN_TOP_LEFT, ((i - 2) % 2) * 132, 92 + (((i - 2) / 2) * 48)); + lv_obj_add_event_cb( + relay_buttons[i], + relay_button_event_cb, + LV_EVENT_CLICKED, + reinterpret_cast(static_cast(i)) + ); + + relay_button_labels[i] = lv_label_create(relay_buttons[i]); + lv_label_set_text(relay_button_labels[i], "Relay"); + lv_obj_center(relay_button_labels[i]); + lv_obj_add_flag(relay_buttons[i], LV_OBJ_FLAG_HIDDEN); + } touch_count_label = lv_label_create(touch_card); - lv_label_set_text(touch_count_label, "Touch count: 0"); + lv_label_set_text(touch_count_label, "Relay controls"); lv_obj_set_style_text_color(touch_count_label, lv_color_hex(0xB8C0C8), 0); lv_obj_set_width(touch_count_label, 250); - lv_obj_align(touch_count_label, LV_ALIGN_TOP_LEFT, 0, 105); + lv_obj_align(touch_count_label, LV_ALIGN_TOP_LEFT, 0, 142); lv_obj_t *footer = lv_label_create(screen); lv_label_set_text(footer, "Polling Cargo ESP /api/v1/status every 5 seconds");