From f862dd1aa8ad606c68e5a7aa520b6ddfde06e396 Mon Sep 17 00:00:00 2001 From: Christos Themelis Date: Fri, 20 Feb 2026 22:15:35 +0200 Subject: [PATCH] wip --- examples/companion_radio/MyMesh.cpp | 20 + examples/companion_radio/MyMesh.h | 3 + examples/companion_radio/main.cpp | 249 +++++- examples/companion_radio/task_clock.cpp | 25 + examples/companion_radio/task_lvgl.cpp | 19 + examples/companion_radio/uiManager.cpp | 797 ++++++++++++++++++ examples/companion_radio/uiTasks.cpp | 50 ++ examples/simple_secure_chat_ui/uiManager.cpp | 4 +- .../sensecap_indicator-espnow/platformio.ini | 3 +- 9 files changed, 1136 insertions(+), 34 deletions(-) create mode 100644 examples/companion_radio/task_clock.cpp create mode 100644 examples/companion_radio/task_lvgl.cpp create mode 100644 examples/companion_radio/uiManager.cpp create mode 100644 examples/companion_radio/uiTasks.cpp diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 2dad7866a..d29ed4575 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -872,6 +872,10 @@ void MyMesh::begin(bool has_display) { const char *MyMesh::getNodeName() { return _prefs.node_name; } + +const char* MyMesh::getFirmwareVer() { return FIRMWARE_VERSION; } +const char* MyMesh::getBuildDate() { return FIRMWARE_BUILD_DATE; } + NodePrefs *MyMesh::getNodePrefs() { return &_prefs; } @@ -1747,6 +1751,22 @@ void MyMesh::enterCLIRescue() { Serial.println("========= CLI Rescue ========="); } +void MyMesh::handleCommand(const char* command) { + while (*command == ' ') command++; // skip leading space + if (strcmp(command, "erase") == 0) { + bool success = _store->formatFileSystem(); + if (success) { + Serial.println(" > erase done"); + } else { + Serial.println(" Error: erase failed"); + } + } else if (strcmp(cli_command, "reboot") == 0) { + board.reboot(); // doesn't return + } else { + Serial.println(" Error: unknown command"); + } +} + void MyMesh::checkCLIRescueCmd() { int len = strlen(cli_command); while (Serial.available() && len < sizeof(cli_command)-1) { diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 95265a19a..a9cf7e34d 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -92,6 +92,8 @@ public: void startInterface(BaseSerialInterface &serial); const char *getNodeName(); + const char *getFirmwareVer(); + const char *getBuildDate(); NodePrefs *getNodePrefs(); uint32_t getBLEPin(); @@ -158,6 +160,7 @@ protected: public: void savePrefs() { _store->savePrefs(_prefs, sensors.node_lat, sensors.node_lon); } + void handleCommand(const char* command); private: void writeOKFrame(); diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 7e636acee..75d875e4c 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -2,6 +2,77 @@ #include #include "MyMesh.h" +#include "esp_log.h" +#include +#include +#include +#include + +#include "../src/fonts/fonts.h" +#include "../lvgl/lvgl.h" + +#include "../include/uiExternals.h" +#include "../include/lgfx.h" +#include "../include/uiDefines.h" +#include "../include/uiManager.h" +#include "../include/uiTasks.h" +#include "uiTouch.h" + +#define TAG "main" + +static uint32_t screenWidth; +static uint32_t screenHeight; +static lv_disp_draw_buf_t draw_buf; +static lv_color_t disp_draw_buf[800 * 480 / 10]; +//static lv_color_t disp_draw_buf; +static lv_disp_drv_t disp_drv; + +UIManager *uiManager; + +SemaphoreHandle_t semaphoreData; + +TwoWire I2Cone = TwoWire(0); +#ifndef SEEED_SENSECAP_INDICATOR +Adafruit_SSD1306 display = Adafruit_SSD1306(128, 64, &I2Cone, OLED_RESET); +#endif + +SPIClass& spi = SPI; +uint16_t touchCalibration_x0 = 300, touchCalibration_x1 = 3600, touchCalibration_y0 = 300, touchCalibration_y1 = 3600; +uint8_t touchCalibration_rotate = 1, touchCalibration_invert_x = 2, touchCalibration_invert_y = 0; + +/* Display flushing */ +void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p) +{ + uint32_t w = (area->x2 - area->x1 + 1); + uint32_t h = (area->y2 - area->y1 + 1); + + //lcd.fillScreen(TFT_WHITE); +#if (LV_COLOR_16_SWAP != 0) + lcd.pushImageDMA(area->x1, area->y1, w, h,(lgfx::rgb565_t*)&color_p->full); +#else + lcd.pushImageDMA(area->x1, area->y1, w, h,(lgfx::rgb565_t*)&color_p->full);// +#endif + + lv_disp_flush_ready(disp); + +} + +void my_touchpad_read(lv_indev_drv_t * indev_driver, lv_indev_data_t * data) +{ + uint16_t x, y; + + if (lcd.getTouch(&x, &y)) + { + data->state = LV_INDEV_STATE_PR; + data->point.x = x; + data->point.y = y; + } + else + { + data->state = LV_INDEV_STATE_REL; + } +} + // Believe it or not, this std C function is busted on some platforms! static uint32_t _atoi(const char* sp) { uint32_t n = 0; @@ -86,17 +157,17 @@ static uint32_t _atoi(const char* sp) { #endif /* GLOBAL OBJECTS */ -#ifdef DISPLAY_CLASS - #include "UITask.h" - UITask ui_task(&board, &serial_interface); -#endif +// #ifdef DISPLAY_CLASS +// #include "UITask.h" +// UITask ui_task(&board, &serial_interface); +// #endif StdRNG fast_rng; SimpleMeshTables tables; MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store - #ifdef DISPLAY_CLASS - , &ui_task - #endif + // #ifdef DISPLAY_CLASS + // , &ui_task + // #endif ); /* END GLOBAL OBJECTS */ @@ -105,23 +176,106 @@ void halt() { while (1) ; } +void configureDisplay() { + ESP_LOGI(TAG, "Configuring display..."); + + screenWidth = lcd.width(); + screenHeight = lcd.height(); + + lv_disp_draw_buf_init(&draw_buf, disp_draw_buf, NULL, screenWidth * screenHeight / 10); + + /* Initialize the display */ + lv_disp_drv_init(&disp_drv); + /* Change the following line to your display resolution */ + disp_drv.hor_res = screenWidth; + disp_drv.ver_res = screenHeight; + disp_drv.flush_cb = my_disp_flush; + disp_drv.draw_buf = &draw_buf; + lv_disp_drv_register(&disp_drv); + + /* Initialize the (dummy) input device driver */ + static lv_indev_drv_t indev_drv; + lv_indev_drv_init(&indev_drv); + indev_drv.type = LV_INDEV_TYPE_POINTER; + indev_drv.read_cb = my_touchpad_read; + lv_indev_drv_register(&indev_drv); +#ifdef TFT_BL + pinMode(TFT_BL, OUTPUT); + digitalWrite(TFT_BL, HIGH); +#endif + + lcd.fillScreen(0x000000u); +} + +void initializeDisplay() { + ESP_LOGI(TAG, "Initializing display..."); + lcd.begin(); + lcd.fillScreen(0x000000u); + lcd.setTextSize(2); + lcd.setRotation(1); + //lcd.setBrightness(127); +} + +void initializeTouchScreen() { + ESP_LOGI(TAG, "Initializing touch screen..."); + touch_init(); +} + +void initializeLVGL() { + ESP_LOGI(TAG, "Initializing LVGL..."); + lv_init(); +} + +void initializeUI() { + + Serial.println("initialize UI..."); + + /* + #ifdef ENABLE_STARTUP_LOGO + lv_disp_load_scr(ui_ScreenLogo); + for( int i=0; i < 100; i++ ){ + lv_task_handler(); + delay(10); + } + lv_scr_load_anim(ui_Screen1, LV_SCR_LOAD_ANIM_MOVE_TOP, 500, 0, true); + #else + ui_init(); + #endif + */ + uiManager = new UIManager(); + uiManager->setNightMode(false); +} + void setup() { Serial.begin(115200); board.begin(); -#ifdef DISPLAY_CLASS - DisplayDriver* disp = NULL; - if (display.begin()) { - disp = &display; - disp->startFrame(); - #ifdef ST7789 - disp->setTextSize(2); - #endif - disp->drawTextCentered(disp->width() / 2, 28, "Loading..."); - disp->endFrame(); - } +#ifdef SEEED_SENSECAP_INDICATOR + initializeDisplay(); + delay(200); + + initializeLVGL(); + initializeTouchScreen(); + configureDisplay(); + + initializeUI(); + + createTasks(); + lv_timer_handler(); #endif +// #ifdef DISPLAY_CLASS +// DisplayDriver* disp = NULL; +// if (display.begin()) { +// disp = &display; +// disp->startFrame(); +// #ifdef ST7789 +// disp->setTextSize(2); +// #endif +// disp->drawTextCentered(disp->width() / 2, 28, "Loading..."); +// disp->endFrame(); +// } +// #endif if (!radio_init()) { halt(); } @@ -186,11 +340,11 @@ void setup() { SPIFFS.begin(true); store.begin(); the_mesh.begin( - #ifdef DISPLAY_CLASS - disp != NULL - #else + // #ifdef DISPLAY_CLASS + // disp != NULL + // #else false - #endif + //#endif ); #ifdef WIFI_SSID @@ -212,16 +366,49 @@ void setup() { sensors.begin(); -#ifdef DISPLAY_CLASS - ui_task.begin(disp, &sensors, the_mesh.getNodePrefs()); // still want to pass this in as dependency, as prefs might be moved -#endif +// #ifdef DISPLAY_CLASS +// ui_task.begin(disp, &sensors, the_mesh.getNodePrefs()); // still want to pass this in as dependency, as prefs might be moved +// #endif + vTaskResume(t_core1_core); + + Serial.println("Setup completed"); + + the_mesh.advert(); + + Serial.print("MeshCore "); + Serial.println(the_mesh.getFirmwareVer()); + Serial.print("Build date: "); + Serial.println(the_mesh.getBuildDate()); +} + +// void loop() { +// the_mesh.loop(); +// sensors.loop(); +// // #ifdef DISPLAY_CLASS +// // ui_task.loop(); +// // #endif +// rtc_clock.tick(); +// } + +void core_task(void *pvParameters) { + + vTaskSuspend(NULL); + + ESP_LOGI(TAG, "MeshCore: Task running on core %d", xPortGetCoreID()); + + while (1) { + the_mesh.loop(); + sensors.loop(); + rtc_clock.tick(); + vTaskDelay(DELAY_CORE_TASK / portTICK_PERIOD_MS); + } } void loop() { - the_mesh.loop(); - sensors.loop(); -#ifdef DISPLAY_CLASS - ui_task.loop(); -#endif - rtc_clock.tick(); + vTaskDelete(NULL); } + + +void handleCommand(const char* command) { + the_mesh.handleCommand(command); +} \ No newline at end of file diff --git a/examples/companion_radio/task_clock.cpp b/examples/companion_radio/task_clock.cpp new file mode 100644 index 000000000..752a5147c --- /dev/null +++ b/examples/companion_radio/task_clock.cpp @@ -0,0 +1,25 @@ +#include + +#include "esp_log.h" +#include "uiDefines.h" +#include "uiVars.h" + +#define TAG "clock_task" + +void clock_task(void *pvParameters) { + + vTaskSuspend(NULL); + + ESP_LOGI(TAG, "Clock manager: Task running on core %d", xPortGetCoreID()); + + uiManager->clearDateTime(); + + // TODO: sync clock + while (1) { + // uiManager->updateDateTime( + // myClock->getTimeStruct() + // ); + // uiManager->updateValues(); + vTaskDelay(DELAY_CLOCK_TASK / portTICK_PERIOD_MS); + } +} \ No newline at end of file diff --git a/examples/companion_radio/task_lvgl.cpp b/examples/companion_radio/task_lvgl.cpp new file mode 100644 index 000000000..d88b6cd38 --- /dev/null +++ b/examples/companion_radio/task_lvgl.cpp @@ -0,0 +1,19 @@ +#include + +#include "esp_log.h" +#include "uiDefines.h" +#include "uiVars.h" + +#define TAG "lvgl_task" + +void lvgl_task(void *pvParameters) { + + vTaskSuspend(NULL); + + ESP_LOGI(TAG, "UI manager: Task running on core %d", xPortGetCoreID()); + + while (1) { + lv_timer_handler(); + vTaskDelay(DELAY_LVGL_TASK / portTICK_PERIOD_MS); + } +} \ No newline at end of file diff --git a/examples/companion_radio/uiManager.cpp b/examples/companion_radio/uiManager.cpp new file mode 100644 index 000000000..87e5de782 --- /dev/null +++ b/examples/companion_radio/uiManager.cpp @@ -0,0 +1,797 @@ +#include + +#include "esp_log.h" +#include "uiDefines.h" +#include "uiVars.h" + +#include "uiManager.h" + +#include "../src/fonts/fonts.h" + +#include +#include + +#if defined(LANG_GR) +const char *UIManager::days[7] = {"Κυρ", "Δευ", "Τρι", "Τετ", "Πεμ", "Παρ", "Σαβ"}; +const char *UIManager::months[12] = {"Ιαν", "Φεβ", "Μαρ", "Απρ", "Μαι", "Ιουν", + "Ιουλ", "Αυγ", "Σεπ", "Οκτ", "Νοε", "Δεκ"}; +#elif defined(LANG_EN) +const char *UIManager::days[7] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; +const char *UIManager::months[12] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; +#endif + +#define TAG "UIManager" + +extern void handleCommand(char *msg); + +UIManager::UIManager() { + + tmp_buf = (char*)malloc(128); + + lv_disp_t * dispp = lv_disp_get_default(); + lv_theme_t * theme = lv_theme_default_init(dispp, lv_palette_main(LV_PALETTE_BLUE), lv_palette_main(LV_PALETTE_RED), + false, LV_FONT_DEFAULT); + lv_disp_set_theme(dispp, theme); + ui_Screen1_screen_init(); + ui____initial_actions0 = lv_obj_create(NULL); + lv_disp_load_scr(ui_Screen1); + +} + +void UIManager::format_time(uint32_t ts, char *buf, size_t len) +{ + time_t t = ts; + struct tm *tm_info = localtime(&t); + strftime(buf, len, "%H:%M:%S", tm_info); +} + +void UIManager::format_datetime(char *buf, size_t size, const struct tm *timeinfo) { + char tmp[64]; + strftime(tmp, sizeof(tmp), "%a, %d %b %Y", timeinfo); + + int wday = timeinfo->tm_wday; // 0=Κυρ ... 6=Σαβ + int mon = timeinfo->tm_mon; // 0=Ιαν ... 11=Δεκ + + // replace %a and %b with selected language + snprintf(buf, size, "%s, %02d %s %d", days[wday], timeinfo->tm_mday, months[mon], 1900 + timeinfo->tm_year); +} + +void UIManager::updateDateTime(const struct tm timeinfo) { + // TODO: Add to settings "Date format" + char date_str[50]; + format_datetime(date_str, sizeof(date_str), &timeinfo); + lv_label_set_text(ui_ValueDate, date_str); + + // TODO: Add to settings "Hour format" + strftime(tmp_buf, 50, "%H:%M", &timeinfo); // 24h format + //strftime(tmp_buf, 50, "%I:%M %p", &timeinfo); // 12h format + lv_label_set_text(ui_ValueTime, tmp_buf); + + // TODO: Add to settings "dim at night" + // TODO: Add to settings "dim hours" + // TODO: Add to settings "dim percentage" + if (timeinfo.tm_hour > 21 || timeinfo.tm_hour < 7) { + setNightMode(true); + } else { + setNightMode(false); + } +} + +void UIManager::clearDateTime() { + #if defined(LANG_EN) + uiManager->updateInfo("Clock sync...", COLOR_WHITE); + #elif defined(LANG_GR) + uiManager->updateInfo("Συγχρονισμός ώρας...", COLOR_WHITE); + #endif + lv_label_set_text(ui_ValueDate, "--- --/--/----"); + lv_label_set_text(ui_ValueTime, "--:--"); +} + +void UIManager::timestampToTime(time_t timestamp, char *buffer, size_t buffer_size) { + struct tm *time_info; + time_info = localtime(×tamp); + strftime(buffer, buffer_size, "%H:%M", time_info); +} + +const char* UIManager::convertDegreesToDirection(int degrees) { + // Normalize degrees to [0, 360) + degrees = degrees % 360; + if (degrees < 0) degrees += 360; + +#if defined(LANG_EN) + static constexpr const char* dirs[] = {"N", "NE", "E", "SE", "S", "SW", "W", "NW"}; +#elif defined(LANG_GR) + static constexpr const char* dirs[] = {"Β", "ΒΑ", "Α", "ΝΑ", "Ν", "ΝΔ", "Δ", "ΒΔ"}; +#else + #error "No Language defined!" +#endif + + // Each direction covers 45°, starting at N = 0° + int index = static_cast((degrees + 22.5) / 45.0) % 8; + return dirs[index]; +} + + +int UIManager::windSpeedToBeaufort(float speed) { + static const float limits[] = { + 0.5, 1.5, 3.3, 5.5, 7.9, 10.7, + 13.8, 17.1, 20.7, 24.4, 28.4, 32.6 + }; + + for (int i = 0; i < 12; ++i) + if (speed < limits[i]) + return i; + return 12; +} + +void UIManager::updateValues() { + lv_label_set_text(ui_ValueTime, "--:--"); +} + +void UIManager::updateInfo(const char *str, uint32_t color) { + // lv_label_set_text(ui_ValueLastUpdate, str); + // lv_obj_set_style_text_color(ui_ValueLastUpdate, lv_color_hex(color), LV_PART_MAIN | LV_STATE_DEFAULT); +} + +void UIManager::addChatBubble(const char *time_str, const char *sender, const char *msg,bool is_self) +{ + // Remove oldest + if (chat_count >= MAX_CHAT_MESSAGES) { + lv_obj_del(chat_items[0]); + memmove(&chat_items[0], &chat_items[1], sizeof(lv_obj_t*) * (MAX_CHAT_MESSAGES - 1)); + chat_count--; + } + + // Row container (align bubble left/right) + lv_obj_t *row = lv_obj_create(ui_ChannelMessages); + lv_obj_set_width(row, lv_pct(100)); + lv_obj_set_height(row, LV_SIZE_CONTENT); + lv_obj_set_style_bg_opa(row, 0, 0); + lv_obj_set_style_pad_all(row, 0, 0); + lv_obj_set_style_border_width(row, 0, 0); + lv_obj_set_style_outline_width(row, 0, 0); + lv_obj_clear_flag(row, LV_OBJ_FLAG_SCROLLABLE); + + lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(row, + is_self ? LV_FLEX_ALIGN_END : LV_FLEX_ALIGN_START, + LV_FLEX_ALIGN_START, + LV_FLEX_ALIGN_START); + + // Bubble container (COLUMN) + lv_obj_t *bubble = lv_obj_create(row); + lv_obj_set_width(bubble, lv_pct(85)); + lv_obj_set_height(bubble, LV_SIZE_CONTENT); + lv_obj_set_style_radius(bubble, 12, 0); + lv_obj_set_style_pad_all(bubble, 10, 0); + lv_obj_set_style_bg_color(bubble, + is_self ? lv_color_hex(0x1E88E5) : lv_color_hex(0x2C2C2C), 0); + + // IMPORTANT: vertical layout inside bubble + lv_obj_set_flex_flow(bubble, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(bubble, + LV_FLEX_ALIGN_START, + LV_FLEX_ALIGN_START, + LV_FLEX_ALIGN_START); + + // Header row (sender + time) + lv_obj_t *hdr = lv_obj_create(bubble); + lv_obj_set_width(hdr, lv_pct(100)); + lv_obj_set_height(hdr, LV_SIZE_CONTENT); + lv_obj_set_style_bg_opa(hdr, 0, 0); + lv_obj_set_style_border_width(hdr, 0, 0); + lv_obj_set_style_pad_all(hdr, 0, 0); + lv_obj_set_style_outline_width(hdr, 0, 0); + + lv_obj_set_flex_flow(hdr, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(hdr, + LV_FLEX_ALIGN_SPACE_BETWEEN, + LV_FLEX_ALIGN_START, //LV_FLEX_ALIGN_CENTER, // Ευθυγράμμιση ονόματος/ώρας στον κάθετο άξονα + LV_FLEX_ALIGN_START); + + lv_obj_t *lbl_sender = lv_label_create(hdr); + lv_label_set_text(lbl_sender, sender); + lv_obj_set_style_text_color(lbl_sender, + is_self ? lv_color_hex(0xE3F2FD) : lv_color_hex(0x90CAF9), 0); + lv_obj_set_style_text_font(lbl_sender, &lv_font_arial_22, 0); + + lv_obj_t *lbl_time = lv_label_create(hdr); + lv_label_set_text(lbl_time, time_str); + lv_obj_set_style_text_color(lbl_time, lv_color_hex(0xB0B0B0), 0); + lv_obj_set_style_text_font(lbl_time, &lv_font_arial_20, 0); + + // Message body (below header) + lv_obj_t *lbl_msg = lv_label_create(bubble); + lv_label_set_text(lbl_msg, msg); + lv_label_set_long_mode(lbl_msg, LV_LABEL_LONG_WRAP); + lv_obj_set_width(lbl_msg, lv_pct(100)); + + lv_obj_set_style_text_color(lbl_msg, lv_color_hex(0xFFFFFF), 0); + lv_obj_set_style_text_font(lbl_msg, &lv_font_arial_26, 0); + + // Spacing between header and text + //lv_obj_set_style_margin_top(lbl_msg, 6, 0); + lv_obj_set_style_pad_row(bubble, 6, 0); + + chat_items[chat_count++] = row; + + lv_obj_scroll_to_view(row, LV_ANIM_ON); + } + +void UIManager::addPrivateChatBubble(const char *time_str, const char *msg, bool is_self) { + + lv_obj_set_style_pad_bottom(ui_ContactMessages, 20, 0); + + // 1. Row container + lv_obj_t* row = LvObj(ui_ContactMessages) + .width(lv_pct(100)) + .height(LV_SIZE_CONTENT) + .bgOpa(0) + .border(0) + .padAll(4) + .scrollable(false) + .flexFlow(LV_FLEX_FLOW_ROW) + .flexAlign( + is_self ? LV_FLEX_ALIGN_END : LV_FLEX_ALIGN_START, + LV_FLEX_ALIGN_START, + LV_FLEX_ALIGN_START + ); + + // 2. Aligner (Column) - label and time + lv_obj_t* aligner = LvObj(row) + .width(LV_SIZE_CONTENT) + .height(LV_SIZE_CONTENT) + .bgOpa(0) + .border(0) + .padAll(0) + .scrollable(false) + .flexFlow(LV_FLEX_FLOW_ROW) + .flexAlign( + is_self ? LV_FLEX_ALIGN_END : LV_FLEX_ALIGN_START, + LV_FLEX_ALIGN_START, + LV_FLEX_ALIGN_START + ); + lv_obj_set_style_pad_row(aligner, 4, 0); + + // 3. Το Label-Bubble (Ενοποιημένο) + lv_obj_t *lbl_msg = lv_label_create(aligner); + + // Long mode για wrap + lv_label_set_long_mode(lbl_msg, LV_LABEL_LONG_WRAP); + lv_label_set_text(lbl_msg, msg); + lv_obj_set_style_text_font(lbl_msg, &lv_font_arial_22, 0); + + // Fixed max width για wrap + lv_obj_set_width(lbl_msg, 400); // max πλάτος + lv_obj_set_height(lbl_msg, LV_SIZE_CONTENT); // αυτόματο ύψος + + // Bubble style + lv_obj_set_style_bg_opa(lbl_msg, LV_OPA_COVER, 0); + lv_obj_set_style_radius(lbl_msg, 12, 0); + lv_obj_set_style_pad_all(lbl_msg, 12, 0); + + if(is_self) { + lv_obj_set_style_bg_color(lbl_msg, lv_color_hex(0x1E88E5), 0); + lv_obj_set_style_text_color(lbl_msg, lv_color_hex(0xFFFFFF), 0); + } else { + lv_obj_set_style_bg_color(lbl_msg, lv_color_hex(0xFFFFFF), 0); + lv_obj_set_style_text_color(lbl_msg, lv_color_hex(0x000000), 0); + } + + // 4. Η Ώρα + lv_obj_t *lbl_time = lv_label_create(aligner); + lv_label_set_text(lbl_time, time_str); + lv_obj_set_style_text_color(lbl_time, lv_color_hex(0x808080), 0); + lv_obj_set_style_text_font(lbl_time, &lv_font_arial_14, 0); + + lv_obj_scroll_to_view(row, LV_ANIM_ON); +} + +void UIManager::getInitials(const char *name, char *out) { + out[0] = 0; + if (!name || !name[0]) return; + + const char *p = name; + while (*p && !isalnum((unsigned char)*p)) { + p++; + } + + char first = (*p) ? *p : name[0]; + char second = 0; + const char *space = strchr(name, ' '); + + if (space) { + const char *s = space + 1; + while (*s && !isalnum((unsigned char)*s)) { + s++; + } + if (*s) { + second = *s; + } + } + + out[0] = toupper((unsigned char)first); + if (second) { + out[1] = toupper((unsigned char)second); + out[2] = 0; + } else { + out[1] = 0; + } +} + +void UIManager::formatLastSeen(uint32_t ts, char *out, size_t len) { + if (ts == 0) { + snprintf(out, len, "Never"); + return; + } + + time_t t = (time_t)ts; + struct tm *tm = localtime(&t); + + if (tm == nullptr) { + snprintf(out, len, "Unknown"); + return; + } + + snprintf(out, len, "%02d:%02d %02d/%02d/%02d", + tm->tm_hour, + tm->tm_min, + tm->tm_mday, + tm->tm_mon + 1, + (tm->tm_year + 1900) % 100); // Προσθέτουμε το 1900 και παίρνουμε τα τελευταία 2 ψηφία +} + +static void onContactClick(lv_event_t *e) +{ + UIManager *self = (UIManager*) lv_event_get_user_data(e); + if(self) self->handleContactClick(e); +} + +void UIManager::handleContactClick(lv_event_t *e) +{ + lv_obj_t *row = lv_event_get_target(e); + ContactInfo *c = (ContactInfo*) lv_obj_get_user_data(row); + + Serial.printf("Clicked: %s\n", c->name); +} + +void UIManager::addContactToUI(ContactInfo c) +{ + const int ROW_W = 200; + const int ROW_H = 64; + const int AVATAR = 44; + const int PAD = 4; + + // ============================ + // List row button + // ============================ + lv_obj_t* btn = lv_list_add_btn(ui_Contacts, nullptr, nullptr); + + LvObj(btn, true) + .size(ROW_W, ROW_H) + .padAll(0) + .bgColor(0x000000) + .bgOpa(LV_OPA_COVER) + .noDecor() + .border(1) + .scrollable(false); + + lv_obj_set_layout(btn, 0); + lv_obj_set_style_border_side(btn, LV_BORDER_SIDE_BOTTOM, 0); + lv_obj_set_style_border_color(btn, lv_color_hex(0x222222), 0); + lv_obj_set_style_bg_color(btn, lv_color_hex(0x111111), LV_STATE_PRESSED); + + // Store ContactInfo + auto* store = new ContactInfo(c); + lv_obj_set_user_data(btn, store); + + lv_obj_add_event_cb(btn, onContactClick, LV_EVENT_CLICKED, this); + + // ============================ + // Avatar container + // ============================ + lv_obj_t* content = LvObj(btn) + .size(ROW_H, ROW_H) + .position(0, 0) + .padAll(0) + .bgOpa(LV_OPA_TRANSP) + .border(0) + .scrollable(false) + .raw(); + + // Avatar circle + lv_obj_t* avatar = LvObj(content) + .size(AVATAR, AVATAR) + .position(0, 10) + .radius(LV_RADIUS_CIRCLE) + .bgColor(0x4A90E2) + .border(0) + .raw(); + + // Initials + char initials[4]; + if (c.type == ADV_TYPE_REPEATER) { + strcpy(initials, "(R)"); + } else { + getInitials(c.name, initials); + } + + LvLabel(avatar) + .text(initials) + .font(&lv_font_arial_22) + .textColor(0xFFFFFF) + .align(LV_ALIGN_CENTER); + + // ============================ + // Text column + // ============================ + int text_x = PAD + AVATAR + PAD; + int text_w = ROW_W - text_x - PAD; + + lv_obj_t* text_col = LvObj(btn) + .position(text_x, 0) + .size(text_w, ROW_H - PAD) + .padAll(0) + .bgOpa(LV_OPA_TRANSP) + .border(0) + .scrollable(false) + .raw(); + + // Name label + LvLabel(text_col) + .text(c.name) + .position(0, PAD + 4) + .width(text_w) + .font(&lv_font_arial_20) + .textColor(0xFFFFFF) + .wrap(false); + + // Last seen + char lastSeen[32]; + formatLastSeen(c.lastmod, lastSeen, sizeof(lastSeen)); + + LvLabel(text_col) + .text(lastSeen) + .position(0, PAD + 32) + .width(text_w) + .font(&lv_font_arial_16) + .textColor(0x888888) + .wrap(false); + + // ============================ + // Disable child clicks + // ============================ + LvObj(avatar, true).clickable(false); + LvObj(text_col, true).clickable(false); +} + +void UIManager::onShowKeyboard() +{ + LvKeyboard(ui_Keyboard, true).show(true); + LvObj(ui_DimOverlay, true).clickable(true); + LvObj(ui_ChannelInput, true).positionY(channelInputBaseKeybOnY); + LvObj(ui_SendBtn, true).positionY(channelInputBaseKeybOnY); +} + +void UIManager::onHideKeyboard() +{ + LvKeyboard(ui_Keyboard, true).show(false); + LvObj(ui_DimOverlay, true).clickable(false); + LvObj(ui_ChannelInput, true).positionY(channelInputBaseY); + LvObj(ui_SendBtn, true).positionY(channelInputBaseY); +} + +static void s_onChannelInputFocus(lv_event_t *e) +{ + UIManager *self = (UIManager*) lv_event_get_user_data(e); + if(self) self->onChannelInputFocus(e); +} + +void UIManager::onChannelInputFocus(lv_event_t* e) +{ + lv_obj_t* ta = lv_event_get_target(e); + if(!ui_Keyboard || !ta) return; + + lv_keyboard_set_textarea(ui_Keyboard, ta); + onShowKeyboard(); +} + +static void s_onSendClick(lv_event_t *e) +{ + UIManager *self = (UIManager*) lv_event_get_user_data(e); + if(self) self->onSendClick(e); +} + +void UIManager::onSendClick(lv_event_t* e) +{ + + char fullMessage[260]; + char msgCopy[200]; + + const char* msg = lv_textarea_get_text(ui_ChannelInput); + if(msg == NULL || msg[0] == '\0') return; + + strncpy(msgCopy, msg, sizeof(msgCopy) - 1); + msgCopy[sizeof(msgCopy) - 1] = '\0'; + + lv_textarea_set_text(ui_ChannelInput, ""); + + snprintf(fullMessage, sizeof(fullMessage), "public %s", msgCopy); + //handleCommand(fullMessage); + handleCommand(msgCopy); + + char time_buf[16]; + time_t now = time(NULL); + struct tm t; + localtime_r(&now, &t); + + sprintf(time_buf, "%02d:%02d:%02d\n", t.tm_hour, t.tm_min, t.tm_sec); + addChatBubble(time_buf, "Me", msgCopy, true); + + onHideKeyboard(); +} + +static void s_onKeyboardEvent(lv_event_t *e) +{ + UIManager *self = (UIManager*) lv_event_get_user_data(e); + if(self) self->onKeyboardEvent(e); +} + +void UIManager::onKeyboardEvent(lv_event_t* e) +{ + lv_event_code_t code = lv_event_get_code(e); + + if(code == LV_EVENT_READY || code == LV_EVENT_CANCEL) + { + LvKeyboard(ui_Keyboard, true).show(false); + + LvObj(ui_ChannelInput, true).positionY(channelInputBaseY); + LvObj(ui_SendBtn, true).positionY(channelInputBaseY); + + LvObj(ui_DimOverlay, true) + .bgOpa(0) + .clickable(false); + } +} + + +static void s_onDimOverlayClick(lv_event_t *e) +{ + UIManager *self = (UIManager*) lv_event_get_user_data(e); + if(self) self->onDimOverlayClick(e); +} + +void UIManager::onDimOverlayClick(lv_event_t* e) +{ + onHideKeyboard(); +} + +static void onScrollBeginEvent(lv_event_t *e) +{ + UIManager *self = (UIManager*) lv_event_get_user_data(e); + if(self) self->scroll_begin_event(e); +} + +void UIManager::scroll_begin_event(lv_event_t *e) +{ + if (lv_event_get_code(e) == LV_EVENT_SCROLL_BEGIN) { + lv_anim_t* a = (lv_anim_t*)lv_event_get_param(e); + if (a) a->time = 0; + + } +} + +void UIManager::ui_Screen1_screen_init(void) +{ + //lv_disp_set_rotation(disp, LV_DISP_ROT_90); + //ui_Screen1 = lv_obj_create(NULL); + + ui_Screen1 = LvObj(NULL) + .scrollable(false) + .bgColor(0x000000) + .bgOpa(255); + + LvTabView tabView(ui_Screen1); + tabView + .size(480, 480) + .align(LV_ALIGN_CENTER) + .bgColor(0x000000) + .contentNoScroll() + .tabBtnBg(0x424242) + .tabBtnText(0xFFFFFF, &lv_font_arial_18); + + ui_TabView1 = tabView.raw(); + + #if defined(LANG_EN) + ui_TabPageHome = ui_TabView1.addTab("Home"); + ui_TabPageContacts = ui_TabView1.addTab("Contacts"); + ui_TabPageChannels = ui_TabView1.addTab("Channels"); + ui_TabPageSettings = ui_TabView1.addTab("Settings"); + #elif defined(LANG_GR) + ui_TabPageHome = tabView.addTab("Αρχική"); + ui_TabPageContacts = tabView.addTab("Επαφές"); + ui_TabPageChannels = tabView.addTab("Κανάλια"); + ui_TabPageSettings = tabView.addTab("Ρυθμίσεις"); + #endif + + LvObj(ui_TabPageHome) + .scrollable(false) + .bgOpa(0) + .bgColor(0x000000); + LvObj(ui_TabPageContacts) + .scrollable(false) + .bgOpa(0) + .bgColor(0x000000); + LvObj(ui_TabPageChannels) + .scrollable(false) + .bgOpa(0) + .bgColor(0x000000); + LvObj(ui_TabPageSettings) + .scrollable(false) + .bgOpa(0) + .bgColor(0x000000); + + ui_ValueDate = LvLabel(ui_TabPageHome) + .text("--- --/--/----") + .width(LV_SIZE_CONTENT) + .height(LV_SIZE_CONTENT) + .font(&lv_font_arial_40) + .textColor(0xFFFFFF) + .opa(255) + .align(LV_ALIGN_CENTER) + .position(0, -165); + + + ui_ValueTime = LvLabel(ui_TabPageHome) + .text("--:--") + .width(LV_SIZE_CONTENT) + .height(LV_SIZE_CONTENT) + .font(&lv_font_arial_48) + .textColor(0xFFFFFF) + .opa(255) + .align(LV_ALIGN_CENTER) + .position(0, -100); + + ui_Contacts = LvList(ui_TabPageContacts) + .width(250) + .height(400) + .align(LV_ALIGN_CENTER) + .position(-274, 0) + .transparent() + .raw(); + + lv_obj_set_style_bg_opa(ui_Contacts, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(ui_Contacts, 0, 0); + lv_obj_set_style_outline_width(ui_Contacts, 0, 0); + lv_obj_set_style_shadow_width(ui_Contacts, 0, 0); + //lv_obj_set_scrollbar_mode(ui_Contacts, LV_SCROLLBAR_MODE_OFF); + lv_obj_set_style_bg_opa(ui_Contacts, LV_OPA_TRANSP, LV_PART_ITEMS); + lv_obj_set_style_border_width(ui_Contacts, 0, LV_PART_ITEMS); + + ui_ContactMessages = LvList(ui_TabPageContacts) + .width(500) + .height(400) + .align(LV_ALIGN_CENTER) + .position(124, 0) + .transparent() + .raw(); + + lv_obj_set_style_bg_color(ui_ContactMessages, lv_color_hex(0), 0); + lv_obj_set_style_bg_opa(ui_ContactMessages, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(ui_ContactMessages, 0, 0); + lv_obj_set_style_outline_width(ui_ContactMessages, 0, 0); + lv_obj_set_style_shadow_width(ui_ContactMessages, 0, 0); + //lv_obj_set_scrollbar_mode(ui_ContactMessages, LV_SCROLLBAR_MODE_OFF); + lv_obj_set_style_bg_opa(ui_ContactMessages, LV_OPA_TRANSP, LV_PART_ITEMS); + lv_obj_set_style_border_width(ui_ContactMessages, 0, LV_PART_ITEMS); + + // LvObj(ui_TabPageContacts) + // .size(2, 400) + // .position(222, 0) + // .bgColor(0x444444) + // .border(0) + // .scrollable(false) + // .radius(0); + + ui_Channels = LvDropdown(ui_TabPageChannels) + .options("Public") + .width(291) + .align(LV_ALIGN_CENTER) + .position(-243, -182) + .clickable(true) + .raw(); + + ui_ChannelMessages = LvList(ui_TabPageChannels) + .width(780) + .height(280) + .align(LV_ALIGN_CENTER) + .transparent() + .padRow(10) + .position(0, 0) + .bgColor(0) + .bgOpa(0) + .border(0) + .noDecor() + .raw(); + + //lv_obj_set_scrollbar_mode(ui_ChannelMessages, LV_SCROLLBAR_MODE_OFF); + lv_obj_set_style_bg_opa(ui_ChannelMessages, LV_OPA_TRANSP, LV_PART_ITEMS); + lv_obj_set_style_border_width(ui_ChannelMessages, 0, LV_PART_ITEMS); + + ui_ChannelDivider = LvObj(ui_TabPageChannels) + .size(780, 1) + .align(LV_ALIGN_CENTER) + .position(0, 150) + .bgColor(0x444444) + .border(0) + .raw(); + + ui_DimOverlay = LvObj(ui_Screen1) + .size(lv_pct(100), lv_pct(100)) + .align(LV_ALIGN_CENTER) + .bgColor(0x000000) + .bgOpa(0) + .bringToFront() + .onClick(s_onDimOverlayClick, this) + .scrollable(false) + .clickable(true) + .raw(); + lv_obj_remove_style_all(ui_DimOverlay); // no border/padding + lv_obj_clear_flag(ui_DimOverlay, LV_OBJ_FLAG_SCROLL_CHAIN_HOR); + lv_obj_clear_flag(ui_DimOverlay, LV_OBJ_FLAG_SCROLL_CHAIN_VER); + + + ui_ChannelInput = LvTextArea(ui_TabPageChannels) + .size(670, 40) + .align(LV_ALIGN_CENTER) + .position(-50, channelInputBaseY) + .oneLine(true) + #if defined(LANG_EN) + .placeholder("Write message...") + #elif defined(LANG_GR) + .placeholder("Γράψε μήνυμα...") + #endif + .font(&lv_font_arial_20) + .bgColor(0x111111) + .textColor(0xFFFFFF) + .borderColor(0x444444) + .borderWidth(1) + .radius(6) + .onFocus(s_onChannelInputFocus, this) + .raw(); + + ui_SendBtn = LvButton(ui_TabPageChannels) + .size(90, 42) + .align(LV_ALIGN_CENTER) + .position(350, channelInputBaseY) + .bgColor(0x3A7AFE) + .onClick(s_onSendClick, this) + .raw(); + + iu_SendLabel = LvLabel(ui_SendBtn) + #if defined(LANG_EN) + .text("Send") + #elif defined(LANG_GR) + .text("Αποστολή") + #endif + .font(&lv_font_arial_18); + lv_obj_center(iu_SendLabel); + + ui_Keyboard = LvKeyboard(lv_layer_top()) + .size(480, 200) + .align(LV_ALIGN_BOTTOM_MID) + .show(false) + .onEvent(s_onKeyboardEvent, this); +} + +void UIManager::setNightMode(bool night) { + + if (!ui_DimOverlay) return; + if (night) { + lv_obj_set_style_bg_opa(ui_DimOverlay, 192, 0); // 75% dark + } else { + lv_obj_set_style_bg_opa(ui_DimOverlay, 0, 0); // none + } +} + diff --git a/examples/companion_radio/uiTasks.cpp b/examples/companion_radio/uiTasks.cpp new file mode 100644 index 000000000..b55220496 --- /dev/null +++ b/examples/companion_radio/uiTasks.cpp @@ -0,0 +1,50 @@ +#include + +#include "esp_log.h" +#include "uiDefines.h" +#include "uiTasks.h" +#include "uiVars.h" + +// Tasks +TaskHandle_t t_core0_lvgl; +TaskHandle_t t_core1_clock; +TaskHandle_t t_core1_core; + +#define TAG "createTasks" + +void createTasks() { + Serial.println("Creating Tasks..."); + + xTaskCreatePinnedToCore( + lvgl_task, // Task function. + "LVGL_Manager", // Name of task. + 10000, // Stack size of task + NULL, // Parameter of the task + 5, // Priority of the task + &t_core0_lvgl, // Task handle to keep track of created task + 0); // Pin task to core 0 + + xTaskCreatePinnedToCore( + core_task, // Task function. + "MeshCore", // Name of task. + 10000, // Stack size of task + NULL, // Parameter of the task + 4, // Priority of the task + &t_core1_core, // Task handle to keep track of created task + 1); // Pin task to core 1 + + xTaskCreatePinnedToCore( + clock_task, // Task function. + "CLOCK_Manager", // Name of task. + 10000, // Stack size of task + NULL, // Parameter of the task + 4, // Priority of the task + &t_core1_clock, // Task handle to keep track of created task + 1); // Pin task to core 1 + + ESP_LOGD(TAG, "All tasks created\nStarting tasks..."); + + vTaskResume(t_core0_lvgl); + vTaskResume(t_core1_clock); + +} diff --git a/examples/simple_secure_chat_ui/uiManager.cpp b/examples/simple_secure_chat_ui/uiManager.cpp index 3c6db7053..d6380e1fb 100644 --- a/examples/simple_secure_chat_ui/uiManager.cpp +++ b/examples/simple_secure_chat_ui/uiManager.cpp @@ -23,7 +23,7 @@ const char *UIManager::months[12] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", #define TAG "UIManager" -extern void handleCommand(char *msg); +//extern void handleCommand(char *msg); UIManager::UIManager() { @@ -517,7 +517,7 @@ void UIManager::onSendClick(lv_event_t* e) lv_textarea_set_text(ui_ChannelInput, ""); snprintf(fullMessage, sizeof(fullMessage), "public %s", msgCopy); - handleCommand(fullMessage); + //handleCommand(fullMessage); char time_buf[16]; time_t now = time(NULL); diff --git a/variants/sensecap_indicator-espnow/platformio.ini b/variants/sensecap_indicator-espnow/platformio.ini index 1b33bc09f..07694149f 100644 --- a/variants/sensecap_indicator-espnow/platformio.ini +++ b/variants/sensecap_indicator-espnow/platformio.ini @@ -51,7 +51,8 @@ build_flags = ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 ; NOTE: DO NOT ENABLE --> -D ESPNOW_DEBUG_LOGGING=1 build_src_filter = ${SenseCapIndicator-ESPNow.build_src_filter} - +<../examples/simple_secure_chat_ui/*.cpp> + +<../examples/companion_radio/*.cpp> + ;+<../examples/simple_secure_chat_ui/*.cpp> + + lib_deps =