From e6ea30fb687c66645dea0f618dcb624c12ab543b Mon Sep 17 00:00:00 2001 From: Juan Pena Date: Mon, 26 Jan 2026 23:28:12 -0500 Subject: [PATCH 01/14] Autohide battery and hops --- source/graphics/TFT/TFTView_320x240.cpp | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/source/graphics/TFT/TFTView_320x240.cpp b/source/graphics/TFT/TFTView_320x240.cpp index e65e60bf..ebba6331 100644 --- a/source/graphics/TFT/TFTView_320x240.cpp +++ b/source/graphics/TFT/TFTView_320x240.cpp @@ -169,7 +169,8 @@ void TFTView_320x240::init(IClientBase *client) ui_init_boot(); FileLoader::init(&fileSystem); - FileLoader::loadBootImage(objects.boot_logo); + if (!FileLoader::loadBootImage(objects.boot_logo)) + lv_image_set_src(objects.boot_logo, &img_meshtastic_boot_logo_image); // if boot logo is too big remove the label and center the image lv_obj_update_layout(objects.boot_logo); if (lv_obj_get_height(objects.boot_logo) > lv_display_get_vertical_resolution(displaydriver->getDisplay()) / 2) { @@ -918,7 +919,7 @@ void TFTView_320x240::timer_event_shutdown(lv_timer_t *timer) THIS->controller->stop(); delay(1000); #if defined(ARCH_PORTDUINO) - exit(0); + exit(2); #elif defined(ARCH_ESP32) esp_deep_sleep_start(); #else @@ -4685,6 +4686,15 @@ void TFTView_320x240::addNode(uint32_t nodeNum, uint8_t ch, const char *userShor lv_obj_set_style_align(ui_Telemetry2Label, LV_ALIGN_TOP_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_text_align(ui_Telemetry2Label, LV_TEXT_ALIGN_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); + // optimisation: hide all 6ix extended labels by default; enable only when set + // lv_obj_add_flag(ui_lastHeardLabel, LV_OBJ_FLAG_HIDDEN); // lastHeard + lv_obj_add_flag(ui_BatteryLabel, LV_OBJ_FLAG_HIDDEN); // hasKey + lv_obj_add_flag(ui_SignalLabel, LV_OBJ_FLAG_HIDDEN); // update + lv_obj_add_flag(ui_PositionLabel, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(ui_Position2Label, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(ui_Telemetry1Label, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(ui_Telemetry2Label, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_event_cb(nodeButton, ui_event_NodeButton, LV_EVENT_ALL, (void *)nodeNum); // move node into new position within nodePanel @@ -4882,6 +4892,8 @@ void TFTView_320x240::updatePosition(uint32_t nodeNum, int32_t lat, int32_t lon, // store lat/lon in user_data, because we need these values later to calculate the distance to us panel->LV_OBJ_IDX(node_pos1_idx)->user_data = (void *)lat; panel->LV_OBJ_IDX(node_pos2_idx)->user_data = (void *)lon; + lv_obj_remove_flag(panel->LV_OBJ_IDX(node_pos1_idx), LV_OBJ_FLAG_HIDDEN); + lv_obj_remove_flag(panel->LV_OBJ_IDX(node_pos2_idx), LV_OBJ_FLAG_HIDDEN); } applyNodesFilter(nodeNum); @@ -4991,6 +5003,7 @@ void TFTView_320x240::updateMetrics(uint32_t nodeNum, uint32_t bat_level, float bat_level = std::min(bat_level, (uint32_t)100); sprintf(buf, "%d%% %0.2fV", bat_level, voltage); lv_label_set_text(it->second->LV_OBJ_IDX(node_bat_idx), buf); + lv_obj_remove_flag(it->second->LV_OBJ_IDX(node_bat_idx), LV_OBJ_FLAG_HIDDEN); } } } @@ -5016,11 +5029,13 @@ void TFTView_320x240::updateEnvironmentMetrics(uint32_t nodeNum, const meshtasti } } lv_label_set_text(it->second->LV_OBJ_IDX(node_tm1_idx), buf); + lv_obj_remove_flag(it->second->LV_OBJ_IDX(node_tm1_idx), LV_OBJ_FLAG_HIDDEN); if (metrics.iaq > 0 && metrics.iaq < 1000) { sprintf(buf, "IAQ: %d %.1fV %.1fmA", metrics.iaq, metrics.voltage, metrics.current); lv_label_set_text(it->second->LV_OBJ_IDX(node_tm2_idx), buf); it->second->LV_OBJ_IDX(node_tm2_idx)->user_data = (void *)(uint32_t)metrics.iaq; + lv_obj_remove_flag(it->second->LV_OBJ_IDX(node_tm2_idx), LV_OBJ_FLAG_HIDDEN); } applyNodesFilter(nodeNum); } @@ -5064,6 +5079,7 @@ void TFTView_320x240::updateSignalStrength(uint32_t nodeNum, int32_t rssi, float } lv_label_set_text(it->second->LV_OBJ_IDX(node_sig_idx), buf); it->second->LV_OBJ_IDX(node_sig_idx)->user_data = 0; + lv_obj_remove_flag(it->second->LV_OBJ_IDX(node_sig_idx), LV_OBJ_FLAG_HIDDEN); } } } @@ -5077,6 +5093,7 @@ void TFTView_320x240::updateHopsAway(uint32_t nodeNum, uint8_t hopsAway) sprintf(buf, _("hops: %d"), (int)hopsAway); lv_label_set_text(it->second->LV_OBJ_IDX(node_sig_idx), buf); it->second->LV_OBJ_IDX(node_sig_idx)->user_data = (void *)(unsigned long)hopsAway; + lv_obj_remove_flag(it->second->LV_OBJ_IDX(node_sig_idx), LV_OBJ_FLAG_HIDDEN); } } } From a9cc239fd55dc2507e156d8434ec5fe17c6ee4ee Mon Sep 17 00:00:00 2001 From: Juan Pena Date: Mon, 26 Jan 2026 23:35:34 -0500 Subject: [PATCH 02/14] Theme Update 1 --- generated/ui_320x240/screens.c | 10 ++-- source/graphics/TFT/TFTView_320x240.cpp | 4 +- source/graphics/TFT/Themes.cpp | 66 ++++++++++++------------- 3 files changed, 42 insertions(+), 38 deletions(-) diff --git a/generated/ui_320x240/screens.c b/generated/ui_320x240/screens.c index 1900cabf..69ddcfac 100644 --- a/generated/ui_320x240/screens.c +++ b/generated/ui_320x240/screens.c @@ -915,7 +915,8 @@ void create_screen_main_screen() { lv_label_set_text(obj, ""); lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_style_align(obj, LV_ALIGN_TOP_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_align(obj, LV_TEXT_ALIGN_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_align(obj, LV_TEXT_ALIGN_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_color(obj, lv_color_hex(0xff5ded96), LV_PART_MAIN | LV_STATE_DEFAULT); // Changes Signal Color to green } { // PositionLabel @@ -927,7 +928,7 @@ void create_screen_main_screen() { lv_label_set_text(obj, ""); lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_style_align(obj, LV_ALIGN_TOP_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_color(obj, lv_color_hex(0xff05f6cb), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_color(obj, lv_color_hex(0xff05f6cb), LV_PART_MAIN | LV_STATE_DEFAULT); } { // Position2Label @@ -938,7 +939,7 @@ void create_screen_main_screen() { lv_label_set_long_mode(obj, LV_LABEL_LONG_SCROLL); lv_label_set_text(obj, ""); lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_align(obj, LV_ALIGN_TOP_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_align(obj, LV_ALIGN_TOP_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); } { // Telemetry1Label @@ -1263,6 +1264,9 @@ void create_screen_main_screen() { lv_obj_set_style_pad_left(obj, 3, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_max_height(obj, 25, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_pad_bottom(obj, 2, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(obj, lv_color_hex(0xff1f2a37), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_color(obj, lv_color_hex(0xffffffff), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_color(obj, lv_color_hex(0xff1f2a37), LV_PART_MAIN | LV_STATE_DEFAULT); } { // KeyboardButton_0 diff --git a/source/graphics/TFT/TFTView_320x240.cpp b/source/graphics/TFT/TFTView_320x240.cpp index ebba6331..74fade5b 100644 --- a/source/graphics/TFT/TFTView_320x240.cpp +++ b/source/graphics/TFT/TFTView_320x240.cpp @@ -4688,8 +4688,8 @@ void TFTView_320x240::addNode(uint32_t nodeNum, uint8_t ch, const char *userShor // optimisation: hide all 6ix extended labels by default; enable only when set // lv_obj_add_flag(ui_lastHeardLabel, LV_OBJ_FLAG_HIDDEN); // lastHeard - lv_obj_add_flag(ui_BatteryLabel, LV_OBJ_FLAG_HIDDEN); // hasKey - lv_obj_add_flag(ui_SignalLabel, LV_OBJ_FLAG_HIDDEN); // update + lv_obj_add_flag(ui_BatteryLabel, LV_OBJ_FLAG_HIDDEN); // Autohide battery + lv_obj_add_flag(ui_SignalLabel, LV_OBJ_FLAG_HIDDEN); // Autohide signal/hops lv_obj_add_flag(ui_PositionLabel, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(ui_Position2Label, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(ui_Telemetry1Label, LV_OBJ_FLAG_HIDDEN); diff --git a/source/graphics/TFT/Themes.cpp b/source/graphics/TFT/Themes.cpp index 71521443..1361e487 100644 --- a/source/graphics/TFT/Themes.cpp +++ b/source/graphics/TFT/Themes.cpp @@ -95,84 +95,84 @@ enum ThemeColor { uint32_t themeColor[][2] = { // dark, light - {0xff303030, 0xfff4f4f4}, // eMainScreenStyle - {0xff436C70, 0xff67ea94}, // eTopPanelBg + {0xff020712, 0xfff4f4f4}, // eMainScreenStyle + {0xff0f1423, 0xff67ea94}, // eTopPanelBg {0xffE0E0E0, 0xff212121}, // eTopPanelText - {0xff436C70, 0xff67ea94}, // eTopImageBg + {0xff0f1423, 0xff67ea94}, // eTopImageBg {0xffffffff, 0xff212121}, // eTopImageRecolor {255, 255}, // eTopImageRecolorOpa {0xffffffff, 0xff212121}, // ePositiveImageRecolor, - {0xff303030, 0xfff4f4f0}, // ePanelBg - {0xff303030, 0xfffafafa}, // ePanelPressedBg + {0xff020712, 0xfff4f4f0}, // ePanelBg + {0xff020712, 0xfffafafa}, // ePanelPressedBg {0xfff0f0f0, 0xff212121}, // ePanelText - {0xff67ea94, 0xff67ea94}, // ePanelBorder - {0xff404040, 0xffffffff}, // eNodePanelBg - {0xff808080, 0xff979797}, // eNodePanelBorder + {0xff020712, 0xff67ea94}, // ePanelBorder + {0xff111724, 0xffffffff}, // eNodePanelBg changed for better updates + {0xff111724, 0xff979797}, // eNodePanelBorder {0xfff0f0f0, 0xff212121}, // eNodePanelText - {0xff404040, 0xffffffff}, // eNodeButtonBg + {0xff111724, 0xffffffff}, // eNodeButtonBg {0, 0}, // eNodeButtonBgOpa - {0xff585858, 0xffffffff}, // eButtonPanelBg - {0xff585858, 0xffeaeae0}, // eMainButtonBg + {0xff0a0f1c, 0xffffffff}, // eButtonPanelBg + {0xff0a0f1c, 0xffeaeae0}, // eMainButtonBg {0xffaafbff, 0xff101010}, // eMainButtonText {0xff67ea94, 0xff67ea94}, // eMainButtonBorder {0xff9e9e9e, 0xffc0c0c0}, // eMainButtonShadow {0xff67ea94, 0xff757575}, // eMainButtonImageRecolor {0, 255}, // eMainButtonImageRecolorOpa - {0xff303030, 0xfffafaf4}, // eHomeContainerBg + {0xff020712, 0xfffafaf4}, // eHomeContainerBg {0xff67EA94, 0xffaaaaaa}, // eHomeContainerBorder {0xff2B824A, 0xff999999}, // eHomeContainerShadow {0xffaafbff, 0xff294337}, // eHomeContainerText - {0xff303030, 0xffffffff}, // eHomeButtonBg + {0xff020712, 0xffffffff}, // eHomeButtonBg {0xffffffff, 0xff101010}, // eHomeButtonText - {0xff303030, 0xffd0d0d0}, // eHomeButtonBorder + {0xff020712, 0xffd0d0d0}, // eHomeButtonBorder {0xff606060, 0xff57a6b3}, // eHomeButtonImageRecolor {0, 255}, // eHomeButtonImageRecolorOpa - {0xff404040, 0xfffafaf4}, // eChannelButtonBg + {0xff111724, 0xfffafaf4}, // eChannelButtonBg {0xffA0A0A0, 0xffD0D0D0}, // eChannelButtonBorder {0xffffffff, 0xff101010}, // eChannelButtonText - {0xff303030, 0xfff0f0f0}, // eSettingsPanelBg + {0xff020712, 0xfff0f0f0}, // eSettingsPanelBg {0xffaafbff, 0xff003c9f}, // eSettingsPanelText {0, 0xff979797}, // eSettingsPanelBorder {0, 0xff7e7e7e}, // eSettingsPanelShadow {250, 250}, // eSettingsPanelBgOpa - {0xff505050, 0xffeaeae0}, // eSettingsButtonBg + {0xff111724, 0xffeaeae0}, // eSettingsButtonBg {0xffaafbff, 0xff294337}, // eSettingsButtonText - {0xff303030, 0xffd0d0d0}, // eSettingsButtonBorder + {0xff111724, 0xffd0d0d0}, // eSettingsButtonBorder {0, 0xff67ea94}, // eSettingsButtonImageRecolor {0, 255}, // eSettingsButtonImageRecolorOpa - {0xff404040, 0xffffffff}, // eSettingsLabelBg - {0xff404040, 0xff808080}, // eSettingsLabelBorder - {0xff303030, 0xfff4f4f4}, // eTabViewBg + {0xff111724, 0xffffffff}, // eSettingsLabelBg + {0xff111724, 0xff808080}, // eSettingsLabelBorder + {0xff020712, 0xfff4f4f4}, // eTabViewBg {0xffaafbff, 0xff003c9f}, // eTabViewText - {0xff303030, 0xffe0e0e0}, // eTabButtonDefaultBg - {0xff303030, 0xffffffff}, // eTabButtonActiveBg + {0xff020712, 0xffe0e0e0}, // eTabButtonDefaultBg + {0xff020712, 0xffffffff}, // eTabButtonActiveBg {0xff67ea94, 0xffaafbff}, // eTabButtonPressedBg {0xffA0A0A0, 0xff606060}, // eTabButtonDefaultText {0xffffffff, 0xff101010}, // eTabButtonActiveText {0xffffffff, 0xffffffff}, // eTabButtonPressedText {0xff505050, 0xffb0b0b0}, // eTabButtonDefaultBorder - {0xff303030, 0xfffbfce9}, // eChatMessageBg + {0xff111724, 0xfffbfce9}, // eChatMessageBg {255, 255}, // eChatMessageBgOpa {0xffffffff, 0xff294337}, // eChatMessageText - {0xff707070, 0xff888888}, // eChatMessageBorder - {0xff404040, 0xffffffff}, // eNewMessageBg + {0xff111724, 0xff888888}, // eChatMessageBorder + {0xff111724, 0xffffffff}, // eNewMessageBg {255, 255}, // eNewMessageBgOpa {0xffd0d0d0, 0xff294337}, // eNewMessageText - {0xff808080, 0xff888888}, // eNewMessageBorder - {0xff303030, 0xfffbfbfb}, // eAlertPanelBg - {0xff303030, 0xfff4f4f4}, // eBtnMatrixBorderMain + {0xff111724, 0xff888888}, // eNewMessageBorder + {0xff020712, 0xfffbfbfb}, // eAlertPanelBg + {0xff020712, 0xfff4f4f4}, // eBtnMatrixBorderMain {0xff67ea94, 0xff67ea94}, // eBtnMatrixBorderItems {0xff606060, 0xfffffff8}, // eBtnMatrixBgItems {0xffaafbff, 0xff212121}, // eBtnMatrixTextItems - {0xffaafbff, 0xff212121}, // eBatteryPercentageText + {0xff5ded96, 0xff212121}, // eBatteryPercentageText // changed {0xffaafbff, 0xff003c9f}, // eColorTextLabel {0xff404040, 0xffe0e0e0}, // eSpinnerMainArc {0xff67ea94, 0xff67ea94}, // eSpinnerIndicatorArc {0xffaafbff, 0xff212121}, // eTableHeadingText, - {0xff303030, 0xfff4f4f0}, // eTableHeadingBg + {0xff020712, 0xfff4f4f0}, // eTableHeadingBg {0xffaafbff, 0xff212121}, // eTableItemText, - {0xff505050, 0xfff4f4f0}, // eTableItemBg - {0xff303030, 0xffd4d4d0}, // eTableItemDarkBg + {0xff111724, 0xfff4f4f0}, // eTableItemBg + {0xff020712, 0xffd4d4d0}, // eTableItemDarkBg {0xff404040, 0xffe0e0e0}, // eTableBorder {0xff404040, 0xffe0e0e0} // eTableCellBorder }; From 5799d9bdbb05a27b52474adcbe8b1eec0910b097 Mon Sep 17 00:00:00 2001 From: Juan Pena Date: Tue, 27 Jan 2026 18:18:24 -0500 Subject: [PATCH 03/14] Enhance README with additional library description Added a description emphasizing the faster and more streamlined nature of the library. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 50f9d3e3..4c0ed87c 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@

Meshtastic device-ui library

A versatile UI library for the meshtastic® project

+

This is meant to be a faster and more cut down version

From 5e46bb8094b6277c279e0a4818e5dacde8eaeed0 Mon Sep 17 00:00:00 2001 From: Juan Pena Date: Tue, 27 Jan 2026 18:33:02 -0500 Subject: [PATCH 04/14] Update README with T-DECK details and credit Add additional information about T-DECK and credits. --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 4c0ed87c..dd70c94d 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,9 @@

Meshtastic device-ui library

A versatile UI library for the meshtastic® project

This is meant to be a faster and more cut down version

+

ONLY FOR T-DECK & T-DECK PLUS

+

This has be edited somewhat by me @limitlezz

+

Real Credit goes to the Meshtastic UI Devs

From 38d6c0eb92db8fe3e5ea3392be81961bd3407042 Mon Sep 17 00:00:00 2001 From: Juan Pena Date: Tue, 27 Jan 2026 19:15:39 -0500 Subject: [PATCH 05/14] Theme Update 2 --- source/graphics/TFT/Themes.cpp | 62 +++++++++++++++++----------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/source/graphics/TFT/Themes.cpp b/source/graphics/TFT/Themes.cpp index 1361e487..28f4e112 100644 --- a/source/graphics/TFT/Themes.cpp +++ b/source/graphics/TFT/Themes.cpp @@ -96,7 +96,7 @@ enum ThemeColor { uint32_t themeColor[][2] = { // dark, light {0xff020712, 0xfff4f4f4}, // eMainScreenStyle - {0xff0f1423, 0xff67ea94}, // eTopPanelBg + {0xff0f1423, 0xff67ea94}, // eTopPanelBg 0d0f1b {0xffE0E0E0, 0xff212121}, // eTopPanelText {0xff0f1423, 0xff67ea94}, // eTopImageBg {0xffffffff, 0xff212121}, // eTopImageRecolor @@ -106,10 +106,10 @@ uint32_t themeColor[][2] = { {0xff020712, 0xfffafafa}, // ePanelPressedBg {0xfff0f0f0, 0xff212121}, // ePanelText {0xff020712, 0xff67ea94}, // ePanelBorder - {0xff111724, 0xffffffff}, // eNodePanelBg changed for better updates - {0xff111724, 0xff979797}, // eNodePanelBorder + {0xff141723, 0xffffffff}, // eNodePanelBg BG of the whole pannel + {0xff141723, 0xff979797}, // eNodePanelBorder {0xfff0f0f0, 0xff212121}, // eNodePanelText - {0xff111724, 0xffffffff}, // eNodeButtonBg + {0xff141723, 0xffffffff}, // eNodeButtonBg {0, 0}, // eNodeButtonBgOpa {0xff0a0f1c, 0xffffffff}, // eButtonPanelBg {0xff0a0f1c, 0xffeaeae0}, // eMainButtonBg @@ -127,52 +127,52 @@ uint32_t themeColor[][2] = { {0xff020712, 0xffd0d0d0}, // eHomeButtonBorder {0xff606060, 0xff57a6b3}, // eHomeButtonImageRecolor {0, 255}, // eHomeButtonImageRecolorOpa - {0xff111724, 0xfffafaf4}, // eChannelButtonBg - {0xffA0A0A0, 0xffD0D0D0}, // eChannelButtonBorder + {0xff141723, 0xfffafaf4}, // eChannelButtonBg //CHAT Button BG + {0xff141723, 0xffD0D0D0}, // eChannelButtonBorder {0xffffffff, 0xff101010}, // eChannelButtonText {0xff020712, 0xfff0f0f0}, // eSettingsPanelBg {0xffaafbff, 0xff003c9f}, // eSettingsPanelText {0, 0xff979797}, // eSettingsPanelBorder {0, 0xff7e7e7e}, // eSettingsPanelShadow {250, 250}, // eSettingsPanelBgOpa - {0xff111724, 0xffeaeae0}, // eSettingsButtonBg + {0xff141723, 0xffeaeae0}, // eSettingsButtonBg {0xffaafbff, 0xff294337}, // eSettingsButtonText - {0xff111724, 0xffd0d0d0}, // eSettingsButtonBorder + {0xff020712, 0xffd0d0d0}, // eSettingsButtonBorder {0, 0xff67ea94}, // eSettingsButtonImageRecolor {0, 255}, // eSettingsButtonImageRecolorOpa - {0xff111724, 0xffffffff}, // eSettingsLabelBg - {0xff111724, 0xff808080}, // eSettingsLabelBorder - {0xff020712, 0xfff4f4f4}, // eTabViewBg - {0xffaafbff, 0xff003c9f}, // eTabViewText - {0xff020712, 0xffe0e0e0}, // eTabButtonDefaultBg - {0xff020712, 0xffffffff}, // eTabButtonActiveBg - {0xff67ea94, 0xffaafbff}, // eTabButtonPressedBg - {0xffA0A0A0, 0xff606060}, // eTabButtonDefaultText - {0xffffffff, 0xff101010}, // eTabButtonActiveText - {0xffffffff, 0xffffffff}, // eTabButtonPressedText - {0xff505050, 0xffb0b0b0}, // eTabButtonDefaultBorder - {0xff111724, 0xfffbfce9}, // eChatMessageBg + {0xff141723, 0xffffffff}, // eSettingsLabelBg + {0xff141723, 0xff808080}, // eSettingsLabelBorder + {0xff020712, 0xfff4f4f4}, // eTabViewBg // Tabview background + {0xffaafbff, 0xff003c9f}, // eTabViewText // Tabview text color + {0xff020712, 0xffe0e0e0}, // eTabButtonDefaultBg //Tab buttons in tabview like filters and settings + {0xff020712, 0xffffffff}, // eTabButtonActiveBg //Tab buttons in tabview like filters and settings + {0xff67ea94, 0xffaafbff}, // eTabButtonPressedBg //Tab buttons in tabview like filters and settings + {0xffA0A0A0, 0xff606060}, // eTabButtonDefaultText //Tab buttons in tabview like filters and settings + {0xffffffff, 0xff101010}, // eTabButtonActiveText //Tab buttons in tabview like filters and settings + {0xffffffff, 0xffffffff}, // eTabButtonPressedText //Tab buttons in tabview like filters and settings + {0xff505050, 0xffb0b0b0}, // eTabButtonDefaultBorder //Tab buttons in tabview like filters and settings + {0xff141723, 0xfffbfce9}, // eChatMessageBg // Maybe in chats page each chat BG?? Rn Testing {255, 255}, // eChatMessageBgOpa {0xffffffff, 0xff294337}, // eChatMessageText - {0xff111724, 0xff888888}, // eChatMessageBorder - {0xff111724, 0xffffffff}, // eNewMessageBg + {0xff141723, 0xff888888}, // eChatMessageBorder // Maybe in chats page each chat border?? Rn Testing + {0xff141723, 0xffffffff}, // eNewMessageBg // Every individual chat new message BG {255, 255}, // eNewMessageBgOpa {0xffd0d0d0, 0xff294337}, // eNewMessageText - {0xff111724, 0xff888888}, // eNewMessageBorder + {0xff141723, 0xff888888}, // eNewMessageBorder // Every individual chat new message border {0xff020712, 0xfffbfbfb}, // eAlertPanelBg - {0xff020712, 0xfff4f4f4}, // eBtnMatrixBorderMain - {0xff67ea94, 0xff67ea94}, // eBtnMatrixBorderItems - {0xff606060, 0xfffffff8}, // eBtnMatrixBgItems - {0xffaafbff, 0xff212121}, // eBtnMatrixTextItems + {0xff020712, 0xfff4f4f4}, // eBtnMatrixBorderMain // Lock Screen + {0xff67ea94, 0xff67ea94}, // eBtnMatrixBorderItems //Lock Screen + {0xff606060, 0xfffffff8}, // eBtnMatrixBgItems // Lock Screen + {0xffaafbff, 0xff212121}, // eBtnMatrixTextItems // Lock Screen {0xff5ded96, 0xff212121}, // eBatteryPercentageText // changed {0xffaafbff, 0xff003c9f}, // eColorTextLabel - {0xff404040, 0xffe0e0e0}, // eSpinnerMainArc - {0xff67ea94, 0xff67ea94}, // eSpinnerIndicatorArc + {0xff404040, 0xffe0e0e0}, // eSpinnerMainArc // Arc at boot screen + {0xff67ea94, 0xff67ea94}, // eSpinnerIndicatorArc // Arc in things like scanner {0xffaafbff, 0xff212121}, // eTableHeadingText, {0xff020712, 0xfff4f4f0}, // eTableHeadingBg {0xffaafbff, 0xff212121}, // eTableItemText, - {0xff111724, 0xfff4f4f0}, // eTableItemBg - {0xff020712, 0xffd4d4d0}, // eTableItemDarkBg + {0xff141723, 0xfff4f4f0}, // eTableItemBg + {0xff020712, 0xffd4d4d0}, // eTableItemDarkBg was 020712 testing rn {0xff404040, 0xffe0e0e0}, // eTableBorder {0xff404040, 0xffe0e0e0} // eTableCellBorder }; From c25fc51468cbdb89381b2da5a08ab96894224cc6 Mon Sep 17 00:00:00 2001 From: Juan Pena Date: Tue, 27 Jan 2026 19:33:55 -0500 Subject: [PATCH 06/14] Theme Update 3 | Disabled keybaord & Removed keyboard Icon. --- generated/ui_320x240/images.c | 3 +- generated/ui_320x240/images.h | 33 +++--- generated/ui_320x240/screens.c | 147 +++++------------------- include/lv_conf.h | 2 +- source/graphics/TFT/TFTView_320x240.cpp | 4 +- 5 files changed, 52 insertions(+), 137 deletions(-) diff --git a/generated/ui_320x240/images.c b/generated/ui_320x240/images.c index 02cfd575..6941bf59 100644 --- a/generated/ui_320x240/images.c +++ b/generated/ui_320x240/images.c @@ -1,6 +1,6 @@ #include "images.h" -const ext_img_desc_t images[90] = { +const ext_img_desc_t images[89] = { { "meshtastic_boot_logo_image", &img_meshtastic_boot_logo_image }, { "settings_button_image", &img_settings_button_image }, { "map_button_image", &img_map_button_image }, @@ -15,7 +15,6 @@ const ext_img_desc_t images[90] = { { "home_bluetooth_on_button_image", &img_home_bluetooth_on_button_image }, { "home_memory_button", &img_home_memory_button }, { "node_client_image", &img_node_client_image }, - { "keyboard_image", &img_keyboard_image }, { "top_nodes_image", &img_top_nodes_image }, { "top_group_image", &img_top_group_image }, { "top_chats_image", &img_top_chats_image }, diff --git a/generated/ui_320x240/images.h b/generated/ui_320x240/images.h index a235d45f..823a579d 100644 --- a/generated/ui_320x240/images.h +++ b/generated/ui_320x240/images.h @@ -1,12 +1,12 @@ -#ifndef EEZ_LVGL_UI_IMAGES_H -#define EEZ_LVGL_UI_IMAGES_H - -#include "lvgl.h" - -#ifdef __cplusplus -extern "C" { -#endif - +#ifndef EEZ_LVGL_UI_IMAGES_H +#define EEZ_LVGL_UI_IMAGES_H + +#include "lvgl.h" + +#ifdef __cplusplus +extern "C" { +#endif + extern const lv_img_dsc_t img_meshtastic_boot_logo_image; extern const lv_img_dsc_t img_settings_button_image; extern const lv_img_dsc_t img_map_button_image; @@ -21,7 +21,6 @@ extern const lv_img_dsc_t img_home_wlan_button_image; extern const lv_img_dsc_t img_home_bluetooth_on_button_image; extern const lv_img_dsc_t img_home_memory_button; extern const lv_img_dsc_t img_node_client_image; -extern const lv_img_dsc_t img_keyboard_image; extern const lv_img_dsc_t img_top_nodes_image; extern const lv_img_dsc_t img_top_group_image; extern const lv_img_dsc_t img_top_chats_image; @@ -106,11 +105,11 @@ typedef struct _ext_img_desc_t { } ext_img_desc_t; #endif -extern const ext_img_desc_t images[90]; - - -#ifdef __cplusplus -} -#endif - +extern const ext_img_desc_t images[89]; + + +#ifdef __cplusplus +} +#endif + #endif /*EEZ_LVGL_UI_IMAGES_H*/ \ No newline at end of file diff --git a/generated/ui_320x240/screens.c b/generated/ui_320x240/screens.c index 69ddcfac..2c6c9ae5 100644 --- a/generated/ui_320x240/screens.c +++ b/generated/ui_320x240/screens.c @@ -916,7 +916,7 @@ void create_screen_main_screen() { lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_style_align(obj, LV_ALIGN_TOP_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_text_align(obj, LV_TEXT_ALIGN_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_color(obj, lv_color_hex(0xff5ded96), LV_PART_MAIN | LV_STATE_DEFAULT); // Changes Signal Color to green + lv_obj_set_style_text_color(obj, lv_color_hex(0xff5ded96), LV_PART_MAIN | LV_STATE_DEFAULT); // Changes Signal / hops Color to green } { // PositionLabel @@ -1249,7 +1249,7 @@ void create_screen_main_screen() { lv_obj_t *obj = lv_textarea_create(parent_obj); objects.message_input_area = obj; lv_obj_set_pos(obj, 6, -1); - lv_obj_set_size(obj, LV_PCT(85), 25); + lv_obj_set_size(obj, LV_PCT(96), 25); // Adjusted width to fit within MessagesPanel was 85 lv_textarea_set_max_length(obj, 220); lv_textarea_set_placeholder_text(obj, "Enter Text ..."); lv_textarea_set_one_line(obj, true); @@ -1272,13 +1272,7 @@ void create_screen_main_screen() { // KeyboardButton_0 lv_obj_t *obj = lv_btn_create(parent_obj); objects.keyboard_button_0 = obj; - lv_obj_set_pos(obj, -5, -2); - lv_obj_set_size(obj, 25, 24); - lv_obj_set_style_align(obj, LV_ALIGN_BOTTOM_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(obj, lv_color_hex(0xffffffff), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_image_src(obj, &img_keyboard_image, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_image_recolor(obj, lv_color_hex(0xff373737), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_image_recolor_opa(obj, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); } } } @@ -3126,7 +3120,7 @@ void create_screen_main_screen() { lv_obj_t *obj = lv_textarea_create(parent_obj); objects.settings_user_short_textarea = obj; lv_obj_set_pos(obj, 0, 18); - lv_obj_set_size(obj, LV_PCT(42), 25); + lv_obj_set_size(obj, LV_PCT(50), 25); // was 42 lv_textarea_set_max_length(obj, 4); lv_textarea_set_one_line(obj, true); lv_textarea_set_password_mode(obj, false); @@ -3140,13 +3134,7 @@ void create_screen_main_screen() { // KeyboardButton_1 lv_obj_t *obj = lv_btn_create(parent_obj); objects.keyboard_button_1 = obj; - lv_obj_set_pos(obj, 163, 18); - lv_obj_set_size(obj, 25, 24); - lv_obj_set_style_align(obj, LV_ALIGN_DEFAULT, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(obj, lv_color_hex(0xffffffff), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_image_src(obj, &img_keyboard_image, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_image_recolor(obj, lv_color_hex(0xff373737), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_image_recolor_opa(obj, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); } { lv_obj_t *obj = lv_label_create(parent_obj); @@ -3162,7 +3150,7 @@ void create_screen_main_screen() { lv_obj_t *obj = lv_textarea_create(parent_obj); objects.settings_user_long_textarea = obj; lv_obj_set_pos(obj, 0, 75); - lv_obj_set_size(obj, LV_PCT(88), 25); + lv_obj_set_size(obj, LV_PCT(100), 25); // was 88 lv_textarea_set_max_length(obj, 39); lv_textarea_set_one_line(obj, true); lv_textarea_set_password_mode(obj, false); @@ -3176,13 +3164,7 @@ void create_screen_main_screen() { // KeyboardButton_2 lv_obj_t *obj = lv_btn_create(parent_obj); objects.keyboard_button_2 = obj; - lv_obj_set_pos(obj, 200, 75); - lv_obj_set_size(obj, 25, 24); - lv_obj_set_style_align(obj, LV_ALIGN_DEFAULT, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(obj, lv_color_hex(0xffffffff), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_image_src(obj, &img_keyboard_image, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_image_recolor(obj, lv_color_hex(0xff373737), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_image_recolor_opa(obj, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); } { lv_obj_t *obj = lv_obj_create(parent_obj); @@ -3718,7 +3700,7 @@ void create_screen_main_screen() { lv_obj_t *obj = lv_textarea_create(parent_obj); objects.settings_wifi_ssid_textarea = obj; lv_obj_set_pos(obj, 0, 18); - lv_obj_set_size(obj, LV_PCT(88), 25); + lv_obj_set_size(obj, LV_PCT(100), 25); // was 88 lv_textarea_set_max_length(obj, 32); lv_textarea_set_one_line(obj, true); lv_textarea_set_password_mode(obj, false); @@ -3732,13 +3714,7 @@ void create_screen_main_screen() { // KeyboardButton_8 lv_obj_t *obj = lv_btn_create(parent_obj); objects.keyboard_button_8 = obj; - lv_obj_set_pos(obj, 200, 18); - lv_obj_set_size(obj, 25, 24); - lv_obj_set_style_align(obj, LV_ALIGN_DEFAULT, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(obj, lv_color_hex(0xffffffff), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_image_src(obj, &img_keyboard_image, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_image_recolor(obj, lv_color_hex(0xff373737), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_image_recolor_opa(obj, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); } { lv_obj_t *obj = lv_label_create(parent_obj); @@ -3754,7 +3730,7 @@ void create_screen_main_screen() { lv_obj_t *obj = lv_textarea_create(parent_obj); objects.settings_wifi_password_textarea = obj; lv_obj_set_pos(obj, 0, 75); - lv_obj_set_size(obj, LV_PCT(88), 25); + lv_obj_set_size(obj, LV_PCT(100), 25); // was 88 lv_textarea_set_max_length(obj, 64); lv_textarea_set_one_line(obj, true); lv_textarea_set_password_mode(obj, false); @@ -3768,13 +3744,7 @@ void create_screen_main_screen() { // KeyboardButton_9 lv_obj_t *obj = lv_btn_create(parent_obj); objects.keyboard_button_9 = obj; - lv_obj_set_pos(obj, 200, 75); - lv_obj_set_size(obj, 25, 24); - lv_obj_set_style_align(obj, LV_ALIGN_DEFAULT, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(obj, lv_color_hex(0xffffffff), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_image_src(obj, &img_keyboard_image, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_image_recolor(obj, lv_color_hex(0xff373737), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_image_recolor_opa(obj, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); } { lv_obj_t *obj = lv_obj_create(parent_obj); @@ -4042,8 +4012,8 @@ void create_screen_main_screen() { // SettingsScreenLockPasswordTextarea lv_obj_t *obj = lv_textarea_create(parent_obj); objects.settings_screen_lock_password_textarea = obj; - lv_obj_set_pos(obj, -30, 70); - lv_obj_set_size(obj, LV_PCT(40), 25); + lv_obj_set_pos(obj, -20, 70); // was -30 + lv_obj_set_size(obj, LV_PCT(70), 25); // was 40 lv_textarea_set_accepted_chars(obj, "1234567890"); lv_textarea_set_max_length(obj, 6); lv_textarea_set_placeholder_text(obj, "123456"); @@ -4074,13 +4044,7 @@ void create_screen_main_screen() { // KeyboardButton_7 lv_obj_t *obj = lv_btn_create(parent_obj); objects.keyboard_button_7 = obj; - lv_obj_set_pos(obj, 2, 70); - lv_obj_set_size(obj, 25, 24); - lv_obj_set_style_align(obj, LV_ALIGN_TOP_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(obj, lv_color_hex(0xffffffff), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_image_src(obj, &img_keyboard_image, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_image_recolor(obj, lv_color_hex(0xff373737), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_image_recolor_opa(obj, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); } } } @@ -4486,7 +4450,7 @@ void create_screen_main_screen() { lv_obj_t *obj = lv_label_create(parent_obj); objects.obj19 = obj; lv_obj_set_pos(obj, 0, 0); - lv_obj_set_size(obj, 200, LV_SIZE_CONTENT); + lv_obj_set_size(obj, LV_PCT(100), LV_SIZE_CONTENT); // was 200 no PCT lv_label_set_long_mode(obj, LV_LABEL_LONG_CLIP); lv_label_set_text(obj, _("Channel Name")); lv_obj_set_style_text_color(obj, lv_color_hex(0xff7ff5f9), LV_PART_MAIN | LV_STATE_DEFAULT); @@ -4499,7 +4463,7 @@ void create_screen_main_screen() { lv_obj_t *obj = lv_textarea_create(parent_obj); objects.settings_modify_channel_name_textarea = obj; lv_obj_set_pos(obj, 0, 18); - lv_obj_set_size(obj, LV_PCT(90), 25); + lv_obj_set_size(obj, LV_PCT(100), 25); lv_textarea_set_max_length(obj, 11); lv_textarea_set_placeholder_text(obj, "LongFast"); lv_textarea_set_one_line(obj, true); @@ -4514,19 +4478,13 @@ void create_screen_main_screen() { // KeyboardButton_3 lv_obj_t *obj = lv_btn_create(parent_obj); objects.keyboard_button_3 = obj; - lv_obj_set_pos(obj, 224, 18); - lv_obj_set_size(obj, 25, 24); - lv_obj_set_style_align(obj, LV_ALIGN_DEFAULT, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(obj, lv_color_hex(0xffffffff), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_image_src(obj, &img_keyboard_image, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_image_recolor(obj, lv_color_hex(0xff373737), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_image_recolor_opa(obj, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); } { lv_obj_t *obj = lv_label_create(parent_obj); objects.obj20 = obj; - lv_obj_set_pos(obj, 0, 60); - lv_obj_set_size(obj, 200, LV_SIZE_CONTENT); + lv_obj_set_pos(obj, 0, 60); + lv_obj_set_size(obj, LV_PCT(100), LV_SIZE_CONTENT); // was 200 no PCT lv_label_set_long_mode(obj, LV_LABEL_LONG_CLIP); lv_label_set_text(obj, _("Pre-shared Key")); lv_obj_set_style_text_color(obj, lv_color_hex(0xff7ff5f9), LV_PART_MAIN | LV_STATE_DEFAULT); @@ -4538,7 +4496,7 @@ void create_screen_main_screen() { lv_obj_t *obj = lv_textarea_create(parent_obj); objects.settings_modify_channel_psk_textarea = obj; lv_obj_set_pos(obj, 0, 80); - lv_obj_set_size(obj, LV_PCT(90), 25); + lv_obj_set_size(obj, LV_PCT(100), 25); lv_textarea_set_accepted_chars(obj, "1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+=/"); lv_textarea_set_max_length(obj, 44); lv_textarea_set_placeholder_text(obj, "AQ=="); @@ -4558,7 +4516,7 @@ void create_screen_main_screen() { // SettingsModifyChannelKeyGenerateButton lv_obj_t *obj = lv_btn_create(parent_obj); objects.settings_modify_channel_key_generate_button = obj; - lv_obj_set_pos(obj, 224, 54); + lv_obj_set_pos(obj, 224, 48); // was 54 lv_obj_set_size(obj, 25, 24); lv_obj_set_style_align(obj, LV_ALIGN_DEFAULT, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_color(obj, lv_color_hex(0xffffffff), LV_PART_MAIN | LV_STATE_DEFAULT); @@ -4570,13 +4528,7 @@ void create_screen_main_screen() { // KeyboardButton_4 lv_obj_t *obj = lv_btn_create(parent_obj); objects.keyboard_button_4 = obj; - lv_obj_set_pos(obj, 224, 80); - lv_obj_set_size(obj, 25, 24); - lv_obj_set_style_align(obj, LV_ALIGN_DEFAULT, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(obj, lv_color_hex(0xffffffff), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_image_src(obj, &img_keyboard_image, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_image_recolor(obj, lv_color_hex(0xff373737), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_image_recolor_opa(obj, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); } { // SettingsModifyTrashButton @@ -4918,8 +4870,8 @@ void create_screen_main_screen() { // NodesFilterNameArea lv_obj_t *obj = lv_textarea_create(parent_obj); objects.nodes_filter_name_area = obj; - lv_obj_set_pos(obj, -40, -2); - lv_obj_set_size(obj, LV_PCT(60), 24); + lv_obj_set_pos(obj, -10, -2); // was 40 + lv_obj_set_size(obj, LV_PCT(70), 24); // was 60 lv_textarea_set_max_length(obj, 15); lv_textarea_set_placeholder_text(obj, "!Enter Filter ..."); lv_textarea_set_one_line(obj, true); @@ -4939,15 +4891,7 @@ void create_screen_main_screen() { // KeyboardButton_5 lv_obj_t *obj = lv_btn_create(parent_obj); objects.keyboard_button_5 = obj; - lv_obj_set_pos(obj, -10, -2); - lv_obj_set_size(obj, 25, 24); - lv_obj_set_style_align(obj, LV_ALIGN_BOTTOM_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(obj, lv_color_hex(0xffffffff), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_image_src(obj, &img_keyboard_image, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_image_recolor(obj, lv_color_hex(0xff373737), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_image_recolor_opa(obj, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_shadow_width(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_shadow_ofs_y(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); } } } @@ -5110,8 +5054,8 @@ void create_screen_main_screen() { // NodesHLNameArea lv_obj_t *obj = lv_textarea_create(parent_obj); objects.nodes_hl_name_area = obj; - lv_obj_set_pos(obj, -40, -2); - lv_obj_set_size(obj, LV_PCT(60), 24); + lv_obj_set_pos(obj, -20, -2); + lv_obj_set_size(obj, LV_PCT(70), 24); lv_textarea_set_max_length(obj, 15); lv_textarea_set_placeholder_text(obj, "Enter Filter ..."); lv_textarea_set_one_line(obj, true); @@ -5131,15 +5075,7 @@ void create_screen_main_screen() { // KeyboardButton_6 lv_obj_t *obj = lv_btn_create(parent_obj); objects.keyboard_button_6 = obj; - lv_obj_set_pos(obj, -10, -2); - lv_obj_set_size(obj, 25, 24); - lv_obj_set_style_align(obj, LV_ALIGN_BOTTOM_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(obj, lv_color_hex(0xffffffff), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_image_src(obj, &img_keyboard_image, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_image_recolor(obj, lv_color_hex(0xff373737), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_image_recolor_opa(obj, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_shadow_width(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_shadow_ofs_y(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); } } } @@ -6219,13 +6155,7 @@ void create_screen_main_screen() { // KeyboardButton_10 lv_obj_t *obj = lv_btn_create(parent_obj); objects.keyboard_button_10 = obj; - lv_obj_set_pos(obj, 200, 72); - lv_obj_set_size(obj, 25, 24); - lv_obj_set_style_align(obj, LV_ALIGN_DEFAULT, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(obj, lv_color_hex(0xffffffff), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_image_src(obj, &img_keyboard_image, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_image_recolor(obj, lv_color_hex(0xff373737), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_image_recolor_opa(obj, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); } { lv_obj_t *obj = lv_label_create(parent_obj); @@ -6241,7 +6171,7 @@ void create_screen_main_screen() { lv_obj_t *obj = lv_textarea_create(parent_obj); objects.setup_user_long_textarea = obj; lv_obj_set_pos(obj, 0, 119); - lv_obj_set_size(obj, LV_PCT(88), 25); + lv_obj_set_size(obj, LV_PCT(96), 25); lv_textarea_set_max_length(obj, 39); lv_textarea_set_one_line(obj, true); lv_textarea_set_password_mode(obj, false); @@ -6255,13 +6185,7 @@ void create_screen_main_screen() { // KeyboardButton_11 lv_obj_t *obj = lv_btn_create(parent_obj); objects.keyboard_button_11 = obj; - lv_obj_set_pos(obj, 200, 121); - lv_obj_set_size(obj, 25, 24); - lv_obj_set_style_align(obj, LV_ALIGN_DEFAULT, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(obj, lv_color_hex(0xffffffff), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_image_src(obj, &img_keyboard_image, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_image_recolor(obj, lv_color_hex(0xff373737), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_image_recolor_opa(obj, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); } { lv_obj_t *obj = lv_obj_create(parent_obj); @@ -6319,16 +6243,7 @@ void create_screen_main_screen() { // Keyboard lv_obj_t *obj = lv_keyboard_create(parent_obj); objects.keyboard = obj; - lv_obj_set_pos(obj, 1, 28); - lv_obj_set_size(obj, LV_PCT(100), LV_PCT(50)); lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); - lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_align(obj, LV_ALIGN_TOP_MID, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_color(obj, lv_color_hex(0xff1b43db), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_font(obj, &lv_font_montserrat_16, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(obj, lv_color_hex(0xffccd1d8), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_max_height(obj, 300, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_max_width(obj, 600, LV_PART_MAIN | LV_STATE_DEFAULT); } } diff --git a/include/lv_conf.h b/include/lv_conf.h index bb20143d..80c43493 100644 --- a/include/lv_conf.h +++ b/include/lv_conf.h @@ -83,7 +83,7 @@ *====================*/ /*Default display refresh, input device read and animation step period.*/ -#define LV_DEF_REFR_PERIOD 40 /*[ms]*/ +#define LV_DEF_REFR_PERIOD 33 /*[ms]*/ //Switch to 30 fps /*Default Dot Per Inch. Used to initialize default sizes such as widgets sized, style paddings. *(Not so important, you can adjust it to modify default sizes and spaces)*/ diff --git a/source/graphics/TFT/TFTView_320x240.cpp b/source/graphics/TFT/TFTView_320x240.cpp index 74fade5b..57da9420 100644 --- a/source/graphics/TFT/TFTView_320x240.cpp +++ b/source/graphics/TFT/TFTView_320x240.cpp @@ -74,7 +74,7 @@ constexpr lv_color_t colorRed = LV_COLOR_HEX(0xff5555); constexpr lv_color_t colorDarkRed = LV_COLOR_HEX(0xa70a0a); constexpr lv_color_t colorOrange = LV_COLOR_HEX(0xff8c04); constexpr lv_color_t colorYellow = LV_COLOR_HEX(0xdbd251); -constexpr lv_color_t colorBlueGreen = LV_COLOR_HEX(0x05f6cb); +constexpr lv_color_t colorBlueGreen = LV_COLOR_HEX(0x5ded96); constexpr lv_color_t colorBlue = LV_COLOR_HEX(0x436C70); constexpr lv_color_t colorGray = LV_COLOR_HEX(0x757575); constexpr lv_color_t colorLightGray = LV_COLOR_HEX(0xAAFBFF); @@ -4651,6 +4651,7 @@ void TFTView_320x240::addNode(uint32_t nodeNum, uint8_t ch, const char *userShor lv_obj_set_pos(ui_SignalLabel, 8, 1); lv_obj_set_align(ui_SignalLabel, LV_ALIGN_TOP_RIGHT); lv_label_set_text(ui_SignalLabel, ""); + lv_obj_set_style_text_color(ui_SignalLabel, lv_color_hex(0xff5ded96), LV_PART_MAIN | LV_STATE_DEFAULT); ui_SignalLabel->user_data = (void *)-1; // TODO viaMqtt; // used for filtering (applyNodesFilter) // PositionLabel lv_obj_t *ui_PositionLabel = lv_label_create(p); @@ -6563,6 +6564,7 @@ void TFTView_320x240::addChat(uint32_t from, uint32_t to, uint8_t ch) lv_obj_set_style_pad_bottom(chatBtn, 0, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_pad_row(chatBtn, 0, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_pad_column(chatBtn, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(chatBtn, lv_color_hex(0xff141723), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_move_to_index(chatBtn, 0); char buf[64]; From 7441370039de8893d401773da579e0e44aca414d Mon Sep 17 00:00:00 2001 From: Juan Pena Date: Wed, 28 Jan 2026 12:16:40 -0500 Subject: [PATCH 07/14] Fixed Typing area in filters --- .DS_Store | Bin 0 -> 10244 bytes .github/.DS_Store | Bin 0 -> 6148 bytes .trunk/.DS_Store | Bin 0 -> 6148 bytes generated/.DS_Store | Bin 0 -> 6148 bytes generated/ui_320x240/screens.c | 14 ++++++++++++-- include/.DS_Store | Bin 0 -> 6148 bytes source/.DS_Store | Bin 0 -> 6148 bytes 7 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 .DS_Store create mode 100644 .github/.DS_Store create mode 100644 .trunk/.DS_Store create mode 100644 generated/.DS_Store create mode 100644 include/.DS_Store create mode 100644 source/.DS_Store diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..7a63f9905beadf14d3e65451b24f16336516c13f GIT binary patch literal 10244 zcmeI1ziSjh6vyA1z2wdxa0melg$;<9CYa7xo#8~mMo2HYA0at%cY9neX>9gd3$f5r z@E;IU*jS`B*xA^}#zL(83;e#Bxy`)U*+{w|v#|RvJMYc=?04Sm-AsrmTHo)k5+y`b z#mTmQ6&KIJ?R*xFOXkiK*a-1N9ZG3P6KYYf;2mxR1%d)WfuKN8ASiGV6u@^jAJr?` zhHFqDC=e9L72x+nf|G5-*i2jX>cB-U0bpa?RtG*~9iU4I;DH!&I64MvD zK24jGd-imBY{}E-?IQ1#ix1<_MK$y=7MRIScB*rB$D@Ry;3YJekvd+2hZ5$gVU}*8 zHkqb{O5LKaP~`c$+kP*btFoBST~B|uN@sftzD~w`3(#taaq43Rxpf=A+AVt6TxebV zqu+Ndjm*A;x!V&Yiuj-}+D`!!BtM9f;mh5vrpRGP5iQh+XRO-wTtJyP>6%;RT zzO>t9emXMw^H`a?*ly>ImtS+<8jbM$V^m5PBf?{oih7~SW4~7ki>JR5DvzhlN3(h! zqlWjuxDUz>f0leVzbL+fm*mUGp@v?Z{v_X7o*uq=n@x~aJHJ3C+8w6DBVEgmkC^#h zAHi|?8Pd=kIG~RppzHht-n7Wvd2Qsac2>7lkC`WG4)>gjS^G9}+e>nbKIACm z1j0vfter4NMZM#g#i@(xSJ_&^V8xdZSRNw3)`1zq5flgt1O=5RIejDH{r^Yh z|NnQx!rwuGpum5mKomDOn(KI=sN|ver+R{O6DJ?+muag)aA6m|ueRgy2XDvUHTTqy za`U-iY^JRWN1O_WRpGE4&i&PmO^j_An`x`V&;QQ=>^z9y%VGV``%`M4LFf|J|M|{? S(1jANKhJtyu5mB&`u{J|*wp<1 literal 0 HcmV?d00001 diff --git a/.github/.DS_Store b/.github/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..32b2e7ea3deae2175ce26456e9575fdee484c6e6 GIT binary patch literal 6148 zcmeHK!AiqG5Pe&Vwg}RL2ao#;y?WY~~mC zqK(FYF<=a=7?AISrwWFIjbi+CaEL7cu}^ap&ZU=-7@sgCY!tadaT-e0P?uT^r{T2w zElT1f4;f@ZwJ|vF<=b*D+XLY zE3zqHDb?1smy=rSsdrQn@oN-UA)LfgOkXL*C)6mkdzlbJ!bXu6iv1CYG}vGa{3!!p D9~V-} literal 0 HcmV?d00001 diff --git a/.trunk/.DS_Store b/.trunk/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..33da6af0ca8a4eee2a17f087ba2a10f17d4a1936 GIT binary patch literal 6148 zcmeHKK~4iP40PHGT5;)-W4_QUr=?n6&<|8?MX9vgtb}@xcmogM1BhqvEyl6CBBYf# zAynCtGg-$@ypw3XAtE5;zeU)xE#S1+YU8|r%1gcU{*AK&j@KkrX@ZGPd^J@IKduJ(p$kWWVq z9dMR*$V$)C(Sw$y$B%qI`h2o^+K}aQ#w*W<(=jmyi~(cd;ut{9W*OfIT5Aj#1I9ql z0Dm7ml(ABbg6Y$NCS3u5Bbbw*&vyxq@rspV6vPO`NhnZ4onA4Vgv0LDuTqSH5>76y zjCpirr#BRrR)^gucXE}WwZ?!kkTWop$0^tUhx7OUJjkAm0b}5z7;vL{QP1&8T3d^k u<67&XODGHbMZrD*Hh!+kXJnq>OAR)w4CG`uqs{sD%t>qupVN$c;9$E>W9^nrMuT7P?6Ab#A3N(aZ(V$vt{Bq%B=jZ!s^i zEnyCr1OLqd{&vr3L(lX;UCMpG-SKp_T&vudIBU5|)5&5r!Bs2}*~iV>=WP{pb|7>8 zl%JW`rINk@^%wIzi(>k~I#8c557Gx_t-b+uu>K9HQ>$;s+N&?e+@o*DjOc6W@7trk z=o5@6jPfcpR&b*#qud&us2>$N?_algAAasv;QiA1b@eXqVgf(L(3IGC3Olp*M}9LoWh;!89MT=RSYOcb+#$RO;GfY@N2 JIq;_rd;=W#q=End literal 0 HcmV?d00001 diff --git a/generated/ui_320x240/screens.c b/generated/ui_320x240/screens.c index 2c6c9ae5..7677c244 100644 --- a/generated/ui_320x240/screens.c +++ b/generated/ui_320x240/screens.c @@ -4870,7 +4870,7 @@ void create_screen_main_screen() { // NodesFilterNameArea lv_obj_t *obj = lv_textarea_create(parent_obj); objects.nodes_filter_name_area = obj; - lv_obj_set_pos(obj, -10, -2); // was 40 + lv_obj_set_pos(obj, -8, -2); // was 40 lv_obj_set_size(obj, LV_PCT(70), 24); // was 60 lv_textarea_set_max_length(obj, 15); lv_textarea_set_placeholder_text(obj, "!Enter Filter ..."); @@ -5054,7 +5054,7 @@ void create_screen_main_screen() { // NodesHLNameArea lv_obj_t *obj = lv_textarea_create(parent_obj); objects.nodes_hl_name_area = obj; - lv_obj_set_pos(obj, -20, -2); + lv_obj_set_pos(obj, -8, -2); lv_obj_set_size(obj, LV_PCT(70), 24); lv_textarea_set_max_length(obj, 15); lv_textarea_set_placeholder_text(obj, "Enter Filter ..."); @@ -6243,6 +6243,16 @@ void create_screen_main_screen() { // Keyboard lv_obj_t *obj = lv_keyboard_create(parent_obj); objects.keyboard = obj; + /*lv_obj_set_pos(obj, 1, 28); + lv_obj_set_size(obj, LV_PCT(100), LV_PCT(50)); + lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_align(obj, LV_ALIGN_TOP_MID, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_color(obj, lv_color_hex(0xff1b43db), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_font(obj, &lv_font_montserrat_16, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(obj, lv_color_hex(0xffccd1d8), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_max_height(obj, 300, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_max_width(obj, 600, LV_PART_MAIN | LV_STATE_DEFAULT);*/ lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); } } diff --git a/include/.DS_Store b/include/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..f8a8c17b2bd7be12b2022e07a602f76ba28ebe74 GIT binary patch literal 6148 zcmeHKJ5Iwu5S=BC6hV^)iE|@v>>}PGso>(~9g*hJb#b`1s zi&ZPu490*l@cS6xZ?{h~DyhJ=@%t90GPlW1o!+)!8u+q6|5q{1934G7*m}bFxX^*w5OLgJ9ug=jV9Hm!rwNEUHed z8H@pA;P)}W-|m3sR8oO!=li|>(B7iVvRPivVC4m2d;auxu^X}YCZbxkSBQ6{W3DBw z>6R*5(H(lxT^wC~?u*gA)Gu4rL_ccH__vmUvk9u8O-)OJyrNv8= zue;b6q3Y8s*N?a|ddPM4v*)(2r*@OuQ@Qc$x>K%e@;?~Bp3M@U2-;{27z4&Y!vL=j z36wDnSPS~41A{*T03(>a;GTaeI42312CN0)fjCJ8N~#M-3@7QZCm}8kSPM!zxp4S! zp|T4n6z8jB{=|oqO9gE-28;pCz@a=2dH=ucKmY3_dol)$fj`B7i?T^J#w&%rwexbk u*Cx;@l!fDJL7##lY{&kFx8hT%7x)ss0j2?KL0BO6Bj9PU!5H{a2EG8B_kqp; literal 0 HcmV?d00001 From 09a40fd22187fe91c98fd34ddb4388443aabc736 Mon Sep 17 00:00:00 2001 From: Juan Pena Date: Wed, 28 Jan 2026 16:30:50 -0500 Subject: [PATCH 08/14] Keyboard Removal Pt.2 --- generated/ui_320x240/screens.c | 88 - generated/ui_320x240/screens.h | 161 +- include/graphics/view/TFT/TFTView_320x240.h | 10 +- include/lv_conf.h | 2 +- source/graphics/TFT/TFTView_320x240.cpp | 10463 +++++++++--------- 5 files changed, 5241 insertions(+), 5483 deletions(-) diff --git a/generated/ui_320x240/screens.c b/generated/ui_320x240/screens.c index 7677c244..ea8ada9a 100644 --- a/generated/ui_320x240/screens.c +++ b/generated/ui_320x240/screens.c @@ -1268,12 +1268,6 @@ void create_screen_main_screen() { lv_obj_set_style_text_color(obj, lv_color_hex(0xffffffff), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_color(obj, lv_color_hex(0xff1f2a37), LV_PART_MAIN | LV_STATE_DEFAULT); } - { - // KeyboardButton_0 - lv_obj_t *obj = lv_btn_create(parent_obj); - objects.keyboard_button_0 = obj; - lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); - } } } { @@ -3130,12 +3124,6 @@ void create_screen_main_screen() { lv_obj_set_style_align(obj, LV_ALIGN_TOP_MID, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_text_align(obj, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN | LV_STATE_DEFAULT); } - { - // KeyboardButton_1 - lv_obj_t *obj = lv_btn_create(parent_obj); - objects.keyboard_button_1 = obj; - lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); - } { lv_obj_t *obj = lv_label_create(parent_obj); lv_obj_set_pos(obj, 0, 55); @@ -3160,12 +3148,6 @@ void create_screen_main_screen() { lv_obj_set_style_align(obj, LV_ALIGN_TOP_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_text_align(obj, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN | LV_STATE_DEFAULT); } - { - // KeyboardButton_2 - lv_obj_t *obj = lv_btn_create(parent_obj); - objects.keyboard_button_2 = obj; - lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); - } { lv_obj_t *obj = lv_obj_create(parent_obj); objects.obj3 = obj; @@ -3710,12 +3692,6 @@ void create_screen_main_screen() { lv_obj_set_style_align(obj, LV_ALIGN_TOP_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_text_align(obj, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN | LV_STATE_DEFAULT); } - { - // KeyboardButton_8 - lv_obj_t *obj = lv_btn_create(parent_obj); - objects.keyboard_button_8 = obj; - lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); - } { lv_obj_t *obj = lv_label_create(parent_obj); lv_obj_set_pos(obj, 0, 55); @@ -3740,12 +3716,6 @@ void create_screen_main_screen() { lv_obj_set_style_align(obj, LV_ALIGN_TOP_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_text_align(obj, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN | LV_STATE_DEFAULT); } - { - // KeyboardButton_9 - lv_obj_t *obj = lv_btn_create(parent_obj); - objects.keyboard_button_9 = obj; - lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); - } { lv_obj_t *obj = lv_obj_create(parent_obj); objects.obj8 = obj; @@ -4040,12 +4010,6 @@ void create_screen_main_screen() { create_user_widget_ok_cancel_widget(obj, 315); lv_obj_set_style_align(obj, LV_ALIGN_BOTTOM_MID, LV_PART_MAIN | LV_STATE_DEFAULT); } - { - // KeyboardButton_7 - lv_obj_t *obj = lv_btn_create(parent_obj); - objects.keyboard_button_7 = obj; - lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); - } } } { @@ -4474,12 +4438,6 @@ void create_screen_main_screen() { lv_obj_set_style_text_align(obj, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_pad_left(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT); } - { - // KeyboardButton_3 - lv_obj_t *obj = lv_btn_create(parent_obj); - objects.keyboard_button_3 = obj; - lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); - } { lv_obj_t *obj = lv_label_create(parent_obj); objects.obj20 = obj; @@ -4524,12 +4482,6 @@ void create_screen_main_screen() { lv_obj_set_style_bg_image_recolor(obj, lv_color_hex(0xff373737), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_image_recolor_opa(obj, 255, LV_PART_MAIN | LV_STATE_DEFAULT); } - { - // KeyboardButton_4 - lv_obj_t *obj = lv_btn_create(parent_obj); - objects.keyboard_button_4 = obj; - lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); - } { // SettingsModifyTrashButton lv_obj_t *obj = lv_btn_create(parent_obj); @@ -4887,12 +4839,6 @@ void create_screen_main_screen() { lv_obj_set_style_max_height(obj, 25, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_pad_bottom(obj, 2, LV_PART_MAIN | LV_STATE_DEFAULT); } - { - // KeyboardButton_5 - lv_obj_t *obj = lv_btn_create(parent_obj); - objects.keyboard_button_5 = obj; - lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); - } } } } @@ -5071,12 +5017,6 @@ void create_screen_main_screen() { lv_obj_set_style_max_height(obj, 25, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_pad_bottom(obj, 2, LV_PART_MAIN | LV_STATE_DEFAULT); } - { - // KeyboardButton_6 - lv_obj_t *obj = lv_btn_create(parent_obj); - objects.keyboard_button_6 = obj; - lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); - } } } } @@ -6151,12 +6091,6 @@ void create_screen_main_screen() { lv_obj_set_style_align(obj, LV_ALIGN_TOP_MID, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_text_align(obj, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN | LV_STATE_DEFAULT); } - { - // KeyboardButton_10 - lv_obj_t *obj = lv_btn_create(parent_obj); - objects.keyboard_button_10 = obj; - lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); - } { lv_obj_t *obj = lv_label_create(parent_obj); lv_obj_set_pos(obj, 0, 100); @@ -6181,12 +6115,6 @@ void create_screen_main_screen() { lv_obj_set_style_align(obj, LV_ALIGN_TOP_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_text_align(obj, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN | LV_STATE_DEFAULT); } - { - // KeyboardButton_11 - lv_obj_t *obj = lv_btn_create(parent_obj); - objects.keyboard_button_11 = obj; - lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); - } { lv_obj_t *obj = lv_obj_create(parent_obj); objects.obj27 = obj; @@ -6239,22 +6167,6 @@ void create_screen_main_screen() { } } } - { - // Keyboard - lv_obj_t *obj = lv_keyboard_create(parent_obj); - objects.keyboard = obj; - /*lv_obj_set_pos(obj, 1, 28); - lv_obj_set_size(obj, LV_PCT(100), LV_PCT(50)); - lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); - lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_align(obj, LV_ALIGN_TOP_MID, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_color(obj, lv_color_hex(0xff1b43db), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_font(obj, &lv_font_montserrat_16, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(obj, lv_color_hex(0xffccd1d8), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_max_height(obj, 300, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_max_width(obj, 600, LV_PART_MAIN | LV_STATE_DEFAULT);*/ - lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); - } } tick_screen_main_screen(); diff --git a/generated/ui_320x240/screens.h b/generated/ui_320x240/screens.h index ebd6e864..1b809e27 100644 --- a/generated/ui_320x240/screens.h +++ b/generated/ui_320x240/screens.h @@ -1,70 +1,70 @@ -#ifndef EEZ_LVGL_UI_SCREENS_H -#define EEZ_LVGL_UI_SCREENS_H - -#include "lvgl.h" - -#ifdef __cplusplus -extern "C" { -#endif - - -// advanced settings -extern lv_obj_t * ui_AdvancedSettingsPanel; -extern lv_obj_t * ui_SettingsTabView; -extern lv_obj_t * ui_TabPageGeneral; -extern lv_obj_t * ui_GeneralLanguageButton; -extern lv_obj_t * ui_LanguageLabel; -extern lv_obj_t * ui_GeneralTimezoneButton; -extern lv_obj_t * ui_TimezoneLabel; -extern lv_obj_t * ui_GeneralScreenButton; -extern lv_obj_t * ui_ScreenLabel; -extern lv_obj_t * ui_GeneralMapsButton; -extern lv_obj_t * ui_MapsLabel; -extern lv_obj_t * ui_GeneralAudioButton; -extern lv_obj_t * ui_AudioLabel1; -extern lv_obj_t * ui_TabPageRadio; -extern lv_obj_t * ui_RadioBluetoothButton; -extern lv_obj_t * ui_BluetoothLabel; -extern lv_obj_t * ui_RadioDeviceButton; -extern lv_obj_t * ui_DeviceLabel; -extern lv_obj_t * ui_RadioDisplayButton; -extern lv_obj_t * ui_DisplayLabel; -extern lv_obj_t * ui_RadioLoRaButton; -extern lv_obj_t * ui_LoRaLabel; -extern lv_obj_t * ui_RadioNetworkButton; -extern lv_obj_t * ui_NetworkLabel; -extern lv_obj_t * ui_RadioPositionButton; -extern lv_obj_t * ui_PositionLabel; -extern lv_obj_t * ui_RadioPowerButton; -extern lv_obj_t * ui_PowerLabel; -extern lv_obj_t * ui_TabPageModules; -extern lv_obj_t * ui_ModuleCannedMsgButton; -extern lv_obj_t * ui_CannedMsgLabel; -extern lv_obj_t * ui_ModuleSaFButton; -extern lv_obj_t * ui_StoreAndForwardLabel; -extern lv_obj_t * ui_ModuleTelemetryButton; -extern lv_obj_t * ui_TelemetryLabel; -extern lv_obj_t * ui_ModuleMQTTButton; -extern lv_obj_t * ui_MQTTLabel; -extern lv_obj_t * ui_ModuleRangeTestButton; -extern lv_obj_t * ui_RangeTestLabel; -extern lv_obj_t * ui_ModuleAudioButton; -extern lv_obj_t * ui_AudioLabel; -extern lv_obj_t * ui_ModuleSerialButton; -extern lv_obj_t * ui_SerialLabel; -extern lv_obj_t * ui_ModuleExtNotificationButton; -extern lv_obj_t * ui_ExtNotificationLabel; -extern lv_obj_t * ui_ModuleNeighborInfoButton; -extern lv_obj_t * ui_NeighborInfoLabel; -extern lv_obj_t * ui_ModuleAmbientLightingButton; -extern lv_obj_t * ui_AmbientLightingLabel; -extern lv_obj_t * ui_ModuleDetectionSensorButton; -extern lv_obj_t * ui_DetectionSensorLabel; -extern lv_obj_t * ui_ModuleRemoteHardwareButton; -extern lv_obj_t * ui_RemoteHardwareLabel; - -void create_tabview_settings(void); - +#ifndef EEZ_LVGL_UI_SCREENS_H +#define EEZ_LVGL_UI_SCREENS_H + +#include "lvgl.h" + +#ifdef __cplusplus +extern "C" { +#endif + + +// advanced settings +extern lv_obj_t * ui_AdvancedSettingsPanel; +extern lv_obj_t * ui_SettingsTabView; +extern lv_obj_t * ui_TabPageGeneral; +extern lv_obj_t * ui_GeneralLanguageButton; +extern lv_obj_t * ui_LanguageLabel; +extern lv_obj_t * ui_GeneralTimezoneButton; +extern lv_obj_t * ui_TimezoneLabel; +extern lv_obj_t * ui_GeneralScreenButton; +extern lv_obj_t * ui_ScreenLabel; +extern lv_obj_t * ui_GeneralMapsButton; +extern lv_obj_t * ui_MapsLabel; +extern lv_obj_t * ui_GeneralAudioButton; +extern lv_obj_t * ui_AudioLabel1; +extern lv_obj_t * ui_TabPageRadio; +extern lv_obj_t * ui_RadioBluetoothButton; +extern lv_obj_t * ui_BluetoothLabel; +extern lv_obj_t * ui_RadioDeviceButton; +extern lv_obj_t * ui_DeviceLabel; +extern lv_obj_t * ui_RadioDisplayButton; +extern lv_obj_t * ui_DisplayLabel; +extern lv_obj_t * ui_RadioLoRaButton; +extern lv_obj_t * ui_LoRaLabel; +extern lv_obj_t * ui_RadioNetworkButton; +extern lv_obj_t * ui_NetworkLabel; +extern lv_obj_t * ui_RadioPositionButton; +extern lv_obj_t * ui_PositionLabel; +extern lv_obj_t * ui_RadioPowerButton; +extern lv_obj_t * ui_PowerLabel; +extern lv_obj_t * ui_TabPageModules; +extern lv_obj_t * ui_ModuleCannedMsgButton; +extern lv_obj_t * ui_CannedMsgLabel; +extern lv_obj_t * ui_ModuleSaFButton; +extern lv_obj_t * ui_StoreAndForwardLabel; +extern lv_obj_t * ui_ModuleTelemetryButton; +extern lv_obj_t * ui_TelemetryLabel; +extern lv_obj_t * ui_ModuleMQTTButton; +extern lv_obj_t * ui_MQTTLabel; +extern lv_obj_t * ui_ModuleRangeTestButton; +extern lv_obj_t * ui_RangeTestLabel; +extern lv_obj_t * ui_ModuleAudioButton; +extern lv_obj_t * ui_AudioLabel; +extern lv_obj_t * ui_ModuleSerialButton; +extern lv_obj_t * ui_SerialLabel; +extern lv_obj_t * ui_ModuleExtNotificationButton; +extern lv_obj_t * ui_ExtNotificationLabel; +extern lv_obj_t * ui_ModuleNeighborInfoButton; +extern lv_obj_t * ui_NeighborInfoLabel; +extern lv_obj_t * ui_ModuleAmbientLightingButton; +extern lv_obj_t * ui_AmbientLightingLabel; +extern lv_obj_t * ui_ModuleDetectionSensorButton; +extern lv_obj_t * ui_DetectionSensorLabel; +extern lv_obj_t * ui_ModuleRemoteHardwareButton; +extern lv_obj_t * ui_RemoteHardwareLabel; + +void create_tabview_settings(void); + typedef struct _objects_t { lv_obj_t *boot_screen; lv_obj_t *main_screen; @@ -160,7 +160,6 @@ typedef struct _objects_t { lv_obj_t *messages_panel; lv_obj_t *messages_container; lv_obj_t *message_input_area; - lv_obj_t *keyboard_button_0; lv_obj_t *chats_panel; lv_obj_t *chats_button; lv_obj_t *chats_button_label; @@ -299,9 +298,7 @@ typedef struct _objects_t { lv_obj_t *home_cancel_qr_button; lv_obj_t *settings_username_panel; lv_obj_t *settings_user_short_textarea; - lv_obj_t *keyboard_button_1; lv_obj_t *settings_user_long_textarea; - lv_obj_t *keyboard_button_2; lv_obj_t *obj3; lv_obj_t *obj3__ok_cancel_panel_w; lv_obj_t *obj3__ok_button_w; @@ -349,9 +346,7 @@ typedef struct _objects_t { lv_obj_t *obj7__cancel_button_w; lv_obj_t *settings_wifi_panel; lv_obj_t *settings_wifi_ssid_textarea; - lv_obj_t *keyboard_button_8; lv_obj_t *settings_wifi_password_textarea; - lv_obj_t *keyboard_button_9; lv_obj_t *obj8; lv_obj_t *obj8__ok_cancel_panel_w; lv_obj_t *obj8__ok_button_w; @@ -384,7 +379,6 @@ typedef struct _objects_t { lv_obj_t *obj12__ok_cancel_panel_w; lv_obj_t *obj12__ok_button_w; lv_obj_t *obj12__cancel_button_w; - lv_obj_t *keyboard_button_7; lv_obj_t *settings_input_control_panel; lv_obj_t *settings_mouse_input_dropdown; lv_obj_t *settings_keyboard_input_dropdown; @@ -429,11 +423,9 @@ typedef struct _objects_t { lv_obj_t *settings_modify_channel_panel; lv_obj_t *obj19; lv_obj_t *settings_modify_channel_name_textarea; - lv_obj_t *keyboard_button_3; lv_obj_t *obj20; lv_obj_t *settings_modify_channel_psk_textarea; lv_obj_t *settings_modify_channel_key_generate_button; - lv_obj_t *keyboard_button_4; lv_obj_t *settings_modify_trash_button; lv_obj_t *obj21; lv_obj_t *obj21__ok_cancel_panel_w; @@ -460,7 +452,6 @@ typedef struct _objects_t { lv_obj_t *nodes_filter_position_switch; lv_obj_t *nodes_filter_name_label; lv_obj_t *nodes_filter_name_area; - lv_obj_t *keyboard_button_5; lv_obj_t *tab_page_highlight; lv_obj_t *nodes_hl_active_chat_label; lv_obj_t *nodes_hl_active_chat_switch; @@ -472,7 +463,6 @@ typedef struct _objects_t { lv_obj_t *nodes_hliaq_switch; lv_obj_t *nodes_hl_name_label; lv_obj_t *nodes_hl_name_area; - lv_obj_t *keyboard_button_6; lv_obj_t *mesh_detector_panel; lv_obj_t *detector_radar_panel; lv_obj_t *radar_beam; @@ -538,16 +528,13 @@ typedef struct _objects_t { lv_obj_t *setup_panel; lv_obj_t *setup_region_dropdown; lv_obj_t *setup_user_short_textarea; - lv_obj_t *keyboard_button_10; lv_obj_t *setup_user_long_textarea; - lv_obj_t *keyboard_button_11; lv_obj_t *obj27; lv_obj_t *obj27__ok_cancel_panel_w; lv_obj_t *obj27__ok_button_w; lv_obj_t *obj27__cancel_button_w; lv_obj_t *alert_panel; lv_obj_t *alert_label; - lv_obj_t *keyboard; lv_obj_t *blank_screen_button; lv_obj_t *screen_lock_button_matrix; lv_obj_t *lock_screen_digits_label; @@ -580,15 +567,15 @@ void tick_screen_calibration_screen(); void create_user_widget_ok_cancel_widget(lv_obj_t *parent_obj, int startWidgetIndex); void tick_user_widget_ok_cancel_widget(int startWidgetIndex); - + void tick_screen_by_id(enum ScreensEnum screenId); void tick_screen(int screen_index); void create_screens(); - - -#ifdef __cplusplus -} -#endif - + + +#ifdef __cplusplus +} +#endif + #endif /*EEZ_LVGL_UI_SCREENS_H*/ \ No newline at end of file diff --git a/include/graphics/view/TFT/TFTView_320x240.h b/include/graphics/view/TFT/TFTView_320x240.h index ba6cb4bf..69d7b0a8 100644 --- a/include/graphics/view/TFT/TFTView_320x240.h +++ b/include/graphics/view/TFT/TFTView_320x240.h @@ -214,8 +214,9 @@ class TFTView_320x240 : public MeshtasticView void updateTheme(void); void ui_events_init(void); void ui_set_active(lv_obj_t *b, lv_obj_t *p, lv_obj_t *tp); - void showKeyboard(lv_obj_t *textArea); - void hideKeyboard(lv_obj_t *panel); + // Removed: Virtual keyboard not needed for T-Deck + // void showKeyboard(lv_obj_t *textArea); + // void hideKeyboard(lv_obj_t *panel); lv_obj_t *showQrCode(lv_obj_t *parent, const char *data); void enablePanel(lv_obj_t *panel); @@ -305,8 +306,9 @@ class TFTView_320x240 : public MeshtasticView // blank screen static void ui_event_BlankScreenButton(lv_event_t *e); - static void ui_event_KeyboardButton(lv_event_t *e); - static void ui_event_Keyboard(lv_event_t *e); + // Removed: Virtual keyboard event handlers not needed for T-Deck + // static void ui_event_KeyboardButton(lv_event_t *e); + // static void ui_event_Keyboard(lv_event_t *e); static void ui_event_message_ready(lv_event_t *e); diff --git a/include/lv_conf.h b/include/lv_conf.h index 80c43493..d01a5c07 100644 --- a/include/lv_conf.h +++ b/include/lv_conf.h @@ -630,7 +630,7 @@ #define LV_USE_IMAGEBUTTON 1 -#define LV_USE_KEYBOARD 1 +#define LV_USE_KEYBOARD 0 // Removed: Virtual keyboard not needed for T-Deck #define LV_USE_LABEL 1 #if LV_USE_LABEL diff --git a/source/graphics/TFT/TFTView_320x240.cpp b/source/graphics/TFT/TFTView_320x240.cpp index 57da9420..d605a72a 100644 --- a/source/graphics/TFT/TFTView_320x240.cpp +++ b/source/graphics/TFT/TFTView_320x240.cpp @@ -468,9 +468,7 @@ void TFTView_320x240::ui_set_active(lv_obj_t *b, lv_obj_t *p, lv_obj_t *tp) lv_obj_add_flag(activePanel, LV_OBJ_FLAG_HIDDEN); if (activePanel == objects.messages_panel) { lv_obj_remove_state(objects.message_input_area, LV_STATE_FOCUSED); - if (!lv_obj_has_flag(objects.keyboard, LV_OBJ_FLAG_HIDDEN)) { - hideKeyboard(objects.messages_panel); - } + // Removed: keyboard handling uint32_t channelOrNode = (unsigned long)activeMsgContainer->user_data; // remove empty messageContainer if we are leaving messages panel if (channelOrNode >= c_max_channels) { @@ -502,11 +500,11 @@ void TFTView_320x240::ui_set_active(lv_obj_t *b, lv_obj_t *p, lv_obj_t *tp) activePanel = p; if (activePanel == objects.messages_panel) { lv_group_focus_obj(objects.message_input_area); - } else if (inputdriver->hasKeyboardDevice() || inputdriver->hasEncoderDevice()) { + } else if (inputdriver->hasEncoderDevice()) { setGroupFocus(activePanel); } - lv_obj_add_flag(objects.keyboard, LV_OBJ_FLAG_HIDDEN); + // Removed: lv_obj_add_flag(objects.keyboard, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(objects.msg_popup_panel, LV_OBJ_FLAG_HIDDEN); } @@ -563,8 +561,8 @@ void TFTView_320x240::apply_hotfix(void) // fix size for 480 pixel height displays if (v >= 480) { - // keyboard size limit - lv_obj_set_size(objects.keyboard, LV_PCT(100), LV_PCT(45)); + // keyboard size limit - REMOVED: Virtual keyboard not needed for T-Deck + // lv_obj_set_size(objects.keyboard, LV_PCT(100), LV_PCT(45)); // resize channel buttons buttonSize = 40; @@ -586,7 +584,7 @@ void TFTView_320x240::apply_hotfix(void) lv_obj_set_style_text_font(objects.home_qr_label, &ui_font_montserrat_16, LV_PART_MAIN | LV_STATE_DEFAULT); } - lv_obj_move_foreground(objects.keyboard); + // Removed: lv_obj_move_foreground(objects.keyboard); lv_obj_add_flag(objects.detector_radar_panel, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(objects.detected_node_button, LV_OBJ_FLAG_HIDDEN); lv_label_set_text(objects.detector_start_label, _("Start")); @@ -746,20 +744,8 @@ void TFTView_320x240::ui_events_init(void) lv_obj_add_event_cb(objects.msg_restore_panel, this->ui_event_MsgRestoreButton, LV_EVENT_CLICKED, NULL); lv_obj_add_event_cb(objects.alert_panel, this->ui_event_AlertButton, LV_EVENT_CLICKED, NULL); - // keyboard - lv_obj_add_event_cb(objects.keyboard, ui_event_Keyboard, LV_EVENT_CLICKED, this); - lv_obj_add_event_cb(objects.keyboard_button_0, ui_event_KeyboardButton, LV_EVENT_CLICKED, (void *)0); - lv_obj_add_event_cb(objects.keyboard_button_1, ui_event_KeyboardButton, LV_EVENT_CLICKED, (void *)1); - lv_obj_add_event_cb(objects.keyboard_button_2, ui_event_KeyboardButton, LV_EVENT_CLICKED, (void *)2); - lv_obj_add_event_cb(objects.keyboard_button_3, ui_event_KeyboardButton, LV_EVENT_CLICKED, (void *)3); - lv_obj_add_event_cb(objects.keyboard_button_4, ui_event_KeyboardButton, LV_EVENT_CLICKED, (void *)4); - lv_obj_add_event_cb(objects.keyboard_button_5, ui_event_KeyboardButton, LV_EVENT_CLICKED, (void *)5); - lv_obj_add_event_cb(objects.keyboard_button_6, ui_event_KeyboardButton, LV_EVENT_CLICKED, (void *)6); - lv_obj_add_event_cb(objects.keyboard_button_7, ui_event_KeyboardButton, LV_EVENT_CLICKED, (void *)7); - lv_obj_add_event_cb(objects.keyboard_button_8, ui_event_KeyboardButton, LV_EVENT_CLICKED, (void *)8); - lv_obj_add_event_cb(objects.keyboard_button_9, ui_event_KeyboardButton, LV_EVENT_CLICKED, (void *)9); - lv_obj_add_event_cb(objects.keyboard_button_10, ui_event_KeyboardButton, LV_EVENT_CLICKED, (void *)10); - lv_obj_add_event_cb(objects.keyboard_button_11, ui_event_KeyboardButton, LV_EVENT_CLICKED, (void *)11); + // keyboard - REMOVED: Virtual keyboard not needed for T-Deck + // Removed all keyboard event callbacks // message text area lv_obj_add_event_cb(objects.message_input_area, ui_event_message_ready, LV_EVENT_ALL, NULL); @@ -1564,1413 +1550,1371 @@ void TFTView_320x240::ui_event_BlankScreenButton(lv_event_t *e) void TFTView_320x240::ui_event_KeyboardButton(lv_event_t *e) { - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED) { - uint32_t keyBtnIdx = (unsigned long)e->user_data; - switch (keyBtnIdx) { - case 0: - if (lv_obj_has_flag(objects.keyboard, LV_OBJ_FLAG_HIDDEN)) { - lv_obj_remove_flag(objects.keyboard, LV_OBJ_FLAG_HIDDEN); - THIS->showKeyboard(objects.message_input_area); - } else { - THIS->hideKeyboard(objects.messages_panel); + // REMOVED: Virtual keyboard not needed for T-Deck + // This function is no longer used + return; + + /** + * handle events for virtual keyboard - REMOVED: Not needed for T-Deck + */ + void TFTView_320x240::ui_event_Keyboard(lv_event_t * e) + { + // REMOVED: Virtual keyboard not needed for T-Deck + return; + + void TFTView_320x240::ui_event_message_ready(lv_event_t * e) + { + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_READY) { + char *txt = (char *)lv_textarea_get_text(objects.message_input_area); + uint32_t len = strlen(txt); + if (len) { + if (txt[len - 1] == ' ') { // use space+return combo to start new line in same message + lv_textarea_add_char(objects.message_input_area, CR_REPLACEMENT); + } else { + THIS->handleAddMessage(txt); + lv_textarea_set_text(objects.message_input_area, ""); + // Removed: keyboard handling + lv_group_focus_obj(objects.message_input_area); + } + } } - lv_group_focus_obj(objects.message_input_area); - return; // continue play animation, don't hide keyboard immediately - case 1: - THIS->showKeyboard(objects.settings_user_short_textarea); - lv_group_focus_obj(objects.settings_user_short_textarea); - break; - case 2: - THIS->showKeyboard(objects.settings_user_long_textarea); - lv_group_focus_obj(objects.settings_user_long_textarea); - break; - case 3: - THIS->showKeyboard(objects.settings_modify_channel_name_textarea); - lv_group_focus_obj(objects.settings_modify_channel_name_textarea); - break; - case 4: - THIS->showKeyboard(objects.settings_modify_channel_psk_textarea); - lv_group_focus_obj(objects.settings_modify_channel_psk_textarea); - break; - case 5: - THIS->showKeyboard(objects.nodes_filter_name_area); - lv_group_focus_obj(objects.nodes_filter_name_area); - break; - case 6: - THIS->showKeyboard(objects.nodes_hl_name_area); - lv_group_focus_obj(objects.nodes_hl_name_area); - break; - case 7: - THIS->showKeyboard(objects.settings_screen_lock_password_textarea); - lv_group_focus_obj(objects.settings_screen_lock_password_textarea); - break; - case 8: - THIS->showKeyboard(objects.settings_wifi_ssid_textarea); - lv_group_focus_obj(objects.settings_wifi_ssid_textarea); - break; - case 9: - THIS->showKeyboard(objects.settings_wifi_password_textarea); - lv_group_focus_obj(objects.settings_wifi_password_textarea); - break; - case 10: - THIS->showKeyboard(objects.setup_user_short_textarea); - lv_group_focus_obj(objects.setup_user_short_textarea); - break; - case 11: - THIS->showKeyboard(objects.setup_user_long_textarea); - lv_group_focus_obj(objects.setup_user_long_textarea); - break; - default: - ILOG_ERROR("missing keyboard <-> textarea assignment"); } - lv_obj_has_flag(objects.keyboard, LV_OBJ_FLAG_HIDDEN) ? lv_obj_remove_flag(objects.keyboard, LV_OBJ_FLAG_HIDDEN) - : lv_obj_add_flag(objects.keyboard, LV_OBJ_FLAG_HIDDEN); - } -} -/** - * handle events for virtual keyboard - */ -void TFTView_320x240::ui_event_Keyboard(lv_event_t *e) -{ - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED) { - lv_obj_t *kb = lv_event_get_target_obj(e); - uint32_t btn_id = lv_keyboard_get_selected_button(kb); - - switch (btn_id) { - case 22: { // enter (filtered out by one-liner text input area, so we replace it) - // lv_obj_t *ta = lv_keyboard_get_textarea(kb); - // lv_textarea_add_char(ta, ' '); - // lv_textarea_add_char(ta, CR_REPLACEMENT); - break; + // basic settings buttons + + void TFTView_320x240::ui_event_user_button(lv_event_t * e) + { + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone) { + lv_textarea_set_text(objects.settings_user_short_textarea, THIS->db.short_name); + lv_textarea_set_text(objects.settings_user_long_textarea, THIS->db.long_name); + lv_obj_clear_flag(objects.settings_username_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.settings_user_short_textarea); + THIS->disablePanel(objects.controller_panel); + THIS->disablePanel(objects.tab_page_basic_settings); + THIS->activeSettings = eUsername; + } } - case 35: { // keyboard - lv_keyboard_set_popovers(objects.keyboard, !lv_keyboard_get_popovers(kb)); - break; + + void TFTView_320x240::ui_event_role_button(lv_event_t * e) + { + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone && THIS->db.config.has_device) { + lv_dropdown_set_selected(objects.settings_device_role_dropdown, THIS->role2val(THIS->db.config.device.role)); + lv_obj_clear_flag(objects.settings_device_role_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.settings_device_role_dropdown); + THIS->disablePanel(objects.controller_panel); + THIS->disablePanel(objects.tab_page_basic_settings); + THIS->activeSettings = eDeviceRole; + } } - case 36: { // left - break; + + void TFTView_320x240::ui_event_region_button(lv_event_t * e) + { + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone && THIS->db.config.has_lora) { + lv_dropdown_set_selected(objects.settings_region_dropdown, THIS->db.config.lora.region - 1); + lv_obj_clear_flag(objects.settings_region_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.settings_region_dropdown); + THIS->disablePanel(objects.controller_panel); + THIS->disablePanel(objects.tab_page_basic_settings); + THIS->activeSettings = eRegion; + } } - case 38: { // right - break; + + void TFTView_320x240::ui_event_preset_button(lv_event_t * e) + { + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone && THIS->db.config.lora.use_preset) { + THIS->activeSettings = eModemPreset; + lv_dropdown_set_selected(objects.settings_modem_preset_dropdown, + THIS->preset2val(THIS->db.config.lora.modem_preset)); + + char buf[60]; + sprintf(buf, _("FrequencySlot: %d (%g MHz)"), THIS->db.config.lora.channel_num, + LoRaPresets::getRadioFreq(THIS->db.config.lora.region, THIS->db.config.lora.modem_preset, + THIS->db.config.lora.channel_num)); + lv_label_set_text(objects.frequency_slot_label, buf); + + uint32_t numChannels = + LoRaPresets::getNumChannels(THIS->db.config.lora.region, THIS->db.config.lora.modem_preset); + lv_slider_set_range(objects.frequency_slot_slider, 1, numChannels); + lv_slider_set_value(objects.frequency_slot_slider, THIS->db.config.lora.channel_num, LV_ANIM_OFF); + + lv_obj_clear_flag(objects.settings_modem_preset_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.settings_modem_preset_dropdown); + THIS->disablePanel(objects.controller_panel); + } } - case 39: { // checkmark - if (THIS->activePanel == objects.messages_panel) { - THIS->hideKeyboard(objects.messages_panel); - } else { - lv_obj_add_flag(kb, LV_OBJ_FLAG_HIDDEN); + + void TFTView_320x240::ui_event_wifi_button(lv_event_t * e) + { + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED && THIS->db.config.has_network && THIS->activeSettings == eNone) { + lv_textarea_set_text(objects.settings_wifi_ssid_textarea, THIS->db.config.network.wifi_ssid); + lv_textarea_set_text(objects.settings_wifi_password_textarea, THIS->db.config.network.wifi_psk); + lv_obj_clear_flag(objects.settings_wifi_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.settings_wifi_ssid_textarea); + THIS->disablePanel(objects.controller_panel); + THIS->disablePanel(objects.tab_page_basic_settings); + THIS->activeSettings = eWifi; } - lv_group_focus_obj(objects.message_input_area); - break; } - default: - break; - // const char *txt = lv_keyboard_get_button_text(kb, btn_id); + + void TFTView_320x240::ui_event_language_button(lv_event_t * e) + { + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone) { + lv_dropdown_set_selected(objects.settings_language_dropdown, THIS->language2val(THIS->db.uiConfig.language)); + lv_obj_clear_flag(objects.settings_language_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.settings_language_dropdown); + THIS->disablePanel(objects.controller_panel); + THIS->disablePanel(objects.tab_page_basic_settings); + THIS->activeSettings = eLanguage; + } } - } -} -void TFTView_320x240::ui_event_message_ready(lv_event_t *e) -{ - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_READY) { - char *txt = (char *)lv_textarea_get_text(objects.message_input_area); - uint32_t len = strlen(txt); - if (len) { - if (txt[len - 1] == ' ') { // use space+return combo to start new line in same message - lv_textarea_add_char(objects.message_input_area, CR_REPLACEMENT); - } else { - THIS->handleAddMessage(txt); - lv_textarea_set_text(objects.message_input_area, ""); - if (!lv_obj_has_flag(objects.keyboard, LV_OBJ_FLAG_HIDDEN)) { - THIS->hideKeyboard(objects.messages_panel); + void TFTView_320x240::ui_event_channel_button(lv_event_t * e) + { + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone) { + // primary channel is not necessarily channel[0], setup ui with primary on top + int pos = 1; + for (int i = 0; i < c_max_channels; i++) { + meshtastic_Channel &ch = THIS->db.channel[i]; + if (ch.has_settings && ch.role != meshtastic_Channel_Role_DISABLED) { + const char *channelName = ch.settings.name; + if (ch.settings.name[0] == '\0' && ch.settings.psk.size == 1 && ch.settings.psk.bytes[0] == 0x01) { + channelName = LoRaPresets::modemPresetToString(THIS->db.config.lora.modem_preset); + } + if (ch.role == meshtastic_Channel_Role_PRIMARY) { + THIS->ch_label[0]->user_data = (void *)i; + lv_label_set_text(THIS->ch_label[0], channelName); + } else { + THIS->ch_label[pos]->user_data = (void *)i; + lv_label_set_text(THIS->ch_label[pos++], channelName); + } + } + } + for (int i = pos; i < c_max_channels; i++) { + THIS->ch_label[i]->user_data = (void *)-1; + lv_label_set_text(THIS->ch_label[i], _("")); + } + lv_obj_clear_flag(objects.settings_channel_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.settings_channel0_button); + THIS->disablePanel(objects.controller_panel); + THIS->disablePanel(objects.tab_page_basic_settings); + THIS->activeSettings = eChannel; + + // create scratch channels to store temporary changes until cancelled or applied + THIS->channel_scratch = new meshtastic_Channel[c_max_channels]; + for (int i = 0; i < c_max_channels; i++) { + THIS->channel_scratch[i] = THIS->db.channel[i]; } - lv_group_focus_obj(objects.message_input_area); } } - } -} -// basic settings buttons - -void TFTView_320x240::ui_event_user_button(lv_event_t *e) -{ - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone) { - lv_textarea_set_text(objects.settings_user_short_textarea, THIS->db.short_name); - lv_textarea_set_text(objects.settings_user_long_textarea, THIS->db.long_name); - lv_obj_clear_flag(objects.settings_username_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.settings_user_short_textarea); - THIS->disablePanel(objects.controller_panel); - THIS->disablePanel(objects.tab_page_basic_settings); - THIS->activeSettings = eUsername; - } -} + void TFTView_320x240::ui_event_brightness_button(lv_event_t * e) + { + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone) { + char buf[20]; + uint32_t brightness = round(THIS->db.uiConfig.screen_brightness * 100.0 / 255.0); + lv_snprintf(buf, sizeof(buf), _("Brightness: %d%%"), brightness); + lv_label_set_text(objects.settings_brightness_label, buf); + lv_slider_set_value(objects.brightness_slider, brightness, LV_ANIM_OFF); + lv_obj_clear_flag(objects.settings_brightness_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.brightness_slider); + THIS->disablePanel(objects.controller_panel); + THIS->disablePanel(objects.tab_page_basic_settings); + THIS->activeSettings = eScreenBrightness; + } + } -void TFTView_320x240::ui_event_role_button(lv_event_t *e) -{ - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone && THIS->db.config.has_device) { - lv_dropdown_set_selected(objects.settings_device_role_dropdown, THIS->role2val(THIS->db.config.device.role)); - lv_obj_clear_flag(objects.settings_device_role_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.settings_device_role_dropdown); - THIS->disablePanel(objects.controller_panel); - THIS->disablePanel(objects.tab_page_basic_settings); - THIS->activeSettings = eDeviceRole; - } -} + void TFTView_320x240::ui_event_theme_button(lv_event_t * e) + { + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone) { + lv_dropdown_set_selected(objects.settings_theme_dropdown, THIS->db.uiConfig.theme); + lv_obj_clear_flag(objects.settings_theme_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.settings_theme_dropdown); + THIS->disablePanel(objects.controller_panel); + THIS->disablePanel(objects.tab_page_basic_settings); + THIS->activeSettings = eTheme; + } + } -void TFTView_320x240::ui_event_region_button(lv_event_t *e) -{ - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone && THIS->db.config.has_lora) { - lv_dropdown_set_selected(objects.settings_region_dropdown, THIS->db.config.lora.region - 1); - lv_obj_clear_flag(objects.settings_region_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.settings_region_dropdown); - THIS->disablePanel(objects.controller_panel); - THIS->disablePanel(objects.tab_page_basic_settings); - THIS->activeSettings = eRegion; - } -} + void TFTView_320x240::ui_event_calibration_button(lv_event_t * e) + { + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED) { + lv_screen_load_anim(objects.calibration_screen, LV_SCR_LOAD_ANIM_NONE, 0, 0, false); + } + } -void TFTView_320x240::ui_event_preset_button(lv_event_t *e) -{ - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone && THIS->db.config.lora.use_preset) { - THIS->activeSettings = eModemPreset; - lv_dropdown_set_selected(objects.settings_modem_preset_dropdown, THIS->preset2val(THIS->db.config.lora.modem_preset)); - - char buf[60]; - sprintf(buf, _("FrequencySlot: %d (%g MHz)"), THIS->db.config.lora.channel_num, - LoRaPresets::getRadioFreq(THIS->db.config.lora.region, THIS->db.config.lora.modem_preset, - THIS->db.config.lora.channel_num)); - lv_label_set_text(objects.frequency_slot_label, buf); - - uint32_t numChannels = LoRaPresets::getNumChannels(THIS->db.config.lora.region, THIS->db.config.lora.modem_preset); - lv_slider_set_range(objects.frequency_slot_slider, 1, numChannels); - lv_slider_set_value(objects.frequency_slot_slider, THIS->db.config.lora.channel_num, LV_ANIM_OFF); - - lv_obj_clear_flag(objects.settings_modem_preset_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.settings_modem_preset_dropdown); - THIS->disablePanel(objects.controller_panel); - } -} + void TFTView_320x240::ui_event_timeout_button(lv_event_t * e) + { + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone) { + uint32_t timeout = THIS->db.uiConfig.screen_timeout; + char buf[32]; + if (timeout == 0) + lv_snprintf(buf, sizeof(buf), _("Timeout: off")); + else + lv_snprintf(buf, sizeof(buf), _("Timeout: %ds"), timeout); + lv_label_set_text(objects.settings_screen_timeout_label, buf); + lv_obj_clear_flag(objects.settings_screen_timeout_panel, LV_OBJ_FLAG_HIDDEN); + lv_slider_set_value(objects.screen_timeout_slider, timeout, LV_ANIM_OFF); + lv_group_focus_obj(objects.screen_timeout_slider); + THIS->disablePanel(objects.controller_panel); + THIS->disablePanel(objects.tab_page_basic_settings); + THIS->activeSettings = eScreenTimeout; + } + } -void TFTView_320x240::ui_event_wifi_button(lv_event_t *e) -{ - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED && THIS->db.config.has_network && THIS->activeSettings == eNone) { - lv_textarea_set_text(objects.settings_wifi_ssid_textarea, THIS->db.config.network.wifi_ssid); - lv_textarea_set_text(objects.settings_wifi_password_textarea, THIS->db.config.network.wifi_psk); - lv_obj_clear_flag(objects.settings_wifi_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.settings_wifi_ssid_textarea); - THIS->disablePanel(objects.controller_panel); - THIS->disablePanel(objects.tab_page_basic_settings); - THIS->activeSettings = eWifi; - } -} + void TFTView_320x240::ui_event_screen_lock_button(lv_event_t * e) + { + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone) { + char buf[10]; + lv_snprintf(buf, 7, "%06d", THIS->db.uiConfig.pin_code); + lv_textarea_set_text(objects.settings_screen_lock_password_textarea, buf); + if (THIS->db.uiConfig.screen_lock) { + lv_obj_add_state(objects.settings_screen_lock_switch, LV_STATE_CHECKED); + } else { + lv_obj_remove_state(objects.settings_screen_lock_switch, LV_STATE_CHECKED); + } + if (THIS->db.uiConfig.settings_lock) { + lv_obj_add_state(objects.settings_settings_lock_switch, LV_STATE_CHECKED); + } else { + lv_obj_remove_state(objects.settings_settings_lock_switch, LV_STATE_CHECKED); + } -void TFTView_320x240::ui_event_language_button(lv_event_t *e) -{ - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone) { - lv_dropdown_set_selected(objects.settings_language_dropdown, THIS->language2val(THIS->db.uiConfig.language)); - lv_obj_clear_flag(objects.settings_language_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.settings_language_dropdown); - THIS->disablePanel(objects.controller_panel); - THIS->disablePanel(objects.tab_page_basic_settings); - THIS->activeSettings = eLanguage; - } -} + lv_obj_clear_flag(objects.settings_screen_lock_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.settings_screen_lock_switch); + THIS->disablePanel(objects.controller_panel); + THIS->disablePanel(objects.tab_page_basic_settings); + THIS->activeSettings = eScreenLock; + } + } -void TFTView_320x240::ui_event_channel_button(lv_event_t *e) -{ - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone) { - // primary channel is not necessarily channel[0], setup ui with primary on top - int pos = 1; - for (int i = 0; i < c_max_channels; i++) { - meshtastic_Channel &ch = THIS->db.channel[i]; - if (ch.has_settings && ch.role != meshtastic_Channel_Role_DISABLED) { - const char *channelName = ch.settings.name; - if (ch.settings.name[0] == '\0' && ch.settings.psk.size == 1 && ch.settings.psk.bytes[0] == 0x01) { - channelName = LoRaPresets::modemPresetToString(THIS->db.config.lora.modem_preset); + void TFTView_320x240::ui_event_input_button(lv_event_t * e) + { + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone) { + std::vector ptr_events = THIS->inputdriver->getPointerDevices(); + std::string ptr_dropdown = _("none"); + for (std::string &s : ptr_events) { + ptr_dropdown += '\n' + s; } - if (ch.role == meshtastic_Channel_Role_PRIMARY) { - THIS->ch_label[0]->user_data = (void *)i; - lv_label_set_text(THIS->ch_label[0], channelName); + lv_dropdown_set_options(objects.settings_mouse_input_dropdown, ptr_dropdown.c_str()); + std::string current_ptr = THIS->inputdriver->getCurrentPointerDevice(); + uint32_t ptrOption = lv_dropdown_get_option_index(objects.settings_mouse_input_dropdown, current_ptr.c_str()); + lv_dropdown_set_selected(objects.settings_mouse_input_dropdown, ptrOption); + + std::vector kbd_events = THIS->inputdriver->getKeyboardDevices(); + std::string kbd_dropdown = _("none"); + for (std::string &s : kbd_events) { + kbd_dropdown += '\n' + s; + } + lv_dropdown_set_options(objects.settings_keyboard_input_dropdown, kbd_dropdown.c_str()); + std::string current_kbd = THIS->inputdriver->getCurrentKeyboardDevice(); + uint32_t kbdOption = lv_dropdown_get_option_index(objects.settings_keyboard_input_dropdown, current_kbd.c_str()); + lv_dropdown_set_selected(objects.settings_keyboard_input_dropdown, kbdOption); + + lv_dropdown_get_selected_str(objects.settings_keyboard_input_dropdown, THIS->old_val1_scratch, + sizeof(old_val1_scratch)); + lv_dropdown_get_selected_str(objects.settings_mouse_input_dropdown, THIS->old_val2_scratch, + sizeof(old_val2_scratch)); + + lv_obj_clear_flag(objects.settings_input_control_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.settings_mouse_input_dropdown); + THIS->disablePanel(objects.controller_panel); + THIS->disablePanel(objects.tab_page_basic_settings); + THIS->activeSettings = eInputControl; + } + } + + void TFTView_320x240::ui_event_alert_button(lv_event_t * e) + { + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone && + THIS->db.module_config.has_external_notification) { + bool alert_enabled = THIS->db.module_config.external_notification.alert_message_buzzer && + THIS->db.module_config.external_notification.enabled && !THIS->db.silent; + if (alert_enabled) { + lv_obj_add_state(objects.settings_alert_buzzer_switch, LV_STATE_CHECKED); } else { - THIS->ch_label[pos]->user_data = (void *)i; - lv_label_set_text(THIS->ch_label[pos++], channelName); + lv_obj_remove_state(objects.settings_alert_buzzer_switch, LV_STATE_CHECKED); + } + // populate dropdown + if (lv_dropdown_get_option_count(objects.settings_ringtone_dropdown) <= 1) { + for (int i = 2; i < numRingtones; i++) { + lv_dropdown_add_option(objects.settings_ringtone_dropdown, ringtone[i].name, i); + } } + + lv_dropdown_set_selected(objects.settings_ringtone_dropdown, THIS->db.uiConfig.ring_tone_id - 1); + lv_obj_clear_flag(objects.settings_alert_buzzer_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.settings_alert_buzzer_switch); + THIS->disablePanel(objects.controller_panel); + THIS->disablePanel(objects.tab_page_basic_settings); + THIS->activeSettings = eAlertBuzzer; } } - for (int i = pos; i < c_max_channels; i++) { - THIS->ch_label[i]->user_data = (void *)-1; - lv_label_set_text(THIS->ch_label[i], _("")); + + // backup & restore + void TFTView_320x240::ui_event_backup_button(lv_event_t * e) + { + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone) { + lv_obj_clear_flag(objects.settings_backup_restore_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.settings_backup_restore_dropdown); + THIS->disablePanel(objects.controller_panel); + THIS->disablePanel(objects.tab_page_basic_settings); + THIS->activeSettings = eBackupRestore; + } } - lv_obj_clear_flag(objects.settings_channel_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.settings_channel0_button); - THIS->disablePanel(objects.controller_panel); - THIS->disablePanel(objects.tab_page_basic_settings); - THIS->activeSettings = eChannel; - // create scratch channels to store temporary changes until cancelled or applied - THIS->channel_scratch = new meshtastic_Channel[c_max_channels]; - for (int i = 0; i < c_max_channels; i++) { - THIS->channel_scratch[i] = THIS->db.channel[i]; + // configuration reset + void TFTView_320x240::ui_event_reset_button(lv_event_t * e) + { + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone) { + lv_obj_clear_flag(objects.settings_reset_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.settings_reset_dropdown); + THIS->disablePanel(objects.controller_panel); + THIS->disablePanel(objects.tab_page_basic_settings); + THIS->activeSettings = eReset; + } } - } -} -void TFTView_320x240::ui_event_brightness_button(lv_event_t *e) -{ - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone) { - char buf[20]; - uint32_t brightness = round(THIS->db.uiConfig.screen_brightness * 100.0 / 255.0); - lv_snprintf(buf, sizeof(buf), _("Brightness: %d%%"), brightness); - lv_label_set_text(objects.settings_brightness_label, buf); - lv_slider_set_value(objects.brightness_slider, brightness, LV_ANIM_OFF); - lv_obj_clear_flag(objects.settings_brightness_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.brightness_slider); - THIS->disablePanel(objects.controller_panel); - THIS->disablePanel(objects.tab_page_basic_settings); - THIS->activeSettings = eScreenBrightness; - } -} + // reboot / shutdown + void TFTView_320x240::ui_event_reboot_button(lv_event_t * e) + { + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone) { + lv_obj_remove_flag(objects.boot_logo_button, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(objects.bluetooth_button, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(objects.boot_logo_arc, LV_OBJ_FLAG_HIDDEN); + lv_screen_load_anim(objects.boot_screen, LV_SCR_LOAD_ANIM_FADE_IN, 1000, 0, false); + lv_obj_clear_flag(objects.reboot_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.cancel_reboot_button); + THIS->disablePanel(objects.controller_panel); + THIS->disablePanel(objects.tab_page_basic_settings); + THIS->activeSettings = eReboot; + } + } -void TFTView_320x240::ui_event_theme_button(lv_event_t *e) -{ - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone) { - lv_dropdown_set_selected(objects.settings_theme_dropdown, THIS->db.uiConfig.theme); - lv_obj_clear_flag(objects.settings_theme_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.settings_theme_dropdown); - THIS->disablePanel(objects.controller_panel); - THIS->disablePanel(objects.tab_page_basic_settings); - THIS->activeSettings = eTheme; - } -} + void TFTView_320x240::ui_event_device_reboot_button(lv_event_t * e) + { + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED) { + THIS->controller->requestReboot(5, THIS->ownNode); + lv_screen_load_anim(objects.blank_screen, LV_SCR_LOAD_ANIM_FADE_OUT, 4000, 1000, false); + lv_obj_add_flag(objects.reboot_panel, LV_OBJ_FLAG_HIDDEN); + if (THIS->controller->isStandalone()) { + lv_timer_create(timer_event_reboot, 4000, NULL); + } + } + } -void TFTView_320x240::ui_event_calibration_button(lv_event_t *e) -{ - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED) { - lv_screen_load_anim(objects.calibration_screen, LV_SCR_LOAD_ANIM_NONE, 0, 0, false); - } -} + void TFTView_320x240::ui_event_device_progmode_button(lv_event_t * e) + { + static bool ignoreClicked = false; + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED) { + if (ignoreClicked) { // prevent long press to enter this setting + ignoreClicked = false; + return; + } -void TFTView_320x240::ui_event_timeout_button(lv_event_t *e) -{ - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone) { - uint32_t timeout = THIS->db.uiConfig.screen_timeout; - char buf[32]; - if (timeout == 0) - lv_snprintf(buf, sizeof(buf), _("Timeout: off")); - else - lv_snprintf(buf, sizeof(buf), _("Timeout: %ds"), timeout); - lv_label_set_text(objects.settings_screen_timeout_label, buf); - lv_obj_clear_flag(objects.settings_screen_timeout_panel, LV_OBJ_FLAG_HIDDEN); - lv_slider_set_value(objects.screen_timeout_slider, timeout, LV_ANIM_OFF); - lv_group_focus_obj(objects.screen_timeout_slider); - THIS->disablePanel(objects.controller_panel); - THIS->disablePanel(objects.tab_page_basic_settings); - THIS->activeSettings = eScreenTimeout; - } -} + meshtastic_Config_NetworkConfig &network = THIS->db.config.network; + if (network.wifi_enabled) { + network.wifi_enabled = false; + THIS->controller->sendConfig(meshtastic_Config_NetworkConfig{network}); + } + meshtastic_Config_BluetoothConfig &bluetooth = THIS->db.config.bluetooth; + bluetooth.mode = meshtastic_Config_BluetoothConfig_PairingMode_FIXED_PIN; + bluetooth.fixed_pin = random(100000, 999999); + bluetooth.enabled = true; + THIS->controller->sendConfig(meshtastic_Config_BluetoothConfig{bluetooth}, THIS->ownNode); + lv_screen_load_anim(objects.blank_screen, LV_SCR_LOAD_ANIM_FADE_OUT, 4000, 1000, false); + lv_obj_add_flag(objects.reboot_panel, LV_OBJ_FLAG_HIDDEN); + } else if (event_code == LV_EVENT_LONG_PRESSED) { + if (!THIS->controller->isStandalone()) { +#if defined(HAS_SCREEN) && HAS_SCREEN == 1 + ignoreClicked = true; + // open dialog + lv_obj_remove_flag(objects.settings_reboot_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.settings_reboot_panel); + THIS->activeSettings = eDisplayMode; +#endif + } + } + } -void TFTView_320x240::ui_event_screen_lock_button(lv_event_t *e) -{ - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone) { - char buf[10]; - lv_snprintf(buf, 7, "%06d", THIS->db.uiConfig.pin_code); - lv_textarea_set_text(objects.settings_screen_lock_password_textarea, buf); - if (THIS->db.uiConfig.screen_lock) { - lv_obj_add_state(objects.settings_screen_lock_switch, LV_STATE_CHECKED); - } else { - lv_obj_remove_state(objects.settings_screen_lock_switch, LV_STATE_CHECKED); + void TFTView_320x240::ui_event_device_shutdown_button(lv_event_t * e) + { + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED) { + THIS->controller->requestShutdown(5, THIS->ownNode); + lv_screen_load_anim(objects.blank_screen, LV_SCR_LOAD_ANIM_FADE_OUT, 4000, 1000, false); + lv_obj_add_flag(objects.reboot_panel, LV_OBJ_FLAG_HIDDEN); + if (THIS->controller->isStandalone()) { + lv_timer_create(timer_event_shutdown, 4000, NULL); + } + } } - if (THIS->db.uiConfig.settings_lock) { - lv_obj_add_state(objects.settings_settings_lock_switch, LV_STATE_CHECKED); - } else { - lv_obj_remove_state(objects.settings_settings_lock_switch, LV_STATE_CHECKED); + + void TFTView_320x240::ui_event_device_cancel_button(lv_event_t * e) + { + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED) { + lv_screen_load_anim(objects.main_screen, LV_SCR_LOAD_ANIM_NONE, 0, 0, false); + lv_obj_add_flag(objects.reboot_panel, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(objects.settings_reboot_panel, LV_OBJ_FLAG_HIDDEN); + THIS->enablePanel(objects.controller_panel); + THIS->enablePanel(objects.tab_page_basic_settings); + lv_group_focus_obj(objects.basic_settings_reboot_button); + THIS->activeSettings = eNone; + } } - lv_obj_clear_flag(objects.settings_screen_lock_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.settings_screen_lock_switch); - THIS->disablePanel(objects.controller_panel); - THIS->disablePanel(objects.tab_page_basic_settings); - THIS->activeSettings = eScreenLock; - } -} + void TFTView_320x240::ui_event_modify_channel(lv_event_t * e) + { + static bool ignoreClicked = false; + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eChannel) { + if (ignoreClicked) { // prevent long press to enter this setting + ignoreClicked = false; + return; + } + uint32_t btn_id = (unsigned long)e->user_data; + int8_t ch = (signed long)THIS->ch_label[btn_id]->user_data; + if (ch != -1) { + meshtastic_ChannelSettings_psk_t &psk = THIS->channel_scratch[ch].settings.psk; + std::string base64 = THIS->pskToBase64(psk.bytes, psk.size); + lv_textarea_set_text(objects.settings_modify_channel_psk_textarea, base64.c_str()); + lv_textarea_set_text(objects.settings_modify_channel_name_textarea, THIS->channel_scratch[ch].settings.name); + objects.settings_modify_channel_name_textarea->user_data = (void *)btn_id; + } else { + for (int i = 0; i < c_max_channels; i++) { + if (THIS->channel_scratch[i].role == meshtastic_Channel_Role_DISABLED) { + // the first created channel is PRIMARY + bool found = false; + for (int j = 0; j < c_max_channels; j++) { + if (THIS->channel_scratch[j].role == meshtastic_Channel_Role_PRIMARY) { + found = true; + break; + } + } + if (!found) { + THIS->channel_scratch[i].role = meshtastic_Channel_Role_PRIMARY; + if (i == 0) { + btn_id = 0; // place on top + } else { + // FIXME: swap ids as in long press + ILOG_ERROR("node does not have primary channel!"); + } + } else + THIS->channel_scratch[i].role = meshtastic_Channel_Role_SECONDARY; + + lv_textarea_set_text(objects.settings_modify_channel_psk_textarea, ""); + lv_textarea_set_text(objects.settings_modify_channel_name_textarea, ""); + THIS->ch_label[btn_id]->user_data = (void *)i; + objects.settings_modify_channel_name_textarea->user_data = (void *)btn_id; + break; + } + } + } -void TFTView_320x240::ui_event_input_button(lv_event_t *e) -{ - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone) { - std::vector ptr_events = THIS->inputdriver->getPointerDevices(); - std::string ptr_dropdown = _("none"); - for (std::string &s : ptr_events) { - ptr_dropdown += '\n' + s; - } - lv_dropdown_set_options(objects.settings_mouse_input_dropdown, ptr_dropdown.c_str()); - std::string current_ptr = THIS->inputdriver->getCurrentPointerDevice(); - uint32_t ptrOption = lv_dropdown_get_option_index(objects.settings_mouse_input_dropdown, current_ptr.c_str()); - lv_dropdown_set_selected(objects.settings_mouse_input_dropdown, ptrOption); - - std::vector kbd_events = THIS->inputdriver->getKeyboardDevices(); - std::string kbd_dropdown = _("none"); - for (std::string &s : kbd_events) { - kbd_dropdown += '\n' + s; - } - lv_dropdown_set_options(objects.settings_keyboard_input_dropdown, kbd_dropdown.c_str()); - std::string current_kbd = THIS->inputdriver->getCurrentKeyboardDevice(); - uint32_t kbdOption = lv_dropdown_get_option_index(objects.settings_keyboard_input_dropdown, current_kbd.c_str()); - lv_dropdown_set_selected(objects.settings_keyboard_input_dropdown, kbdOption); - - lv_dropdown_get_selected_str(objects.settings_keyboard_input_dropdown, THIS->old_val1_scratch, sizeof(old_val1_scratch)); - lv_dropdown_get_selected_str(objects.settings_mouse_input_dropdown, THIS->old_val2_scratch, sizeof(old_val2_scratch)); - - lv_obj_clear_flag(objects.settings_input_control_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.settings_mouse_input_dropdown); - THIS->disablePanel(objects.controller_panel); - THIS->disablePanel(objects.tab_page_basic_settings); - THIS->activeSettings = eInputControl; + THIS->disablePanel(objects.settings_channel_panel); + lv_obj_clear_flag(objects.settings_modify_channel_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.settings_modify_channel_name_textarea); + THIS->activeSettings = eModifyChannel; + } +#if 0 // TODO: simple swap not allowed: primary channel must be id 0 + else if (event_code == LV_EVENT_LONG_PRESSED && THIS->activeSettings == eChannel) { + ignoreClicked = true; + // make channel primary on long press; swap with current primary (role, id and name) + uint8_t btn_id = (uint8_t)(unsigned long)e->user_data; + int8_t ch = (signed long)THIS->ch_label[btn_id]->user_data; + if (btn_id != 0 && ch != -1) { + int32_t primary_id = (signed long)THIS->ch_label[0]->user_data; + THIS->channel_scratch[primary_id].role = meshtastic_Channel_Role_SECONDARY; + THIS->channel_scratch[ch].role = meshtastic_Channel_Role_PRIMARY; + THIS->ch_label[0]->user_data = (void *)(uint32_t)ch; + THIS->ch_label[btn_id]->user_data = (void *)primary_id; + lv_label_set_text(THIS->ch_label[0], THIS->channel_scratch[ch].settings.name); + lv_label_set_text(THIS->ch_label[btn_id], THIS->channel_scratch[primary_id].settings.name); + } } -} - -void TFTView_320x240::ui_event_alert_button(lv_event_t *e) -{ - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone && THIS->db.module_config.has_external_notification) { - bool alert_enabled = THIS->db.module_config.external_notification.alert_message_buzzer && - THIS->db.module_config.external_notification.enabled && !THIS->db.silent; - if (alert_enabled) { - lv_obj_add_state(objects.settings_alert_buzzer_switch, LV_STATE_CHECKED); - } else { - lv_obj_remove_state(objects.settings_alert_buzzer_switch, LV_STATE_CHECKED); +#endif } - // populate dropdown - if (lv_dropdown_get_option_count(objects.settings_ringtone_dropdown) <= 1) { - for (int i = 2; i < numRingtones; i++) { - lv_dropdown_add_option(objects.settings_ringtone_dropdown, ringtone[i].name, i); + + void TFTView_320x240::ui_event_generate_psk(lv_event_t * e) + { + std::string base64 = lv_textarea_get_text(objects.settings_modify_channel_psk_textarea); + if (base64.size() == 0 || THIS->qr) { + meshtastic_ChannelSettings_psk_t psk{.size = 32}; + std::mt19937 generator(millis() + psk.bytes[7]); // Mersenne Twister number generator + for (int i = 0; i < 8; i++) { + int r = generator(); + memcpy(&psk.bytes[i * 4], &r, 4); + } + base64 = THIS->pskToBase64(psk.bytes, psk.size); + lv_textarea_set_text(objects.settings_modify_channel_psk_textarea, base64.c_str()); } + + std::string base64Https = base64; + for (char &c : base64Https) { + if (c == '+') + c = '-'; + else if (c == '/') + c = '_'; + else if (c == '=') + c = '\0'; // remove paddings at the end of the url + } + std::string qr = "https://meshtastic.org/e/#" + base64Https; + lv_obj_remove_flag(objects.settings_modify_channel_qr_panel, LV_OBJ_FLAG_HIDDEN); + THIS->qr = THIS->showQrCode(objects.settings_modify_channel_qr_panel, qr.c_str()); } - lv_dropdown_set_selected(objects.settings_ringtone_dropdown, THIS->db.uiConfig.ring_tone_id - 1); - lv_obj_clear_flag(objects.settings_alert_buzzer_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.settings_alert_buzzer_switch); - THIS->disablePanel(objects.controller_panel); - THIS->disablePanel(objects.tab_page_basic_settings); - THIS->activeSettings = eAlertBuzzer; - } -} + void TFTView_320x240::ui_event_qr_code(lv_event_t * e) + { + lv_obj_add_flag(objects.settings_modify_channel_qr_panel, LV_OBJ_FLAG_HIDDEN); + lv_obj_delete(THIS->qr); + THIS->qr = nullptr; + } -// backup & restore -void TFTView_320x240::ui_event_backup_button(lv_event_t *e) -{ - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone) { - lv_obj_clear_flag(objects.settings_backup_restore_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.settings_backup_restore_dropdown); - THIS->disablePanel(objects.controller_panel); - THIS->disablePanel(objects.tab_page_basic_settings); - THIS->activeSettings = eBackupRestore; - } -} + void TFTView_320x240::ui_event_delete_channel(lv_event_t * e) + { + lv_textarea_set_text(objects.settings_modify_channel_psk_textarea, ""); + lv_textarea_set_text(objects.settings_modify_channel_name_textarea, ""); + } -// configuration reset -void TFTView_320x240::ui_event_reset_button(lv_event_t *e) -{ - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone) { - lv_obj_clear_flag(objects.settings_reset_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.settings_reset_dropdown); - THIS->disablePanel(objects.controller_panel); - THIS->disablePanel(objects.tab_page_basic_settings); - THIS->activeSettings = eReset; - } -} + void TFTView_320x240::ui_event_calibration_screen_loaded(lv_event_t * e) + { + uint16_t *parameters = (uint16_t *)THIS->db.uiConfig.calibration_data.bytes; + memset(parameters, 0, 16); // clear all calibration data + bool done = THIS->displaydriver->calibrate(parameters); + THIS->db.uiConfig.calibration_data.size = 16; + char buf[32]; + lv_snprintf(buf, sizeof(buf), _("Screen Calibration: %s"), done ? _("done") : _("default")); + lv_label_set_text(objects.basic_settings_calibration_label, buf); + lv_screen_load_anim(objects.main_screen, LV_SCR_LOAD_ANIM_FADE_ON, 200, 0, false); + THIS->controller->storeUIConfig(THIS->db.uiConfig); + } -// reboot / shutdown -void TFTView_320x240::ui_event_reboot_button(lv_event_t *e) -{ - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone) { - lv_obj_remove_flag(objects.boot_logo_button, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(objects.bluetooth_button, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(objects.boot_logo_arc, LV_OBJ_FLAG_HIDDEN); - lv_screen_load_anim(objects.boot_screen, LV_SCR_LOAD_ANIM_FADE_IN, 1000, 0, false); - lv_obj_clear_flag(objects.reboot_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.cancel_reboot_button); - THIS->disablePanel(objects.controller_panel); - THIS->disablePanel(objects.tab_page_basic_settings); - THIS->activeSettings = eReboot; - } -} + void TFTView_320x240::ui_event_pin_screen_button(lv_event_t * e) + { + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED && lv_scr_act() == objects.lock_screen) { + static const char *hidden[7] = {"o o o o o o", "* o o o o o", "* * o o o o", "* * * o o o", + "* * * * o o", "* * * * * o", "* * * * * *"}; + static char pinEntered[7]{}; + lv_obj_t *obj = (lv_obj_t *)lv_event_get_target(e); + uint32_t id = lv_buttonmatrix_get_selected_button(obj); + const char *key = lv_buttonmatrix_get_button_text(obj, id); + switch (*key) { + case 'X': { + pinKeys = 0; + lv_label_set_text(objects.lock_screen_digits_label, hidden[pinKeys]); + if (!screenLocked) { + lv_screen_load_anim(objects.main_screen, LV_SCR_LOAD_ANIM_FADE_IN, 100, 0, false); + } else { + // TODO: init screen saver + } + break; + } + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': { + if (pinKeys < 6) { + pinEntered[pinKeys++] = *key; + lv_label_set_text(objects.lock_screen_digits_label, hidden[pinKeys]); + + char buf[10]; + lv_snprintf(buf, 7, "%06d", THIS->db.uiConfig.pin_code); + if (pinKeys == 6 && strcmp(pinEntered, buf) == 0) { + // unlock screen + pinKeys = 0; + screenLocked = false; + lv_obj_clear_flag(objects.tab_page_basic_settings, LV_OBJ_FLAG_HIDDEN); + lv_screen_load_anim(objects.main_screen, LV_SCR_LOAD_ANIM_FADE_IN, 100, 0, false); + lv_label_set_text(objects.lock_screen_digits_label, hidden[pinKeys]); + } + } + break; + } + case 'D': { + if (pinKeys > 0) { + pinEntered[--pinKeys] = '\0'; + lv_label_set_text(objects.lock_screen_digits_label, hidden[pinKeys]); + } + break; + } + default: + break; + } + } + } -void TFTView_320x240::ui_event_device_reboot_button(lv_event_t *e) -{ - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED) { - THIS->controller->requestReboot(5, THIS->ownNode); - lv_screen_load_anim(objects.blank_screen, LV_SCR_LOAD_ANIM_FADE_OUT, 4000, 1000, false); - lv_obj_add_flag(objects.reboot_panel, LV_OBJ_FLAG_HIDDEN); - if (THIS->controller->isStandalone()) { - lv_timer_create(timer_event_reboot, 4000, NULL); + void TFTView_320x240::ui_event_backup_restore_radio_button(lv_event_t * e) + { + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED) { + lv_obj_remove_state(objects.settings_backup_checkbox, LV_STATE_CHECKED); + lv_obj_remove_state(objects.settings_restore_checkbox, LV_STATE_CHECKED); + lv_obj_add_state(lv_event_get_target_obj(e), LV_STATE_CHECKED); + } } - } -} -void TFTView_320x240::ui_event_device_progmode_button(lv_event_t *e) -{ - static bool ignoreClicked = false; - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED) { - if (ignoreClicked) { // prevent long press to enter this setting - ignoreClicked = false; - return; + void TFTView_320x240::ui_event_zoomSlider(lv_event_t * e) + { + THIS->map->setZoom(lv_slider_get_value(objects.zoom_slider)); + THIS->updateLocationMap(THIS->map->getObjectsOnMap()); } - meshtastic_Config_NetworkConfig &network = THIS->db.config.network; - if (network.wifi_enabled) { - network.wifi_enabled = false; - THIS->controller->sendConfig(meshtastic_Config_NetworkConfig{network}); + void TFTView_320x240::ui_event_zoomIn(lv_event_t * e) + { + THIS->map->setZoom(MapTileSettings::getZoomLevel() + 1); + THIS->updateLocationMap(THIS->map->getObjectsOnMap()); } - meshtastic_Config_BluetoothConfig &bluetooth = THIS->db.config.bluetooth; - bluetooth.mode = meshtastic_Config_BluetoothConfig_PairingMode_FIXED_PIN; - bluetooth.fixed_pin = random(100000, 999999); - bluetooth.enabled = true; - THIS->controller->sendConfig(meshtastic_Config_BluetoothConfig{bluetooth}, THIS->ownNode); - lv_screen_load_anim(objects.blank_screen, LV_SCR_LOAD_ANIM_FADE_OUT, 4000, 1000, false); - lv_obj_add_flag(objects.reboot_panel, LV_OBJ_FLAG_HIDDEN); - } else if (event_code == LV_EVENT_LONG_PRESSED) { - if (!THIS->controller->isStandalone()) { -#if defined(HAS_SCREEN) && HAS_SCREEN == 1 - ignoreClicked = true; - // open dialog - lv_obj_remove_flag(objects.settings_reboot_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.settings_reboot_panel); - THIS->activeSettings = eDisplayMode; -#endif + + void TFTView_320x240::ui_event_zoomOut(lv_event_t * e) + { + THIS->map->setZoom(MapTileSettings::getZoomLevel() - 1); + THIS->updateLocationMap(THIS->map->getObjectsOnMap()); } - } -} -void TFTView_320x240::ui_event_device_shutdown_button(lv_event_t *e) -{ - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED) { - THIS->controller->requestShutdown(5, THIS->ownNode); - lv_screen_load_anim(objects.blank_screen, LV_SCR_LOAD_ANIM_FADE_OUT, 4000, 1000, false); - lv_obj_add_flag(objects.reboot_panel, LV_OBJ_FLAG_HIDDEN); - if (THIS->controller->isStandalone()) { - lv_timer_create(timer_event_shutdown, 4000, NULL); + void TFTView_320x240::ui_event_lockGps(lv_event_t * e) + { + bool gpsLocked = lv_obj_has_state(objects.gps_lock_button, LV_STATE_CHECKED); + THIS->map->setLocked(gpsLocked); + THIS->db.uiConfig.map_data.follow_gps = gpsLocked; + THIS->controller->storeUIConfig(THIS->db.uiConfig); } - } -} -void TFTView_320x240::ui_event_device_cancel_button(lv_event_t *e) -{ - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED) { - lv_screen_load_anim(objects.main_screen, LV_SCR_LOAD_ANIM_NONE, 0, 0, false); - lv_obj_add_flag(objects.reboot_panel, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(objects.settings_reboot_panel, LV_OBJ_FLAG_HIDDEN); - THIS->enablePanel(objects.controller_panel); - THIS->enablePanel(objects.tab_page_basic_settings); - lv_group_focus_obj(objects.basic_settings_reboot_button); - THIS->activeSettings = eNone; - } -} + void TFTView_320x240::ui_event_mapBrightnessSlider(lv_event_t * e) + { + uint32_t br = lv_slider_get_value(objects.map_brightness_slider); + lv_obj_set_style_bg_color(objects.map_panel, lv_color_make(br, br, br), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(objects.raw_map_panel, lv_color_make(br, br, br), LV_PART_MAIN | LV_STATE_DEFAULT); + } -void TFTView_320x240::ui_event_modify_channel(lv_event_t *e) -{ - static bool ignoreClicked = false; - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eChannel) { - if (ignoreClicked) { // prevent long press to enter this setting - ignoreClicked = false; - return; + void TFTView_320x240::ui_event_mapContrastSlider(lv_event_t * e) + { + uint32_t ct = lv_slider_get_value(objects.map_contrast_slider); + lv_obj_set_style_opa(objects.raw_map_panel, ct, LV_PART_MAIN | LV_STATE_DEFAULT); } - uint32_t btn_id = (unsigned long)e->user_data; - int8_t ch = (signed long)THIS->ch_label[btn_id]->user_data; - if (ch != -1) { - meshtastic_ChannelSettings_psk_t &psk = THIS->channel_scratch[ch].settings.psk; - std::string base64 = THIS->pskToBase64(psk.bytes, psk.size); - lv_textarea_set_text(objects.settings_modify_channel_psk_textarea, base64.c_str()); - lv_textarea_set_text(objects.settings_modify_channel_name_textarea, THIS->channel_scratch[ch].settings.name); - objects.settings_modify_channel_name_textarea->user_data = (void *)btn_id; - } else { - for (int i = 0; i < c_max_channels; i++) { - if (THIS->channel_scratch[i].role == meshtastic_Channel_Role_DISABLED) { - // the first created channel is PRIMARY - bool found = false; - for (int j = 0; j < c_max_channels; j++) { - if (THIS->channel_scratch[j].role == meshtastic_Channel_Role_PRIMARY) { - found = true; - break; - } - } - if (!found) { - THIS->channel_scratch[i].role = meshtastic_Channel_Role_PRIMARY; - if (i == 0) { - btn_id = 0; // place on top - } else { - // FIXME: swap ids as in long press - ILOG_ERROR("node does not have primary channel!"); - } - } else - THIS->channel_scratch[i].role = meshtastic_Channel_Role_SECONDARY; - lv_textarea_set_text(objects.settings_modify_channel_psk_textarea, ""); - lv_textarea_set_text(objects.settings_modify_channel_name_textarea, ""); - THIS->ch_label[btn_id]->user_data = (void *)i; - objects.settings_modify_channel_name_textarea->user_data = (void *)btn_id; + void TFTView_320x240::ui_event_map_style_dropdown(lv_event_t * e) + { + lv_dropdown_get_selected_str(objects.map_style_dropdown, THIS->db.uiConfig.map_data.style, + sizeof(THIS->db.uiConfig.map_data.style)); + MapTileSettings::setTileStyle(THIS->db.uiConfig.map_data.style); + THIS->controller->storeUIConfig(THIS->db.uiConfig); + lv_obj_add_flag(objects.map_osd_panel, LV_OBJ_FLAG_HIDDEN); + THIS->map->forceRedraw(); + } + + void TFTView_320x240::ui_event_mapNodeButton(lv_event_t * e) + { + // navigate to node in node list + uint32_t nodeNum = (unsigned long)e->user_data; + ILOG_DEBUG("map node %08x", nodeNum); + lv_obj_t *panel = THIS->nodes[nodeNum]; + THIS->ui_set_active(objects.nodes_button, objects.nodes_panel, objects.top_nodes_panel); + lv_obj_scroll_to_view(panel, LV_ANIM_ON); + if (panel != currentPanel) + ui_event_NodeButton(e); + } + + void TFTView_320x240::ui_event_chatNodeButton(lv_event_t * e) + { + uint32_t nodeNum = (unsigned long)e->user_data; + auto it = THIS->nodes.find(nodeNum); + if (it != THIS->nodes.end()) { + lv_obj_t *panel = it->second; + THIS->ui_set_active(objects.nodes_button, objects.nodes_panel, objects.top_nodes_panel); + lv_obj_scroll_to_view(panel, LV_ANIM_ON); + if (panel != currentPanel) + ui_event_NodeButton(e); + } + } + + void TFTView_320x240::ui_event_positionButton(lv_event_t * e) + { + // navigate to position in map + lv_obj_t *p = (lv_obj_t *)e->user_data; + int32_t lat = (long)p->LV_OBJ_IDX(node_pos1_idx)->user_data; + int32_t lon = (long)p->LV_OBJ_IDX(node_pos2_idx)->user_data; + if (lat && lon) { + THIS->ui_set_active(objects.map_button, objects.map_panel, objects.top_map_panel); + if (!THIS->map) { + THIS->loadMap(); + } + THIS->map->setScrolledPosition(lat * 1e-7, lon * 1e-7); + } + } + + void TFTView_320x240::ui_screen_event_cb(lv_event_t * e) + { + if (THIS->activePanel == objects.map_panel) { + lv_dir_t dir = lv_indev_get_gesture_dir(lv_indev_active()); + switch (dir) { + case LV_DIR_LEFT: + e->user_data = (void *)6; + break; + case LV_DIR_RIGHT: + e->user_data = (void *)4; + break; + case LV_DIR_TOP: + e->user_data = (void *)2; + break; + case LV_DIR_BOTTOM: + e->user_data = (void *)8; + break; + default: break; } + ILOG_DEBUG("gesture: %d", (uint16_t)dir); + THIS->ui_event_arrow(e); } } - THIS->disablePanel(objects.settings_channel_panel); - lv_obj_clear_flag(objects.settings_modify_channel_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.settings_modify_channel_name_textarea); - THIS->activeSettings = eModifyChannel; - } -#if 0 // TODO: simple swap not allowed: primary channel must be id 0 - else if (event_code == LV_EVENT_LONG_PRESSED && THIS->activeSettings == eChannel) { - ignoreClicked = true; - // make channel primary on long press; swap with current primary (role, id and name) - uint8_t btn_id = (uint8_t)(unsigned long)e->user_data; - int8_t ch = (signed long)THIS->ch_label[btn_id]->user_data; - if (btn_id != 0 && ch != -1) { - int32_t primary_id = (signed long)THIS->ch_label[0]->user_data; - THIS->channel_scratch[primary_id].role = meshtastic_Channel_Role_SECONDARY; - THIS->channel_scratch[ch].role = meshtastic_Channel_Role_PRIMARY; - THIS->ch_label[0]->user_data = (void *)(uint32_t)ch; - THIS->ch_label[btn_id]->user_data = (void *)primary_id; - lv_label_set_text(THIS->ch_label[0], THIS->channel_scratch[ch].settings.name); - lv_label_set_text(THIS->ch_label[btn_id], THIS->channel_scratch[primary_id].settings.name); + void TFTView_320x240::ui_event_arrow(lv_event_t * e) + { + if (THIS->map && THIS->map->redrawComplete()) { + uint16_t deltaX = 0; + uint16_t deltaY = 0; + ScrollDirection direction = (ScrollDirection)(unsigned long)e->user_data; + switch (direction) { + case scrollDownLeft: + deltaX = 1; + deltaY = -1; + break; + case scrollDown: + deltaX = 0; + deltaY = -1; + break; + case scrollDownRight: + deltaX = -1; + deltaY = -1; + break; + case scrollLeft: + deltaX = 1; + deltaY = 0; + break; + case scrollRight: + deltaX = -1; + deltaY = 0; + break; + case scrollUpLeft: + deltaX = 1; + deltaY = 1; + break; + case scrollUp: + deltaX = 0; + deltaY = 1; + break; + case scrollUpRight: + deltaX = -1; + deltaY = 1; + break; + default: + break; + }; + if (!THIS->map->scroll(deltaX, deltaY)) + THIS->map->forceRedraw(); + } + THIS->updateLocationMap(THIS->map->getObjectsOnMap()); } - } -#endif -} -void TFTView_320x240::ui_event_generate_psk(lv_event_t *e) -{ - std::string base64 = lv_textarea_get_text(objects.settings_modify_channel_psk_textarea); - if (base64.size() == 0 || THIS->qr) { - meshtastic_ChannelSettings_psk_t psk{.size = 32}; - std::mt19937 generator(millis() + psk.bytes[7]); // Mersenne Twister number generator - for (int i = 0; i < 8; i++) { - int r = generator(); - memcpy(&psk.bytes[i * 4], &r, 4); - } - base64 = THIS->pskToBase64(psk.bytes, psk.size); - lv_textarea_set_text(objects.settings_modify_channel_psk_textarea, base64.c_str()); - } + void TFTView_320x240::ui_event_navHome(lv_event_t * e) + { + static bool ignoreClicked = false; + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED) { + if (ignoreClicked) { // prevent long press to enter this setting + ignoreClicked = false; + return; + } + THIS->map->moveHome(); + } else if (event_code == LV_EVENT_LONG_PRESSED) { + ignoreClicked = true; + float lat, lon; + THIS->map->setHomePosition(); + THIS->map->getHomeLocation(lat, lon); + + int32_t ilat = lat * 1e7f; + int32_t ilon = lon * 1e7f; + THIS->db.uiConfig.has_map_data = true; + THIS->db.uiConfig.map_data.has_home = true; + THIS->db.uiConfig.map_data.home.latitude = ilat; + THIS->db.uiConfig.map_data.home.longitude = ilon; + THIS->db.uiConfig.map_data.home.zoom = MapTileSettings::getZoomLevel(); + THIS->controller->storeUIConfig(THIS->db.uiConfig); - std::string base64Https = base64; - for (char &c : base64Https) { - if (c == '+') - c = '-'; - else if (c == '/') - c = '_'; - else if (c == '=') - c = '\0'; // remove paddings at the end of the url - } - std::string qr = "https://meshtastic.org/e/#" + base64Https; - lv_obj_remove_flag(objects.settings_modify_channel_qr_panel, LV_OBJ_FLAG_HIDDEN); - THIS->qr = THIS->showQrCode(objects.settings_modify_channel_qr_panel, qr.c_str()); - lv_obj_add_state(objects.keyboard_button_3, LV_STATE_DISABLED); - lv_obj_add_state(objects.keyboard_button_4, LV_STATE_DISABLED); -} + meshtastic_Config_PositionConfig &position = THIS->db.config.position; + if (position.fixed_position) { + THIS->updatePosition(THIS->ownNode, ilat, ilon, 0, 0, 0); + if (position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) { + // grey out text to indicate it's a fixed position vs. actual GPS position + Themes::recolorText(objects.home_location_label, false); + THIS->controller->sendConfig( + meshtastic_Position{.latitude_i = ilat, + .longitude_i = ilon, + .time = uint32_t(VALID_TIME(THIS->actTime) ? THIS->actTime : 0), + .location_source = meshtastic_Position_LocSource_LOC_MANUAL}); + } + } + } + } -void TFTView_320x240::ui_event_qr_code(lv_event_t *e) -{ - lv_obj_remove_state(objects.keyboard_button_3, LV_STATE_DISABLED); - lv_obj_remove_state(objects.keyboard_button_4, LV_STATE_DISABLED); - lv_obj_add_flag(objects.settings_modify_channel_qr_panel, LV_OBJ_FLAG_HIDDEN); - lv_obj_delete(THIS->qr); - THIS->qr = nullptr; -} + void TFTView_320x240::loadMap(void) + { + if (!map) { +#if LV_USE_FS_ARDUINO_SD + map = new MapPanel(objects.raw_map_panel); +#elif defined(HAS_SD_MMC) + map = new MapPanel(objects.raw_map_panel, new SDCardService()); +#elif defined(HAS_SDCARD) + map = new MapPanel(objects.raw_map_panel, new SdFatService()); +#elif defined(ARCH_PORTDUINO) + map = new MapPanel(objects.raw_map_panel, new SDCardService()); // TODO: LinuxFileSystemService +#else + map = new MapPanel(objects.raw_map_panel); +#endif + map->setHomeLocationImage(objects.home_location_image); + lv_obj_add_flag(objects.home_location_image, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_event_cb(objects.home_location_image, ui_event_mapNodeButton, LV_EVENT_CLICKED, (void *)ownNode); + + // center map to GPS > home > other nodes > default location + if (db.config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) { + map->setGpsPositionImage(objects.gps_position_image); + lv_obj_clear_flag(objects.gps_position_image, LV_OBJ_FLAG_HIDDEN); + } else { + lv_obj_add_flag(objects.gps_position_image, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(objects.gps_lock_button, LV_OBJ_FLAG_HIDDEN); + } + if (hasPosition) { + if (db.uiConfig.map_data.has_home) { + map->setHomeLocation(db.uiConfig.map_data.home.latitude * 1e-7, + db.uiConfig.map_data.home.longitude * 1e-7); + map->setZoom(db.uiConfig.map_data.home.zoom); + } else { + map->setHomeLocation(myLatitude * 1e-7, myLongitude * 1e-7); + map->setZoom(13); + } + map->setGpsPosition(myLatitude * 1e-7, myLongitude * 1e-7); + } else if (db.uiConfig.map_data.has_home) { + map->setHomeLocation(db.uiConfig.map_data.home.latitude * 1e-7, db.uiConfig.map_data.home.longitude * 1e-7); + map->setZoom(db.uiConfig.map_data.home.zoom); + } else if (nodeObjects.size() >= 1) { + // no gps, no saved position then center the home location among other available nodes + std::vector sortedLat; + std::vector sortedLon; + sortedLat.reserve(nodeObjects.size()); + sortedLon.reserve(nodeObjects.size()); + for (auto it : nodeObjects) { + lv_obj_t *p = nodes[it.first]; + int32_t lat = (long)p->LV_OBJ_IDX(node_pos1_idx)->user_data; + int32_t lon = (long)p->LV_OBJ_IDX(node_pos2_idx)->user_data; + if (lat && lon) { + sortedLat.push_back(lat); + sortedLon.push_back(lon); + } + } + std::sort(sortedLat.begin(), sortedLat.end()); + std::sort(sortedLon.begin(), sortedLon.end()); + int64_t latcenter = 0; + int64_t loncenter = 0; + int32_t count = 0; + // select just the closest 60% of nodes, ignore the rest + int pp = 100 / 20; + for (int i = sortedLat.size() / pp; i < pp * sortedLat.size() / pp; i++) { + latcenter += sortedLat[i]; + loncenter += sortedLon[i]; + count++; + } + latcenter /= count; + loncenter /= count; + map->setHomeLocation(latcenter * 1e-7, loncenter * 1e-7); + + // calculate optimal zoom factor to fit in all nodes of this range + lv_obj_update_layout(objects.raw_map_panel); + float rangeDeg = 1e-7 * (sortedLon[(pp - 1) * sortedLon.size() / pp] - sortedLon[sortedLon.size() / pp]); + float distanceKm = abs(rangeDeg * 111.32 * cos(1e-7 * sortedLat[sortedLat.size() / 2])); + uint32_t zoom = sqrt(156.543034f / distanceKm * abs(cos(1e-7 * sortedLat[sortedLat.size() / 2])) * 256) + 1; + map->setZoom(zoom); + } else { + // use default location @theBigBentern + map->setZoom(3); + } -void TFTView_320x240::ui_event_delete_channel(lv_event_t *e) -{ - lv_textarea_set_text(objects.settings_modify_channel_psk_textarea, ""); - lv_textarea_set_text(objects.settings_modify_channel_name_textarea, ""); -} + if (db.uiConfig.map_data.follow_gps) { + lv_obj_set_state(objects.gps_lock_button, LV_STATE_CHECKED, true); + map->setLocked(true); + } -void TFTView_320x240::ui_event_calibration_screen_loaded(lv_event_t *e) -{ - uint16_t *parameters = (uint16_t *)THIS->db.uiConfig.calibration_data.bytes; - memset(parameters, 0, 16); // clear all calibration data - bool done = THIS->displaydriver->calibrate(parameters); - THIS->db.uiConfig.calibration_data.size = 16; - char buf[32]; - lv_snprintf(buf, sizeof(buf), _("Screen Calibration: %s"), done ? _("done") : _("default")); - lv_label_set_text(objects.basic_settings_calibration_label, buf); - lv_screen_load_anim(objects.main_screen, LV_SCR_LOAD_ANIM_FADE_ON, 200, 0, false); - THIS->controller->storeUIConfig(THIS->db.uiConfig); -} + // finally add all node images to the map + if (!nodeObjects.empty()) { + for (auto it : nodeObjects) { + lv_obj_t *p = nodes[it.first]; + float lat = 1e-7 * (long)p->LV_OBJ_IDX(node_pos1_idx)->user_data; + float lon = 1e-7 * (long)p->LV_OBJ_IDX(node_pos2_idx)->user_data; + map->add(it.first, lat, lon, drawObjectCB); + lv_obj_add_flag(it.second, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_event_cb(it.second, ui_event_mapNodeButton, LV_EVENT_CLICKED, (void *)it.first); + } + } + updateLocationMap(map->getObjectsOnMap()); + } -void TFTView_320x240::ui_event_pin_screen_button(lv_event_t *e) -{ - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED && lv_scr_act() == objects.lock_screen) { - static const char *hidden[7] = {"o o o o o o", "* o o o o o", "* * o o o o", "* * * o o o", - "* * * * o o", "* * * * * o", "* * * * * *"}; - static char pinEntered[7]{}; - lv_obj_t *obj = (lv_obj_t *)lv_event_get_target(e); - uint32_t id = lv_buttonmatrix_get_selected_button(obj); - const char *key = lv_buttonmatrix_get_button_text(obj, id); - switch (*key) { - case 'X': { - pinKeys = 0; - lv_label_set_text(objects.lock_screen_digits_label, hidden[pinKeys]); - if (!screenLocked) { - lv_screen_load_anim(objects.main_screen, LV_SCR_LOAD_ANIM_FADE_IN, 100, 0, false); + if (sdCard) { + if (!sdCard->isUpdated()) { + map->setNoTileImage(&img_no_tile_image); + std::set mapStyles = sdCard->loadMapStyles(MapTileSettings::getPrefix()); + if (mapStyles.find("/map") != mapStyles.end()) { + // no styles found, but the /map directory, so use it + MapTileSettings::setPrefix("/map"); + MapTileSettings::setTileStyle(""); + lv_obj_add_flag(objects.map_style_dropdown, LV_OBJ_FLAG_HIDDEN); + } else if (!mapStyles.empty()) { + // populate dropdown + uint16_t pos = 0; + bool savedStyleOK = false; + lv_dropdown_set_options(objects.map_style_dropdown, ""); + for (auto it : mapStyles) { + lv_dropdown_add_option(objects.map_style_dropdown, it.c_str(), pos); + if (it == db.uiConfig.map_data.style) { + lv_dropdown_set_selected(objects.map_style_dropdown, pos); + MapTileSettings::setTileStyle(db.uiConfig.map_data.style); + savedStyleOK = true; + } + pos++; + } + if (!savedStyleOK) { + // no such style on SD, pick first one we found + char style[20]; + lv_dropdown_set_selected(objects.map_style_dropdown, 0); + lv_dropdown_get_selected_str(objects.map_style_dropdown, style, sizeof(style)); + MapTileSettings::setTileStyle(style); + } + MapTileSettings::setPrefix("/maps"); + } else { + messageAlert(_("No map tiles found on SDCard!"), true); + map->setNoTileImage(&img_no_tile_image); + } + map->forceRedraw(); + } } else { - // TODO: init screen saver + lv_dropdown_set_options(objects.map_style_dropdown, ""); } - break; + + lv_obj_clear_flag(objects.map_panel, LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(objects.raw_map_panel, LV_OBJ_FLAG_HIDDEN); } - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': { - if (pinKeys < 6) { - pinEntered[pinKeys++] = *key; - lv_label_set_text(objects.lock_screen_digits_label, hidden[pinKeys]); - char buf[10]; - lv_snprintf(buf, 7, "%06d", THIS->db.uiConfig.pin_code); - if (pinKeys == 6 && strcmp(pinEntered, buf) == 0) { - // unlock screen - pinKeys = 0; - screenLocked = false; - lv_obj_clear_flag(objects.tab_page_basic_settings, LV_OBJ_FLAG_HIDDEN); - lv_screen_load_anim(objects.main_screen, LV_SCR_LOAD_ANIM_FADE_IN, 100, 0, false); - lv_label_set_text(objects.lock_screen_digits_label, hidden[pinKeys]); + void TFTView_320x240::updateLocationMap(uint32_t num) + { + lv_label_set_text_fmt(objects.top_map_label, _("Locations Map (%d/%d)"), num, nodeCount); + } + + /** + * add node location image for display on map + */ + void TFTView_320x240::addOrUpdateMap(uint32_t nodeNum, int32_t lat, int32_t lon) + { + auto it = nodeObjects.find(nodeNum); + if (it == nodeObjects.end()) { + uint32_t bgColor, fgColor; + std::tie(bgColor, fgColor) = nodeColor(nodeNum); + lv_obj_t *img = lv_image_create(objects.raw_map_panel); + lv_obj_set_size(img, 40, 35); + lv_img_set_src(img, &img_circle_image); + lv_image_set_inner_align(img, LV_IMAGE_ALIGN_TOP_MID); + lv_obj_set_style_opa(img, 180, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_image_recolor(img, lv_color_hex(bgColor), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_image_recolor_opa(img, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_top(img, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_bottom(img, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_left(img, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_right(img, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + + lv_obj_t *lbl = lv_label_create(img); + lv_obj_set_pos(lbl, 0, 0); + lv_obj_set_size(lbl, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_style_text_color(lbl, lv_color_black(), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_opa(img, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_image_recolor_opa(img, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_font(lbl, &lv_font_montserrat_10, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_align(lbl, LV_ALIGN_BOTTOM_MID, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_align(lbl, LV_ALIGN_BOTTOM_MID, LV_PART_MAIN | LV_STATE_DEFAULT); + + lv_obj_t *p = nodes[nodeNum]; + lv_label_set_text_fmt(lbl, "%s", lv_label_get_text(p->LV_OBJ_IDX(node_lbs_idx))); + + // position label callback + lv_obj_add_flag(p->LV_OBJ_IDX(node_pos1_idx), LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_event_cb(p->LV_OBJ_IDX(node_pos1_idx), ui_event_positionButton, LV_EVENT_CLICKED, (void *)p); + + nodeObjects[nodeNum] = img; + if (map) { + map->add(nodeNum, lat * 1e-7, lon * 1e-7, drawObjectCB); + lv_obj_add_flag(img, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_event_cb(img, ui_event_mapNodeButton, LV_EVENT_CLICKED, (void *)nodeNum); + updateLocationMap(map->getObjectsOnMap()); + } + } else { + if (map) { + map->update(it->first, lat * 1e-7, lon * 1e-7); } } - break; } - case 'D': { - if (pinKeys > 0) { - pinEntered[--pinKeys] = '\0'; - lv_label_set_text(objects.lock_screen_digits_label, hidden[pinKeys]); + + void TFTView_320x240::removeFromMap(uint32_t nodeNum) + { + auto it = nodeObjects.find(nodeNum); + if (it == nodeObjects.end()) + return; + + lv_obj_t *img = it->second; + if (map) { + map->remove(it->first); + updateLocationMap(map->getObjectsOnMap()); } - break; + nodeObjects.erase(nodeNum); + lv_obj_remove_event_cb(img, ui_event_mapNodeButton); + lv_obj_delete(img); } - default: - break; + + void TFTView_320x240::ui_event_mesh_detector(lv_event_t * e) + { + THIS->ui_set_active(objects.settings_button, objects.mesh_detector_panel, objects.top_mesh_detector_panel); } - } -} -void TFTView_320x240::ui_event_backup_restore_radio_button(lv_event_t *e) -{ - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED) { - lv_obj_remove_state(objects.settings_backup_checkbox, LV_STATE_CHECKED); - lv_obj_remove_state(objects.settings_restore_checkbox, LV_STATE_CHECKED); - lv_obj_add_state(lv_event_get_target_obj(e), LV_STATE_CHECKED); - } -} + void TFTView_320x240::ui_event_mesh_detector_start(_lv_event_t * e) + { + lv_obj_add_flag(objects.detector_contact_button, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(objects.detector_heard_label, LV_OBJ_FLAG_HIDDEN); + if (!THIS->detectorRunning) { + lv_label_set_text(objects.detector_start_label, _("Stop")); + + // create radar animation + lv_anim_t &a = THIS->radar; + lv_anim_init(&a); + lv_anim_set_var(&a, objects.radar_beam); + lv_anim_set_values(&a, 0, 3600); + lv_anim_set_repeat_count(&a, 1800); + lv_anim_set_duration(&a, 7200); + lv_anim_set_exec_cb(&a, ui_anim_radar_cb); + lv_anim_start(&a); + lv_obj_clear_flag(objects.detector_radar_panel, LV_OBJ_FLAG_HIDDEN); + } else { + lv_label_set_text(objects.detector_start_label, _("Start")); + lv_anim_del(&objects.radar_beam, ui_anim_radar_cb); + lv_obj_add_flag(objects.detector_radar_panel, LV_OBJ_FLAG_HIDDEN); + } + THIS->detectorRunning = !THIS->detectorRunning; + THIS->controller->sendPing(); + } -void TFTView_320x240::ui_event_zoomSlider(lv_event_t *e) -{ - THIS->map->setZoom(lv_slider_get_value(objects.zoom_slider)); - THIS->updateLocationMap(THIS->map->getObjectsOnMap()); -} + void TFTView_320x240::ui_event_signal_scanner(lv_event_t * e) + { + if (currentPanel) { + THIS->setNodeImage(currentNode, + (MeshtasticView::eRole)(unsigned long)currentPanel->LV_OBJ_IDX(node_img_idx)->user_data, false, + objects.signal_scanner_node_image); + const char *lbs = lv_label_get_text(currentPanel->LV_OBJ_IDX(node_lbs_idx)); + lv_label_set_text(objects.signal_scanner_node_button_label, lbs); + lv_obj_clear_state(objects.signal_scanner_start_button, LV_STATE_DISABLED); + } else { + lv_label_set_text(objects.signal_scanner_node_button_label, _("choose\nnode")); + lv_obj_add_state(objects.signal_scanner_start_button, LV_STATE_DISABLED); + } + lv_label_set_text(objects.signal_scanner_start_label, _("Start")); + THIS->ui_set_active(objects.settings_button, objects.signal_scanner_panel, objects.top_signal_scanner_panel); + } -void TFTView_320x240::ui_event_zoomIn(lv_event_t *e) -{ - THIS->map->setZoom(MapTileSettings::getZoomLevel() + 1); - THIS->updateLocationMap(THIS->map->getObjectsOnMap()); -} + void TFTView_320x240::ui_event_signal_scanner_node(lv_event_t * e) + { + THIS->chooseNodeSignalScanner = true; + THIS->selectedHops = lv_dropdown_get_selected(objects.nodes_filter_hops_dropdown); + lv_dropdown_set_selected(objects.nodes_filter_hops_dropdown, 7); // 0 hops away + THIS->ui_set_active(objects.nodes_button, objects.nodes_panel, objects.top_nodes_panel); + THIS->updateNodesFiltered(true); + THIS->updateNodesStatus(); + } -void TFTView_320x240::ui_event_zoomOut(lv_event_t *e) -{ - THIS->map->setZoom(MapTileSettings::getZoomLevel() - 1); - THIS->updateLocationMap(THIS->map->getObjectsOnMap()); -} + void TFTView_320x240::ui_event_signal_scanner_start(lv_event_t * e) + { + lv_event_code_t event_code = lv_event_get_code(e); + if (currentNode) { + static bool ignoreClicked = false; + if (event_code == LV_EVENT_CLICKED) { + if (ignoreClicked) { + ignoreClicked = false; + return; + } + if (spinnerButton) { + lv_label_set_text(objects.signal_scanner_start_label, _("Start")); + lv_obj_delete(spinnerButton); + spinnerButton = nullptr; + THIS->scans = 0; + } else { + THIS->scanSignal(0); + } + } else if (event_code == LV_EVENT_LONG_PRESSED) { + ignoreClicked = true; + lv_obj_t *obj = lv_spinner_create(objects.signal_scanner_panel); + spinnerButton = obj; + spinnerButton->user_data = (void *)objects.signal_scanner_panel; + lv_spinner_set_anim_params(obj, 5000, 300); + lv_obj_set_pos(obj, 0, -50); + lv_obj_set_size(obj, 68, 68); + lv_obj_set_style_align(obj, LV_ALIGN_CENTER, LV_PART_MAIN | LV_STATE_DEFAULT); + add_style_spinner_style(obj); + lv_label_set_text(objects.signal_scanner_start_label, "30s"); + THIS->scans = 6 + 1; + } + } + } -void TFTView_320x240::ui_event_lockGps(lv_event_t *e) -{ - bool gpsLocked = lv_obj_has_state(objects.gps_lock_button, LV_STATE_CHECKED); - THIS->map->setLocked(gpsLocked); - THIS->db.uiConfig.map_data.follow_gps = gpsLocked; - THIS->controller->storeUIConfig(THIS->db.uiConfig); -} + void TFTView_320x240::ui_event_trace_route(lv_event_t * e) + { + // show still old route setup while processing is ongoing + time_t now; + time(&now); + if (spinnerButton && (now - startTime) < 30) { + THIS->ui_set_active(objects.settings_button, objects.trace_route_panel, objects.top_trace_route_panel); + return; + } + THIS->removeSpinner(); -void TFTView_320x240::ui_event_mapBrightnessSlider(lv_event_t *e) -{ - uint32_t br = lv_slider_get_value(objects.map_brightness_slider); - lv_obj_set_style_bg_color(objects.map_panel, lv_color_make(br, br, br), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(objects.raw_map_panel, lv_color_make(br, br, br), LV_PART_MAIN | LV_STATE_DEFAULT); -} + // remove old route except first button and spinner panel + ILOG_DEBUG("removing old route: %d %d %d", lv_obj_get_child_cnt(objects.trace_route_panel), + lv_obj_get_child_cnt(objects.route_towards_panel), lv_obj_get_child_cnt(objects.route_back_panel)); -void TFTView_320x240::ui_event_mapContrastSlider(lv_event_t *e) -{ - uint32_t ct = lv_slider_get_value(objects.map_contrast_slider); - lv_obj_set_style_opa(objects.raw_map_panel, ct, LV_PART_MAIN | LV_STATE_DEFAULT); -} + uint16_t children = lv_obj_get_child_cnt(objects.trace_route_panel) - 1; + while (children > 1) { + if (objects.trace_route_panel->spec_attr->children[children]->class_p == &lv_button_class) { + lv_obj_delete(objects.trace_route_panel->spec_attr->children[children]); + } + children--; + } -void TFTView_320x240::ui_event_map_style_dropdown(lv_event_t *e) -{ - lv_dropdown_get_selected_str(objects.map_style_dropdown, THIS->db.uiConfig.map_data.style, - sizeof(THIS->db.uiConfig.map_data.style)); - MapTileSettings::setTileStyle(THIS->db.uiConfig.map_data.style); - THIS->controller->storeUIConfig(THIS->db.uiConfig); - lv_obj_add_flag(objects.map_osd_panel, LV_OBJ_FLAG_HIDDEN); - THIS->map->forceRedraw(); -} - -void TFTView_320x240::ui_event_mapNodeButton(lv_event_t *e) -{ - // navigate to node in node list - uint32_t nodeNum = (unsigned long)e->user_data; - ILOG_DEBUG("map node %08x", nodeNum); - lv_obj_t *panel = THIS->nodes[nodeNum]; - THIS->ui_set_active(objects.nodes_button, objects.nodes_panel, objects.top_nodes_panel); - lv_obj_scroll_to_view(panel, LV_ANIM_ON); - if (panel != currentPanel) - ui_event_NodeButton(e); -} - -void TFTView_320x240::ui_event_chatNodeButton(lv_event_t *e) -{ - uint32_t nodeNum = (unsigned long)e->user_data; - auto it = THIS->nodes.find(nodeNum); - if (it != THIS->nodes.end()) { - lv_obj_t *panel = it->second; - THIS->ui_set_active(objects.nodes_button, objects.nodes_panel, objects.top_nodes_panel); - lv_obj_scroll_to_view(panel, LV_ANIM_ON); - if (panel != currentPanel) - ui_event_NodeButton(e); - } -} - -void TFTView_320x240::ui_event_positionButton(lv_event_t *e) -{ - // navigate to position in map - lv_obj_t *p = (lv_obj_t *)e->user_data; - int32_t lat = (long)p->LV_OBJ_IDX(node_pos1_idx)->user_data; - int32_t lon = (long)p->LV_OBJ_IDX(node_pos2_idx)->user_data; - if (lat && lon) { - THIS->ui_set_active(objects.map_button, objects.map_panel, objects.top_map_panel); - if (!THIS->map) { - THIS->loadMap(); - } - THIS->map->setScrolledPosition(lat * 1e-7, lon * 1e-7); - } -} - -void TFTView_320x240::ui_screen_event_cb(lv_event_t *e) -{ - if (THIS->activePanel == objects.map_panel) { - lv_dir_t dir = lv_indev_get_gesture_dir(lv_indev_active()); - switch (dir) { - case LV_DIR_LEFT: - e->user_data = (void *)6; - break; - case LV_DIR_RIGHT: - e->user_data = (void *)4; - break; - case LV_DIR_TOP: - e->user_data = (void *)2; - break; - case LV_DIR_BOTTOM: - e->user_data = (void *)8; - break; - default: - break; - } - ILOG_DEBUG("gesture: %d", (uint16_t)dir); - THIS->ui_event_arrow(e); - } -} - -void TFTView_320x240::ui_event_arrow(lv_event_t *e) -{ - if (THIS->map && THIS->map->redrawComplete()) { - uint16_t deltaX = 0; - uint16_t deltaY = 0; - ScrollDirection direction = (ScrollDirection)(unsigned long)e->user_data; - switch (direction) { - case scrollDownLeft: - deltaX = 1; - deltaY = -1; - break; - case scrollDown: - deltaX = 0; - deltaY = -1; - break; - case scrollDownRight: - deltaX = -1; - deltaY = -1; - break; - case scrollLeft: - deltaX = 1; - deltaY = 0; - break; - case scrollRight: - deltaX = -1; - deltaY = 0; - break; - case scrollUpLeft: - deltaX = 1; - deltaY = 1; - break; - case scrollUp: - deltaX = 0; - deltaY = 1; - break; - case scrollUpRight: - deltaX = -1; - deltaY = 1; - break; - default: - break; - }; - if (!THIS->map->scroll(deltaX, deltaY)) - THIS->map->forceRedraw(); - } - THIS->updateLocationMap(THIS->map->getObjectsOnMap()); -} - -void TFTView_320x240::ui_event_navHome(lv_event_t *e) -{ - static bool ignoreClicked = false; - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED) { - if (ignoreClicked) { // prevent long press to enter this setting - ignoreClicked = false; - return; - } - THIS->map->moveHome(); - } else if (event_code == LV_EVENT_LONG_PRESSED) { - ignoreClicked = true; - float lat, lon; - THIS->map->setHomePosition(); - THIS->map->getHomeLocation(lat, lon); - - int32_t ilat = lat * 1e7f; - int32_t ilon = lon * 1e7f; - THIS->db.uiConfig.has_map_data = true; - THIS->db.uiConfig.map_data.has_home = true; - THIS->db.uiConfig.map_data.home.latitude = ilat; - THIS->db.uiConfig.map_data.home.longitude = ilon; - THIS->db.uiConfig.map_data.home.zoom = MapTileSettings::getZoomLevel(); - THIS->controller->storeUIConfig(THIS->db.uiConfig); + // forward route + children = lv_obj_get_child_cnt(objects.route_towards_panel); + while (children > 0) { + children--; + if (objects.route_towards_panel->spec_attr->children[children]->class_p == &lv_button_class) { + lv_obj_delete(objects.route_towards_panel->spec_attr->children[children]); + } + } - meshtastic_Config_PositionConfig &position = THIS->db.config.position; - if (position.fixed_position) { - THIS->updatePosition(THIS->ownNode, ilat, ilon, 0, 0, 0); - if (position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) { - // grey out text to indicate it's a fixed position vs. actual GPS position - Themes::recolorText(objects.home_location_label, false); - THIS->controller->sendConfig(meshtastic_Position{.latitude_i = ilat, - .longitude_i = ilon, - .time = uint32_t(VALID_TIME(THIS->actTime) ? THIS->actTime : 0), - .location_source = meshtastic_Position_LocSource_LOC_MANUAL}); + // backward route + children = lv_obj_get_child_cnt(objects.route_back_panel); + while (children > 0) { + children--; + if (objects.route_back_panel->spec_attr->children[children]->class_p == &lv_button_class) { + lv_obj_delete(objects.route_back_panel->spec_attr->children[children]); + } } - } - } -} -void TFTView_320x240::loadMap(void) -{ - if (!map) { -#if LV_USE_FS_ARDUINO_SD - map = new MapPanel(objects.raw_map_panel); -#elif defined(HAS_SD_MMC) - map = new MapPanel(objects.raw_map_panel, new SDCardService()); -#elif defined(HAS_SDCARD) - map = new MapPanel(objects.raw_map_panel, new SdFatService()); -#elif defined(ARCH_PORTDUINO) - map = new MapPanel(objects.raw_map_panel, new SDCardService()); // TODO: LinuxFileSystemService -#else - map = new MapPanel(objects.raw_map_panel); -#endif - map->setHomeLocationImage(objects.home_location_image); - lv_obj_add_flag(objects.home_location_image, LV_OBJ_FLAG_CLICKABLE); - lv_obj_add_event_cb(objects.home_location_image, ui_event_mapNodeButton, LV_EVENT_CLICKED, (void *)ownNode); - - // center map to GPS > home > other nodes > default location - if (db.config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) { - map->setGpsPositionImage(objects.gps_position_image); - lv_obj_clear_flag(objects.gps_position_image, LV_OBJ_FLAG_HIDDEN); - } else { - lv_obj_add_flag(objects.gps_position_image, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(objects.gps_lock_button, LV_OBJ_FLAG_HIDDEN); - } - if (hasPosition) { - if (db.uiConfig.map_data.has_home) { - map->setHomeLocation(db.uiConfig.map_data.home.latitude * 1e-7, db.uiConfig.map_data.home.longitude * 1e-7); - map->setZoom(db.uiConfig.map_data.home.zoom); - } else { - map->setHomeLocation(myLatitude * 1e-7, myLongitude * 1e-7); - map->setZoom(13); - } - map->setGpsPosition(myLatitude * 1e-7, myLongitude * 1e-7); - } else if (db.uiConfig.map_data.has_home) { - map->setHomeLocation(db.uiConfig.map_data.home.latitude * 1e-7, db.uiConfig.map_data.home.longitude * 1e-7); - map->setZoom(db.uiConfig.map_data.home.zoom); - } else if (nodeObjects.size() >= 1) { - // no gps, no saved position then center the home location among other available nodes - std::vector sortedLat; - std::vector sortedLon; - sortedLat.reserve(nodeObjects.size()); - sortedLon.reserve(nodeObjects.size()); - for (auto it : nodeObjects) { - lv_obj_t *p = nodes[it.first]; - int32_t lat = (long)p->LV_OBJ_IDX(node_pos1_idx)->user_data; - int32_t lon = (long)p->LV_OBJ_IDX(node_pos2_idx)->user_data; - if (lat && lon) { - sortedLat.push_back(lat); - sortedLon.push_back(lon); - } - } - std::sort(sortedLat.begin(), sortedLat.end()); - std::sort(sortedLon.begin(), sortedLon.end()); - int64_t latcenter = 0; - int64_t loncenter = 0; - int32_t count = 0; - // select just the closest 60% of nodes, ignore the rest - int pp = 100 / 20; - for (int i = sortedLat.size() / pp; i < pp * sortedLat.size() / pp; i++) { - latcenter += sortedLat[i]; - loncenter += sortedLon[i]; - count++; - } - latcenter /= count; - loncenter /= count; - map->setHomeLocation(latcenter * 1e-7, loncenter * 1e-7); - - // calculate optimal zoom factor to fit in all nodes of this range - lv_obj_update_layout(objects.raw_map_panel); - float rangeDeg = 1e-7 * (sortedLon[(pp - 1) * sortedLon.size() / pp] - sortedLon[sortedLon.size() / pp]); - float distanceKm = abs(rangeDeg * 111.32 * cos(1e-7 * sortedLat[sortedLat.size() / 2])); - uint32_t zoom = sqrt(156.543034f / distanceKm * abs(cos(1e-7 * sortedLat[sortedLat.size() / 2])) * 256) + 1; - map->setZoom(zoom); - } else { - // use default location @theBigBentern - map->setZoom(3); - } + lv_obj_clear_flag(objects.start_button_panel, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(objects.hop_routes_panel, LV_OBJ_FLAG_HIDDEN); - if (db.uiConfig.map_data.follow_gps) { - lv_obj_set_state(objects.gps_lock_button, LV_STATE_CHECKED, true); - map->setLocked(true); + if (currentPanel) { + THIS->setNodeImage(THIS->currentNode, + (MeshtasticView::eRole)(unsigned long)currentPanel->LV_OBJ_IDX(node_img_idx)->user_data, false, + objects.trace_route_to_image); + const char *lbl = lv_label_get_text(currentPanel->LV_OBJ_IDX(node_lbl_idx)); + lv_label_set_text(objects.trace_route_to_button_label, lbl); + lv_obj_clear_state(objects.trace_route_start_button, LV_STATE_DISABLED); + } else { + lv_label_set_text(objects.trace_route_to_button_label, _("choose target node")); + lv_obj_add_state(objects.trace_route_start_button, LV_STATE_DISABLED); + } + lv_label_set_text(objects.trace_route_start_label, _("Start")); + THIS->ui_set_active(objects.settings_button, objects.trace_route_panel, objects.top_trace_route_panel); } - // finally add all node images to the map - if (!nodeObjects.empty()) { - for (auto it : nodeObjects) { - lv_obj_t *p = nodes[it.first]; - float lat = 1e-7 * (long)p->LV_OBJ_IDX(node_pos1_idx)->user_data; - float lon = 1e-7 * (long)p->LV_OBJ_IDX(node_pos2_idx)->user_data; - map->add(it.first, lat, lon, drawObjectCB); - lv_obj_add_flag(it.second, LV_OBJ_FLAG_CLICKABLE); - lv_obj_add_event_cb(it.second, ui_event_mapNodeButton, LV_EVENT_CLICKED, (void *)it.first); + void TFTView_320x240::ui_event_trace_route_to(lv_event_t * e) + { + if (!spinnerButton) { + THIS->chooseNodeTraceRoute = true; + THIS->ui_set_active(objects.nodes_button, objects.nodes_panel, objects.top_nodes_panel); } } - updateLocationMap(map->getObjectsOnMap()); - } - if (sdCard) { - if (!sdCard->isUpdated()) { - map->setNoTileImage(&img_no_tile_image); - std::set mapStyles = sdCard->loadMapStyles(MapTileSettings::getPrefix()); - if (mapStyles.find("/map") != mapStyles.end()) { - // no styles found, but the /map directory, so use it - MapTileSettings::setPrefix("/map"); - MapTileSettings::setTileStyle(""); - lv_obj_add_flag(objects.map_style_dropdown, LV_OBJ_FLAG_HIDDEN); - } else if (!mapStyles.empty()) { - // populate dropdown - uint16_t pos = 0; - bool savedStyleOK = false; - lv_dropdown_set_options(objects.map_style_dropdown, ""); - for (auto it : mapStyles) { - lv_dropdown_add_option(objects.map_style_dropdown, it.c_str(), pos); - if (it == db.uiConfig.map_data.style) { - lv_dropdown_set_selected(objects.map_style_dropdown, pos); - MapTileSettings::setTileStyle(db.uiConfig.map_data.style); - savedStyleOK = true; + void TFTView_320x240::ui_event_trace_route_start(lv_event_t * e) + { + if (!spinnerButton) { + if (currentPanel) { + time(&startTime); + lv_obj_t *obj = lv_spinner_create(objects.start_button_panel); + spinnerButton = obj; + lv_spinner_set_anim_params(obj, 5000, 300); + lv_obj_set_pos(obj, 0, 0); + lv_obj_set_size(obj, 68, 68); + lv_obj_set_style_align(obj, LV_ALIGN_CENTER, LV_PART_MAIN | LV_STATE_DEFAULT); + add_style_spinner_style(obj); + lv_label_set_text(objects.trace_route_start_label, "30s"); + + // retrieve nodeNum from current node + // FIXME: remove for loop + for (auto &it : THIS->nodes) { + if (it.second == currentPanel) { + uint32_t requestId; + uint32_t to = it.first; + uint8_t ch = (uint8_t)(unsigned long)currentPanel->user_data; + // trial: hoplimit optimization for direct messages + int8_t hopsAway = (signed long)THIS->nodes[to]->LV_OBJ_IDX(node_sig_idx)->user_data; + if (hopsAway < 0) + hopsAway = 5; + uint8_t hopLimit = (hopsAway < THIS->db.config.lora.hop_limit ? hopsAway + 1 : hopsAway); + requestId = THIS->requests.addRequest(to, ResponseHandler::TraceRouteRequest); + THIS->controller->traceRoute(to, ch, hopLimit, requestId); + break; + } } - pos++; } - if (!savedStyleOK) { - // no such style on SD, pick first one we found - char style[20]; - lv_dropdown_set_selected(objects.map_style_dropdown, 0); - lv_dropdown_get_selected_str(objects.map_style_dropdown, style, sizeof(style)); - MapTileSettings::setTileStyle(style); - } - MapTileSettings::setPrefix("/maps"); } else { - messageAlert(_("No map tiles found on SDCard!"), true); - map->setNoTileImage(&img_no_tile_image); + // restart + ui_event_trace_route(e); } - map->forceRedraw(); } - } else { - lv_dropdown_set_options(objects.map_style_dropdown, ""); - } - - lv_obj_clear_flag(objects.map_panel, LV_OBJ_FLAG_HIDDEN); - lv_obj_clear_flag(objects.raw_map_panel, LV_OBJ_FLAG_HIDDEN); -} - -void TFTView_320x240::updateLocationMap(uint32_t num) -{ - lv_label_set_text_fmt(objects.top_map_label, _("Locations Map (%d/%d)"), num, nodeCount); -} -/** - * add node location image for display on map - */ -void TFTView_320x240::addOrUpdateMap(uint32_t nodeNum, int32_t lat, int32_t lon) -{ - auto it = nodeObjects.find(nodeNum); - if (it == nodeObjects.end()) { - uint32_t bgColor, fgColor; - std::tie(bgColor, fgColor) = nodeColor(nodeNum); - lv_obj_t *img = lv_image_create(objects.raw_map_panel); - lv_obj_set_size(img, 40, 35); - lv_img_set_src(img, &img_circle_image); - lv_image_set_inner_align(img, LV_IMAGE_ALIGN_TOP_MID); - lv_obj_set_style_opa(img, 180, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_image_recolor(img, lv_color_hex(bgColor), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_image_recolor_opa(img, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_top(img, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_bottom(img, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_left(img, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_right(img, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - - lv_obj_t *lbl = lv_label_create(img); - lv_obj_set_pos(lbl, 0, 0); - lv_obj_set_size(lbl, LV_SIZE_CONTENT, LV_SIZE_CONTENT); - lv_obj_set_style_text_color(lbl, lv_color_black(), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_opa(img, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_image_recolor_opa(img, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_font(lbl, &lv_font_montserrat_10, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_align(lbl, LV_ALIGN_BOTTOM_MID, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_align(lbl, LV_ALIGN_BOTTOM_MID, LV_PART_MAIN | LV_STATE_DEFAULT); - - lv_obj_t *p = nodes[nodeNum]; - lv_label_set_text_fmt(lbl, "%s", lv_label_get_text(p->LV_OBJ_IDX(node_lbs_idx))); - - // position label callback - lv_obj_add_flag(p->LV_OBJ_IDX(node_pos1_idx), LV_OBJ_FLAG_CLICKABLE); - lv_obj_add_event_cb(p->LV_OBJ_IDX(node_pos1_idx), ui_event_positionButton, LV_EVENT_CLICKED, (void *)p); - - nodeObjects[nodeNum] = img; - if (map) { - map->add(nodeNum, lat * 1e-7, lon * 1e-7, drawObjectCB); - lv_obj_add_flag(img, LV_OBJ_FLAG_CLICKABLE); - lv_obj_add_event_cb(img, ui_event_mapNodeButton, LV_EVENT_CLICKED, (void *)nodeNum); - updateLocationMap(map->getObjectsOnMap()); - } - } else { - if (map) { - map->update(it->first, lat * 1e-7, lon * 1e-7); + void TFTView_320x240::ui_event_trace_route_node(lv_event_t * e) + { + // navigate to node in node list + lv_obj_t *panel = (lv_obj_t *)e->user_data; + THIS->ui_set_active(objects.nodes_button, objects.nodes_panel, objects.top_nodes_panel); + lv_obj_scroll_to_view(panel, LV_ANIM_ON); } - } -} -void TFTView_320x240::removeFromMap(uint32_t nodeNum) -{ - auto it = nodeObjects.find(nodeNum); - if (it == nodeObjects.end()) - return; - - lv_obj_t *img = it->second; - if (map) { - map->remove(it->first); - updateLocationMap(map->getObjectsOnMap()); - } - nodeObjects.erase(nodeNum); - lv_obj_remove_event_cb(img, ui_event_mapNodeButton); - lv_obj_delete(img); -} - -void TFTView_320x240::ui_event_mesh_detector(lv_event_t *e) -{ - THIS->ui_set_active(objects.settings_button, objects.mesh_detector_panel, objects.top_mesh_detector_panel); -} - -void TFTView_320x240::ui_event_mesh_detector_start(_lv_event_t *e) -{ - lv_obj_add_flag(objects.detector_contact_button, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(objects.detector_heard_label, LV_OBJ_FLAG_HIDDEN); - if (!THIS->detectorRunning) { - lv_label_set_text(objects.detector_start_label, _("Stop")); - - // create radar animation - lv_anim_t &a = THIS->radar; - lv_anim_init(&a); - lv_anim_set_var(&a, objects.radar_beam); - lv_anim_set_values(&a, 0, 3600); - lv_anim_set_repeat_count(&a, 1800); - lv_anim_set_duration(&a, 7200); - lv_anim_set_exec_cb(&a, ui_anim_radar_cb); - lv_anim_start(&a); - lv_obj_clear_flag(objects.detector_radar_panel, LV_OBJ_FLAG_HIDDEN); - } else { - lv_label_set_text(objects.detector_start_label, _("Start")); - lv_anim_del(&objects.radar_beam, ui_anim_radar_cb); - lv_obj_add_flag(objects.detector_radar_panel, LV_OBJ_FLAG_HIDDEN); - } - THIS->detectorRunning = !THIS->detectorRunning; - THIS->controller->sendPing(); -} - -void TFTView_320x240::ui_event_signal_scanner(lv_event_t *e) -{ - if (currentPanel) { - THIS->setNodeImage(currentNode, (MeshtasticView::eRole)(unsigned long)currentPanel->LV_OBJ_IDX(node_img_idx)->user_data, - false, objects.signal_scanner_node_image); - const char *lbs = lv_label_get_text(currentPanel->LV_OBJ_IDX(node_lbs_idx)); - lv_label_set_text(objects.signal_scanner_node_button_label, lbs); - lv_obj_clear_state(objects.signal_scanner_start_button, LV_STATE_DISABLED); - } else { - lv_label_set_text(objects.signal_scanner_node_button_label, _("choose\nnode")); - lv_obj_add_state(objects.signal_scanner_start_button, LV_STATE_DISABLED); - } - lv_label_set_text(objects.signal_scanner_start_label, _("Start")); - THIS->ui_set_active(objects.settings_button, objects.signal_scanner_panel, objects.top_signal_scanner_panel); -} - -void TFTView_320x240::ui_event_signal_scanner_node(lv_event_t *e) -{ - THIS->chooseNodeSignalScanner = true; - THIS->selectedHops = lv_dropdown_get_selected(objects.nodes_filter_hops_dropdown); - lv_dropdown_set_selected(objects.nodes_filter_hops_dropdown, 7); // 0 hops away - THIS->ui_set_active(objects.nodes_button, objects.nodes_panel, objects.top_nodes_panel); - THIS->updateNodesFiltered(true); - THIS->updateNodesStatus(); -} - -void TFTView_320x240::ui_event_signal_scanner_start(lv_event_t *e) -{ - lv_event_code_t event_code = lv_event_get_code(e); - if (currentNode) { - static bool ignoreClicked = false; - if (event_code == LV_EVENT_CLICKED) { - if (ignoreClicked) { - ignoreClicked = false; - return; - } + void TFTView_320x240::removeSpinner(void) + { if (spinnerButton) { - lv_label_set_text(objects.signal_scanner_start_label, _("Start")); lv_obj_delete(spinnerButton); spinnerButton = nullptr; - THIS->scans = 0; - } else { - THIS->scanSignal(0); + startTime = 0; } - } else if (event_code == LV_EVENT_LONG_PRESSED) { - ignoreClicked = true; - lv_obj_t *obj = lv_spinner_create(objects.signal_scanner_panel); - spinnerButton = obj; - spinnerButton->user_data = (void *)objects.signal_scanner_panel; - lv_spinner_set_anim_params(obj, 5000, 300); - lv_obj_set_pos(obj, 0, -50); - lv_obj_set_size(obj, 68, 68); - lv_obj_set_style_align(obj, LV_ALIGN_CENTER, LV_PART_MAIN | LV_STATE_DEFAULT); - add_style_spinner_style(obj); - lv_label_set_text(objects.signal_scanner_start_label, "30s"); - THIS->scans = 6 + 1; } - } -} - -void TFTView_320x240::ui_event_trace_route(lv_event_t *e) -{ - // show still old route setup while processing is ongoing - time_t now; - time(&now); - if (spinnerButton && (now - startTime) < 30) { - THIS->ui_set_active(objects.settings_button, objects.trace_route_panel, objects.top_trace_route_panel); - return; - } - THIS->removeSpinner(); - - // remove old route except first button and spinner panel - ILOG_DEBUG("removing old route: %d %d %d", lv_obj_get_child_cnt(objects.trace_route_panel), - lv_obj_get_child_cnt(objects.route_towards_panel), lv_obj_get_child_cnt(objects.route_back_panel)); - uint16_t children = lv_obj_get_child_cnt(objects.trace_route_panel) - 1; - while (children > 1) { - if (objects.trace_route_panel->spec_attr->children[children]->class_p == &lv_button_class) { - lv_obj_delete(objects.trace_route_panel->spec_attr->children[children]); + void TFTView_320x240::ui_event_node_details(lv_event_t * e) + { + THIS->ui_set_active(objects.settings_button, objects.details_panel, objects.top_nodes_panel); } - children--; - } - // forward route - children = lv_obj_get_child_cnt(objects.route_towards_panel); - while (children > 0) { - children--; - if (objects.route_towards_panel->spec_attr->children[children]->class_p == &lv_button_class) { - lv_obj_delete(objects.route_towards_panel->spec_attr->children[children]); + void TFTView_320x240::ui_event_statistics(lv_event_t * e) + { + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED) { + THIS->ui_set_active(objects.settings_button, objects.tools_statistics_panel, objects.top_statistics_panel); + } else if (event_code == LV_EVENT_LONG_PRESSED) { + // clear statistics table + THIS->updateStatistics(meshtastic_MeshPacket{.from = 0}); + } } - } - // backward route - children = lv_obj_get_child_cnt(objects.route_back_panel); - while (children > 0) { - children--; - if (objects.route_back_panel->spec_attr->children[children]->class_p == &lv_button_class) { - lv_obj_delete(objects.route_back_panel->spec_attr->children[children]); + void TFTView_320x240::ui_event_packet_log(lv_event_t * e) + { + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED) { + THIS->ui_set_active(objects.settings_button, objects.tools_packet_log_panel, objects.top_packet_log_panel); + THIS->packetLogEnabled = true; + } else if (event_code == LV_EVENT_LONG_PRESSED) { + THIS->packetCounter = 0; + lv_obj_clean(objects.tools_packet_log_panel); + } } - } - - lv_obj_clear_flag(objects.start_button_panel, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(objects.hop_routes_panel, LV_OBJ_FLAG_HIDDEN); - - if (currentPanel) { - THIS->setNodeImage(THIS->currentNode, - (MeshtasticView::eRole)(unsigned long)currentPanel->LV_OBJ_IDX(node_img_idx)->user_data, false, - objects.trace_route_to_image); - const char *lbl = lv_label_get_text(currentPanel->LV_OBJ_IDX(node_lbl_idx)); - lv_label_set_text(objects.trace_route_to_button_label, lbl); - lv_obj_clear_state(objects.trace_route_start_button, LV_STATE_DISABLED); - } else { - lv_label_set_text(objects.trace_route_to_button_label, _("choose target node")); - lv_obj_add_state(objects.trace_route_start_button, LV_STATE_DISABLED); - } - lv_label_set_text(objects.trace_route_start_label, _("Start")); - THIS->ui_set_active(objects.settings_button, objects.trace_route_panel, objects.top_trace_route_panel); -} -void TFTView_320x240::ui_event_trace_route_to(lv_event_t *e) -{ - if (!spinnerButton) { - THIS->chooseNodeTraceRoute = true; - THIS->ui_set_active(objects.nodes_button, objects.nodes_panel, objects.top_nodes_panel); - } -} + void TFTView_320x240::packetDetected(const meshtastic_MeshPacket &p) + { + uint32_t heard = 0; + if (p.from != ownNode) + heard = p.from; + if (p.to != 0xffffffff && p.to != ownNode) + heard = p.to; + + if (heard) { + if (p.to == ownNode && p.decoded.portnum == meshtastic_PortNum_NODEINFO_APP) { + // we finally sensed a two-way contact to us; stop the detector + detectorRunning = false; + lv_label_set_text(objects.detector_start_label, _("Start")); + lv_anim_del(&objects.radar_beam, ui_anim_radar_cb); + lv_obj_add_flag(objects.detector_radar_panel, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(objects.detector_heard_label, LV_OBJ_FLAG_HIDDEN); + + setNodeImage(p.from, (MeshtasticView::eRole)(unsigned long)nodes[p.from]->LV_OBJ_IDX(node_img_idx)->user_data, + false, objects.detector_contact_image); + const char *lbl = lv_label_get_text(nodes[p.from]->LV_OBJ_IDX(node_lbl_idx)); + + char from[5]; + char *userShort = (char *)&(nodes[p.from]->LV_OBJ_IDX(node_lbs_idx)->user_data); + int pos = 0; + while (pos < 4 && userShort[pos] != 0) { + from[pos] = userShort[pos]; + pos++; + } + from[pos] = '\0'; -void TFTView_320x240::ui_event_trace_route_start(lv_event_t *e) -{ - if (!spinnerButton) { - if (currentPanel) { - time(&startTime); - lv_obj_t *obj = lv_spinner_create(objects.start_button_panel); - spinnerButton = obj; - lv_spinner_set_anim_params(obj, 5000, 300); - lv_obj_set_pos(obj, 0, 0); - lv_obj_set_size(obj, 68, 68); - lv_obj_set_style_align(obj, LV_ALIGN_CENTER, LV_PART_MAIN | LV_STATE_DEFAULT); - add_style_spinner_style(obj); - lv_label_set_text(objects.trace_route_start_label, "30s"); - - // retrieve nodeNum from current node - // FIXME: remove for loop - for (auto &it : THIS->nodes) { - if (it.second == currentPanel) { - uint32_t requestId; - uint32_t to = it.first; - uint8_t ch = (uint8_t)(unsigned long)currentPanel->user_data; - // trial: hoplimit optimization for direct messages - int8_t hopsAway = (signed long)THIS->nodes[to]->LV_OBJ_IDX(node_sig_idx)->user_data; - if (hopsAway < 0) - hopsAway = 5; - uint8_t hopLimit = (hopsAway < THIS->db.config.lora.hop_limit ? hopsAway + 1 : hopsAway); - requestId = THIS->requests.addRequest(to, ResponseHandler::TraceRouteRequest); - THIS->controller->traceRoute(to, ch, hopLimit, requestId); - break; + char buf[64]; + lv_snprintf(buf, 64, "%s(%04x)\n%s", from, p.from & 0xffff, lbl); + lv_label_set_text(objects.detector_contact_label, buf); + lv_obj_clear_flag(objects.detector_contact_button, LV_OBJ_FLAG_HIDDEN); + } else { + char buf[20]; + lv_snprintf(buf, 20, _("heard: !%08x"), heard); + lv_label_set_text(objects.detector_heard_label, buf); + lv_obj_clear_flag(objects.detector_heard_label, LV_OBJ_FLAG_HIDDEN); } } } - } else { - // restart - ui_event_trace_route(e); - } -} - -void TFTView_320x240::ui_event_trace_route_node(lv_event_t *e) -{ - // navigate to node in node list - lv_obj_t *panel = (lv_obj_t *)e->user_data; - THIS->ui_set_active(objects.nodes_button, objects.nodes_panel, objects.top_nodes_panel); - lv_obj_scroll_to_view(panel, LV_ANIM_ON); -} - -void TFTView_320x240::removeSpinner(void) -{ - if (spinnerButton) { - lv_obj_delete(spinnerButton); - spinnerButton = nullptr; - startTime = 0; - } -} - -void TFTView_320x240::ui_event_node_details(lv_event_t *e) -{ - THIS->ui_set_active(objects.settings_button, objects.details_panel, objects.top_nodes_panel); -} - -void TFTView_320x240::ui_event_statistics(lv_event_t *e) -{ - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED) { - THIS->ui_set_active(objects.settings_button, objects.tools_statistics_panel, objects.top_statistics_panel); - } else if (event_code == LV_EVENT_LONG_PRESSED) { - // clear statistics table - THIS->updateStatistics(meshtastic_MeshPacket{.from = 0}); - } -} - -void TFTView_320x240::ui_event_packet_log(lv_event_t *e) -{ - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED) { - THIS->ui_set_active(objects.settings_button, objects.tools_packet_log_panel, objects.top_packet_log_panel); - THIS->packetLogEnabled = true; - } else if (event_code == LV_EVENT_LONG_PRESSED) { - THIS->packetCounter = 0; - lv_obj_clean(objects.tools_packet_log_panel); - } -} -void TFTView_320x240::packetDetected(const meshtastic_MeshPacket &p) -{ - uint32_t heard = 0; - if (p.from != ownNode) - heard = p.from; - if (p.to != 0xffffffff && p.to != ownNode) - heard = p.to; - - if (heard) { - if (p.to == ownNode && p.decoded.portnum == meshtastic_PortNum_NODEINFO_APP) { - // we finally sensed a two-way contact to us; stop the detector - detectorRunning = false; - lv_label_set_text(objects.detector_start_label, _("Start")); - lv_anim_del(&objects.radar_beam, ui_anim_radar_cb); - lv_obj_add_flag(objects.detector_radar_panel, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(objects.detector_heard_label, LV_OBJ_FLAG_HIDDEN); + void TFTView_320x240::writePacketLog(const meshtastic_MeshPacket &p) + { + static std::unordered_map name = { + {0, "unknown"}, {1, "text message"}, {2, "remote hardware"}, {3, "position"}, {4, "node info"}, + {5, "routing"}, {6, "admin"}, {7, "text message"}, {8, "waypoint"}, {9, "audio"}, + {10, "sensor"}, {32, "reply"}, {33, "ip tunnel"}, {34, "paxcounter"}, {64, "serial"}, + {65, "store forward"}, {66, "range test"}, {67, "telemetry"}, {68, "ZPS"}, {69, "simulator"}, + {70, "tracert"}, {71, "neighbor info"}, {72, "atax"}, {73, "map report"}, {74, "power stress"}, + {256, "private"}, {257, "atax forwarder"}}; + + // ignore admin packages initiated by us + if (p.from == ownNode && p.decoded.portnum == meshtastic_PortNum_ADMIN_APP) + return; - setNodeImage(p.from, (MeshtasticView::eRole)(unsigned long)nodes[p.from]->LV_OBJ_IDX(node_img_idx)->user_data, false, - objects.detector_contact_image); - const char *lbl = lv_label_get_text(nodes[p.from]->LV_OBJ_IDX(node_lbl_idx)); + // get actual time + char timebuf[16]; + time_t curr_time; +#ifdef ARCH_PORTDUINO + time(&curr_time); +#else + curr_time = actTime; +#endif + tm *curr_tm = localtime(&curr_time); + if (VALID_TIME(curr_time)) { + strftime(timebuf, 16, "%T", curr_tm); + } else { + strcpy(timebuf, "??:??:??"); + } + // get node name from char from[5]; char *userShort = (char *)&(nodes[p.from]->LV_OBJ_IDX(node_lbs_idx)->user_data); int pos = 0; @@ -2980,4409 +2924,4322 @@ void TFTView_320x240::packetDetected(const meshtastic_MeshPacket &p) } from[pos] = '\0'; - char buf[64]; - lv_snprintf(buf, 64, "%s(%04x)\n%s", from, p.from & 0xffff, lbl); - lv_label_set_text(objects.detector_contact_label, buf); - lv_obj_clear_flag(objects.detector_contact_button, LV_OBJ_FLAG_HIDDEN); - } else { - char buf[20]; - lv_snprintf(buf, 20, _("heard: !%08x"), heard); - lv_label_set_text(objects.detector_heard_label, buf); - lv_obj_clear_flag(objects.detector_heard_label, LV_OBJ_FLAG_HIDDEN); - } - } -} + char buf[256]; + if (p.to == 0xffffffff) + sprintf(buf, "%s: ch%d %s:%04x->all: %s", timebuf, p.channel, from, p.from & 0xffff, + name[p.decoded.portnum]); // note: this may crash if there's a new portnum not in this map... + else + sprintf(buf, "%s: ch%d %s:%04x->%s%04x: %s", timebuf, p.channel, from, p.from & 0xffff, + p.to == ownNode ? "*" : "", p.to & 0xffff, name[p.decoded.portnum]); + + if (p.decoded.portnum == meshtastic_PortNum_TELEMETRY_APP) { + meshtastic_Telemetry telemetry; + if (pb_decode_from_bytes(p.decoded.payload.bytes, p.decoded.payload.size, &meshtastic_Telemetry_msg, + &telemetry)) { + switch (telemetry.which_variant) { + case meshtastic_Telemetry_device_metrics_tag: { + if (p.from == ownNode) + return; // suppress (internal) battery level packets + strcat(buf, " dev"); + break; + } + case meshtastic_Telemetry_environment_metrics_tag: { + strcat(buf, " env"); + break; + } + case meshtastic_Telemetry_air_quality_metrics_tag: { + strcat(buf, " air"); + break; + } + case meshtastic_Telemetry_power_metrics_tag: { + strcat(buf, " pow"); + break; + } + case meshtastic_Telemetry_local_stats_tag: { + strcat(buf, " dev"); // bug in firmware that this is local? + } + default: + break; + } + } + } else if (p.decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP) { + // print the recorded route and add from/to manually + strcat(buf, "\n"); + int pos = strlen(buf); + if (p.to == ownNode) { + pos += snprintf(&buf[pos], 16, "%04x", ownNode & 0xffff); + } -void TFTView_320x240::writePacketLog(const meshtastic_MeshPacket &p) -{ - static std::unordered_map name = { - {0, "unknown"}, {1, "text message"}, {2, "remote hardware"}, {3, "position"}, {4, "node info"}, - {5, "routing"}, {6, "admin"}, {7, "text message"}, {8, "waypoint"}, {9, "audio"}, - {10, "sensor"}, {32, "reply"}, {33, "ip tunnel"}, {34, "paxcounter"}, {64, "serial"}, - {65, "store forward"}, {66, "range test"}, {67, "telemetry"}, {68, "ZPS"}, {69, "simulator"}, - {70, "tracert"}, {71, "neighbor info"}, {72, "atax"}, {73, "map report"}, {74, "power stress"}, - {256, "private"}, {257, "atax forwarder"}}; - - // ignore admin packages initiated by us - if (p.from == ownNode && p.decoded.portnum == meshtastic_PortNum_ADMIN_APP) - return; + meshtastic_RouteDiscovery route; + if (pb_decode_from_bytes(p.decoded.payload.bytes, p.decoded.payload.size, &meshtastic_RouteDiscovery_msg, + &route)) { + for (int i = 0; i < route.route_count; i++) { + uint32_t nodeNum = route.route[i]; + if (nodeNum != UINT32_MAX) { + pos += snprintf(&buf[pos], 16, "->%04x", nodeNum & 0xffff); + } else { + strcat(buf, "->unk"); + pos += 6; + } + } + if (p.to == ownNode) { + pos += snprintf(&buf[pos], 16, "->%04x", p.from & 0xffff); + } + } + } - // get actual time - char timebuf[16]; - time_t curr_time; -#ifdef ARCH_PORTDUINO - time(&curr_time); -#else - curr_time = actTime; -#endif - tm *curr_tm = localtime(&curr_time); - if (VALID_TIME(curr_time)) { - strftime(timebuf, 16, "%T", curr_tm); - } else { - strcpy(timebuf, "??:??:??"); - } + if (packetCounter >= PACKET_LOGS_MAX) { + // delete oldest entry + lv_obj_del(objects.tools_packet_log_panel->spec_attr->children[0]); + } else { + packetCounter++; + char top[24]; + sprintf(top, _("Packet Log: %d"), packetCounter); + lv_label_set_text(objects.top_packet_log_label, top); + } + lv_obj_t *pLabel = lv_label_create(objects.tools_packet_log_panel); + lv_obj_set_pos(pLabel, 0, 0); + lv_obj_set_size(pLabel, LV_PCT(100), LV_SIZE_CONTENT); + uint32_t bgColor, fgColor; + std::tie(bgColor, fgColor) = nodeColor(p.from); + lv_obj_set_style_bg_color(pLabel, lv_color_hex(bgColor), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_color(pLabel, lv_color_hex(fgColor), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(pLabel, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_label_set_text(pLabel, buf); + + // auto-scroll if last item is visible + if (lv_obj_get_scroll_bottom(objects.tools_packet_log_panel) < 20) + lv_obj_scroll_to_view(pLabel, LV_ANIM_OFF); + } + + void TFTView_320x240::updateStatistics(const meshtastic_MeshPacket &p) + { + struct Stats { + uint32_t id; + uint16_t row; + uint16_t tel; + uint16_t pos; + uint16_t inf; + uint16_t trc; + uint16_t txt; + uint16_t nbr; + uint32_t sum; + + bool operator==(const Stats &rhs) const { return id == rhs.id; } + + Stats &operator+=(const Stats &rhs) + { + this->tel += rhs.tel; + this->pos += rhs.pos; + this->inf += rhs.inf; + this->trc += rhs.trc; + this->txt += rhs.txt; + this->nbr += rhs.nbr; + this->sum += 1; + return *this; + } - // get node name from - char from[5]; - char *userShort = (char *)&(nodes[p.from]->LV_OBJ_IDX(node_lbs_idx)->user_data); - int pos = 0; - while (pos < 4 && userShort[pos] != 0) { - from[pos] = userShort[pos]; - pos++; - } - from[pos] = '\0'; - - char buf[256]; - if (p.to == 0xffffffff) - sprintf(buf, "%s: ch%d %s:%04x->all: %s", timebuf, p.channel, from, p.from & 0xffff, - name[p.decoded.portnum]); // note: this may crash if there's a new portnum not in this map... - else - sprintf(buf, "%s: ch%d %s:%04x->%s%04x: %s", timebuf, p.channel, from, p.from & 0xffff, p.to == ownNode ? "*" : "", - p.to & 0xffff, name[p.decoded.portnum]); - - if (p.decoded.portnum == meshtastic_PortNum_TELEMETRY_APP) { - meshtastic_Telemetry telemetry; - if (pb_decode_from_bytes(p.decoded.payload.bytes, p.decoded.payload.size, &meshtastic_Telemetry_msg, &telemetry)) { - switch (telemetry.which_variant) { - case meshtastic_Telemetry_device_metrics_tag: { - if (p.from == ownNode) - return; // suppress (internal) battery level packets - strcat(buf, " dev"); + bool operator<(const Stats &rhs) const + { + return sum > rhs.sum; // sort reverse but skip equal values + } + }; + static std::list stats; + + if (p.from == 0) { + // clear table + stats.clear(); + for (int i = 1; i < statisticTableRows; i++) { + for (int j = 0; j < 7; j++) { + lv_table_set_cell_value(objects.statistics_table, i, j, ""); + } + } + return; + } + + // update statistic for node + Stats stat = {p.from}; + switch (p.decoded.portnum) { + case meshtastic_PortNum_TELEMETRY_APP: { + meshtastic_Telemetry telemetry; + if (pb_decode_from_bytes(p.decoded.payload.bytes, p.decoded.payload.size, &meshtastic_Telemetry_msg, + &telemetry)) { + if (telemetry.which_variant == meshtastic_Telemetry_device_metrics_tag) { + if (p.from == ownNode) + return; // suppress (internal) battery level packets + } + } + stat.tel++; break; } - case meshtastic_Telemetry_environment_metrics_tag: { - strcat(buf, " env"); + case meshtastic_PortNum_POSITION_APP: { + stat.pos++; break; } - case meshtastic_Telemetry_air_quality_metrics_tag: { - strcat(buf, " air"); + case meshtastic_PortNum_NODEINFO_APP: { + stat.inf++; break; } - case meshtastic_Telemetry_power_metrics_tag: { - strcat(buf, " pow"); + case meshtastic_PortNum_ROUTING_APP: + case meshtastic_PortNum_TRACEROUTE_APP: { + stat.trc++; break; } - case meshtastic_Telemetry_local_stats_tag: { - strcat(buf, " dev"); // bug in firmware that this is local? + case meshtastic_PortNum_TEXT_MESSAGE_APP: + case meshtastic_PortNum_RANGE_TEST_APP: { + stat.txt++; + break; } - default: + case meshtastic_PortNum_NEIGHBORINFO_APP: { + stat.nbr++; break; } - } - } else if (p.decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP) { - // print the recorded route and add from/to manually - strcat(buf, "\n"); - int pos = strlen(buf); - if (p.to == ownNode) { - pos += snprintf(&buf[pos], 16, "%04x", ownNode & 0xffff); - } - - meshtastic_RouteDiscovery route; - if (pb_decode_from_bytes(p.decoded.payload.bytes, p.decoded.payload.size, &meshtastic_RouteDiscovery_msg, &route)) { - for (int i = 0; i < route.route_count; i++) { - uint32_t nodeNum = route.route[i]; - if (nodeNum != UINT32_MAX) { - pos += snprintf(&buf[pos], 16, "->%04x", nodeNum & 0xffff); - } else { - strcat(buf, "->unk"); - pos += 6; - } + case meshtastic_PortNum_ADMIN_APP: { + // ignore + break; } - if (p.to == ownNode) { - pos += snprintf(&buf[pos], 16, "->%04x", p.from & 0xffff); + default: + ILOG_WARN("packet portnum in stats unhandled: %d", p.decoded.portnum); + stat.sum++; + return; } - } - } - if (packetCounter >= PACKET_LOGS_MAX) { - // delete oldest entry - lv_obj_del(objects.tools_packet_log_panel->spec_attr->children[0]); - } else { - packetCounter++; - char top[24]; - sprintf(top, _("Packet Log: %d"), packetCounter); - lv_label_set_text(objects.top_packet_log_label, top); - } - lv_obj_t *pLabel = lv_label_create(objects.tools_packet_log_panel); - lv_obj_set_pos(pLabel, 0, 0); - lv_obj_set_size(pLabel, LV_PCT(100), LV_SIZE_CONTENT); - uint32_t bgColor, fgColor; - std::tie(bgColor, fgColor) = nodeColor(p.from); - lv_obj_set_style_bg_color(pLabel, lv_color_hex(bgColor), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_color(pLabel, lv_color_hex(fgColor), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(pLabel, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_label_set_text(pLabel, buf); - - // auto-scroll if last item is visible - if (lv_obj_get_scroll_bottom(objects.tools_packet_log_panel) < 20) - lv_obj_scroll_to_view(pLabel, LV_ANIM_OFF); -} + std::list::iterator it = std::find(stats.begin(), stats.end(), stat); + if (it == stats.end()) { + stat.row = stats.size(); + stat.sum = 1; + // TODO: stop if memory limit is reached + stats.push_back(stat); + } else { + *it += stat; + } -void TFTView_320x240::updateStatistics(const meshtastic_MeshPacket &p) -{ - struct Stats { - uint32_t id; - uint16_t row; - uint16_t tel; - uint16_t pos; - uint16_t inf; - uint16_t trc; - uint16_t txt; - uint16_t nbr; - uint32_t sum; + stats.sort(); - bool operator==(const Stats &rhs) const { return id == rhs.id; } + // fill packet statistics table + char buf[10]; + int row = 1; + bool move = false; - Stats &operator+=(const Stats &rhs) - { - this->tel += rhs.tel; - this->pos += rhs.pos; - this->inf += rhs.inf; - this->trc += rhs.trc; - this->txt += rhs.txt; - this->nbr += rhs.nbr; - this->sum += 1; - return *this; - } + for (auto it2 : stats) { + if (it2.id == p.from || move) { + buf[0] = '\0'; + auto it = nodes.find(it2.id); // node may have been removed from nodes, so check if still there + if (it != nodes.end() && it->second) { + char *userData = (char *)&(it->second->LV_OBJ_IDX(node_lbs_idx)->user_data); + if (userData) { + buf[0] = userData[0]; + buf[1] = userData[1]; + buf[2] = userData[2]; + buf[3] = userData[3]; + buf[4] = '\0'; + } + } - bool operator<(const Stats &rhs) const - { - return sum > rhs.sum; // sort reverse but skip equal values + lv_table_set_cell_value(objects.statistics_table, row, 0, buf); + sprintf(buf, "%d", it2.tel); + lv_table_set_cell_value(objects.statistics_table, row, 1, buf); + sprintf(buf, "%d", it2.pos); + lv_table_set_cell_value(objects.statistics_table, row, 2, buf); + sprintf(buf, "%d", it2.inf); + lv_table_set_cell_value(objects.statistics_table, row, 3, buf); + sprintf(buf, "%d", it2.trc); + lv_table_set_cell_value(objects.statistics_table, row, 4, buf); + sprintf(buf, "%d", it2.nbr); + lv_table_set_cell_value(objects.statistics_table, row, 5, buf); + sprintf(buf, "%d", it2.sum); + lv_table_set_cell_value(objects.statistics_table, row, 6, buf); + + if (row != it2.row) { + it2.row = row; + move = true; + } else { + break; + } + } + row++; + if (row >= statisticTableRows) // fill rows till bottom of 320x240 display + break; + } } - }; - static std::list stats; - if (p.from == 0) { - // clear table - stats.clear(); - for (int i = 1; i < statisticTableRows; i++) { - for (int j = 0; j < 7; j++) { - lv_table_set_cell_value(objects.statistics_table, i, j, ""); + void TFTView_320x240::ui_event_statistics_table(lv_event_t * e) + { + lv_draw_task_t *draw_task = lv_event_get_draw_task(e); + lv_draw_dsc_base_t *base_dsc = (lv_draw_dsc_base_t *)lv_draw_task_get_draw_dsc(draw_task); + // if the cells are drawn... + if (base_dsc->part == LV_PART_ITEMS) { + // make the texts in the first cell blueish + lv_draw_fill_dsc_t *fill_draw_dsc = lv_draw_task_get_fill_dsc(draw_task); + if (fill_draw_dsc) { + uint32_t row = base_dsc->id1; + if (row == 0) { + fill_draw_dsc->color = lv_color_mix(lv_palette_main(LV_PALETTE_BLUE), fill_draw_dsc->color, LV_OPA_20); + } + // make every 2nd row grayish + else { + Themes::recolorTableRow(fill_draw_dsc, row % 2 == 0); + } + } } } - return; - } - // update statistic for node - Stats stat = {p.from}; - switch (p.decoded.portnum) { - case meshtastic_PortNum_TELEMETRY_APP: { - meshtastic_Telemetry telemetry; - if (pb_decode_from_bytes(p.decoded.payload.bytes, p.decoded.payload.size, &meshtastic_Telemetry_msg, &telemetry)) { - if (telemetry.which_variant == meshtastic_Telemetry_device_metrics_tag) { - if (p.from == ownNode) - return; // suppress (internal) battery level packets + void TFTView_320x240::requestSetup(void) + { + ui_set_active(objects.settings_button, objects.initial_setup_panel, objects.top_setup_panel); + lv_dropdown_set_selected(objects.setup_region_dropdown, 0); + lv_obj_clear_flag(objects.initial_setup_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.setup_region_dropdown); + THIS->disablePanel(objects.controller_panel); + THIS->activeSettings = eSetup; + } + + /** + * update signal strength text and image for home screen + */ + void TFTView_320x240::updateSignalStrength(int32_t rssi, float snr) + { + // remember time we last heard a node + time(&lastHeard); + + if (rssi != 0 || snr != 0.0) { + char buf[40]; + sprintf(buf, "SNR: %.1f\nRSSI: %" PRId32, snr, rssi); + lv_label_set_text(objects.home_signal_label, buf); + + uint32_t pct = signalStrength2Percent(rssi, snr); + sprintf(buf, "(%d%%)", pct); + lv_label_set_text(objects.home_signal_pct_label, buf); + if (pct > 80) { + lv_obj_set_style_bg_image_src(objects.home_signal_button, &img_home_signal_button_image, + LV_PART_MAIN | LV_STATE_DEFAULT); + } else if (pct > 60) { + lv_obj_set_style_bg_image_src(objects.home_signal_button, &img_home_strong_signal_image, + LV_PART_MAIN | LV_STATE_DEFAULT); + } else if (pct > 40) { + lv_obj_set_style_bg_image_src(objects.home_signal_button, &img_home_good_signal_image, + LV_PART_MAIN | LV_STATE_DEFAULT); + } else if (pct > 20) { + lv_obj_set_style_bg_image_src(objects.home_signal_button, &img_home_fair_signal_image, + LV_PART_MAIN | LV_STATE_DEFAULT); + } else if (pct > 1) { + lv_obj_set_style_bg_image_src(objects.home_signal_button, &img_home_weak_signal_image, + LV_PART_MAIN | LV_STATE_DEFAULT); + } else { + lv_obj_set_style_bg_image_src(objects.home_signal_button, &img_home_no_signal_image, + LV_PART_MAIN | LV_STATE_DEFAULT); + } } } - stat.tel++; - break; - } - case meshtastic_PortNum_POSITION_APP: { - stat.pos++; - break; - } - case meshtastic_PortNum_NODEINFO_APP: { - stat.inf++; - break; - } - case meshtastic_PortNum_ROUTING_APP: - case meshtastic_PortNum_TRACEROUTE_APP: { - stat.trc++; - break; - } - case meshtastic_PortNum_TEXT_MESSAGE_APP: - case meshtastic_PortNum_RANGE_TEST_APP: { - stat.txt++; - break; - } - case meshtastic_PortNum_NEIGHBORINFO_APP: { - stat.nbr++; - break; - } - case meshtastic_PortNum_ADMIN_APP: { - // ignore - break; - } - default: - ILOG_WARN("packet portnum in stats unhandled: %d", p.decoded.portnum); - stat.sum++; - return; - } - - std::list::iterator it = std::find(stats.begin(), stats.end(), stat); - if (it == stats.end()) { - stat.row = stats.size(); - stat.sum = 1; - // TODO: stop if memory limit is reached - stats.push_back(stat); - } else { - *it += stat; - } - - stats.sort(); - // fill packet statistics table - char buf[10]; - int row = 1; - bool move = false; + /** + * Translate proto modem preset enum value to numerical position in dropdown menu + */ + uint32_t TFTView_320x240::preset2val(meshtastic_Config_LoRaConfig_ModemPreset preset) + { + int32_t val[] = {0, -1, -1, 4, 3, 7, 5, 1, 6, 2}; - for (auto it2 : stats) { - if (it2.id == p.from || move) { - buf[0] = '\0'; - auto it = nodes.find(it2.id); // node may have been removed from nodes, so check if still there - if (it != nodes.end() && it->second) { - char *userData = (char *)&(it->second->LV_OBJ_IDX(node_lbs_idx)->user_data); - if (userData) { - buf[0] = userData[0]; - buf[1] = userData[1]; - buf[2] = userData[2]; - buf[3] = userData[3]; - buf[4] = '\0'; - } - } - - lv_table_set_cell_value(objects.statistics_table, row, 0, buf); - sprintf(buf, "%d", it2.tel); - lv_table_set_cell_value(objects.statistics_table, row, 1, buf); - sprintf(buf, "%d", it2.pos); - lv_table_set_cell_value(objects.statistics_table, row, 2, buf); - sprintf(buf, "%d", it2.inf); - lv_table_set_cell_value(objects.statistics_table, row, 3, buf); - sprintf(buf, "%d", it2.trc); - lv_table_set_cell_value(objects.statistics_table, row, 4, buf); - sprintf(buf, "%d", it2.nbr); - lv_table_set_cell_value(objects.statistics_table, row, 5, buf); - sprintf(buf, "%d", it2.sum); - lv_table_set_cell_value(objects.statistics_table, row, 6, buf); - - if (row != it2.row) { - it2.row = row; - move = true; - } else { - break; + if (preset > (sizeof(val) / sizeof(val[0]) - 1) || val[preset] == -1) { + ILOG_WARN("unknown or deprecated preset value: %d", preset); + return 0; } + return uint32_t(val[preset]); } - row++; - if (row >= statisticTableRows) // fill rows till bottom of 320x240 display - break; - } -} -void TFTView_320x240::ui_event_statistics_table(lv_event_t *e) -{ - lv_draw_task_t *draw_task = lv_event_get_draw_task(e); - lv_draw_dsc_base_t *base_dsc = (lv_draw_dsc_base_t *)lv_draw_task_get_draw_dsc(draw_task); - // if the cells are drawn... - if (base_dsc->part == LV_PART_ITEMS) { - // make the texts in the first cell blueish - lv_draw_fill_dsc_t *fill_draw_dsc = lv_draw_task_get_fill_dsc(draw_task); - if (fill_draw_dsc) { - uint32_t row = base_dsc->id1; - if (row == 0) { - fill_draw_dsc->color = lv_color_mix(lv_palette_main(LV_PALETTE_BLUE), fill_draw_dsc->color, LV_OPA_20); - } - // make every 2nd row grayish - else { - Themes::recolorTableRow(fill_draw_dsc, row % 2 == 0); + /** + * Translate value from dropdown menu to modem preset proto enum + */ + meshtastic_Config_LoRaConfig_ModemPreset TFTView_320x240::val2preset(uint32_t val) + { + meshtastic_Config_LoRaConfig_ModemPreset preset[] = { + meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE, + meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, + meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST, + meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW}; + if (val > (sizeof(preset) / sizeof(preset[0]) - 1)) { + ILOG_ERROR("unknown preset value: %d", val); + return meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; } + return preset[val]; } - } -} - -void TFTView_320x240::requestSetup(void) -{ - ui_set_active(objects.settings_button, objects.initial_setup_panel, objects.top_setup_panel); - lv_dropdown_set_selected(objects.setup_region_dropdown, 0); - lv_obj_clear_flag(objects.initial_setup_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.setup_region_dropdown); - THIS->disablePanel(objects.controller_panel); - THIS->activeSettings = eSetup; -} - -/** - * update signal strength text and image for home screen - */ -void TFTView_320x240::updateSignalStrength(int32_t rssi, float snr) -{ - // remember time we last heard a node - time(&lastHeard); - - if (rssi != 0 || snr != 0.0) { - char buf[40]; - sprintf(buf, "SNR: %.1f\nRSSI: %" PRId32, snr, rssi); - lv_label_set_text(objects.home_signal_label, buf); - - uint32_t pct = signalStrength2Percent(rssi, snr); - sprintf(buf, "(%d%%)", pct); - lv_label_set_text(objects.home_signal_pct_label, buf); - if (pct > 80) { - lv_obj_set_style_bg_image_src(objects.home_signal_button, &img_home_signal_button_image, - LV_PART_MAIN | LV_STATE_DEFAULT); - } else if (pct > 60) { - lv_obj_set_style_bg_image_src(objects.home_signal_button, &img_home_strong_signal_image, - LV_PART_MAIN | LV_STATE_DEFAULT); - } else if (pct > 40) { - lv_obj_set_style_bg_image_src(objects.home_signal_button, &img_home_good_signal_image, - LV_PART_MAIN | LV_STATE_DEFAULT); - } else if (pct > 20) { - lv_obj_set_style_bg_image_src(objects.home_signal_button, &img_home_fair_signal_image, - LV_PART_MAIN | LV_STATE_DEFAULT); - } else if (pct > 1) { - lv_obj_set_style_bg_image_src(objects.home_signal_button, &img_home_weak_signal_image, - LV_PART_MAIN | LV_STATE_DEFAULT); - } else { - lv_obj_set_style_bg_image_src(objects.home_signal_button, &img_home_no_signal_image, LV_PART_MAIN | LV_STATE_DEFAULT); - } - } -} - -/** - * Translate proto modem preset enum value to numerical position in dropdown menu - */ -uint32_t TFTView_320x240::preset2val(meshtastic_Config_LoRaConfig_ModemPreset preset) -{ - int32_t val[] = {0, -1, -1, 4, 3, 7, 5, 1, 6, 2}; - - if (preset > (sizeof(val) / sizeof(val[0]) - 1) || val[preset] == -1) { - ILOG_WARN("unknown or deprecated preset value: %d", preset); - return 0; - } - return uint32_t(val[preset]); -} -/** - * Translate value from dropdown menu to modem preset proto enum - */ -meshtastic_Config_LoRaConfig_ModemPreset TFTView_320x240::val2preset(uint32_t val) -{ - meshtastic_Config_LoRaConfig_ModemPreset preset[] = { - meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE, - meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, - meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST, - meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW}; - if (val > (sizeof(preset) / sizeof(preset[0]) - 1)) { - ILOG_ERROR("unknown preset value: %d", val); - return meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; - } - return preset[val]; -} - -/** - * Translate proto role enum value to numerical position in dropdown menu - */ -uint32_t TFTView_320x240::role2val(meshtastic_Config_DeviceConfig_Role role) -{ + /** + * Translate proto role enum value to numerical position in dropdown menu + */ + uint32_t TFTView_320x240::role2val(meshtastic_Config_DeviceConfig_Role role) + { #ifdef USE_ROUTER_ROLE - int32_t val[] = {0, 1, 2, -1, 3, 4, 5, 6, 7, 8, 9}; + int32_t val[] = {0, 1, 2, -1, 3, 4, 5, 6, 7, 8, 9}; #else - int32_t val[] = {0, 1, -1, -1, -1, 2, 3, 4, 5, 6, 7}; + int32_t val[] = {0, 1, -1, -1, -1, 2, 3, 4, 5, 6, 7}; #endif - if (role > 10 || val[role] == -1) { - ILOG_WARN("unknown role value: %d", role); - return 0; - } - return uint32_t(val[role]); -} + if (role > 10 || val[role] == -1) { + ILOG_WARN("unknown role value: %d", role); + return 0; + } + return uint32_t(val[role]); + } -/** - * Translate value from dropdown menu to role proto enum - */ -meshtastic_Config_DeviceConfig_Role TFTView_320x240::val2role(uint32_t val) -{ - meshtastic_Config_DeviceConfig_Role role[] = {meshtastic_Config_DeviceConfig_Role_CLIENT, - meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE, + /** + * Translate value from dropdown menu to role proto enum + */ + meshtastic_Config_DeviceConfig_Role TFTView_320x240::val2role(uint32_t val) + { + meshtastic_Config_DeviceConfig_Role role[] = {meshtastic_Config_DeviceConfig_Role_CLIENT, + meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE, #ifdef USE_ROUTER_ROLE - meshtastic_Config_DeviceConfig_Role_ROUTER, - meshtastic_Config_DeviceConfig_Role_REPEATER, + meshtastic_Config_DeviceConfig_Role_ROUTER, + meshtastic_Config_DeviceConfig_Role_REPEATER, #endif - meshtastic_Config_DeviceConfig_Role_TRACKER, - meshtastic_Config_DeviceConfig_Role_SENSOR, - meshtastic_Config_DeviceConfig_Role_TAK, - meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN, - meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND, - meshtastic_Config_DeviceConfig_Role_TAK_TRACKER, - meshtastic_Config_DeviceConfig_Role_ROUTER_LATE}; - if (val > 10) { - ILOG_WARN("unknown role value: %d", val); - return meshtastic_Config_DeviceConfig_Role_CLIENT; - } - return role[val]; -} + meshtastic_Config_DeviceConfig_Role_TRACKER, + meshtastic_Config_DeviceConfig_Role_SENSOR, + meshtastic_Config_DeviceConfig_Role_TAK, + meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN, + meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND, + meshtastic_Config_DeviceConfig_Role_TAK_TRACKER, + meshtastic_Config_DeviceConfig_Role_ROUTER_LATE}; + if (val > 10) { + ILOG_WARN("unknown role value: %d", val); + return meshtastic_Config_DeviceConfig_Role_CLIENT; + } + return role[val]; + } -/** - * Translate language proto enum value to (alphabetical) position in dropdown menu - */ -uint32_t TFTView_320x240::language2val(meshtastic_Language lang) -{ - switch (lang) { - case meshtastic_Language_ENGLISH: - return 0; - case meshtastic_Language_FRENCH: - return 7; - case meshtastic_Language_GERMAN: - return 4; - case meshtastic_Language_ITALIAN: - return 8; - case meshtastic_Language_PORTUGUESE: - return 12; - case meshtastic_Language_SPANISH: - return 6; - case meshtastic_Language_SWEDISH: - return 17; - case meshtastic_Language_FINNISH: - return 16; - case meshtastic_Language_POLISH: - return 11; - case meshtastic_Language_TURKISH: - return 18; - case meshtastic_Language_SERBIAN: - return 15; - case meshtastic_Language_RUSSIAN: - return 13; - case meshtastic_Language_DUTCH: - return 9; - case meshtastic_Language_GREEK: - return 5; - case meshtastic_Language_NORWEGIAN: - return 10; - case meshtastic_Language_SLOVENIAN: - return 14; - case meshtastic_Language_UKRAINIAN: - return 19; - case meshtastic_Language_BULGARIAN: - return 1; - case meshtastic_Language_CZECH: - return 2; - case meshtastic_Language_DANISH: - return 3; - case meshtastic_Language_SIMPLIFIED_CHINESE: - return 20; - case meshtastic_Language_TRADITIONAL_CHINESE: - return 21; - default: - ILOG_WARN("unknown language uiconfig: %d", lang); - } - return 0; -} + /** + * Translate language proto enum value to (alphabetical) position in dropdown menu + */ + uint32_t TFTView_320x240::language2val(meshtastic_Language lang) + { + switch (lang) { + case meshtastic_Language_ENGLISH: + return 0; + case meshtastic_Language_FRENCH: + return 7; + case meshtastic_Language_GERMAN: + return 4; + case meshtastic_Language_ITALIAN: + return 8; + case meshtastic_Language_PORTUGUESE: + return 12; + case meshtastic_Language_SPANISH: + return 6; + case meshtastic_Language_SWEDISH: + return 17; + case meshtastic_Language_FINNISH: + return 16; + case meshtastic_Language_POLISH: + return 11; + case meshtastic_Language_TURKISH: + return 18; + case meshtastic_Language_SERBIAN: + return 15; + case meshtastic_Language_RUSSIAN: + return 13; + case meshtastic_Language_DUTCH: + return 9; + case meshtastic_Language_GREEK: + return 5; + case meshtastic_Language_NORWEGIAN: + return 10; + case meshtastic_Language_SLOVENIAN: + return 14; + case meshtastic_Language_UKRAINIAN: + return 19; + case meshtastic_Language_BULGARIAN: + return 1; + case meshtastic_Language_CZECH: + return 2; + case meshtastic_Language_DANISH: + return 3; + case meshtastic_Language_SIMPLIFIED_CHINESE: + return 20; + case meshtastic_Language_TRADITIONAL_CHINESE: + return 21; + default: + ILOG_WARN("unknown language uiconfig: %d", lang); + } + return 0; + } -/** - * Translate value from dropdown menu to language proto enum - */ -meshtastic_Language TFTView_320x240::val2language(uint32_t val) -{ - switch (val) { - case 0: - return meshtastic_Language_ENGLISH; - case 7: - return meshtastic_Language_FRENCH; - case 4: - return meshtastic_Language_GERMAN; - case 8: - return meshtastic_Language_ITALIAN; - case 12: - return meshtastic_Language_PORTUGUESE; - case 6: - return meshtastic_Language_SPANISH; - case 17: - return meshtastic_Language_SWEDISH; - case 16: - return meshtastic_Language_FINNISH; - case 11: - return meshtastic_Language_POLISH; - case 18: - return meshtastic_Language_TURKISH; - case 15: - return meshtastic_Language_SERBIAN; - case 13: - return meshtastic_Language_RUSSIAN; - case 9: - return meshtastic_Language_DUTCH; - case 5: - return meshtastic_Language_GREEK; - case 10: - return meshtastic_Language_NORWEGIAN; - case 14: - return meshtastic_Language_SLOVENIAN; - case 19: - return meshtastic_Language_UKRAINIAN; - case 1: - return meshtastic_Language_BULGARIAN; - case 2: - return meshtastic_Language_CZECH; - case 3: - return meshtastic_Language_DANISH; - case 20: - return meshtastic_Language_SIMPLIFIED_CHINESE; - case 21: - return meshtastic_Language_TRADITIONAL_CHINESE; - default: - ILOG_WARN("unknown language val: %d", val); - } - return meshtastic_Language_ENGLISH; -} + /** + * Translate value from dropdown menu to language proto enum + */ + meshtastic_Language TFTView_320x240::val2language(uint32_t val) + { + switch (val) { + case 0: + return meshtastic_Language_ENGLISH; + case 7: + return meshtastic_Language_FRENCH; + case 4: + return meshtastic_Language_GERMAN; + case 8: + return meshtastic_Language_ITALIAN; + case 12: + return meshtastic_Language_PORTUGUESE; + case 6: + return meshtastic_Language_SPANISH; + case 17: + return meshtastic_Language_SWEDISH; + case 16: + return meshtastic_Language_FINNISH; + case 11: + return meshtastic_Language_POLISH; + case 18: + return meshtastic_Language_TURKISH; + case 15: + return meshtastic_Language_SERBIAN; + case 13: + return meshtastic_Language_RUSSIAN; + case 9: + return meshtastic_Language_DUTCH; + case 5: + return meshtastic_Language_GREEK; + case 10: + return meshtastic_Language_NORWEGIAN; + case 14: + return meshtastic_Language_SLOVENIAN; + case 19: + return meshtastic_Language_UKRAINIAN; + case 1: + return meshtastic_Language_BULGARIAN; + case 2: + return meshtastic_Language_CZECH; + case 3: + return meshtastic_Language_DANISH; + case 20: + return meshtastic_Language_SIMPLIFIED_CHINESE; + case 21: + return meshtastic_Language_TRADITIONAL_CHINESE; + default: + ILOG_WARN("unknown language val: %d", val); + } + return meshtastic_Language_ENGLISH; + } -/** - * @brief Set lv_i18n language - */ -void TFTView_320x240::setLocale(meshtastic_Language lang) -{ - const char *locale = "en_US.UTF-8"; - switch (lang) { - case meshtastic_Language_ENGLISH: - lv_i18n_set_locale("en"); - break; - case meshtastic_Language_BULGARIAN: - lv_i18n_set_locale("bg"); - locale = "bg_BG.UTF-8"; - break; - case meshtastic_Language_GERMAN: - lv_i18n_set_locale("de"); - locale = "de_DE.UTF-8"; - break; - case meshtastic_Language_SPANISH: - lv_i18n_set_locale("es"); - locale = "es_ES.UTF-8"; - break; - case meshtastic_Language_FRENCH: - lv_i18n_set_locale("fr"); - locale = "fr_FR.UTF-8"; - break; - case meshtastic_Language_ITALIAN: - lv_i18n_set_locale("it"); - locale = "it_IT.UTF-8"; - break; - case meshtastic_Language_PORTUGUESE: - lv_i18n_set_locale("pt"); - locale = "pt_PT.UTF-8"; - break; - case meshtastic_Language_SWEDISH: - lv_i18n_set_locale("se"); - locale = "sv_SE.UTF-8"; - break; - case meshtastic_Language_FINNISH: - lv_i18n_set_locale("fi"); - locale = "fi_FI.UTF-8"; - break; - case meshtastic_Language_POLISH: - lv_i18n_set_locale("pl"); - locale = "pl_PL.UTF-8"; - break; - case meshtastic_Language_TURKISH: - lv_i18n_set_locale("tr"); - locale = "tr_TR.UTF-8"; - break; - case meshtastic_Language_SERBIAN: - lv_i18n_set_locale("sr"); - locale = "sr_RS.UTF-8"; - break; - case meshtastic_Language_DUTCH: - lv_i18n_set_locale("nl"); - locale = "nl_NL.UTF-8"; - break; - case meshtastic_Language_RUSSIAN: - lv_i18n_set_locale("ru"); - locale = "ru_RU.UTF-8"; - break; - case meshtastic_Language_GREEK: - lv_i18n_set_locale("el"); - locale = "el_GR.UTF-8"; - break; - case meshtastic_Language_NORWEGIAN: - lv_i18n_set_locale("no"); - locale = "no_NO.UTF-8"; - break; - case meshtastic_Language_SLOVENIAN: - lv_i18n_set_locale("sl"); - locale = "sl_SI.UTF-8"; - break; - case meshtastic_Language_UKRAINIAN: - lv_i18n_set_locale("uk"); - locale = "uk_UA.UTF-8"; - break; - case meshtastic_Language_CZECH: - lv_i18n_set_locale("cs"); - locale = "cs_CZ.UTF-8"; - break; - case meshtastic_Language_DANISH: - lv_i18n_set_locale("da"); - locale = "da_DK.UTF-8"; - break; - case meshtastic_Language_SIMPLIFIED_CHINESE: - lv_i18n_set_locale("cn"); - locale = "zh_CN.UTF-8"; - break; - case meshtastic_Language_TRADITIONAL_CHINESE: - lv_i18n_set_locale("tw"); - locale = "zh_TW.UTF-8"; - break; - default: - ILOG_WARN("Language %d not implemented", lang); - break; - } + /** + * @brief Set lv_i18n language + */ + void TFTView_320x240::setLocale(meshtastic_Language lang) + { + const char *locale = "en_US.UTF-8"; + switch (lang) { + case meshtastic_Language_ENGLISH: + lv_i18n_set_locale("en"); + break; + case meshtastic_Language_BULGARIAN: + lv_i18n_set_locale("bg"); + locale = "bg_BG.UTF-8"; + break; + case meshtastic_Language_GERMAN: + lv_i18n_set_locale("de"); + locale = "de_DE.UTF-8"; + break; + case meshtastic_Language_SPANISH: + lv_i18n_set_locale("es"); + locale = "es_ES.UTF-8"; + break; + case meshtastic_Language_FRENCH: + lv_i18n_set_locale("fr"); + locale = "fr_FR.UTF-8"; + break; + case meshtastic_Language_ITALIAN: + lv_i18n_set_locale("it"); + locale = "it_IT.UTF-8"; + break; + case meshtastic_Language_PORTUGUESE: + lv_i18n_set_locale("pt"); + locale = "pt_PT.UTF-8"; + break; + case meshtastic_Language_SWEDISH: + lv_i18n_set_locale("se"); + locale = "sv_SE.UTF-8"; + break; + case meshtastic_Language_FINNISH: + lv_i18n_set_locale("fi"); + locale = "fi_FI.UTF-8"; + break; + case meshtastic_Language_POLISH: + lv_i18n_set_locale("pl"); + locale = "pl_PL.UTF-8"; + break; + case meshtastic_Language_TURKISH: + lv_i18n_set_locale("tr"); + locale = "tr_TR.UTF-8"; + break; + case meshtastic_Language_SERBIAN: + lv_i18n_set_locale("sr"); + locale = "sr_RS.UTF-8"; + break; + case meshtastic_Language_DUTCH: + lv_i18n_set_locale("nl"); + locale = "nl_NL.UTF-8"; + break; + case meshtastic_Language_RUSSIAN: + lv_i18n_set_locale("ru"); + locale = "ru_RU.UTF-8"; + break; + case meshtastic_Language_GREEK: + lv_i18n_set_locale("el"); + locale = "el_GR.UTF-8"; + break; + case meshtastic_Language_NORWEGIAN: + lv_i18n_set_locale("no"); + locale = "no_NO.UTF-8"; + break; + case meshtastic_Language_SLOVENIAN: + lv_i18n_set_locale("sl"); + locale = "sl_SI.UTF-8"; + break; + case meshtastic_Language_UKRAINIAN: + lv_i18n_set_locale("uk"); + locale = "uk_UA.UTF-8"; + break; + case meshtastic_Language_CZECH: + lv_i18n_set_locale("cs"); + locale = "cs_CZ.UTF-8"; + break; + case meshtastic_Language_DANISH: + lv_i18n_set_locale("da"); + locale = "da_DK.UTF-8"; + break; + case meshtastic_Language_SIMPLIFIED_CHINESE: + lv_i18n_set_locale("cn"); + locale = "zh_CN.UTF-8"; + break; + case meshtastic_Language_TRADITIONAL_CHINESE: + lv_i18n_set_locale("tw"); + locale = "zh_TW.UTF-8"; + break; + default: + ILOG_WARN("Language %d not implemented", lang); + break; + } #if defined(LOCALE_SUPPORT) - std::locale::global(std::locale(locale)); + std::locale::global(std::locale(locale)); #else - (void)locale; + (void)locale; #endif -} - -/** - * @brief Set language (using dropdown strings) - */ -void TFTView_320x240::setLanguage(meshtastic_Language lang) -{ - char buf1[20], buf2[40]; - lv_dropdown_set_selected(objects.settings_language_dropdown, language2val(lang)); - lv_dropdown_get_selected_str(objects.settings_language_dropdown, buf1, sizeof(buf1)); - lv_snprintf(buf2, sizeof(buf2), _("Language: %s"), buf1); - lv_label_set_text(objects.basic_settings_language_label, buf2); -} - -/** - * @brief Set timeout - */ -void TFTView_320x240::setTimeout(uint32_t timeout) -{ - char buf[32]; - if (timeout == 0) - lv_snprintf(buf, sizeof(buf), _("Screen Timeout: off")); - else - lv_snprintf(buf, sizeof(buf), _("Screen Timeout: %ds"), timeout); - lv_label_set_text(objects.basic_settings_timeout_label, buf); - THIS->displaydriver->setScreenTimeout(timeout); -} - -/** - * @brief Set brightness - */ -void TFTView_320x240::setBrightness(uint32_t brightness) -{ - char buf[32]; - lv_snprintf(buf, sizeof(buf), _("Screen Brightness: %d%%"), uint16_t(round((brightness * 100) / 255.0))); - lv_label_set_text(objects.basic_settings_brightness_label, buf); - THIS->displaydriver->setBrightness((uint8_t)brightness); -} - -/** - * @brief Set theme to new value - */ -void TFTView_320x240::setTheme(uint32_t value) -{ - char buf1[30], buf2[30]; - lv_dropdown_set_selected(objects.settings_theme_dropdown, value); - lv_dropdown_get_selected_str(objects.settings_theme_dropdown, buf1, sizeof(buf1)); - lv_snprintf(buf2, sizeof(buf2), _("Theme: %s"), buf1); - lv_label_set_text(objects.basic_settings_theme_label, buf2); - - // change theme and redraw UI - Themes::set(Themes::Theme(value)); - updateTheme(); -} - -/** - * @brief Save all data from node options panel - */ -void TFTView_320x240::storeNodeOptions(void) -{ - // store node filter options - meshtastic_NodeFilter &filter = db.uiConfig.node_filter; - db.uiConfig.has_node_filter = true; - filter.unknown_switch = lv_obj_has_state(objects.nodes_filter_unknown_switch, LV_STATE_CHECKED); - filter.offline_switch = lv_obj_has_state(objects.nodes_filter_offline_switch, LV_STATE_CHECKED); - filter.public_key_switch = lv_obj_has_state(objects.nodes_filter_public_key_switch, LV_STATE_CHECKED); - // filter.channel = lv_dropdown_get_selected(objects.nodes_filter_channel_dropdown); - filter.hops_away = lv_dropdown_get_selected(objects.nodes_filter_hops_dropdown); - // filter.mqtt_switch = lv_obj_has_state(objects.nodes_filter_mqtt_switch, LV_STATE_CHECKED); - filter.position_switch = lv_obj_has_state(objects.nodes_filter_position_switch, LV_STATE_CHECKED); - strncpy(filter.node_name, lv_textarea_get_text(objects.nodes_filter_name_area), sizeof(filter.node_name)); - - // store node highlight options - meshtastic_NodeHighlight &highlight = db.uiConfig.node_highlight; - db.uiConfig.has_node_highlight = true; - highlight.chat_switch = lv_obj_has_state(objects.nodes_hl_active_chat_switch, LV_STATE_CHECKED); - highlight.position_switch = lv_obj_has_state(objects.nodes_hl_position_switch, LV_STATE_CHECKED); - highlight.telemetry_switch = lv_obj_has_state(objects.nodes_hl_telemetry_switch, LV_STATE_CHECKED); - highlight.iaq_switch = lv_obj_has_state(objects.nodes_hliaq_switch, LV_STATE_CHECKED); - strncpy(highlight.node_name, lv_textarea_get_text(objects.nodes_hl_name_area), sizeof(highlight.node_name)); - - controller->storeUIConfig(db.uiConfig); -} - -/** - * @brief erase chat and all its resources - */ -void TFTView_320x240::eraseChat(uint32_t channelOrNode) -{ - if (chats.find(channelOrNode) == chats.end()) { - ILOG_WARN("eraseChat: channelOrNode %d not found", channelOrNode); - return; - } - if (channelOrNode < c_max_channels) { - uint8_t ch = (uint8_t)channelOrNode; - if (state == MeshtasticView::eRunning) { - lv_obj_delete_delayed(chats.at(ch), 500); - } else { - lv_obj_del(chats.at(ch)); } - lv_obj_del(channelGroup.at(ch)); - channelGroup[ch] = nullptr; - chats.erase(ch); - } else { - uint32_t nodeNum = channelOrNode; - if (state == MeshtasticView::eRunning) { - lv_obj_delete_delayed(chats.at(nodeNum), 500); - } else { - lv_obj_delete(chats.at(nodeNum)); + + /** + * @brief Set language (using dropdown strings) + */ + void TFTView_320x240::setLanguage(meshtastic_Language lang) + { + char buf1[20], buf2[40]; + lv_dropdown_set_selected(objects.settings_language_dropdown, language2val(lang)); + lv_dropdown_get_selected_str(objects.settings_language_dropdown, buf1, sizeof(buf1)); + lv_snprintf(buf2, sizeof(buf2), _("Language: %s"), buf1); + lv_label_set_text(objects.basic_settings_language_label, buf2); } - lv_obj_del(messages.at(nodeNum)); - messages.erase(nodeNum); - chats.erase(nodeNum); - } -} -/** - * @brief clears all (persistent) chat messages - */ -void TFTView_320x240::clearChatHistory(void) -{ - for (auto &it : chats) { - lv_obj_delete(it.second); - if (it.first < c_max_channels) { - lv_obj_delete(channelGroup[it.first]); - channelGroup[it.first] = nullptr; - } else { - lv_obj_delete(messages[it.first]); + /** + * @brief Set timeout + */ + void TFTView_320x240::setTimeout(uint32_t timeout) + { + char buf[32]; + if (timeout == 0) + lv_snprintf(buf, sizeof(buf), _("Screen Timeout: off")); + else + lv_snprintf(buf, sizeof(buf), _("Screen Timeout: %ds"), timeout); + lv_label_set_text(objects.basic_settings_timeout_label, buf); + THIS->displaydriver->setScreenTimeout(timeout); + } + + /** + * @brief Set brightness + */ + void TFTView_320x240::setBrightness(uint32_t brightness) + { + char buf[32]; + lv_snprintf(buf, sizeof(buf), _("Screen Brightness: %d%%"), uint16_t(round((brightness * 100) / 255.0))); + lv_label_set_text(objects.basic_settings_brightness_label, buf); + THIS->displaydriver->setBrightness((uint8_t)brightness); } - } - chats.clear(); - messages.clear(); - updateActiveChats(); - updateNodesFiltered(true); - controller->removeTextMessages(0, 0, 0); -} -/** - * @brief User widget OK button handling - * - * @param e - */ -void TFTView_320x240::ui_event_ok(lv_event_t *e) -{ - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED) { - switch (THIS->activeSettings) { - case eSetup: { - meshtastic_Config_LoRaConfig_RegionCode region = - (meshtastic_Config_LoRaConfig_RegionCode)(lv_dropdown_get_selected(objects.setup_region_dropdown) + 1); - - uint32_t numChannels = LoRaPresets::getNumChannels(region, THIS->db.config.lora.modem_preset); - // if (numChannels == 0) { - // // region not possible for selected preset, revert - // lv_dropdown_set_selected(objects.settings_region_dropdown, THIS->db.config.lora.region - 1); - // return; - // } - - if (region != THIS->db.config.lora.region) { - char buf1[10], buf2[30]; - lv_dropdown_get_selected_str(objects.setup_region_dropdown, buf1, sizeof(buf1)); - lv_snprintf(buf2, sizeof(buf2), _("Region: %s"), buf1); - lv_label_set_text(objects.basic_settings_region_label, buf2); - - meshtastic_Config_LoRaConfig &lora = THIS->db.config.lora; - uint32_t defaultSlot = lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET ? lora.channel_num : 0; - if (defaultSlot == 0) { - defaultSlot = - LoRaPresets::getDefaultSlot(region, THIS->db.config.lora.modem_preset, THIS->db.channel[0].settings.name); - } - lora.region = region; - lora.channel_num = (defaultSlot <= numChannels ? defaultSlot : 1); - THIS->controller->sendConfig(meshtastic_Config_LoRaConfig{lora}, THIS->ownNode); - } - - char buf[30]; - const char *userShort = lv_textarea_get_text(objects.setup_user_short_textarea); - const char *userLong = lv_textarea_get_text(objects.setup_user_long_textarea); - if (strcmp(userShort, THIS->db.short_name) || strcmp(userLong, THIS->db.long_name)) { - lv_snprintf(buf, sizeof(buf), _("User name: %s"), userShort); - lv_label_set_text(objects.basic_settings_user_label, buf); - lv_label_set_text(objects.user_name_short_label, userShort); - lv_label_set_text(objects.user_name_label, userLong); - strcpy(THIS->db.short_name, userShort); - strcpy(THIS->db.long_name, userLong); - meshtastic_User user{}; // TODO: don't overwrite is_licensed - strcpy(user.short_name, userShort); - strcpy(user.long_name, userLong); - THIS->controller->sendConfig(user, THIS->ownNode); - } - THIS->notifyReboot(true); + /** + * @brief Set theme to new value + */ + void TFTView_320x240::setTheme(uint32_t value) + { + char buf1[30], buf2[30]; + lv_dropdown_set_selected(objects.settings_theme_dropdown, value); + lv_dropdown_get_selected_str(objects.settings_theme_dropdown, buf1, sizeof(buf1)); + lv_snprintf(buf2, sizeof(buf2), _("Theme: %s"), buf1); + lv_label_set_text(objects.basic_settings_theme_label, buf2); - lv_obj_add_flag(objects.initial_setup_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.home_button); - break; + // change theme and redraw UI + Themes::set(Themes::Theme(value)); + updateTheme(); } - case eUsername: { - char buf[30]; - const char *userShort = lv_textarea_get_text(objects.settings_user_short_textarea); - const char *userLong = lv_textarea_get_text(objects.settings_user_long_textarea); - if (strcmp(userShort, THIS->db.short_name) || strcmp(userLong, THIS->db.long_name)) { - lv_snprintf(buf, sizeof(buf), _("User name: %s"), userShort); - lv_label_set_text(objects.basic_settings_user_label, buf); - lv_label_set_text(objects.user_name_short_label, userShort); - lv_label_set_text(objects.user_name_label, userLong); - strcpy(THIS->db.short_name, userShort); - strcpy(THIS->db.long_name, userLong); - meshtastic_User user{}; // TODO: don't overwrite is_licensed - strcpy(user.short_name, userShort); - strcpy(user.long_name, userLong); - THIS->controller->sendConfig(user, THIS->ownNode); - THIS->notifyReboot(true); - } - lv_obj_add_flag(objects.settings_username_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_user_button); - break; - } - case eDeviceRole: { - meshtastic_Config_DeviceConfig &device = THIS->db.config.device; - meshtastic_Config_DeviceConfig_Role role = - THIS->val2role(lv_dropdown_get_selected(objects.settings_device_role_dropdown)); - if (role != device.role) { - char buf1[30], buf2[40]; - lv_dropdown_get_selected_str(objects.settings_device_role_dropdown, buf1, sizeof(buf1)); - lv_snprintf(buf2, sizeof(buf2), _("Device Role: %s"), buf1); - lv_label_set_text(objects.basic_settings_role_label, buf2); + /** + * @brief Save all data from node options panel + */ + void TFTView_320x240::storeNodeOptions(void) + { + // store node filter options + meshtastic_NodeFilter &filter = db.uiConfig.node_filter; + db.uiConfig.has_node_filter = true; + filter.unknown_switch = lv_obj_has_state(objects.nodes_filter_unknown_switch, LV_STATE_CHECKED); + filter.offline_switch = lv_obj_has_state(objects.nodes_filter_offline_switch, LV_STATE_CHECKED); + filter.public_key_switch = lv_obj_has_state(objects.nodes_filter_public_key_switch, LV_STATE_CHECKED); + // filter.channel = lv_dropdown_get_selected(objects.nodes_filter_channel_dropdown); + filter.hops_away = lv_dropdown_get_selected(objects.nodes_filter_hops_dropdown); + // filter.mqtt_switch = lv_obj_has_state(objects.nodes_filter_mqtt_switch, LV_STATE_CHECKED); + filter.position_switch = lv_obj_has_state(objects.nodes_filter_position_switch, LV_STATE_CHECKED); + strncpy(filter.node_name, lv_textarea_get_text(objects.nodes_filter_name_area), sizeof(filter.node_name)); + + // store node highlight options + meshtastic_NodeHighlight &highlight = db.uiConfig.node_highlight; + db.uiConfig.has_node_highlight = true; + highlight.chat_switch = lv_obj_has_state(objects.nodes_hl_active_chat_switch, LV_STATE_CHECKED); + highlight.position_switch = lv_obj_has_state(objects.nodes_hl_position_switch, LV_STATE_CHECKED); + highlight.telemetry_switch = lv_obj_has_state(objects.nodes_hl_telemetry_switch, LV_STATE_CHECKED); + highlight.iaq_switch = lv_obj_has_state(objects.nodes_hliaq_switch, LV_STATE_CHECKED); + strncpy(highlight.node_name, lv_textarea_get_text(objects.nodes_hl_name_area), sizeof(highlight.node_name)); - device.role = role; - THIS->controller->sendConfig(meshtastic_Config_DeviceConfig{device}, THIS->ownNode); - THIS->notifyReboot(true); - } - lv_obj_add_flag(objects.settings_device_role_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_role_button); - break; + controller->storeUIConfig(db.uiConfig); } - case eRegion: { - meshtastic_Config_LoRaConfig_RegionCode region = - (meshtastic_Config_LoRaConfig_RegionCode)(lv_dropdown_get_selected(objects.settings_region_dropdown) + 1); - uint32_t numChannels = LoRaPresets::getNumChannels(region, THIS->db.config.lora.modem_preset); - if (numChannels == 0) { - // region not possible for selected preset, revert - lv_dropdown_set_selected(objects.settings_region_dropdown, THIS->db.config.lora.region - 1); + /** + * @brief erase chat and all its resources + */ + void TFTView_320x240::eraseChat(uint32_t channelOrNode) + { + if (chats.find(channelOrNode) == chats.end()) { + ILOG_WARN("eraseChat: channelOrNode %d not found", channelOrNode); return; } - - if (region != THIS->db.config.lora.region) { - char buf1[10], buf2[30]; - lv_dropdown_get_selected_str(objects.settings_region_dropdown, buf1, sizeof(buf1)); - lv_snprintf(buf2, sizeof(buf2), _("Region: %s"), buf1); - lv_label_set_text(objects.basic_settings_region_label, buf2); - - meshtastic_Config_LoRaConfig &lora = THIS->db.config.lora; - uint32_t defaultSlot = lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET ? lora.channel_num : 0; - if (defaultSlot == 0) { - defaultSlot = - LoRaPresets::getDefaultSlot(region, THIS->db.config.lora.modem_preset, THIS->db.channel[0].settings.name); + if (channelOrNode < c_max_channels) { + uint8_t ch = (uint8_t)channelOrNode; + if (state == MeshtasticView::eRunning) { + lv_obj_delete_delayed(chats.at(ch), 500); + } else { + lv_obj_del(chats.at(ch)); } - lora.region = region; - lora.channel_num = (defaultSlot <= numChannels ? defaultSlot : 1); - THIS->controller->sendConfig(meshtastic_Config_LoRaConfig{lora}, THIS->ownNode); - THIS->notifyReboot(true); + lv_obj_del(channelGroup.at(ch)); + channelGroup[ch] = nullptr; + chats.erase(ch); + } else { + uint32_t nodeNum = channelOrNode; + if (state == MeshtasticView::eRunning) { + lv_obj_delete_delayed(chats.at(nodeNum), 500); + } else { + lv_obj_delete(chats.at(nodeNum)); + } + lv_obj_del(messages.at(nodeNum)); + messages.erase(nodeNum); + chats.erase(nodeNum); } - lv_obj_add_flag(objects.settings_region_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_region_button); - break; } - case eModemPreset: { - meshtastic_Config_LoRaConfig &lora = THIS->db.config.lora; - meshtastic_Config_LoRaConfig_ModemPreset preset = - THIS->val2preset(lv_dropdown_get_selected(objects.settings_modem_preset_dropdown)); - uint16_t channelNum = lv_slider_get_value(objects.frequency_slot_slider); - if (preset != lora.modem_preset || lora.channel_num != channelNum) { - char buf1[16], buf2[32]; - lv_dropdown_get_selected_str(objects.settings_modem_preset_dropdown, buf1, sizeof(buf1)); - lv_snprintf(buf2, sizeof(buf2), _("Modem Preset: %s"), buf1); - lv_label_set_text(objects.basic_settings_modem_preset_label, buf2); - lora.use_preset = true; - lora.modem_preset = preset; - lora.channel_num = channelNum; - THIS->controller->sendConfig(meshtastic_Config_LoRaConfig{lora}, THIS->ownNode); - THIS->notifyReboot(true); - } - lv_obj_add_flag(objects.settings_modem_preset_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_modem_preset_button); - break; - } - case eChannel: { - for (int i = 0; i < c_max_channels; i++) { - // check if channel changed, then update and send to radio - if (memcmp(&THIS->db.channel[i], &THIS->channel_scratch[i], sizeof(THIS->channel_scratch[i])) != 0) { - THIS->channel_scratch[i].has_settings = true; - THIS->updateChannelConfig(THIS->channel_scratch[i]); - THIS->controller->sendConfig(THIS->channel_scratch[i], THIS->ownNode); + /** + * @brief clears all (persistent) chat messages + */ + void TFTView_320x240::clearChatHistory(void) + { + for (auto &it : chats) { + lv_obj_delete(it.second); + if (it.first < c_max_channels) { + lv_obj_delete(channelGroup[it.first]); + channelGroup[it.first] = nullptr; + } else { + lv_obj_delete(messages[it.first]); } } - - int8_t ch = (signed long)THIS->ch_label[0]->user_data; - THIS->setChannelName(THIS->db.channel[ch]); - lv_obj_clear_state(objects.settings_channel_panel, LV_STATE_DISABLED); - lv_obj_add_flag(objects.settings_channel_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_channel_button); - delete[] THIS->channel_scratch; - break; - } - case eWifi: { - char buf[30]; - const char *ssid = lv_textarea_get_text(objects.settings_wifi_ssid_textarea); - const char *psk = lv_textarea_get_text(objects.settings_wifi_password_textarea); - if (strlen(ssid) == 0 || strlen(psk) == 0) - return; - lv_snprintf(buf, sizeof(buf), _("WiFi: %s"), ssid[0] ? ssid : _("")); - lv_label_set_text(objects.basic_settings_wifi_label, buf); - if (strcmp(THIS->db.config.network.wifi_ssid, ssid) != 0 || strcmp(THIS->db.config.network.wifi_psk, psk) != 0) { - strcpy(THIS->db.config.network.wifi_ssid, ssid); - strcpy(THIS->db.config.network.wifi_psk, psk); - THIS->db.config.network.wifi_enabled = true; - THIS->controller->sendConfig(meshtastic_Config_NetworkConfig{THIS->db.config.network}, THIS->ownNode); - THIS->notifyReboot(true); - } - THIS->enablePanel(objects.home_panel); - lv_obj_add_flag(objects.settings_wifi_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_wifi_button); - break; + chats.clear(); + messages.clear(); + updateActiveChats(); + updateNodesFiltered(true); + controller->removeTextMessages(0, 0, 0); } - case eLanguage: { - uint32_t value = lv_dropdown_get_selected(objects.settings_language_dropdown); - meshtastic_Language lang = THIS->val2language(value); - if (lang != THIS->db.uiConfig.language) { - THIS->db.uiConfig.language = lang; - THIS->controller->storeUIConfig(THIS->db.uiConfig); - THIS->controller->requestReboot(3, THIS->ownNode); - THIS->notifyReboot(true); - } - lv_obj_add_flag(objects.settings_language_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_language_button); - break; - } - case eScreenTimeout: { - uint32_t value = lv_slider_get_value(objects.screen_timeout_slider); - if (value > 5) - value -= value % 5; - if (value != THIS->db.uiConfig.screen_timeout) { - THIS->setTimeout(value); - THIS->db.uiConfig.screen_timeout = value; - THIS->controller->storeUIConfig(THIS->db.uiConfig); - } - lv_obj_add_flag(objects.settings_screen_timeout_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_timeout_button); - break; - } - case eScreenLock: { - const char *pin = lv_textarea_get_text(objects.settings_screen_lock_password_textarea); - bool screenLock = lv_obj_has_state(objects.settings_screen_lock_switch, LV_STATE_CHECKED); - bool settingsLock = lv_obj_has_state(objects.settings_settings_lock_switch, LV_STATE_CHECKED); - if ((screenLock || settingsLock) && (atol(pin) == 0 || strlen(pin) != 6)) - return; // require pin != "000000" - if ((screenLock != THIS->db.uiConfig.screen_lock) || settingsLock != THIS->db.uiConfig.settings_lock || - atol(pin) != THIS->db.uiConfig.pin_code) { - THIS->db.uiConfig.screen_lock = screenLock; - THIS->db.uiConfig.settings_lock = settingsLock; - THIS->db.uiConfig.pin_code = atol(pin); - THIS->controller->storeUIConfig(THIS->db.uiConfig); - } + /** + * @brief User widget OK button handling + * + * @param e + */ + void TFTView_320x240::ui_event_ok(lv_event_t * e) + { + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED) { + switch (THIS->activeSettings) { + case eSetup: { + meshtastic_Config_LoRaConfig_RegionCode region = + (meshtastic_Config_LoRaConfig_RegionCode)(lv_dropdown_get_selected(objects.setup_region_dropdown) + 1); + + uint32_t numChannels = LoRaPresets::getNumChannels(region, THIS->db.config.lora.modem_preset); + // if (numChannels == 0) { + // // region not possible for selected preset, revert + // lv_dropdown_set_selected(objects.settings_region_dropdown, THIS->db.config.lora.region - 1); + // return; + // } + + if (region != THIS->db.config.lora.region) { + char buf1[10], buf2[30]; + lv_dropdown_get_selected_str(objects.setup_region_dropdown, buf1, sizeof(buf1)); + lv_snprintf(buf2, sizeof(buf2), _("Region: %s"), buf1); + lv_label_set_text(objects.basic_settings_region_label, buf2); + + meshtastic_Config_LoRaConfig &lora = THIS->db.config.lora; + uint32_t defaultSlot = + lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET ? lora.channel_num : 0; + if (defaultSlot == 0) { + defaultSlot = LoRaPresets::getDefaultSlot(region, THIS->db.config.lora.modem_preset, + THIS->db.channel[0].settings.name); + } + lora.region = region; + lora.channel_num = (defaultSlot <= numChannels ? defaultSlot : 1); + THIS->controller->sendConfig(meshtastic_Config_LoRaConfig{lora}, THIS->ownNode); + } - char buf[40]; - lv_snprintf(buf, 40, _("Lock: %s/%s"), screenLock ? _("on") : _("off"), settingsLock ? _("on") : _("off")); - lv_label_set_text(objects.basic_settings_screen_lock_label, buf); - lv_obj_add_flag(objects.settings_screen_lock_panel, LV_OBJ_FLAG_HIDDEN); + char buf[30]; + const char *userShort = lv_textarea_get_text(objects.setup_user_short_textarea); + const char *userLong = lv_textarea_get_text(objects.setup_user_long_textarea); + if (strcmp(userShort, THIS->db.short_name) || strcmp(userLong, THIS->db.long_name)) { + lv_snprintf(buf, sizeof(buf), _("User name: %s"), userShort); + lv_label_set_text(objects.basic_settings_user_label, buf); + lv_label_set_text(objects.user_name_short_label, userShort); + lv_label_set_text(objects.user_name_label, userLong); + strcpy(THIS->db.short_name, userShort); + strcpy(THIS->db.long_name, userLong); + meshtastic_User user{}; // TODO: don't overwrite is_licensed + strcpy(user.short_name, userShort); + strcpy(user.long_name, userLong); + THIS->controller->sendConfig(user, THIS->ownNode); + } + THIS->notifyReboot(true); - break; - } - case eScreenBrightness: { - int32_t value = lv_slider_get_value(objects.brightness_slider) * 255 / 100; - if (value != THIS->db.uiConfig.screen_brightness) { - THIS->setBrightness(value); - THIS->db.uiConfig.screen_brightness = value; - THIS->controller->storeUIConfig(THIS->db.uiConfig); - } - lv_obj_add_flag(objects.settings_brightness_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_brightness_button); - break; - } - case eTheme: { - uint32_t value = lv_dropdown_get_selected(objects.settings_theme_dropdown); - if (value != THIS->db.uiConfig.theme) { - THIS->setTheme(value); - THIS->db.uiConfig.theme = meshtastic_Theme(value); - THIS->controller->storeUIConfig(THIS->db.uiConfig); - lv_obj_set_style_bg_img_recolor(objects.settings_button, colorMesh, LV_PART_MAIN | LV_STATE_DEFAULT); - } + lv_obj_add_flag(objects.initial_setup_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.home_button); + break; + } + case eUsername: { + char buf[30]; + const char *userShort = lv_textarea_get_text(objects.settings_user_short_textarea); + const char *userLong = lv_textarea_get_text(objects.settings_user_long_textarea); + if (strcmp(userShort, THIS->db.short_name) || strcmp(userLong, THIS->db.long_name)) { + lv_snprintf(buf, sizeof(buf), _("User name: %s"), userShort); + lv_label_set_text(objects.basic_settings_user_label, buf); + lv_label_set_text(objects.user_name_short_label, userShort); + lv_label_set_text(objects.user_name_label, userLong); + strcpy(THIS->db.short_name, userShort); + strcpy(THIS->db.long_name, userLong); + meshtastic_User user{}; // TODO: don't overwrite is_licensed + strcpy(user.short_name, userShort); + strcpy(user.long_name, userLong); + THIS->controller->sendConfig(user, THIS->ownNode); + THIS->notifyReboot(true); + } + lv_obj_add_flag(objects.settings_username_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_user_button); + break; + } + case eDeviceRole: { + meshtastic_Config_DeviceConfig &device = THIS->db.config.device; + meshtastic_Config_DeviceConfig_Role role = + THIS->val2role(lv_dropdown_get_selected(objects.settings_device_role_dropdown)); + + if (role != device.role) { + char buf1[30], buf2[40]; + lv_dropdown_get_selected_str(objects.settings_device_role_dropdown, buf1, sizeof(buf1)); + lv_snprintf(buf2, sizeof(buf2), _("Device Role: %s"), buf1); + lv_label_set_text(objects.basic_settings_role_label, buf2); + + device.role = role; + THIS->controller->sendConfig(meshtastic_Config_DeviceConfig{device}, THIS->ownNode); + THIS->notifyReboot(true); + } + lv_obj_add_flag(objects.settings_device_role_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_role_button); + break; + } + case eRegion: { + meshtastic_Config_LoRaConfig_RegionCode region = + (meshtastic_Config_LoRaConfig_RegionCode)(lv_dropdown_get_selected(objects.settings_region_dropdown) + 1); + + uint32_t numChannels = LoRaPresets::getNumChannels(region, THIS->db.config.lora.modem_preset); + if (numChannels == 0) { + // region not possible for selected preset, revert + lv_dropdown_set_selected(objects.settings_region_dropdown, THIS->db.config.lora.region - 1); + return; + } - lv_obj_add_flag(objects.settings_theme_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_theme_button); - lv_obj_invalidate(objects.main_screen); - break; - } - case eInputControl: { - char new_val_kbd[10], new_val_ptr[10]; - lv_dropdown_get_selected_str(objects.settings_keyboard_input_dropdown, new_val_kbd, sizeof(new_val_kbd)); - lv_dropdown_get_selected_str(objects.settings_mouse_input_dropdown, new_val_ptr, sizeof(new_val_ptr)); + if (region != THIS->db.config.lora.region) { + char buf1[10], buf2[30]; + lv_dropdown_get_selected_str(objects.settings_region_dropdown, buf1, sizeof(buf1)); + lv_snprintf(buf2, sizeof(buf2), _("Region: %s"), buf1); + lv_label_set_text(objects.basic_settings_region_label, buf2); + + meshtastic_Config_LoRaConfig &lora = THIS->db.config.lora; + uint32_t defaultSlot = + lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET ? lora.channel_num : 0; + if (defaultSlot == 0) { + defaultSlot = LoRaPresets::getDefaultSlot(region, THIS->db.config.lora.modem_preset, + THIS->db.channel[0].settings.name); + } + lora.region = region; + lora.channel_num = (defaultSlot <= numChannels ? defaultSlot : 1); + THIS->controller->sendConfig(meshtastic_Config_LoRaConfig{lora}, THIS->ownNode); + THIS->notifyReboot(true); + } + lv_obj_add_flag(objects.settings_region_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_region_button); + break; + } + case eModemPreset: { + meshtastic_Config_LoRaConfig &lora = THIS->db.config.lora; + meshtastic_Config_LoRaConfig_ModemPreset preset = + THIS->val2preset(lv_dropdown_get_selected(objects.settings_modem_preset_dropdown)); + uint16_t channelNum = lv_slider_get_value(objects.frequency_slot_slider); + if (preset != lora.modem_preset || lora.channel_num != channelNum) { + char buf1[16], buf2[32]; + lv_dropdown_get_selected_str(objects.settings_modem_preset_dropdown, buf1, sizeof(buf1)); + lv_snprintf(buf2, sizeof(buf2), _("Modem Preset: %s"), buf1); + lv_label_set_text(objects.basic_settings_modem_preset_label, buf2); + + lora.use_preset = true; + lora.modem_preset = preset; + lora.channel_num = channelNum; + THIS->controller->sendConfig(meshtastic_Config_LoRaConfig{lora}, THIS->ownNode); + THIS->notifyReboot(true); + } + lv_obj_add_flag(objects.settings_modem_preset_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_modem_preset_button); + break; + } + case eChannel: { + for (int i = 0; i < c_max_channels; i++) { + // check if channel changed, then update and send to radio + if (memcmp(&THIS->db.channel[i], &THIS->channel_scratch[i], sizeof(THIS->channel_scratch[i])) != 0) { + THIS->channel_scratch[i].has_settings = true; + THIS->updateChannelConfig(THIS->channel_scratch[i]); + THIS->controller->sendConfig(THIS->channel_scratch[i], THIS->ownNode); + } + } - bool error = false; - if (strcmp(THIS->old_val1_scratch, new_val_kbd) != 0) { - if (strcmp(THIS->old_val1_scratch, _("none")) != 0) { - THIS->inputdriver->releaseKeyboardDevice(); + int8_t ch = (signed long)THIS->ch_label[0]->user_data; + THIS->setChannelName(THIS->db.channel[ch]); + lv_obj_clear_state(objects.settings_channel_panel, LV_STATE_DISABLED); + lv_obj_add_flag(objects.settings_channel_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_channel_button); + delete[] THIS->channel_scratch; + break; } - if (strcmp(new_val_kbd, _("none")) != 0) { - error &= THIS->inputdriver->useKeyboardDevice(new_val_kbd); + case eWifi: { + char buf[30]; + const char *ssid = lv_textarea_get_text(objects.settings_wifi_ssid_textarea); + const char *psk = lv_textarea_get_text(objects.settings_wifi_password_textarea); + if (strlen(ssid) == 0 || strlen(psk) == 0) + return; + lv_snprintf(buf, sizeof(buf), _("WiFi: %s"), ssid[0] ? ssid : _("")); + lv_label_set_text(objects.basic_settings_wifi_label, buf); + if (strcmp(THIS->db.config.network.wifi_ssid, ssid) != 0 || + strcmp(THIS->db.config.network.wifi_psk, psk) != 0) { + strcpy(THIS->db.config.network.wifi_ssid, ssid); + strcpy(THIS->db.config.network.wifi_psk, psk); + THIS->db.config.network.wifi_enabled = true; + THIS->controller->sendConfig(meshtastic_Config_NetworkConfig{THIS->db.config.network}, THIS->ownNode); + THIS->notifyReboot(true); + } + THIS->enablePanel(objects.home_panel); + lv_obj_add_flag(objects.settings_wifi_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_wifi_button); + break; } - } - if (strcmp(THIS->old_val2_scratch, new_val_ptr) != 0) { - if (strcmp(THIS->old_val2_scratch, _("none")) != 0) { - THIS->inputdriver->releasePointerDevice(); + case eLanguage: { + uint32_t value = lv_dropdown_get_selected(objects.settings_language_dropdown); + meshtastic_Language lang = THIS->val2language(value); + if (lang != THIS->db.uiConfig.language) { + THIS->db.uiConfig.language = lang; + THIS->controller->storeUIConfig(THIS->db.uiConfig); + THIS->controller->requestReboot(3, THIS->ownNode); + THIS->notifyReboot(true); + } + + lv_obj_add_flag(objects.settings_language_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_language_button); + break; } - if (strcmp(new_val_ptr, _("none")) != 0) { - error &= THIS->inputdriver->usePointerDevice(new_val_ptr); + case eScreenTimeout: { + uint32_t value = lv_slider_get_value(objects.screen_timeout_slider); + if (value > 5) + value -= value % 5; + if (value != THIS->db.uiConfig.screen_timeout) { + THIS->setTimeout(value); + THIS->db.uiConfig.screen_timeout = value; + THIS->controller->storeUIConfig(THIS->db.uiConfig); + } + lv_obj_add_flag(objects.settings_screen_timeout_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_timeout_button); + break; } - } + case eScreenLock: { + const char *pin = lv_textarea_get_text(objects.settings_screen_lock_password_textarea); + bool screenLock = lv_obj_has_state(objects.settings_screen_lock_switch, LV_STATE_CHECKED); + bool settingsLock = lv_obj_has_state(objects.settings_settings_lock_switch, LV_STATE_CHECKED); + if ((screenLock || settingsLock) && (atol(pin) == 0 || strlen(pin) != 6)) + return; // require pin != "000000" + if ((screenLock != THIS->db.uiConfig.screen_lock) || settingsLock != THIS->db.uiConfig.settings_lock || + atol(pin) != THIS->db.uiConfig.pin_code) { + THIS->db.uiConfig.screen_lock = screenLock; + THIS->db.uiConfig.settings_lock = settingsLock; + THIS->db.uiConfig.pin_code = atol(pin); + THIS->controller->storeUIConfig(THIS->db.uiConfig); + } - THIS->setInputButtonLabel(); + char buf[40]; + lv_snprintf(buf, 40, _("Lock: %s/%s"), screenLock ? _("on") : _("off"), settingsLock ? _("on") : _("off")); + lv_label_set_text(objects.basic_settings_screen_lock_label, buf); + lv_obj_add_flag(objects.settings_screen_lock_panel, LV_OBJ_FLAG_HIDDEN); - if (error) { - ILOG_WARN("failed to use %s/%s", new_val_kbd, new_val_ptr); - return; - } + break; + } + case eScreenBrightness: { + int32_t value = lv_slider_get_value(objects.brightness_slider) * 255 / 100; + if (value != THIS->db.uiConfig.screen_brightness) { + THIS->setBrightness(value); + THIS->db.uiConfig.screen_brightness = value; + THIS->controller->storeUIConfig(THIS->db.uiConfig); + } + lv_obj_add_flag(objects.settings_brightness_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_brightness_button); + break; + } + case eTheme: { + uint32_t value = lv_dropdown_get_selected(objects.settings_theme_dropdown); + if (value != THIS->db.uiConfig.theme) { + THIS->setTheme(value); + THIS->db.uiConfig.theme = meshtastic_Theme(value); + THIS->controller->storeUIConfig(THIS->db.uiConfig); + lv_obj_set_style_bg_img_recolor(objects.settings_button, colorMesh, LV_PART_MAIN | LV_STATE_DEFAULT); + } - std::string current_kbd = THIS->inputdriver->getCurrentKeyboardDevice(); - std::string current_ptr = THIS->inputdriver->getCurrentPointerDevice(); - if (strcmp(current_kbd.c_str(), _("none")) == 0 && strcmp(current_ptr.c_str(), _("none")) == 0 && THIS->input_group) { - lv_group_delete(THIS->input_group); - THIS->input_group = nullptr; - } else if (strcmp(THIS->old_val1_scratch, current_kbd.c_str()) != 0 || - strcmp(THIS->old_val2_scratch, current_ptr.c_str()) != 0) { - THIS->setInputGroup(); - } + lv_obj_add_flag(objects.settings_theme_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_theme_button); + lv_obj_invalidate(objects.main_screen); + break; + } + case eInputControl: { + char new_val_kbd[10], new_val_ptr[10]; + lv_dropdown_get_selected_str(objects.settings_keyboard_input_dropdown, new_val_kbd, sizeof(new_val_kbd)); + lv_dropdown_get_selected_str(objects.settings_mouse_input_dropdown, new_val_ptr, sizeof(new_val_ptr)); + + bool error = false; + if (strcmp(THIS->old_val1_scratch, new_val_kbd) != 0) { + if (strcmp(THIS->old_val1_scratch, _("none")) != 0) { + THIS->inputdriver->releaseKeyboardDevice(); + } + if (strcmp(new_val_kbd, _("none")) != 0) { + error &= THIS->inputdriver->useKeyboardDevice(new_val_kbd); + } + } + if (strcmp(THIS->old_val2_scratch, new_val_ptr) != 0) { + if (strcmp(THIS->old_val2_scratch, _("none")) != 0) { + THIS->inputdriver->releasePointerDevice(); + } + if (strcmp(new_val_ptr, _("none")) != 0) { + error &= THIS->inputdriver->usePointerDevice(new_val_ptr); + } + } - lv_obj_add_flag(objects.settings_input_control_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_input_button); - break; - } - case eAlertBuzzer: { - meshtastic_ModuleConfig_ExternalNotificationConfig &config = THIS->db.module_config.external_notification; - int tone = lv_dropdown_get_selected(objects.settings_ringtone_dropdown) + 1; - - bool silent = false; - bool alert_message = lv_obj_has_state(objects.settings_alert_buzzer_switch, LV_STATE_CHECKED); - if ((!config.enabled || !config.alert_message_buzzer) && alert_message) { - if (!config.enabled || !config.alert_message_buzzer || !config.use_pwm || !config.use_i2s_as_buzzer) { - config.enabled = true; - config.alert_message_buzzer = true; - config.use_pwm = true; - config.nag_timeout = 0; + THIS->setInputButtonLabel(); + + if (error) { + ILOG_WARN("failed to use %s/%s", new_val_kbd, new_val_ptr); + return; + } + + std::string current_kbd = THIS->inputdriver->getCurrentKeyboardDevice(); + std::string current_ptr = THIS->inputdriver->getCurrentPointerDevice(); + if (strcmp(current_kbd.c_str(), _("none")) == 0 && strcmp(current_ptr.c_str(), _("none")) == 0 && + THIS->input_group) { + lv_group_delete(THIS->input_group); + THIS->input_group = nullptr; + } else if (strcmp(THIS->old_val1_scratch, current_kbd.c_str()) != 0 || + strcmp(THIS->old_val2_scratch, current_ptr.c_str()) != 0) { + THIS->setInputGroup(); + } + + lv_obj_add_flag(objects.settings_input_control_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_input_button); + break; + } + case eAlertBuzzer: { + meshtastic_ModuleConfig_ExternalNotificationConfig &config = THIS->db.module_config.external_notification; + int tone = lv_dropdown_get_selected(objects.settings_ringtone_dropdown) + 1; + + bool silent = false; + bool alert_message = lv_obj_has_state(objects.settings_alert_buzzer_switch, LV_STATE_CHECKED); + if ((!config.enabled || !config.alert_message_buzzer) && alert_message) { + if (!config.enabled || !config.alert_message_buzzer || !config.use_pwm || !config.use_i2s_as_buzzer) { + config.enabled = true; + config.alert_message_buzzer = true; + config.use_pwm = true; + config.nag_timeout = 0; #ifdef USE_I2S_BUZZER - config.use_i2s_as_buzzer = true; - config.use_pwm = false; + config.use_i2s_as_buzzer = true; + config.use_pwm = false; #endif - } - THIS->notifyReboot(true); - THIS->controller->sendConfig(meshtastic_ModuleConfig_ExternalNotificationConfig{config}, THIS->ownNode); - } else if (config.alert_message_buzzer && !alert_message) { - silent = true; - } + } + THIS->notifyReboot(true); + THIS->controller->sendConfig(meshtastic_ModuleConfig_ExternalNotificationConfig{config}, THIS->ownNode); + } else if (config.alert_message_buzzer && !alert_message) { + silent = true; + } - THIS->controller->sendConfig(ringtone[silent ? 0 : tone].rtttl, THIS->ownNode); - THIS->db.uiConfig.ring_tone_id = tone; - THIS->db.silent = silent; - THIS->db.uiConfig.alert_enabled = !silent; - THIS->setBellText(THIS->db.uiConfig.alert_enabled, !silent); - THIS->controller->storeUIConfig(THIS->db.uiConfig); + THIS->controller->sendConfig(ringtone[silent ? 0 : tone].rtttl, THIS->ownNode); + THIS->db.uiConfig.ring_tone_id = tone; + THIS->db.silent = silent; + THIS->db.uiConfig.alert_enabled = !silent; + THIS->setBellText(THIS->db.uiConfig.alert_enabled, !silent); + THIS->controller->storeUIConfig(THIS->db.uiConfig); - lv_obj_add_flag(objects.settings_alert_buzzer_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_alert_button); - break; - } - case eBackupRestore: { - uint32_t option = lv_dropdown_get_selected(objects.settings_backup_restore_dropdown); - if (lv_obj_has_state(objects.settings_backup_checkbox, LV_STATE_CHECKED)) { - THIS->backup(option); - } else if (lv_obj_has_state(objects.settings_restore_checkbox, LV_STATE_CHECKED)) { - THIS->restore(option); - } - lv_obj_add_flag(objects.settings_backup_restore_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_backup_restore_button); - break; - } - case eReset: { - uint32_t option = lv_dropdown_get_selected(objects.settings_reset_dropdown); - if (option == 2) { - THIS->clearChatHistory(); - } else { - THIS->notifyReboot(true); - THIS->controller->requestReset(option, THIS->ownNode); - } - lv_obj_add_flag(objects.settings_reset_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_reset_button); - break; - } - case eDisplayMode: { - meshtastic_Config_DisplayConfig &display = THIS->db.config.display; - meshtastic_Config_BluetoothConfig &bluetooth = THIS->db.config.bluetooth; - display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT; - bluetooth.enabled = true; - THIS->controller->sendConfig(meshtastic_Config_DisplayConfig{display}, THIS->ownNode); - THIS->controller->sendConfig(meshtastic_Config_BluetoothConfig{bluetooth}, THIS->ownNode); - THIS->controller->requestReboot(5, THIS->ownNode); - lv_screen_load_anim(objects.blank_screen, LV_SCR_LOAD_ANIM_FADE_OUT, 4000, 1000, false); - lv_obj_add_flag(objects.reboot_panel, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(objects.settings_reboot_panel, LV_OBJ_FLAG_HIDDEN); - break; - } - case eModifyChannel: { - meshtastic_ChannelSettings_psk_t psk = {}; - const char *name = lv_textarea_get_text(objects.settings_modify_channel_name_textarea); - const char *base64 = lv_textarea_get_text(objects.settings_modify_channel_psk_textarea); - uint8_t btn_id = (unsigned long)objects.settings_modify_channel_name_textarea->user_data; - int8_t ch = (signed long)THIS->ch_label[btn_id]->user_data; - - if (strlen(base64) == 0 && strlen(name) == 0) { - // delete channel - THIS->channel_scratch[ch].role = meshtastic_Channel_Role_DISABLED; - THIS->channel_scratch[ch].settings.psk.size = 0; - memset(THIS->channel_scratch[ch].settings.name, 0, sizeof(THIS->channel_scratch[ch].settings.name)); - memset(THIS->channel_scratch[ch].settings.psk.bytes, 0, sizeof(THIS->channel_scratch[ch].settings.psk.bytes)); - THIS->channel_scratch[ch].has_settings = false; - lv_label_set_text(THIS->ch_label[btn_id], _("")); - THIS->activeSettings = eChannel; - } else { - int paddings = (4 - strlen(base64) % 4) % 4; - while (paddings-- > 0) { - lv_textarea_add_text(objects.settings_modify_channel_psk_textarea, "="); - } - - if (THIS->base64ToPsk(lv_textarea_get_text(objects.settings_modify_channel_psk_textarea), psk.bytes, psk.size)) { - if (strlen(name) || psk.size) { - // TODO: fill temp storage -> user data - lv_label_set_text(THIS->ch_label[btn_id], name); - strcpy(THIS->channel_scratch[ch].settings.name, name); - memcpy(THIS->channel_scratch[ch].settings.psk.bytes, psk.bytes, 32); - THIS->channel_scratch[ch].settings.psk.size = psk.size; + lv_obj_add_flag(objects.settings_alert_buzzer_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_alert_button); + break; + } + case eBackupRestore: { + uint32_t option = lv_dropdown_get_selected(objects.settings_backup_restore_dropdown); + if (lv_obj_has_state(objects.settings_backup_checkbox, LV_STATE_CHECKED)) { + THIS->backup(option); + } else if (lv_obj_has_state(objects.settings_restore_checkbox, LV_STATE_CHECKED)) { + THIS->restore(option); + } + lv_obj_add_flag(objects.settings_backup_restore_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_backup_restore_button); + break; + } + case eReset: { + uint32_t option = lv_dropdown_get_selected(objects.settings_reset_dropdown); + if (option == 2) { + THIS->clearChatHistory(); + } else { + THIS->notifyReboot(true); + THIS->controller->requestReset(option, THIS->ownNode); + } + lv_obj_add_flag(objects.settings_reset_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_reset_button); + break; + } + case eDisplayMode: { + meshtastic_Config_DisplayConfig &display = THIS->db.config.display; + meshtastic_Config_BluetoothConfig &bluetooth = THIS->db.config.bluetooth; + display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT; + bluetooth.enabled = true; + THIS->controller->sendConfig(meshtastic_Config_DisplayConfig{display}, THIS->ownNode); + THIS->controller->sendConfig(meshtastic_Config_BluetoothConfig{bluetooth}, THIS->ownNode); + THIS->controller->requestReboot(5, THIS->ownNode); + lv_screen_load_anim(objects.blank_screen, LV_SCR_LOAD_ANIM_FADE_OUT, 4000, 1000, false); + lv_obj_add_flag(objects.reboot_panel, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(objects.settings_reboot_panel, LV_OBJ_FLAG_HIDDEN); + break; + } + case eModifyChannel: { + meshtastic_ChannelSettings_psk_t psk = {}; + const char *name = lv_textarea_get_text(objects.settings_modify_channel_name_textarea); + const char *base64 = lv_textarea_get_text(objects.settings_modify_channel_psk_textarea); + uint8_t btn_id = (unsigned long)objects.settings_modify_channel_name_textarea->user_data; + int8_t ch = (signed long)THIS->ch_label[btn_id]->user_data; + + if (strlen(base64) == 0 && strlen(name) == 0) { + // delete channel + THIS->channel_scratch[ch].role = meshtastic_Channel_Role_DISABLED; + THIS->channel_scratch[ch].settings.psk.size = 0; + memset(THIS->channel_scratch[ch].settings.name, 0, sizeof(THIS->channel_scratch[ch].settings.name)); + memset(THIS->channel_scratch[ch].settings.psk.bytes, 0, + sizeof(THIS->channel_scratch[ch].settings.psk.bytes)); + THIS->channel_scratch[ch].has_settings = false; + lv_label_set_text(THIS->ch_label[btn_id], _("")); THIS->activeSettings = eChannel; + } else { + int paddings = (4 - strlen(base64) % 4) % 4; + while (paddings-- > 0) { + lv_textarea_add_text(objects.settings_modify_channel_psk_textarea, "="); + } + + if (THIS->base64ToPsk(lv_textarea_get_text(objects.settings_modify_channel_psk_textarea), psk.bytes, + psk.size)) { + if (strlen(name) || psk.size) { + // TODO: fill temp storage -> user data + lv_label_set_text(THIS->ch_label[btn_id], name); + strcpy(THIS->channel_scratch[ch].settings.name, name); + memcpy(THIS->channel_scratch[ch].settings.psk.bytes, psk.bytes, 32); + THIS->channel_scratch[ch].settings.psk.size = psk.size; + THIS->activeSettings = eChannel; + } + } + THIS->channel_scratch[ch].role = + (ch == 0) ? meshtastic_Channel_Role_PRIMARY : meshtastic_Channel_Role_SECONDARY; + } + if (THIS->activeSettings == eChannel) { + lv_obj_add_flag(objects.settings_modify_channel_panel, LV_OBJ_FLAG_HIDDEN); + THIS->enablePanel(objects.settings_channel_panel); + lv_group_focus_obj(objects.settings_channel0_button); } + return; } - THIS->channel_scratch[ch].role = (ch == 0) ? meshtastic_Channel_Role_PRIMARY : meshtastic_Channel_Role_SECONDARY; - } - if (THIS->activeSettings == eChannel) { - lv_obj_add_flag(objects.settings_modify_channel_panel, LV_OBJ_FLAG_HIDDEN); - THIS->enablePanel(objects.settings_channel_panel); - lv_group_focus_obj(objects.settings_channel0_button); + default: + ILOG_ERROR("Unhandled ok event"); + break; + } + THIS->enablePanel(objects.controller_panel); + THIS->enablePanel(objects.tab_page_basic_settings); + THIS->activeSettings = eNone; } - return; - } - default: - ILOG_ERROR("Unhandled ok event"); - break; } - THIS->enablePanel(objects.controller_panel); - THIS->enablePanel(objects.tab_page_basic_settings); - THIS->activeSettings = eNone; - } -} -/** - * @brief Cancel button user widget handling - * - * @param e - */ -void TFTView_320x240::ui_event_cancel(lv_event_t *e) -{ - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED) { - switch (THIS->activeSettings) { - case TFTView_320x240::eSetup: { - THIS->ui_set_active(objects.home_button, objects.home_panel, objects.top_panel); - // lv_obj_add_flag(objects.initial_setup_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.home_button); - break; - } - case TFTView_320x240::eUsername: { - lv_obj_add_flag(objects.settings_username_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_user_button); - break; - } - case TFTView_320x240::eDeviceRole: { - lv_obj_add_flag(objects.settings_device_role_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_role_button); - break; - } - case TFTView_320x240::eRegion: { - lv_obj_add_flag(objects.settings_region_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_region_button); - break; - } - case TFTView_320x240::eModemPreset: { - lv_obj_add_flag(objects.settings_modem_preset_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_modem_preset_button); - break; - } - case TFTView_320x240::eChannel: { - delete[] THIS->channel_scratch; - lv_obj_add_flag(objects.settings_channel_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_channel_button); - break; - } - case TFTView_320x240::eWifi: { - lv_obj_add_flag(objects.settings_wifi_panel, LV_OBJ_FLAG_HIDDEN); - THIS->enablePanel(objects.home_panel); - lv_group_focus_obj(objects.home_wlan_button); - break; - } - case TFTView_320x240::eLanguage: { - lv_obj_add_flag(objects.settings_language_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_language_button); - break; - } - case TFTView_320x240::eScreenTimeout: { - lv_obj_add_flag(objects.settings_screen_timeout_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_timeout_button); - break; - } - case eScreenLock: { - lv_obj_add_flag(objects.settings_screen_lock_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_screen_lock_button); - break; - } - case TFTView_320x240::eScreenBrightness: { - lv_obj_add_flag(objects.settings_brightness_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_brightness_button); - // revert to old brightness value - uint32_t old_brightness = THIS->db.uiConfig.screen_brightness; - THIS->displaydriver->setBrightness((uint8_t)old_brightness); - break; - } - case TFTView_320x240::eTheme: { - lv_obj_add_flag(objects.settings_theme_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_theme_button); - break; - } - case TFTView_320x240::eInputControl: { - lv_obj_add_flag(objects.settings_input_control_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_input_button); - break; - } - case TFTView_320x240::eAlertBuzzer: { - lv_obj_add_flag(objects.settings_alert_buzzer_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_alert_button); - break; - } - case TFTView_320x240::eBackupRestore: { - lv_obj_add_flag(objects.settings_backup_restore_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_backup_restore_button); - break; - } - case TFTView_320x240::eReset: { - lv_obj_add_flag(objects.settings_reset_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_reset_button); - break; - } - case TFTView_320x240::eDisplayMode: { - lv_obj_add_flag(objects.settings_reboot_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_reset_button); - break; + /** + * @brief Cancel button user widget handling + * + * @param e + */ + void TFTView_320x240::ui_event_cancel(lv_event_t * e) + { + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED) { + switch (THIS->activeSettings) { + case TFTView_320x240::eSetup: { + THIS->ui_set_active(objects.home_button, objects.home_panel, objects.top_panel); + // lv_obj_add_flag(objects.initial_setup_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.home_button); + break; + } + case TFTView_320x240::eUsername: { + lv_obj_add_flag(objects.settings_username_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_user_button); + break; + } + case TFTView_320x240::eDeviceRole: { + lv_obj_add_flag(objects.settings_device_role_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_role_button); + break; + } + case TFTView_320x240::eRegion: { + lv_obj_add_flag(objects.settings_region_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_region_button); + break; + } + case TFTView_320x240::eModemPreset: { + lv_obj_add_flag(objects.settings_modem_preset_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_modem_preset_button); + break; + } + case TFTView_320x240::eChannel: { + delete[] THIS->channel_scratch; + lv_obj_add_flag(objects.settings_channel_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_channel_button); + break; + } + case TFTView_320x240::eWifi: { + lv_obj_add_flag(objects.settings_wifi_panel, LV_OBJ_FLAG_HIDDEN); + THIS->enablePanel(objects.home_panel); + lv_group_focus_obj(objects.home_wlan_button); + break; + } + case TFTView_320x240::eLanguage: { + lv_obj_add_flag(objects.settings_language_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_language_button); + break; + } + case TFTView_320x240::eScreenTimeout: { + lv_obj_add_flag(objects.settings_screen_timeout_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_timeout_button); + break; + } + case eScreenLock: { + lv_obj_add_flag(objects.settings_screen_lock_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_screen_lock_button); + break; + } + case TFTView_320x240::eScreenBrightness: { + lv_obj_add_flag(objects.settings_brightness_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_brightness_button); + // revert to old brightness value + uint32_t old_brightness = THIS->db.uiConfig.screen_brightness; + THIS->displaydriver->setBrightness((uint8_t)old_brightness); + break; + } + case TFTView_320x240::eTheme: { + lv_obj_add_flag(objects.settings_theme_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_theme_button); + break; + } + case TFTView_320x240::eInputControl: { + lv_obj_add_flag(objects.settings_input_control_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_input_button); + break; + } + case TFTView_320x240::eAlertBuzzer: { + lv_obj_add_flag(objects.settings_alert_buzzer_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_alert_button); + break; + } + case TFTView_320x240::eBackupRestore: { + lv_obj_add_flag(objects.settings_backup_restore_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_backup_restore_button); + break; + } + case TFTView_320x240::eReset: { + lv_obj_add_flag(objects.settings_reset_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_reset_button); + break; + } + case TFTView_320x240::eDisplayMode: { + lv_obj_add_flag(objects.settings_reboot_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_reset_button); + break; + } + case TFTView_320x240::eModifyChannel: { + lv_obj_add_flag(objects.settings_modify_channel_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.settings_channel0_button); + THIS->enablePanel(objects.settings_channel_panel); + THIS->activeSettings = eChannel; + return; + } + default: + ILOG_ERROR("Unhandled cancel event"); + break; + } + + THIS->enablePanel(objects.controller_panel); + THIS->enablePanel(objects.tab_page_basic_settings); + THIS->activeSettings = eNone; + } } - case TFTView_320x240::eModifyChannel: { - lv_obj_add_flag(objects.settings_modify_channel_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.settings_channel0_button); - THIS->enablePanel(objects.settings_channel_panel); - THIS->activeSettings = eChannel; - return; + + // end button event handlers + + void TFTView_320x240::ui_event_screen_timeout_slider(lv_event_t * e) + { + lv_obj_t *slider = lv_event_get_target_obj(e); + char buf[20]; + uint32_t value = lv_slider_get_value(slider); + if (value > 5) + value -= value % 5; + if (value == 0) + lv_snprintf(buf, sizeof(buf), _("Timeout: off")); + else + lv_snprintf(buf, sizeof(buf), _("Timeout: %ds"), value); + lv_label_set_text(objects.settings_screen_timeout_label, buf); } - default: - ILOG_ERROR("Unhandled cancel event"); - break; + + void TFTView_320x240::ui_event_brightness_slider(lv_event_t * e) + { + lv_obj_t *slider = lv_event_get_target_obj(e); + char buf[20]; + lv_snprintf(buf, sizeof(buf), _("Brightness: %d%%"), (int)lv_slider_get_value(slider)); + lv_label_set_text(objects.settings_brightness_label, buf); + THIS->displaydriver->setBrightness((uint8_t)(lv_slider_get_value(slider) * 255 / 100)); } - THIS->enablePanel(objects.controller_panel); - THIS->enablePanel(objects.tab_page_basic_settings); - THIS->activeSettings = eNone; - } -} + void TFTView_320x240::ui_event_frequency_slot_slider(lv_event_t * e) + { + lv_obj_t *slider = lv_event_get_target_obj(e); + char buf[40]; + uint32_t channel = (uint32_t)lv_slider_get_value(slider); + sprintf(buf, _("FrequencySlot: %d (%g MHz)"), channel, + LoRaPresets::getRadioFreq(THIS->db.config.lora.region, + THIS->val2preset(lv_dropdown_get_selected(objects.settings_modem_preset_dropdown)), + channel)); + lv_label_set_text(objects.frequency_slot_label, buf); + } -// end button event handlers + void TFTView_320x240::ui_event_modem_preset_dropdown(lv_event_t * e) + { + lv_obj_t *dropdown = lv_event_get_target_obj(e); + meshtastic_Config_LoRaConfig_ModemPreset preset = + (meshtastic_Config_LoRaConfig_ModemPreset)lv_dropdown_get_selected(dropdown); + uint32_t numChannels = LoRaPresets::getNumChannels(THIS->db.config.lora.region, preset); + if (numChannels == 0) { + // preset not possible for this region, revert + lv_dropdown_set_selected(dropdown, THIS->db.config.lora.modem_preset); + numChannels = LoRaPresets::getNumChannels(THIS->db.config.lora.region, THIS->db.config.lora.modem_preset); + return; + } -void TFTView_320x240::ui_event_screen_timeout_slider(lv_event_t *e) -{ - lv_obj_t *slider = lv_event_get_target_obj(e); - char buf[20]; - uint32_t value = lv_slider_get_value(slider); - if (value > 5) - value -= value % 5; - if (value == 0) - lv_snprintf(buf, sizeof(buf), _("Timeout: off")); - else - lv_snprintf(buf, sizeof(buf), _("Timeout: %ds"), value); - lv_label_set_text(objects.settings_screen_timeout_label, buf); -} + uint32_t channel = + LoRaPresets::getDefaultSlot(THIS->db.config.lora.region, preset, THIS->db.channel[0].settings.name); + if (channel > numChannels) + channel = 1; + lv_slider_set_range(objects.frequency_slot_slider, 1, numChannels); + lv_slider_set_value(objects.frequency_slot_slider, channel, LV_ANIM_ON); -void TFTView_320x240::ui_event_brightness_slider(lv_event_t *e) -{ - lv_obj_t *slider = lv_event_get_target_obj(e); - char buf[20]; - lv_snprintf(buf, sizeof(buf), _("Brightness: %d%%"), (int)lv_slider_get_value(slider)); - lv_label_set_text(objects.settings_brightness_label, buf); - THIS->displaydriver->setBrightness((uint8_t)(lv_slider_get_value(slider) * 255 / 100)); -} + char buf[40]; + sprintf(buf, _("FrequencySlot: %d (%g MHz)"), channel, + LoRaPresets::getRadioFreq(THIS->db.config.lora.region, preset, channel)); + lv_label_set_text(objects.frequency_slot_label, buf); + } -void TFTView_320x240::ui_event_frequency_slot_slider(lv_event_t *e) -{ - lv_obj_t *slider = lv_event_get_target_obj(e); - char buf[40]; - uint32_t channel = (uint32_t)lv_slider_get_value(slider); - sprintf(buf, _("FrequencySlot: %d (%g MHz)"), channel, - LoRaPresets::getRadioFreq(THIS->db.config.lora.region, - THIS->val2preset(lv_dropdown_get_selected(objects.settings_modem_preset_dropdown)), - channel)); - lv_label_set_text(objects.frequency_slot_label, buf); -} + void TFTView_320x240::ui_event_setup_region_dropdown(lv_event_t * e) {} -void TFTView_320x240::ui_event_modem_preset_dropdown(lv_event_t *e) -{ - lv_obj_t *dropdown = lv_event_get_target_obj(e); - meshtastic_Config_LoRaConfig_ModemPreset preset = - (meshtastic_Config_LoRaConfig_ModemPreset)lv_dropdown_get_selected(dropdown); - uint32_t numChannels = LoRaPresets::getNumChannels(THIS->db.config.lora.region, preset); - if (numChannels == 0) { - // preset not possible for this region, revert - lv_dropdown_set_selected(dropdown, THIS->db.config.lora.modem_preset); - numChannels = LoRaPresets::getNumChannels(THIS->db.config.lora.region, THIS->db.config.lora.modem_preset); - return; - } + // animations + void TFTView_320x240::ui_anim_node_panel_cb(void *var, int32_t v) + { + lv_obj_set_height((lv_obj_t *)var, v); + } - uint32_t channel = LoRaPresets::getDefaultSlot(THIS->db.config.lora.region, preset, THIS->db.channel[0].settings.name); - if (channel > numChannels) - channel = 1; - lv_slider_set_range(objects.frequency_slot_slider, 1, numChannels); - lv_slider_set_value(objects.frequency_slot_slider, channel, LV_ANIM_ON); + void TFTView_320x240::ui_anim_radar_cb(void *var, int32_t r) + { + lv_img_set_angle(objects.radar_beam, r); + } + + /** + * @brief Dynamically show user widget + * First a panel is created where the widget is located in, then the widget is drawn. + * "active_widget" contains the surrounding panel which must be destroyed + * to remove the widget from the screen (e.g. by pressing OK/Cancel). + * + * @param func + */ + void TFTView_320x240::showUserWidget(UserWidgetFunc createWidget) + { + lv_obj_t *obj = lv_obj_create(objects.main_screen); + lv_obj_set_pos(obj, 39, 25); + lv_obj_set_size(obj, LV_PCT(88), LV_PCT(90)); + lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_color(obj, colorDarkGray, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_width(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_radius(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + activeWidget = obj; - char buf[40]; - sprintf(buf, _("FrequencySlot: %d (%g MHz)"), channel, - LoRaPresets::getRadioFreq(THIS->db.config.lora.region, preset, channel)); - lv_label_set_text(objects.frequency_slot_label, buf); -} + createWidget(activeWidget, NULL, 0); + } -void TFTView_320x240::ui_event_setup_region_dropdown(lv_event_t *e) {} + void TFTView_320x240::handleAddMessage(char *msg) + { + // retrieve nodeNum + channel from activeMsgContainer + uint32_t to = UINT32_MAX; + uint8_t ch = 0; + uint8_t hopLimit = db.config.lora.hop_limit; + uint32_t requestId; + uint32_t channelOrNode = (unsigned long)activeMsgContainer->user_data; + bool usePkc = false; -// animations -void TFTView_320x240::ui_anim_node_panel_cb(void *var, int32_t v) -{ - lv_obj_set_height((lv_obj_t *)var, v); -} + auto callback = [this](const ResponseHandler::Request &req, ResponseHandler::EventType evt, int32_t pass) { + this->onTextMessageCallback(req, evt, pass); + }; -void TFTView_320x240::ui_anim_radar_cb(void *var, int32_t r) -{ - lv_img_set_angle(objects.radar_beam, r); -} + if (channelOrNode < c_max_channels) { + ch = (uint8_t)channelOrNode; + requestId = requests.addRequest(ch, ResponseHandler::TextMessageRequest, (void *)(long)ch, callback); + } else { + ch = (uint8_t)(unsigned long)nodes[channelOrNode]->user_data; + to = channelOrNode; + usePkc = (unsigned long)nodes[to]->LV_OBJ_IDX(node_bat_idx)->user_data; // hasKey + requestId = requests.addRequest(to, ResponseHandler::TextMessageRequest, (void *)to, callback); + // trial: hoplimit optimization for direct text messages + int8_t hopsAway = (signed long)nodes[to]->LV_OBJ_IDX(node_sig_idx)->user_data; + if (hopsAway < 0) + hopsAway = db.config.lora.hop_limit; + hopLimit = (hopsAway < db.config.lora.hop_limit ? hopsAway + 1 : hopsAway); + } -/** - * @brief Dynamically show user widget - * First a panel is created where the widget is located in, then the widget is drawn. - * "active_widget" contains the surrounding panel which must be destroyed - * to remove the widget from the screen (e.g. by pressing OK/Cancel). - * - * @param func - */ -void TFTView_320x240::showUserWidget(UserWidgetFunc createWidget) -{ - lv_obj_t *obj = lv_obj_create(objects.main_screen); - lv_obj_set_pos(obj, 39, 25); - lv_obj_set_size(obj, LV_PCT(88), LV_PCT(90)); - lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); - lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_bg_color(obj, colorDarkGray, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_radius(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - activeWidget = obj; - - createWidget(activeWidget, NULL, 0); -} + // tweak to allow multiple lines in single line text area + for (int i = 0; i < strlen(msg); i++) + if (msg[i] == CR_REPLACEMENT) + msg[i] = '\n'; -void TFTView_320x240::handleAddMessage(char *msg) -{ - // retrieve nodeNum + channel from activeMsgContainer - uint32_t to = UINT32_MAX; - uint8_t ch = 0; - uint8_t hopLimit = db.config.lora.hop_limit; - uint32_t requestId; - uint32_t channelOrNode = (unsigned long)activeMsgContainer->user_data; - bool usePkc = false; - - auto callback = [this](const ResponseHandler::Request &req, ResponseHandler::EventType evt, int32_t pass) { - this->onTextMessageCallback(req, evt, pass); - }; + controller->sendTextMessage(to, ch, hopLimit, actTime, requestId, usePkc, msg); + addMessage(activeMsgContainer, actTime, requestId, msg, LogMessage::eNone); + } - if (channelOrNode < c_max_channels) { - ch = (uint8_t)channelOrNode; - requestId = requests.addRequest(ch, ResponseHandler::TextMessageRequest, (void *)(long)ch, callback); - } else { - ch = (uint8_t)(unsigned long)nodes[channelOrNode]->user_data; - to = channelOrNode; - usePkc = (unsigned long)nodes[to]->LV_OBJ_IDX(node_bat_idx)->user_data; // hasKey - requestId = requests.addRequest(to, ResponseHandler::TextMessageRequest, (void *)to, callback); - // trial: hoplimit optimization for direct text messages - int8_t hopsAway = (signed long)nodes[to]->LV_OBJ_IDX(node_sig_idx)->user_data; - if (hopsAway < 0) - hopsAway = db.config.lora.hop_limit; - hopLimit = (hopsAway < db.config.lora.hop_limit ? hopsAway + 1 : hopsAway); - } + /** + * display message that has just been written and sent out + */ + void TFTView_320x240::addMessage(lv_obj_t * container, uint32_t msgTime, uint32_t requestId, char *msg, + LogMessage::MsgStatus status) + { + lv_obj_t *hiddenPanel = lv_obj_create(container); + lv_obj_set_width(hiddenPanel, lv_pct(100)); + lv_obj_set_height(hiddenPanel, LV_SIZE_CONTENT); + lv_obj_set_align(hiddenPanel, LV_ALIGN_CENTER); + lv_obj_clear_flag(hiddenPanel, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_radius(hiddenPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + add_style_panel_style(hiddenPanel); + + lv_obj_set_style_border_width(hiddenPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_left(hiddenPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_right(hiddenPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_top(hiddenPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_bottom(hiddenPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + hiddenPanel->user_data = (void *)requestId; + + // add timestamp + char buf[284]; // 237 + 4 + 40 + 2 + 1 + buf[0] = '\0'; + uint32_t len = timestamp(buf, msgTime, status == LogMessage::eNone); + strcat(&buf[len], msg); + + lv_obj_t *textLabel = lv_label_create(hiddenPanel); + // calculate expected size of text bubble, to make it look nicer + lv_coord_t width = lv_txt_get_width(buf, strlen(buf), &ui_font_montserrat_12, 0); + lv_obj_set_width(textLabel, std::max(std::min(width, 200) + 10, 40)); + lv_obj_set_height(textLabel, LV_SIZE_CONTENT); + lv_obj_set_y(textLabel, 0); + lv_obj_set_align(textLabel, LV_ALIGN_RIGHT_MID); + lv_label_set_text(textLabel, buf); + + add_style_chat_message_style(textLabel); + + lv_obj_scroll_to_view(hiddenPanel, LV_ANIM_ON); + lv_obj_move_foreground(objects.message_input_area); + + switch (status) { + case LogMessage::eHeard: + lv_obj_set_style_border_color(textLabel, colorYellow, LV_PART_MAIN | LV_STATE_DEFAULT); + break; + case LogMessage::eAcked: + lv_obj_set_style_border_color(textLabel, colorBlueGreen, LV_PART_MAIN | LV_STATE_DEFAULT); + break; + case LogMessage::eFailed: + lv_obj_set_style_border_color(textLabel, colorRed, LV_PART_MAIN | LV_STATE_DEFAULT); + break; + default: + break; + } + } - // tweak to allow multiple lines in single line text area - for (int i = 0; i < strlen(msg); i++) - if (msg[i] == CR_REPLACEMENT) - msg[i] = '\n'; + void TFTView_320x240::addNode(uint32_t nodeNum, uint8_t ch, const char *userShort, const char *userLong, + uint32_t lastHeard, eRole role, bool hasKey, bool unmessagable) + { + // lv_obj nodesPanel children | user data (4 bytes) + // ================================================== + // [0]: img | role + // [1]: btn | ll group + // [2]: lbl user long | nodeNum + // [3]: lbl user short | userShort (4 chars) + // [4]: lbl battery | hasKey + // [5]: lbl lastHeard | lastHeard / curtime + // [6]: lbl signal (or hops) | hops away + // [7]: lbl position 1 | lat + // [8]: lbl position 2 | lon + // [9]: lbl telemetry 1 | + // [10]: lbl telemetry 2 | iaq + // panel user_data: ch + + ILOG_DEBUG("addNode(%d): num=0x%08x, lastseen=%d, name=%s(%s), role=%d", nodeCount, nodeNum, lastHeard, userLong, + userShort, role); + while (nodeCount >= MAX_NUM_NODES_VIEW) { + purgeNode(nodeNum); + } - controller->sendTextMessage(to, ch, hopLimit, actTime, requestId, usePkc, msg); - addMessage(activeMsgContainer, actTime, requestId, msg, LogMessage::eNone); -} + lv_obj_t *p = lv_obj_create(objects.nodes_panel); + lv_ll_t *lv_group_ll = &lv_group_get_default()->obj_ll; -/** - * display message that has just been written and sent out - */ -void TFTView_320x240::addMessage(lv_obj_t *container, uint32_t msgTime, uint32_t requestId, char *msg, - LogMessage::MsgStatus status) -{ - lv_obj_t *hiddenPanel = lv_obj_create(container); - lv_obj_set_width(hiddenPanel, lv_pct(100)); - lv_obj_set_height(hiddenPanel, LV_SIZE_CONTENT); - lv_obj_set_align(hiddenPanel, LV_ALIGN_CENTER); - lv_obj_clear_flag(hiddenPanel, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_radius(hiddenPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - add_style_panel_style(hiddenPanel); - - lv_obj_set_style_border_width(hiddenPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_left(hiddenPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_right(hiddenPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_top(hiddenPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_bottom(hiddenPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - hiddenPanel->user_data = (void *)requestId; - - // add timestamp - char buf[284]; // 237 + 4 + 40 + 2 + 1 - buf[0] = '\0'; - uint32_t len = timestamp(buf, msgTime, status == LogMessage::eNone); - strcat(&buf[len], msg); - - lv_obj_t *textLabel = lv_label_create(hiddenPanel); - // calculate expected size of text bubble, to make it look nicer - lv_coord_t width = lv_txt_get_width(buf, strlen(buf), &ui_font_montserrat_12, 0); - lv_obj_set_width(textLabel, std::max(std::min(width, 200) + 10, 40)); - lv_obj_set_height(textLabel, LV_SIZE_CONTENT); - lv_obj_set_y(textLabel, 0); - lv_obj_set_align(textLabel, LV_ALIGN_RIGHT_MID); - lv_label_set_text(textLabel, buf); - - add_style_chat_message_style(textLabel); - - lv_obj_scroll_to_view(hiddenPanel, LV_ANIM_ON); - lv_obj_move_foreground(objects.message_input_area); - - switch (status) { - case LogMessage::eHeard: - lv_obj_set_style_border_color(textLabel, colorYellow, LV_PART_MAIN | LV_STATE_DEFAULT); - break; - case LogMessage::eAcked: - lv_obj_set_style_border_color(textLabel, colorBlueGreen, LV_PART_MAIN | LV_STATE_DEFAULT); - break; - case LogMessage::eFailed: - lv_obj_set_style_border_color(textLabel, colorRed, LV_PART_MAIN | LV_STATE_DEFAULT); - break; - default: - break; - } -} + p->user_data = (void *)(uint32_t)ch; + nodes[nodeNum] = p; + nodeCount++; + + // NodePanel + lv_obj_set_pos(p, LV_PCT(0), 0); + lv_obj_set_size(p, LV_PCT(100), 53); + lv_obj_set_align(p, LV_ALIGN_CENTER); + lv_obj_set_style_pad_top(p, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_bottom(p, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_remove_flag(p, lv_obj_flag_t(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_PRESS_LOCK | LV_OBJ_FLAG_CLICK_FOCUSABLE | + LV_OBJ_FLAG_GESTURE_BUBBLE | LV_OBJ_FLAG_SNAPPABLE | LV_OBJ_FLAG_SCROLLABLE)); + add_style_node_panel_style(p); + + // NodeImage + lv_obj_t *img = lv_img_create(p); + setNodeImage(nodeNum, role, unmessagable, img); + lv_obj_set_pos(img, -5, 3); + lv_obj_set_size(img, 32, 32); + lv_obj_clear_flag(img, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_radius(img, 6, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(img, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_opa(img, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_width(img, 2, LV_PART_MAIN | LV_STATE_DEFAULT); + if (!hasKey) { + lv_obj_set_style_border_color(img, colorRed, LV_PART_MAIN | LV_STATE_DEFAULT); + } + if (unmessagable) { + // node role icon is not clickable and replaced with a cancelled icon + img->user_data = (void *)eRole::unmessagable; + } else { + img->user_data = (void *)role; + } -void TFTView_320x240::addNode(uint32_t nodeNum, uint8_t ch, const char *userShort, const char *userLong, uint32_t lastHeard, - eRole role, bool hasKey, bool unmessagable) -{ - // lv_obj nodesPanel children | user data (4 bytes) - // ================================================== - // [0]: img | role - // [1]: btn | ll group - // [2]: lbl user long | nodeNum - // [3]: lbl user short | userShort (4 chars) - // [4]: lbl battery | hasKey - // [5]: lbl lastHeard | lastHeard / curtime - // [6]: lbl signal (or hops) | hops away - // [7]: lbl position 1 | lat - // [8]: lbl position 2 | lon - // [9]: lbl telemetry 1 | - // [10]: lbl telemetry 2 | iaq - // panel user_data: ch - - ILOG_DEBUG("addNode(%d): num=0x%08x, lastseen=%d, name=%s(%s), role=%d", nodeCount, nodeNum, lastHeard, userLong, userShort, - role); - while (nodeCount >= MAX_NUM_NODES_VIEW) { - purgeNode(nodeNum); - } + // NodeButton + lv_obj_t *nodeButton = lv_btn_create(p); + lv_obj_set_pos(nodeButton, 0, 0); + lv_obj_set_size(nodeButton, LV_PCT(106), LV_PCT(100)); + add_style_node_button_style(nodeButton); + lv_obj_set_align(nodeButton, LV_ALIGN_CENTER); + lv_obj_add_flag(nodeButton, LV_OBJ_FLAG_SCROLL_ON_FOCUS); + lv_obj_set_style_shadow_width(nodeButton, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_max_height(nodeButton, 132, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_min_height(nodeButton, 50, LV_PART_MAIN | LV_STATE_DEFAULT); + nodeButton->user_data = _lv_ll_get_tail(lv_group_ll); + + // UserNameLabel + lv_obj_t *ln_lbl = lv_label_create(p); + lv_obj_set_pos(ln_lbl, -5, 35); + lv_obj_set_size(ln_lbl, LV_PCT(80), LV_SIZE_CONTENT); + lv_label_set_long_mode(ln_lbl, LV_LABEL_LONG_SCROLL); + lv_label_set_text(ln_lbl, userLong); + ln_lbl->user_data = (void *)nodeNum; + lv_obj_set_style_align(ln_lbl, LV_ALIGN_TOP_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); + + // UserNameShortLabel + lv_obj_t *sn_lbl = lv_label_create(p); + lv_obj_set_pos(sn_lbl, 30, 10); + lv_obj_set_size(sn_lbl, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_label_set_long_mode(sn_lbl, LV_LABEL_LONG_WRAP); + lv_obj_set_style_align(sn_lbl, LV_ALIGN_TOP_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_font(sn_lbl, &ui_font_montserrat_14, LV_PART_MAIN | LV_STATE_DEFAULT); + // if short name contains only non-printable glyphs replace with short id + if (lv_txt_get_width(userShort, strlen(userShort), &ui_font_montserrat_14, 0) <= 4) { + lv_label_set_text_fmt(sn_lbl, "%04x", nodeNum & 0xffff); + } else { + lv_label_set_text(sn_lbl, userShort); + } + char *modUserShort = lv_label_get_text(sn_lbl); + + // keep a copy of the (4-byte) short name for use in many other widgets + char *userData = (char *)&(sn_lbl->user_data); + userData[0] = modUserShort[0]; + if (userData[0] == 0x00) + userData[0] = ' '; + userData[1] = modUserShort[1]; + if (userData[1] == 0x00) + userData[1] = ' '; + userData[2] = modUserShort[2]; + if (userData[2] == 0x00) + userData[2] = ' '; + userData[3] = modUserShort[3]; + if (userData[3] == 0x00) + userData[3] = ' '; + + // BatteryLabel + lv_obj_t *ui_BatteryLabel = lv_label_create(p); + lv_obj_set_pos(ui_BatteryLabel, 8, 17); + lv_obj_set_size(ui_BatteryLabel, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_align(ui_BatteryLabel, LV_ALIGN_TOP_RIGHT); + lv_label_set_text(ui_BatteryLabel, ""); + lv_obj_set_style_text_align(ui_BatteryLabel, LV_TEXT_ALIGN_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); + ui_BatteryLabel->user_data = (void *)hasKey; + // LastHeardLabel + lv_obj_t *ui_lastHeardLabel = lv_label_create(p); + lv_obj_set_pos(ui_lastHeardLabel, 8, 33); + lv_obj_set_size(ui_lastHeardLabel, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_style_align(ui_lastHeardLabel, LV_ALIGN_TOP_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_label_set_long_mode(ui_lastHeardLabel, LV_LABEL_LONG_CLIP); + + // TODO: devices without actual time will report all nodes as lastseen = now + if (lastHeard) { + lastHeard = std::min(curtime, (time_t)lastHeard); // adapt values too large - lv_obj_t *p = lv_obj_create(objects.nodes_panel); - lv_ll_t *lv_group_ll = &lv_group_get_default()->obj_ll; + char buf[20]; + bool isOnline = lastHeardToString(lastHeard, buf); + lv_label_set_text(ui_lastHeardLabel, buf); + if (isOnline) { + nodesOnline++; + } + } else { + lv_label_set_text(ui_lastHeardLabel, ""); + } - p->user_data = (void *)(uint32_t)ch; - nodes[nodeNum] = p; - nodeCount++; - - // NodePanel - lv_obj_set_pos(p, LV_PCT(0), 0); - lv_obj_set_size(p, LV_PCT(100), 53); - lv_obj_set_align(p, LV_ALIGN_CENTER); - lv_obj_set_style_pad_top(p, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_bottom(p, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_remove_flag(p, lv_obj_flag_t(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_PRESS_LOCK | LV_OBJ_FLAG_CLICK_FOCUSABLE | - LV_OBJ_FLAG_GESTURE_BUBBLE | LV_OBJ_FLAG_SNAPPABLE | LV_OBJ_FLAG_SCROLLABLE)); - add_style_node_panel_style(p); - - // NodeImage - lv_obj_t *img = lv_img_create(p); - setNodeImage(nodeNum, role, unmessagable, img); - lv_obj_set_pos(img, -5, 3); - lv_obj_set_size(img, 32, 32); - lv_obj_clear_flag(img, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_radius(img, 6, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(img, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_opa(img, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(img, 2, LV_PART_MAIN | LV_STATE_DEFAULT); - if (!hasKey) { - lv_obj_set_style_border_color(img, colorRed, LV_PART_MAIN | LV_STATE_DEFAULT); - } - if (unmessagable) { - // node role icon is not clickable and replaced with a cancelled icon - img->user_data = (void *)eRole::unmessagable; - } else { - img->user_data = (void *)role; - } + lv_obj_set_style_text_align(ui_lastHeardLabel, LV_TEXT_ALIGN_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); + ui_lastHeardLabel->user_data = (void *)lastHeard; + // SignalLabel / hopsAway + lv_obj_t *ui_SignalLabel = lv_label_create(p); + lv_obj_set_width(ui_SignalLabel, LV_SIZE_CONTENT); + lv_obj_set_height(ui_SignalLabel, LV_SIZE_CONTENT); + lv_obj_set_pos(ui_SignalLabel, 8, 1); + lv_obj_set_align(ui_SignalLabel, LV_ALIGN_TOP_RIGHT); + lv_label_set_text(ui_SignalLabel, ""); + lv_obj_set_style_text_color(ui_SignalLabel, lv_color_hex(0xff5ded96), LV_PART_MAIN | LV_STATE_DEFAULT); + ui_SignalLabel->user_data = (void *)-1; // TODO viaMqtt; // used for filtering (applyNodesFilter) + // PositionLabel + lv_obj_t *ui_PositionLabel = lv_label_create(p); + lv_obj_set_pos(ui_PositionLabel, -5, 49); + lv_obj_set_size(ui_PositionLabel, 120, LV_SIZE_CONTENT); + lv_label_set_long_mode(ui_PositionLabel, LV_LABEL_LONG_CLIP); + lv_label_set_text(ui_PositionLabel, ""); + lv_obj_set_style_align(ui_PositionLabel, LV_ALIGN_TOP_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_color(ui_PositionLabel, colorBlueGreen, LV_PART_MAIN | LV_STATE_DEFAULT); + ui_PositionLabel->user_data = 0; // store latitude + // Position2Label + lv_obj_t *ui_Position2Label = lv_label_create(p); + lv_obj_set_pos(ui_Position2Label, -5, 63); + lv_obj_set_size(ui_Position2Label, 108, LV_SIZE_CONTENT); + lv_label_set_long_mode(ui_Position2Label, LV_LABEL_LONG_SCROLL); + lv_label_set_text(ui_Position2Label, ""); + lv_obj_set_style_align(ui_Position2Label, LV_ALIGN_TOP_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); + ui_Position2Label->user_data = 0; // store longitude + // Telemetry1Label + lv_obj_t *ui_Telemetry1Label = lv_label_create(p); + lv_obj_set_pos(ui_Telemetry1Label, 8, 49); + lv_obj_set_size(ui_Telemetry1Label, 130, LV_SIZE_CONTENT); + lv_label_set_long_mode(ui_Telemetry1Label, LV_LABEL_LONG_CLIP); + lv_label_set_text(ui_Telemetry1Label, ""); + lv_obj_set_style_align(ui_Telemetry1Label, LV_ALIGN_TOP_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_align(ui_Telemetry1Label, LV_TEXT_ALIGN_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); + // Telemetry2Label + lv_obj_t *ui_Telemetry2Label = lv_label_create(p); + lv_obj_set_pos(ui_Telemetry2Label, 8, 63); + lv_obj_set_size(ui_Telemetry2Label, 130, LV_SIZE_CONTENT); + lv_label_set_long_mode(ui_Telemetry2Label, LV_LABEL_LONG_CLIP); + lv_label_set_text(ui_Telemetry2Label, ""); + lv_obj_set_style_align(ui_Telemetry2Label, LV_ALIGN_TOP_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_align(ui_Telemetry2Label, LV_TEXT_ALIGN_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); + + // optimisation: hide all 6ix extended labels by default; enable only when set + // lv_obj_add_flag(ui_lastHeardLabel, LV_OBJ_FLAG_HIDDEN); // lastHeard + lv_obj_add_flag(ui_BatteryLabel, LV_OBJ_FLAG_HIDDEN); // Autohide battery + lv_obj_add_flag(ui_SignalLabel, LV_OBJ_FLAG_HIDDEN); // Autohide signal/hops + lv_obj_add_flag(ui_PositionLabel, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(ui_Position2Label, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(ui_Telemetry1Label, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(ui_Telemetry2Label, LV_OBJ_FLAG_HIDDEN); + + lv_obj_add_event_cb(nodeButton, ui_event_NodeButton, LV_EVENT_ALL, (void *)nodeNum); + + // move node into new position within nodePanel + if (lastHeard) { + lv_obj_t **children = objects.nodes_panel->spec_attr->children; + int i = objects.nodes_panel->spec_attr->child_cnt - 1; + while (i > 1) { + if (lastHeard <= (time_t)(children[i - 1]->LV_OBJ_IDX(node_lh_idx)->user_data)) + break; + i--; + } + if (i >= 1 && i < objects.nodes_panel->spec_attr->child_cnt - 1) { + lv_obj_move_to_index(p, i); + // re-arrange the group linked list by moving the new button (now at the tail) into the right position + void *after = children[i + 1]->LV_OBJ_IDX(node_btn_idx)->user_data; + _lv_ll_move_before(lv_group_ll, nodeButton->user_data, after); + } + } - // NodeButton - lv_obj_t *nodeButton = lv_btn_create(p); - lv_obj_set_pos(nodeButton, 0, 0); - lv_obj_set_size(nodeButton, LV_PCT(106), LV_PCT(100)); - add_style_node_button_style(nodeButton); - lv_obj_set_align(nodeButton, LV_ALIGN_CENTER); - lv_obj_add_flag(nodeButton, LV_OBJ_FLAG_SCROLL_ON_FOCUS); - lv_obj_set_style_shadow_width(nodeButton, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_max_height(nodeButton, 132, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_min_height(nodeButton, 50, LV_PART_MAIN | LV_STATE_DEFAULT); - nodeButton->user_data = _lv_ll_get_tail(lv_group_ll); - - // UserNameLabel - lv_obj_t *ln_lbl = lv_label_create(p); - lv_obj_set_pos(ln_lbl, -5, 35); - lv_obj_set_size(ln_lbl, LV_PCT(80), LV_SIZE_CONTENT); - lv_label_set_long_mode(ln_lbl, LV_LABEL_LONG_SCROLL); - lv_label_set_text(ln_lbl, userLong); - ln_lbl->user_data = (void *)nodeNum; - lv_obj_set_style_align(ln_lbl, LV_ALIGN_TOP_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); - - // UserNameShortLabel - lv_obj_t *sn_lbl = lv_label_create(p); - lv_obj_set_pos(sn_lbl, 30, 10); - lv_obj_set_size(sn_lbl, LV_SIZE_CONTENT, LV_SIZE_CONTENT); - lv_label_set_long_mode(sn_lbl, LV_LABEL_LONG_WRAP); - lv_obj_set_style_align(sn_lbl, LV_ALIGN_TOP_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_font(sn_lbl, &ui_font_montserrat_14, LV_PART_MAIN | LV_STATE_DEFAULT); - // if short name contains only non-printable glyphs replace with short id - if (lv_txt_get_width(userShort, strlen(userShort), &ui_font_montserrat_14, 0) <= 4) { - lv_label_set_text_fmt(sn_lbl, "%04x", nodeNum & 0xffff); - } else { - lv_label_set_text(sn_lbl, userShort); - } - char *modUserShort = lv_label_get_text(sn_lbl); - - // keep a copy of the (4-byte) short name for use in many other widgets - char *userData = (char *)&(sn_lbl->user_data); - userData[0] = modUserShort[0]; - if (userData[0] == 0x00) - userData[0] = ' '; - userData[1] = modUserShort[1]; - if (userData[1] == 0x00) - userData[1] = ' '; - userData[2] = modUserShort[2]; - if (userData[2] == 0x00) - userData[2] = ' '; - userData[3] = modUserShort[3]; - if (userData[3] == 0x00) - userData[3] = ' '; - - // BatteryLabel - lv_obj_t *ui_BatteryLabel = lv_label_create(p); - lv_obj_set_pos(ui_BatteryLabel, 8, 17); - lv_obj_set_size(ui_BatteryLabel, LV_SIZE_CONTENT, LV_SIZE_CONTENT); - lv_obj_set_align(ui_BatteryLabel, LV_ALIGN_TOP_RIGHT); - lv_label_set_text(ui_BatteryLabel, ""); - lv_obj_set_style_text_align(ui_BatteryLabel, LV_TEXT_ALIGN_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); - ui_BatteryLabel->user_data = (void *)hasKey; - // LastHeardLabel - lv_obj_t *ui_lastHeardLabel = lv_label_create(p); - lv_obj_set_pos(ui_lastHeardLabel, 8, 33); - lv_obj_set_size(ui_lastHeardLabel, LV_SIZE_CONTENT, LV_SIZE_CONTENT); - lv_obj_set_style_align(ui_lastHeardLabel, LV_ALIGN_TOP_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_label_set_long_mode(ui_lastHeardLabel, LV_LABEL_LONG_CLIP); - - // TODO: devices without actual time will report all nodes as lastseen = now - if (lastHeard) { - lastHeard = std::min(curtime, (time_t)lastHeard); // adapt values too large - - char buf[20]; - bool isOnline = lastHeardToString(lastHeard, buf); - lv_label_set_text(ui_lastHeardLabel, buf); - if (isOnline) { - nodesOnline++; + if (!nodesChanged) { + applyNodesFilter(nodeNum); + updateNodesStatus(); + } } - } else { - lv_label_set_text(ui_lastHeardLabel, ""); - } - lv_obj_set_style_text_align(ui_lastHeardLabel, LV_TEXT_ALIGN_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); - ui_lastHeardLabel->user_data = (void *)lastHeard; - // SignalLabel / hopsAway - lv_obj_t *ui_SignalLabel = lv_label_create(p); - lv_obj_set_width(ui_SignalLabel, LV_SIZE_CONTENT); - lv_obj_set_height(ui_SignalLabel, LV_SIZE_CONTENT); - lv_obj_set_pos(ui_SignalLabel, 8, 1); - lv_obj_set_align(ui_SignalLabel, LV_ALIGN_TOP_RIGHT); - lv_label_set_text(ui_SignalLabel, ""); - lv_obj_set_style_text_color(ui_SignalLabel, lv_color_hex(0xff5ded96), LV_PART_MAIN | LV_STATE_DEFAULT); - ui_SignalLabel->user_data = (void *)-1; // TODO viaMqtt; // used for filtering (applyNodesFilter) - // PositionLabel - lv_obj_t *ui_PositionLabel = lv_label_create(p); - lv_obj_set_pos(ui_PositionLabel, -5, 49); - lv_obj_set_size(ui_PositionLabel, 120, LV_SIZE_CONTENT); - lv_label_set_long_mode(ui_PositionLabel, LV_LABEL_LONG_CLIP); - lv_label_set_text(ui_PositionLabel, ""); - lv_obj_set_style_align(ui_PositionLabel, LV_ALIGN_TOP_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_color(ui_PositionLabel, colorBlueGreen, LV_PART_MAIN | LV_STATE_DEFAULT); - ui_PositionLabel->user_data = 0; // store latitude - // Position2Label - lv_obj_t *ui_Position2Label = lv_label_create(p); - lv_obj_set_pos(ui_Position2Label, -5, 63); - lv_obj_set_size(ui_Position2Label, 108, LV_SIZE_CONTENT); - lv_label_set_long_mode(ui_Position2Label, LV_LABEL_LONG_SCROLL); - lv_label_set_text(ui_Position2Label, ""); - lv_obj_set_style_align(ui_Position2Label, LV_ALIGN_TOP_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); - ui_Position2Label->user_data = 0; // store longitude - // Telemetry1Label - lv_obj_t *ui_Telemetry1Label = lv_label_create(p); - lv_obj_set_pos(ui_Telemetry1Label, 8, 49); - lv_obj_set_size(ui_Telemetry1Label, 130, LV_SIZE_CONTENT); - lv_label_set_long_mode(ui_Telemetry1Label, LV_LABEL_LONG_CLIP); - lv_label_set_text(ui_Telemetry1Label, ""); - lv_obj_set_style_align(ui_Telemetry1Label, LV_ALIGN_TOP_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_align(ui_Telemetry1Label, LV_TEXT_ALIGN_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); - // Telemetry2Label - lv_obj_t *ui_Telemetry2Label = lv_label_create(p); - lv_obj_set_pos(ui_Telemetry2Label, 8, 63); - lv_obj_set_size(ui_Telemetry2Label, 130, LV_SIZE_CONTENT); - lv_label_set_long_mode(ui_Telemetry2Label, LV_LABEL_LONG_CLIP); - lv_label_set_text(ui_Telemetry2Label, ""); - lv_obj_set_style_align(ui_Telemetry2Label, LV_ALIGN_TOP_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_align(ui_Telemetry2Label, LV_TEXT_ALIGN_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); - - // optimisation: hide all 6ix extended labels by default; enable only when set - // lv_obj_add_flag(ui_lastHeardLabel, LV_OBJ_FLAG_HIDDEN); // lastHeard - lv_obj_add_flag(ui_BatteryLabel, LV_OBJ_FLAG_HIDDEN); // Autohide battery - lv_obj_add_flag(ui_SignalLabel, LV_OBJ_FLAG_HIDDEN); // Autohide signal/hops - lv_obj_add_flag(ui_PositionLabel, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(ui_Position2Label, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(ui_Telemetry1Label, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(ui_Telemetry2Label, LV_OBJ_FLAG_HIDDEN); - - lv_obj_add_event_cb(nodeButton, ui_event_NodeButton, LV_EVENT_ALL, (void *)nodeNum); - - // move node into new position within nodePanel - if (lastHeard) { - lv_obj_t **children = objects.nodes_panel->spec_attr->children; - int i = objects.nodes_panel->spec_attr->child_cnt - 1; - while (i > 1) { - if (lastHeard <= (time_t)(children[i - 1]->LV_OBJ_IDX(node_lh_idx)->user_data)) - break; - i--; - } - if (i >= 1 && i < objects.nodes_panel->spec_attr->child_cnt - 1) { - lv_obj_move_to_index(p, i); - // re-arrange the group linked list by moving the new button (now at the tail) into the right position - void *after = children[i + 1]->LV_OBJ_IDX(node_btn_idx)->user_data; - _lv_ll_move_before(lv_group_ll, nodeButton->user_data, after); + void TFTView_320x240::setMyInfo(uint32_t nodeNum) + { + ownNode = nodeNum; } - } - - if (!nodesChanged) { - applyNodesFilter(nodeNum); - updateNodesStatus(); - } -} - -void TFTView_320x240::setMyInfo(uint32_t nodeNum) -{ - ownNode = nodeNum; -} - -void TFTView_320x240::setDeviceMetaData(int hw_model, const char *version, bool has_bluetooth, bool has_wifi, bool has_eth, - bool can_shutdown) -{ -} -void TFTView_320x240::addOrUpdateNode(uint32_t nodeNum, uint8_t channel, uint32_t lastHeard, const meshtastic_User &cfg) -{ - if (nodes.find(nodeNum) == nodes.end()) { - addNode(nodeNum, channel, cfg.short_name, cfg.long_name, lastHeard, (MeshtasticView::eRole)cfg.role, - cfg.public_key.size != 0, cfg.has_is_unmessagable && cfg.is_unmessagable); - } else { - updateNode(nodeNum, channel, cfg); - } -} - -/** - * @brief update node userName and image - * - * @param nodeNum - * @param ch - * @param userShort - * @param userLong - * @param lastHeard - * @param role - * @param viaMqtt - */ -// void TFTView_320x240::updateNode(uint32_t nodeNum, uint8_t ch, const char *userShort, const char *userLong, uint32_t lastHeard, -// eRole role, bool hasKey, bool viaMqtt) -void TFTView_320x240::updateNode(uint32_t nodeNum, uint8_t ch, const meshtastic_User &cfg) -{ - db.user = cfg; - auto it = nodes.find(nodeNum); - if (it != nodes.end() && it->second) { - if (it->first == ownNode) { - // update related settings buttons and store role in image user data - char buf[30]; - lv_snprintf(buf, sizeof(buf), _("User name: %s"), cfg.short_name); - lv_label_set_text(objects.basic_settings_user_label, buf); - - char buf1[30], buf2[40]; - lv_dropdown_set_selected(objects.settings_device_role_dropdown, - role2val(meshtastic_Config_DeviceConfig_Role(cfg.role))); - lv_dropdown_get_selected_str(objects.settings_device_role_dropdown, buf1, sizeof(buf1)); - lv_snprintf(buf2, sizeof(buf2), _("Device Role: %s"), buf1); - lv_label_set_text(objects.basic_settings_role_label, buf2); + void TFTView_320x240::setDeviceMetaData(int hw_model, const char *version, bool has_bluetooth, bool has_wifi, + bool has_eth, bool can_shutdown) + { + } - // update DB - strcpy(db.short_name, cfg.short_name); - strcpy(db.long_name, cfg.long_name); - db.config.device.role = cfg.role; - } - lv_label_set_text(it->second->LV_OBJ_IDX(node_lbl_idx), cfg.long_name); - it->second->LV_OBJ_IDX(node_lbl_idx)->user_data = (void *)nodeNum; - lv_label_set_text(it->second->LV_OBJ_IDX(node_lbs_idx), cfg.short_name); - char *userData = (char *)&(it->second->LV_OBJ_IDX(node_lbs_idx)->user_data); - userData[0] = cfg.short_name[0]; - if (userData[0] == 0x00) - userData[0] = ' '; - userData[1] = cfg.short_name[1]; - if (userData[1] == 0x00) - userData[1] = ' '; - userData[2] = cfg.short_name[2]; - if (userData[2] == 0x00) - userData[2] = ' '; - userData[3] = cfg.short_name[3]; - if (userData[3] == 0x00) - userData[3] = ' '; - - setNodeImage(nodeNum, (MeshtasticView::eRole)cfg.role, cfg.has_is_unmessagable && cfg.is_unmessagable, - it->second->LV_OBJ_IDX(node_img_idx)); - - if (cfg.public_key.size != 0) { - // set border color to bg color - lv_color_t color = lv_obj_get_style_bg_color(it->second->LV_OBJ_IDX(node_img_idx), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(it->second->LV_OBJ_IDX(node_img_idx), color, LV_PART_MAIN | LV_STATE_DEFAULT); - } else { - lv_obj_set_style_border_color(it->second->LV_OBJ_IDX(node_img_idx), colorRed, LV_PART_MAIN | LV_STATE_DEFAULT); + void TFTView_320x240::addOrUpdateNode(uint32_t nodeNum, uint8_t channel, uint32_t lastHeard, const meshtastic_User &cfg) + { + if (nodes.find(nodeNum) == nodes.end()) { + addNode(nodeNum, channel, cfg.short_name, cfg.long_name, lastHeard, (MeshtasticView::eRole)cfg.role, + cfg.public_key.size != 0, cfg.has_is_unmessagable && cfg.is_unmessagable); + } else { + updateNode(nodeNum, channel, cfg); + } } - // update chat name - auto ct = chats.find(it->first); - if (ct != chats.end()) { - char buf[64]; - lv_snprintf(buf, sizeof(buf), "%s: %s", lv_label_get_text(it->second->LV_OBJ_IDX(node_lbs_idx)), - lv_label_get_text(it->second->LV_OBJ_IDX(node_lbl_idx))); - lv_label_set_text(ct->second->spec_attr->children[0], buf); + /** + * @brief update node userName and image + * + * @param nodeNum + * @param ch + * @param userShort + * @param userLong + * @param lastHeard + * @param role + * @param viaMqtt + */ + // void TFTView_320x240::updateNode(uint32_t nodeNum, uint8_t ch, const char *userShort, const char *userLong, uint32_t + // lastHeard, + // eRole role, bool hasKey, bool viaMqtt) + void TFTView_320x240::updateNode(uint32_t nodeNum, uint8_t ch, const meshtastic_User &cfg) + { + db.user = cfg; + auto it = nodes.find(nodeNum); + if (it != nodes.end() && it->second) { + if (it->first == ownNode) { + // update related settings buttons and store role in image user data + char buf[30]; + lv_snprintf(buf, sizeof(buf), _("User name: %s"), cfg.short_name); + lv_label_set_text(objects.basic_settings_user_label, buf); + + char buf1[30], buf2[40]; + lv_dropdown_set_selected(objects.settings_device_role_dropdown, + role2val(meshtastic_Config_DeviceConfig_Role(cfg.role))); + lv_dropdown_get_selected_str(objects.settings_device_role_dropdown, buf1, sizeof(buf1)); + lv_snprintf(buf2, sizeof(buf2), _("Device Role: %s"), buf1); + lv_label_set_text(objects.basic_settings_role_label, buf2); + + // update DB + strcpy(db.short_name, cfg.short_name); + strcpy(db.long_name, cfg.long_name); + db.config.device.role = cfg.role; + } + lv_label_set_text(it->second->LV_OBJ_IDX(node_lbl_idx), cfg.long_name); + it->second->LV_OBJ_IDX(node_lbl_idx)->user_data = (void *)nodeNum; + lv_label_set_text(it->second->LV_OBJ_IDX(node_lbs_idx), cfg.short_name); + char *userData = (char *)&(it->second->LV_OBJ_IDX(node_lbs_idx)->user_data); + userData[0] = cfg.short_name[0]; + if (userData[0] == 0x00) + userData[0] = ' '; + userData[1] = cfg.short_name[1]; + if (userData[1] == 0x00) + userData[1] = ' '; + userData[2] = cfg.short_name[2]; + if (userData[2] == 0x00) + userData[2] = ' '; + userData[3] = cfg.short_name[3]; + if (userData[3] == 0x00) + userData[3] = ' '; + + setNodeImage(nodeNum, (MeshtasticView::eRole)cfg.role, cfg.has_is_unmessagable && cfg.is_unmessagable, + it->second->LV_OBJ_IDX(node_img_idx)); + + if (cfg.public_key.size != 0) { + // set border color to bg color + lv_color_t color = + lv_obj_get_style_bg_color(it->second->LV_OBJ_IDX(node_img_idx), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_color(it->second->LV_OBJ_IDX(node_img_idx), color, LV_PART_MAIN | LV_STATE_DEFAULT); + } else { + lv_obj_set_style_border_color(it->second->LV_OBJ_IDX(node_img_idx), colorRed, + LV_PART_MAIN | LV_STATE_DEFAULT); + } + + // update chat name + auto ct = chats.find(it->first); + if (ct != chats.end()) { + char buf[64]; + lv_snprintf(buf, sizeof(buf), "%s: %s", lv_label_get_text(it->second->LV_OBJ_IDX(node_lbs_idx)), + lv_label_get_text(it->second->LV_OBJ_IDX(node_lbl_idx))); + lv_label_set_text(ct->second->spec_attr->children[0], buf); + } + } } - } -} -void TFTView_320x240::updatePosition(uint32_t nodeNum, int32_t lat, int32_t lon, int32_t alt, uint32_t sats, uint32_t precision) -{ - int32_t altU = abs(alt) < 10000 ? alt : 0; - char units[3] = {}; - if (db.config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_METRIC) { - units[0] = 'm'; - } else { - units[0] = 'f'; - units[1] = 't'; - altU = int32_t(float(altU) * 3.28084); - } - if (nodeNum == ownNode) { - char buf[64]; - int latSeconds = (int)round(lat * 1e-7 * 3600); - int latDegrees = latSeconds / 3600; - latSeconds = abs(latSeconds % 3600); - int latMinutes = latSeconds / 60; - latSeconds %= 60; - char latLetter = (lat > 0) ? 'N' : 'S'; - - int lonSeconds = (int)round(lon * 1e-7 * 3600); - int lonDegrees = lonSeconds / 3600; - lonSeconds = abs(lonSeconds % 3600); - int lonMinutes = lonSeconds / 60; - lonSeconds %= 60; - char lonLetter = (lon > 0) ? 'E' : 'W'; - - if (sats) - sprintf(buf, "%c%02i° %2i'%02i\" %u sats\n%c%02i° %2i'%02i\" %d%s", latLetter, abs(latDegrees), latMinutes, - latSeconds, sats, lonLetter, abs(lonDegrees), lonMinutes, lonSeconds, altU, units); - else - sprintf(buf, "%c%02i° %2i'%02i\"\n%c%02i° %2i'%02i\" %d%s", latLetter, abs(latDegrees), latMinutes, latSeconds, - lonLetter, abs(lonDegrees), lonMinutes, lonSeconds, altU, units); - - lv_label_set_text(objects.home_location_label, buf); - - if (lat != 0 && lon != 0) { - hasPosition = true; - myLatitude = lat; - myLongitude = lon; - - // go through existing node list and update distance - // TODO: need incremental update!? - for (auto &it : nodes) { - if (it.first != ownNode) { - int32_t nlat = (long)it.second->LV_OBJ_IDX(node_pos1_idx)->user_data; - int32_t nlon = (long)it.second->LV_OBJ_IDX(node_pos2_idx)->user_data; - if (nlat != 0 && nlon != 0) { - updateDistance(it.first, nlat, nlon); + void TFTView_320x240::updatePosition(uint32_t nodeNum, int32_t lat, int32_t lon, int32_t alt, uint32_t sats, + uint32_t precision) + { + int32_t altU = abs(alt) < 10000 ? alt : 0; + char units[3] = {}; + if (db.config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_METRIC) { + units[0] = 'm'; + } else { + units[0] = 'f'; + units[1] = 't'; + altU = int32_t(float(altU) * 3.28084); + } + if (nodeNum == ownNode) { + char buf[64]; + int latSeconds = (int)round(lat * 1e-7 * 3600); + int latDegrees = latSeconds / 3600; + latSeconds = abs(latSeconds % 3600); + int latMinutes = latSeconds / 60; + latSeconds %= 60; + char latLetter = (lat > 0) ? 'N' : 'S'; + + int lonSeconds = (int)round(lon * 1e-7 * 3600); + int lonDegrees = lonSeconds / 3600; + lonSeconds = abs(lonSeconds % 3600); + int lonMinutes = lonSeconds / 60; + lonSeconds %= 60; + char lonLetter = (lon > 0) ? 'E' : 'W'; + + if (sats) + sprintf(buf, "%c%02i° %2i'%02i\" %u sats\n%c%02i° %2i'%02i\" %d%s", latLetter, abs(latDegrees), + latMinutes, latSeconds, sats, lonLetter, abs(lonDegrees), lonMinutes, lonSeconds, altU, units); + else + sprintf(buf, "%c%02i° %2i'%02i\"\n%c%02i° %2i'%02i\" %d%s", latLetter, abs(latDegrees), latMinutes, + latSeconds, lonLetter, abs(lonDegrees), lonMinutes, lonSeconds, altU, units); + + lv_label_set_text(objects.home_location_label, buf); + + if (lat != 0 && lon != 0) { + hasPosition = true; + myLatitude = lat; + myLongitude = lon; + + // go through existing node list and update distance + // TODO: need incremental update!? + for (auto &it : nodes) { + if (it.first != ownNode) { + int32_t nlat = (long)it.second->LV_OBJ_IDX(node_pos1_idx)->user_data; + int32_t nlon = (long)it.second->LV_OBJ_IDX(node_pos2_idx)->user_data; + if (nlat != 0 && nlon != 0) { + updateDistance(it.first, nlat, nlon); + } + } + } + // update own location on map + if (map) + map->setGpsPosition(lat * 1e-7, lon * 1e-7); + } + } else { + if (lat != 0 && lon != 0) { + if (hasPosition) { + updateDistance(nodeNum, lat, lon); } + addOrUpdateMap(nodeNum, lat, lon); } } - // update own location on map - if (map) - map->setGpsPosition(lat * 1e-7, lon * 1e-7); - } - } else { - if (lat != 0 && lon != 0) { - if (hasPosition) { - updateDistance(nodeNum, lat, lon); + + if (lat != 0 && lon != 0) { + char buf[32]; + sprintf(buf, "%.5f %.5f", lat * 1e-7, lon * 1e-7); + lv_obj_t *panel = nodes[nodeNum]; + lv_label_set_text(panel->LV_OBJ_IDX(node_pos1_idx), buf); + if (sats) + sprintf(buf, "%d%s MSL %u sats", altU, units, sats); + sprintf(buf, "%d%s MSL", altU, units); + lv_label_set_text(panel->LV_OBJ_IDX(node_pos2_idx), buf); + // store lat/lon in user_data, because we need these values later to calculate the distance to us + panel->LV_OBJ_IDX(node_pos1_idx)->user_data = (void *)lat; + panel->LV_OBJ_IDX(node_pos2_idx)->user_data = (void *)lon; + lv_obj_remove_flag(panel->LV_OBJ_IDX(node_pos1_idx), LV_OBJ_FLAG_HIDDEN); + lv_obj_remove_flag(panel->LV_OBJ_IDX(node_pos2_idx), LV_OBJ_FLAG_HIDDEN); } - addOrUpdateMap(nodeNum, lat, lon); + + applyNodesFilter(nodeNum); } - } - if (lat != 0 && lon != 0) { - char buf[32]; - sprintf(buf, "%.5f %.5f", lat * 1e-7, lon * 1e-7); - lv_obj_t *panel = nodes[nodeNum]; - lv_label_set_text(panel->LV_OBJ_IDX(node_pos1_idx), buf); - if (sats) - sprintf(buf, "%d%s MSL %u sats", altU, units, sats); - sprintf(buf, "%d%s MSL", altU, units); - lv_label_set_text(panel->LV_OBJ_IDX(node_pos2_idx), buf); - // store lat/lon in user_data, because we need these values later to calculate the distance to us - panel->LV_OBJ_IDX(node_pos1_idx)->user_data = (void *)lat; - panel->LV_OBJ_IDX(node_pos2_idx)->user_data = (void *)lon; - lv_obj_remove_flag(panel->LV_OBJ_IDX(node_pos1_idx), LV_OBJ_FLAG_HIDDEN); - lv_obj_remove_flag(panel->LV_OBJ_IDX(node_pos2_idx), LV_OBJ_FLAG_HIDDEN); - } + void TFTView_320x240::updateDistance(uint32_t nodeNum, int32_t lat, int32_t lon) + { + // if we know our position then calculate (simple) distance to other node in km + float dx = 71.5 * 1e-7 * (myLongitude - lon); + float dy = 111.3 * 1e-7 * (myLatitude - lat); + float dist = sqrt(dx * dx + dy * dy); - applyNodesFilter(nodeNum); -} + // add distance to user short field + char buf[32]; + char *userData = (char *)&(nodes[nodeNum]->LV_OBJ_IDX(node_lbs_idx)->user_data); + buf[0] = userData[0]; + buf[1] = userData[1]; + buf[2] = userData[2]; + buf[3] = userData[3]; + buf[4] = '\n'; + + if (db.config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_METRIC) { + if (dist > 1.0) + sprintf(&buf[5], "%.1f km ", dist); + else + sprintf(&buf[5], "%d m ", (uint32_t)round(dist * 1000)); + } else { + if (dist > 0.1) + sprintf(&buf[5], "%.1f mi ", round(dist * 0.621371)); + else + sprintf(&buf[5], "%d ft ", uint32_t(dist * 3280.84)); + } + // we used the userShort label to add the distance, so re-arrange a bit the position + lv_obj_t *userShort = nodes[nodeNum]->LV_OBJ_IDX(node_lbs_idx); + lv_label_set_text(userShort, buf); + lv_obj_set_pos(userShort, 30, -1); + } + + /** + * @brief Update battery level and air utilisation + * + * @param nodeNum + * @param bat_level + * @param voltage + * @param chUtil + * @param airUtil + */ + void TFTView_320x240::updateMetrics(uint32_t nodeNum, uint32_t bat_level, float voltage, float chUtil, float airUtil) + { + auto it = nodes.find(nodeNum); + if (it != nodes.end()) { + char buf[48]; + if (it->first == ownNode) { + sprintf(buf, _("Util %0.1f%% Air %0.1f%%"), chUtil, airUtil); + lv_label_set_text(it->second->LV_OBJ_IDX(node_sig_idx), buf); + + // update battery percentage and symbol + if (bat_level != 0 || voltage != 0) { + uint32_t shown_level = std::min(bat_level, (uint32_t)100); + sprintf(buf, "%d%%", shown_level); + bool alert = false; + + BatteryLevel level; + BatteryLevel::Status status = level.calcStatus(bat_level, voltage); + switch (status) { + case BatteryLevel::Plugged: + lv_obj_set_style_bg_image_src(objects.battery_image, &img_battery_plug_image, + LV_PART_MAIN | LV_STATE_DEFAULT); + if (shown_level == 100) + buf[0] = '\0'; + break; + case BatteryLevel::Charging: + lv_obj_set_style_bg_image_src(objects.battery_image, &img_battery_bolt_image, + LV_PART_MAIN | LV_STATE_DEFAULT); + break; + case BatteryLevel::Full: + lv_obj_set_style_bg_image_src(objects.battery_image, &img_battery_full_image, + LV_PART_MAIN | LV_STATE_DEFAULT); + break; + case BatteryLevel::Mid: + lv_obj_set_style_bg_image_src(objects.battery_image, &img_battery_mid_image, + LV_PART_MAIN | LV_STATE_DEFAULT); + break; + case BatteryLevel::Low: + lv_obj_set_style_bg_image_src(objects.battery_image, &img_battery_low_image, + LV_PART_MAIN | LV_STATE_DEFAULT); + break; + case BatteryLevel::Empty: + lv_obj_set_style_bg_image_src(objects.battery_image, &img_battery_empty_image, + LV_PART_MAIN | LV_STATE_DEFAULT); + break; + case BatteryLevel::Warn: + lv_obj_set_style_bg_image_src(objects.battery_image, &img_battery_empty_warn_image, + LV_PART_MAIN | LV_STATE_DEFAULT); + buf[0] = '\0'; + alert = true; + break; + default: + ILOG_ERROR("unhandled battery level %d", status); + break; + } + Themes::recolorTopLabel(objects.battery_percentage_label, alert); + lv_obj_set_style_bg_image_recolor_opa(objects.battery_image, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_label_set_text(objects.battery_percentage_label, buf); + } + } -void TFTView_320x240::updateDistance(uint32_t nodeNum, int32_t lat, int32_t lon) -{ - // if we know our position then calculate (simple) distance to other node in km - float dx = 71.5 * 1e-7 * (myLongitude - lon); - float dy = 111.3 * 1e-7 * (myLatitude - lat); - float dist = sqrt(dx * dx + dy * dy); - - // add distance to user short field - char buf[32]; - char *userData = (char *)&(nodes[nodeNum]->LV_OBJ_IDX(node_lbs_idx)->user_data); - buf[0] = userData[0]; - buf[1] = userData[1]; - buf[2] = userData[2]; - buf[3] = userData[3]; - buf[4] = '\n'; - - if (db.config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_METRIC) { - if (dist > 1.0) - sprintf(&buf[5], "%.1f km ", dist); - else - sprintf(&buf[5], "%d m ", (uint32_t)round(dist * 1000)); - } else { - if (dist > 0.1) - sprintf(&buf[5], "%.1f mi ", round(dist * 0.621371)); - else - sprintf(&buf[5], "%d ft ", uint32_t(dist * 3280.84)); - } - // we used the userShort label to add the distance, so re-arrange a bit the position - lv_obj_t *userShort = nodes[nodeNum]->LV_OBJ_IDX(node_lbs_idx); - lv_label_set_text(userShort, buf); - lv_obj_set_pos(userShort, 30, -1); -} + if (bat_level != 0 || voltage != 0) { + bat_level = std::min(bat_level, (uint32_t)100); + sprintf(buf, "%d%% %0.2fV", bat_level, voltage); + lv_label_set_text(it->second->LV_OBJ_IDX(node_bat_idx), buf); + lv_obj_remove_flag(it->second->LV_OBJ_IDX(node_bat_idx), LV_OBJ_FLAG_HIDDEN); + } + } + } -/** - * @brief Update battery level and air utilisation - * - * @param nodeNum - * @param bat_level - * @param voltage - * @param chUtil - * @param airUtil - */ -void TFTView_320x240::updateMetrics(uint32_t nodeNum, uint32_t bat_level, float voltage, float chUtil, float airUtil) -{ - auto it = nodes.find(nodeNum); - if (it != nodes.end()) { - char buf[48]; - if (it->first == ownNode) { - sprintf(buf, _("Util %0.1f%% Air %0.1f%%"), chUtil, airUtil); - lv_label_set_text(it->second->LV_OBJ_IDX(node_sig_idx), buf); - - // update battery percentage and symbol - if (bat_level != 0 || voltage != 0) { - uint32_t shown_level = std::min(bat_level, (uint32_t)100); - sprintf(buf, "%d%%", shown_level); - bool alert = false; - - BatteryLevel level; - BatteryLevel::Status status = level.calcStatus(bat_level, voltage); - switch (status) { - case BatteryLevel::Plugged: - lv_obj_set_style_bg_image_src(objects.battery_image, &img_battery_plug_image, - LV_PART_MAIN | LV_STATE_DEFAULT); - if (shown_level == 100) - buf[0] = '\0'; - break; - case BatteryLevel::Charging: - lv_obj_set_style_bg_image_src(objects.battery_image, &img_battery_bolt_image, - LV_PART_MAIN | LV_STATE_DEFAULT); - break; - case BatteryLevel::Full: - lv_obj_set_style_bg_image_src(objects.battery_image, &img_battery_full_image, - LV_PART_MAIN | LV_STATE_DEFAULT); - break; - case BatteryLevel::Mid: - lv_obj_set_style_bg_image_src(objects.battery_image, &img_battery_mid_image, LV_PART_MAIN | LV_STATE_DEFAULT); - break; - case BatteryLevel::Low: - lv_obj_set_style_bg_image_src(objects.battery_image, &img_battery_low_image, LV_PART_MAIN | LV_STATE_DEFAULT); - break; - case BatteryLevel::Empty: - lv_obj_set_style_bg_image_src(objects.battery_image, &img_battery_empty_image, - LV_PART_MAIN | LV_STATE_DEFAULT); - break; - case BatteryLevel::Warn: - lv_obj_set_style_bg_image_src(objects.battery_image, &img_battery_empty_warn_image, - LV_PART_MAIN | LV_STATE_DEFAULT); - buf[0] = '\0'; - alert = true; - break; - default: - ILOG_ERROR("unhandled battery level %d", status); - break; + void TFTView_320x240::updateEnvironmentMetrics(uint32_t nodeNum, const meshtastic_EnvironmentMetrics &metrics) + { + auto it = nodes.find(nodeNum); + if (it != nodes.end()) { + char buf[50]; + if (db.config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_METRIC) { + if ((int)metrics.relative_humidity > 0) { + sprintf(buf, "%2.1f°C %d%% %3.1fhPa", metrics.temperature, (int)metrics.relative_humidity, + metrics.barometric_pressure); + } else { + sprintf(buf, "%2.1f°C %3.1fhPa", metrics.temperature, metrics.barometric_pressure); + } + } else { + if ((int)metrics.relative_humidity > 0) { + sprintf(buf, "%2.1f°F %d%% %3.1finHg", metrics.temperature * 9 / 5 + 32, (int)metrics.relative_humidity, + metrics.barometric_pressure / 33.86f); + } else { + sprintf(buf, "%2.1f°F %3.1finHg", metrics.temperature * 9 / 5 + 32, metrics.barometric_pressure / 33.86f); + } + } + lv_label_set_text(it->second->LV_OBJ_IDX(node_tm1_idx), buf); + lv_obj_remove_flag(it->second->LV_OBJ_IDX(node_tm1_idx), LV_OBJ_FLAG_HIDDEN); + + if (metrics.iaq > 0 && metrics.iaq < 1000) { + sprintf(buf, "IAQ: %d %.1fV %.1fmA", metrics.iaq, metrics.voltage, metrics.current); + lv_label_set_text(it->second->LV_OBJ_IDX(node_tm2_idx), buf); + it->second->LV_OBJ_IDX(node_tm2_idx)->user_data = (void *)(uint32_t)metrics.iaq; + lv_obj_remove_flag(it->second->LV_OBJ_IDX(node_tm2_idx), LV_OBJ_FLAG_HIDDEN); } - Themes::recolorTopLabel(objects.battery_percentage_label, alert); - lv_obj_set_style_bg_image_recolor_opa(objects.battery_image, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_label_set_text(objects.battery_percentage_label, buf); + applyNodesFilter(nodeNum); } } - if (bat_level != 0 || voltage != 0) { - bat_level = std::min(bat_level, (uint32_t)100); - sprintf(buf, "%d%% %0.2fV", bat_level, voltage); - lv_label_set_text(it->second->LV_OBJ_IDX(node_bat_idx), buf); - lv_obj_remove_flag(it->second->LV_OBJ_IDX(node_bat_idx), LV_OBJ_FLAG_HIDDEN); + void TFTView_320x240::updateAirQualityMetrics(uint32_t nodeNum, const meshtastic_AirQualityMetrics &metrics) + { + auto it = nodes.find(nodeNum); + if (it != nodes.end() && it->first != ownNode) { + // TODO + // char buf[32]; + // sprintf(buf, "%d %d", metrics.particles_03um, metrics.pm100_environmental); + // lv_label_set_text(it->second->LV_OBJ_IDX(node_tm2_idx), buf); + } } - } -} -void TFTView_320x240::updateEnvironmentMetrics(uint32_t nodeNum, const meshtastic_EnvironmentMetrics &metrics) -{ - auto it = nodes.find(nodeNum); - if (it != nodes.end()) { - char buf[50]; - if (db.config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_METRIC) { - if ((int)metrics.relative_humidity > 0) { - sprintf(buf, "%2.1f°C %d%% %3.1fhPa", metrics.temperature, (int)metrics.relative_humidity, - metrics.barometric_pressure); - } else { - sprintf(buf, "%2.1f°C %3.1fhPa", metrics.temperature, metrics.barometric_pressure); - } - } else { - if ((int)metrics.relative_humidity > 0) { - sprintf(buf, "%2.1f°F %d%% %3.1finHg", metrics.temperature * 9 / 5 + 32, (int)metrics.relative_humidity, - metrics.barometric_pressure / 33.86f); - } else { - sprintf(buf, "%2.1f°F %3.1finHg", metrics.temperature * 9 / 5 + 32, metrics.barometric_pressure / 33.86f); + void TFTView_320x240::updatePowerMetrics(uint32_t nodeNum, const meshtastic_PowerMetrics &metrics) + { + auto it = nodes.find(nodeNum); + if (it != nodes.end() && it->first != ownNode) { + // TODO + // char buf[32]; + // sprintf(buf, "%0.1fmA %0.2fV", metrics.ch1_current, metrics.ch1_voltage); + // lv_label_set_text(it->second->LV_OBJ_IDX(node_tm2_idx), buf); } } - lv_label_set_text(it->second->LV_OBJ_IDX(node_tm1_idx), buf); - lv_obj_remove_flag(it->second->LV_OBJ_IDX(node_tm1_idx), LV_OBJ_FLAG_HIDDEN); - if (metrics.iaq > 0 && metrics.iaq < 1000) { - sprintf(buf, "IAQ: %d %.1fV %.1fmA", metrics.iaq, metrics.voltage, metrics.current); - lv_label_set_text(it->second->LV_OBJ_IDX(node_tm2_idx), buf); - it->second->LV_OBJ_IDX(node_tm2_idx)->user_data = (void *)(uint32_t)metrics.iaq; - lv_obj_remove_flag(it->second->LV_OBJ_IDX(node_tm2_idx), LV_OBJ_FLAG_HIDDEN); + /** + * update signal strength for direct neighbors + */ + void TFTView_320x240::updateSignalStrength(uint32_t nodeNum, int32_t rssi, float snr) + { + if (nodeNum != ownNode) { + auto it = nodes.find(nodeNum); + if (it != nodes.end()) { + char buf[32]; + if (rssi == 0 && snr == 0.0) { + buf[0] = '\0'; + } else { + sprintf(buf, "rssi: %d snr: %.1f", rssi, snr); + } + lv_label_set_text(it->second->LV_OBJ_IDX(node_sig_idx), buf); + it->second->LV_OBJ_IDX(node_sig_idx)->user_data = 0; + lv_obj_remove_flag(it->second->LV_OBJ_IDX(node_sig_idx), LV_OBJ_FLAG_HIDDEN); + } + } } - applyNodesFilter(nodeNum); - } -} - -void TFTView_320x240::updateAirQualityMetrics(uint32_t nodeNum, const meshtastic_AirQualityMetrics &metrics) -{ - auto it = nodes.find(nodeNum); - if (it != nodes.end() && it->first != ownNode) { - // TODO - // char buf[32]; - // sprintf(buf, "%d %d", metrics.particles_03um, metrics.pm100_environmental); - // lv_label_set_text(it->second->LV_OBJ_IDX(node_tm2_idx), buf); - } -} - -void TFTView_320x240::updatePowerMetrics(uint32_t nodeNum, const meshtastic_PowerMetrics &metrics) -{ - auto it = nodes.find(nodeNum); - if (it != nodes.end() && it->first != ownNode) { - // TODO - // char buf[32]; - // sprintf(buf, "%0.1fmA %0.2fV", metrics.ch1_current, metrics.ch1_voltage); - // lv_label_set_text(it->second->LV_OBJ_IDX(node_tm2_idx), buf); - } -} -/** - * update signal strength for direct neighbors - */ -void TFTView_320x240::updateSignalStrength(uint32_t nodeNum, int32_t rssi, float snr) -{ - if (nodeNum != ownNode) { - auto it = nodes.find(nodeNum); - if (it != nodes.end()) { - char buf[32]; - if (rssi == 0 && snr == 0.0) { - buf[0] = '\0'; - } else { - sprintf(buf, "rssi: %d snr: %.1f", rssi, snr); + void TFTView_320x240::updateHopsAway(uint32_t nodeNum, uint8_t hopsAway) + { + if (nodeNum != ownNode) { + auto it = nodes.find(nodeNum); + if (it != nodes.end()) { + char buf[32]; + sprintf(buf, _("hops: %d"), (int)hopsAway); + lv_label_set_text(it->second->LV_OBJ_IDX(node_sig_idx), buf); + it->second->LV_OBJ_IDX(node_sig_idx)->user_data = (void *)(unsigned long)hopsAway; + lv_obj_remove_flag(it->second->LV_OBJ_IDX(node_sig_idx), LV_OBJ_FLAG_HIDDEN); + } } - lv_label_set_text(it->second->LV_OBJ_IDX(node_sig_idx), buf); - it->second->LV_OBJ_IDX(node_sig_idx)->user_data = 0; - lv_obj_remove_flag(it->second->LV_OBJ_IDX(node_sig_idx), LV_OBJ_FLAG_HIDDEN); } - } -} -void TFTView_320x240::updateHopsAway(uint32_t nodeNum, uint8_t hopsAway) -{ - if (nodeNum != ownNode) { - auto it = nodes.find(nodeNum); - if (it != nodes.end()) { - char buf[32]; - sprintf(buf, _("hops: %d"), (int)hopsAway); - lv_label_set_text(it->second->LV_OBJ_IDX(node_sig_idx), buf); - it->second->LV_OBJ_IDX(node_sig_idx)->user_data = (void *)(unsigned long)hopsAway; - lv_obj_remove_flag(it->second->LV_OBJ_IDX(node_sig_idx), LV_OBJ_FLAG_HIDDEN); - } - } -} + void TFTView_320x240::updateConnectionStatus(const meshtastic_DeviceConnectionStatus &status) + { + db.connectionStatus = status; + if (status.has_wifi) { + if (db.config.network.wifi_enabled || db.config.network.eth_enabled) { + if (status.wifi.has_status) { + char buf[20]; + uint32_t ip = status.wifi.status.ip_address; + sprintf(buf, "%d.%d.%d.%d", ip & 0xff, (ip & 0xff00) >> 8, (ip & 0xff0000) >> 16, + (ip & 0xff000000) >> 24); + lv_label_set_text(objects.home_wlan_label, buf); + Themes::recolorButton(objects.home_wlan_button, true); + Themes::recolorText(objects.home_wlan_label, true); + if (status.wifi.status.is_connected) { + lv_obj_set_style_bg_img_src(objects.home_wlan_button, &img_home_wlan_button_image, + LV_PART_MAIN | LV_STATE_DEFAULT); + } else { + lv_obj_set_style_bg_img_src(objects.home_wlan_button, &img_home_wlan_off_image, + LV_PART_MAIN | LV_STATE_DEFAULT); + } -void TFTView_320x240::updateConnectionStatus(const meshtastic_DeviceConnectionStatus &status) -{ - db.connectionStatus = status; - if (status.has_wifi) { - if (db.config.network.wifi_enabled || db.config.network.eth_enabled) { - if (status.wifi.has_status) { - char buf[20]; - uint32_t ip = status.wifi.status.ip_address; - sprintf(buf, "%d.%d.%d.%d", ip & 0xff, (ip & 0xff00) >> 8, (ip & 0xff0000) >> 16, (ip & 0xff000000) >> 24); - lv_label_set_text(objects.home_wlan_label, buf); - Themes::recolorButton(objects.home_wlan_button, true); - Themes::recolorText(objects.home_wlan_label, true); - if (status.wifi.status.is_connected) { - lv_obj_set_style_bg_img_src(objects.home_wlan_button, &img_home_wlan_button_image, - LV_PART_MAIN | LV_STATE_DEFAULT); + if (status.wifi.status.is_mqtt_connected) { + Themes::recolorButton(objects.home_mqtt_button, true, 255); + Themes::recolorText(objects.home_mqtt_label, true); + } else { + Themes::recolorButton(objects.home_mqtt_button, db.module_config.mqtt.enabled); + Themes::recolorText(objects.home_mqtt_label, false); + } + } } else { + Themes::recolorButton(objects.home_wlan_button, false); + Themes::recolorText(objects.home_wlan_label, false); + if (status.wifi.status.is_mqtt_connected) { + Themes::recolorButton(objects.home_mqtt_button, true, 255); + Themes::recolorText(objects.home_mqtt_label, true); + } else { + Themes::recolorButton(objects.home_mqtt_button, db.module_config.mqtt.enabled, 100); + Themes::recolorText(objects.home_mqtt_label, false); + } lv_obj_set_style_bg_img_src(objects.home_wlan_button, &img_home_wlan_off_image, LV_PART_MAIN | LV_STATE_DEFAULT); } + } else { + lv_obj_add_flag(objects.home_wlan_label, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(objects.home_wlan_button, LV_OBJ_FLAG_HIDDEN); + } - if (status.wifi.status.is_mqtt_connected) { - Themes::recolorButton(objects.home_mqtt_button, true, 255); - Themes::recolorText(objects.home_mqtt_label, true); + if (status.has_bluetooth) { + if (db.config.bluetooth.enabled) { + if (status.bluetooth.is_connected) { + char buf[20]; + uint32_t mac = ownNode; + lv_obj_set_style_text_color(objects.home_bluetooth_label, colorLightGray, + LV_PART_MAIN | LV_STATE_DEFAULT); + sprintf(buf, "??:??:%02x:%02x:%02x:%02x", mac & 0xff, (mac & 0xff00) >> 8, (mac & 0xff0000) >> 16, + (mac & 0xff000000) >> 24); + lv_label_set_text(objects.home_bluetooth_label, buf); + lv_obj_set_style_bg_opa(objects.home_bluetooth_button, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_img_src(objects.home_bluetooth_button, &img_home_bluetooth_on_button_image, + LV_PART_MAIN | LV_STATE_DEFAULT); + } else { + lv_obj_set_style_text_color(objects.home_bluetooth_label, colorMidGray, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_img_src(objects.home_bluetooth_button, &img_home_bluetooth_on_button_image, + LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_img_recolor_opa(objects.home_bluetooth_button, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + } } else { - Themes::recolorButton(objects.home_mqtt_button, db.module_config.mqtt.enabled); - Themes::recolorText(objects.home_mqtt_label, false); + lv_obj_set_style_text_color(objects.home_bluetooth_label, colorMidGray, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_img_src(objects.home_bluetooth_button, &img_home_bluetooth_off_button_image, + LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_img_recolor_opa(objects.home_bluetooth_button, 255, LV_PART_MAIN | LV_STATE_DEFAULT); } - } - } else { - Themes::recolorButton(objects.home_wlan_button, false); - Themes::recolorText(objects.home_wlan_label, false); - if (status.wifi.status.is_mqtt_connected) { - Themes::recolorButton(objects.home_mqtt_button, true, 255); - Themes::recolorText(objects.home_mqtt_label, true); } else { - Themes::recolorButton(objects.home_mqtt_button, db.module_config.mqtt.enabled, 100); - Themes::recolorText(objects.home_mqtt_label, false); + lv_obj_add_flag(objects.home_bluetooth_label, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(objects.home_bluetooth_button, LV_OBJ_FLAG_HIDDEN); } - lv_obj_set_style_bg_img_src(objects.home_wlan_button, &img_home_wlan_off_image, LV_PART_MAIN | LV_STATE_DEFAULT); - } - } else { - lv_obj_add_flag(objects.home_wlan_label, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(objects.home_wlan_button, LV_OBJ_FLAG_HIDDEN); - } - if (status.has_bluetooth) { - if (db.config.bluetooth.enabled) { - if (status.bluetooth.is_connected) { - char buf[20]; - uint32_t mac = ownNode; - lv_obj_set_style_text_color(objects.home_bluetooth_label, colorLightGray, LV_PART_MAIN | LV_STATE_DEFAULT); - sprintf(buf, "??:??:%02x:%02x:%02x:%02x", mac & 0xff, (mac & 0xff00) >> 8, (mac & 0xff0000) >> 16, - (mac & 0xff000000) >> 24); - lv_label_set_text(objects.home_bluetooth_label, buf); - lv_obj_set_style_bg_opa(objects.home_bluetooth_button, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_img_src(objects.home_bluetooth_button, &img_home_bluetooth_on_button_image, - LV_PART_MAIN | LV_STATE_DEFAULT); + if (status.has_ethernet) { + if (status.ethernet.status.is_connected) { + char buf[20]; + uint32_t mac = ownNode; + sprintf(buf, "??:??:%02x:%02x:%02x:%02x", mac & 0xff000000, mac & 0xff0000, mac & 0xff00, mac & 0xff); + lv_label_set_text(objects.home_ethernet_label, buf); + lv_obj_set_style_text_color(objects.home_ethernet_label, colorLightGray, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(objects.home_ethernet_button, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + } else { + lv_obj_set_style_bg_img_recolor_opa(objects.home_ethernet_button, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_color(objects.home_ethernet_label, colorMidGray, LV_PART_MAIN | LV_STATE_DEFAULT); + } } else { - lv_obj_set_style_text_color(objects.home_bluetooth_label, colorMidGray, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_img_src(objects.home_bluetooth_button, &img_home_bluetooth_on_button_image, - LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_img_recolor_opa(objects.home_bluetooth_button, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_add_flag(objects.home_ethernet_label, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(objects.home_ethernet_button, LV_OBJ_FLAG_HIDDEN); } - } else { - lv_obj_set_style_text_color(objects.home_bluetooth_label, colorMidGray, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_img_src(objects.home_bluetooth_button, &img_home_bluetooth_off_button_image, - LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_img_recolor_opa(objects.home_bluetooth_button, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - } - } else { - lv_obj_add_flag(objects.home_bluetooth_label, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(objects.home_bluetooth_button, LV_OBJ_FLAG_HIDDEN); - } - - if (status.has_ethernet) { - if (status.ethernet.status.is_connected) { - char buf[20]; - uint32_t mac = ownNode; - sprintf(buf, "??:??:%02x:%02x:%02x:%02x", mac & 0xff000000, mac & 0xff0000, mac & 0xff00, mac & 0xff); - lv_label_set_text(objects.home_ethernet_label, buf); - lv_obj_set_style_text_color(objects.home_ethernet_label, colorLightGray, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(objects.home_ethernet_button, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - } else { - lv_obj_set_style_bg_img_recolor_opa(objects.home_ethernet_button, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_color(objects.home_ethernet_label, colorMidGray, LV_PART_MAIN | LV_STATE_DEFAULT); } - } else { - lv_obj_add_flag(objects.home_ethernet_label, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(objects.home_ethernet_button, LV_OBJ_FLAG_HIDDEN); - } -} -// ResponseHandler callbacks + // ResponseHandler callbacks -void TFTView_320x240::onTextMessageCallback(const ResponseHandler::Request &req, ResponseHandler::EventType evt, int32_t result) -{ - ILOG_DEBUG("onTextMessageCallback: %d %d", evt, result); - if (evt == ResponseHandler::found) { - handleTextMessageResponse((unsigned long)req.cookie, req.id, false, result); - } else if (evt == ResponseHandler::removed) { - handleTextMessageResponse((unsigned long)req.cookie, req.id, true, result); - } else { - ILOG_DEBUG("onTextMessageCallback: timeout!"); - } -} + void TFTView_320x240::onTextMessageCallback(const ResponseHandler::Request &req, ResponseHandler::EventType evt, + int32_t result) + { + ILOG_DEBUG("onTextMessageCallback: %d %d", evt, result); + if (evt == ResponseHandler::found) { + handleTextMessageResponse((unsigned long)req.cookie, req.id, false, result); + } else if (evt == ResponseHandler::removed) { + handleTextMessageResponse((unsigned long)req.cookie, req.id, true, result); + } else { + ILOG_DEBUG("onTextMessageCallback: timeout!"); + } + } -void TFTView_320x240::onPositionCallback(const ResponseHandler::Request &req, ResponseHandler::EventType evt, int32_t) {} + void TFTView_320x240::onPositionCallback(const ResponseHandler::Request &req, ResponseHandler::EventType evt, int32_t) {} -void TFTView_320x240::onTracerouteCallback(const ResponseHandler::Request &req, ResponseHandler::EventType evt, int32_t) {} + void TFTView_320x240::onTracerouteCallback(const ResponseHandler::Request &req, ResponseHandler::EventType evt, int32_t) + { + } -/** - * handle response from routing - */ -void TFTView_320x240::handleResponse(uint32_t from, const uint32_t id, const meshtastic_Routing &routing, - const meshtastic_MeshPacket &p) -{ - ResponseHandler::Request req{}; - bool ack = false; - if (from == ownNode) { - req = requests.findRequest(id); - } else { - req = requests.removeRequest(id); - ack = true; - } + /** + * handle response from routing + */ + void TFTView_320x240::handleResponse(uint32_t from, const uint32_t id, const meshtastic_Routing &routing, + const meshtastic_MeshPacket &p) + { + ResponseHandler::Request req{}; + bool ack = false; + if (from == ownNode) { + req = requests.findRequest(id); + } else { + req = requests.removeRequest(id); + ack = true; + } - if (req.type == ResponseHandler::noRequest) { - ILOG_WARN("request id 0x%08x not valid (anymore)", id); - } else { - ILOG_DEBUG("handleResponse request id 0x%08x", id); - } - ILOG_DEBUG("routing tag variant: %d, error: %d", routing.which_variant, routing.error_reason); - switch (routing.which_variant) { - case meshtastic_Routing_error_reason_tag: { - if (routing.error_reason == meshtastic_Routing_Error_NONE) { - if (req.type == ResponseHandler::TraceRouteRequest) { - handleTraceRouteResponse(routing); - } else if (req.type == ResponseHandler::TextMessageRequest) { - handleTextMessageResponse((unsigned long)req.cookie, id, ack, false); - } else if (req.type == ResponseHandler::PositionRequest) { - handlePositionResponse(from, id, p.rx_rssi, p.rx_snr, p.hop_limit == p.hop_start); - } - } else if (routing.error_reason == meshtastic_Routing_Error_MAX_RETRANSMIT) { - ResponseHandler::Request req = requests.removeRequest(id); - if (req.type == ResponseHandler::TraceRouteRequest) { - handleTraceRouteResponse(routing); - } else if (req.type == ResponseHandler::TextMessageRequest) { - handleTextMessageResponse((unsigned long)req.cookie, id, ack, true); - } - } else if (routing.error_reason == meshtastic_Routing_Error_NO_RESPONSE) { - if (req.type == ResponseHandler::PositionRequest) { - handlePositionResponse(from, id, p.rx_rssi, p.rx_snr, p.hop_limit == p.hop_start); - } - } else if (routing.error_reason == meshtastic_Routing_Error_NO_CHANNEL || - routing.error_reason == meshtastic_Routing_Error_PKI_UNKNOWN_PUBKEY) { - if (req.type == ResponseHandler::TextMessageRequest) { - handleTextMessageResponse((unsigned long)req.cookie, id, ack, true); - // we probably have a wrong key; mark it as bad and don't use in future - if ((unsigned long)nodes[from]->LV_OBJ_IDX(node_bat_idx)->user_data == 1) { - ILOG_DEBUG("public key mismatch"); - nodes[from]->LV_OBJ_IDX(node_bat_idx)->user_data = (void *)2; - lv_obj_set_style_border_color(nodes[from]->LV_OBJ_IDX(node_img_idx), colorRed, - LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_image_src(objects.top_messages_node_image, &img_lock_slash_image, - LV_PART_MAIN | LV_STATE_DEFAULT); + if (req.type == ResponseHandler::noRequest) { + ILOG_WARN("request id 0x%08x not valid (anymore)", id); + } else { + ILOG_DEBUG("handleResponse request id 0x%08x", id); + } + ILOG_DEBUG("routing tag variant: %d, error: %d", routing.which_variant, routing.error_reason); + switch (routing.which_variant) { + case meshtastic_Routing_error_reason_tag: { + if (routing.error_reason == meshtastic_Routing_Error_NONE) { + if (req.type == ResponseHandler::TraceRouteRequest) { + handleTraceRouteResponse(routing); + } else if (req.type == ResponseHandler::TextMessageRequest) { + handleTextMessageResponse((unsigned long)req.cookie, id, ack, false); + } else if (req.type == ResponseHandler::PositionRequest) { + handlePositionResponse(from, id, p.rx_rssi, p.rx_snr, p.hop_limit == p.hop_start); + } + } else if (routing.error_reason == meshtastic_Routing_Error_MAX_RETRANSMIT) { + ResponseHandler::Request req = requests.removeRequest(id); + if (req.type == ResponseHandler::TraceRouteRequest) { + handleTraceRouteResponse(routing); + } else if (req.type == ResponseHandler::TextMessageRequest) { + handleTextMessageResponse((unsigned long)req.cookie, id, ack, true); + } + } else if (routing.error_reason == meshtastic_Routing_Error_NO_RESPONSE) { + if (req.type == ResponseHandler::PositionRequest) { + handlePositionResponse(from, id, p.rx_rssi, p.rx_snr, p.hop_limit == p.hop_start); + } + } else if (routing.error_reason == meshtastic_Routing_Error_NO_CHANNEL || + routing.error_reason == meshtastic_Routing_Error_PKI_UNKNOWN_PUBKEY) { + if (req.type == ResponseHandler::TextMessageRequest) { + handleTextMessageResponse((unsigned long)req.cookie, id, ack, true); + // we probably have a wrong key; mark it as bad and don't use in future + if ((unsigned long)nodes[from]->LV_OBJ_IDX(node_bat_idx)->user_data == 1) { + ILOG_DEBUG("public key mismatch"); + nodes[from]->LV_OBJ_IDX(node_bat_idx)->user_data = (void *)2; + lv_obj_set_style_border_color(nodes[from]->LV_OBJ_IDX(node_img_idx), colorRed, + LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_image_src(objects.top_messages_node_image, &img_lock_slash_image, + LV_PART_MAIN | LV_STATE_DEFAULT); + } + } + } else { + ILOG_DEBUG("got Routing_Error %d", routing.error_reason); } + break; + } + case meshtastic_Routing_route_request_tag: { + ILOG_ERROR("got meshtastic_Routing_route_request_tag"); + break; + } + case meshtastic_Routing_route_reply_tag: { + ILOG_DEBUG("got meshtastic_Routing_route_reply_tag"); + handleResponse(from, id, routing.route_reply); + break; + } + default: + ILOG_ERROR("unhandled meshtastic_Routing tag"); + break; } - } else { - ILOG_DEBUG("got Routing_Error %d", routing.error_reason); } - break; - } - case meshtastic_Routing_route_request_tag: { - ILOG_ERROR("got meshtastic_Routing_route_request_tag"); - break; - } - case meshtastic_Routing_route_reply_tag: { - ILOG_DEBUG("got meshtastic_Routing_route_reply_tag"); - handleResponse(from, id, routing.route_reply); - break; - } - default: - ILOG_ERROR("unhandled meshtastic_Routing tag"); - break; - } -} - -/** - * Signal scanner - */ -void TFTView_320x240::scanSignal(uint32_t scanNo) -{ - if (scans == 1 && spinnerButton) { - lv_label_set_text(objects.signal_scanner_start_label, _("Start")); - removeSpinner(); - } else { - uint32_t requestId; - uint32_t to = currentNode; - uint8_t ch = (uint8_t)(unsigned long)currentPanel->user_data; - requestId = requests.addRequest(to, ResponseHandler::PositionRequest, (void *)to); - controller->requestPosition(to, ch, requestId); - objects.signal_scanner_panel->user_data = (void *)requestId; - } -} -void TFTView_320x240::handlePositionResponse(uint32_t from, uint32_t request_id, int32_t rx_rssi, float rx_snr, bool isNeighbor) -{ - if (request_id == (unsigned long)objects.signal_scanner_panel->user_data) { - requests.removeRequest(request_id); + /** + * Signal scanner + */ + void TFTView_320x240::scanSignal(uint32_t scanNo) + { + if (scans == 1 && spinnerButton) { + lv_label_set_text(objects.signal_scanner_start_label, _("Start")); + removeSpinner(); + } else { + uint32_t requestId; + uint32_t to = currentNode; + uint8_t ch = (uint8_t)(unsigned long)currentPanel->user_data; + requestId = requests.addRequest(to, ResponseHandler::PositionRequest, (void *)to); + controller->requestPosition(to, ch, requestId); + objects.signal_scanner_panel->user_data = (void *)requestId; + } + } - if (from == currentNode && isNeighbor) { - char buf[20]; - sprintf(buf, "SNR\n%.1f", rx_snr); - lv_label_set_text(objects.signal_scanner_snr_label, buf); - sprintf(buf, "RSSI\n%d", rx_rssi); - lv_label_set_text(objects.signal_scanner_rssi_label, buf); - lv_slider_set_value(objects.snr_slider, rx_snr, LV_ANIM_ON); - lv_slider_set_value(objects.rssi_slider, rx_rssi, LV_ANIM_ON); - sprintf(buf, "%d%%", signalStrength2Percent(rx_rssi, rx_snr)); - lv_label_set_text(objects.signal_scanner_start_label, buf); + void TFTView_320x240::handlePositionResponse(uint32_t from, uint32_t request_id, int32_t rx_rssi, float rx_snr, + bool isNeighbor) + { + if (request_id == (unsigned long)objects.signal_scanner_panel->user_data) { + requests.removeRequest(request_id); + + if (from == currentNode && isNeighbor) { + char buf[20]; + sprintf(buf, "SNR\n%.1f", rx_snr); + lv_label_set_text(objects.signal_scanner_snr_label, buf); + sprintf(buf, "RSSI\n%d", rx_rssi); + lv_label_set_text(objects.signal_scanner_rssi_label, buf); + lv_slider_set_value(objects.snr_slider, rx_snr, LV_ANIM_ON); + lv_slider_set_value(objects.rssi_slider, rx_rssi, LV_ANIM_ON); + sprintf(buf, "%d%%", signalStrength2Percent(rx_rssi, rx_snr)); + lv_label_set_text(objects.signal_scanner_start_label, buf); + } + } else { + ILOG_DEBUG("handlePositionResponse: drop reply with not matching request 0x%08x", request_id); + } } - } else { - ILOG_DEBUG("handlePositionResponse: drop reply with not matching request 0x%08x", request_id); - } -} -/** - * Trace Route: handle ack or timeout - */ -void TFTView_320x240::handleTraceRouteResponse(const meshtastic_Routing &routing) -{ - ILOG_DEBUG("handleTraceRouteResponse: route has %d hops", routing.route_reply.route_count); - if (routing.error_reason != meshtastic_Routing_Error_NONE) { - lv_label_set_text(objects.trace_route_start_label, _("Start")); - removeSpinner(); - } else { - // we got a first ACK to our route request - if (spinnerButton) { - lv_obj_set_style_outline_color(objects.trace_route_start_button, lv_color_hex(0xDBD251), - LV_PART_MAIN | LV_STATE_DEFAULT); + /** + * Trace Route: handle ack or timeout + */ + void TFTView_320x240::handleTraceRouteResponse(const meshtastic_Routing &routing) + { + ILOG_DEBUG("handleTraceRouteResponse: route has %d hops", routing.route_reply.route_count); + if (routing.error_reason != meshtastic_Routing_Error_NONE) { + lv_label_set_text(objects.trace_route_start_label, _("Start")); + removeSpinner(); + } else { + // we got a first ACK to our route request + if (spinnerButton) { + lv_obj_set_style_outline_color(objects.trace_route_start_button, lv_color_hex(0xDBD251), + LV_PART_MAIN | LV_STATE_DEFAULT); + } + } } - } -} -void TFTView_320x240::handleResponse(uint32_t from, uint32_t id, const meshtastic_RouteDiscovery &route) -{ - ILOG_DEBUG("handleResponse: trace route has %d / %d hops", route.route_count, route.route_back_count); - lv_obj_add_flag(objects.start_button_panel, LV_OBJ_FLAG_HIDDEN); - lv_obj_clear_flag(objects.hop_routes_panel, LV_OBJ_FLAG_HIDDEN); + void TFTView_320x240::handleResponse(uint32_t from, uint32_t id, const meshtastic_RouteDiscovery &route) + { + ILOG_DEBUG("handleResponse: trace route has %d / %d hops", route.route_count, route.route_back_count); + lv_obj_add_flag(objects.start_button_panel, LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(objects.hop_routes_panel, LV_OBJ_FLAG_HIDDEN); - if (id && requests.findRequest(id).type == ResponseHandler::TraceRouteRequest) { - requests.removeRequest(id); - } + if (id && requests.findRequest(id).type == ResponseHandler::TraceRouteRequest) { + requests.removeRequest(id); + } - for (int i = route.route_count; i > 0; i--) { - addNodeToTraceRoute(route.route[i - 1], objects.route_towards_panel); - } + for (int i = route.route_count; i > 0; i--) { + addNodeToTraceRoute(route.route[i - 1], objects.route_towards_panel); + } - for (int i = 0; i < route.route_back_count; i++) { - addNodeToTraceRoute(route.route_back[i], objects.route_back_panel); - } + for (int i = 0; i < route.route_back_count; i++) { + addNodeToTraceRoute(route.route_back[i], objects.route_back_panel); + } - // route contains only intermediate nodes, so add our node - addNodeToTraceRoute(ownNode, objects.trace_route_panel); -} + // route contains only intermediate nodes, so add our node + addNodeToTraceRoute(ownNode, objects.trace_route_panel); + } -void TFTView_320x240::addNodeToTraceRoute(uint32_t nodeNum, lv_obj_t *panel) -{ - // check if node exists, and get its panel - lv_obj_t *nodePanel = nullptr; - auto it = nodes.find(nodeNum); - if (it != nodes.end()) { - nodePanel = it->second; - } - lv_obj_t *btn = lv_btn_create(panel); - // objects.trace_route_to_button = btn; - lv_obj_set_pos(btn, 0, 0); - lv_obj_set_size(btn, LV_PCT(100), 38); - add_style_settings_button_style(btn); - lv_obj_set_style_align(btn, LV_ALIGN_TOP_MID, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_top(btn, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_bottom(btn, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_radius(btn, 6, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_shadow_width(btn, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_shadow_ofs_y(btn, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(btn, 1, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(btn, colorMidGray, LV_PART_MAIN | LV_STATE_DEFAULT); - { + void TFTView_320x240::addNodeToTraceRoute(uint32_t nodeNum, lv_obj_t * panel) { - lv_obj_t *img = lv_img_create(btn); - if (nodePanel) { - setNodeImage(nodeNum, (MeshtasticView::eRole)(unsigned long)nodePanel->LV_OBJ_IDX(node_img_idx)->user_data, false, - img); - } else { - setNodeImage(0, eRole::unknown, false, img); + // check if node exists, and get its panel + lv_obj_t *nodePanel = nullptr; + auto it = nodes.find(nodeNum); + if (it != nodes.end()) { + nodePanel = it->second; } - lv_obj_set_pos(img, -5, 3); - lv_obj_set_size(img, 32, 32); - lv_obj_clear_flag(img, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_border_width(img, 3, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_image_recolor_opa(img, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_align(img, LV_ALIGN_TOP_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_radius(img, 6, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(img, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - } - { - // TraceRouteToButtonLabel - lv_obj_t *label = lv_label_create(btn); - lv_obj_set_pos(label, 35, 10); - lv_obj_set_size(label, LV_PCT(80), LV_SIZE_CONTENT); - lv_label_set_long_mode(label, LV_LABEL_LONG_SCROLL); - if (nodePanel) { - if (nodeNum != ownNode) { - lv_obj_add_event_cb(btn, ui_event_trace_route_node, LV_EVENT_CLICKED, nodePanel); - lv_label_set_text(label, lv_label_get_text(nodePanel->LV_OBJ_IDX(node_lbs_idx))); - if (strlen(lv_label_get_text(label)) >= 5) - lv_obj_set_pos(label, 35, -1); - } else { - lv_label_set_text(label, lv_label_get_text(nodePanel->LV_OBJ_IDX(node_lbl_idx))); + lv_obj_t *btn = lv_btn_create(panel); + // objects.trace_route_to_button = btn; + lv_obj_set_pos(btn, 0, 0); + lv_obj_set_size(btn, LV_PCT(100), 38); + add_style_settings_button_style(btn); + lv_obj_set_style_align(btn, LV_ALIGN_TOP_MID, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_top(btn, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_bottom(btn, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_radius(btn, 6, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_shadow_width(btn, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_shadow_ofs_y(btn, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_width(btn, 1, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_color(btn, colorMidGray, LV_PART_MAIN | LV_STATE_DEFAULT); + { + { + lv_obj_t *img = lv_img_create(btn); + if (nodePanel) { + setNodeImage(nodeNum, + (MeshtasticView::eRole)(unsigned long)nodePanel->LV_OBJ_IDX(node_img_idx)->user_data, false, + img); + } else { + setNodeImage(0, eRole::unknown, false, img); + } + lv_obj_set_pos(img, -5, 3); + lv_obj_set_size(img, 32, 32); + lv_obj_clear_flag(img, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_border_width(img, 3, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_image_recolor_opa(img, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_align(img, LV_ALIGN_TOP_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_radius(img, 6, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(img, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + } + { + // TraceRouteToButtonLabel + lv_obj_t *label = lv_label_create(btn); + lv_obj_set_pos(label, 35, 10); + lv_obj_set_size(label, LV_PCT(80), LV_SIZE_CONTENT); + lv_label_set_long_mode(label, LV_LABEL_LONG_SCROLL); + if (nodePanel) { + if (nodeNum != ownNode) { + lv_obj_add_event_cb(btn, ui_event_trace_route_node, LV_EVENT_CLICKED, nodePanel); + lv_label_set_text(label, lv_label_get_text(nodePanel->LV_OBJ_IDX(node_lbs_idx))); + if (strlen(lv_label_get_text(label)) >= 5) + lv_obj_set_pos(label, 35, -1); + } else { + lv_label_set_text(label, lv_label_get_text(nodePanel->LV_OBJ_IDX(node_lbl_idx))); + } + } else { + char buf[20]; + if (nodeNum != UINT32_MAX) { + lv_snprintf(buf, 16, "!%08x", nodeNum); + lv_label_set_text(label, buf); + } else + lv_label_set_text(label, _("unknown")); + } + lv_obj_set_style_align(label, LV_ALIGN_TOP_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); } - } else { - char buf[20]; - if (nodeNum != UINT32_MAX) { - lv_snprintf(buf, 16, "!%08x", nodeNum); - lv_label_set_text(label, buf); - } else - lv_label_set_text(label, _("unknown")); } - lv_obj_set_style_align(label, LV_ALIGN_TOP_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); } - } -} -/** - * @brief purge oldest node from node list (and all its memory) - * @param nodeNum node that is being added and already contained in nodes[], so don't remove it! - */ -void TFTView_320x240::purgeNode(uint32_t nodeNum) -{ - if (nodeCount <= 1) - return; + /** + * @brief purge oldest node from node list (and all its memory) + * @param nodeNum node that is being added and already contained in nodes[], so don't remove it! + */ + void TFTView_320x240::purgeNode(uint32_t nodeNum) + { + if (nodeCount <= 1) + return; - lv_obj_t **children = objects.nodes_panel->spec_attr->children; - int last = objects.nodes_panel->spec_attr->child_cnt - 1; - int i = last; + lv_obj_t **children = objects.nodes_panel->spec_attr->children; + int last = objects.nodes_panel->spec_attr->child_cnt - 1; + int i = last; #ifndef ALWAYS_PURGE_OLDEST_NODE - time_t curr_time; + time_t curr_time; #ifdef ARCH_PORTDUINO - time(&curr_time); + time(&curr_time); #else - curr_time = actTime; + curr_time = actTime; #endif - // prefer purging older unknown nodes first (but not the brand new ones) - while ((eRole)(long)(children[i]->LV_OBJ_IDX(node_img_idx)->user_data) != eRole::unknown || - curr_time < (time_t)(children[i]->LV_OBJ_IDX(node_lh_idx)->user_data) + 120 || - (unsigned long)(children[i]->LV_OBJ_IDX(node_lbl_idx)->user_data) == nodeNum || - chats.find((unsigned long)(children[i]->LV_OBJ_IDX(node_lbl_idx)->user_data)) != chats.end()) { - if (i < (last + 1) / 5) { // keep 80% named nodes and 20% unknown (not fresh) nodes - i = last; - break; - } - i--; - } + // prefer purging older unknown nodes first (but not the brand new ones) + while ((eRole)(long)(children[i]->LV_OBJ_IDX(node_img_idx)->user_data) != eRole::unknown || + curr_time < (time_t)(children[i]->LV_OBJ_IDX(node_lh_idx)->user_data) + 120 || + (unsigned long)(children[i]->LV_OBJ_IDX(node_lbl_idx)->user_data) == nodeNum || + chats.find((unsigned long)(children[i]->LV_OBJ_IDX(node_lbl_idx)->user_data)) != chats.end()) { + if (i < (last + 1) / 5) { // keep 80% named nodes and 20% unknown (not fresh) nodes + i = last; + break; + } + i--; + } #endif - lv_obj_t *p = children[i]; - uint32_t oldest = (unsigned long)(p->LV_OBJ_IDX(node_lbl_idx)->user_data); - uint32_t lastHeard = (unsigned long)p->LV_OBJ_IDX(node_lh_idx)->user_data; - if (lastHeard > 0 && (curtime - lastHeard <= secs_until_offline)) - nodesOnline--; - - ILOG_INFO("removing oldest node 0x%08x", oldest); - lv_obj_delete(p); - { - auto it = messages.find(oldest); - if (it != messages.end()) { - lv_obj_delete(it->second); - messages.erase(oldest); + lv_obj_t *p = children[i]; + uint32_t oldest = (unsigned long)(p->LV_OBJ_IDX(node_lbl_idx)->user_data); + uint32_t lastHeard = (unsigned long)p->LV_OBJ_IDX(node_lh_idx)->user_data; + if (lastHeard > 0 && (curtime - lastHeard <= secs_until_offline)) + nodesOnline--; + + ILOG_INFO("removing oldest node 0x%08x", oldest); + lv_obj_delete(p); + { + auto it = messages.find(oldest); + if (it != messages.end()) { + lv_obj_delete(it->second); + messages.erase(oldest); + } + } + + { + auto it = chats.find(oldest); + if (it != chats.end()) { + lv_obj_delete(it->second); + chats.erase(oldest); + updateActiveChats(); + } + } + removeFromMap(oldest); + nodes.erase(oldest); + nodeCount--; + nodesChanged = true; // flag to force re-apply node filter + } + + /** + * @brief apply enabled filters and highlight node + * + * @param nodeNum + * @param reset : set true when filter has changed (to recalculate number of filtered nodes) + * @return true + * @return false + */ + bool TFTView_320x240::applyNodesFilter(uint32_t nodeNum, bool reset) + { + lv_obj_t *panel = nodes[nodeNum]; + bool hide = false; + if (nodeNum != ownNode /* && filter.active*/) { // TODO + if (lv_obj_has_state(objects.nodes_filter_unknown_switch, LV_STATE_CHECKED)) { + if (lv_img_get_src(panel->LV_OBJ_IDX(node_img_idx)) == &img_circle_question_image) { + hide = true; + } + } + if (lv_obj_has_state(objects.nodes_filter_offline_switch, LV_STATE_CHECKED)) { + time_t lastHeard = (time_t)panel->LV_OBJ_IDX(node_lh_idx)->user_data; + if (lastHeard == 0 || curtime - lastHeard > secs_until_offline) + hide = true; + } + if (lv_obj_has_state(objects.nodes_filter_public_key_switch, LV_STATE_CHECKED)) { + bool hasKey = (unsigned long)panel->LV_OBJ_IDX(node_bat_idx)->user_data == 1; + if (!hasKey) + hide = true; + } + if (lv_dropdown_get_selected(objects.nodes_filter_channel_dropdown) != 0) { + int selected = lv_dropdown_get_selected(objects.nodes_filter_channel_dropdown); + if (selected != 0) { + uint8_t ch = (uint8_t)(unsigned long)panel->user_data; + if (selected - 1 != ch) + hide = true; + } + } + if (lv_dropdown_get_selected(objects.nodes_filter_hops_dropdown) != 0) { + int32_t hopsAway = (signed long)panel->LV_OBJ_IDX(node_sig_idx)->user_data; + int selected = lv_dropdown_get_selected(objects.nodes_filter_hops_dropdown) - 7; + if (hopsAway < 0) + hide = true; + else if (selected <= 0) { + if (hopsAway > -selected) + hide = true; + } else { + if (hopsAway < selected) + hide = true; + } + } +#if 0 + if (lv_obj_has_state(objects.nodes_filter_mqtt_switch, LV_STATE_CHECKED)) { + bool viaMqtt = false; // TODO (unsigned long)panel->LV_OBJ_IDX(node_sig_idx)->user_data; + if (viaMqtt) + hide = true; } - } +#endif + if (lv_obj_has_state(objects.nodes_filter_position_switch, LV_STATE_CHECKED)) { + if (lv_label_get_text(panel->LV_OBJ_IDX(node_pos1_idx))[0] == '\0') + hide = true; + } + const char *name = lv_textarea_get_text(objects.nodes_filter_name_area); + if (name[0] != '\0') { + if (name[0] != '!') { // use '!' char to negate search result + if (!strcasestr(lv_label_get_text(panel->LV_OBJ_IDX(node_lbl_idx)), name) && + !strcasestr(lv_label_get_text(panel->LV_OBJ_IDX(node_lbs_idx)), name)) { + hide = true; + } + } else { + if (strcasestr(lv_label_get_text(panel->LV_OBJ_IDX(node_lbl_idx)), &name[1]) || + strcasestr(lv_label_get_text(panel->LV_OBJ_IDX(node_lbs_idx)), &name[1])) { + hide = true; + } + } + } + } + if (hide) { + if (reset || !lv_obj_has_flag(panel, LV_OBJ_FLAG_HIDDEN)) { + lv_obj_add_flag(panel, LV_OBJ_FLAG_HIDDEN); + nodesFiltered++; + } + } else { + lv_obj_clear_flag(panel, LV_OBJ_FLAG_HIDDEN); + } - { - auto it = chats.find(oldest); - if (it != chats.end()) { - lv_obj_delete(it->second); - chats.erase(oldest); - updateActiveChats(); + // hide node location if filtered + if (map) + map->update(nodeNum, hide); + + bool highlight = false; + if (true /*highlight.active*/) { // TODO + if (lv_obj_has_state(objects.nodes_hl_active_chat_switch, LV_STATE_CHECKED)) { + auto it = chats.find(nodeNum); + if (it != nodes.end()) { + lv_obj_set_style_border_color(panel, colorOrange, LV_PART_MAIN | LV_STATE_DEFAULT); + highlight = true; + } + } + if (lv_obj_has_state(objects.nodes_hl_position_switch, LV_STATE_CHECKED)) { + if (lv_label_get_text(panel->LV_OBJ_IDX(node_pos1_idx))[0] != '\0') { + lv_obj_set_style_border_color(panel, colorBlueGreen, LV_PART_MAIN | LV_STATE_DEFAULT); + highlight = true; + } + } + if (lv_obj_has_state(objects.nodes_hl_telemetry_switch, LV_STATE_CHECKED)) { + if (lv_label_get_text(panel->LV_OBJ_IDX(node_tm1_idx))[0] != '\0') { + lv_obj_set_style_border_color(panel, colorBlue, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_width(panel, 2, LV_PART_MAIN | LV_STATE_DEFAULT); + highlight = true; + } + } + if (lv_obj_has_state(objects.nodes_hliaq_switch, LV_STATE_CHECKED)) { + if (lv_label_get_text(panel->LV_OBJ_IDX(node_tm2_idx))[0] != '\0') { + uint32_t iaq = (unsigned long)panel->LV_OBJ_IDX(node_tm2_idx)->user_data; + // IAQ color code + lv_color_t fg, bg; + if (iaq <= 50) { + fg = lv_color_hex(0x00000000); + bg = lv_color_hex(0x000ce810); + } else if (iaq <= 100) { + fg = lv_color_hex(0x00000000); + bg = lv_color_hex(0x00faf646); + } else if (iaq <= 150) { + fg = lv_color_hex(0x00000000); + bg = lv_color_hex(0x00f98204); + } else if (iaq <= 200) { + fg = lv_color_hex(0x00000000); + bg = lv_color_hex(0x00e42104); + } else if (iaq <= 300) { + fg = lv_color_hex(0xffffffff); + bg = lv_color_hex(0x009b2970); + } else { + fg = lv_color_hex(0xffffffff); + bg = lv_color_hex(0x001d1414); + } + lv_obj_set_style_text_color(panel->LV_OBJ_IDX(node_tm2_idx), fg, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(panel->LV_OBJ_IDX(node_tm2_idx), bg, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(panel->LV_OBJ_IDX(node_tm2_idx), 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_color(panel, bg, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_width(panel, 2, LV_PART_MAIN | LV_STATE_DEFAULT); + highlight = true; + } + } + const char *name = lv_textarea_get_text(objects.nodes_hl_name_area); + if (name[0] != '\0') { + if (strcasestr(lv_label_get_text(panel->LV_OBJ_IDX(node_lbl_idx)), name) || + strcasestr(lv_label_get_text(panel->LV_OBJ_IDX(node_lbs_idx)), name)) { + lv_obj_set_style_border_color(panel, colorMesh, LV_PART_MAIN | LV_STATE_DEFAULT); + highlight = true; + } + } + } + if (!highlight) { + lv_obj_set_style_border_color(panel, colorMidGray, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_width(panel, 1, LV_PART_MAIN | LV_STATE_DEFAULT); + } + return hide; // TODO || filter.active; } - } - removeFromMap(oldest); - nodes.erase(oldest); - nodeCount--; - nodesChanged = true; // flag to force re-apply node filter -} -/** - * @brief apply enabled filters and highlight node - * - * @param nodeNum - * @param reset : set true when filter has changed (to recalculate number of filtered nodes) - * @return true - * @return false - */ -bool TFTView_320x240::applyNodesFilter(uint32_t nodeNum, bool reset) -{ - lv_obj_t *panel = nodes[nodeNum]; - bool hide = false; - if (nodeNum != ownNode /* && filter.active*/) { // TODO - if (lv_obj_has_state(objects.nodes_filter_unknown_switch, LV_STATE_CHECKED)) { - if (lv_img_get_src(panel->LV_OBJ_IDX(node_img_idx)) == &img_circle_question_image) { - hide = true; + void TFTView_320x240::messageAlert(const char *alert, bool show) + { + lv_label_set_text(objects.alert_label, alert); + if (show) + lv_obj_clear_flag(objects.alert_panel, LV_OBJ_FLAG_HIDDEN); + else + lv_obj_add_flag(objects.alert_panel, LV_OBJ_FLAG_HIDDEN); + } + + /** + * @brief mark the sent message as either heard or acknowledged or failed + * + * @param channelOrNode + * @param id + * @param ack + */ + void TFTView_320x240::handleTextMessageResponse(uint32_t channelOrNode, const uint32_t id, bool ack, bool err) + { + lv_obj_t *msgContainer; + if (channelOrNode < c_max_channels) { + msgContainer = channelGroup[(uint8_t)channelOrNode]; + ack = true; // treat messages sent to group channel same as ack + } else { + msgContainer = messages[channelOrNode]; + } + if (!msgContainer) { + ILOG_WARN("received unexpected response nodeNum/channel 0x%08x for request id 0x%08x", channelOrNode, id); + return; + } + // go through all hiddenPanels and search for requestId + uint16_t i = msgContainer->spec_attr->child_cnt; + while (i-- > 0) { + lv_obj_t *panel = msgContainer->spec_attr->children[i]; + uint32_t requestId = (unsigned long)panel->user_data; + if (requestId == id) { + // now give the textlabel border another color + lv_obj_t *textLabel = panel->spec_attr->children[0]; + lv_obj_set_style_border_color(textLabel, + err ? colorRed + : ack ? colorBlueGreen + : colorYellow, + LV_PART_MAIN | LV_STATE_DEFAULT); + + // store message + break; + } } } - if (lv_obj_has_state(objects.nodes_filter_offline_switch, LV_STATE_CHECKED)) { - time_t lastHeard = (time_t)panel->LV_OBJ_IDX(node_lh_idx)->user_data; - if (lastHeard == 0 || curtime - lastHeard > secs_until_offline) - hide = true; - } - if (lv_obj_has_state(objects.nodes_filter_public_key_switch, LV_STATE_CHECKED)) { - bool hasKey = (unsigned long)panel->LV_OBJ_IDX(node_bat_idx)->user_data == 1; - if (!hasKey) - hide = true; - } - if (lv_dropdown_get_selected(objects.nodes_filter_channel_dropdown) != 0) { - int selected = lv_dropdown_get_selected(objects.nodes_filter_channel_dropdown); - if (selected != 0) { - uint8_t ch = (uint8_t)(unsigned long)panel->user_data; - if (selected - 1 != ch) - hide = true; + + void TFTView_320x240::packetReceived(const meshtastic_MeshPacket &p) + { + MeshtasticView::packetReceived(p); + + // try update time from packet + if (!VALID_TIME(actTime) && VALID_TIME(p.rx_time)) + updateTime(p.rx_time); + + if (detectorRunning) { + packetDetected(p); + } + if (packetLogEnabled) { + writePacketLog(p); } + if (p.from != ownNode) { + updateSignalStrength(p.rx_rssi, p.rx_snr); + } + updateStatistics(p); } - if (lv_dropdown_get_selected(objects.nodes_filter_hops_dropdown) != 0) { - int32_t hopsAway = (signed long)panel->LV_OBJ_IDX(node_sig_idx)->user_data; - int selected = lv_dropdown_get_selected(objects.nodes_filter_hops_dropdown) - 7; - if (hopsAway < 0) - hide = true; - else if (selected <= 0) { - if (hopsAway > -selected) - hide = true; + + void TFTView_320x240::notifyConnected(const char *info) + { + if (state == MeshtasticView::eBooting) { + updateBootMessage(info); } else { - if (hopsAway < selected) - hide = true; + if (state == MeshtasticView::eDisconnected) { + messageAlert(_("Connected!"), true); + // force re-sync with node + THIS->controller->setConfigRequested(true); + } + state = MeshtasticView::eRunning; } } -#if 0 - if (lv_obj_has_state(objects.nodes_filter_mqtt_switch, LV_STATE_CHECKED)) { - bool viaMqtt = false; // TODO (unsigned long)panel->LV_OBJ_IDX(node_sig_idx)->user_data; - if (viaMqtt) - hide = true; - } -#endif - if (lv_obj_has_state(objects.nodes_filter_position_switch, LV_STATE_CHECKED)) { - if (lv_label_get_text(panel->LV_OBJ_IDX(node_pos1_idx))[0] == '\0') - hide = true; - } - const char *name = lv_textarea_get_text(objects.nodes_filter_name_area); - if (name[0] != '\0') { - if (name[0] != '!') { // use '!' char to negate search result - if (!strcasestr(lv_label_get_text(panel->LV_OBJ_IDX(node_lbl_idx)), name) && - !strcasestr(lv_label_get_text(panel->LV_OBJ_IDX(node_lbs_idx)), name)) { - hide = true; - } + + void TFTView_320x240::notifyDisconnected(const char *info) + { + if (state == MeshtasticView::eBooting) { + updateBootMessage(info); } else { - if (strcasestr(lv_label_get_text(panel->LV_OBJ_IDX(node_lbl_idx)), &name[1]) || - strcasestr(lv_label_get_text(panel->LV_OBJ_IDX(node_lbs_idx)), &name[1])) { - hide = true; + if (state == MeshtasticView::eRunning) { + messageAlert(_("Disconnected!"), true); } + state = MeshtasticView::eDisconnected; } } - } - if (hide) { - if (reset || !lv_obj_has_flag(panel, LV_OBJ_FLAG_HIDDEN)) { - lv_obj_add_flag(panel, LV_OBJ_FLAG_HIDDEN); - nodesFiltered++; - } - } else { - lv_obj_clear_flag(panel, LV_OBJ_FLAG_HIDDEN); - } - - // hide node location if filtered - if (map) - map->update(nodeNum, hide); - bool highlight = false; - if (true /*highlight.active*/) { // TODO - if (lv_obj_has_state(objects.nodes_hl_active_chat_switch, LV_STATE_CHECKED)) { - auto it = chats.find(nodeNum); - if (it != nodes.end()) { - lv_obj_set_style_border_color(panel, colorOrange, LV_PART_MAIN | LV_STATE_DEFAULT); - highlight = true; - } - } - if (lv_obj_has_state(objects.nodes_hl_position_switch, LV_STATE_CHECKED)) { - if (lv_label_get_text(panel->LV_OBJ_IDX(node_pos1_idx))[0] != '\0') { - lv_obj_set_style_border_color(panel, colorBlueGreen, LV_PART_MAIN | LV_STATE_DEFAULT); - highlight = true; - } - } - if (lv_obj_has_state(objects.nodes_hl_telemetry_switch, LV_STATE_CHECKED)) { - if (lv_label_get_text(panel->LV_OBJ_IDX(node_tm1_idx))[0] != '\0') { - lv_obj_set_style_border_color(panel, colorBlue, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(panel, 2, LV_PART_MAIN | LV_STATE_DEFAULT); - highlight = true; - } - } - if (lv_obj_has_state(objects.nodes_hliaq_switch, LV_STATE_CHECKED)) { - if (lv_label_get_text(panel->LV_OBJ_IDX(node_tm2_idx))[0] != '\0') { - uint32_t iaq = (unsigned long)panel->LV_OBJ_IDX(node_tm2_idx)->user_data; - // IAQ color code - lv_color_t fg, bg; - if (iaq <= 50) { - fg = lv_color_hex(0x00000000); - bg = lv_color_hex(0x000ce810); - } else if (iaq <= 100) { - fg = lv_color_hex(0x00000000); - bg = lv_color_hex(0x00faf646); - } else if (iaq <= 150) { - fg = lv_color_hex(0x00000000); - bg = lv_color_hex(0x00f98204); - } else if (iaq <= 200) { - fg = lv_color_hex(0x00000000); - bg = lv_color_hex(0x00e42104); - } else if (iaq <= 300) { - fg = lv_color_hex(0xffffffff); - bg = lv_color_hex(0x009b2970); - } else { - fg = lv_color_hex(0xffffffff); - bg = lv_color_hex(0x001d1414); + void TFTView_320x240::notifyResync(bool show) + { + if (controller->isStandalone()) { + if (show) + notifyReboot(true); + } else { + messageAlert(_("Resync ..."), show); + if (!show) { + lv_screen_load_anim(objects.main_screen, LV_SCR_LOAD_ANIM_NONE, 0, 0, false); } - lv_obj_set_style_text_color(panel->LV_OBJ_IDX(node_tm2_idx), fg, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(panel->LV_OBJ_IDX(node_tm2_idx), bg, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(panel->LV_OBJ_IDX(node_tm2_idx), 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(panel, bg, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(panel, 2, LV_PART_MAIN | LV_STATE_DEFAULT); - highlight = true; } } - const char *name = lv_textarea_get_text(objects.nodes_hl_name_area); - if (name[0] != '\0') { - if (strcasestr(lv_label_get_text(panel->LV_OBJ_IDX(node_lbl_idx)), name) || - strcasestr(lv_label_get_text(panel->LV_OBJ_IDX(node_lbs_idx)), name)) { - lv_obj_set_style_border_color(panel, colorMesh, LV_PART_MAIN | LV_STATE_DEFAULT); - highlight = true; + + void TFTView_320x240::notifyReboot(bool show) + { + messageAlert(_("Rebooting ..."), show); + if (controller->isStandalone()) { + lv_timer_create(timer_event_reboot, 8000, NULL); } } - } - if (!highlight) { - lv_obj_set_style_border_color(panel, colorMidGray, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(panel, 1, LV_PART_MAIN | LV_STATE_DEFAULT); - } - return hide; // TODO || filter.active; -} - -void TFTView_320x240::messageAlert(const char *alert, bool show) -{ - lv_label_set_text(objects.alert_label, alert); - if (show) - lv_obj_clear_flag(objects.alert_panel, LV_OBJ_FLAG_HIDDEN); - else - lv_obj_add_flag(objects.alert_panel, LV_OBJ_FLAG_HIDDEN); -} -/** - * @brief mark the sent message as either heard or acknowledged or failed - * - * @param channelOrNode - * @param id - * @param ack - */ -void TFTView_320x240::handleTextMessageResponse(uint32_t channelOrNode, const uint32_t id, bool ack, bool err) -{ - lv_obj_t *msgContainer; - if (channelOrNode < c_max_channels) { - msgContainer = channelGroup[(uint8_t)channelOrNode]; - ack = true; // treat messages sent to group channel same as ack - } else { - msgContainer = messages[channelOrNode]; - } - if (!msgContainer) { - ILOG_WARN("received unexpected response nodeNum/channel 0x%08x for request id 0x%08x", channelOrNode, id); - return; - } - // go through all hiddenPanels and search for requestId - uint16_t i = msgContainer->spec_attr->child_cnt; - while (i-- > 0) { - lv_obj_t *panel = msgContainer->spec_attr->children[i]; - uint32_t requestId = (unsigned long)panel->user_data; - if (requestId == id) { - // now give the textlabel border another color - lv_obj_t *textLabel = panel->spec_attr->children[0]; - lv_obj_set_style_border_color(textLabel, - err ? colorRed - : ack ? colorBlueGreen - : colorYellow, - LV_PART_MAIN | LV_STATE_DEFAULT); - - // store message - break; + void TFTView_320x240::notifyShutdown(void) + { + messageAlert(_("Shutting down ..."), true); } - } -} - -void TFTView_320x240::packetReceived(const meshtastic_MeshPacket &p) -{ - MeshtasticView::packetReceived(p); - - // try update time from packet - if (!VALID_TIME(actTime) && VALID_TIME(p.rx_time)) - updateTime(p.rx_time); - - if (detectorRunning) { - packetDetected(p); - } - if (packetLogEnabled) { - writePacketLog(p); - } - if (p.from != ownNode) { - updateSignalStrength(p.rx_rssi, p.rx_snr); - } - updateStatistics(p); -} -void TFTView_320x240::notifyConnected(const char *info) -{ - if (state == MeshtasticView::eBooting) { - updateBootMessage(info); - } else { - if (state == MeshtasticView::eDisconnected) { - messageAlert(_("Connected!"), true); - // force re-sync with node - THIS->controller->setConfigRequested(true); + void TFTView_320x240::blankScreen(bool enable) + { + ILOG_DEBUG("%s screen (%s)", enable ? "blank" : "unblank", screenLocked ? "locked" : "timeout"); + if (enable) + lv_screen_load_anim(objects.blank_screen, LV_SCR_LOAD_ANIM_FADE_OUT, 1000, 0, false); + else { + if (objects.main_screen) + lv_screen_load_anim(objects.main_screen, LV_SCR_LOAD_ANIM_NONE, 0, 0, false); + else + lv_screen_load_anim(objects.boot_screen, LV_SCR_LOAD_ANIM_NONE, 0, 0, false); + } } - state = MeshtasticView::eRunning; - } -} -void TFTView_320x240::notifyDisconnected(const char *info) -{ - if (state == MeshtasticView::eBooting) { - updateBootMessage(info); - } else { - if (state == MeshtasticView::eRunning) { - messageAlert(_("Disconnected!"), true); + void TFTView_320x240::screenSaving(bool enabled) + { + if (enabled) { + // overlay main screen with blank screen to prevent accidentally pressing buttons + lv_screen_load_anim(objects.blank_screen, LV_SCR_LOAD_ANIM_FADE_OUT, 0, 0, false); + lv_group_focus_obj(objects.blank_screen_button); + screenLocked = true; + screenUnlockRequest = false; + } else { + if (THIS->db.uiConfig.screen_lock) { + ILOG_DEBUG("showing lock screen"); + lv_screen_load_anim(objects.lock_screen, LV_SCR_LOAD_ANIM_NONE, 0, 0, false); + } else if (objects.main_screen) { + ILOG_DEBUG("showing main screen"); + lv_screen_load_anim(objects.main_screen, LV_SCR_LOAD_ANIM_NONE, 0, 0, false); + if (THIS->activeSettings != eNone) { + lv_event_t e = {.code = LV_EVENT_CLICKED}; + ui_event_cancel(&e); + } + screenLocked = false; + } else { + ILOG_DEBUG("showing boot screen"); + lv_screen_load_anim(objects.boot_screen, LV_SCR_LOAD_ANIM_NONE, 0, 0, false); + screenLocked = false; + } + } } - state = MeshtasticView::eDisconnected; - } -} -void TFTView_320x240::notifyResync(bool show) -{ - if (controller->isStandalone()) { - if (show) - notifyReboot(true); - } else { - messageAlert(_("Resync ..."), show); - if (!show) { - lv_screen_load_anim(objects.main_screen, LV_SCR_LOAD_ANIM_NONE, 0, 0, false); + bool TFTView_320x240::isScreenLocked(void) + { + return screenLocked && !screenUnlockRequest; } - } -} - -void TFTView_320x240::notifyReboot(bool show) -{ - messageAlert(_("Rebooting ..."), show); - if (controller->isStandalone()) { - lv_timer_create(timer_event_reboot, 8000, NULL); - } -} -void TFTView_320x240::notifyShutdown(void) -{ - messageAlert(_("Shutting down ..."), true); -} - -void TFTView_320x240::blankScreen(bool enable) -{ - ILOG_DEBUG("%s screen (%s)", enable ? "blank" : "unblank", screenLocked ? "locked" : "timeout"); - if (enable) - lv_screen_load_anim(objects.blank_screen, LV_SCR_LOAD_ANIM_FADE_OUT, 1000, 0, false); - else { - if (objects.main_screen) - lv_screen_load_anim(objects.main_screen, LV_SCR_LOAD_ANIM_NONE, 0, 0, false); - else - lv_screen_load_anim(objects.boot_screen, LV_SCR_LOAD_ANIM_NONE, 0, 0, false); - } -} + void TFTView_320x240::updateChannelConfig(const meshtastic_Channel &ch) + { + static lv_obj_t *btn[c_max_channels] = {objects.channel_button0, objects.channel_button1, objects.channel_button2, + objects.channel_button3, objects.channel_button4, objects.channel_button5, + objects.channel_button6, objects.channel_button7}; + db.channel[ch.index] = ch; + + if (ch.role != meshtastic_Channel_Role_DISABLED) { + setChannelName(ch); + + lv_obj_set_width(btn[ch.index], lv_pct(70)); + lv_obj_set_style_pad_left(btn[ch.index], 8, LV_PART_MAIN | LV_STATE_DEFAULT); + + lv_obj_t *lockImage = NULL; + if (lv_obj_get_child_cnt(btn[ch.index]) == 1) + lockImage = lv_img_create(btn[ch.index]); + else + lockImage = lv_obj_get_child(btn[ch.index], 1); + + uint32_t recolor = 0; + + if (memcmp(ch.settings.psk.bytes, "\001\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000", 16) == 0) { + lv_image_set_src(lockImage, &img_groups_key_image); + recolor = 0xF2E459; // yellow + } else if (memcmp(ch.settings.psk.bytes, "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000", + 16) == 0) { + lv_image_set_src(lockImage, &img_groups_unlock_image); + recolor = 0xF72B2B; // reddish + } else { + lv_image_set_src(lockImage, &img_groups_lock_image); + recolor = 0x1EC174; // green + } + lv_obj_set_width(lockImage, LV_SIZE_CONTENT); /// 1 + lv_obj_set_height(lockImage, LV_SIZE_CONTENT); /// 1 + lv_obj_set_align(lockImage, LV_ALIGN_LEFT_MID); + lv_obj_add_flag(lockImage, LV_OBJ_FLAG_ADV_HITTEST); /// Flags + lv_obj_clear_flag(lockImage, LV_OBJ_FLAG_SCROLLABLE); /// Flags + lv_obj_set_style_img_recolor(lockImage, lv_color_hex(recolor), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_img_recolor_opa(lockImage, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + + lv_obj_t *bellImage = NULL; + if (lv_obj_get_child_cnt(btn[ch.index]) < 3) + bellImage = lv_img_create(btn[ch.index]); + else + bellImage = lv_obj_get_child(btn[ch.index], 2); + lv_obj_set_width(bellImage, LV_SIZE_CONTENT); /// 1 + lv_obj_set_height(bellImage, LV_SIZE_CONTENT); /// 1 + lv_obj_set_align(bellImage, LV_ALIGN_RIGHT_MID); + lv_obj_add_flag(bellImage, LV_OBJ_FLAG_ADV_HITTEST); /// Flags + lv_obj_clear_flag(bellImage, LV_OBJ_FLAG_SCROLLABLE); /// Flags + lv_obj_set_style_img_recolor_opa(bellImage, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + updateGroupChannel(ch.index); + } else { + // display smaller button with just the channel number + char buf[10]; + lv_snprintf(buf, sizeof(buf), "%d", ch.index); + lv_label_set_text(channel[ch.index], buf); + lv_obj_set_width(btn[ch.index], lv_pct(30)); -void TFTView_320x240::screenSaving(bool enabled) -{ - if (enabled) { - // overlay main screen with blank screen to prevent accidentally pressing buttons - lv_screen_load_anim(objects.blank_screen, LV_SCR_LOAD_ANIM_FADE_OUT, 0, 0, false); - lv_group_focus_obj(objects.blank_screen_button); - screenLocked = true; - screenUnlockRequest = false; - } else { - if (THIS->db.uiConfig.screen_lock) { - ILOG_DEBUG("showing lock screen"); - lv_screen_load_anim(objects.lock_screen, LV_SCR_LOAD_ANIM_NONE, 0, 0, false); - } else if (objects.main_screen) { - ILOG_DEBUG("showing main screen"); - lv_screen_load_anim(objects.main_screen, LV_SCR_LOAD_ANIM_NONE, 0, 0, false); - if (THIS->activeSettings != eNone) { - lv_event_t e = {.code = LV_EVENT_CLICKED}; - ui_event_cancel(&e); - } - screenLocked = false; - } else { - ILOG_DEBUG("showing boot screen"); - lv_screen_load_anim(objects.boot_screen, LV_SCR_LOAD_ANIM_NONE, 0, 0, false); - screenLocked = false; + if (lv_obj_get_child_cnt(btn[ch.index]) == 2) { + lv_obj_delete(lv_obj_get_child(btn[ch.index], 1)); + } + } } - } -} -bool TFTView_320x240::isScreenLocked(void) -{ - return screenLocked && !screenUnlockRequest; -} + // redraw bell icons and color + void TFTView_320x240::updateGroupChannel(uint8_t chId) + { + static lv_obj_t *btn[c_max_channels] = {objects.channel_button0, objects.channel_button1, objects.channel_button2, + objects.channel_button3, objects.channel_button4, objects.channel_button5, + objects.channel_button6, objects.channel_button7}; + + lv_obj_t *bellImage = lv_obj_get_child(btn[chId], 2); + if (db.channel[chId].settings.module_settings.is_muted) { + lv_obj_set_style_img_recolor(bellImage, lv_color_hex(0xffab0000), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_image_set_src(bellImage, &img_groups_bell_slash_image); + } else { + Themes::recolorImage(bellImage, true); + lv_image_set_src(bellImage, &img_groups_bell_image); + } + } -void TFTView_320x240::updateChannelConfig(const meshtastic_Channel &ch) -{ - static lv_obj_t *btn[c_max_channels] = {objects.channel_button0, objects.channel_button1, objects.channel_button2, - objects.channel_button3, objects.channel_button4, objects.channel_button5, - objects.channel_button6, objects.channel_button7}; - db.channel[ch.index] = ch; - - if (ch.role != meshtastic_Channel_Role_DISABLED) { - setChannelName(ch); - - lv_obj_set_width(btn[ch.index], lv_pct(70)); - lv_obj_set_style_pad_left(btn[ch.index], 8, LV_PART_MAIN | LV_STATE_DEFAULT); - - lv_obj_t *lockImage = NULL; - if (lv_obj_get_child_cnt(btn[ch.index]) == 1) - lockImage = lv_img_create(btn[ch.index]); - else - lockImage = lv_obj_get_child(btn[ch.index], 1); - - uint32_t recolor = 0; - - if (memcmp(ch.settings.psk.bytes, "\001\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000", 16) == 0) { - lv_image_set_src(lockImage, &img_groups_key_image); - recolor = 0xF2E459; // yellow - } else if (memcmp(ch.settings.psk.bytes, "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000", 16) == 0) { - lv_image_set_src(lockImage, &img_groups_unlock_image); - recolor = 0xF72B2B; // reddish - } else { - lv_image_set_src(lockImage, &img_groups_lock_image); - recolor = 0x1EC174; // green - } - lv_obj_set_width(lockImage, LV_SIZE_CONTENT); /// 1 - lv_obj_set_height(lockImage, LV_SIZE_CONTENT); /// 1 - lv_obj_set_align(lockImage, LV_ALIGN_LEFT_MID); - lv_obj_add_flag(lockImage, LV_OBJ_FLAG_ADV_HITTEST); /// Flags - lv_obj_clear_flag(lockImage, LV_OBJ_FLAG_SCROLLABLE); /// Flags - lv_obj_set_style_img_recolor(lockImage, lv_color_hex(recolor), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_img_recolor_opa(lockImage, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - - lv_obj_t *bellImage = NULL; - if (lv_obj_get_child_cnt(btn[ch.index]) < 3) - bellImage = lv_img_create(btn[ch.index]); - else - bellImage = lv_obj_get_child(btn[ch.index], 2); - lv_obj_set_width(bellImage, LV_SIZE_CONTENT); /// 1 - lv_obj_set_height(bellImage, LV_SIZE_CONTENT); /// 1 - lv_obj_set_align(bellImage, LV_ALIGN_RIGHT_MID); - lv_obj_add_flag(bellImage, LV_OBJ_FLAG_ADV_HITTEST); /// Flags - lv_obj_clear_flag(bellImage, LV_OBJ_FLAG_SCROLLABLE); /// Flags - lv_obj_set_style_img_recolor_opa(bellImage, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - updateGroupChannel(ch.index); - } else { - // display smaller button with just the channel number - char buf[10]; - lv_snprintf(buf, sizeof(buf), "%d", ch.index); - lv_label_set_text(channel[ch.index], buf); - lv_obj_set_width(btn[ch.index], lv_pct(30)); + void TFTView_320x240::updateDeviceConfig(const meshtastic_Config_DeviceConfig &cfg) + { + db.config.device = cfg; + db.config.has_device = true; - if (lv_obj_get_child_cnt(btn[ch.index]) == 2) { - lv_obj_delete(lv_obj_get_child(btn[ch.index], 1)); + char buf1[30], buf2[40]; + lv_dropdown_set_selected(objects.settings_device_role_dropdown, role2val(cfg.role)); + lv_dropdown_get_selected_str(objects.settings_device_role_dropdown, buf1, sizeof(buf1)); + lv_snprintf(buf2, sizeof(buf2), _("Device Role: %s"), buf1); + lv_label_set_text(objects.basic_settings_role_label, buf2); } - } -} - -// redraw bell icons and color -void TFTView_320x240::updateGroupChannel(uint8_t chId) -{ - static lv_obj_t *btn[c_max_channels] = {objects.channel_button0, objects.channel_button1, objects.channel_button2, - objects.channel_button3, objects.channel_button4, objects.channel_button5, - objects.channel_button6, objects.channel_button7}; - - lv_obj_t *bellImage = lv_obj_get_child(btn[chId], 2); - if (db.channel[chId].settings.module_settings.is_muted) { - lv_obj_set_style_img_recolor(bellImage, lv_color_hex(0xffab0000), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_image_set_src(bellImage, &img_groups_bell_slash_image); - } else { - Themes::recolorImage(bellImage, true); - lv_image_set_src(bellImage, &img_groups_bell_image); - } -} -void TFTView_320x240::updateDeviceConfig(const meshtastic_Config_DeviceConfig &cfg) -{ - db.config.device = cfg; - db.config.has_device = true; - - char buf1[30], buf2[40]; - lv_dropdown_set_selected(objects.settings_device_role_dropdown, role2val(cfg.role)); - lv_dropdown_get_selected_str(objects.settings_device_role_dropdown, buf1, sizeof(buf1)); - lv_snprintf(buf2, sizeof(buf2), _("Device Role: %s"), buf1); - lv_label_set_text(objects.basic_settings_role_label, buf2); -} + void TFTView_320x240::updatePositionConfig(const meshtastic_Config_PositionConfig &cfg) + { + db.config.position = cfg; + db.config.has_position = true; + if (cfg.gps_mode != meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) { + if (cfg.fixed_position && db.uiConfig.map_data.has_home) { + updatePosition(ownNode, db.uiConfig.map_data.home.latitude, db.uiConfig.map_data.home.longitude, 0, 0, 0); + } + // grey out text to indicate it's a fixed position vs. actual GPS position + Themes::recolorText(objects.home_location_label, !cfg.fixed_position); + } + Themes::recolorButton(objects.home_location_button, cfg.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED); + } -void TFTView_320x240::updatePositionConfig(const meshtastic_Config_PositionConfig &cfg) -{ - db.config.position = cfg; - db.config.has_position = true; - if (cfg.gps_mode != meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) { - if (cfg.fixed_position && db.uiConfig.map_data.has_home) { - updatePosition(ownNode, db.uiConfig.map_data.home.latitude, db.uiConfig.map_data.home.longitude, 0, 0, 0); - } - // grey out text to indicate it's a fixed position vs. actual GPS position - Themes::recolorText(objects.home_location_label, !cfg.fixed_position); - } - Themes::recolorButton(objects.home_location_button, cfg.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED); -} + void TFTView_320x240::updatePowerConfig(const meshtastic_Config_PowerConfig &cfg) + { + db.config.power = cfg; + db.config.has_power = true; + } -void TFTView_320x240::updatePowerConfig(const meshtastic_Config_PowerConfig &cfg) -{ - db.config.power = cfg; - db.config.has_power = true; -} + void TFTView_320x240::updateNetworkConfig(const meshtastic_Config_NetworkConfig &cfg) + { + db.config.network = cfg; + db.config.has_network = true; -void TFTView_320x240::updateNetworkConfig(const meshtastic_Config_NetworkConfig &cfg) -{ - db.config.network = cfg; - db.config.has_network = true; + char buf[40]; + lv_snprintf(buf, sizeof(buf), _("WiFi: %s"), cfg.wifi_ssid[0] ? cfg.wifi_ssid : _("")); + lv_label_set_text(objects.basic_settings_wifi_label, buf); + } - char buf[40]; - lv_snprintf(buf, sizeof(buf), _("WiFi: %s"), cfg.wifi_ssid[0] ? cfg.wifi_ssid : _("")); - lv_label_set_text(objects.basic_settings_wifi_label, buf); -} + void TFTView_320x240::updateDisplayConfig(const meshtastic_Config_DisplayConfig &cfg) + { + db.config.display = cfg; + db.config.has_display = true; + if (!controller->isStandalone() && cfg.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + meshtastic_Config_DisplayConfig &display = db.config.display; + display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR; + THIS->controller->sendConfig(meshtastic_Config_DisplayConfig{display}, THIS->ownNode); + } + } -void TFTView_320x240::updateDisplayConfig(const meshtastic_Config_DisplayConfig &cfg) -{ - db.config.display = cfg; - db.config.has_display = true; - if (!controller->isStandalone() && cfg.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { - meshtastic_Config_DisplayConfig &display = db.config.display; - display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR; - THIS->controller->sendConfig(meshtastic_Config_DisplayConfig{display}, THIS->ownNode); - } -} + void TFTView_320x240::updateLoRaConfig(const meshtastic_Config_LoRaConfig &cfg) + { + db.config.lora = cfg; + db.config.has_lora = true; + + if (cfg.use_preset) { + // This must be run before displaying LoRa frequency as channel of 0 ("calculate from hash") leads to an integer + // underflow + if (!db.config.lora.channel_num) { + db.config.lora.channel_num = LoRaPresets::getDefaultSlot( + db.config.lora.region, THIS->db.config.lora.modem_preset, THIS->db.channel[0].settings.name); + } + char buf1[20], buf2[32]; + lv_dropdown_set_selected(objects.settings_modem_preset_dropdown, preset2val(cfg.modem_preset)); + lv_dropdown_get_selected_str(objects.settings_modem_preset_dropdown, buf1, sizeof(buf1)); + lv_snprintf(buf2, sizeof(buf2), _("Modem Preset: %s"), buf1); + lv_label_set_text(objects.basic_settings_modem_preset_label, buf2); -void TFTView_320x240::updateLoRaConfig(const meshtastic_Config_LoRaConfig &cfg) -{ - db.config.lora = cfg; - db.config.has_lora = true; - - if (cfg.use_preset) { - // This must be run before displaying LoRa frequency as channel of 0 ("calculate from hash") leads to an integer underflow - if (!db.config.lora.channel_num) { - db.config.lora.channel_num = LoRaPresets::getDefaultSlot(db.config.lora.region, THIS->db.config.lora.modem_preset, - THIS->db.channel[0].settings.name); - } - char buf1[20], buf2[32]; - lv_dropdown_set_selected(objects.settings_modem_preset_dropdown, preset2val(cfg.modem_preset)); - lv_dropdown_get_selected_str(objects.settings_modem_preset_dropdown, buf1, sizeof(buf1)); - lv_snprintf(buf2, sizeof(buf2), _("Modem Preset: %s"), buf1); - lv_label_set_text(objects.basic_settings_modem_preset_label, buf2); - - uint32_t numChannels = LoRaPresets::getNumChannels(cfg.region, cfg.modem_preset); - lv_slider_set_range(objects.frequency_slot_slider, 1, numChannels); - lv_slider_set_value(objects.frequency_slot_slider, db.config.lora.channel_num, LV_ANIM_OFF); - } else { - lv_label_set_text(objects.basic_settings_modem_preset_label, _("Modem Preset: custom")); - } + uint32_t numChannels = LoRaPresets::getNumChannels(cfg.region, cfg.modem_preset); + lv_slider_set_range(objects.frequency_slot_slider, 1, numChannels); + lv_slider_set_value(objects.frequency_slot_slider, db.config.lora.channel_num, LV_ANIM_OFF); + } else { + lv_label_set_text(objects.basic_settings_modem_preset_label, _("Modem Preset: custom")); + } - char region[30]; - lv_snprintf(region, sizeof(region), _("Region: %s"), LoRaPresets::loRaRegionToString(cfg.region)); - lv_label_set_text(objects.basic_settings_region_label, region); + char region[30]; + lv_snprintf(region, sizeof(region), _("Region: %s"), LoRaPresets::loRaRegionToString(cfg.region)); + lv_label_set_text(objects.basic_settings_region_label, region); - showLoRaFrequency(db.config.lora); + showLoRaFrequency(db.config.lora); - if (db.config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_UNSET) { - // update channel names again now that region is known - for (int i = 0; i < c_max_channels; i++) { - if (db.channel[i].has_settings && db.channel[i].role != meshtastic_Channel_Role_DISABLED) { - setChannelName(db.channel[i]); + if (db.config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_UNSET) { + // update channel names again now that region is known + for (int i = 0; i < c_max_channels; i++) { + if (db.channel[i].has_settings && db.channel[i].role != meshtastic_Channel_Role_DISABLED) { + setChannelName(db.channel[i]); + } + } + } else { + requestSetup(); } } - } else { - requestSetup(); - } -} -void TFTView_320x240::showLoRaFrequency(const meshtastic_Config_LoRaConfig &cfg) -{ - char loraFreq[48]; - if (!cfg.region) { - strcpy(loraFreq, _("region unset")); - } else if (cfg.use_preset) { - float frequency = LoRaPresets::getRadioFreq(cfg.region, cfg.modem_preset, cfg.channel_num) + cfg.frequency_offset; - sprintf(loraFreq, "LoRa %g MHz\n[%s kHz]", frequency, LoRaPresets::getBandwidthString(cfg.modem_preset)); - lv_obj_remove_state(objects.basic_settings_modem_preset_button, LV_STATE_DISABLED); - } else { - float frequency = cfg.override_frequency + cfg.frequency_offset; - sprintf(loraFreq, "LoRa %g MHz\n[%d kHz]", frequency, cfg.bandwidth); - lv_obj_add_state(objects.basic_settings_modem_preset_button, LV_STATE_DISABLED); - } + void TFTView_320x240::showLoRaFrequency(const meshtastic_Config_LoRaConfig &cfg) + { + char loraFreq[48]; + if (!cfg.region) { + strcpy(loraFreq, _("region unset")); + } else if (cfg.use_preset) { + float frequency = LoRaPresets::getRadioFreq(cfg.region, cfg.modem_preset, cfg.channel_num) + cfg.frequency_offset; + sprintf(loraFreq, "LoRa %g MHz\n[%s kHz]", frequency, LoRaPresets::getBandwidthString(cfg.modem_preset)); + lv_obj_remove_state(objects.basic_settings_modem_preset_button, LV_STATE_DISABLED); + } else { + float frequency = cfg.override_frequency + cfg.frequency_offset; + sprintf(loraFreq, "LoRa %g MHz\n[%d kHz]", frequency, cfg.bandwidth); + lv_obj_add_state(objects.basic_settings_modem_preset_button, LV_STATE_DISABLED); + } - lv_label_set_text(objects.home_lora_label, loraFreq); - Themes::recolorButton(objects.home_lora_button, cfg.tx_enabled); - Themes::recolorText(objects.home_lora_label, cfg.tx_enabled); - if (!cfg.tx_enabled) { - lv_obj_clear_flag(objects.top_lora_tx_panel, LV_OBJ_FLAG_HIDDEN); - } else { - lv_obj_add_flag(objects.top_lora_tx_panel, LV_OBJ_FLAG_HIDDEN); - } -} + lv_label_set_text(objects.home_lora_label, loraFreq); + Themes::recolorButton(objects.home_lora_button, cfg.tx_enabled); + Themes::recolorText(objects.home_lora_label, cfg.tx_enabled); + if (!cfg.tx_enabled) { + lv_obj_clear_flag(objects.top_lora_tx_panel, LV_OBJ_FLAG_HIDDEN); + } else { + lv_obj_add_flag(objects.top_lora_tx_panel, LV_OBJ_FLAG_HIDDEN); + } + } -void TFTView_320x240::setBellText(bool banner, bool sound) -{ - if (banner && sound) { - lv_label_set_text(objects.home_bell_label, _("Banner & Sound")); - } else if (banner) { - lv_label_set_text(objects.home_bell_label, _("Banner only")); - } else if (sound) { - lv_label_set_text(objects.home_bell_label, _("Sound only")); - } else { - lv_label_set_text(objects.home_bell_label, _("silent")); - } + void TFTView_320x240::setBellText(bool banner, bool sound) + { + if (banner && sound) { + lv_label_set_text(objects.home_bell_label, _("Banner & Sound")); + } else if (banner) { + lv_label_set_text(objects.home_bell_label, _("Banner only")); + } else if (sound) { + lv_label_set_text(objects.home_bell_label, _("Sound only")); + } else { + lv_label_set_text(objects.home_bell_label, _("silent")); + } - char buf[40]; - lv_snprintf(buf, sizeof(buf), _("Message Alert: %s"), - db.module_config.external_notification.alert_message_buzzer - ? (!sound ? _("silent") : ringtone[db.uiConfig.ring_tone_id].name) - : "off"); - lv_label_set_text(objects.basic_settings_alert_label, buf); - - Themes::recolorButton(objects.home_bell_button, banner || sound); - Themes::recolorText(objects.home_bell_label, banner || sound); -} + char buf[40]; + lv_snprintf(buf, sizeof(buf), _("Message Alert: %s"), + db.module_config.external_notification.alert_message_buzzer + ? (!sound ? _("silent") : ringtone[db.uiConfig.ring_tone_id].name) + : "off"); + lv_label_set_text(objects.basic_settings_alert_label, buf); -/** - * auto set primary(secondary) channel name (based on region) - */ -void TFTView_320x240::setChannelName(const meshtastic_Channel &ch) -{ - char buf[40]; - if (ch.role == meshtastic_Channel_Role_PRIMARY) { - sprintf(buf, _("Channel: %s"), - strlen(ch.settings.name) ? ch.settings.name - : db.config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET - ? ("") - : LoRaPresets::modemPresetToString(db.config.lora.modem_preset)); - lv_label_set_text(objects.basic_settings_channel_label, buf); - - sprintf(buf, "*%s", - strlen(ch.settings.name) ? ch.settings.name - : db.config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET - ? ("") - : LoRaPresets::modemPresetToString(db.config.lora.modem_preset)); - } else { - if (ch.settings.name[0] == '\0' && ch.settings.psk.size == 1 && ch.settings.psk.bytes[0] == 0x01) { - sprintf(buf, "%s", LoRaPresets::modemPresetToString(db.config.lora.modem_preset)); - } else { - strcpy(buf, ch.settings.name); + Themes::recolorButton(objects.home_bell_button, banner || sound); + Themes::recolorText(objects.home_bell_label, banner || sound); } - } - lv_label_set_text(channel[ch.index], buf); + /** + * auto set primary(secondary) channel name (based on region) + */ + void TFTView_320x240::setChannelName(const meshtastic_Channel &ch) + { + char buf[40]; + if (ch.role == meshtastic_Channel_Role_PRIMARY) { + sprintf(buf, _("Channel: %s"), + strlen(ch.settings.name) ? ch.settings.name + : db.config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET + ? ("") + : LoRaPresets::modemPresetToString(db.config.lora.modem_preset)); + lv_label_set_text(objects.basic_settings_channel_label, buf); + + sprintf(buf, "*%s", + strlen(ch.settings.name) ? ch.settings.name + : db.config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET + ? ("") + : LoRaPresets::modemPresetToString(db.config.lora.modem_preset)); + } else { + if (ch.settings.name[0] == '\0' && ch.settings.psk.size == 1 && ch.settings.psk.bytes[0] == 0x01) { + sprintf(buf, "%s", LoRaPresets::modemPresetToString(db.config.lora.modem_preset)); + } else { + strcpy(buf, ch.settings.name); + } + } + + lv_label_set_text(channel[ch.index], buf); - // rename chat - auto it = chats.find(ch.index); - if (it != chats.end()) { - char buf2[64]; - sprintf(buf2, "%d: %s", (int)ch.index, buf); - lv_label_set_text(it->second->spec_attr->children[0], buf2); - } -} + // rename chat + auto it = chats.find(ch.index); + if (it != chats.end()) { + char buf2[64]; + sprintf(buf2, "%d: %s", (int)ch.index, buf); + lv_label_set_text(it->second->spec_attr->children[0], buf2); + } + } -void TFTView_320x240::backup(uint32_t option) -{ + void TFTView_320x240::backup(uint32_t option) + { #if defined(HAS_SDCARD) || defined(HAS_SD_MMC) || defined(ARCH_PORTDUINO) - meshtastic_Config_SecurityConfig_public_key_t &pubkey = db.config.security.public_key; - meshtastic_Config_SecurityConfig_private_key_t &privkey = db.config.security.private_key; + meshtastic_Config_SecurityConfig_public_key_t &pubkey = db.config.security.public_key; + meshtastic_Config_SecurityConfig_private_key_t &privkey = db.config.security.private_key; - std::stringstream path; - path << "/keys/" << std::hex << std::setw(8) << std::setfill('0') << ownNode << ".yml"; + std::stringstream path; + path << "/keys/" << std::hex << std::setw(8) << std::setfill('0') << ownNode << ".yml"; #if defined(ARCH_PORTDUINO) || defined(HAS_SD_MMC) - SDFs.mkdir("/keys"); - File sd = SDFs.open(path.str().c_str(), FILE_WRITE); + SDFs.mkdir("/keys"); + File sd = SDFs.open(path.str().c_str(), FILE_WRITE); #else - SDFs.mkdir("/keys"); - FsFile sd = SDFs.open(path.str().c_str(), O_RDWR | O_CREAT); + SDFs.mkdir("/keys"); + FsFile sd = SDFs.open(path.str().c_str(), O_RDWR | O_CREAT); #endif - if (sd) { - sd.println("config:"); - sd.println(" security:"); - sd.print(" privateKey: base64:"); - sd.println(pskToBase64(privkey.bytes, privkey.size).c_str()); - sd.print(" publicKey: base64:"); - sd.println(pskToBase64(pubkey.bytes, pubkey.size).c_str()); - ILOG_INFO("backup pub/priv keys done."); - } else { - ILOG_ERROR("open file %s for backup failed", path.str().c_str()); - messageAlert(_("Failed to write keys!"), true); - } - sd.close(); + if (sd) { + sd.println("config:"); + sd.println(" security:"); + sd.print(" privateKey: base64:"); + sd.println(pskToBase64(privkey.bytes, privkey.size).c_str()); + sd.print(" publicKey: base64:"); + sd.println(pskToBase64(pubkey.bytes, pubkey.size).c_str()); + ILOG_INFO("backup pub/priv keys done."); + } else { + ILOG_ERROR("open file %s for backup failed", path.str().c_str()); + messageAlert(_("Failed to write keys!"), true); + } + sd.close(); #endif -} + } -void TFTView_320x240::restore(uint32_t option) -{ + void TFTView_320x240::restore(uint32_t option) + { #if defined(HAS_SDCARD) || defined(HAS_SD_MMC) || defined(ARCH_PORTDUINO) - meshtastic_Config_SecurityConfig_public_key_t &pubkey = db.config.security.public_key; - meshtastic_Config_SecurityConfig_private_key_t &privkey = db.config.security.private_key; + meshtastic_Config_SecurityConfig_public_key_t &pubkey = db.config.security.public_key; + meshtastic_Config_SecurityConfig_private_key_t &privkey = db.config.security.private_key; - std::stringstream path; - path << "/keys/" << std::hex << std::setw(8) << std::setfill('0') << ownNode << ".yml"; + std::stringstream path; + path << "/keys/" << std::hex << std::setw(8) << std::setfill('0') << ownNode << ".yml"; #if defined(ARCH_PORTDUINO) || defined(HAS_SD_MMC) - File sd = SDFs.open(path.str().c_str(), FILE_READ); + File sd = SDFs.open(path.str().c_str(), FILE_READ); #else - FsFile sd = SDFs.open(path.str().c_str(), O_RDONLY); + FsFile sd = SDFs.open(path.str().c_str(), O_RDONLY); #endif - if (sd) { - // TODO: improve parsing file contents - sd.readStringUntil('\n'); // config: - sd.readStringUntil('\n'); // security: - String privKey = sd.readStringUntil('\n'); // privateKey: base64: - String pubKey = sd.readStringUntil('\n'); // publicKey: base64: - if (privKey.indexOf("privateKey:") > 0 && pubKey.indexOf("publicKey:") > 0) { - String b64priv = privKey.substring(privKey.lastIndexOf(":") + 1); - String b64pub = pubKey.substring(pubKey.lastIndexOf(":") + 1); - b64priv.trim(); - b64pub.trim(); - if (base64ToPsk(b64priv.c_str(), privkey.bytes, privkey.size) && - base64ToPsk(b64pub.c_str(), pubkey.bytes, pubkey.size) && - controller->sendConfig(meshtastic_Config_SecurityConfig{db.config.security})) { - ILOG_INFO("restore pub/priv keys sent to radio"); + if (sd) { + // TODO: improve parsing file contents + sd.readStringUntil('\n'); // config: + sd.readStringUntil('\n'); // security: + String privKey = sd.readStringUntil('\n'); // privateKey: base64: + String pubKey = sd.readStringUntil('\n'); // publicKey: base64: + if (privKey.indexOf("privateKey:") > 0 && pubKey.indexOf("publicKey:") > 0) { + String b64priv = privKey.substring(privKey.lastIndexOf(":") + 1); + String b64pub = pubKey.substring(pubKey.lastIndexOf(":") + 1); + b64priv.trim(); + b64pub.trim(); + if (base64ToPsk(b64priv.c_str(), privkey.bytes, privkey.size) && + base64ToPsk(b64pub.c_str(), pubkey.bytes, pubkey.size) && + controller->sendConfig(meshtastic_Config_SecurityConfig{db.config.security})) { + ILOG_INFO("restore pub/priv keys sent to radio"); + } else { + ILOG_ERROR("decoding keys failed"); + messageAlert(_("Failed to restore keys!"), true); + } + } else { + ILOG_ERROR("file %s contents don't match backup", path.str().c_str()); + messageAlert(_("Failed to parse keys!"), true); + } } else { - ILOG_ERROR("decoding keys failed"); - messageAlert(_("Failed to restore keys!"), true); - } - } else { - ILOG_ERROR("file %s contents don't match backup", path.str().c_str()); - messageAlert(_("Failed to parse keys!"), true); - } - } else { - ILOG_ERROR("open file %s failed", path.str().c_str()); - messageAlert(_("Failed to retrieve keys!"), true); - } - sd.close(); + ILOG_ERROR("open file %s failed", path.str().c_str()); + messageAlert(_("Failed to retrieve keys!"), true); + } + sd.close(); #endif -} + } -/** - * @brief write local time stamp into buffer - * if date is not current also add day/month - * Note: time string ends with linefeed - * - * @param buf allocated buffer - * @param datetime date/time to write - * @param update update with actual time, otherwise using time from parameter 'time' - * @return length of time string - */ -uint32_t TFTView_320x240::timestamp(char *buf, uint32_t datetime, bool update) -{ - time_t local = datetime; - if (update) { + /** + * @brief write local time stamp into buffer + * if date is not current also add day/month + * Note: time string ends with linefeed + * + * @param buf allocated buffer + * @param datetime date/time to write + * @param update update with actual time, otherwise using time from parameter 'time' + * @return length of time string + */ + uint32_t TFTView_320x240::timestamp(char *buf, uint32_t datetime, bool update) + { + time_t local = datetime; + if (update) { #ifdef ARCH_PORTDUINO - time(&local); + time(&local); #else - if (VALID_TIME(actTime)) - local = actTime; + if (VALID_TIME(actTime)) + local = actTime; #endif - } - if (VALID_TIME(local)) { - std::tm date_tm{}; - localtime_r(&local, &date_tm); - if (!update) - return strftime(buf, 20, "%y/%m/%d %R\n", &date_tm); - else - return strftime(buf, 20, "%R\n", &date_tm); - } else - return 0; -} - -/** - * calculate percentage value from rssi and snr - * Note: ranges are based on the axis values of the signal scanner - */ -int32_t TFTView_320x240::signalStrength2Percent(int32_t rx_rssi, float rx_snr) -{ + } + if (VALID_TIME(local)) { + std::tm date_tm{}; + localtime_r(&local, &date_tm); + if (!update) + return strftime(buf, 20, "%y/%m/%d %R\n", &date_tm); + else + return strftime(buf, 20, "%R\n", &date_tm); + } else + return 0; + } + + /** + * calculate percentage value from rssi and snr + * Note: ranges are based on the axis values of the signal scanner + */ + int32_t TFTView_320x240::signalStrength2Percent(int32_t rx_rssi, float rx_snr) + { #if defined(USE_SX127x) - int p_snr = ((std::max(rx_snr, -19.0f) + 19.0f) / 33.0f) * 100.0f; // range -19..14 - int p_rssi = ((std::max(rx_rssi, -145L) + 145) * 100) / 90; // range -145..-55 + int p_snr = ((std::max(rx_snr, -19.0f) + 19.0f) / 33.0f) * 100.0f; // range -19..14 + int p_rssi = ((std::max(rx_rssi, -145L) + 145) * 100) / 90; // range -145..-55 #else - int p_snr = ((std::max(rx_snr, -18.0f) + 18.0f) / 26.0f) * 100.0f; // range -18..8 - int p_rssi = ((std::max(rx_rssi, -125) + 125) * 100) / 100; // range -125..-25 + int p_snr = ((std::max(rx_snr, -18.0f) + 18.0f) / 26.0f) * 100.0f; // range -18..8 + int p_rssi = ((std::max(rx_rssi, -125) + 125) * 100) / 100; // range -125..-25 #endif - return std::min((p_snr + p_rssi * 2) / 3, 100); -} + return std::min((p_snr + p_rssi * 2) / 3, 100); + } -void TFTView_320x240::updateBluetoothConfig(const meshtastic_Config_BluetoothConfig &cfg, uint32_t id) -{ - db.config.bluetooth = cfg; - db.config.has_bluetooth = true; + void TFTView_320x240::updateBluetoothConfig(const meshtastic_Config_BluetoothConfig &cfg, uint32_t id) + { + db.config.bluetooth = cfg; + db.config.has_bluetooth = true; - if (ownNode == 0) { - ownNode = id; - } + if (ownNode == 0) { + ownNode = id; + } - if (state <= MeshtasticView::eBootScreenDone && state != MeshtasticView::eWaitingForReboot) { - enterProgrammingMode(); - } -} + if (state <= MeshtasticView::eBootScreenDone && state != MeshtasticView::eWaitingForReboot) { + enterProgrammingMode(); + } + } -void TFTView_320x240::updateSecurityConfig(const meshtastic_Config_SecurityConfig &cfg) -{ - db.config.security = cfg; - db.config.has_security = true; + void TFTView_320x240::updateSecurityConfig(const meshtastic_Config_SecurityConfig &cfg) + { + db.config.security = cfg; + db.config.has_security = true; - // display public key in qr code label - char buf[64]; - lv_snprintf(buf, sizeof(buf), "%s", pskToBase64((uint8_t *)cfg.public_key.bytes, cfg.public_key.size).c_str()); - lv_label_set_text(objects.home_qr_label, buf); -} + // display public key in qr code label + char buf[64]; + lv_snprintf(buf, sizeof(buf), "%s", pskToBase64((uint8_t *)cfg.public_key.bytes, cfg.public_key.size).c_str()); + lv_label_set_text(objects.home_qr_label, buf); + } -void TFTView_320x240::updateSessionKeyConfig(const meshtastic_Config_SessionkeyConfig &cfg) -{ - // TODO -} + void TFTView_320x240::updateSessionKeyConfig(const meshtastic_Config_SessionkeyConfig &cfg) + { + // TODO + } -/// ---- module updates ---- + /// ---- module updates ---- -void TFTView_320x240::updateMQTTModule(const meshtastic_ModuleConfig_MQTTConfig &cfg) -{ - db.module_config.mqtt = cfg; - db.module_config.has_mqtt = true; + void TFTView_320x240::updateMQTTModule(const meshtastic_ModuleConfig_MQTTConfig &cfg) + { + db.module_config.mqtt = cfg; + db.module_config.has_mqtt = true; - char buf[32]; - lv_snprintf(buf, sizeof(buf), "%s", db.module_config.mqtt.root); - lv_label_set_text(objects.home_mqtt_label, buf); + char buf[32]; + lv_snprintf(buf, sizeof(buf), "%s", db.module_config.mqtt.root); + lv_label_set_text(objects.home_mqtt_label, buf); - if (!db.module_config.mqtt.enabled) { - Themes::recolorButton(objects.home_mqtt_button, false); - Themes::recolorText(objects.home_mqtt_label, false); - } -} + if (!db.module_config.mqtt.enabled) { + Themes::recolorButton(objects.home_mqtt_button, false); + Themes::recolorText(objects.home_mqtt_label, false); + } + } -void TFTView_320x240::updateExtNotificationModule(const meshtastic_ModuleConfig_ExternalNotificationConfig &cfg) -{ - db.module_config.external_notification = cfg; - db.module_config.has_external_notification = true; - - char buf[32]; - lv_snprintf(buf, sizeof(buf), _("Message Alert: %s"), - db.module_config.external_notification.alert_message_buzzer && db.module_config.external_notification.enabled - ? _("on") - : _("off")); - lv_label_set_text(objects.basic_settings_alert_label, buf); -} + void TFTView_320x240::updateExtNotificationModule(const meshtastic_ModuleConfig_ExternalNotificationConfig &cfg) + { + db.module_config.external_notification = cfg; + db.module_config.has_external_notification = true; -void TFTView_320x240::updateRingtone(const char rtttl[231]) -{ - // retrieving ringtone index for dropdown - uint16_t rtIndex = 0; - for (int i = 0; i < numRingtones; i++) { - if (strncmp(ringtone[i].rtttl, rtttl, 16) == 0) { - rtIndex = i; - break; + char buf[32]; + lv_snprintf(buf, sizeof(buf), _("Message Alert: %s"), + db.module_config.external_notification.alert_message_buzzer && + db.module_config.external_notification.enabled + ? _("on") + : _("off")); + lv_label_set_text(objects.basic_settings_alert_label, buf); } - } - if (rtIndex != 0) - db.uiConfig.ring_tone_id = rtIndex; - if (db.uiConfig.ring_tone_id == 0) - db.uiConfig.ring_tone_id = 1; - - // update home panel bell text - setBellText(db.uiConfig.alert_enabled, !db.silent); - bool off = !db.uiConfig.alert_enabled && db.silent; - Themes::recolorButton(objects.home_bell_button, !off); - Themes::recolorText(objects.home_bell_label, !off); - objects.home_bell_button->user_data = (void *)off; -} -void TFTView_320x240::updateTime(uint32_t timeVal) -{ - time_t localtime; - time(&localtime); + void TFTView_320x240::updateRingtone(const char rtttl[231]) + { + // retrieving ringtone index for dropdown + uint16_t rtIndex = 0; + for (int i = 0; i < numRingtones; i++) { + if (strncmp(ringtone[i].rtttl, rtttl, 16) == 0) { + rtIndex = i; + break; + } + } + if (rtIndex != 0) + db.uiConfig.ring_tone_id = rtIndex; + if (db.uiConfig.ring_tone_id == 0) + db.uiConfig.ring_tone_id = 1; - if (VALID_TIME(localtime)) { - if (actTime != localtime) { - ILOG_DEBUG("update (local)time: %d -> %d", actTime, localtime); - actTime = localtime; + // update home panel bell text + setBellText(db.uiConfig.alert_enabled, !db.silent); + bool off = !db.uiConfig.alert_enabled && db.silent; + Themes::recolorButton(objects.home_bell_button, !off); + Themes::recolorText(objects.home_bell_label, !off); + objects.home_bell_button->user_data = (void *)off; } - } else { - if (timeVal > actTime) { - ILOG_DEBUG("update (act)time: %d -> %d", actTime, timeVal); - actTime = timeVal; + + void TFTView_320x240::updateTime(uint32_t timeVal) + { + time_t localtime; + time(&localtime); + + if (VALID_TIME(localtime)) { + if (actTime != localtime) { + ILOG_DEBUG("update (local)time: %d -> %d", actTime, localtime); + actTime = localtime; + } + } else { + if (timeVal > actTime) { + ILOG_DEBUG("update (act)time: %d -> %d", actTime, timeVal); + actTime = timeVal; + } + } } - } -} -/** - * @brief Create a new container for a node or group channel if it does not exist - * - * @param from - * @param to: UINT32_MAX for broadcast, ownNode (= us) otherwise - * @param channel - */ -lv_obj_t *TFTView_320x240::newMessageContainer(uint32_t from, uint32_t to, uint8_t ch) -{ - if (to == UINT32_MAX || from == 0) { - if (channelGroup[ch] != nullptr) - return channelGroup[ch]; - } else { - auto it = messages.find(from); - if (it != messages.end() && it->second) - return it->second; - } + /** + * @brief Create a new container for a node or group channel if it does not exist + * + * @param from + * @param to: UINT32_MAX for broadcast, ownNode (= us) otherwise + * @param channel + */ + lv_obj_t *TFTView_320x240::newMessageContainer(uint32_t from, uint32_t to, uint8_t ch) + { + if (to == UINT32_MAX || from == 0) { + if (channelGroup[ch] != nullptr) + return channelGroup[ch]; + } else { + auto it = messages.find(from); + if (it != messages.end() && it->second) + return it->second; + } - // create container for new messages - lv_obj_t *container = lv_obj_create(objects.messages_panel); - lv_obj_remove_style_all(container); - lv_obj_set_width(container, lv_pct(100)); - lv_obj_set_height(container, lv_pct(88)); - lv_obj_set_x(container, 0); - lv_obj_set_y(container, 0); - lv_obj_set_align(container, LV_ALIGN_TOP_MID); - lv_obj_set_flex_flow(container, LV_FLEX_FLOW_COLUMN); - lv_obj_set_flex_align(container, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START); - lv_obj_clear_flag(container, lv_obj_flag_t(LV_OBJ_FLAG_PRESS_LOCK | LV_OBJ_FLAG_CLICK_FOCUSABLE | LV_OBJ_FLAG_GESTURE_BUBBLE | - LV_OBJ_FLAG_SNAPPABLE | LV_OBJ_FLAG_SCROLL_ELASTIC)); /// Flags - lv_obj_set_scrollbar_mode(container, LV_SCROLLBAR_MODE_ACTIVE); - lv_obj_set_scroll_dir(container, LV_DIR_VER); - lv_obj_set_style_pad_left(container, 6, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_right(container, 6, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_top(container, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_bottom(container, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_row(container, 6, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_column(container, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - - // store new message container - if (to == UINT32_MAX || from == 0) { - channelGroup[ch] = container; - } else { - messages[from] = container; - } + // create container for new messages + lv_obj_t *container = lv_obj_create(objects.messages_panel); + lv_obj_remove_style_all(container); + lv_obj_set_width(container, lv_pct(100)); + lv_obj_set_height(container, lv_pct(88)); + lv_obj_set_x(container, 0); + lv_obj_set_y(container, 0); + lv_obj_set_align(container, LV_ALIGN_TOP_MID); + lv_obj_set_flex_flow(container, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(container, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START); + lv_obj_clear_flag(container, + lv_obj_flag_t(LV_OBJ_FLAG_PRESS_LOCK | LV_OBJ_FLAG_CLICK_FOCUSABLE | LV_OBJ_FLAG_GESTURE_BUBBLE | + LV_OBJ_FLAG_SNAPPABLE | LV_OBJ_FLAG_SCROLL_ELASTIC)); /// Flags + lv_obj_set_scrollbar_mode(container, LV_SCROLLBAR_MODE_ACTIVE); + lv_obj_set_scroll_dir(container, LV_DIR_VER); + lv_obj_set_style_pad_left(container, 6, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_right(container, 6, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_top(container, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_bottom(container, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_row(container, 6, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_column(container, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + + // store new message container + if (to == UINT32_MAX || from == 0) { + channelGroup[ch] = container; + } else { + messages[from] = container; + } - // optionally add chat to chatPanel to access the container - addChat(from, to, ch); + // optionally add chat to chatPanel to access the container + addChat(from, to, ch); - return container; -} + return container; + } -/** - * @brief insert a mew message that arrived into a or container - * - * @param from source node - * @param to destination node - * @param ch channel - * @param size length of msg - * @param msg text message - * @param time in/out: message time (maybe overwritten when 0) - * @param restore if restoring then skip banners and highlight - */ -void TFTView_320x240::newMessage(uint32_t from, uint32_t to, uint8_t ch, const char *msg, uint32_t &msgTime, bool restore) -{ - ILOG_DEBUG("newMessage: from:0x%08x, to:0x%08x, ch:%d, time:%d", from, to, ch, msgTime); - int pos = 0; - char buf[284]; // 237 + 4 + 40 + 2 + 1 - lv_obj_t *container = nullptr; - if (to == UINT32_MAX) { // message for group, prepend short name to msg - if (nodes.find(from) == nodes.end()) { - pos += sprintf(buf, "%04x ", from & 0xffff); - } else { - // original short name is held in userData, extract it and add msg - char *userData = (char *)&(nodes[from]->LV_OBJ_IDX(node_lbs_idx)->user_data); - while (pos < 4 && userData[pos] != 0) { - buf[pos] = userData[pos]; - pos++; + /** + * @brief insert a mew message that arrived into a or container + * + * @param from source node + * @param to destination node + * @param ch channel + * @param size length of msg + * @param msg text message + * @param time in/out: message time (maybe overwritten when 0) + * @param restore if restoring then skip banners and highlight + */ + void TFTView_320x240::newMessage(uint32_t from, uint32_t to, uint8_t ch, const char *msg, uint32_t &msgTime, bool restore) + { + ILOG_DEBUG("newMessage: from:0x%08x, to:0x%08x, ch:%d, time:%d", from, to, ch, msgTime); + int pos = 0; + char buf[284]; // 237 + 4 + 40 + 2 + 1 + lv_obj_t *container = nullptr; + if (to == UINT32_MAX) { // message for group, prepend short name to msg + if (nodes.find(from) == nodes.end()) { + pos += sprintf(buf, "%04x ", from & 0xffff); + } else { + // original short name is held in userData, extract it and add msg + char *userData = (char *)&(nodes[from]->LV_OBJ_IDX(node_lbs_idx)->user_data); + while (pos < 4 && userData[pos] != 0) { + buf[pos] = userData[pos]; + pos++; + } + } + buf[pos++] = ' '; + container = channelGroup[ch]; + } else { // message for us + container = messages[from]; } - } - buf[pos++] = ' '; - container = channelGroup[ch]; - } else { // message for us - container = messages[from]; - } - // if it's the first message we need a container - if (!container) { - container = newMessageContainer(from, to, ch); - } + // if it's the first message we need a container + if (!container) { + container = newMessageContainer(from, to, ch); + } - pos += timestamp(&buf[pos], msgTime, !restore); - sprintf(&buf[pos], "%s", msg); + pos += timestamp(&buf[pos], msgTime, !restore); + sprintf(&buf[pos], "%s", msg); - // place message into container - newMessage(from, container, ch, buf); + // place message into container + newMessage(from, container, ch, buf); - if (!restore) { - // display msg popup if not already viewing the messages - if (container != activeMsgContainer || activePanel != objects.messages_panel) { - unreadMessages++; - updateUnreadMessages(); - if (activePanel != objects.messages_panel && db.uiConfig.alert_enabled && - !db.channel[ch].settings.module_settings.is_muted) { - showMessagePopup(from, to, ch, lv_label_get_text(nodes[from]->LV_OBJ_IDX(node_lbl_idx))); + if (!restore) { + // display msg popup if not already viewing the messages + if (container != activeMsgContainer || activePanel != objects.messages_panel) { + unreadMessages++; + updateUnreadMessages(); + if (activePanel != objects.messages_panel && db.uiConfig.alert_enabled && + !db.channel[ch].settings.module_settings.is_muted) { + showMessagePopup(from, to, ch, lv_label_get_text(nodes[from]->LV_OBJ_IDX(node_lbl_idx))); + } + lv_obj_add_flag(container, LV_OBJ_FLAG_HIDDEN); + } + if (container != activeMsgContainer) + highlightChat(from, to, ch); + } else { + if (container != activeMsgContainer) + lv_obj_add_flag(container, LV_OBJ_FLAG_HIDDEN); } - lv_obj_add_flag(container, LV_OBJ_FLAG_HIDDEN); } - if (container != activeMsgContainer) - highlightChat(from, to, ch); - } else { - if (container != activeMsgContainer) - lv_obj_add_flag(container, LV_OBJ_FLAG_HIDDEN); - } -} - -/** - * @brief Display message bubble in related message container - * - * @param nodeNum - * @param container - * @param ch - * @param msg - */ -void TFTView_320x240::newMessage(uint32_t nodeNum, lv_obj_t *container, uint8_t ch, const char *msg) -{ - lv_obj_t *hiddenPanel = lv_obj_create(container); - lv_obj_set_width(hiddenPanel, lv_pct(100)); - lv_obj_set_height(hiddenPanel, LV_SIZE_CONTENT); /// 50 - lv_obj_set_align(hiddenPanel, LV_ALIGN_CENTER); - lv_obj_clear_flag(hiddenPanel, LV_OBJ_FLAG_SCROLLABLE); /// Flags - lv_obj_set_style_radius(hiddenPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - add_style_panel_style(hiddenPanel); - lv_obj_set_style_pad_left(hiddenPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_right(hiddenPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_top(hiddenPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_bottom(hiddenPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - - lv_obj_t *msgLabel = lv_label_create(hiddenPanel); - // calculate expected size of text bubble, to make it look nicer - lv_coord_t width = lv_txt_get_width(msg, strlen(msg), &ui_font_montserrat_14, 0); - lv_obj_set_width(msgLabel, std::max(std::min((int32_t)(width), 160) + 10, 40)); - lv_obj_set_height(msgLabel, LV_SIZE_CONTENT); - lv_obj_set_align(msgLabel, LV_ALIGN_LEFT_MID); - lv_label_set_text(msgLabel, msg); - add_style_new_message_style(msgLabel); - - if (state == MeshtasticView::eRunning) { - lv_obj_scroll_to_view(hiddenPanel, LV_ANIM_ON); - lv_obj_move_foreground(objects.message_input_area); - } - lv_obj_add_event_cb(hiddenPanel, ui_event_chatNodeButton, LV_EVENT_CLICKED, (void *)nodeNum); -} -/** - * restore messages from persistent log - */ -void TFTView_320x240::restoreMessage(const LogMessage &msg) -{ - //((uint8_t *)msg.bytes)[msg._size] = 0; - // ILOG_DEBUG("restoring msg from:0x%08x, to:0x%08x, ch:%d, time:%d, status:%d, trash:%d, size:%d, '%s'", msg.from, msg.to, - // msg.ch, msg.time, (int)msg.status, msg.trashFlag, msg._size, msg.bytes); - - if (msg.from == ownNode) { - lv_obj_t *container = nullptr; - if (msg.to == UINT32_MAX) { - if (msg.trashFlag && chats.find(msg.ch) != chats.end()) { - ILOG_DEBUG("trashFlag set for channel %d", msg.ch); - eraseChat(msg.ch); - return; - } else { - container = newMessageContainer(msg.from, msg.to, msg.ch); + /** + * @brief Display message bubble in related message container + * + * @param nodeNum + * @param container + * @param ch + * @param msg + */ + void TFTView_320x240::newMessage(uint32_t nodeNum, lv_obj_t * container, uint8_t ch, const char *msg) + { + lv_obj_t *hiddenPanel = lv_obj_create(container); + lv_obj_set_width(hiddenPanel, lv_pct(100)); + lv_obj_set_height(hiddenPanel, LV_SIZE_CONTENT); /// 50 + lv_obj_set_align(hiddenPanel, LV_ALIGN_CENTER); + lv_obj_clear_flag(hiddenPanel, LV_OBJ_FLAG_SCROLLABLE); /// Flags + lv_obj_set_style_radius(hiddenPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + add_style_panel_style(hiddenPanel); + lv_obj_set_style_pad_left(hiddenPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_right(hiddenPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_top(hiddenPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_bottom(hiddenPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + + lv_obj_t *msgLabel = lv_label_create(hiddenPanel); + // calculate expected size of text bubble, to make it look nicer + lv_coord_t width = lv_txt_get_width(msg, strlen(msg), &ui_font_montserrat_14, 0); + lv_obj_set_width(msgLabel, std::max(std::min((int32_t)(width), 160) + 10, 40)); + lv_obj_set_height(msgLabel, LV_SIZE_CONTENT); + lv_obj_set_align(msgLabel, LV_ALIGN_LEFT_MID); + lv_label_set_text(msgLabel, msg); + add_style_new_message_style(msgLabel); + + if (state == MeshtasticView::eRunning) { + lv_obj_scroll_to_view(hiddenPanel, LV_ANIM_ON); + lv_obj_move_foreground(objects.message_input_area); } - } else { - if (nodes.find(msg.to) != nodes.end()) { - if (msg.trashFlag && chats.find(msg.to) != chats.end()) { - ILOG_DEBUG("trashFlag set for node %08x", msg.to); - eraseChat(msg.to); + lv_obj_add_event_cb(hiddenPanel, ui_event_chatNodeButton, LV_EVENT_CLICKED, (void *)nodeNum); + } + + /** + * restore messages from persistent log + */ + void TFTView_320x240::restoreMessage(const LogMessage &msg) + { + //((uint8_t *)msg.bytes)[msg._size] = 0; + // ILOG_DEBUG("restoring msg from:0x%08x, to:0x%08x, ch:%d, time:%d, status:%d, trash:%d, size:%d, '%s'", msg.from, + // msg.to, + // msg.ch, msg.time, (int)msg.status, msg.trashFlag, msg._size, msg.bytes); + + if (msg.from == ownNode) { + lv_obj_t *container = nullptr; + if (msg.to == UINT32_MAX) { + if (msg.trashFlag && chats.find(msg.ch) != chats.end()) { + ILOG_DEBUG("trashFlag set for channel %d", msg.ch); + eraseChat(msg.ch); + return; + } else { + container = newMessageContainer(msg.from, msg.to, msg.ch); + } + } else { + if (nodes.find(msg.to) != nodes.end()) { + if (msg.trashFlag && chats.find(msg.to) != chats.end()) { + ILOG_DEBUG("trashFlag set for node %08x", msg.to); + eraseChat(msg.to); + return; + } else { + container = newMessageContainer(msg.to, msg.from, msg.ch); + } + } else { + ILOG_DEBUG("to node 0x%08x not in db", msg.to); + MeshtasticView::addOrUpdateNode(msg.to, msg.ch, 0, eRole::unknown, false, false); + } + } + if (container) { + if (container != activeMsgContainer) + lv_obj_add_flag(container, LV_OBJ_FLAG_HIDDEN); + addMessage(container, msg.time, 0, (char *)msg.bytes, msg.status); + } + } else if (nodes.find(msg.from) != nodes.end()) { + if (msg.trashFlag && chats.find(msg.from) != chats.end()) { + ILOG_DEBUG("trashFlag set for node %08x", msg.from); + eraseChat(msg.from); return; } else { - container = newMessageContainer(msg.to, msg.from, msg.ch); + uint32_t time = msg.time ? msg.time : UINT32_MAX; // don't overwrite 0 with actual time + newMessage(msg.from, msg.to, msg.ch, (const char *)msg.bytes, time); } } else { - ILOG_DEBUG("to node 0x%08x not in db", msg.to); - MeshtasticView::addOrUpdateNode(msg.to, msg.ch, 0, eRole::unknown, false, false); - } - } - if (container) { - if (container != activeMsgContainer) + int pos = 0; + char buf[284]; // 237 + 4 + 40 + 2 + 1 + if (msg.to != UINT32_MAX) { + // from node not in db + ILOG_DEBUG("from node 0x%08x not in db", msg.from); + MeshtasticView::addOrUpdateNode(msg.from, msg.ch, 0, eRole::unknown, false, false); + } else { + ILOG_DEBUG("from node 0x%08x not in db and no need to insert", msg.from); + pos += sprintf(buf, "%04x ", msg.from & 0xffff); + } + uint32_t len = timestamp(buf + pos, msg.time, false); + memcpy(buf + pos + len, msg.bytes, msg.length()); + buf[pos + len + msg.length()] = 0; + + lv_obj_t *container = newMessageContainer(msg.from, msg.to, msg.ch); lv_obj_add_flag(container, LV_OBJ_FLAG_HIDDEN); - addMessage(container, msg.time, 0, (char *)msg.bytes, msg.status); - } - } else if (nodes.find(msg.from) != nodes.end()) { - if (msg.trashFlag && chats.find(msg.from) != chats.end()) { - ILOG_DEBUG("trashFlag set for node %08x", msg.from); - eraseChat(msg.from); - return; - } else { - uint32_t time = msg.time ? msg.time : UINT32_MAX; // don't overwrite 0 with actual time - newMessage(msg.from, msg.to, msg.ch, (const char *)msg.bytes, time); - } - } else { - int pos = 0; - char buf[284]; // 237 + 4 + 40 + 2 + 1 - if (msg.to != UINT32_MAX) { - // from node not in db - ILOG_DEBUG("from node 0x%08x not in db", msg.from); - MeshtasticView::addOrUpdateNode(msg.from, msg.ch, 0, eRole::unknown, false, false); - } else { - ILOG_DEBUG("from node 0x%08x not in db and no need to insert", msg.from); - pos += sprintf(buf, "%04x ", msg.from & 0xffff); + newMessage(msg.from, container, msg.ch, buf); + } } - uint32_t len = timestamp(buf + pos, msg.time, false); - memcpy(buf + pos + len, msg.bytes, msg.length()); - buf[pos + len + msg.length()] = 0; - lv_obj_t *container = newMessageContainer(msg.from, msg.to, msg.ch); - lv_obj_add_flag(container, LV_OBJ_FLAG_HIDDEN); - newMessage(msg.from, container, msg.ch, buf); - } -} + /** + * @brief Add a new chat to the chat panel to access the message container + * + * @param from + * @param to + * @param ch + */ + void TFTView_320x240::addChat(uint32_t from, uint32_t to, uint8_t ch) + { + uint32_t index = ((to == UINT32_MAX || from == 0) ? ch : from); + auto it = chats.find(index); + if (it != chats.end()) + return; -/** - * @brief Add a new chat to the chat panel to access the message container - * - * @param from - * @param to - * @param ch - */ -void TFTView_320x240::addChat(uint32_t from, uint32_t to, uint8_t ch) -{ - uint32_t index = ((to == UINT32_MAX || from == 0) ? ch : from); - auto it = chats.find(index); - if (it != chats.end()) - return; + lv_obj_t *chatDelBtn = nullptr; + lv_obj_t *parent_obj = objects.chats_panel; + + // ChatsButton + lv_obj_t *chatBtn = lv_btn_create(parent_obj); + lv_obj_set_pos(chatBtn, 0, 0); + lv_obj_set_size(chatBtn, LV_PCT(100), buttonSize); + lv_obj_add_flag(chatBtn, LV_OBJ_FLAG_SCROLL_ON_FOCUS); + lv_obj_clear_flag(chatBtn, LV_OBJ_FLAG_SCROLLABLE); + add_style_home_button_style(chatBtn); + lv_obj_set_style_align(chatBtn, LV_ALIGN_TOP_MID, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_color(chatBtn, colorMidGray, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_width(chatBtn, 1, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_shadow_ofs_x(chatBtn, 1, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_shadow_ofs_y(chatBtn, 2, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_radius(chatBtn, 6, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_left(chatBtn, 3, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_right(chatBtn, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_top(chatBtn, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_bottom(chatBtn, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_row(chatBtn, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_column(chatBtn, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(chatBtn, lv_color_hex(0xff141723), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_move_to_index(chatBtn, 0); - lv_obj_t *chatDelBtn = nullptr; - lv_obj_t *parent_obj = objects.chats_panel; - - // ChatsButton - lv_obj_t *chatBtn = lv_btn_create(parent_obj); - lv_obj_set_pos(chatBtn, 0, 0); - lv_obj_set_size(chatBtn, LV_PCT(100), buttonSize); - lv_obj_add_flag(chatBtn, LV_OBJ_FLAG_SCROLL_ON_FOCUS); - lv_obj_clear_flag(chatBtn, LV_OBJ_FLAG_SCROLLABLE); - add_style_home_button_style(chatBtn); - lv_obj_set_style_align(chatBtn, LV_ALIGN_TOP_MID, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(chatBtn, colorMidGray, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(chatBtn, 1, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_shadow_ofs_x(chatBtn, 1, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_shadow_ofs_y(chatBtn, 2, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_radius(chatBtn, 6, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_left(chatBtn, 3, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_right(chatBtn, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_top(chatBtn, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_bottom(chatBtn, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_row(chatBtn, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_column(chatBtn, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(chatBtn, lv_color_hex(0xff141723), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_move_to_index(chatBtn, 0); - - char buf[64]; - if (to == UINT32_MAX || from == 0) { - sprintf(buf, "%d: %s", (int)ch, lv_label_get_text(channel[ch])); - } else { - auto it = nodes.find(from); - if (it != nodes.end()) { - sprintf(buf, "%s: %s", lv_label_get_text(it->second->LV_OBJ_IDX(node_lbs_idx)), - lv_label_get_text(it->second->LV_OBJ_IDX(node_lbl_idx))); - } else { - sprintf(buf, "!%08x", from); - } - } + char buf[64]; + if (to == UINT32_MAX || from == 0) { + sprintf(buf, "%d: %s", (int)ch, lv_label_get_text(channel[ch])); + } else { + auto it = nodes.find(from); + if (it != nodes.end()) { + sprintf(buf, "%s: %s", lv_label_get_text(it->second->LV_OBJ_IDX(node_lbs_idx)), + lv_label_get_text(it->second->LV_OBJ_IDX(node_lbl_idx))); + } else { + sprintf(buf, "!%08x", from); + } + } - { - lv_obj_t *parent_obj = chatBtn; - { - // ChatsButtonLabel - lv_obj_t *obj = lv_label_create(parent_obj); - objects.chats_button_label = obj; - lv_obj_set_pos(obj, 0, 0); - lv_obj_set_size(obj, LV_PCT(100), LV_SIZE_CONTENT); - lv_label_set_long_mode(obj, LV_LABEL_LONG_DOT); - lv_label_set_text(obj, buf); - lv_obj_set_style_align(obj, LV_ALIGN_LEFT_MID, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_align(obj, LV_TEXT_ALIGN_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); - } - { - // ChatDelButton - lv_obj_t *obj = lv_btn_create(parent_obj); - chatDelBtn = obj; - lv_obj_set_pos(obj, -3, -1); - lv_obj_set_size(obj, 40, 23); - lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_align(obj, LV_ALIGN_RIGHT_MID, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(obj, colorDarkRed, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); { - lv_obj_t *parent_obj = obj; + lv_obj_t *parent_obj = chatBtn; + { + // ChatsButtonLabel + lv_obj_t *obj = lv_label_create(parent_obj); + objects.chats_button_label = obj; + lv_obj_set_pos(obj, 0, 0); + lv_obj_set_size(obj, LV_PCT(100), LV_SIZE_CONTENT); + lv_label_set_long_mode(obj, LV_LABEL_LONG_DOT); + lv_label_set_text(obj, buf); + lv_obj_set_style_align(obj, LV_ALIGN_LEFT_MID, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_align(obj, LV_TEXT_ALIGN_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); + } { - // DelLabel - lv_obj_t *chatDelBtn = lv_label_create(parent_obj); - lv_obj_set_pos(chatDelBtn, 0, 0); - lv_obj_set_size(chatDelBtn, LV_SIZE_CONTENT, LV_SIZE_CONTENT); - lv_label_set_text(chatDelBtn, _("DEL")); - lv_obj_set_style_align(chatDelBtn, LV_ALIGN_CENTER, LV_PART_MAIN | LV_STATE_DEFAULT); + // ChatDelButton + lv_obj_t *obj = lv_btn_create(parent_obj); + chatDelBtn = obj; + lv_obj_set_pos(obj, -3, -1); + lv_obj_set_size(obj, 40, 23); + lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_align(obj, LV_ALIGN_RIGHT_MID, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(obj, colorDarkRed, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); + { + lv_obj_t *parent_obj = obj; + { + // DelLabel + lv_obj_t *chatDelBtn = lv_label_create(parent_obj); + lv_obj_set_pos(chatDelBtn, 0, 0); + lv_obj_set_size(chatDelBtn, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_label_set_text(chatDelBtn, _("DEL")); + lv_obj_set_style_align(chatDelBtn, LV_ALIGN_CENTER, LV_PART_MAIN | LV_STATE_DEFAULT); + } + } } } - } - } - - chats[index] = chatBtn; - updateActiveChats(); - if (index > c_max_channels) { - if (nodes.find(index) != nodes.end()) - applyNodesFilter(index); - } - - lv_obj_add_event_cb(chatBtn, ui_event_ChatButton, LV_EVENT_ALL, (void *)index); - lv_obj_add_event_cb(chatDelBtn, ui_event_ChatDelButton, LV_EVENT_CLICKED, (void *)index); -} - -void TFTView_320x240::highlightChat(uint32_t from, uint32_t to, uint8_t ch) -{ - uint32_t index = ((to == UINT32_MAX || from == 0) ? ch : from); - auto it = chats.find(index); - if (it != chats.end()) { - // mark chat in color - lv_obj_set_style_border_color(it->second, colorOrange, LV_PART_MAIN | LV_STATE_DEFAULT); - } -} - -void TFTView_320x240::updateActiveChats(void) -{ - char buf[48]; - sprintf(buf, _p("%d active chat(s)", chats.size()), chats.size()); - lv_label_set_text(objects.top_chats_label, buf); -} - -/** - * @brief Display banner showing to be patient while restoring messages - */ -void TFTView_320x240::notifyRestoreMessages(int32_t percentage) -{ - lv_bar_set_value(objects.message_restore_bar, percentage, LV_ANIM_OFF); -} -void TFTView_320x240::notifyMessagesRestored(void) -{ - MeshtasticView::notifyMessagesRestored(); - lv_obj_add_flag(objects.msg_restore_panel, LV_OBJ_FLAG_HIDDEN); - updateActiveChats(); - updateNodesFiltered(true); -} - -/** - * @brief display new message popup panel - * - * @param from sender (NULL for removing popup) - * @param to individual or group message - * @param ch received channel - */ -void TFTView_320x240::showMessagePopup(uint32_t from, uint32_t to, uint8_t ch, const char *name) -{ - if (name) { - static char buf[64]; - sprintf(buf, _("New message from \n%s"), name); - buf[38] = '\0'; // cut too long userName - lv_label_set_text(objects.msg_popup_label, buf); - if (to == UINT32_MAX) - objects.msg_popup_button->user_data = (void *)(uint32_t)ch; // store the channel in the button's data - else - objects.msg_popup_button->user_data = (void *)from; // store the node in the button's data - lv_obj_clear_flag(objects.msg_popup_panel, LV_OBJ_FLAG_HIDDEN); - - if (db.module_config.external_notification.alert_message) - lv_disp_trig_activity(NULL); - - lv_group_focus_obj(objects.msg_popup_button); - } -} - -void TFTView_320x240::hideMessagePopup(void) -{ - lv_obj_add_flag(objects.msg_popup_panel, LV_OBJ_FLAG_HIDDEN); -} - -/** - * @brief Display messages of a group channel - * - * @param ch - */ -void TFTView_320x240::showMessages(uint8_t ch) -{ - if (!messagesRestored) { - // display message restoration progress banner - lv_obj_clear_flag(objects.msg_popup_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.msg_popup_button); - return; - } - - lv_obj_add_flag(activeMsgContainer, LV_OBJ_FLAG_HIDDEN); - activeMsgContainer = channelGroup[ch]; - if (!activeMsgContainer) { - activeMsgContainer = newMessageContainer(0, UINT32_MAX, ch); - } - - activeMsgContainer->user_data = (void *)(uint32_t)ch; - lv_obj_clear_flag(activeMsgContainer, LV_OBJ_FLAG_HIDDEN); - lv_label_set_text(objects.top_group_chat_label, lv_label_get_text(channel[ch])); - ui_set_active(objects.messages_button, objects.messages_panel, objects.top_group_chat_panel); -} + chats[index] = chatBtn; + updateActiveChats(); + if (index > c_max_channels) { + if (nodes.find(index) != nodes.end()) + applyNodesFilter(index); + } -/** - * @brief Display messages from a node - * - * @param nodeNum - */ -void TFTView_320x240::showMessages(uint32_t nodeNum) -{ - lv_obj_add_flag(activeMsgContainer, LV_OBJ_FLAG_HIDDEN); - activeMsgContainer = messages[nodeNum]; - if (!activeMsgContainer) { - activeMsgContainer = newMessageContainer(nodeNum, 0, 0); - } - activeMsgContainer->user_data = (void *)nodeNum; - lv_obj_clear_flag(activeMsgContainer, LV_OBJ_FLAG_HIDDEN); - lv_obj_t *p = nodes[nodeNum]; - if (p) { - lv_label_set_text(objects.top_messages_node_label, lv_label_get_text(p->LV_OBJ_IDX(node_lbl_idx))); - ui_set_active(objects.messages_button, objects.messages_panel, objects.top_messages_panel); - switch ((unsigned long)p->LV_OBJ_IDX(node_bat_idx)->user_data) { - case 0: - lv_obj_set_style_bg_image_src(objects.top_messages_node_image, &img_lock_channel_image, - LV_PART_MAIN | LV_STATE_DEFAULT); - break; - case 1: - lv_obj_set_style_bg_image_src(objects.top_messages_node_image, &img_lock_secure_image, - LV_PART_MAIN | LV_STATE_DEFAULT); - break; - default: - lv_obj_set_style_bg_image_src(objects.top_messages_node_image, &img_lock_slash_image, - LV_PART_MAIN | LV_STATE_DEFAULT); - break; + lv_obj_add_event_cb(chatBtn, ui_event_ChatButton, LV_EVENT_ALL, (void *)index); + lv_obj_add_event_cb(chatDelBtn, ui_event_ChatDelButton, LV_EVENT_CLICKED, (void *)index); } - unreadMessages = 0; // TODO: not all messages may be actually read - updateUnreadMessages(); - } else { - // TODO: log error - } -} -/** - * @brief Place keyboard at a suitable space above or below the text input area - * - * @param textArea - */ -void TFTView_320x240::showKeyboard(lv_obj_t *textArea) -{ - lv_area_t text_coords, kb_coords; - lv_obj_get_coords(textArea, &text_coords); - lv_obj_get_coords(objects.keyboard, &kb_coords); - uint32_t kb_h = kb_coords.y2 - kb_coords.y1; - uint32_t v = lv_display_get_vertical_resolution(displaydriver->getDisplay()); + void TFTView_320x240::highlightChat(uint32_t from, uint32_t to, uint8_t ch) + { + uint32_t index = ((to == UINT32_MAX || from == 0) ? ch : from); + auto it = chats.find(index); + if (it != chats.end()) { + // mark chat in color + lv_obj_set_style_border_color(it->second, colorOrange, LV_PART_MAIN | LV_STATE_DEFAULT); + } + } - if (textArea == objects.message_input_area) { - // if keyboard is to be shown in message input area then scroll the panel using animation - static auto panelAnimCB = [](void *var, int32_t v) { lv_obj_set_y((lv_obj_t *)var, v); }; - static auto kbdAnimCB = [](void *var, int32_t v) { lv_obj_set_y((lv_obj_t *)var, v); }; - - static lv_anim_t a1; - lv_area_t panel_coords; - lv_obj_get_coords(objects.messages_panel, &panel_coords); - - lv_anim_init(&a1); - lv_anim_set_var(&a1, objects.messages_panel); - lv_anim_set_exec_cb(&a1, panelAnimCB); - lv_anim_set_values(&a1, panel_coords.y1, panel_coords.y1 - kb_h); - lv_anim_set_duration(&a1, 300); - lv_anim_set_path_cb(&a1, lv_anim_path_linear); - lv_anim_start(&a1); - - static lv_anim_t a2; - lv_anim_init(&a2); - lv_anim_set_var(&a2, objects.keyboard); - lv_anim_set_exec_cb(&a2, kbdAnimCB); - lv_anim_set_values(&a2, v, v - kb_h); - lv_anim_set_duration(&a2, 300); - lv_anim_set_path_cb(&a2, lv_anim_path_linear); - lv_anim_start(&a2); - } else { - if (text_coords.y1 > kb_h + 30) { - // if enough place above put under top panel - lv_obj_set_pos(objects.keyboard, 0, 28); - } else if ((text_coords.y1 + 10) > v / 2) { - // if text area is at lower half then place above text area - lv_obj_set_pos(objects.keyboard, 0, text_coords.y1 - kb_h - 2); - } else { - // place below text area - lv_obj_set_pos(objects.keyboard, 0, text_coords.y2 + 3); + void TFTView_320x240::updateActiveChats(void) + { + char buf[48]; + sprintf(buf, _p("%d active chat(s)", chats.size()), chats.size()); + lv_label_set_text(objects.top_chats_label, buf); } - } - lv_keyboard_set_textarea(objects.keyboard, textArea); -} -void TFTView_320x240::hideKeyboard(lv_obj_t *panel) -{ - lv_area_t kb_coords; - lv_obj_get_coords(objects.keyboard, &kb_coords); - uint32_t kb_h = kb_coords.y2 - kb_coords.y1; - - if (panel == objects.messages_panel) { - static auto panelAnimCB = [](void *var, int32_t v) { lv_obj_set_y((lv_obj_t *)var, v); }; - static auto kbdAnimCB = [](void *var, int32_t v) { lv_obj_set_y((lv_obj_t *)var, v); }; - static auto deleted_cb = [](_lv_anim_t *) { lv_obj_add_flag(objects.keyboard, LV_OBJ_FLAG_HIDDEN); }; - - static lv_anim_t a1; - lv_area_t panel_coords; - lv_obj_get_coords(panel, &panel_coords); - - lv_anim_init(&a1); - lv_anim_set_var(&a1, panel); - lv_anim_set_exec_cb(&a1, panelAnimCB); - lv_anim_set_values(&a1, panel_coords.y1, panel_coords.y1 + kb_h); - lv_anim_set_duration(&a1, 300); - lv_anim_set_path_cb(&a1, lv_anim_path_linear); - lv_anim_start(&a1); - - static lv_anim_t a2; - lv_anim_init(&a2); - lv_anim_set_var(&a2, objects.keyboard); - lv_anim_set_exec_cb(&a2, kbdAnimCB); - lv_anim_set_values(&a2, kb_coords.y1, kb_coords.y1 + kb_h); - lv_anim_set_duration(&a2, 300); - lv_anim_set_path_cb(&a2, lv_anim_path_linear); - lv_anim_set_deleted_cb(&a2, deleted_cb); - lv_anim_start(&a2); - } -} + /** + * @brief Display banner showing to be patient while restoring messages + */ + void TFTView_320x240::notifyRestoreMessages(int32_t percentage) + { + lv_bar_set_value(objects.message_restore_bar, percentage, LV_ANIM_OFF); + } -lv_obj_t *TFTView_320x240::showQrCode(lv_obj_t *parent, const char *data) -{ - lv_color_t bg_color = colorMesh; - lv_color_t fg_color = lv_palette_darken(LV_PALETTE_BLUE, 4); - qr = lv_qrcode_create(parent); - int32_t size = std::min(lv_obj_get_width(parent), lv_obj_get_height(parent)) - 8; - lv_qrcode_set_size(qr, size); - lv_qrcode_set_dark_color(qr, fg_color); - lv_qrcode_set_light_color(qr, bg_color); - lv_qrcode_update(qr, data, strlen(data)); - lv_obj_center(qr); - lv_obj_set_style_border_color(qr, fg_color, 0); - lv_obj_set_style_border_width(qr, 4, 0); - return qr; -} + void TFTView_320x240::notifyMessagesRestored(void) + { + MeshtasticView::notifyMessagesRestored(); + lv_obj_add_flag(objects.msg_restore_panel, LV_OBJ_FLAG_HIDDEN); + updateActiveChats(); + updateNodesFiltered(true); + } -/** - * Enable underlying panel, buttons and scrollbar after it was disabled - */ -void TFTView_320x240::enablePanel(lv_obj_t *panel) -{ - lv_obj_clear_state(panel, LV_STATE_DISABLED); - lv_obj_set_scrollbar_mode(panel, LV_SCROLLBAR_MODE_AUTO); - lv_obj_add_flag(panel, LV_OBJ_FLAG_SCROLLABLE); + /** + * @brief display new message popup panel + * + * @param from sender (NULL for removing popup) + * @param to individual or group message + * @param ch received channel + */ + void TFTView_320x240::showMessagePopup(uint32_t from, uint32_t to, uint8_t ch, const char *name) + { + if (name) { + static char buf[64]; + sprintf(buf, _("New message from \n%s"), name); + buf[38] = '\0'; // cut too long userName + lv_label_set_text(objects.msg_popup_label, buf); + if (to == UINT32_MAX) + objects.msg_popup_button->user_data = (void *)(uint32_t)ch; // store the channel in the button's data + else + objects.msg_popup_button->user_data = (void *)from; // store the node in the button's data + lv_obj_clear_flag(objects.msg_popup_panel, LV_OBJ_FLAG_HIDDEN); + + if (db.module_config.external_notification.alert_message) + lv_disp_trig_activity(NULL); + + lv_group_focus_obj(objects.msg_popup_button); + } + } - auto enableButtons = [](lv_obj_t *obj, void *) -> lv_obj_tree_walk_res_t { - if (obj->class_p == &lv_button_class) { - lv_obj_clear_state(obj, LV_STATE_DISABLED); + void TFTView_320x240::hideMessagePopup(void) + { + lv_obj_add_flag(objects.msg_popup_panel, LV_OBJ_FLAG_HIDDEN); } - return LV_OBJ_TREE_WALK_NEXT; - }; - lv_obj_tree_walk(panel, enableButtons, NULL); -} + /** + * @brief Display messages of a group channel + * + * @param ch + */ + void TFTView_320x240::showMessages(uint8_t ch) + { + if (!messagesRestored) { + // display message restoration progress banner + lv_obj_clear_flag(objects.msg_popup_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.msg_popup_button); + return; + } -/** - * Disable underlying panel with it's children buttons and scrollbar - */ -void TFTView_320x240::disablePanel(lv_obj_t *panel) -{ - lv_obj_add_state(panel, LV_STATE_DISABLED); - lv_obj_set_scrollbar_mode(panel, LV_SCROLLBAR_MODE_OFF); - lv_obj_clear_flag(panel, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(activeMsgContainer, LV_OBJ_FLAG_HIDDEN); + activeMsgContainer = channelGroup[ch]; + if (!activeMsgContainer) { + activeMsgContainer = newMessageContainer(0, UINT32_MAX, ch); + } - auto disableButtons = [](lv_obj_t *obj, void *) -> lv_obj_tree_walk_res_t { - if (obj->class_p == &lv_button_class) { - lv_obj_add_state(obj, LV_STATE_DISABLED); + activeMsgContainer->user_data = (void *)(uint32_t)ch; + lv_obj_clear_flag(activeMsgContainer, LV_OBJ_FLAG_HIDDEN); + lv_label_set_text(objects.top_group_chat_label, lv_label_get_text(channel[ch])); + ui_set_active(objects.messages_button, objects.messages_panel, objects.top_group_chat_panel); } - return LV_OBJ_TREE_WALK_NEXT; - }; - - lv_obj_tree_walk(panel, disableButtons, NULL); -} -/** - * Set focus to first button of a panel - */ -void TFTView_320x240::setGroupFocus(lv_obj_t *panel) -{ - if (panel == objects.home_panel) { - lv_group_focus_obj(objects.home_mail_button); - } else if (panel == objects.nodes_panel) { - lv_group_focus_obj(objects.node_button); - } else if (panel == objects.groups_panel) { - lv_group_focus_obj(objects.channel_button0); - } else if (panel == objects.messages_panel) { - lv_group_focus_obj(objects.message_input_area); - } else if (panel == objects.chats_panel) { - if (chats.size() > 0) { - lv_group_focus_obj(panel->spec_attr->children[1]); // TODO: does not work + /** + * @brief Display messages from a node + * + * @param nodeNum + */ + void TFTView_320x240::showMessages(uint32_t nodeNum) + { + lv_obj_add_flag(activeMsgContainer, LV_OBJ_FLAG_HIDDEN); + activeMsgContainer = messages[nodeNum]; + if (!activeMsgContainer) { + activeMsgContainer = newMessageContainer(nodeNum, 0, 0); + } + activeMsgContainer->user_data = (void *)nodeNum; + lv_obj_clear_flag(activeMsgContainer, LV_OBJ_FLAG_HIDDEN); + lv_obj_t *p = nodes[nodeNum]; + if (p) { + lv_label_set_text(objects.top_messages_node_label, lv_label_get_text(p->LV_OBJ_IDX(node_lbl_idx))); + ui_set_active(objects.messages_button, objects.messages_panel, objects.top_messages_panel); + switch ((unsigned long)p->LV_OBJ_IDX(node_bat_idx)->user_data) { + case 0: + lv_obj_set_style_bg_image_src(objects.top_messages_node_image, &img_lock_channel_image, + LV_PART_MAIN | LV_STATE_DEFAULT); + break; + case 1: + lv_obj_set_style_bg_image_src(objects.top_messages_node_image, &img_lock_secure_image, + LV_PART_MAIN | LV_STATE_DEFAULT); + break; + default: + lv_obj_set_style_bg_image_src(objects.top_messages_node_image, &img_lock_slash_image, + LV_PART_MAIN | LV_STATE_DEFAULT); + break; + } + unreadMessages = 0; // TODO: not all messages may be actually read + updateUnreadMessages(); + } else { + // TODO: log error + } } - } else if (panel == objects.map_panel) { - } else if (panel == objects.settings_screen_lock_panel) { - lv_group_focus_obj(objects.screen_lock_button_matrix); - } else if (panel == objects.controller_panel) { - lv_group_focus_obj(objects.basic_settings_user_button); - } else { - for (int i = 0; i < lv_obj_get_child_count(panel); i++) { - if (panel->spec_attr->children[i]->class_p == &lv_button_class) { - lv_group_focus_obj(panel->spec_attr->children[i]); - break; - } + /** + * @brief Place keyboard at a suitable space above or below the text input area + * + * @param textArea + */ + void TFTView_320x240::showKeyboard(lv_obj_t * textArea) + { + // REMOVED: Virtual keyboard not needed for T-Deck + // This function is no longer used + return; } - } -} -/** - * input group used by keyboard and/or pointer for dynamic assignment - */ -void TFTView_320x240::setInputGroup(void) -{ - lv_group_t *group = lv_group_get_default(); + void TFTView_320x240::hideKeyboard(lv_obj_t * panel) + { + // REMOVED: Virtual keyboard not needed for T-Deck + // This function is no longer used + return; + } - if (group && inputdriver->hasKeyboardDevice()) - lv_indev_set_group(inputdriver->getKeyboard(), group); + lv_obj_t *TFTView_320x240::showQrCode(lv_obj_t * parent, const char *data) + { + lv_color_t bg_color = colorMesh; + lv_color_t fg_color = lv_palette_darken(LV_PALETTE_BLUE, 4); + qr = lv_qrcode_create(parent); + int32_t size = std::min(lv_obj_get_width(parent), lv_obj_get_height(parent)) - 8; + lv_qrcode_set_size(qr, size); + lv_qrcode_set_dark_color(qr, fg_color); + lv_qrcode_set_light_color(qr, bg_color); + lv_qrcode_update(qr, data, strlen(data)); + lv_obj_center(qr); + lv_obj_set_style_border_color(qr, fg_color, 0); + lv_obj_set_style_border_width(qr, 4, 0); + return qr; + } + + /** + * Enable underlying panel, buttons and scrollbar after it was disabled + */ + void TFTView_320x240::enablePanel(lv_obj_t * panel) + { + lv_obj_clear_state(panel, LV_STATE_DISABLED); + lv_obj_set_scrollbar_mode(panel, LV_SCROLLBAR_MODE_AUTO); + lv_obj_add_flag(panel, LV_OBJ_FLAG_SCROLLABLE); - if (group && inputdriver->hasPointerDevice()) - lv_indev_set_group(inputdriver->getPointer(), group); -} + auto enableButtons = [](lv_obj_t *obj, void *) -> lv_obj_tree_walk_res_t { + if (obj->class_p == &lv_button_class) { + lv_obj_clear_state(obj, LV_STATE_DISABLED); + } + return LV_OBJ_TREE_WALK_NEXT; + }; -void TFTView_320x240::setInputButtonLabel(void) -{ - // update input button label - std::string current_kbd = inputdriver->getCurrentKeyboardDevice(); - std::string current_ptr = inputdriver->getCurrentPointerDevice(); + lv_obj_tree_walk(panel, enableButtons, NULL); + } - char label[40]; - lv_snprintf(label, sizeof(label), _("Input Control: %s/%s"), current_ptr.c_str(), current_kbd.c_str()); - lv_label_set_text(objects.basic_settings_input_label, label); -} -// -------- helpers -------- + /** + * Disable underlying panel with it's children buttons and scrollbar + */ + void TFTView_320x240::disablePanel(lv_obj_t * panel) + { + lv_obj_add_state(panel, LV_STATE_DISABLED); + lv_obj_set_scrollbar_mode(panel, LV_SCROLLBAR_MODE_OFF); + lv_obj_clear_flag(panel, LV_OBJ_FLAG_SCROLLABLE); -void TFTView_320x240::removeNode(uint32_t nodeNum) -{ - auto it = nodes.find(nodeNum); - if (it != nodes.end()) { - } -} + auto disableButtons = [](lv_obj_t *obj, void *) -> lv_obj_tree_walk_res_t { + if (obj->class_p == &lv_button_class) { + lv_obj_add_state(obj, LV_STATE_DISABLED); + } + return LV_OBJ_TREE_WALK_NEXT; + }; -void TFTView_320x240::setNodeImage(uint32_t nodeNum, eRole role, bool unmessagable, lv_obj_t *img) -{ - uint32_t bgColor, fgColor; - std::tie(bgColor, fgColor) = nodeColor(nodeNum); - if (unmessagable) { - lv_image_set_src(img, &img_unmessagable_image); - lv_obj_set_style_border_color(img, lv_color_hex(bgColor), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(img, lv_color_hex(0x202020), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_img_recolor(img, lv_color_hex(0xFF5555), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_img_recolor_opa(img, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - return; - } else { - switch (role) { - case client: - case client_mute: - case client_hidden: - case tak: { - lv_image_set_src(img, &img_node_client_image); - break; - } - case router_client: { - lv_image_set_src(img, &img_top_nodes_image); - break; + lv_obj_tree_walk(panel, disableButtons, NULL); } - case repeater: - case router: - case router_late: { - lv_image_set_src(img, &img_node_router_image); - break; - } - case tracker: - case sensor: - case lost_and_found: - case tak_tracker: { - lv_image_set_src(img, &img_node_sensor_image); - break; - } - case unknown: { - lv_image_set_src(img, &img_circle_question_image); - break; + + /** + * Set focus to first button of a panel + */ + void TFTView_320x240::setGroupFocus(lv_obj_t * panel) + { + if (panel == objects.home_panel) { + lv_group_focus_obj(objects.home_mail_button); + } else if (panel == objects.nodes_panel) { + lv_group_focus_obj(objects.node_button); + } else if (panel == objects.groups_panel) { + lv_group_focus_obj(objects.channel_button0); + } else if (panel == objects.messages_panel) { + lv_group_focus_obj(objects.message_input_area); + } else if (panel == objects.chats_panel) { + if (chats.size() > 0) { + lv_group_focus_obj(panel->spec_attr->children[1]); // TODO: does not work + } + } else if (panel == objects.map_panel) { + + } else if (panel == objects.settings_screen_lock_panel) { + lv_group_focus_obj(objects.screen_lock_button_matrix); + } else if (panel == objects.controller_panel) { + lv_group_focus_obj(objects.basic_settings_user_button); + } else { + for (int i = 0; i < lv_obj_get_child_count(panel); i++) { + if (panel->spec_attr->children[i]->class_p == &lv_button_class) { + lv_group_focus_obj(panel->spec_attr->children[i]); + break; + } + } + } } - default: - lv_image_set_src(img, &img_node_client_image); - break; + + /** + * input group used by keyboard and/or pointer for dynamic assignment + */ + void TFTView_320x240::setInputGroup(void) + { + lv_group_t *group = lv_group_get_default(); + + if (group && inputdriver->hasKeyboardDevice()) + lv_indev_set_group(inputdriver->getKeyboard(), group); + + if (group && inputdriver->hasPointerDevice()) + lv_indev_set_group(inputdriver->getPointer(), group); } - } - lv_obj_set_style_bg_color(img, lv_color_hex(bgColor), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(img, lv_color_hex(bgColor), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_img_recolor_opa(img, fgColor ? 0 : 255, LV_PART_MAIN | LV_STATE_DEFAULT); -} -void TFTView_320x240::updateNodesStatus(void) -{ - char buf[40]; - lv_snprintf(buf, sizeof(buf), _p("%d of %d nodes online", nodeCount), nodesOnline, nodeCount); - lv_label_set_text(objects.home_nodes_label, buf); + void TFTView_320x240::setInputButtonLabel(void) + { + // update input button label + std::string current_kbd = inputdriver->getCurrentKeyboardDevice(); + std::string current_ptr = inputdriver->getCurrentPointerDevice(); - if (nodesFiltered) - lv_snprintf(buf, sizeof(buf), _("Filter: %d of %d nodes"), nodeCount - nodesFiltered, nodeCount); - lv_label_set_text(objects.top_nodes_online_label, buf); -} + char label[40]; + lv_snprintf(label, sizeof(label), _("Input Control: %s/%s"), current_ptr.c_str(), current_kbd.c_str()); + lv_label_set_text(objects.basic_settings_input_label, label); + } + // -------- helpers -------- -/** - * @brief Dynamically update all nodes filter and highlight - * Because the update can take quite some time (tens of ms) it is done in smaller - * chunks of 10 nodes per invocation, so it must be periodically called - * TODO: check for side effects if new nodes are inserted or removed during filter processing - * @param reset indicates to start update from beginning of node list otherwise - * continue with iterator position or skip if done - */ -void TFTView_320x240::updateNodesFiltered(bool reset) -{ - static auto it = nodes.begin(); - if (reset || nodesChanged) { - nodesFiltered = 0; - nodesChanged = false; - processingFilter = true; - it = nodes.begin(); - } + void TFTView_320x240::removeNode(uint32_t nodeNum) + { + auto it = nodes.find(nodeNum); + if (it != nodes.end()) { + } + } - for (int i = 0; i < 10 && it != nodes.end(); i++) { - applyNodesFilter(it->first, true); - it++; - } + void TFTView_320x240::setNodeImage(uint32_t nodeNum, eRole role, bool unmessagable, lv_obj_t *img) + { + uint32_t bgColor, fgColor; + std::tie(bgColor, fgColor) = nodeColor(nodeNum); + if (unmessagable) { + lv_image_set_src(img, &img_unmessagable_image); + lv_obj_set_style_border_color(img, lv_color_hex(bgColor), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(img, lv_color_hex(0x202020), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_img_recolor(img, lv_color_hex(0xFF5555), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_img_recolor_opa(img, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + return; + } else { + switch (role) { + case client: + case client_mute: + case client_hidden: + case tak: { + lv_image_set_src(img, &img_node_client_image); + break; + } + case router_client: { + lv_image_set_src(img, &img_top_nodes_image); + break; + } + case repeater: + case router: + case router_late: { + lv_image_set_src(img, &img_node_router_image); + break; + } + case tracker: + case sensor: + case lost_and_found: + case tak_tracker: { + lv_image_set_src(img, &img_node_sensor_image); + break; + } + case unknown: { + lv_image_set_src(img, &img_circle_question_image); + break; + } + default: + lv_image_set_src(img, &img_node_client_image); + break; + } + } + lv_obj_set_style_bg_color(img, lv_color_hex(bgColor), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_color(img, lv_color_hex(bgColor), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_img_recolor_opa(img, fgColor ? 0 : 255, LV_PART_MAIN | LV_STATE_DEFAULT); + } - if (it == nodes.end()) { - processingFilter = false; - } - updateNodesStatus(); -} + void TFTView_320x240::updateNodesStatus(void) + { + char buf[40]; + lv_snprintf(buf, sizeof(buf), _p("%d of %d nodes online", nodeCount), nodesOnline, nodeCount); + lv_label_set_text(objects.home_nodes_label, buf); + + if (nodesFiltered) + lv_snprintf(buf, sizeof(buf), _("Filter: %d of %d nodes"), nodeCount - nodesFiltered, nodeCount); + lv_label_set_text(objects.top_nodes_online_label, buf); + } + + /** + * @brief Dynamically update all nodes filter and highlight + * Because the update can take quite some time (tens of ms) it is done in smaller + * chunks of 10 nodes per invocation, so it must be periodically called + * TODO: check for side effects if new nodes are inserted or removed during filter processing + * @param reset indicates to start update from beginning of node list otherwise + * continue with iterator position or skip if done + */ + void TFTView_320x240::updateNodesFiltered(bool reset) + { + static auto it = nodes.begin(); + if (reset || nodesChanged) { + nodesFiltered = 0; + nodesChanged = false; + processingFilter = true; + it = nodes.begin(); + } -/** - * @brief Update last heard display/user_data/counter to current time - * - * @param nodeNum - */ -void TFTView_320x240::updateLastHeard(uint32_t nodeNum) -{ - auto it = nodes.find(nodeNum); - if (it != nodes.end() && it->second) { - time_t lastHeard = (time_t)it->second->LV_OBJ_IDX(node_lh_idx)->user_data; - it->second->LV_OBJ_IDX(node_lh_idx)->user_data = (void *)curtime; - lv_label_set_text(it->second->LV_OBJ_IDX(node_lh_idx), _("now")); - if (it->first != ownNode) { - if (lastHeard > 0 && curtime - lastHeard >= secs_until_offline) { - nodesOnline++; - applyNodesFilter(nodeNum); - updateNodesStatus(); + for (int i = 0; i < 10 && it != nodes.end(); i++) { + applyNodesFilter(it->first, true); + it++; } - // move to top position - lv_obj_move_to_index(it->second, 1); - // re-arrange the group linked list i.e. move the node after the top position - lv_ll_t *lv_group_ll = &lv_group_get_default()->obj_ll; - void *act = it->second->LV_OBJ_IDX(node_btn_idx)->user_data; - if (lv_group_ll && act) - _lv_ll_move_before(lv_group_ll, act, _lv_ll_get_next(lv_group_ll, topNodeLL)); + if (it == nodes.end()) { + processingFilter = false; + } + updateNodesStatus(); } - } -} -/** - * @brief update last heard display for all nodes; also update nodes online - * - */ -void TFTView_320x240::updateAllLastHeard(void) -{ - uint16_t online = 0; - time_t lastHeard; - for (auto it : nodes) { - char buf[32]; - if (it.first == ownNode) { // own node is always now, so do update - lastHeard = curtime; - it.second->LV_OBJ_IDX(node_lh_idx)->user_data = (void *)lastHeard; - } else { - lastHeard = (time_t)it.second->LV_OBJ_IDX(node_lh_idx)->user_data; + /** + * @brief Update last heard display/user_data/counter to current time + * + * @param nodeNum + */ + void TFTView_320x240::updateLastHeard(uint32_t nodeNum) + { + auto it = nodes.find(nodeNum); + if (it != nodes.end() && it->second) { + time_t lastHeard = (time_t)it->second->LV_OBJ_IDX(node_lh_idx)->user_data; + it->second->LV_OBJ_IDX(node_lh_idx)->user_data = (void *)curtime; + lv_label_set_text(it->second->LV_OBJ_IDX(node_lh_idx), _("now")); + if (it->first != ownNode) { + if (lastHeard > 0 && curtime - lastHeard >= secs_until_offline) { + nodesOnline++; + applyNodesFilter(nodeNum); + updateNodesStatus(); + } + // move to top position + lv_obj_move_to_index(it->second, 1); + + // re-arrange the group linked list i.e. move the node after the top position + lv_ll_t *lv_group_ll = &lv_group_get_default()->obj_ll; + void *act = it->second->LV_OBJ_IDX(node_btn_idx)->user_data; + if (lv_group_ll && act) + _lv_ll_move_before(lv_group_ll, act, _lv_ll_get_next(lv_group_ll, topNodeLL)); + } + } } - if (lastHeard) { - bool isOnline = lastHeardToString(lastHeard, buf); - lv_label_set_text(it.second->LV_OBJ_IDX(node_lh_idx), buf); - if (isOnline) - online++; + + /** + * @brief update last heard display for all nodes; also update nodes online + * + */ + void TFTView_320x240::updateAllLastHeard(void) + { + uint16_t online = 0; + time_t lastHeard; + for (auto it : nodes) { + char buf[32]; + if (it.first == ownNode) { // own node is always now, so do update + lastHeard = curtime; + it.second->LV_OBJ_IDX(node_lh_idx)->user_data = (void *)lastHeard; + } else { + lastHeard = (time_t)it.second->LV_OBJ_IDX(node_lh_idx)->user_data; + } + if (lastHeard) { + bool isOnline = lastHeardToString(lastHeard, buf); + lv_label_set_text(it.second->LV_OBJ_IDX(node_lh_idx), buf); + if (isOnline) + online++; + } + } + nodesOnline = online; + updateNodesFiltered(true); + updateNodesStatus(); } - } - nodesOnline = online; - updateNodesFiltered(true); - updateNodesStatus(); -} -void TFTView_320x240::updateUnreadMessages(void) -{ - char buf[64]; - if (unreadMessages > 0) { - sprintf(buf, unreadMessages == 1 ? _("%d new message") : _("%d new messages"), unreadMessages); - lv_obj_set_style_bg_img_src(objects.home_mail_button, &img_home_mail_unread_button_image, - LV_PART_MAIN | LV_STATE_DEFAULT); - } else { - strcpy(buf, _("no new messages")); - lv_obj_set_style_bg_img_src(objects.home_mail_button, &img_home_mail_button_image, LV_PART_MAIN | LV_STATE_DEFAULT); - } - lv_label_set_text(objects.home_mail_label, buf); -} + void TFTView_320x240::updateUnreadMessages(void) + { + char buf[64]; + if (unreadMessages > 0) { + sprintf(buf, unreadMessages == 1 ? _("%d new message") : _("%d new messages"), unreadMessages); + lv_obj_set_style_bg_img_src(objects.home_mail_button, &img_home_mail_unread_button_image, + LV_PART_MAIN | LV_STATE_DEFAULT); + } else { + strcpy(buf, _("no new messages")); + lv_obj_set_style_bg_img_src(objects.home_mail_button, &img_home_mail_button_image, + LV_PART_MAIN | LV_STATE_DEFAULT); + } + lv_label_set_text(objects.home_mail_label, buf); + } -/** - * @brief Called once a second to update time label - * - */ -void TFTView_320x240::updateTime(void) -{ - char buf[80]; - time_t curr_time; + /** + * @brief Called once a second to update time label + * + */ + void TFTView_320x240::updateTime(void) + { + char buf[80]; + time_t curr_time; #ifdef ARCH_PORTDUINO - time(&curr_time); + time(&curr_time); #else - curr_time = actTime; + curr_time = actTime; #endif - tm *curr_tm = localtime(&curr_time); + tm *curr_tm = localtime(&curr_time); - int len = 0; - if (VALID_TIME(curr_time) && (unsigned long)objects.home_time_button->user_data == 0) { - if (db.config.display.use_12h_clock) { - len = strftime(buf, 40, "%I:%M:%S %p\n%a %d-%b-%g", curr_tm); - } else { - len = strftime(buf, 40, "%T %Z%z\n%a %d-%b-%g", curr_tm); - } - } else { - uint32_t uptime = millis() / 1000; - int hours = uptime / 3600; - uptime -= hours * 3600; - int minutes = uptime / 60; - int seconds = uptime - minutes * 60; + int len = 0; + if (VALID_TIME(curr_time) && (unsigned long)objects.home_time_button->user_data == 0) { + if (db.config.display.use_12h_clock) { + len = strftime(buf, 40, "%I:%M:%S %p\n%a %d-%b-%g", curr_tm); + } else { + len = strftime(buf, 40, "%T %Z%z\n%a %d-%b-%g", curr_tm); + } + } else { + uint32_t uptime = millis() / 1000; + int hours = uptime / 3600; + uptime -= hours * 3600; + int minutes = uptime / 60; + int seconds = uptime - minutes * 60; - sprintf(&buf[len], _("uptime: %02d:%02d:%02d"), hours, minutes, seconds); - } - lv_label_set_text(objects.home_time_label, buf); -} + sprintf(&buf[len], _("uptime: %02d:%02d:%02d"), hours, minutes, seconds); + } + lv_label_set_text(objects.home_time_label, buf); + } -bool TFTView_320x240::updateSDCard(void) -{ - formatSD = false; - if (sdCard) { - delete sdCard; - sdCard = nullptr; - } + bool TFTView_320x240::updateSDCard(void) + { + formatSD = false; + if (sdCard) { + delete sdCard; + sdCard = nullptr; + } #ifdef HAS_SDCARD - char buf[64]; + char buf[64]; #ifdef HAS_SD_MMC - sdCard = new SDCard; + sdCard = new SDCard; #else - sdCard = new SdFsCard; + sdCard = new SdFsCard; #endif - ISdCard::ErrorType err = ISdCard::ErrorType::eNoError; - if (sdCard->init() && sdCard->cardType() != ISdCard::eNone) { - ILOG_DEBUG("SdCard init successful, card type: %d", sdCard->cardType()); - ISdCard::CardType cardType = sdCard->cardType(); - ISdCard::FatType fatType = sdCard->fatType(); - uint32_t usedSpace = sdCard->usedBytes() / (1024 * 1024); - uint32_t totalSpace = sdCard->cardSize() / (1024 * 1024); - uint32_t totalSpaceGB = (sdCard->cardSize() + 500000000ULL) / (1000ULL * 1000ULL * 1000ULL); - - sprintf(buf, _("%s: %d GB (%s)\nUsed: %0.2f GB (%d%%)"), - cardType == ISdCard::eMMC ? "MMC" - : cardType == ISdCard::eSD ? "SDSC" - : cardType == ISdCard::eSDHC ? "SDHC" - : cardType == ISdCard::eSDXC ? "SDXC" - : "UNKN", - totalSpaceGB, - fatType == ISdCard::eExFat ? "exFAT" - : fatType == ISdCard::eFat32 ? "FAT32" - : fatType == ISdCard::eFat16 ? "FAT16" - : "???", - float(sdCard->usedBytes()) / 1024.0f / 1024.0f / 1024.0f, - totalSpace ? ((usedSpace * 100) + totalSpace / 2) / totalSpace : 0); - Themes::recolorButton(objects.home_sd_card_button, true); - Themes::recolorText(objects.home_sd_card_label, true); - cardDetected = true; - } else { - ILOG_DEBUG("SdFsCard init failed"); - err = sdCard->errorType(); - delete sdCard; - sdCard = nullptr; - } + ISdCard::ErrorType err = ISdCard::ErrorType::eNoError; + if (sdCard->init() && sdCard->cardType() != ISdCard::eNone) { + ILOG_DEBUG("SdCard init successful, card type: %d", sdCard->cardType()); + ISdCard::CardType cardType = sdCard->cardType(); + ISdCard::FatType fatType = sdCard->fatType(); + uint32_t usedSpace = sdCard->usedBytes() / (1024 * 1024); + uint32_t totalSpace = sdCard->cardSize() / (1024 * 1024); + uint32_t totalSpaceGB = (sdCard->cardSize() + 500000000ULL) / (1000ULL * 1000ULL * 1000ULL); + + sprintf(buf, _("%s: %d GB (%s)\nUsed: %0.2f GB (%d%%)"), + cardType == ISdCard::eMMC ? "MMC" + : cardType == ISdCard::eSD ? "SDSC" + : cardType == ISdCard::eSDHC ? "SDHC" + : cardType == ISdCard::eSDXC ? "SDXC" + : "UNKN", + totalSpaceGB, + fatType == ISdCard::eExFat ? "exFAT" + : fatType == ISdCard::eFat32 ? "FAT32" + : fatType == ISdCard::eFat16 ? "FAT16" + : "???", + float(sdCard->usedBytes()) / 1024.0f / 1024.0f / 1024.0f, + totalSpace ? ((usedSpace * 100) + totalSpace / 2) / totalSpace : 0); + Themes::recolorButton(objects.home_sd_card_button, true); + Themes::recolorText(objects.home_sd_card_label, true); + cardDetected = true; + } else { + ILOG_DEBUG("SdFsCard init failed"); + err = sdCard->errorType(); + delete sdCard; + sdCard = nullptr; + } - if (!cardDetected || err != ISdCard::ErrorType::eNoError) { - switch (err) { - case ISdCard::ErrorType::eSlotEmpty: - ILOG_ERROR("SD card slot empty"); - lv_snprintf(buf, sizeof(buf), _("SD slot empty")); - break; - case ISdCard::ErrorType::eFormatError: - ILOG_ERROR("SD invalid format"); - lv_snprintf(buf, sizeof(buf), _("SD invalid format")); - formatSD = true; - break; - case ISdCard::ErrorType::eNoMbrError: - ILOG_ERROR("SD mbr not found"); - lv_snprintf(buf, sizeof(buf), _("SD mbr not found")); - formatSD = true; - break; - case ISdCard::ErrorType::eCardError: - ILOG_ERROR("SD card error"); - lv_snprintf(buf, sizeof(buf), _("SD card error")); - break; - default: - ILOG_ERROR("SD unknown error"); - lv_snprintf(buf, sizeof(buf), _("SD unknown error")); - break; - } - Themes::recolorButton(objects.home_sd_card_button, false); - Themes::recolorText(objects.home_sd_card_label, false); - // allow backup/restore only if there is an SD card detected - lv_obj_add_state(objects.basic_settings_backup_restore_button, LV_STATE_DISABLED); - } else { - // enable backup/restore - lv_obj_clear_state(objects.basic_settings_backup_restore_button, LV_STATE_DISABLED); - } - lv_label_set_text(objects.home_sd_card_label, buf); + if (!cardDetected || err != ISdCard::ErrorType::eNoError) { + switch (err) { + case ISdCard::ErrorType::eSlotEmpty: + ILOG_ERROR("SD card slot empty"); + lv_snprintf(buf, sizeof(buf), _("SD slot empty")); + break; + case ISdCard::ErrorType::eFormatError: + ILOG_ERROR("SD invalid format"); + lv_snprintf(buf, sizeof(buf), _("SD invalid format")); + formatSD = true; + break; + case ISdCard::ErrorType::eNoMbrError: + ILOG_ERROR("SD mbr not found"); + lv_snprintf(buf, sizeof(buf), _("SD mbr not found")); + formatSD = true; + break; + case ISdCard::ErrorType::eCardError: + ILOG_ERROR("SD card error"); + lv_snprintf(buf, sizeof(buf), _("SD card error")); + break; + default: + ILOG_ERROR("SD unknown error"); + lv_snprintf(buf, sizeof(buf), _("SD unknown error")); + break; + } + Themes::recolorButton(objects.home_sd_card_button, false); + Themes::recolorText(objects.home_sd_card_label, false); + // allow backup/restore only if there is an SD card detected + lv_obj_add_state(objects.basic_settings_backup_restore_button, LV_STATE_DISABLED); + } else { + // enable backup/restore + lv_obj_clear_state(objects.basic_settings_backup_restore_button, LV_STATE_DISABLED); + } + lv_label_set_text(objects.home_sd_card_label, buf); #else - lv_obj_add_flag(objects.home_sd_card_button, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(objects.home_sd_card_label, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(objects.home_sd_card_button, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(objects.home_sd_card_label, LV_OBJ_FLAG_HIDDEN); #if defined(ARCH_PORTDUINO) - cardDetected = true; // use PortduinoFS instead - sdCard = new SDCard; + cardDetected = true; // use PortduinoFS instead + sdCard = new SDCard; #endif #endif - if (!sdCard) - sdCard = new NoSdCard; - return cardDetected; -} + if (!sdCard) + sdCard = new NoSdCard; + return cardDetected; + } -void TFTView_320x240::formatSDCard(void) -{ - if (sdCard) { - delete sdCard; - sdCard = nullptr; - } + void TFTView_320x240::formatSDCard(void) + { + if (sdCard) { + delete sdCard; + sdCard = nullptr; + } #ifdef HAS_SDCARD #ifdef HAS_SD_MMC - sdCard = new SDCard; + sdCard = new SDCard; #else - sdCard = new SdFsCard; + sdCard = new SdFsCard; #endif - ILOG_DEBUG("formatting SD card"); - if (sdCard->format()) { - updateSDCard(); - } else { - lv_label_set_text(objects.home_sd_card_label, "SD format failed"); - } + ILOG_DEBUG("formatting SD card"); + if (sdCard->format()) { + updateSDCard(); + } else { + lv_label_set_text(objects.home_sd_card_label, "SD format failed"); + } #endif - if (!sdCard) - sdCard = new NoSdCard; -} + if (!sdCard) + sdCard = new NoSdCard; + } -void TFTView_320x240::updateFreeMem(void) -{ - // only update if HomePanel is active (since this is some critical code that did crash sporadically) - if (activePanel == objects.home_panel && (unsigned long)objects.home_memory_button->user_data) { - char buf[64]; - uint32_t freeHeap = 0; - uint32_t freeHeap_pct = 0; + void TFTView_320x240::updateFreeMem(void) + { + // only update if HomePanel is active (since this is some critical code that did crash sporadically) + if (activePanel == objects.home_panel && (unsigned long)objects.home_memory_button->user_data) { + char buf[64]; + uint32_t freeHeap = 0; + uint32_t freeHeap_pct = 0; - lv_mem_monitor_t mon; - lv_mem_monitor(&mon); + lv_mem_monitor_t mon; + lv_mem_monitor(&mon); #ifdef ARDUINO_ARCH_ESP32 - freeHeap = ESP.getFreeHeap(); - freeHeap_pct = 100 * freeHeap / ESP.getHeapSize(); - sprintf(buf, _("Heap: %d (%d%%)\nLVGL: %d (%d%%)"), freeHeap, freeHeap_pct, mon.free_size, 100 - mon.used_pct); + freeHeap = ESP.getFreeHeap(); + freeHeap_pct = 100 * freeHeap / ESP.getHeapSize(); + sprintf(buf, _("Heap: %d (%d%%)\nLVGL: %d (%d%%)"), freeHeap, freeHeap_pct, mon.free_size, 100 - mon.used_pct); #elif defined(ARCH_PORTDUINO) - static uint32_t totalMem = LinuxHelper::getTotalMem(); - if (totalMem != 0) { - freeHeap = LinuxHelper::getAvailableMem(); - freeHeap_pct = 100 * freeHeap / totalMem; - } - sprintf(buf, _("Heap: %d (%d%%)\nLVGL: %d (%d%%)"), freeHeap, freeHeap_pct, mon.free_size / 1024, 100 - mon.used_pct); + static uint32_t totalMem = LinuxHelper::getTotalMem(); + if (totalMem != 0) { + freeHeap = LinuxHelper::getAvailableMem(); + freeHeap_pct = 100 * freeHeap / totalMem; + } + sprintf(buf, _("Heap: %d (%d%%)\nLVGL: %d (%d%%)"), freeHeap, freeHeap_pct, mon.free_size / 1024, + 100 - mon.used_pct); #else - buf[0] = '\0'; + buf[0] = '\0'; #endif - lv_label_set_text(objects.home_memory_label, buf); - } -} + lv_label_set_text(objects.home_memory_label, buf); + } + } -void TFTView_320x240::task_handler(void) -{ - MeshtasticView::task_handler(); + void TFTView_320x240::task_handler(void) + { + MeshtasticView::task_handler(); - if (screensInitialised) { - if (map) - map->task_handler(); + if (screensInitialised) { + if (map) + map->task_handler(); - if (curtime - lastrun1 >= 1) { // call every 1s - if (map) { - updateLocationMap(THIS->map->getObjectsOnMap()); - } + if (curtime - lastrun1 >= 1) { // call every 1s + if (map) { + updateLocationMap(THIS->map->getObjectsOnMap()); + } - lastrun1 = curtime; - actTime++; - updateTime(); + lastrun1 = curtime; + actTime++; + updateTime(); - if (curtime - lastrun5 >= 5) { // call every 5s - lastrun5 = curtime; - if (scans > 0 && activePanel == objects.signal_scanner_panel) { - scanSignal(scans); - scans--; - } - if (startTime) { - if (curtime - startTime > 30) { - lv_label_set_text(objects.trace_route_start_label, _("Start")); - lv_obj_set_style_outline_color(objects.trace_route_start_button, colorMesh, - LV_PART_MAIN | LV_STATE_DEFAULT); - removeSpinner(); - } else { - char buf[16]; - sprintf(buf, "%ds", ((35 - (curtime - startTime)) / 5) * 5); - lv_label_set_text(objects.trace_route_start_label, buf); + if (curtime - lastrun5 >= 5) { // call every 5s + lastrun5 = curtime; + if (scans > 0 && activePanel == objects.signal_scanner_panel) { + scanSignal(scans); + scans--; + } + if (startTime) { + if (curtime - startTime > 30) { + lv_label_set_text(objects.trace_route_start_label, _("Start")); + lv_obj_set_style_outline_color(objects.trace_route_start_button, colorMesh, + LV_PART_MAIN | LV_STATE_DEFAULT); + removeSpinner(); + } else { + char buf[16]; + sprintf(buf, "%ds", ((35 - (curtime - startTime)) / 5) * 5); + lv_label_set_text(objects.trace_route_start_label, buf); + } + } } - } - } - if (curtime - lastrun10 >= 10) { // call every 10s - lastrun10 = curtime; - updateFreeMem(); + if (curtime - lastrun10 >= 10) { // call every 10s + lastrun10 = curtime; + updateFreeMem(); - if ((db.config.network.wifi_enabled || db.module_config.mqtt.enabled) && !displaydriver->isPowersaving()) { - controller->requestDeviceConnectionStatus(); - } - } - if (curtime - lastrun60 >= 60) { // call every 60s - lastrun60 = curtime; - updateAllLastHeard(); + if ((db.config.network.wifi_enabled || db.module_config.mqtt.enabled) && + !displaydriver->isPowersaving()) { + controller->requestDeviceConnectionStatus(); + } + } + if (curtime - lastrun60 >= 60) { // call every 60s + lastrun60 = curtime; + updateAllLastHeard(); - if (detectorRunning) { - controller->sendPing(); - } + if (detectorRunning) { + controller->sendPing(); + } - // if we didn't hear any node for 1h assume we have no signal - if (curtime - lastHeard > secs_until_offline) { - lv_obj_set_style_bg_image_src(objects.home_signal_button, &img_home_no_signal_image, - LV_PART_MAIN | LV_STATE_DEFAULT); - lv_label_set_text(objects.home_signal_label, _("no signal")); - lv_label_set_text(objects.home_signal_pct_label, ""); + // if we didn't hear any node for 1h assume we have no signal + if (curtime - lastHeard > secs_until_offline) { + lv_obj_set_style_bg_image_src(objects.home_signal_button, &img_home_no_signal_image, + LV_PART_MAIN | LV_STATE_DEFAULT); + lv_label_set_text(objects.home_signal_label, _("no signal")); + lv_label_set_text(objects.home_signal_pct_label, ""); + } + } + } + if (processingFilter || nodesChanged) { + updateNodesFiltered(nodesChanged); } } } - if (processingFilter || nodesChanged) { - updateNodesFiltered(nodesChanged); - } - } -} -// === lvgl C style callbacks === + // === lvgl C style callbacks === -extern "C" { + extern "C" { -void action_on_boot_screen_displayed(lv_event_t *e) -{ - ILOG_DEBUG("action_on_boot_screen_displayed()"); -} -} + void action_on_boot_screen_displayed(lv_event_t *e) + { + ILOG_DEBUG("action_on_boot_screen_displayed()"); + } + } #endif From 796cbf5fd8addedddc1dd66725af00906eef4b2d Mon Sep 17 00:00:00 2001 From: Juan Pena Date: Wed, 28 Jan 2026 20:05:07 -0500 Subject: [PATCH 09/14] Keyboard Backtrack --- generated/ui_320x240/screens.c | 88 + generated/ui_320x240/screens.h | 13 + include/graphics/view/TFT/TFTView_320x240.h | 10 +- source/graphics/TFT/TFTView_320x240.cpp | 10482 +++++++++--------- 4 files changed, 5417 insertions(+), 5176 deletions(-) diff --git a/generated/ui_320x240/screens.c b/generated/ui_320x240/screens.c index ea8ada9a..7677c244 100644 --- a/generated/ui_320x240/screens.c +++ b/generated/ui_320x240/screens.c @@ -1268,6 +1268,12 @@ void create_screen_main_screen() { lv_obj_set_style_text_color(obj, lv_color_hex(0xffffffff), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_color(obj, lv_color_hex(0xff1f2a37), LV_PART_MAIN | LV_STATE_DEFAULT); } + { + // KeyboardButton_0 + lv_obj_t *obj = lv_btn_create(parent_obj); + objects.keyboard_button_0 = obj; + lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); + } } } { @@ -3124,6 +3130,12 @@ void create_screen_main_screen() { lv_obj_set_style_align(obj, LV_ALIGN_TOP_MID, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_text_align(obj, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN | LV_STATE_DEFAULT); } + { + // KeyboardButton_1 + lv_obj_t *obj = lv_btn_create(parent_obj); + objects.keyboard_button_1 = obj; + lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); + } { lv_obj_t *obj = lv_label_create(parent_obj); lv_obj_set_pos(obj, 0, 55); @@ -3148,6 +3160,12 @@ void create_screen_main_screen() { lv_obj_set_style_align(obj, LV_ALIGN_TOP_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_text_align(obj, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN | LV_STATE_DEFAULT); } + { + // KeyboardButton_2 + lv_obj_t *obj = lv_btn_create(parent_obj); + objects.keyboard_button_2 = obj; + lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); + } { lv_obj_t *obj = lv_obj_create(parent_obj); objects.obj3 = obj; @@ -3692,6 +3710,12 @@ void create_screen_main_screen() { lv_obj_set_style_align(obj, LV_ALIGN_TOP_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_text_align(obj, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN | LV_STATE_DEFAULT); } + { + // KeyboardButton_8 + lv_obj_t *obj = lv_btn_create(parent_obj); + objects.keyboard_button_8 = obj; + lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); + } { lv_obj_t *obj = lv_label_create(parent_obj); lv_obj_set_pos(obj, 0, 55); @@ -3716,6 +3740,12 @@ void create_screen_main_screen() { lv_obj_set_style_align(obj, LV_ALIGN_TOP_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_text_align(obj, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN | LV_STATE_DEFAULT); } + { + // KeyboardButton_9 + lv_obj_t *obj = lv_btn_create(parent_obj); + objects.keyboard_button_9 = obj; + lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); + } { lv_obj_t *obj = lv_obj_create(parent_obj); objects.obj8 = obj; @@ -4010,6 +4040,12 @@ void create_screen_main_screen() { create_user_widget_ok_cancel_widget(obj, 315); lv_obj_set_style_align(obj, LV_ALIGN_BOTTOM_MID, LV_PART_MAIN | LV_STATE_DEFAULT); } + { + // KeyboardButton_7 + lv_obj_t *obj = lv_btn_create(parent_obj); + objects.keyboard_button_7 = obj; + lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); + } } } { @@ -4438,6 +4474,12 @@ void create_screen_main_screen() { lv_obj_set_style_text_align(obj, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_pad_left(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT); } + { + // KeyboardButton_3 + lv_obj_t *obj = lv_btn_create(parent_obj); + objects.keyboard_button_3 = obj; + lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); + } { lv_obj_t *obj = lv_label_create(parent_obj); objects.obj20 = obj; @@ -4482,6 +4524,12 @@ void create_screen_main_screen() { lv_obj_set_style_bg_image_recolor(obj, lv_color_hex(0xff373737), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_image_recolor_opa(obj, 255, LV_PART_MAIN | LV_STATE_DEFAULT); } + { + // KeyboardButton_4 + lv_obj_t *obj = lv_btn_create(parent_obj); + objects.keyboard_button_4 = obj; + lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); + } { // SettingsModifyTrashButton lv_obj_t *obj = lv_btn_create(parent_obj); @@ -4839,6 +4887,12 @@ void create_screen_main_screen() { lv_obj_set_style_max_height(obj, 25, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_pad_bottom(obj, 2, LV_PART_MAIN | LV_STATE_DEFAULT); } + { + // KeyboardButton_5 + lv_obj_t *obj = lv_btn_create(parent_obj); + objects.keyboard_button_5 = obj; + lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); + } } } } @@ -5017,6 +5071,12 @@ void create_screen_main_screen() { lv_obj_set_style_max_height(obj, 25, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_pad_bottom(obj, 2, LV_PART_MAIN | LV_STATE_DEFAULT); } + { + // KeyboardButton_6 + lv_obj_t *obj = lv_btn_create(parent_obj); + objects.keyboard_button_6 = obj; + lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); + } } } } @@ -6091,6 +6151,12 @@ void create_screen_main_screen() { lv_obj_set_style_align(obj, LV_ALIGN_TOP_MID, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_text_align(obj, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN | LV_STATE_DEFAULT); } + { + // KeyboardButton_10 + lv_obj_t *obj = lv_btn_create(parent_obj); + objects.keyboard_button_10 = obj; + lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); + } { lv_obj_t *obj = lv_label_create(parent_obj); lv_obj_set_pos(obj, 0, 100); @@ -6115,6 +6181,12 @@ void create_screen_main_screen() { lv_obj_set_style_align(obj, LV_ALIGN_TOP_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_text_align(obj, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN | LV_STATE_DEFAULT); } + { + // KeyboardButton_11 + lv_obj_t *obj = lv_btn_create(parent_obj); + objects.keyboard_button_11 = obj; + lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); + } { lv_obj_t *obj = lv_obj_create(parent_obj); objects.obj27 = obj; @@ -6167,6 +6239,22 @@ void create_screen_main_screen() { } } } + { + // Keyboard + lv_obj_t *obj = lv_keyboard_create(parent_obj); + objects.keyboard = obj; + /*lv_obj_set_pos(obj, 1, 28); + lv_obj_set_size(obj, LV_PCT(100), LV_PCT(50)); + lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_align(obj, LV_ALIGN_TOP_MID, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_color(obj, lv_color_hex(0xff1b43db), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_font(obj, &lv_font_montserrat_16, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(obj, lv_color_hex(0xffccd1d8), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_max_height(obj, 300, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_max_width(obj, 600, LV_PART_MAIN | LV_STATE_DEFAULT);*/ + lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); + } } tick_screen_main_screen(); diff --git a/generated/ui_320x240/screens.h b/generated/ui_320x240/screens.h index 1b809e27..c9deab19 100644 --- a/generated/ui_320x240/screens.h +++ b/generated/ui_320x240/screens.h @@ -160,6 +160,7 @@ typedef struct _objects_t { lv_obj_t *messages_panel; lv_obj_t *messages_container; lv_obj_t *message_input_area; + lv_obj_t *keyboard_button_0; lv_obj_t *chats_panel; lv_obj_t *chats_button; lv_obj_t *chats_button_label; @@ -298,7 +299,9 @@ typedef struct _objects_t { lv_obj_t *home_cancel_qr_button; lv_obj_t *settings_username_panel; lv_obj_t *settings_user_short_textarea; + lv_obj_t *keyboard_button_1; lv_obj_t *settings_user_long_textarea; + lv_obj_t *keyboard_button_2; lv_obj_t *obj3; lv_obj_t *obj3__ok_cancel_panel_w; lv_obj_t *obj3__ok_button_w; @@ -346,7 +349,9 @@ typedef struct _objects_t { lv_obj_t *obj7__cancel_button_w; lv_obj_t *settings_wifi_panel; lv_obj_t *settings_wifi_ssid_textarea; + lv_obj_t *keyboard_button_8; lv_obj_t *settings_wifi_password_textarea; + lv_obj_t *keyboard_button_9; lv_obj_t *obj8; lv_obj_t *obj8__ok_cancel_panel_w; lv_obj_t *obj8__ok_button_w; @@ -379,6 +384,7 @@ typedef struct _objects_t { lv_obj_t *obj12__ok_cancel_panel_w; lv_obj_t *obj12__ok_button_w; lv_obj_t *obj12__cancel_button_w; + lv_obj_t *keyboard_button_7; lv_obj_t *settings_input_control_panel; lv_obj_t *settings_mouse_input_dropdown; lv_obj_t *settings_keyboard_input_dropdown; @@ -423,9 +429,11 @@ typedef struct _objects_t { lv_obj_t *settings_modify_channel_panel; lv_obj_t *obj19; lv_obj_t *settings_modify_channel_name_textarea; + lv_obj_t *keyboard_button_3; lv_obj_t *obj20; lv_obj_t *settings_modify_channel_psk_textarea; lv_obj_t *settings_modify_channel_key_generate_button; + lv_obj_t *keyboard_button_4; lv_obj_t *settings_modify_trash_button; lv_obj_t *obj21; lv_obj_t *obj21__ok_cancel_panel_w; @@ -452,6 +460,7 @@ typedef struct _objects_t { lv_obj_t *nodes_filter_position_switch; lv_obj_t *nodes_filter_name_label; lv_obj_t *nodes_filter_name_area; + lv_obj_t *keyboard_button_5; lv_obj_t *tab_page_highlight; lv_obj_t *nodes_hl_active_chat_label; lv_obj_t *nodes_hl_active_chat_switch; @@ -463,6 +472,7 @@ typedef struct _objects_t { lv_obj_t *nodes_hliaq_switch; lv_obj_t *nodes_hl_name_label; lv_obj_t *nodes_hl_name_area; + lv_obj_t *keyboard_button_6; lv_obj_t *mesh_detector_panel; lv_obj_t *detector_radar_panel; lv_obj_t *radar_beam; @@ -528,13 +538,16 @@ typedef struct _objects_t { lv_obj_t *setup_panel; lv_obj_t *setup_region_dropdown; lv_obj_t *setup_user_short_textarea; + lv_obj_t *keyboard_button_10; lv_obj_t *setup_user_long_textarea; + lv_obj_t *keyboard_button_11; lv_obj_t *obj27; lv_obj_t *obj27__ok_cancel_panel_w; lv_obj_t *obj27__ok_button_w; lv_obj_t *obj27__cancel_button_w; lv_obj_t *alert_panel; lv_obj_t *alert_label; + lv_obj_t *keyboard; lv_obj_t *blank_screen_button; lv_obj_t *screen_lock_button_matrix; lv_obj_t *lock_screen_digits_label; diff --git a/include/graphics/view/TFT/TFTView_320x240.h b/include/graphics/view/TFT/TFTView_320x240.h index 69d7b0a8..ba6cb4bf 100644 --- a/include/graphics/view/TFT/TFTView_320x240.h +++ b/include/graphics/view/TFT/TFTView_320x240.h @@ -214,9 +214,8 @@ class TFTView_320x240 : public MeshtasticView void updateTheme(void); void ui_events_init(void); void ui_set_active(lv_obj_t *b, lv_obj_t *p, lv_obj_t *tp); - // Removed: Virtual keyboard not needed for T-Deck - // void showKeyboard(lv_obj_t *textArea); - // void hideKeyboard(lv_obj_t *panel); + void showKeyboard(lv_obj_t *textArea); + void hideKeyboard(lv_obj_t *panel); lv_obj_t *showQrCode(lv_obj_t *parent, const char *data); void enablePanel(lv_obj_t *panel); @@ -306,9 +305,8 @@ class TFTView_320x240 : public MeshtasticView // blank screen static void ui_event_BlankScreenButton(lv_event_t *e); - // Removed: Virtual keyboard event handlers not needed for T-Deck - // static void ui_event_KeyboardButton(lv_event_t *e); - // static void ui_event_Keyboard(lv_event_t *e); + static void ui_event_KeyboardButton(lv_event_t *e); + static void ui_event_Keyboard(lv_event_t *e); static void ui_event_message_ready(lv_event_t *e); diff --git a/source/graphics/TFT/TFTView_320x240.cpp b/source/graphics/TFT/TFTView_320x240.cpp index d605a72a..cba543e2 100644 --- a/source/graphics/TFT/TFTView_320x240.cpp +++ b/source/graphics/TFT/TFTView_320x240.cpp @@ -468,7 +468,9 @@ void TFTView_320x240::ui_set_active(lv_obj_t *b, lv_obj_t *p, lv_obj_t *tp) lv_obj_add_flag(activePanel, LV_OBJ_FLAG_HIDDEN); if (activePanel == objects.messages_panel) { lv_obj_remove_state(objects.message_input_area, LV_STATE_FOCUSED); - // Removed: keyboard handling + if (!lv_obj_has_flag(objects.keyboard, LV_OBJ_FLAG_HIDDEN)) { + hideKeyboard(objects.messages_panel); + } uint32_t channelOrNode = (unsigned long)activeMsgContainer->user_data; // remove empty messageContainer if we are leaving messages panel if (channelOrNode >= c_max_channels) { @@ -500,11 +502,11 @@ void TFTView_320x240::ui_set_active(lv_obj_t *b, lv_obj_t *p, lv_obj_t *tp) activePanel = p; if (activePanel == objects.messages_panel) { lv_group_focus_obj(objects.message_input_area); - } else if (inputdriver->hasEncoderDevice()) { + } else if (inputdriver->hasKeyboardDevice() || inputdriver->hasEncoderDevice()) { setGroupFocus(activePanel); } - // Removed: lv_obj_add_flag(objects.keyboard, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(objects.keyboard, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(objects.msg_popup_panel, LV_OBJ_FLAG_HIDDEN); } @@ -561,8 +563,8 @@ void TFTView_320x240::apply_hotfix(void) // fix size for 480 pixel height displays if (v >= 480) { - // keyboard size limit - REMOVED: Virtual keyboard not needed for T-Deck - // lv_obj_set_size(objects.keyboard, LV_PCT(100), LV_PCT(45)); + // keyboard size limit + lv_obj_set_size(objects.keyboard, LV_PCT(100), LV_PCT(45)); // resize channel buttons buttonSize = 40; @@ -584,7 +586,7 @@ void TFTView_320x240::apply_hotfix(void) lv_obj_set_style_text_font(objects.home_qr_label, &ui_font_montserrat_16, LV_PART_MAIN | LV_STATE_DEFAULT); } - // Removed: lv_obj_move_foreground(objects.keyboard); + lv_obj_move_foreground(objects.keyboard); lv_obj_add_flag(objects.detector_radar_panel, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(objects.detected_node_button, LV_OBJ_FLAG_HIDDEN); lv_label_set_text(objects.detector_start_label, _("Start")); @@ -744,8 +746,20 @@ void TFTView_320x240::ui_events_init(void) lv_obj_add_event_cb(objects.msg_restore_panel, this->ui_event_MsgRestoreButton, LV_EVENT_CLICKED, NULL); lv_obj_add_event_cb(objects.alert_panel, this->ui_event_AlertButton, LV_EVENT_CLICKED, NULL); - // keyboard - REMOVED: Virtual keyboard not needed for T-Deck - // Removed all keyboard event callbacks + // keyboard + lv_obj_add_event_cb(objects.keyboard, ui_event_Keyboard, LV_EVENT_CLICKED, this); + lv_obj_add_event_cb(objects.keyboard_button_0, ui_event_KeyboardButton, LV_EVENT_CLICKED, (void *)0); + lv_obj_add_event_cb(objects.keyboard_button_1, ui_event_KeyboardButton, LV_EVENT_CLICKED, (void *)1); + lv_obj_add_event_cb(objects.keyboard_button_2, ui_event_KeyboardButton, LV_EVENT_CLICKED, (void *)2); + lv_obj_add_event_cb(objects.keyboard_button_3, ui_event_KeyboardButton, LV_EVENT_CLICKED, (void *)3); + lv_obj_add_event_cb(objects.keyboard_button_4, ui_event_KeyboardButton, LV_EVENT_CLICKED, (void *)4); + lv_obj_add_event_cb(objects.keyboard_button_5, ui_event_KeyboardButton, LV_EVENT_CLICKED, (void *)5); + lv_obj_add_event_cb(objects.keyboard_button_6, ui_event_KeyboardButton, LV_EVENT_CLICKED, (void *)6); + lv_obj_add_event_cb(objects.keyboard_button_7, ui_event_KeyboardButton, LV_EVENT_CLICKED, (void *)7); + lv_obj_add_event_cb(objects.keyboard_button_8, ui_event_KeyboardButton, LV_EVENT_CLICKED, (void *)8); + lv_obj_add_event_cb(objects.keyboard_button_9, ui_event_KeyboardButton, LV_EVENT_CLICKED, (void *)9); + lv_obj_add_event_cb(objects.keyboard_button_10, ui_event_KeyboardButton, LV_EVENT_CLICKED, (void *)10); + lv_obj_add_event_cb(objects.keyboard_button_11, ui_event_KeyboardButton, LV_EVENT_CLICKED, (void *)11); // message text area lv_obj_add_event_cb(objects.message_input_area, ui_event_message_ready, LV_EVENT_ALL, NULL); @@ -1550,3917 +1564,3985 @@ void TFTView_320x240::ui_event_BlankScreenButton(lv_event_t *e) void TFTView_320x240::ui_event_KeyboardButton(lv_event_t *e) { - // REMOVED: Virtual keyboard not needed for T-Deck - // This function is no longer used - return; - - /** - * handle events for virtual keyboard - REMOVED: Not needed for T-Deck - */ - void TFTView_320x240::ui_event_Keyboard(lv_event_t * e) - { - // REMOVED: Virtual keyboard not needed for T-Deck - return; - - void TFTView_320x240::ui_event_message_ready(lv_event_t * e) - { - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_READY) { - char *txt = (char *)lv_textarea_get_text(objects.message_input_area); - uint32_t len = strlen(txt); - if (len) { - if (txt[len - 1] == ' ') { // use space+return combo to start new line in same message - lv_textarea_add_char(objects.message_input_area, CR_REPLACEMENT); - } else { - THIS->handleAddMessage(txt); - lv_textarea_set_text(objects.message_input_area, ""); - // Removed: keyboard handling - lv_group_focus_obj(objects.message_input_area); - } - } + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED) { + uint32_t keyBtnIdx = (unsigned long)e->user_data; + switch (keyBtnIdx) { + case 0: + if (lv_obj_has_flag(objects.keyboard, LV_OBJ_FLAG_HIDDEN)) { + lv_obj_remove_flag(objects.keyboard, LV_OBJ_FLAG_HIDDEN); + THIS->showKeyboard(objects.message_input_area); + } else { + THIS->hideKeyboard(objects.messages_panel); } + lv_group_focus_obj(objects.message_input_area); + return; // continue play animation, don't hide keyboard immediately + case 1: + THIS->showKeyboard(objects.settings_user_short_textarea); + lv_group_focus_obj(objects.settings_user_short_textarea); + break; + case 2: + THIS->showKeyboard(objects.settings_user_long_textarea); + lv_group_focus_obj(objects.settings_user_long_textarea); + break; + case 3: + THIS->showKeyboard(objects.settings_modify_channel_name_textarea); + lv_group_focus_obj(objects.settings_modify_channel_name_textarea); + break; + case 4: + THIS->showKeyboard(objects.settings_modify_channel_psk_textarea); + lv_group_focus_obj(objects.settings_modify_channel_psk_textarea); + break; + case 5: + THIS->showKeyboard(objects.nodes_filter_name_area); + lv_group_focus_obj(objects.nodes_filter_name_area); + break; + case 6: + THIS->showKeyboard(objects.nodes_hl_name_area); + lv_group_focus_obj(objects.nodes_hl_name_area); + break; + case 7: + THIS->showKeyboard(objects.settings_screen_lock_password_textarea); + lv_group_focus_obj(objects.settings_screen_lock_password_textarea); + break; + case 8: + THIS->showKeyboard(objects.settings_wifi_ssid_textarea); + lv_group_focus_obj(objects.settings_wifi_ssid_textarea); + break; + case 9: + THIS->showKeyboard(objects.settings_wifi_password_textarea); + lv_group_focus_obj(objects.settings_wifi_password_textarea); + break; + case 10: + THIS->showKeyboard(objects.setup_user_short_textarea); + lv_group_focus_obj(objects.setup_user_short_textarea); + break; + case 11: + THIS->showKeyboard(objects.setup_user_long_textarea); + lv_group_focus_obj(objects.setup_user_long_textarea); + break; + default: + ILOG_ERROR("missing keyboard <-> textarea assignment"); } + lv_obj_has_flag(objects.keyboard, LV_OBJ_FLAG_HIDDEN) ? lv_obj_remove_flag(objects.keyboard, LV_OBJ_FLAG_HIDDEN) + : lv_obj_add_flag(objects.keyboard, LV_OBJ_FLAG_HIDDEN); + } +} - // basic settings buttons - - void TFTView_320x240::ui_event_user_button(lv_event_t * e) - { - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone) { - lv_textarea_set_text(objects.settings_user_short_textarea, THIS->db.short_name); - lv_textarea_set_text(objects.settings_user_long_textarea, THIS->db.long_name); - lv_obj_clear_flag(objects.settings_username_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.settings_user_short_textarea); - THIS->disablePanel(objects.controller_panel); - THIS->disablePanel(objects.tab_page_basic_settings); - THIS->activeSettings = eUsername; - } +/** + * handle events for virtual keyboard + */ +void TFTView_320x240::ui_event_Keyboard(lv_event_t *e) +{ + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED) { + lv_obj_t *kb = lv_event_get_target_obj(e); + uint32_t btn_id = lv_keyboard_get_selected_button(kb); + + switch (btn_id) { + case 22: { // enter (filtered out by one-liner text input area, so we replace it) + // lv_obj_t *ta = lv_keyboard_get_textarea(kb); + // lv_textarea_add_char(ta, ' '); + // lv_textarea_add_char(ta, CR_REPLACEMENT); + break; } - - void TFTView_320x240::ui_event_role_button(lv_event_t * e) - { - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone && THIS->db.config.has_device) { - lv_dropdown_set_selected(objects.settings_device_role_dropdown, THIS->role2val(THIS->db.config.device.role)); - lv_obj_clear_flag(objects.settings_device_role_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.settings_device_role_dropdown); - THIS->disablePanel(objects.controller_panel); - THIS->disablePanel(objects.tab_page_basic_settings); - THIS->activeSettings = eDeviceRole; - } + case 35: { // keyboard + lv_keyboard_set_popovers(objects.keyboard, !lv_keyboard_get_popovers(kb)); + break; } - - void TFTView_320x240::ui_event_region_button(lv_event_t * e) - { - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone && THIS->db.config.has_lora) { - lv_dropdown_set_selected(objects.settings_region_dropdown, THIS->db.config.lora.region - 1); - lv_obj_clear_flag(objects.settings_region_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.settings_region_dropdown); - THIS->disablePanel(objects.controller_panel); - THIS->disablePanel(objects.tab_page_basic_settings); - THIS->activeSettings = eRegion; - } + case 36: { // left + break; } - - void TFTView_320x240::ui_event_preset_button(lv_event_t * e) - { - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone && THIS->db.config.lora.use_preset) { - THIS->activeSettings = eModemPreset; - lv_dropdown_set_selected(objects.settings_modem_preset_dropdown, - THIS->preset2val(THIS->db.config.lora.modem_preset)); - - char buf[60]; - sprintf(buf, _("FrequencySlot: %d (%g MHz)"), THIS->db.config.lora.channel_num, - LoRaPresets::getRadioFreq(THIS->db.config.lora.region, THIS->db.config.lora.modem_preset, - THIS->db.config.lora.channel_num)); - lv_label_set_text(objects.frequency_slot_label, buf); - - uint32_t numChannels = - LoRaPresets::getNumChannels(THIS->db.config.lora.region, THIS->db.config.lora.modem_preset); - lv_slider_set_range(objects.frequency_slot_slider, 1, numChannels); - lv_slider_set_value(objects.frequency_slot_slider, THIS->db.config.lora.channel_num, LV_ANIM_OFF); - - lv_obj_clear_flag(objects.settings_modem_preset_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.settings_modem_preset_dropdown); - THIS->disablePanel(objects.controller_panel); - } + case 38: { // right + break; } - - void TFTView_320x240::ui_event_wifi_button(lv_event_t * e) - { - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED && THIS->db.config.has_network && THIS->activeSettings == eNone) { - lv_textarea_set_text(objects.settings_wifi_ssid_textarea, THIS->db.config.network.wifi_ssid); - lv_textarea_set_text(objects.settings_wifi_password_textarea, THIS->db.config.network.wifi_psk); - lv_obj_clear_flag(objects.settings_wifi_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.settings_wifi_ssid_textarea); - THIS->disablePanel(objects.controller_panel); - THIS->disablePanel(objects.tab_page_basic_settings); - THIS->activeSettings = eWifi; + case 39: { // checkmark + if (THIS->activePanel == objects.messages_panel) { + THIS->hideKeyboard(objects.messages_panel); + } else { + lv_obj_add_flag(kb, LV_OBJ_FLAG_HIDDEN); } + lv_group_focus_obj(objects.message_input_area); + break; } - - void TFTView_320x240::ui_event_language_button(lv_event_t * e) - { - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone) { - lv_dropdown_set_selected(objects.settings_language_dropdown, THIS->language2val(THIS->db.uiConfig.language)); - lv_obj_clear_flag(objects.settings_language_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.settings_language_dropdown); - THIS->disablePanel(objects.controller_panel); - THIS->disablePanel(objects.tab_page_basic_settings); - THIS->activeSettings = eLanguage; - } + default: + break; + // const char *txt = lv_keyboard_get_button_text(kb, btn_id); } + } +} - void TFTView_320x240::ui_event_channel_button(lv_event_t * e) - { - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone) { - // primary channel is not necessarily channel[0], setup ui with primary on top - int pos = 1; - for (int i = 0; i < c_max_channels; i++) { - meshtastic_Channel &ch = THIS->db.channel[i]; - if (ch.has_settings && ch.role != meshtastic_Channel_Role_DISABLED) { - const char *channelName = ch.settings.name; - if (ch.settings.name[0] == '\0' && ch.settings.psk.size == 1 && ch.settings.psk.bytes[0] == 0x01) { - channelName = LoRaPresets::modemPresetToString(THIS->db.config.lora.modem_preset); - } - if (ch.role == meshtastic_Channel_Role_PRIMARY) { - THIS->ch_label[0]->user_data = (void *)i; - lv_label_set_text(THIS->ch_label[0], channelName); - } else { - THIS->ch_label[pos]->user_data = (void *)i; - lv_label_set_text(THIS->ch_label[pos++], channelName); - } - } - } - for (int i = pos; i < c_max_channels; i++) { - THIS->ch_label[i]->user_data = (void *)-1; - lv_label_set_text(THIS->ch_label[i], _("")); - } - lv_obj_clear_flag(objects.settings_channel_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.settings_channel0_button); - THIS->disablePanel(objects.controller_panel); - THIS->disablePanel(objects.tab_page_basic_settings); - THIS->activeSettings = eChannel; - - // create scratch channels to store temporary changes until cancelled or applied - THIS->channel_scratch = new meshtastic_Channel[c_max_channels]; - for (int i = 0; i < c_max_channels; i++) { - THIS->channel_scratch[i] = THIS->db.channel[i]; +void TFTView_320x240::ui_event_message_ready(lv_event_t *e) +{ + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_READY) { + char *txt = (char *)lv_textarea_get_text(objects.message_input_area); + uint32_t len = strlen(txt); + if (len) { + if (txt[len - 1] == ' ') { // use space+return combo to start new line in same message + lv_textarea_add_char(objects.message_input_area, CR_REPLACEMENT); + } else { + THIS->handleAddMessage(txt); + lv_textarea_set_text(objects.message_input_area, ""); + if (!lv_obj_has_flag(objects.keyboard, LV_OBJ_FLAG_HIDDEN)) { + THIS->hideKeyboard(objects.messages_panel); } + lv_group_focus_obj(objects.message_input_area); } } + } +} - void TFTView_320x240::ui_event_brightness_button(lv_event_t * e) - { - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone) { - char buf[20]; - uint32_t brightness = round(THIS->db.uiConfig.screen_brightness * 100.0 / 255.0); - lv_snprintf(buf, sizeof(buf), _("Brightness: %d%%"), brightness); - lv_label_set_text(objects.settings_brightness_label, buf); - lv_slider_set_value(objects.brightness_slider, brightness, LV_ANIM_OFF); - lv_obj_clear_flag(objects.settings_brightness_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.brightness_slider); - THIS->disablePanel(objects.controller_panel); - THIS->disablePanel(objects.tab_page_basic_settings); - THIS->activeSettings = eScreenBrightness; - } - } +// basic settings buttons - void TFTView_320x240::ui_event_theme_button(lv_event_t * e) - { - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone) { - lv_dropdown_set_selected(objects.settings_theme_dropdown, THIS->db.uiConfig.theme); - lv_obj_clear_flag(objects.settings_theme_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.settings_theme_dropdown); - THIS->disablePanel(objects.controller_panel); - THIS->disablePanel(objects.tab_page_basic_settings); - THIS->activeSettings = eTheme; - } - } +void TFTView_320x240::ui_event_user_button(lv_event_t *e) +{ + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone) { + lv_textarea_set_text(objects.settings_user_short_textarea, THIS->db.short_name); + lv_textarea_set_text(objects.settings_user_long_textarea, THIS->db.long_name); + lv_obj_clear_flag(objects.settings_username_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.settings_user_short_textarea); + THIS->disablePanel(objects.controller_panel); + THIS->disablePanel(objects.tab_page_basic_settings); + THIS->activeSettings = eUsername; + } +} - void TFTView_320x240::ui_event_calibration_button(lv_event_t * e) - { - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED) { - lv_screen_load_anim(objects.calibration_screen, LV_SCR_LOAD_ANIM_NONE, 0, 0, false); - } - } +void TFTView_320x240::ui_event_role_button(lv_event_t *e) +{ + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone && THIS->db.config.has_device) { + lv_dropdown_set_selected(objects.settings_device_role_dropdown, THIS->role2val(THIS->db.config.device.role)); + lv_obj_clear_flag(objects.settings_device_role_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.settings_device_role_dropdown); + THIS->disablePanel(objects.controller_panel); + THIS->disablePanel(objects.tab_page_basic_settings); + THIS->activeSettings = eDeviceRole; + } +} - void TFTView_320x240::ui_event_timeout_button(lv_event_t * e) - { - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone) { - uint32_t timeout = THIS->db.uiConfig.screen_timeout; - char buf[32]; - if (timeout == 0) - lv_snprintf(buf, sizeof(buf), _("Timeout: off")); - else - lv_snprintf(buf, sizeof(buf), _("Timeout: %ds"), timeout); - lv_label_set_text(objects.settings_screen_timeout_label, buf); - lv_obj_clear_flag(objects.settings_screen_timeout_panel, LV_OBJ_FLAG_HIDDEN); - lv_slider_set_value(objects.screen_timeout_slider, timeout, LV_ANIM_OFF); - lv_group_focus_obj(objects.screen_timeout_slider); - THIS->disablePanel(objects.controller_panel); - THIS->disablePanel(objects.tab_page_basic_settings); - THIS->activeSettings = eScreenTimeout; - } - } +void TFTView_320x240::ui_event_region_button(lv_event_t *e) +{ + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone && THIS->db.config.has_lora) { + lv_dropdown_set_selected(objects.settings_region_dropdown, THIS->db.config.lora.region - 1); + lv_obj_clear_flag(objects.settings_region_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.settings_region_dropdown); + THIS->disablePanel(objects.controller_panel); + THIS->disablePanel(objects.tab_page_basic_settings); + THIS->activeSettings = eRegion; + } +} - void TFTView_320x240::ui_event_screen_lock_button(lv_event_t * e) - { - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone) { - char buf[10]; - lv_snprintf(buf, 7, "%06d", THIS->db.uiConfig.pin_code); - lv_textarea_set_text(objects.settings_screen_lock_password_textarea, buf); - if (THIS->db.uiConfig.screen_lock) { - lv_obj_add_state(objects.settings_screen_lock_switch, LV_STATE_CHECKED); - } else { - lv_obj_remove_state(objects.settings_screen_lock_switch, LV_STATE_CHECKED); - } - if (THIS->db.uiConfig.settings_lock) { - lv_obj_add_state(objects.settings_settings_lock_switch, LV_STATE_CHECKED); - } else { - lv_obj_remove_state(objects.settings_settings_lock_switch, LV_STATE_CHECKED); - } +void TFTView_320x240::ui_event_preset_button(lv_event_t *e) +{ + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone && THIS->db.config.lora.use_preset) { + THIS->activeSettings = eModemPreset; + lv_dropdown_set_selected(objects.settings_modem_preset_dropdown, THIS->preset2val(THIS->db.config.lora.modem_preset)); + + char buf[60]; + sprintf(buf, _("FrequencySlot: %d (%g MHz)"), THIS->db.config.lora.channel_num, + LoRaPresets::getRadioFreq(THIS->db.config.lora.region, THIS->db.config.lora.modem_preset, + THIS->db.config.lora.channel_num)); + lv_label_set_text(objects.frequency_slot_label, buf); + + uint32_t numChannels = LoRaPresets::getNumChannels(THIS->db.config.lora.region, THIS->db.config.lora.modem_preset); + lv_slider_set_range(objects.frequency_slot_slider, 1, numChannels); + lv_slider_set_value(objects.frequency_slot_slider, THIS->db.config.lora.channel_num, LV_ANIM_OFF); + + lv_obj_clear_flag(objects.settings_modem_preset_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.settings_modem_preset_dropdown); + THIS->disablePanel(objects.controller_panel); + } +} - lv_obj_clear_flag(objects.settings_screen_lock_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.settings_screen_lock_switch); - THIS->disablePanel(objects.controller_panel); - THIS->disablePanel(objects.tab_page_basic_settings); - THIS->activeSettings = eScreenLock; - } - } +void TFTView_320x240::ui_event_wifi_button(lv_event_t *e) +{ + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED && THIS->db.config.has_network && THIS->activeSettings == eNone) { + lv_textarea_set_text(objects.settings_wifi_ssid_textarea, THIS->db.config.network.wifi_ssid); + lv_textarea_set_text(objects.settings_wifi_password_textarea, THIS->db.config.network.wifi_psk); + lv_obj_clear_flag(objects.settings_wifi_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.settings_wifi_ssid_textarea); + THIS->disablePanel(objects.controller_panel); + THIS->disablePanel(objects.tab_page_basic_settings); + THIS->activeSettings = eWifi; + } +} - void TFTView_320x240::ui_event_input_button(lv_event_t * e) - { - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone) { - std::vector ptr_events = THIS->inputdriver->getPointerDevices(); - std::string ptr_dropdown = _("none"); - for (std::string &s : ptr_events) { - ptr_dropdown += '\n' + s; - } - lv_dropdown_set_options(objects.settings_mouse_input_dropdown, ptr_dropdown.c_str()); - std::string current_ptr = THIS->inputdriver->getCurrentPointerDevice(); - uint32_t ptrOption = lv_dropdown_get_option_index(objects.settings_mouse_input_dropdown, current_ptr.c_str()); - lv_dropdown_set_selected(objects.settings_mouse_input_dropdown, ptrOption); - - std::vector kbd_events = THIS->inputdriver->getKeyboardDevices(); - std::string kbd_dropdown = _("none"); - for (std::string &s : kbd_events) { - kbd_dropdown += '\n' + s; - } - lv_dropdown_set_options(objects.settings_keyboard_input_dropdown, kbd_dropdown.c_str()); - std::string current_kbd = THIS->inputdriver->getCurrentKeyboardDevice(); - uint32_t kbdOption = lv_dropdown_get_option_index(objects.settings_keyboard_input_dropdown, current_kbd.c_str()); - lv_dropdown_set_selected(objects.settings_keyboard_input_dropdown, kbdOption); - - lv_dropdown_get_selected_str(objects.settings_keyboard_input_dropdown, THIS->old_val1_scratch, - sizeof(old_val1_scratch)); - lv_dropdown_get_selected_str(objects.settings_mouse_input_dropdown, THIS->old_val2_scratch, - sizeof(old_val2_scratch)); - - lv_obj_clear_flag(objects.settings_input_control_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.settings_mouse_input_dropdown); - THIS->disablePanel(objects.controller_panel); - THIS->disablePanel(objects.tab_page_basic_settings); - THIS->activeSettings = eInputControl; - } - } +void TFTView_320x240::ui_event_language_button(lv_event_t *e) +{ + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone) { + lv_dropdown_set_selected(objects.settings_language_dropdown, THIS->language2val(THIS->db.uiConfig.language)); + lv_obj_clear_flag(objects.settings_language_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.settings_language_dropdown); + THIS->disablePanel(objects.controller_panel); + THIS->disablePanel(objects.tab_page_basic_settings); + THIS->activeSettings = eLanguage; + } +} - void TFTView_320x240::ui_event_alert_button(lv_event_t * e) - { - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone && - THIS->db.module_config.has_external_notification) { - bool alert_enabled = THIS->db.module_config.external_notification.alert_message_buzzer && - THIS->db.module_config.external_notification.enabled && !THIS->db.silent; - if (alert_enabled) { - lv_obj_add_state(objects.settings_alert_buzzer_switch, LV_STATE_CHECKED); - } else { - lv_obj_remove_state(objects.settings_alert_buzzer_switch, LV_STATE_CHECKED); +void TFTView_320x240::ui_event_channel_button(lv_event_t *e) +{ + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone) { + // primary channel is not necessarily channel[0], setup ui with primary on top + int pos = 1; + for (int i = 0; i < c_max_channels; i++) { + meshtastic_Channel &ch = THIS->db.channel[i]; + if (ch.has_settings && ch.role != meshtastic_Channel_Role_DISABLED) { + const char *channelName = ch.settings.name; + if (ch.settings.name[0] == '\0' && ch.settings.psk.size == 1 && ch.settings.psk.bytes[0] == 0x01) { + channelName = LoRaPresets::modemPresetToString(THIS->db.config.lora.modem_preset); } - // populate dropdown - if (lv_dropdown_get_option_count(objects.settings_ringtone_dropdown) <= 1) { - for (int i = 2; i < numRingtones; i++) { - lv_dropdown_add_option(objects.settings_ringtone_dropdown, ringtone[i].name, i); - } + if (ch.role == meshtastic_Channel_Role_PRIMARY) { + THIS->ch_label[0]->user_data = (void *)i; + lv_label_set_text(THIS->ch_label[0], channelName); + } else { + THIS->ch_label[pos]->user_data = (void *)i; + lv_label_set_text(THIS->ch_label[pos++], channelName); } - - lv_dropdown_set_selected(objects.settings_ringtone_dropdown, THIS->db.uiConfig.ring_tone_id - 1); - lv_obj_clear_flag(objects.settings_alert_buzzer_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.settings_alert_buzzer_switch); - THIS->disablePanel(objects.controller_panel); - THIS->disablePanel(objects.tab_page_basic_settings); - THIS->activeSettings = eAlertBuzzer; } } - - // backup & restore - void TFTView_320x240::ui_event_backup_button(lv_event_t * e) - { - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone) { - lv_obj_clear_flag(objects.settings_backup_restore_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.settings_backup_restore_dropdown); - THIS->disablePanel(objects.controller_panel); - THIS->disablePanel(objects.tab_page_basic_settings); - THIS->activeSettings = eBackupRestore; - } + for (int i = pos; i < c_max_channels; i++) { + THIS->ch_label[i]->user_data = (void *)-1; + lv_label_set_text(THIS->ch_label[i], _("")); } + lv_obj_clear_flag(objects.settings_channel_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.settings_channel0_button); + THIS->disablePanel(objects.controller_panel); + THIS->disablePanel(objects.tab_page_basic_settings); + THIS->activeSettings = eChannel; - // configuration reset - void TFTView_320x240::ui_event_reset_button(lv_event_t * e) - { - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone) { - lv_obj_clear_flag(objects.settings_reset_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.settings_reset_dropdown); - THIS->disablePanel(objects.controller_panel); - THIS->disablePanel(objects.tab_page_basic_settings); - THIS->activeSettings = eReset; - } + // create scratch channels to store temporary changes until cancelled or applied + THIS->channel_scratch = new meshtastic_Channel[c_max_channels]; + for (int i = 0; i < c_max_channels; i++) { + THIS->channel_scratch[i] = THIS->db.channel[i]; } + } +} - // reboot / shutdown - void TFTView_320x240::ui_event_reboot_button(lv_event_t * e) - { - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone) { - lv_obj_remove_flag(objects.boot_logo_button, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(objects.bluetooth_button, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(objects.boot_logo_arc, LV_OBJ_FLAG_HIDDEN); - lv_screen_load_anim(objects.boot_screen, LV_SCR_LOAD_ANIM_FADE_IN, 1000, 0, false); - lv_obj_clear_flag(objects.reboot_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.cancel_reboot_button); - THIS->disablePanel(objects.controller_panel); - THIS->disablePanel(objects.tab_page_basic_settings); - THIS->activeSettings = eReboot; - } - } +void TFTView_320x240::ui_event_brightness_button(lv_event_t *e) +{ + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone) { + char buf[20]; + uint32_t brightness = round(THIS->db.uiConfig.screen_brightness * 100.0 / 255.0); + lv_snprintf(buf, sizeof(buf), _("Brightness: %d%%"), brightness); + lv_label_set_text(objects.settings_brightness_label, buf); + lv_slider_set_value(objects.brightness_slider, brightness, LV_ANIM_OFF); + lv_obj_clear_flag(objects.settings_brightness_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.brightness_slider); + THIS->disablePanel(objects.controller_panel); + THIS->disablePanel(objects.tab_page_basic_settings); + THIS->activeSettings = eScreenBrightness; + } +} - void TFTView_320x240::ui_event_device_reboot_button(lv_event_t * e) - { - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED) { - THIS->controller->requestReboot(5, THIS->ownNode); - lv_screen_load_anim(objects.blank_screen, LV_SCR_LOAD_ANIM_FADE_OUT, 4000, 1000, false); - lv_obj_add_flag(objects.reboot_panel, LV_OBJ_FLAG_HIDDEN); - if (THIS->controller->isStandalone()) { - lv_timer_create(timer_event_reboot, 4000, NULL); - } - } - } +void TFTView_320x240::ui_event_theme_button(lv_event_t *e) +{ + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone) { + lv_dropdown_set_selected(objects.settings_theme_dropdown, THIS->db.uiConfig.theme); + lv_obj_clear_flag(objects.settings_theme_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.settings_theme_dropdown); + THIS->disablePanel(objects.controller_panel); + THIS->disablePanel(objects.tab_page_basic_settings); + THIS->activeSettings = eTheme; + } +} - void TFTView_320x240::ui_event_device_progmode_button(lv_event_t * e) - { - static bool ignoreClicked = false; - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED) { - if (ignoreClicked) { // prevent long press to enter this setting - ignoreClicked = false; - return; - } +void TFTView_320x240::ui_event_calibration_button(lv_event_t *e) +{ + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED) { + lv_screen_load_anim(objects.calibration_screen, LV_SCR_LOAD_ANIM_NONE, 0, 0, false); + } +} - meshtastic_Config_NetworkConfig &network = THIS->db.config.network; - if (network.wifi_enabled) { - network.wifi_enabled = false; - THIS->controller->sendConfig(meshtastic_Config_NetworkConfig{network}); - } - meshtastic_Config_BluetoothConfig &bluetooth = THIS->db.config.bluetooth; - bluetooth.mode = meshtastic_Config_BluetoothConfig_PairingMode_FIXED_PIN; - bluetooth.fixed_pin = random(100000, 999999); - bluetooth.enabled = true; - THIS->controller->sendConfig(meshtastic_Config_BluetoothConfig{bluetooth}, THIS->ownNode); - lv_screen_load_anim(objects.blank_screen, LV_SCR_LOAD_ANIM_FADE_OUT, 4000, 1000, false); - lv_obj_add_flag(objects.reboot_panel, LV_OBJ_FLAG_HIDDEN); - } else if (event_code == LV_EVENT_LONG_PRESSED) { - if (!THIS->controller->isStandalone()) { -#if defined(HAS_SCREEN) && HAS_SCREEN == 1 - ignoreClicked = true; - // open dialog - lv_obj_remove_flag(objects.settings_reboot_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.settings_reboot_panel); - THIS->activeSettings = eDisplayMode; -#endif - } - } - } +void TFTView_320x240::ui_event_timeout_button(lv_event_t *e) +{ + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone) { + uint32_t timeout = THIS->db.uiConfig.screen_timeout; + char buf[32]; + if (timeout == 0) + lv_snprintf(buf, sizeof(buf), _("Timeout: off")); + else + lv_snprintf(buf, sizeof(buf), _("Timeout: %ds"), timeout); + lv_label_set_text(objects.settings_screen_timeout_label, buf); + lv_obj_clear_flag(objects.settings_screen_timeout_panel, LV_OBJ_FLAG_HIDDEN); + lv_slider_set_value(objects.screen_timeout_slider, timeout, LV_ANIM_OFF); + lv_group_focus_obj(objects.screen_timeout_slider); + THIS->disablePanel(objects.controller_panel); + THIS->disablePanel(objects.tab_page_basic_settings); + THIS->activeSettings = eScreenTimeout; + } +} - void TFTView_320x240::ui_event_device_shutdown_button(lv_event_t * e) - { - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED) { - THIS->controller->requestShutdown(5, THIS->ownNode); - lv_screen_load_anim(objects.blank_screen, LV_SCR_LOAD_ANIM_FADE_OUT, 4000, 1000, false); - lv_obj_add_flag(objects.reboot_panel, LV_OBJ_FLAG_HIDDEN); - if (THIS->controller->isStandalone()) { - lv_timer_create(timer_event_shutdown, 4000, NULL); - } - } +void TFTView_320x240::ui_event_screen_lock_button(lv_event_t *e) +{ + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone) { + char buf[10]; + lv_snprintf(buf, 7, "%06d", THIS->db.uiConfig.pin_code); + lv_textarea_set_text(objects.settings_screen_lock_password_textarea, buf); + if (THIS->db.uiConfig.screen_lock) { + lv_obj_add_state(objects.settings_screen_lock_switch, LV_STATE_CHECKED); + } else { + lv_obj_remove_state(objects.settings_screen_lock_switch, LV_STATE_CHECKED); } - - void TFTView_320x240::ui_event_device_cancel_button(lv_event_t * e) - { - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED) { - lv_screen_load_anim(objects.main_screen, LV_SCR_LOAD_ANIM_NONE, 0, 0, false); - lv_obj_add_flag(objects.reboot_panel, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(objects.settings_reboot_panel, LV_OBJ_FLAG_HIDDEN); - THIS->enablePanel(objects.controller_panel); - THIS->enablePanel(objects.tab_page_basic_settings); - lv_group_focus_obj(objects.basic_settings_reboot_button); - THIS->activeSettings = eNone; - } + if (THIS->db.uiConfig.settings_lock) { + lv_obj_add_state(objects.settings_settings_lock_switch, LV_STATE_CHECKED); + } else { + lv_obj_remove_state(objects.settings_settings_lock_switch, LV_STATE_CHECKED); } - void TFTView_320x240::ui_event_modify_channel(lv_event_t * e) - { - static bool ignoreClicked = false; - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eChannel) { - if (ignoreClicked) { // prevent long press to enter this setting - ignoreClicked = false; - return; - } - uint32_t btn_id = (unsigned long)e->user_data; - int8_t ch = (signed long)THIS->ch_label[btn_id]->user_data; - if (ch != -1) { - meshtastic_ChannelSettings_psk_t &psk = THIS->channel_scratch[ch].settings.psk; - std::string base64 = THIS->pskToBase64(psk.bytes, psk.size); - lv_textarea_set_text(objects.settings_modify_channel_psk_textarea, base64.c_str()); - lv_textarea_set_text(objects.settings_modify_channel_name_textarea, THIS->channel_scratch[ch].settings.name); - objects.settings_modify_channel_name_textarea->user_data = (void *)btn_id; - } else { - for (int i = 0; i < c_max_channels; i++) { - if (THIS->channel_scratch[i].role == meshtastic_Channel_Role_DISABLED) { - // the first created channel is PRIMARY - bool found = false; - for (int j = 0; j < c_max_channels; j++) { - if (THIS->channel_scratch[j].role == meshtastic_Channel_Role_PRIMARY) { - found = true; - break; - } - } - if (!found) { - THIS->channel_scratch[i].role = meshtastic_Channel_Role_PRIMARY; - if (i == 0) { - btn_id = 0; // place on top - } else { - // FIXME: swap ids as in long press - ILOG_ERROR("node does not have primary channel!"); - } - } else - THIS->channel_scratch[i].role = meshtastic_Channel_Role_SECONDARY; - - lv_textarea_set_text(objects.settings_modify_channel_psk_textarea, ""); - lv_textarea_set_text(objects.settings_modify_channel_name_textarea, ""); - THIS->ch_label[btn_id]->user_data = (void *)i; - objects.settings_modify_channel_name_textarea->user_data = (void *)btn_id; - break; - } - } - } - - THIS->disablePanel(objects.settings_channel_panel); - lv_obj_clear_flag(objects.settings_modify_channel_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.settings_modify_channel_name_textarea); - THIS->activeSettings = eModifyChannel; - } -#if 0 // TODO: simple swap not allowed: primary channel must be id 0 - else if (event_code == LV_EVENT_LONG_PRESSED && THIS->activeSettings == eChannel) { - ignoreClicked = true; - // make channel primary on long press; swap with current primary (role, id and name) - uint8_t btn_id = (uint8_t)(unsigned long)e->user_data; - int8_t ch = (signed long)THIS->ch_label[btn_id]->user_data; - if (btn_id != 0 && ch != -1) { - int32_t primary_id = (signed long)THIS->ch_label[0]->user_data; - THIS->channel_scratch[primary_id].role = meshtastic_Channel_Role_SECONDARY; - THIS->channel_scratch[ch].role = meshtastic_Channel_Role_PRIMARY; - THIS->ch_label[0]->user_data = (void *)(uint32_t)ch; - THIS->ch_label[btn_id]->user_data = (void *)primary_id; - lv_label_set_text(THIS->ch_label[0], THIS->channel_scratch[ch].settings.name); - lv_label_set_text(THIS->ch_label[btn_id], THIS->channel_scratch[primary_id].settings.name); - } + lv_obj_clear_flag(objects.settings_screen_lock_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.settings_screen_lock_switch); + THIS->disablePanel(objects.controller_panel); + THIS->disablePanel(objects.tab_page_basic_settings); + THIS->activeSettings = eScreenLock; } -#endif - } +} - void TFTView_320x240::ui_event_generate_psk(lv_event_t * e) - { - std::string base64 = lv_textarea_get_text(objects.settings_modify_channel_psk_textarea); - if (base64.size() == 0 || THIS->qr) { - meshtastic_ChannelSettings_psk_t psk{.size = 32}; - std::mt19937 generator(millis() + psk.bytes[7]); // Mersenne Twister number generator - for (int i = 0; i < 8; i++) { - int r = generator(); - memcpy(&psk.bytes[i * 4], &r, 4); - } - base64 = THIS->pskToBase64(psk.bytes, psk.size); - lv_textarea_set_text(objects.settings_modify_channel_psk_textarea, base64.c_str()); - } +void TFTView_320x240::ui_event_input_button(lv_event_t *e) +{ + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone) { + std::vector ptr_events = THIS->inputdriver->getPointerDevices(); + std::string ptr_dropdown = _("none"); + for (std::string &s : ptr_events) { + ptr_dropdown += '\n' + s; + } + lv_dropdown_set_options(objects.settings_mouse_input_dropdown, ptr_dropdown.c_str()); + std::string current_ptr = THIS->inputdriver->getCurrentPointerDevice(); + uint32_t ptrOption = lv_dropdown_get_option_index(objects.settings_mouse_input_dropdown, current_ptr.c_str()); + lv_dropdown_set_selected(objects.settings_mouse_input_dropdown, ptrOption); + + std::vector kbd_events = THIS->inputdriver->getKeyboardDevices(); + std::string kbd_dropdown = _("none"); + for (std::string &s : kbd_events) { + kbd_dropdown += '\n' + s; + } + lv_dropdown_set_options(objects.settings_keyboard_input_dropdown, kbd_dropdown.c_str()); + std::string current_kbd = THIS->inputdriver->getCurrentKeyboardDevice(); + uint32_t kbdOption = lv_dropdown_get_option_index(objects.settings_keyboard_input_dropdown, current_kbd.c_str()); + lv_dropdown_set_selected(objects.settings_keyboard_input_dropdown, kbdOption); + + lv_dropdown_get_selected_str(objects.settings_keyboard_input_dropdown, THIS->old_val1_scratch, sizeof(old_val1_scratch)); + lv_dropdown_get_selected_str(objects.settings_mouse_input_dropdown, THIS->old_val2_scratch, sizeof(old_val2_scratch)); + + lv_obj_clear_flag(objects.settings_input_control_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.settings_mouse_input_dropdown); + THIS->disablePanel(objects.controller_panel); + THIS->disablePanel(objects.tab_page_basic_settings); + THIS->activeSettings = eInputControl; + } +} - std::string base64Https = base64; - for (char &c : base64Https) { - if (c == '+') - c = '-'; - else if (c == '/') - c = '_'; - else if (c == '=') - c = '\0'; // remove paddings at the end of the url +void TFTView_320x240::ui_event_alert_button(lv_event_t *e) +{ + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone && THIS->db.module_config.has_external_notification) { + bool alert_enabled = THIS->db.module_config.external_notification.alert_message_buzzer && + THIS->db.module_config.external_notification.enabled && !THIS->db.silent; + if (alert_enabled) { + lv_obj_add_state(objects.settings_alert_buzzer_switch, LV_STATE_CHECKED); + } else { + lv_obj_remove_state(objects.settings_alert_buzzer_switch, LV_STATE_CHECKED); + } + // populate dropdown + if (lv_dropdown_get_option_count(objects.settings_ringtone_dropdown) <= 1) { + for (int i = 2; i < numRingtones; i++) { + lv_dropdown_add_option(objects.settings_ringtone_dropdown, ringtone[i].name, i); } - std::string qr = "https://meshtastic.org/e/#" + base64Https; - lv_obj_remove_flag(objects.settings_modify_channel_qr_panel, LV_OBJ_FLAG_HIDDEN); - THIS->qr = THIS->showQrCode(objects.settings_modify_channel_qr_panel, qr.c_str()); } - void TFTView_320x240::ui_event_qr_code(lv_event_t * e) - { - lv_obj_add_flag(objects.settings_modify_channel_qr_panel, LV_OBJ_FLAG_HIDDEN); - lv_obj_delete(THIS->qr); - THIS->qr = nullptr; - } + lv_dropdown_set_selected(objects.settings_ringtone_dropdown, THIS->db.uiConfig.ring_tone_id - 1); + lv_obj_clear_flag(objects.settings_alert_buzzer_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.settings_alert_buzzer_switch); + THIS->disablePanel(objects.controller_panel); + THIS->disablePanel(objects.tab_page_basic_settings); + THIS->activeSettings = eAlertBuzzer; + } +} - void TFTView_320x240::ui_event_delete_channel(lv_event_t * e) - { - lv_textarea_set_text(objects.settings_modify_channel_psk_textarea, ""); - lv_textarea_set_text(objects.settings_modify_channel_name_textarea, ""); - } +// backup & restore +void TFTView_320x240::ui_event_backup_button(lv_event_t *e) +{ + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone) { + lv_obj_clear_flag(objects.settings_backup_restore_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.settings_backup_restore_dropdown); + THIS->disablePanel(objects.controller_panel); + THIS->disablePanel(objects.tab_page_basic_settings); + THIS->activeSettings = eBackupRestore; + } +} - void TFTView_320x240::ui_event_calibration_screen_loaded(lv_event_t * e) - { - uint16_t *parameters = (uint16_t *)THIS->db.uiConfig.calibration_data.bytes; - memset(parameters, 0, 16); // clear all calibration data - bool done = THIS->displaydriver->calibrate(parameters); - THIS->db.uiConfig.calibration_data.size = 16; - char buf[32]; - lv_snprintf(buf, sizeof(buf), _("Screen Calibration: %s"), done ? _("done") : _("default")); - lv_label_set_text(objects.basic_settings_calibration_label, buf); - lv_screen_load_anim(objects.main_screen, LV_SCR_LOAD_ANIM_FADE_ON, 200, 0, false); - THIS->controller->storeUIConfig(THIS->db.uiConfig); - } +// configuration reset +void TFTView_320x240::ui_event_reset_button(lv_event_t *e) +{ + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone) { + lv_obj_clear_flag(objects.settings_reset_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.settings_reset_dropdown); + THIS->disablePanel(objects.controller_panel); + THIS->disablePanel(objects.tab_page_basic_settings); + THIS->activeSettings = eReset; + } +} - void TFTView_320x240::ui_event_pin_screen_button(lv_event_t * e) - { - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED && lv_scr_act() == objects.lock_screen) { - static const char *hidden[7] = {"o o o o o o", "* o o o o o", "* * o o o o", "* * * o o o", - "* * * * o o", "* * * * * o", "* * * * * *"}; - static char pinEntered[7]{}; - lv_obj_t *obj = (lv_obj_t *)lv_event_get_target(e); - uint32_t id = lv_buttonmatrix_get_selected_button(obj); - const char *key = lv_buttonmatrix_get_button_text(obj, id); - switch (*key) { - case 'X': { - pinKeys = 0; - lv_label_set_text(objects.lock_screen_digits_label, hidden[pinKeys]); - if (!screenLocked) { - lv_screen_load_anim(objects.main_screen, LV_SCR_LOAD_ANIM_FADE_IN, 100, 0, false); - } else { - // TODO: init screen saver - } - break; - } - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': { - if (pinKeys < 6) { - pinEntered[pinKeys++] = *key; - lv_label_set_text(objects.lock_screen_digits_label, hidden[pinKeys]); - - char buf[10]; - lv_snprintf(buf, 7, "%06d", THIS->db.uiConfig.pin_code); - if (pinKeys == 6 && strcmp(pinEntered, buf) == 0) { - // unlock screen - pinKeys = 0; - screenLocked = false; - lv_obj_clear_flag(objects.tab_page_basic_settings, LV_OBJ_FLAG_HIDDEN); - lv_screen_load_anim(objects.main_screen, LV_SCR_LOAD_ANIM_FADE_IN, 100, 0, false); - lv_label_set_text(objects.lock_screen_digits_label, hidden[pinKeys]); - } - } - break; - } - case 'D': { - if (pinKeys > 0) { - pinEntered[--pinKeys] = '\0'; - lv_label_set_text(objects.lock_screen_digits_label, hidden[pinKeys]); - } - break; - } - default: - break; - } - } - } +// reboot / shutdown +void TFTView_320x240::ui_event_reboot_button(lv_event_t *e) +{ + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eNone) { + lv_obj_remove_flag(objects.boot_logo_button, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(objects.bluetooth_button, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(objects.boot_logo_arc, LV_OBJ_FLAG_HIDDEN); + lv_screen_load_anim(objects.boot_screen, LV_SCR_LOAD_ANIM_FADE_IN, 1000, 0, false); + lv_obj_clear_flag(objects.reboot_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.cancel_reboot_button); + THIS->disablePanel(objects.controller_panel); + THIS->disablePanel(objects.tab_page_basic_settings); + THIS->activeSettings = eReboot; + } +} - void TFTView_320x240::ui_event_backup_restore_radio_button(lv_event_t * e) - { - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED) { - lv_obj_remove_state(objects.settings_backup_checkbox, LV_STATE_CHECKED); - lv_obj_remove_state(objects.settings_restore_checkbox, LV_STATE_CHECKED); - lv_obj_add_state(lv_event_get_target_obj(e), LV_STATE_CHECKED); - } +void TFTView_320x240::ui_event_device_reboot_button(lv_event_t *e) +{ + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED) { + THIS->controller->requestReboot(5, THIS->ownNode); + lv_screen_load_anim(objects.blank_screen, LV_SCR_LOAD_ANIM_FADE_OUT, 4000, 1000, false); + lv_obj_add_flag(objects.reboot_panel, LV_OBJ_FLAG_HIDDEN); + if (THIS->controller->isStandalone()) { + lv_timer_create(timer_event_reboot, 4000, NULL); } + } +} - void TFTView_320x240::ui_event_zoomSlider(lv_event_t * e) - { - THIS->map->setZoom(lv_slider_get_value(objects.zoom_slider)); - THIS->updateLocationMap(THIS->map->getObjectsOnMap()); +void TFTView_320x240::ui_event_device_progmode_button(lv_event_t *e) +{ + static bool ignoreClicked = false; + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED) { + if (ignoreClicked) { // prevent long press to enter this setting + ignoreClicked = false; + return; } - void TFTView_320x240::ui_event_zoomIn(lv_event_t * e) - { - THIS->map->setZoom(MapTileSettings::getZoomLevel() + 1); - THIS->updateLocationMap(THIS->map->getObjectsOnMap()); + meshtastic_Config_NetworkConfig &network = THIS->db.config.network; + if (network.wifi_enabled) { + network.wifi_enabled = false; + THIS->controller->sendConfig(meshtastic_Config_NetworkConfig{network}); } - - void TFTView_320x240::ui_event_zoomOut(lv_event_t * e) - { - THIS->map->setZoom(MapTileSettings::getZoomLevel() - 1); - THIS->updateLocationMap(THIS->map->getObjectsOnMap()); + meshtastic_Config_BluetoothConfig &bluetooth = THIS->db.config.bluetooth; + bluetooth.mode = meshtastic_Config_BluetoothConfig_PairingMode_FIXED_PIN; + bluetooth.fixed_pin = random(100000, 999999); + bluetooth.enabled = true; + THIS->controller->sendConfig(meshtastic_Config_BluetoothConfig{bluetooth}, THIS->ownNode); + lv_screen_load_anim(objects.blank_screen, LV_SCR_LOAD_ANIM_FADE_OUT, 4000, 1000, false); + lv_obj_add_flag(objects.reboot_panel, LV_OBJ_FLAG_HIDDEN); + } else if (event_code == LV_EVENT_LONG_PRESSED) { + if (!THIS->controller->isStandalone()) { +#if defined(HAS_SCREEN) && HAS_SCREEN == 1 + ignoreClicked = true; + // open dialog + lv_obj_remove_flag(objects.settings_reboot_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.settings_reboot_panel); + THIS->activeSettings = eDisplayMode; +#endif } + } +} - void TFTView_320x240::ui_event_lockGps(lv_event_t * e) - { - bool gpsLocked = lv_obj_has_state(objects.gps_lock_button, LV_STATE_CHECKED); - THIS->map->setLocked(gpsLocked); - THIS->db.uiConfig.map_data.follow_gps = gpsLocked; - THIS->controller->storeUIConfig(THIS->db.uiConfig); +void TFTView_320x240::ui_event_device_shutdown_button(lv_event_t *e) +{ + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED) { + THIS->controller->requestShutdown(5, THIS->ownNode); + lv_screen_load_anim(objects.blank_screen, LV_SCR_LOAD_ANIM_FADE_OUT, 4000, 1000, false); + lv_obj_add_flag(objects.reboot_panel, LV_OBJ_FLAG_HIDDEN); + if (THIS->controller->isStandalone()) { + lv_timer_create(timer_event_shutdown, 4000, NULL); } + } +} - void TFTView_320x240::ui_event_mapBrightnessSlider(lv_event_t * e) - { - uint32_t br = lv_slider_get_value(objects.map_brightness_slider); - lv_obj_set_style_bg_color(objects.map_panel, lv_color_make(br, br, br), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(objects.raw_map_panel, lv_color_make(br, br, br), LV_PART_MAIN | LV_STATE_DEFAULT); - } +void TFTView_320x240::ui_event_device_cancel_button(lv_event_t *e) +{ + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED) { + lv_screen_load_anim(objects.main_screen, LV_SCR_LOAD_ANIM_NONE, 0, 0, false); + lv_obj_add_flag(objects.reboot_panel, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(objects.settings_reboot_panel, LV_OBJ_FLAG_HIDDEN); + THIS->enablePanel(objects.controller_panel); + THIS->enablePanel(objects.tab_page_basic_settings); + lv_group_focus_obj(objects.basic_settings_reboot_button); + THIS->activeSettings = eNone; + } +} - void TFTView_320x240::ui_event_mapContrastSlider(lv_event_t * e) - { - uint32_t ct = lv_slider_get_value(objects.map_contrast_slider); - lv_obj_set_style_opa(objects.raw_map_panel, ct, LV_PART_MAIN | LV_STATE_DEFAULT); +void TFTView_320x240::ui_event_modify_channel(lv_event_t *e) +{ + static bool ignoreClicked = false; + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED && THIS->activeSettings == eChannel) { + if (ignoreClicked) { // prevent long press to enter this setting + ignoreClicked = false; + return; } + uint32_t btn_id = (unsigned long)e->user_data; + int8_t ch = (signed long)THIS->ch_label[btn_id]->user_data; + if (ch != -1) { + meshtastic_ChannelSettings_psk_t &psk = THIS->channel_scratch[ch].settings.psk; + std::string base64 = THIS->pskToBase64(psk.bytes, psk.size); + lv_textarea_set_text(objects.settings_modify_channel_psk_textarea, base64.c_str()); + lv_textarea_set_text(objects.settings_modify_channel_name_textarea, THIS->channel_scratch[ch].settings.name); + objects.settings_modify_channel_name_textarea->user_data = (void *)btn_id; + } else { + for (int i = 0; i < c_max_channels; i++) { + if (THIS->channel_scratch[i].role == meshtastic_Channel_Role_DISABLED) { + // the first created channel is PRIMARY + bool found = false; + for (int j = 0; j < c_max_channels; j++) { + if (THIS->channel_scratch[j].role == meshtastic_Channel_Role_PRIMARY) { + found = true; + break; + } + } + if (!found) { + THIS->channel_scratch[i].role = meshtastic_Channel_Role_PRIMARY; + if (i == 0) { + btn_id = 0; // place on top + } else { + // FIXME: swap ids as in long press + ILOG_ERROR("node does not have primary channel!"); + } + } else + THIS->channel_scratch[i].role = meshtastic_Channel_Role_SECONDARY; - void TFTView_320x240::ui_event_map_style_dropdown(lv_event_t * e) - { - lv_dropdown_get_selected_str(objects.map_style_dropdown, THIS->db.uiConfig.map_data.style, - sizeof(THIS->db.uiConfig.map_data.style)); - MapTileSettings::setTileStyle(THIS->db.uiConfig.map_data.style); - THIS->controller->storeUIConfig(THIS->db.uiConfig); - lv_obj_add_flag(objects.map_osd_panel, LV_OBJ_FLAG_HIDDEN); - THIS->map->forceRedraw(); + lv_textarea_set_text(objects.settings_modify_channel_psk_textarea, ""); + lv_textarea_set_text(objects.settings_modify_channel_name_textarea, ""); + THIS->ch_label[btn_id]->user_data = (void *)i; + objects.settings_modify_channel_name_textarea->user_data = (void *)btn_id; + break; + } + } } - void TFTView_320x240::ui_event_mapNodeButton(lv_event_t * e) - { - // navigate to node in node list - uint32_t nodeNum = (unsigned long)e->user_data; - ILOG_DEBUG("map node %08x", nodeNum); - lv_obj_t *panel = THIS->nodes[nodeNum]; - THIS->ui_set_active(objects.nodes_button, objects.nodes_panel, objects.top_nodes_panel); - lv_obj_scroll_to_view(panel, LV_ANIM_ON); - if (panel != currentPanel) - ui_event_NodeButton(e); + THIS->disablePanel(objects.settings_channel_panel); + lv_obj_clear_flag(objects.settings_modify_channel_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.settings_modify_channel_name_textarea); + THIS->activeSettings = eModifyChannel; + } +#if 0 // TODO: simple swap not allowed: primary channel must be id 0 + else if (event_code == LV_EVENT_LONG_PRESSED && THIS->activeSettings == eChannel) { + ignoreClicked = true; + // make channel primary on long press; swap with current primary (role, id and name) + uint8_t btn_id = (uint8_t)(unsigned long)e->user_data; + int8_t ch = (signed long)THIS->ch_label[btn_id]->user_data; + if (btn_id != 0 && ch != -1) { + int32_t primary_id = (signed long)THIS->ch_label[0]->user_data; + THIS->channel_scratch[primary_id].role = meshtastic_Channel_Role_SECONDARY; + THIS->channel_scratch[ch].role = meshtastic_Channel_Role_PRIMARY; + THIS->ch_label[0]->user_data = (void *)(uint32_t)ch; + THIS->ch_label[btn_id]->user_data = (void *)primary_id; + lv_label_set_text(THIS->ch_label[0], THIS->channel_scratch[ch].settings.name); + lv_label_set_text(THIS->ch_label[btn_id], THIS->channel_scratch[primary_id].settings.name); } + } +#endif +} - void TFTView_320x240::ui_event_chatNodeButton(lv_event_t * e) - { - uint32_t nodeNum = (unsigned long)e->user_data; - auto it = THIS->nodes.find(nodeNum); - if (it != THIS->nodes.end()) { - lv_obj_t *panel = it->second; - THIS->ui_set_active(objects.nodes_button, objects.nodes_panel, objects.top_nodes_panel); - lv_obj_scroll_to_view(panel, LV_ANIM_ON); - if (panel != currentPanel) - ui_event_NodeButton(e); +void TFTView_320x240::ui_event_generate_psk(lv_event_t *e) +{ + std::string base64 = lv_textarea_get_text(objects.settings_modify_channel_psk_textarea); + if (base64.size() == 0 || THIS->qr) { + meshtastic_ChannelSettings_psk_t psk{.size = 32}; + std::mt19937 generator(millis() + psk.bytes[7]); // Mersenne Twister number generator + for (int i = 0; i < 8; i++) { + int r = generator(); + memcpy(&psk.bytes[i * 4], &r, 4); + } + base64 = THIS->pskToBase64(psk.bytes, psk.size); + lv_textarea_set_text(objects.settings_modify_channel_psk_textarea, base64.c_str()); + } + + std::string base64Https = base64; + for (char &c : base64Https) { + if (c == '+') + c = '-'; + else if (c == '/') + c = '_'; + else if (c == '=') + c = '\0'; // remove paddings at the end of the url + } + std::string qr = "https://meshtastic.org/e/#" + base64Https; + lv_obj_remove_flag(objects.settings_modify_channel_qr_panel, LV_OBJ_FLAG_HIDDEN); + THIS->qr = THIS->showQrCode(objects.settings_modify_channel_qr_panel, qr.c_str()); + lv_obj_add_state(objects.keyboard_button_3, LV_STATE_DISABLED); + lv_obj_add_state(objects.keyboard_button_4, LV_STATE_DISABLED); +} + +void TFTView_320x240::ui_event_qr_code(lv_event_t *e) +{ + lv_obj_remove_state(objects.keyboard_button_3, LV_STATE_DISABLED); + lv_obj_remove_state(objects.keyboard_button_4, LV_STATE_DISABLED); + lv_obj_add_flag(objects.settings_modify_channel_qr_panel, LV_OBJ_FLAG_HIDDEN); + lv_obj_delete(THIS->qr); + THIS->qr = nullptr; +} + +void TFTView_320x240::ui_event_delete_channel(lv_event_t *e) +{ + lv_textarea_set_text(objects.settings_modify_channel_psk_textarea, ""); + lv_textarea_set_text(objects.settings_modify_channel_name_textarea, ""); +} + +void TFTView_320x240::ui_event_calibration_screen_loaded(lv_event_t *e) +{ + uint16_t *parameters = (uint16_t *)THIS->db.uiConfig.calibration_data.bytes; + memset(parameters, 0, 16); // clear all calibration data + bool done = THIS->displaydriver->calibrate(parameters); + THIS->db.uiConfig.calibration_data.size = 16; + char buf[32]; + lv_snprintf(buf, sizeof(buf), _("Screen Calibration: %s"), done ? _("done") : _("default")); + lv_label_set_text(objects.basic_settings_calibration_label, buf); + lv_screen_load_anim(objects.main_screen, LV_SCR_LOAD_ANIM_FADE_ON, 200, 0, false); + THIS->controller->storeUIConfig(THIS->db.uiConfig); +} + +void TFTView_320x240::ui_event_pin_screen_button(lv_event_t *e) +{ + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED && lv_scr_act() == objects.lock_screen) { + static const char *hidden[7] = {"o o o o o o", "* o o o o o", "* * o o o o", "* * * o o o", + "* * * * o o", "* * * * * o", "* * * * * *"}; + static char pinEntered[7]{}; + lv_obj_t *obj = (lv_obj_t *)lv_event_get_target(e); + uint32_t id = lv_buttonmatrix_get_selected_button(obj); + const char *key = lv_buttonmatrix_get_button_text(obj, id); + switch (*key) { + case 'X': { + pinKeys = 0; + lv_label_set_text(objects.lock_screen_digits_label, hidden[pinKeys]); + if (!screenLocked) { + lv_screen_load_anim(objects.main_screen, LV_SCR_LOAD_ANIM_FADE_IN, 100, 0, false); + } else { + // TODO: init screen saver } + break; } + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': { + if (pinKeys < 6) { + pinEntered[pinKeys++] = *key; + lv_label_set_text(objects.lock_screen_digits_label, hidden[pinKeys]); - void TFTView_320x240::ui_event_positionButton(lv_event_t * e) - { - // navigate to position in map - lv_obj_t *p = (lv_obj_t *)e->user_data; - int32_t lat = (long)p->LV_OBJ_IDX(node_pos1_idx)->user_data; - int32_t lon = (long)p->LV_OBJ_IDX(node_pos2_idx)->user_data; - if (lat && lon) { - THIS->ui_set_active(objects.map_button, objects.map_panel, objects.top_map_panel); - if (!THIS->map) { - THIS->loadMap(); + char buf[10]; + lv_snprintf(buf, 7, "%06d", THIS->db.uiConfig.pin_code); + if (pinKeys == 6 && strcmp(pinEntered, buf) == 0) { + // unlock screen + pinKeys = 0; + screenLocked = false; + lv_obj_clear_flag(objects.tab_page_basic_settings, LV_OBJ_FLAG_HIDDEN); + lv_screen_load_anim(objects.main_screen, LV_SCR_LOAD_ANIM_FADE_IN, 100, 0, false); + lv_label_set_text(objects.lock_screen_digits_label, hidden[pinKeys]); } - THIS->map->setScrolledPosition(lat * 1e-7, lon * 1e-7); } + break; } - - void TFTView_320x240::ui_screen_event_cb(lv_event_t * e) - { - if (THIS->activePanel == objects.map_panel) { - lv_dir_t dir = lv_indev_get_gesture_dir(lv_indev_active()); - switch (dir) { - case LV_DIR_LEFT: - e->user_data = (void *)6; - break; - case LV_DIR_RIGHT: - e->user_data = (void *)4; - break; - case LV_DIR_TOP: - e->user_data = (void *)2; - break; - case LV_DIR_BOTTOM: - e->user_data = (void *)8; - break; - default: - break; - } - ILOG_DEBUG("gesture: %d", (uint16_t)dir); - THIS->ui_event_arrow(e); + case 'D': { + if (pinKeys > 0) { + pinEntered[--pinKeys] = '\0'; + lv_label_set_text(objects.lock_screen_digits_label, hidden[pinKeys]); } + break; + } + default: + break; } + } +} - void TFTView_320x240::ui_event_arrow(lv_event_t * e) - { - if (THIS->map && THIS->map->redrawComplete()) { - uint16_t deltaX = 0; - uint16_t deltaY = 0; - ScrollDirection direction = (ScrollDirection)(unsigned long)e->user_data; - switch (direction) { - case scrollDownLeft: - deltaX = 1; - deltaY = -1; - break; - case scrollDown: - deltaX = 0; - deltaY = -1; - break; - case scrollDownRight: - deltaX = -1; - deltaY = -1; - break; - case scrollLeft: - deltaX = 1; - deltaY = 0; - break; - case scrollRight: - deltaX = -1; - deltaY = 0; - break; - case scrollUpLeft: - deltaX = 1; - deltaY = 1; - break; - case scrollUp: - deltaX = 0; - deltaY = 1; - break; - case scrollUpRight: - deltaX = -1; - deltaY = 1; - break; - default: - break; - }; - if (!THIS->map->scroll(deltaX, deltaY)) - THIS->map->forceRedraw(); - } - THIS->updateLocationMap(THIS->map->getObjectsOnMap()); +void TFTView_320x240::ui_event_backup_restore_radio_button(lv_event_t *e) +{ + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED) { + lv_obj_remove_state(objects.settings_backup_checkbox, LV_STATE_CHECKED); + lv_obj_remove_state(objects.settings_restore_checkbox, LV_STATE_CHECKED); + lv_obj_add_state(lv_event_get_target_obj(e), LV_STATE_CHECKED); + } +} + +void TFTView_320x240::ui_event_zoomSlider(lv_event_t *e) +{ + THIS->map->setZoom(lv_slider_get_value(objects.zoom_slider)); + THIS->updateLocationMap(THIS->map->getObjectsOnMap()); +} + +void TFTView_320x240::ui_event_zoomIn(lv_event_t *e) +{ + THIS->map->setZoom(MapTileSettings::getZoomLevel() + 1); + THIS->updateLocationMap(THIS->map->getObjectsOnMap()); +} + +void TFTView_320x240::ui_event_zoomOut(lv_event_t *e) +{ + THIS->map->setZoom(MapTileSettings::getZoomLevel() - 1); + THIS->updateLocationMap(THIS->map->getObjectsOnMap()); +} + +void TFTView_320x240::ui_event_lockGps(lv_event_t *e) +{ + bool gpsLocked = lv_obj_has_state(objects.gps_lock_button, LV_STATE_CHECKED); + THIS->map->setLocked(gpsLocked); + THIS->db.uiConfig.map_data.follow_gps = gpsLocked; + THIS->controller->storeUIConfig(THIS->db.uiConfig); +} + +void TFTView_320x240::ui_event_mapBrightnessSlider(lv_event_t *e) +{ + uint32_t br = lv_slider_get_value(objects.map_brightness_slider); + lv_obj_set_style_bg_color(objects.map_panel, lv_color_make(br, br, br), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(objects.raw_map_panel, lv_color_make(br, br, br), LV_PART_MAIN | LV_STATE_DEFAULT); +} + +void TFTView_320x240::ui_event_mapContrastSlider(lv_event_t *e) +{ + uint32_t ct = lv_slider_get_value(objects.map_contrast_slider); + lv_obj_set_style_opa(objects.raw_map_panel, ct, LV_PART_MAIN | LV_STATE_DEFAULT); +} + +void TFTView_320x240::ui_event_map_style_dropdown(lv_event_t *e) +{ + lv_dropdown_get_selected_str(objects.map_style_dropdown, THIS->db.uiConfig.map_data.style, + sizeof(THIS->db.uiConfig.map_data.style)); + MapTileSettings::setTileStyle(THIS->db.uiConfig.map_data.style); + THIS->controller->storeUIConfig(THIS->db.uiConfig); + lv_obj_add_flag(objects.map_osd_panel, LV_OBJ_FLAG_HIDDEN); + THIS->map->forceRedraw(); +} + +void TFTView_320x240::ui_event_mapNodeButton(lv_event_t *e) +{ + // navigate to node in node list + uint32_t nodeNum = (unsigned long)e->user_data; + ILOG_DEBUG("map node %08x", nodeNum); + lv_obj_t *panel = THIS->nodes[nodeNum]; + THIS->ui_set_active(objects.nodes_button, objects.nodes_panel, objects.top_nodes_panel); + lv_obj_scroll_to_view(panel, LV_ANIM_ON); + if (panel != currentPanel) + ui_event_NodeButton(e); +} + +void TFTView_320x240::ui_event_chatNodeButton(lv_event_t *e) +{ + uint32_t nodeNum = (unsigned long)e->user_data; + auto it = THIS->nodes.find(nodeNum); + if (it != THIS->nodes.end()) { + lv_obj_t *panel = it->second; + THIS->ui_set_active(objects.nodes_button, objects.nodes_panel, objects.top_nodes_panel); + lv_obj_scroll_to_view(panel, LV_ANIM_ON); + if (panel != currentPanel) + ui_event_NodeButton(e); + } +} + +void TFTView_320x240::ui_event_positionButton(lv_event_t *e) +{ + // navigate to position in map + lv_obj_t *p = (lv_obj_t *)e->user_data; + int32_t lat = (long)p->LV_OBJ_IDX(node_pos1_idx)->user_data; + int32_t lon = (long)p->LV_OBJ_IDX(node_pos2_idx)->user_data; + if (lat && lon) { + THIS->ui_set_active(objects.map_button, objects.map_panel, objects.top_map_panel); + if (!THIS->map) { + THIS->loadMap(); } + THIS->map->setScrolledPosition(lat * 1e-7, lon * 1e-7); + } +} - void TFTView_320x240::ui_event_navHome(lv_event_t * e) - { - static bool ignoreClicked = false; - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED) { - if (ignoreClicked) { // prevent long press to enter this setting - ignoreClicked = false; - return; - } - THIS->map->moveHome(); - } else if (event_code == LV_EVENT_LONG_PRESSED) { - ignoreClicked = true; - float lat, lon; - THIS->map->setHomePosition(); - THIS->map->getHomeLocation(lat, lon); - - int32_t ilat = lat * 1e7f; - int32_t ilon = lon * 1e7f; - THIS->db.uiConfig.has_map_data = true; - THIS->db.uiConfig.map_data.has_home = true; - THIS->db.uiConfig.map_data.home.latitude = ilat; - THIS->db.uiConfig.map_data.home.longitude = ilon; - THIS->db.uiConfig.map_data.home.zoom = MapTileSettings::getZoomLevel(); - THIS->controller->storeUIConfig(THIS->db.uiConfig); +void TFTView_320x240::ui_screen_event_cb(lv_event_t *e) +{ + if (THIS->activePanel == objects.map_panel) { + lv_dir_t dir = lv_indev_get_gesture_dir(lv_indev_active()); + switch (dir) { + case LV_DIR_LEFT: + e->user_data = (void *)6; + break; + case LV_DIR_RIGHT: + e->user_data = (void *)4; + break; + case LV_DIR_TOP: + e->user_data = (void *)2; + break; + case LV_DIR_BOTTOM: + e->user_data = (void *)8; + break; + default: + break; + } + ILOG_DEBUG("gesture: %d", (uint16_t)dir); + THIS->ui_event_arrow(e); + } +} - meshtastic_Config_PositionConfig &position = THIS->db.config.position; - if (position.fixed_position) { - THIS->updatePosition(THIS->ownNode, ilat, ilon, 0, 0, 0); - if (position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) { - // grey out text to indicate it's a fixed position vs. actual GPS position - Themes::recolorText(objects.home_location_label, false); - THIS->controller->sendConfig( - meshtastic_Position{.latitude_i = ilat, - .longitude_i = ilon, - .time = uint32_t(VALID_TIME(THIS->actTime) ? THIS->actTime : 0), - .location_source = meshtastic_Position_LocSource_LOC_MANUAL}); - } - } +void TFTView_320x240::ui_event_arrow(lv_event_t *e) +{ + if (THIS->map && THIS->map->redrawComplete()) { + uint16_t deltaX = 0; + uint16_t deltaY = 0; + ScrollDirection direction = (ScrollDirection)(unsigned long)e->user_data; + switch (direction) { + case scrollDownLeft: + deltaX = 1; + deltaY = -1; + break; + case scrollDown: + deltaX = 0; + deltaY = -1; + break; + case scrollDownRight: + deltaX = -1; + deltaY = -1; + break; + case scrollLeft: + deltaX = 1; + deltaY = 0; + break; + case scrollRight: + deltaX = -1; + deltaY = 0; + break; + case scrollUpLeft: + deltaX = 1; + deltaY = 1; + break; + case scrollUp: + deltaX = 0; + deltaY = 1; + break; + case scrollUpRight: + deltaX = -1; + deltaY = 1; + break; + default: + break; + }; + if (!THIS->map->scroll(deltaX, deltaY)) + THIS->map->forceRedraw(); + } + THIS->updateLocationMap(THIS->map->getObjectsOnMap()); +} + +void TFTView_320x240::ui_event_navHome(lv_event_t *e) +{ + static bool ignoreClicked = false; + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED) { + if (ignoreClicked) { // prevent long press to enter this setting + ignoreClicked = false; + return; + } + THIS->map->moveHome(); + } else if (event_code == LV_EVENT_LONG_PRESSED) { + ignoreClicked = true; + float lat, lon; + THIS->map->setHomePosition(); + THIS->map->getHomeLocation(lat, lon); + + int32_t ilat = lat * 1e7f; + int32_t ilon = lon * 1e7f; + THIS->db.uiConfig.has_map_data = true; + THIS->db.uiConfig.map_data.has_home = true; + THIS->db.uiConfig.map_data.home.latitude = ilat; + THIS->db.uiConfig.map_data.home.longitude = ilon; + THIS->db.uiConfig.map_data.home.zoom = MapTileSettings::getZoomLevel(); + THIS->controller->storeUIConfig(THIS->db.uiConfig); + + meshtastic_Config_PositionConfig &position = THIS->db.config.position; + if (position.fixed_position) { + THIS->updatePosition(THIS->ownNode, ilat, ilon, 0, 0, 0); + if (position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) { + // grey out text to indicate it's a fixed position vs. actual GPS position + Themes::recolorText(objects.home_location_label, false); + THIS->controller->sendConfig(meshtastic_Position{.latitude_i = ilat, + .longitude_i = ilon, + .time = uint32_t(VALID_TIME(THIS->actTime) ? THIS->actTime : 0), + .location_source = meshtastic_Position_LocSource_LOC_MANUAL}); } } + } +} - void TFTView_320x240::loadMap(void) - { - if (!map) { +void TFTView_320x240::loadMap(void) +{ + if (!map) { #if LV_USE_FS_ARDUINO_SD - map = new MapPanel(objects.raw_map_panel); + map = new MapPanel(objects.raw_map_panel); #elif defined(HAS_SD_MMC) - map = new MapPanel(objects.raw_map_panel, new SDCardService()); + map = new MapPanel(objects.raw_map_panel, new SDCardService()); #elif defined(HAS_SDCARD) - map = new MapPanel(objects.raw_map_panel, new SdFatService()); + map = new MapPanel(objects.raw_map_panel, new SdFatService()); #elif defined(ARCH_PORTDUINO) - map = new MapPanel(objects.raw_map_panel, new SDCardService()); // TODO: LinuxFileSystemService + map = new MapPanel(objects.raw_map_panel, new SDCardService()); // TODO: LinuxFileSystemService #else - map = new MapPanel(objects.raw_map_panel); + map = new MapPanel(objects.raw_map_panel); #endif - map->setHomeLocationImage(objects.home_location_image); - lv_obj_add_flag(objects.home_location_image, LV_OBJ_FLAG_CLICKABLE); - lv_obj_add_event_cb(objects.home_location_image, ui_event_mapNodeButton, LV_EVENT_CLICKED, (void *)ownNode); - - // center map to GPS > home > other nodes > default location - if (db.config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) { - map->setGpsPositionImage(objects.gps_position_image); - lv_obj_clear_flag(objects.gps_position_image, LV_OBJ_FLAG_HIDDEN); - } else { - lv_obj_add_flag(objects.gps_position_image, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(objects.gps_lock_button, LV_OBJ_FLAG_HIDDEN); - } - if (hasPosition) { - if (db.uiConfig.map_data.has_home) { - map->setHomeLocation(db.uiConfig.map_data.home.latitude * 1e-7, - db.uiConfig.map_data.home.longitude * 1e-7); - map->setZoom(db.uiConfig.map_data.home.zoom); - } else { - map->setHomeLocation(myLatitude * 1e-7, myLongitude * 1e-7); - map->setZoom(13); - } - map->setGpsPosition(myLatitude * 1e-7, myLongitude * 1e-7); - } else if (db.uiConfig.map_data.has_home) { - map->setHomeLocation(db.uiConfig.map_data.home.latitude * 1e-7, db.uiConfig.map_data.home.longitude * 1e-7); - map->setZoom(db.uiConfig.map_data.home.zoom); - } else if (nodeObjects.size() >= 1) { - // no gps, no saved position then center the home location among other available nodes - std::vector sortedLat; - std::vector sortedLon; - sortedLat.reserve(nodeObjects.size()); - sortedLon.reserve(nodeObjects.size()); - for (auto it : nodeObjects) { - lv_obj_t *p = nodes[it.first]; - int32_t lat = (long)p->LV_OBJ_IDX(node_pos1_idx)->user_data; - int32_t lon = (long)p->LV_OBJ_IDX(node_pos2_idx)->user_data; - if (lat && lon) { - sortedLat.push_back(lat); - sortedLon.push_back(lon); - } - } - std::sort(sortedLat.begin(), sortedLat.end()); - std::sort(sortedLon.begin(), sortedLon.end()); - int64_t latcenter = 0; - int64_t loncenter = 0; - int32_t count = 0; - // select just the closest 60% of nodes, ignore the rest - int pp = 100 / 20; - for (int i = sortedLat.size() / pp; i < pp * sortedLat.size() / pp; i++) { - latcenter += sortedLat[i]; - loncenter += sortedLon[i]; - count++; - } - latcenter /= count; - loncenter /= count; - map->setHomeLocation(latcenter * 1e-7, loncenter * 1e-7); - - // calculate optimal zoom factor to fit in all nodes of this range - lv_obj_update_layout(objects.raw_map_panel); - float rangeDeg = 1e-7 * (sortedLon[(pp - 1) * sortedLon.size() / pp] - sortedLon[sortedLon.size() / pp]); - float distanceKm = abs(rangeDeg * 111.32 * cos(1e-7 * sortedLat[sortedLat.size() / 2])); - uint32_t zoom = sqrt(156.543034f / distanceKm * abs(cos(1e-7 * sortedLat[sortedLat.size() / 2])) * 256) + 1; - map->setZoom(zoom); - } else { - // use default location @theBigBentern - map->setZoom(3); - } + map->setHomeLocationImage(objects.home_location_image); + lv_obj_add_flag(objects.home_location_image, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_event_cb(objects.home_location_image, ui_event_mapNodeButton, LV_EVENT_CLICKED, (void *)ownNode); + + // center map to GPS > home > other nodes > default location + if (db.config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) { + map->setGpsPositionImage(objects.gps_position_image); + lv_obj_clear_flag(objects.gps_position_image, LV_OBJ_FLAG_HIDDEN); + } else { + lv_obj_add_flag(objects.gps_position_image, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(objects.gps_lock_button, LV_OBJ_FLAG_HIDDEN); + } + if (hasPosition) { + if (db.uiConfig.map_data.has_home) { + map->setHomeLocation(db.uiConfig.map_data.home.latitude * 1e-7, db.uiConfig.map_data.home.longitude * 1e-7); + map->setZoom(db.uiConfig.map_data.home.zoom); + } else { + map->setHomeLocation(myLatitude * 1e-7, myLongitude * 1e-7); + map->setZoom(13); + } + map->setGpsPosition(myLatitude * 1e-7, myLongitude * 1e-7); + } else if (db.uiConfig.map_data.has_home) { + map->setHomeLocation(db.uiConfig.map_data.home.latitude * 1e-7, db.uiConfig.map_data.home.longitude * 1e-7); + map->setZoom(db.uiConfig.map_data.home.zoom); + } else if (nodeObjects.size() >= 1) { + // no gps, no saved position then center the home location among other available nodes + std::vector sortedLat; + std::vector sortedLon; + sortedLat.reserve(nodeObjects.size()); + sortedLon.reserve(nodeObjects.size()); + for (auto it : nodeObjects) { + lv_obj_t *p = nodes[it.first]; + int32_t lat = (long)p->LV_OBJ_IDX(node_pos1_idx)->user_data; + int32_t lon = (long)p->LV_OBJ_IDX(node_pos2_idx)->user_data; + if (lat && lon) { + sortedLat.push_back(lat); + sortedLon.push_back(lon); + } + } + std::sort(sortedLat.begin(), sortedLat.end()); + std::sort(sortedLon.begin(), sortedLon.end()); + int64_t latcenter = 0; + int64_t loncenter = 0; + int32_t count = 0; + // select just the closest 60% of nodes, ignore the rest + int pp = 100 / 20; + for (int i = sortedLat.size() / pp; i < pp * sortedLat.size() / pp; i++) { + latcenter += sortedLat[i]; + loncenter += sortedLon[i]; + count++; + } + latcenter /= count; + loncenter /= count; + map->setHomeLocation(latcenter * 1e-7, loncenter * 1e-7); + + // calculate optimal zoom factor to fit in all nodes of this range + lv_obj_update_layout(objects.raw_map_panel); + float rangeDeg = 1e-7 * (sortedLon[(pp - 1) * sortedLon.size() / pp] - sortedLon[sortedLon.size() / pp]); + float distanceKm = abs(rangeDeg * 111.32 * cos(1e-7 * sortedLat[sortedLat.size() / 2])); + uint32_t zoom = sqrt(156.543034f / distanceKm * abs(cos(1e-7 * sortedLat[sortedLat.size() / 2])) * 256) + 1; + map->setZoom(zoom); + } else { + // use default location @theBigBentern + map->setZoom(3); + } - if (db.uiConfig.map_data.follow_gps) { - lv_obj_set_state(objects.gps_lock_button, LV_STATE_CHECKED, true); - map->setLocked(true); - } + if (db.uiConfig.map_data.follow_gps) { + lv_obj_set_state(objects.gps_lock_button, LV_STATE_CHECKED, true); + map->setLocked(true); + } - // finally add all node images to the map - if (!nodeObjects.empty()) { - for (auto it : nodeObjects) { - lv_obj_t *p = nodes[it.first]; - float lat = 1e-7 * (long)p->LV_OBJ_IDX(node_pos1_idx)->user_data; - float lon = 1e-7 * (long)p->LV_OBJ_IDX(node_pos2_idx)->user_data; - map->add(it.first, lat, lon, drawObjectCB); - lv_obj_add_flag(it.second, LV_OBJ_FLAG_CLICKABLE); - lv_obj_add_event_cb(it.second, ui_event_mapNodeButton, LV_EVENT_CLICKED, (void *)it.first); - } - } - updateLocationMap(map->getObjectsOnMap()); + // finally add all node images to the map + if (!nodeObjects.empty()) { + for (auto it : nodeObjects) { + lv_obj_t *p = nodes[it.first]; + float lat = 1e-7 * (long)p->LV_OBJ_IDX(node_pos1_idx)->user_data; + float lon = 1e-7 * (long)p->LV_OBJ_IDX(node_pos2_idx)->user_data; + map->add(it.first, lat, lon, drawObjectCB); + lv_obj_add_flag(it.second, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_event_cb(it.second, ui_event_mapNodeButton, LV_EVENT_CLICKED, (void *)it.first); } + } + updateLocationMap(map->getObjectsOnMap()); + } - if (sdCard) { - if (!sdCard->isUpdated()) { - map->setNoTileImage(&img_no_tile_image); - std::set mapStyles = sdCard->loadMapStyles(MapTileSettings::getPrefix()); - if (mapStyles.find("/map") != mapStyles.end()) { - // no styles found, but the /map directory, so use it - MapTileSettings::setPrefix("/map"); - MapTileSettings::setTileStyle(""); - lv_obj_add_flag(objects.map_style_dropdown, LV_OBJ_FLAG_HIDDEN); - } else if (!mapStyles.empty()) { - // populate dropdown - uint16_t pos = 0; - bool savedStyleOK = false; - lv_dropdown_set_options(objects.map_style_dropdown, ""); - for (auto it : mapStyles) { - lv_dropdown_add_option(objects.map_style_dropdown, it.c_str(), pos); - if (it == db.uiConfig.map_data.style) { - lv_dropdown_set_selected(objects.map_style_dropdown, pos); - MapTileSettings::setTileStyle(db.uiConfig.map_data.style); - savedStyleOK = true; - } - pos++; - } - if (!savedStyleOK) { - // no such style on SD, pick first one we found - char style[20]; - lv_dropdown_set_selected(objects.map_style_dropdown, 0); - lv_dropdown_get_selected_str(objects.map_style_dropdown, style, sizeof(style)); - MapTileSettings::setTileStyle(style); - } - MapTileSettings::setPrefix("/maps"); - } else { - messageAlert(_("No map tiles found on SDCard!"), true); - map->setNoTileImage(&img_no_tile_image); + if (sdCard) { + if (!sdCard->isUpdated()) { + map->setNoTileImage(&img_no_tile_image); + std::set mapStyles = sdCard->loadMapStyles(MapTileSettings::getPrefix()); + if (mapStyles.find("/map") != mapStyles.end()) { + // no styles found, but the /map directory, so use it + MapTileSettings::setPrefix("/map"); + MapTileSettings::setTileStyle(""); + lv_obj_add_flag(objects.map_style_dropdown, LV_OBJ_FLAG_HIDDEN); + } else if (!mapStyles.empty()) { + // populate dropdown + uint16_t pos = 0; + bool savedStyleOK = false; + lv_dropdown_set_options(objects.map_style_dropdown, ""); + for (auto it : mapStyles) { + lv_dropdown_add_option(objects.map_style_dropdown, it.c_str(), pos); + if (it == db.uiConfig.map_data.style) { + lv_dropdown_set_selected(objects.map_style_dropdown, pos); + MapTileSettings::setTileStyle(db.uiConfig.map_data.style); + savedStyleOK = true; } - map->forceRedraw(); + pos++; + } + if (!savedStyleOK) { + // no such style on SD, pick first one we found + char style[20]; + lv_dropdown_set_selected(objects.map_style_dropdown, 0); + lv_dropdown_get_selected_str(objects.map_style_dropdown, style, sizeof(style)); + MapTileSettings::setTileStyle(style); } + MapTileSettings::setPrefix("/maps"); } else { - lv_dropdown_set_options(objects.map_style_dropdown, ""); + messageAlert(_("No map tiles found on SDCard!"), true); + map->setNoTileImage(&img_no_tile_image); } - - lv_obj_clear_flag(objects.map_panel, LV_OBJ_FLAG_HIDDEN); - lv_obj_clear_flag(objects.raw_map_panel, LV_OBJ_FLAG_HIDDEN); + map->forceRedraw(); } + } else { + lv_dropdown_set_options(objects.map_style_dropdown, ""); + } - void TFTView_320x240::updateLocationMap(uint32_t num) - { - lv_label_set_text_fmt(objects.top_map_label, _("Locations Map (%d/%d)"), num, nodeCount); + lv_obj_clear_flag(objects.map_panel, LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(objects.raw_map_panel, LV_OBJ_FLAG_HIDDEN); +} + +void TFTView_320x240::updateLocationMap(uint32_t num) +{ + lv_label_set_text_fmt(objects.top_map_label, _("Locations Map (%d/%d)"), num, nodeCount); +} + +/** + * add node location image for display on map + */ +void TFTView_320x240::addOrUpdateMap(uint32_t nodeNum, int32_t lat, int32_t lon) +{ + auto it = nodeObjects.find(nodeNum); + if (it == nodeObjects.end()) { + uint32_t bgColor, fgColor; + std::tie(bgColor, fgColor) = nodeColor(nodeNum); + lv_obj_t *img = lv_image_create(objects.raw_map_panel); + lv_obj_set_size(img, 40, 35); + lv_img_set_src(img, &img_circle_image); + lv_image_set_inner_align(img, LV_IMAGE_ALIGN_TOP_MID); + lv_obj_set_style_opa(img, 180, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_image_recolor(img, lv_color_hex(bgColor), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_image_recolor_opa(img, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_top(img, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_bottom(img, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_left(img, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_right(img, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + + lv_obj_t *lbl = lv_label_create(img); + lv_obj_set_pos(lbl, 0, 0); + lv_obj_set_size(lbl, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_style_text_color(lbl, lv_color_black(), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_opa(img, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_image_recolor_opa(img, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_font(lbl, &lv_font_montserrat_10, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_align(lbl, LV_ALIGN_BOTTOM_MID, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_align(lbl, LV_ALIGN_BOTTOM_MID, LV_PART_MAIN | LV_STATE_DEFAULT); + + lv_obj_t *p = nodes[nodeNum]; + lv_label_set_text_fmt(lbl, "%s", lv_label_get_text(p->LV_OBJ_IDX(node_lbs_idx))); + + // position label callback + lv_obj_add_flag(p->LV_OBJ_IDX(node_pos1_idx), LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_event_cb(p->LV_OBJ_IDX(node_pos1_idx), ui_event_positionButton, LV_EVENT_CLICKED, (void *)p); + + nodeObjects[nodeNum] = img; + if (map) { + map->add(nodeNum, lat * 1e-7, lon * 1e-7, drawObjectCB); + lv_obj_add_flag(img, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_event_cb(img, ui_event_mapNodeButton, LV_EVENT_CLICKED, (void *)nodeNum); + updateLocationMap(map->getObjectsOnMap()); } + } else { + if (map) { + map->update(it->first, lat * 1e-7, lon * 1e-7); + } + } +} - /** - * add node location image for display on map - */ - void TFTView_320x240::addOrUpdateMap(uint32_t nodeNum, int32_t lat, int32_t lon) - { - auto it = nodeObjects.find(nodeNum); - if (it == nodeObjects.end()) { - uint32_t bgColor, fgColor; - std::tie(bgColor, fgColor) = nodeColor(nodeNum); - lv_obj_t *img = lv_image_create(objects.raw_map_panel); - lv_obj_set_size(img, 40, 35); - lv_img_set_src(img, &img_circle_image); - lv_image_set_inner_align(img, LV_IMAGE_ALIGN_TOP_MID); - lv_obj_set_style_opa(img, 180, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_image_recolor(img, lv_color_hex(bgColor), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_image_recolor_opa(img, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_top(img, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_bottom(img, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_left(img, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_right(img, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - - lv_obj_t *lbl = lv_label_create(img); - lv_obj_set_pos(lbl, 0, 0); - lv_obj_set_size(lbl, LV_SIZE_CONTENT, LV_SIZE_CONTENT); - lv_obj_set_style_text_color(lbl, lv_color_black(), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_opa(img, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_image_recolor_opa(img, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_font(lbl, &lv_font_montserrat_10, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_align(lbl, LV_ALIGN_BOTTOM_MID, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_align(lbl, LV_ALIGN_BOTTOM_MID, LV_PART_MAIN | LV_STATE_DEFAULT); - - lv_obj_t *p = nodes[nodeNum]; - lv_label_set_text_fmt(lbl, "%s", lv_label_get_text(p->LV_OBJ_IDX(node_lbs_idx))); - - // position label callback - lv_obj_add_flag(p->LV_OBJ_IDX(node_pos1_idx), LV_OBJ_FLAG_CLICKABLE); - lv_obj_add_event_cb(p->LV_OBJ_IDX(node_pos1_idx), ui_event_positionButton, LV_EVENT_CLICKED, (void *)p); - - nodeObjects[nodeNum] = img; - if (map) { - map->add(nodeNum, lat * 1e-7, lon * 1e-7, drawObjectCB); - lv_obj_add_flag(img, LV_OBJ_FLAG_CLICKABLE); - lv_obj_add_event_cb(img, ui_event_mapNodeButton, LV_EVENT_CLICKED, (void *)nodeNum); - updateLocationMap(map->getObjectsOnMap()); - } +void TFTView_320x240::removeFromMap(uint32_t nodeNum) +{ + auto it = nodeObjects.find(nodeNum); + if (it == nodeObjects.end()) + return; + + lv_obj_t *img = it->second; + if (map) { + map->remove(it->first); + updateLocationMap(map->getObjectsOnMap()); + } + nodeObjects.erase(nodeNum); + lv_obj_remove_event_cb(img, ui_event_mapNodeButton); + lv_obj_delete(img); +} + +void TFTView_320x240::ui_event_mesh_detector(lv_event_t *e) +{ + THIS->ui_set_active(objects.settings_button, objects.mesh_detector_panel, objects.top_mesh_detector_panel); +} + +void TFTView_320x240::ui_event_mesh_detector_start(_lv_event_t *e) +{ + lv_obj_add_flag(objects.detector_contact_button, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(objects.detector_heard_label, LV_OBJ_FLAG_HIDDEN); + if (!THIS->detectorRunning) { + lv_label_set_text(objects.detector_start_label, _("Stop")); + + // create radar animation + lv_anim_t &a = THIS->radar; + lv_anim_init(&a); + lv_anim_set_var(&a, objects.radar_beam); + lv_anim_set_values(&a, 0, 3600); + lv_anim_set_repeat_count(&a, 1800); + lv_anim_set_duration(&a, 7200); + lv_anim_set_exec_cb(&a, ui_anim_radar_cb); + lv_anim_start(&a); + lv_obj_clear_flag(objects.detector_radar_panel, LV_OBJ_FLAG_HIDDEN); + } else { + lv_label_set_text(objects.detector_start_label, _("Start")); + lv_anim_del(&objects.radar_beam, ui_anim_radar_cb); + lv_obj_add_flag(objects.detector_radar_panel, LV_OBJ_FLAG_HIDDEN); + } + THIS->detectorRunning = !THIS->detectorRunning; + THIS->controller->sendPing(); +} + +void TFTView_320x240::ui_event_signal_scanner(lv_event_t *e) +{ + if (currentPanel) { + THIS->setNodeImage(currentNode, (MeshtasticView::eRole)(unsigned long)currentPanel->LV_OBJ_IDX(node_img_idx)->user_data, + false, objects.signal_scanner_node_image); + const char *lbs = lv_label_get_text(currentPanel->LV_OBJ_IDX(node_lbs_idx)); + lv_label_set_text(objects.signal_scanner_node_button_label, lbs); + lv_obj_clear_state(objects.signal_scanner_start_button, LV_STATE_DISABLED); + } else { + lv_label_set_text(objects.signal_scanner_node_button_label, _("choose\nnode")); + lv_obj_add_state(objects.signal_scanner_start_button, LV_STATE_DISABLED); + } + lv_label_set_text(objects.signal_scanner_start_label, _("Start")); + THIS->ui_set_active(objects.settings_button, objects.signal_scanner_panel, objects.top_signal_scanner_panel); +} + +void TFTView_320x240::ui_event_signal_scanner_node(lv_event_t *e) +{ + THIS->chooseNodeSignalScanner = true; + THIS->selectedHops = lv_dropdown_get_selected(objects.nodes_filter_hops_dropdown); + lv_dropdown_set_selected(objects.nodes_filter_hops_dropdown, 7); // 0 hops away + THIS->ui_set_active(objects.nodes_button, objects.nodes_panel, objects.top_nodes_panel); + THIS->updateNodesFiltered(true); + THIS->updateNodesStatus(); +} + +void TFTView_320x240::ui_event_signal_scanner_start(lv_event_t *e) +{ + lv_event_code_t event_code = lv_event_get_code(e); + if (currentNode) { + static bool ignoreClicked = false; + if (event_code == LV_EVENT_CLICKED) { + if (ignoreClicked) { + ignoreClicked = false; + return; + } + if (spinnerButton) { + lv_label_set_text(objects.signal_scanner_start_label, _("Start")); + lv_obj_delete(spinnerButton); + spinnerButton = nullptr; + THIS->scans = 0; } else { - if (map) { - map->update(it->first, lat * 1e-7, lon * 1e-7); - } + THIS->scanSignal(0); } + } else if (event_code == LV_EVENT_LONG_PRESSED) { + ignoreClicked = true; + lv_obj_t *obj = lv_spinner_create(objects.signal_scanner_panel); + spinnerButton = obj; + spinnerButton->user_data = (void *)objects.signal_scanner_panel; + lv_spinner_set_anim_params(obj, 5000, 300); + lv_obj_set_pos(obj, 0, -50); + lv_obj_set_size(obj, 68, 68); + lv_obj_set_style_align(obj, LV_ALIGN_CENTER, LV_PART_MAIN | LV_STATE_DEFAULT); + add_style_spinner_style(obj); + lv_label_set_text(objects.signal_scanner_start_label, "30s"); + THIS->scans = 6 + 1; } + } +} - void TFTView_320x240::removeFromMap(uint32_t nodeNum) - { - auto it = nodeObjects.find(nodeNum); - if (it == nodeObjects.end()) - return; +void TFTView_320x240::ui_event_trace_route(lv_event_t *e) +{ + // show still old route setup while processing is ongoing + time_t now; + time(&now); + if (spinnerButton && (now - startTime) < 30) { + THIS->ui_set_active(objects.settings_button, objects.trace_route_panel, objects.top_trace_route_panel); + return; + } + THIS->removeSpinner(); - lv_obj_t *img = it->second; - if (map) { - map->remove(it->first); - updateLocationMap(map->getObjectsOnMap()); - } - nodeObjects.erase(nodeNum); - lv_obj_remove_event_cb(img, ui_event_mapNodeButton); - lv_obj_delete(img); + // remove old route except first button and spinner panel + ILOG_DEBUG("removing old route: %d %d %d", lv_obj_get_child_cnt(objects.trace_route_panel), + lv_obj_get_child_cnt(objects.route_towards_panel), lv_obj_get_child_cnt(objects.route_back_panel)); + + uint16_t children = lv_obj_get_child_cnt(objects.trace_route_panel) - 1; + while (children > 1) { + if (objects.trace_route_panel->spec_attr->children[children]->class_p == &lv_button_class) { + lv_obj_delete(objects.trace_route_panel->spec_attr->children[children]); } + children--; + } - void TFTView_320x240::ui_event_mesh_detector(lv_event_t * e) - { - THIS->ui_set_active(objects.settings_button, objects.mesh_detector_panel, objects.top_mesh_detector_panel); + // forward route + children = lv_obj_get_child_cnt(objects.route_towards_panel); + while (children > 0) { + children--; + if (objects.route_towards_panel->spec_attr->children[children]->class_p == &lv_button_class) { + lv_obj_delete(objects.route_towards_panel->spec_attr->children[children]); } + } - void TFTView_320x240::ui_event_mesh_detector_start(_lv_event_t * e) - { - lv_obj_add_flag(objects.detector_contact_button, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(objects.detector_heard_label, LV_OBJ_FLAG_HIDDEN); - if (!THIS->detectorRunning) { - lv_label_set_text(objects.detector_start_label, _("Stop")); - - // create radar animation - lv_anim_t &a = THIS->radar; - lv_anim_init(&a); - lv_anim_set_var(&a, objects.radar_beam); - lv_anim_set_values(&a, 0, 3600); - lv_anim_set_repeat_count(&a, 1800); - lv_anim_set_duration(&a, 7200); - lv_anim_set_exec_cb(&a, ui_anim_radar_cb); - lv_anim_start(&a); - lv_obj_clear_flag(objects.detector_radar_panel, LV_OBJ_FLAG_HIDDEN); - } else { - lv_label_set_text(objects.detector_start_label, _("Start")); - lv_anim_del(&objects.radar_beam, ui_anim_radar_cb); - lv_obj_add_flag(objects.detector_radar_panel, LV_OBJ_FLAG_HIDDEN); - } - THIS->detectorRunning = !THIS->detectorRunning; - THIS->controller->sendPing(); + // backward route + children = lv_obj_get_child_cnt(objects.route_back_panel); + while (children > 0) { + children--; + if (objects.route_back_panel->spec_attr->children[children]->class_p == &lv_button_class) { + lv_obj_delete(objects.route_back_panel->spec_attr->children[children]); } + } - void TFTView_320x240::ui_event_signal_scanner(lv_event_t * e) - { - if (currentPanel) { - THIS->setNodeImage(currentNode, - (MeshtasticView::eRole)(unsigned long)currentPanel->LV_OBJ_IDX(node_img_idx)->user_data, false, - objects.signal_scanner_node_image); - const char *lbs = lv_label_get_text(currentPanel->LV_OBJ_IDX(node_lbs_idx)); - lv_label_set_text(objects.signal_scanner_node_button_label, lbs); - lv_obj_clear_state(objects.signal_scanner_start_button, LV_STATE_DISABLED); - } else { - lv_label_set_text(objects.signal_scanner_node_button_label, _("choose\nnode")); - lv_obj_add_state(objects.signal_scanner_start_button, LV_STATE_DISABLED); + lv_obj_clear_flag(objects.start_button_panel, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(objects.hop_routes_panel, LV_OBJ_FLAG_HIDDEN); + + if (currentPanel) { + THIS->setNodeImage(THIS->currentNode, + (MeshtasticView::eRole)(unsigned long)currentPanel->LV_OBJ_IDX(node_img_idx)->user_data, false, + objects.trace_route_to_image); + const char *lbl = lv_label_get_text(currentPanel->LV_OBJ_IDX(node_lbl_idx)); + lv_label_set_text(objects.trace_route_to_button_label, lbl); + lv_obj_clear_state(objects.trace_route_start_button, LV_STATE_DISABLED); + } else { + lv_label_set_text(objects.trace_route_to_button_label, _("choose target node")); + lv_obj_add_state(objects.trace_route_start_button, LV_STATE_DISABLED); + } + lv_label_set_text(objects.trace_route_start_label, _("Start")); + THIS->ui_set_active(objects.settings_button, objects.trace_route_panel, objects.top_trace_route_panel); +} + +void TFTView_320x240::ui_event_trace_route_to(lv_event_t *e) +{ + if (!spinnerButton) { + THIS->chooseNodeTraceRoute = true; + THIS->ui_set_active(objects.nodes_button, objects.nodes_panel, objects.top_nodes_panel); + } +} + +void TFTView_320x240::ui_event_trace_route_start(lv_event_t *e) +{ + if (!spinnerButton) { + if (currentPanel) { + time(&startTime); + lv_obj_t *obj = lv_spinner_create(objects.start_button_panel); + spinnerButton = obj; + lv_spinner_set_anim_params(obj, 5000, 300); + lv_obj_set_pos(obj, 0, 0); + lv_obj_set_size(obj, 68, 68); + lv_obj_set_style_align(obj, LV_ALIGN_CENTER, LV_PART_MAIN | LV_STATE_DEFAULT); + add_style_spinner_style(obj); + lv_label_set_text(objects.trace_route_start_label, "30s"); + + // retrieve nodeNum from current node + // FIXME: remove for loop + for (auto &it : THIS->nodes) { + if (it.second == currentPanel) { + uint32_t requestId; + uint32_t to = it.first; + uint8_t ch = (uint8_t)(unsigned long)currentPanel->user_data; + // trial: hoplimit optimization for direct messages + int8_t hopsAway = (signed long)THIS->nodes[to]->LV_OBJ_IDX(node_sig_idx)->user_data; + if (hopsAway < 0) + hopsAway = 5; + uint8_t hopLimit = (hopsAway < THIS->db.config.lora.hop_limit ? hopsAway + 1 : hopsAway); + requestId = THIS->requests.addRequest(to, ResponseHandler::TraceRouteRequest); + THIS->controller->traceRoute(to, ch, hopLimit, requestId); + break; + } } - lv_label_set_text(objects.signal_scanner_start_label, _("Start")); - THIS->ui_set_active(objects.settings_button, objects.signal_scanner_panel, objects.top_signal_scanner_panel); } + } else { + // restart + ui_event_trace_route(e); + } +} - void TFTView_320x240::ui_event_signal_scanner_node(lv_event_t * e) - { - THIS->chooseNodeSignalScanner = true; - THIS->selectedHops = lv_dropdown_get_selected(objects.nodes_filter_hops_dropdown); - lv_dropdown_set_selected(objects.nodes_filter_hops_dropdown, 7); // 0 hops away - THIS->ui_set_active(objects.nodes_button, objects.nodes_panel, objects.top_nodes_panel); - THIS->updateNodesFiltered(true); - THIS->updateNodesStatus(); +void TFTView_320x240::ui_event_trace_route_node(lv_event_t *e) +{ + // navigate to node in node list + lv_obj_t *panel = (lv_obj_t *)e->user_data; + THIS->ui_set_active(objects.nodes_button, objects.nodes_panel, objects.top_nodes_panel); + lv_obj_scroll_to_view(panel, LV_ANIM_ON); +} + +void TFTView_320x240::removeSpinner(void) +{ + if (spinnerButton) { + lv_obj_delete(spinnerButton); + spinnerButton = nullptr; + startTime = 0; + } +} + +void TFTView_320x240::ui_event_node_details(lv_event_t *e) +{ + THIS->ui_set_active(objects.settings_button, objects.details_panel, objects.top_nodes_panel); +} + +void TFTView_320x240::ui_event_statistics(lv_event_t *e) +{ + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED) { + THIS->ui_set_active(objects.settings_button, objects.tools_statistics_panel, objects.top_statistics_panel); + } else if (event_code == LV_EVENT_LONG_PRESSED) { + // clear statistics table + THIS->updateStatistics(meshtastic_MeshPacket{.from = 0}); + } +} + +void TFTView_320x240::ui_event_packet_log(lv_event_t *e) +{ + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED) { + THIS->ui_set_active(objects.settings_button, objects.tools_packet_log_panel, objects.top_packet_log_panel); + THIS->packetLogEnabled = true; + } else if (event_code == LV_EVENT_LONG_PRESSED) { + THIS->packetCounter = 0; + lv_obj_clean(objects.tools_packet_log_panel); + } +} + +void TFTView_320x240::packetDetected(const meshtastic_MeshPacket &p) +{ + uint32_t heard = 0; + if (p.from != ownNode) + heard = p.from; + if (p.to != 0xffffffff && p.to != ownNode) + heard = p.to; + + if (heard) { + if (p.to == ownNode && p.decoded.portnum == meshtastic_PortNum_NODEINFO_APP) { + // we finally sensed a two-way contact to us; stop the detector + detectorRunning = false; + lv_label_set_text(objects.detector_start_label, _("Start")); + lv_anim_del(&objects.radar_beam, ui_anim_radar_cb); + lv_obj_add_flag(objects.detector_radar_panel, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(objects.detector_heard_label, LV_OBJ_FLAG_HIDDEN); + + setNodeImage(p.from, (MeshtasticView::eRole)(unsigned long)nodes[p.from]->LV_OBJ_IDX(node_img_idx)->user_data, false, + objects.detector_contact_image); + const char *lbl = lv_label_get_text(nodes[p.from]->LV_OBJ_IDX(node_lbl_idx)); + + char from[5]; + char *userShort = (char *)&(nodes[p.from]->LV_OBJ_IDX(node_lbs_idx)->user_data); + int pos = 0; + while (pos < 4 && userShort[pos] != 0) { + from[pos] = userShort[pos]; + pos++; + } + from[pos] = '\0'; + + char buf[64]; + lv_snprintf(buf, 64, "%s(%04x)\n%s", from, p.from & 0xffff, lbl); + lv_label_set_text(objects.detector_contact_label, buf); + lv_obj_clear_flag(objects.detector_contact_button, LV_OBJ_FLAG_HIDDEN); + } else { + char buf[20]; + lv_snprintf(buf, 20, _("heard: !%08x"), heard); + lv_label_set_text(objects.detector_heard_label, buf); + lv_obj_clear_flag(objects.detector_heard_label, LV_OBJ_FLAG_HIDDEN); } + } +} - void TFTView_320x240::ui_event_signal_scanner_start(lv_event_t * e) - { - lv_event_code_t event_code = lv_event_get_code(e); - if (currentNode) { - static bool ignoreClicked = false; - if (event_code == LV_EVENT_CLICKED) { - if (ignoreClicked) { - ignoreClicked = false; - return; - } - if (spinnerButton) { - lv_label_set_text(objects.signal_scanner_start_label, _("Start")); - lv_obj_delete(spinnerButton); - spinnerButton = nullptr; - THIS->scans = 0; - } else { - THIS->scanSignal(0); - } - } else if (event_code == LV_EVENT_LONG_PRESSED) { - ignoreClicked = true; - lv_obj_t *obj = lv_spinner_create(objects.signal_scanner_panel); - spinnerButton = obj; - spinnerButton->user_data = (void *)objects.signal_scanner_panel; - lv_spinner_set_anim_params(obj, 5000, 300); - lv_obj_set_pos(obj, 0, -50); - lv_obj_set_size(obj, 68, 68); - lv_obj_set_style_align(obj, LV_ALIGN_CENTER, LV_PART_MAIN | LV_STATE_DEFAULT); - add_style_spinner_style(obj); - lv_label_set_text(objects.signal_scanner_start_label, "30s"); - THIS->scans = 6 + 1; - } +void TFTView_320x240::writePacketLog(const meshtastic_MeshPacket &p) +{ + static std::unordered_map name = { + {0, "unknown"}, {1, "text message"}, {2, "remote hardware"}, {3, "position"}, {4, "node info"}, + {5, "routing"}, {6, "admin"}, {7, "text message"}, {8, "waypoint"}, {9, "audio"}, + {10, "sensor"}, {32, "reply"}, {33, "ip tunnel"}, {34, "paxcounter"}, {64, "serial"}, + {65, "store forward"}, {66, "range test"}, {67, "telemetry"}, {68, "ZPS"}, {69, "simulator"}, + {70, "tracert"}, {71, "neighbor info"}, {72, "atax"}, {73, "map report"}, {74, "power stress"}, + {256, "private"}, {257, "atax forwarder"}}; + + // ignore admin packages initiated by us + if (p.from == ownNode && p.decoded.portnum == meshtastic_PortNum_ADMIN_APP) + return; + + // get actual time + char timebuf[16]; + time_t curr_time; +#ifdef ARCH_PORTDUINO + time(&curr_time); +#else + curr_time = actTime; +#endif + tm *curr_tm = localtime(&curr_time); + if (VALID_TIME(curr_time)) { + strftime(timebuf, 16, "%T", curr_tm); + } else { + strcpy(timebuf, "??:??:??"); + } + + // get node name from + char from[5]; + char *userShort = (char *)&(nodes[p.from]->LV_OBJ_IDX(node_lbs_idx)->user_data); + int pos = 0; + while (pos < 4 && userShort[pos] != 0) { + from[pos] = userShort[pos]; + pos++; + } + from[pos] = '\0'; + + char buf[256]; + if (p.to == 0xffffffff) + sprintf(buf, "%s: ch%d %s:%04x->all: %s", timebuf, p.channel, from, p.from & 0xffff, + name[p.decoded.portnum]); // note: this may crash if there's a new portnum not in this map... + else + sprintf(buf, "%s: ch%d %s:%04x->%s%04x: %s", timebuf, p.channel, from, p.from & 0xffff, p.to == ownNode ? "*" : "", + p.to & 0xffff, name[p.decoded.portnum]); + + if (p.decoded.portnum == meshtastic_PortNum_TELEMETRY_APP) { + meshtastic_Telemetry telemetry; + if (pb_decode_from_bytes(p.decoded.payload.bytes, p.decoded.payload.size, &meshtastic_Telemetry_msg, &telemetry)) { + switch (telemetry.which_variant) { + case meshtastic_Telemetry_device_metrics_tag: { + if (p.from == ownNode) + return; // suppress (internal) battery level packets + strcat(buf, " dev"); + break; + } + case meshtastic_Telemetry_environment_metrics_tag: { + strcat(buf, " env"); + break; + } + case meshtastic_Telemetry_air_quality_metrics_tag: { + strcat(buf, " air"); + break; } - } - - void TFTView_320x240::ui_event_trace_route(lv_event_t * e) - { - // show still old route setup while processing is ongoing - time_t now; - time(&now); - if (spinnerButton && (now - startTime) < 30) { - THIS->ui_set_active(objects.settings_button, objects.trace_route_panel, objects.top_trace_route_panel); - return; + case meshtastic_Telemetry_power_metrics_tag: { + strcat(buf, " pow"); + break; } - THIS->removeSpinner(); - - // remove old route except first button and spinner panel - ILOG_DEBUG("removing old route: %d %d %d", lv_obj_get_child_cnt(objects.trace_route_panel), - lv_obj_get_child_cnt(objects.route_towards_panel), lv_obj_get_child_cnt(objects.route_back_panel)); - - uint16_t children = lv_obj_get_child_cnt(objects.trace_route_panel) - 1; - while (children > 1) { - if (objects.trace_route_panel->spec_attr->children[children]->class_p == &lv_button_class) { - lv_obj_delete(objects.trace_route_panel->spec_attr->children[children]); - } - children--; + case meshtastic_Telemetry_local_stats_tag: { + strcat(buf, " dev"); // bug in firmware that this is local? } - - // forward route - children = lv_obj_get_child_cnt(objects.route_towards_panel); - while (children > 0) { - children--; - if (objects.route_towards_panel->spec_attr->children[children]->class_p == &lv_button_class) { - lv_obj_delete(objects.route_towards_panel->spec_attr->children[children]); - } + default: + break; } + } + } else if (p.decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP) { + // print the recorded route and add from/to manually + strcat(buf, "\n"); + int pos = strlen(buf); + if (p.to == ownNode) { + pos += snprintf(&buf[pos], 16, "%04x", ownNode & 0xffff); + } - // backward route - children = lv_obj_get_child_cnt(objects.route_back_panel); - while (children > 0) { - children--; - if (objects.route_back_panel->spec_attr->children[children]->class_p == &lv_button_class) { - lv_obj_delete(objects.route_back_panel->spec_attr->children[children]); + meshtastic_RouteDiscovery route; + if (pb_decode_from_bytes(p.decoded.payload.bytes, p.decoded.payload.size, &meshtastic_RouteDiscovery_msg, &route)) { + for (int i = 0; i < route.route_count; i++) { + uint32_t nodeNum = route.route[i]; + if (nodeNum != UINT32_MAX) { + pos += snprintf(&buf[pos], 16, "->%04x", nodeNum & 0xffff); + } else { + strcat(buf, "->unk"); + pos += 6; } } - - lv_obj_clear_flag(objects.start_button_panel, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(objects.hop_routes_panel, LV_OBJ_FLAG_HIDDEN); - - if (currentPanel) { - THIS->setNodeImage(THIS->currentNode, - (MeshtasticView::eRole)(unsigned long)currentPanel->LV_OBJ_IDX(node_img_idx)->user_data, false, - objects.trace_route_to_image); - const char *lbl = lv_label_get_text(currentPanel->LV_OBJ_IDX(node_lbl_idx)); - lv_label_set_text(objects.trace_route_to_button_label, lbl); - lv_obj_clear_state(objects.trace_route_start_button, LV_STATE_DISABLED); - } else { - lv_label_set_text(objects.trace_route_to_button_label, _("choose target node")); - lv_obj_add_state(objects.trace_route_start_button, LV_STATE_DISABLED); + if (p.to == ownNode) { + pos += snprintf(&buf[pos], 16, "->%04x", p.from & 0xffff); } - lv_label_set_text(objects.trace_route_start_label, _("Start")); - THIS->ui_set_active(objects.settings_button, objects.trace_route_panel, objects.top_trace_route_panel); } + } - void TFTView_320x240::ui_event_trace_route_to(lv_event_t * e) - { - if (!spinnerButton) { - THIS->chooseNodeTraceRoute = true; - THIS->ui_set_active(objects.nodes_button, objects.nodes_panel, objects.top_nodes_panel); - } - } + if (packetCounter >= PACKET_LOGS_MAX) { + // delete oldest entry + lv_obj_del(objects.tools_packet_log_panel->spec_attr->children[0]); + } else { + packetCounter++; + char top[24]; + sprintf(top, _("Packet Log: %d"), packetCounter); + lv_label_set_text(objects.top_packet_log_label, top); + } + lv_obj_t *pLabel = lv_label_create(objects.tools_packet_log_panel); + lv_obj_set_pos(pLabel, 0, 0); + lv_obj_set_size(pLabel, LV_PCT(100), LV_SIZE_CONTENT); + uint32_t bgColor, fgColor; + std::tie(bgColor, fgColor) = nodeColor(p.from); + lv_obj_set_style_bg_color(pLabel, lv_color_hex(bgColor), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_color(pLabel, lv_color_hex(fgColor), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(pLabel, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_label_set_text(pLabel, buf); + + // auto-scroll if last item is visible + if (lv_obj_get_scroll_bottom(objects.tools_packet_log_panel) < 20) + lv_obj_scroll_to_view(pLabel, LV_ANIM_OFF); +} + +void TFTView_320x240::updateStatistics(const meshtastic_MeshPacket &p) +{ + struct Stats { + uint32_t id; + uint16_t row; + uint16_t tel; + uint16_t pos; + uint16_t inf; + uint16_t trc; + uint16_t txt; + uint16_t nbr; + uint32_t sum; + + bool operator==(const Stats &rhs) const { return id == rhs.id; } - void TFTView_320x240::ui_event_trace_route_start(lv_event_t * e) + Stats &operator+=(const Stats &rhs) { - if (!spinnerButton) { - if (currentPanel) { - time(&startTime); - lv_obj_t *obj = lv_spinner_create(objects.start_button_panel); - spinnerButton = obj; - lv_spinner_set_anim_params(obj, 5000, 300); - lv_obj_set_pos(obj, 0, 0); - lv_obj_set_size(obj, 68, 68); - lv_obj_set_style_align(obj, LV_ALIGN_CENTER, LV_PART_MAIN | LV_STATE_DEFAULT); - add_style_spinner_style(obj); - lv_label_set_text(objects.trace_route_start_label, "30s"); - - // retrieve nodeNum from current node - // FIXME: remove for loop - for (auto &it : THIS->nodes) { - if (it.second == currentPanel) { - uint32_t requestId; - uint32_t to = it.first; - uint8_t ch = (uint8_t)(unsigned long)currentPanel->user_data; - // trial: hoplimit optimization for direct messages - int8_t hopsAway = (signed long)THIS->nodes[to]->LV_OBJ_IDX(node_sig_idx)->user_data; - if (hopsAway < 0) - hopsAway = 5; - uint8_t hopLimit = (hopsAway < THIS->db.config.lora.hop_limit ? hopsAway + 1 : hopsAway); - requestId = THIS->requests.addRequest(to, ResponseHandler::TraceRouteRequest); - THIS->controller->traceRoute(to, ch, hopLimit, requestId); - break; - } - } - } - } else { - // restart - ui_event_trace_route(e); - } + this->tel += rhs.tel; + this->pos += rhs.pos; + this->inf += rhs.inf; + this->trc += rhs.trc; + this->txt += rhs.txt; + this->nbr += rhs.nbr; + this->sum += 1; + return *this; } - void TFTView_320x240::ui_event_trace_route_node(lv_event_t * e) + bool operator<(const Stats &rhs) const { - // navigate to node in node list - lv_obj_t *panel = (lv_obj_t *)e->user_data; - THIS->ui_set_active(objects.nodes_button, objects.nodes_panel, objects.top_nodes_panel); - lv_obj_scroll_to_view(panel, LV_ANIM_ON); + return sum > rhs.sum; // sort reverse but skip equal values } + }; + static std::list stats; - void TFTView_320x240::removeSpinner(void) - { - if (spinnerButton) { - lv_obj_delete(spinnerButton); - spinnerButton = nullptr; - startTime = 0; + if (p.from == 0) { + // clear table + stats.clear(); + for (int i = 1; i < statisticTableRows; i++) { + for (int j = 0; j < 7; j++) { + lv_table_set_cell_value(objects.statistics_table, i, j, ""); } } + return; + } - void TFTView_320x240::ui_event_node_details(lv_event_t * e) - { - THIS->ui_set_active(objects.settings_button, objects.details_panel, objects.top_nodes_panel); + // update statistic for node + Stats stat = {p.from}; + switch (p.decoded.portnum) { + case meshtastic_PortNum_TELEMETRY_APP: { + meshtastic_Telemetry telemetry; + if (pb_decode_from_bytes(p.decoded.payload.bytes, p.decoded.payload.size, &meshtastic_Telemetry_msg, &telemetry)) { + if (telemetry.which_variant == meshtastic_Telemetry_device_metrics_tag) { + if (p.from == ownNode) + return; // suppress (internal) battery level packets + } } + stat.tel++; + break; + } + case meshtastic_PortNum_POSITION_APP: { + stat.pos++; + break; + } + case meshtastic_PortNum_NODEINFO_APP: { + stat.inf++; + break; + } + case meshtastic_PortNum_ROUTING_APP: + case meshtastic_PortNum_TRACEROUTE_APP: { + stat.trc++; + break; + } + case meshtastic_PortNum_TEXT_MESSAGE_APP: + case meshtastic_PortNum_RANGE_TEST_APP: { + stat.txt++; + break; + } + case meshtastic_PortNum_NEIGHBORINFO_APP: { + stat.nbr++; + break; + } + case meshtastic_PortNum_ADMIN_APP: { + // ignore + break; + } + default: + ILOG_WARN("packet portnum in stats unhandled: %d", p.decoded.portnum); + stat.sum++; + return; + } - void TFTView_320x240::ui_event_statistics(lv_event_t * e) - { - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED) { - THIS->ui_set_active(objects.settings_button, objects.tools_statistics_panel, objects.top_statistics_panel); - } else if (event_code == LV_EVENT_LONG_PRESSED) { - // clear statistics table - THIS->updateStatistics(meshtastic_MeshPacket{.from = 0}); + std::list::iterator it = std::find(stats.begin(), stats.end(), stat); + if (it == stats.end()) { + stat.row = stats.size(); + stat.sum = 1; + // TODO: stop if memory limit is reached + stats.push_back(stat); + } else { + *it += stat; + } + + stats.sort(); + + // fill packet statistics table + char buf[10]; + int row = 1; + bool move = false; + + for (auto it2 : stats) { + if (it2.id == p.from || move) { + buf[0] = '\0'; + auto it = nodes.find(it2.id); // node may have been removed from nodes, so check if still there + if (it != nodes.end() && it->second) { + char *userData = (char *)&(it->second->LV_OBJ_IDX(node_lbs_idx)->user_data); + if (userData) { + buf[0] = userData[0]; + buf[1] = userData[1]; + buf[2] = userData[2]; + buf[3] = userData[3]; + buf[4] = '\0'; + } + } + + lv_table_set_cell_value(objects.statistics_table, row, 0, buf); + sprintf(buf, "%d", it2.tel); + lv_table_set_cell_value(objects.statistics_table, row, 1, buf); + sprintf(buf, "%d", it2.pos); + lv_table_set_cell_value(objects.statistics_table, row, 2, buf); + sprintf(buf, "%d", it2.inf); + lv_table_set_cell_value(objects.statistics_table, row, 3, buf); + sprintf(buf, "%d", it2.trc); + lv_table_set_cell_value(objects.statistics_table, row, 4, buf); + sprintf(buf, "%d", it2.nbr); + lv_table_set_cell_value(objects.statistics_table, row, 5, buf); + sprintf(buf, "%d", it2.sum); + lv_table_set_cell_value(objects.statistics_table, row, 6, buf); + + if (row != it2.row) { + it2.row = row; + move = true; + } else { + break; } } + row++; + if (row >= statisticTableRows) // fill rows till bottom of 320x240 display + break; + } +} - void TFTView_320x240::ui_event_packet_log(lv_event_t * e) - { - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED) { - THIS->ui_set_active(objects.settings_button, objects.tools_packet_log_panel, objects.top_packet_log_panel); - THIS->packetLogEnabled = true; - } else if (event_code == LV_EVENT_LONG_PRESSED) { - THIS->packetCounter = 0; - lv_obj_clean(objects.tools_packet_log_panel); +void TFTView_320x240::ui_event_statistics_table(lv_event_t *e) +{ + lv_draw_task_t *draw_task = lv_event_get_draw_task(e); + lv_draw_dsc_base_t *base_dsc = (lv_draw_dsc_base_t *)lv_draw_task_get_draw_dsc(draw_task); + // if the cells are drawn... + if (base_dsc->part == LV_PART_ITEMS) { + // make the texts in the first cell blueish + lv_draw_fill_dsc_t *fill_draw_dsc = lv_draw_task_get_fill_dsc(draw_task); + if (fill_draw_dsc) { + uint32_t row = base_dsc->id1; + if (row == 0) { + fill_draw_dsc->color = lv_color_mix(lv_palette_main(LV_PALETTE_BLUE), fill_draw_dsc->color, LV_OPA_20); + } + // make every 2nd row grayish + else { + Themes::recolorTableRow(fill_draw_dsc, row % 2 == 0); } } + } +} - void TFTView_320x240::packetDetected(const meshtastic_MeshPacket &p) - { - uint32_t heard = 0; - if (p.from != ownNode) - heard = p.from; - if (p.to != 0xffffffff && p.to != ownNode) - heard = p.to; - - if (heard) { - if (p.to == ownNode && p.decoded.portnum == meshtastic_PortNum_NODEINFO_APP) { - // we finally sensed a two-way contact to us; stop the detector - detectorRunning = false; - lv_label_set_text(objects.detector_start_label, _("Start")); - lv_anim_del(&objects.radar_beam, ui_anim_radar_cb); - lv_obj_add_flag(objects.detector_radar_panel, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(objects.detector_heard_label, LV_OBJ_FLAG_HIDDEN); - - setNodeImage(p.from, (MeshtasticView::eRole)(unsigned long)nodes[p.from]->LV_OBJ_IDX(node_img_idx)->user_data, - false, objects.detector_contact_image); - const char *lbl = lv_label_get_text(nodes[p.from]->LV_OBJ_IDX(node_lbl_idx)); - - char from[5]; - char *userShort = (char *)&(nodes[p.from]->LV_OBJ_IDX(node_lbs_idx)->user_data); - int pos = 0; - while (pos < 4 && userShort[pos] != 0) { - from[pos] = userShort[pos]; - pos++; - } - from[pos] = '\0'; +void TFTView_320x240::requestSetup(void) +{ + ui_set_active(objects.settings_button, objects.initial_setup_panel, objects.top_setup_panel); + lv_dropdown_set_selected(objects.setup_region_dropdown, 0); + lv_obj_clear_flag(objects.initial_setup_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.setup_region_dropdown); + THIS->disablePanel(objects.controller_panel); + THIS->activeSettings = eSetup; +} - char buf[64]; - lv_snprintf(buf, 64, "%s(%04x)\n%s", from, p.from & 0xffff, lbl); - lv_label_set_text(objects.detector_contact_label, buf); - lv_obj_clear_flag(objects.detector_contact_button, LV_OBJ_FLAG_HIDDEN); - } else { - char buf[20]; - lv_snprintf(buf, 20, _("heard: !%08x"), heard); - lv_label_set_text(objects.detector_heard_label, buf); - lv_obj_clear_flag(objects.detector_heard_label, LV_OBJ_FLAG_HIDDEN); - } - } +/** + * update signal strength text and image for home screen + */ +void TFTView_320x240::updateSignalStrength(int32_t rssi, float snr) +{ + // remember time we last heard a node + time(&lastHeard); + + if (rssi != 0 || snr != 0.0) { + char buf[40]; + sprintf(buf, "SNR: %.1f\nRSSI: %" PRId32, snr, rssi); + lv_label_set_text(objects.home_signal_label, buf); + + uint32_t pct = signalStrength2Percent(rssi, snr); + sprintf(buf, "(%d%%)", pct); + lv_label_set_text(objects.home_signal_pct_label, buf); + if (pct > 80) { + lv_obj_set_style_bg_image_src(objects.home_signal_button, &img_home_signal_button_image, + LV_PART_MAIN | LV_STATE_DEFAULT); + } else if (pct > 60) { + lv_obj_set_style_bg_image_src(objects.home_signal_button, &img_home_strong_signal_image, + LV_PART_MAIN | LV_STATE_DEFAULT); + } else if (pct > 40) { + lv_obj_set_style_bg_image_src(objects.home_signal_button, &img_home_good_signal_image, + LV_PART_MAIN | LV_STATE_DEFAULT); + } else if (pct > 20) { + lv_obj_set_style_bg_image_src(objects.home_signal_button, &img_home_fair_signal_image, + LV_PART_MAIN | LV_STATE_DEFAULT); + } else if (pct > 1) { + lv_obj_set_style_bg_image_src(objects.home_signal_button, &img_home_weak_signal_image, + LV_PART_MAIN | LV_STATE_DEFAULT); + } else { + lv_obj_set_style_bg_image_src(objects.home_signal_button, &img_home_no_signal_image, LV_PART_MAIN | LV_STATE_DEFAULT); } + } +} - void TFTView_320x240::writePacketLog(const meshtastic_MeshPacket &p) - { - static std::unordered_map name = { - {0, "unknown"}, {1, "text message"}, {2, "remote hardware"}, {3, "position"}, {4, "node info"}, - {5, "routing"}, {6, "admin"}, {7, "text message"}, {8, "waypoint"}, {9, "audio"}, - {10, "sensor"}, {32, "reply"}, {33, "ip tunnel"}, {34, "paxcounter"}, {64, "serial"}, - {65, "store forward"}, {66, "range test"}, {67, "telemetry"}, {68, "ZPS"}, {69, "simulator"}, - {70, "tracert"}, {71, "neighbor info"}, {72, "atax"}, {73, "map report"}, {74, "power stress"}, - {256, "private"}, {257, "atax forwarder"}}; - - // ignore admin packages initiated by us - if (p.from == ownNode && p.decoded.portnum == meshtastic_PortNum_ADMIN_APP) - return; +/** + * Translate proto modem preset enum value to numerical position in dropdown menu + */ +uint32_t TFTView_320x240::preset2val(meshtastic_Config_LoRaConfig_ModemPreset preset) +{ + int32_t val[] = {0, -1, -1, 4, 3, 7, 5, 1, 6, 2}; - // get actual time - char timebuf[16]; - time_t curr_time; -#ifdef ARCH_PORTDUINO - time(&curr_time); + if (preset > (sizeof(val) / sizeof(val[0]) - 1) || val[preset] == -1) { + ILOG_WARN("unknown or deprecated preset value: %d", preset); + return 0; + } + return uint32_t(val[preset]); +} + +/** + * Translate value from dropdown menu to modem preset proto enum + */ +meshtastic_Config_LoRaConfig_ModemPreset TFTView_320x240::val2preset(uint32_t val) +{ + meshtastic_Config_LoRaConfig_ModemPreset preset[] = { + meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE, + meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, + meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST, + meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW}; + if (val > (sizeof(preset) / sizeof(preset[0]) - 1)) { + ILOG_ERROR("unknown preset value: %d", val); + return meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; + } + return preset[val]; +} + +/** + * Translate proto role enum value to numerical position in dropdown menu + */ +uint32_t TFTView_320x240::role2val(meshtastic_Config_DeviceConfig_Role role) +{ +#ifdef USE_ROUTER_ROLE + int32_t val[] = {0, 1, 2, -1, 3, 4, 5, 6, 7, 8, 9}; #else - curr_time = actTime; + int32_t val[] = {0, 1, -1, -1, -1, 2, 3, 4, 5, 6, 7}; #endif - tm *curr_tm = localtime(&curr_time); - if (VALID_TIME(curr_time)) { - strftime(timebuf, 16, "%T", curr_tm); - } else { - strcpy(timebuf, "??:??:??"); - } + if (role > 10 || val[role] == -1) { + ILOG_WARN("unknown role value: %d", role); + return 0; + } + return uint32_t(val[role]); +} - // get node name from - char from[5]; - char *userShort = (char *)&(nodes[p.from]->LV_OBJ_IDX(node_lbs_idx)->user_data); - int pos = 0; - while (pos < 4 && userShort[pos] != 0) { - from[pos] = userShort[pos]; - pos++; - } - from[pos] = '\0'; +/** + * Translate value from dropdown menu to role proto enum + */ +meshtastic_Config_DeviceConfig_Role TFTView_320x240::val2role(uint32_t val) +{ + meshtastic_Config_DeviceConfig_Role role[] = {meshtastic_Config_DeviceConfig_Role_CLIENT, + meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE, +#ifdef USE_ROUTER_ROLE + meshtastic_Config_DeviceConfig_Role_ROUTER, + meshtastic_Config_DeviceConfig_Role_REPEATER, +#endif + meshtastic_Config_DeviceConfig_Role_TRACKER, + meshtastic_Config_DeviceConfig_Role_SENSOR, + meshtastic_Config_DeviceConfig_Role_TAK, + meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN, + meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND, + meshtastic_Config_DeviceConfig_Role_TAK_TRACKER, + meshtastic_Config_DeviceConfig_Role_ROUTER_LATE}; + if (val > 10) { + ILOG_WARN("unknown role value: %d", val); + return meshtastic_Config_DeviceConfig_Role_CLIENT; + } + return role[val]; +} - char buf[256]; - if (p.to == 0xffffffff) - sprintf(buf, "%s: ch%d %s:%04x->all: %s", timebuf, p.channel, from, p.from & 0xffff, - name[p.decoded.portnum]); // note: this may crash if there's a new portnum not in this map... - else - sprintf(buf, "%s: ch%d %s:%04x->%s%04x: %s", timebuf, p.channel, from, p.from & 0xffff, - p.to == ownNode ? "*" : "", p.to & 0xffff, name[p.decoded.portnum]); - - if (p.decoded.portnum == meshtastic_PortNum_TELEMETRY_APP) { - meshtastic_Telemetry telemetry; - if (pb_decode_from_bytes(p.decoded.payload.bytes, p.decoded.payload.size, &meshtastic_Telemetry_msg, - &telemetry)) { - switch (telemetry.which_variant) { - case meshtastic_Telemetry_device_metrics_tag: { - if (p.from == ownNode) - return; // suppress (internal) battery level packets - strcat(buf, " dev"); - break; - } - case meshtastic_Telemetry_environment_metrics_tag: { - strcat(buf, " env"); - break; - } - case meshtastic_Telemetry_air_quality_metrics_tag: { - strcat(buf, " air"); - break; - } - case meshtastic_Telemetry_power_metrics_tag: { - strcat(buf, " pow"); - break; - } - case meshtastic_Telemetry_local_stats_tag: { - strcat(buf, " dev"); // bug in firmware that this is local? - } - default: - break; - } - } - } else if (p.decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP) { - // print the recorded route and add from/to manually - strcat(buf, "\n"); - int pos = strlen(buf); - if (p.to == ownNode) { - pos += snprintf(&buf[pos], 16, "%04x", ownNode & 0xffff); - } +/** + * Translate language proto enum value to (alphabetical) position in dropdown menu + */ +uint32_t TFTView_320x240::language2val(meshtastic_Language lang) +{ + switch (lang) { + case meshtastic_Language_ENGLISH: + return 0; + case meshtastic_Language_FRENCH: + return 7; + case meshtastic_Language_GERMAN: + return 4; + case meshtastic_Language_ITALIAN: + return 8; + case meshtastic_Language_PORTUGUESE: + return 12; + case meshtastic_Language_SPANISH: + return 6; + case meshtastic_Language_SWEDISH: + return 17; + case meshtastic_Language_FINNISH: + return 16; + case meshtastic_Language_POLISH: + return 11; + case meshtastic_Language_TURKISH: + return 18; + case meshtastic_Language_SERBIAN: + return 15; + case meshtastic_Language_RUSSIAN: + return 13; + case meshtastic_Language_DUTCH: + return 9; + case meshtastic_Language_GREEK: + return 5; + case meshtastic_Language_NORWEGIAN: + return 10; + case meshtastic_Language_SLOVENIAN: + return 14; + case meshtastic_Language_UKRAINIAN: + return 19; + case meshtastic_Language_BULGARIAN: + return 1; + case meshtastic_Language_CZECH: + return 2; + case meshtastic_Language_DANISH: + return 3; + case meshtastic_Language_SIMPLIFIED_CHINESE: + return 20; + case meshtastic_Language_TRADITIONAL_CHINESE: + return 21; + default: + ILOG_WARN("unknown language uiconfig: %d", lang); + } + return 0; +} - meshtastic_RouteDiscovery route; - if (pb_decode_from_bytes(p.decoded.payload.bytes, p.decoded.payload.size, &meshtastic_RouteDiscovery_msg, - &route)) { - for (int i = 0; i < route.route_count; i++) { - uint32_t nodeNum = route.route[i]; - if (nodeNum != UINT32_MAX) { - pos += snprintf(&buf[pos], 16, "->%04x", nodeNum & 0xffff); - } else { - strcat(buf, "->unk"); - pos += 6; - } - } - if (p.to == ownNode) { - pos += snprintf(&buf[pos], 16, "->%04x", p.from & 0xffff); - } - } - } +/** + * Translate value from dropdown menu to language proto enum + */ +meshtastic_Language TFTView_320x240::val2language(uint32_t val) +{ + switch (val) { + case 0: + return meshtastic_Language_ENGLISH; + case 7: + return meshtastic_Language_FRENCH; + case 4: + return meshtastic_Language_GERMAN; + case 8: + return meshtastic_Language_ITALIAN; + case 12: + return meshtastic_Language_PORTUGUESE; + case 6: + return meshtastic_Language_SPANISH; + case 17: + return meshtastic_Language_SWEDISH; + case 16: + return meshtastic_Language_FINNISH; + case 11: + return meshtastic_Language_POLISH; + case 18: + return meshtastic_Language_TURKISH; + case 15: + return meshtastic_Language_SERBIAN; + case 13: + return meshtastic_Language_RUSSIAN; + case 9: + return meshtastic_Language_DUTCH; + case 5: + return meshtastic_Language_GREEK; + case 10: + return meshtastic_Language_NORWEGIAN; + case 14: + return meshtastic_Language_SLOVENIAN; + case 19: + return meshtastic_Language_UKRAINIAN; + case 1: + return meshtastic_Language_BULGARIAN; + case 2: + return meshtastic_Language_CZECH; + case 3: + return meshtastic_Language_DANISH; + case 20: + return meshtastic_Language_SIMPLIFIED_CHINESE; + case 21: + return meshtastic_Language_TRADITIONAL_CHINESE; + default: + ILOG_WARN("unknown language val: %d", val); + } + return meshtastic_Language_ENGLISH; +} - if (packetCounter >= PACKET_LOGS_MAX) { - // delete oldest entry - lv_obj_del(objects.tools_packet_log_panel->spec_attr->children[0]); - } else { - packetCounter++; - char top[24]; - sprintf(top, _("Packet Log: %d"), packetCounter); - lv_label_set_text(objects.top_packet_log_label, top); - } - lv_obj_t *pLabel = lv_label_create(objects.tools_packet_log_panel); - lv_obj_set_pos(pLabel, 0, 0); - lv_obj_set_size(pLabel, LV_PCT(100), LV_SIZE_CONTENT); - uint32_t bgColor, fgColor; - std::tie(bgColor, fgColor) = nodeColor(p.from); - lv_obj_set_style_bg_color(pLabel, lv_color_hex(bgColor), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_color(pLabel, lv_color_hex(fgColor), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(pLabel, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_label_set_text(pLabel, buf); - - // auto-scroll if last item is visible - if (lv_obj_get_scroll_bottom(objects.tools_packet_log_panel) < 20) - lv_obj_scroll_to_view(pLabel, LV_ANIM_OFF); - } - - void TFTView_320x240::updateStatistics(const meshtastic_MeshPacket &p) - { - struct Stats { - uint32_t id; - uint16_t row; - uint16_t tel; - uint16_t pos; - uint16_t inf; - uint16_t trc; - uint16_t txt; - uint16_t nbr; - uint32_t sum; - - bool operator==(const Stats &rhs) const { return id == rhs.id; } - - Stats &operator+=(const Stats &rhs) - { - this->tel += rhs.tel; - this->pos += rhs.pos; - this->inf += rhs.inf; - this->trc += rhs.trc; - this->txt += rhs.txt; - this->nbr += rhs.nbr; - this->sum += 1; - return *this; - } +/** + * @brief Set lv_i18n language + */ +void TFTView_320x240::setLocale(meshtastic_Language lang) +{ + const char *locale = "en_US.UTF-8"; + switch (lang) { + case meshtastic_Language_ENGLISH: + lv_i18n_set_locale("en"); + break; + case meshtastic_Language_BULGARIAN: + lv_i18n_set_locale("bg"); + locale = "bg_BG.UTF-8"; + break; + case meshtastic_Language_GERMAN: + lv_i18n_set_locale("de"); + locale = "de_DE.UTF-8"; + break; + case meshtastic_Language_SPANISH: + lv_i18n_set_locale("es"); + locale = "es_ES.UTF-8"; + break; + case meshtastic_Language_FRENCH: + lv_i18n_set_locale("fr"); + locale = "fr_FR.UTF-8"; + break; + case meshtastic_Language_ITALIAN: + lv_i18n_set_locale("it"); + locale = "it_IT.UTF-8"; + break; + case meshtastic_Language_PORTUGUESE: + lv_i18n_set_locale("pt"); + locale = "pt_PT.UTF-8"; + break; + case meshtastic_Language_SWEDISH: + lv_i18n_set_locale("se"); + locale = "sv_SE.UTF-8"; + break; + case meshtastic_Language_FINNISH: + lv_i18n_set_locale("fi"); + locale = "fi_FI.UTF-8"; + break; + case meshtastic_Language_POLISH: + lv_i18n_set_locale("pl"); + locale = "pl_PL.UTF-8"; + break; + case meshtastic_Language_TURKISH: + lv_i18n_set_locale("tr"); + locale = "tr_TR.UTF-8"; + break; + case meshtastic_Language_SERBIAN: + lv_i18n_set_locale("sr"); + locale = "sr_RS.UTF-8"; + break; + case meshtastic_Language_DUTCH: + lv_i18n_set_locale("nl"); + locale = "nl_NL.UTF-8"; + break; + case meshtastic_Language_RUSSIAN: + lv_i18n_set_locale("ru"); + locale = "ru_RU.UTF-8"; + break; + case meshtastic_Language_GREEK: + lv_i18n_set_locale("el"); + locale = "el_GR.UTF-8"; + break; + case meshtastic_Language_NORWEGIAN: + lv_i18n_set_locale("no"); + locale = "no_NO.UTF-8"; + break; + case meshtastic_Language_SLOVENIAN: + lv_i18n_set_locale("sl"); + locale = "sl_SI.UTF-8"; + break; + case meshtastic_Language_UKRAINIAN: + lv_i18n_set_locale("uk"); + locale = "uk_UA.UTF-8"; + break; + case meshtastic_Language_CZECH: + lv_i18n_set_locale("cs"); + locale = "cs_CZ.UTF-8"; + break; + case meshtastic_Language_DANISH: + lv_i18n_set_locale("da"); + locale = "da_DK.UTF-8"; + break; + case meshtastic_Language_SIMPLIFIED_CHINESE: + lv_i18n_set_locale("cn"); + locale = "zh_CN.UTF-8"; + break; + case meshtastic_Language_TRADITIONAL_CHINESE: + lv_i18n_set_locale("tw"); + locale = "zh_TW.UTF-8"; + break; + default: + ILOG_WARN("Language %d not implemented", lang); + break; + } - bool operator<(const Stats &rhs) const - { - return sum > rhs.sum; // sort reverse but skip equal values - } - }; - static std::list stats; - - if (p.from == 0) { - // clear table - stats.clear(); - for (int i = 1; i < statisticTableRows; i++) { - for (int j = 0; j < 7; j++) { - lv_table_set_cell_value(objects.statistics_table, i, j, ""); - } - } - return; - } +#if defined(LOCALE_SUPPORT) + std::locale::global(std::locale(locale)); +#else + (void)locale; +#endif +} - // update statistic for node - Stats stat = {p.from}; - switch (p.decoded.portnum) { - case meshtastic_PortNum_TELEMETRY_APP: { - meshtastic_Telemetry telemetry; - if (pb_decode_from_bytes(p.decoded.payload.bytes, p.decoded.payload.size, &meshtastic_Telemetry_msg, - &telemetry)) { - if (telemetry.which_variant == meshtastic_Telemetry_device_metrics_tag) { - if (p.from == ownNode) - return; // suppress (internal) battery level packets - } - } - stat.tel++; - break; - } - case meshtastic_PortNum_POSITION_APP: { - stat.pos++; - break; - } - case meshtastic_PortNum_NODEINFO_APP: { - stat.inf++; - break; - } - case meshtastic_PortNum_ROUTING_APP: - case meshtastic_PortNum_TRACEROUTE_APP: { - stat.trc++; - break; - } - case meshtastic_PortNum_TEXT_MESSAGE_APP: - case meshtastic_PortNum_RANGE_TEST_APP: { - stat.txt++; - break; - } - case meshtastic_PortNum_NEIGHBORINFO_APP: { - stat.nbr++; - break; - } - case meshtastic_PortNum_ADMIN_APP: { - // ignore - break; - } - default: - ILOG_WARN("packet portnum in stats unhandled: %d", p.decoded.portnum); - stat.sum++; - return; - } +/** + * @brief Set language (using dropdown strings) + */ +void TFTView_320x240::setLanguage(meshtastic_Language lang) +{ + char buf1[20], buf2[40]; + lv_dropdown_set_selected(objects.settings_language_dropdown, language2val(lang)); + lv_dropdown_get_selected_str(objects.settings_language_dropdown, buf1, sizeof(buf1)); + lv_snprintf(buf2, sizeof(buf2), _("Language: %s"), buf1); + lv_label_set_text(objects.basic_settings_language_label, buf2); +} - std::list::iterator it = std::find(stats.begin(), stats.end(), stat); - if (it == stats.end()) { - stat.row = stats.size(); - stat.sum = 1; - // TODO: stop if memory limit is reached - stats.push_back(stat); - } else { - *it += stat; - } +/** + * @brief Set timeout + */ +void TFTView_320x240::setTimeout(uint32_t timeout) +{ + char buf[32]; + if (timeout == 0) + lv_snprintf(buf, sizeof(buf), _("Screen Timeout: off")); + else + lv_snprintf(buf, sizeof(buf), _("Screen Timeout: %ds"), timeout); + lv_label_set_text(objects.basic_settings_timeout_label, buf); + THIS->displaydriver->setScreenTimeout(timeout); +} - stats.sort(); +/** + * @brief Set brightness + */ +void TFTView_320x240::setBrightness(uint32_t brightness) +{ + char buf[32]; + lv_snprintf(buf, sizeof(buf), _("Screen Brightness: %d%%"), uint16_t(round((brightness * 100) / 255.0))); + lv_label_set_text(objects.basic_settings_brightness_label, buf); + THIS->displaydriver->setBrightness((uint8_t)brightness); +} - // fill packet statistics table - char buf[10]; - int row = 1; - bool move = false; +/** + * @brief Set theme to new value + */ +void TFTView_320x240::setTheme(uint32_t value) +{ + char buf1[30], buf2[30]; + lv_dropdown_set_selected(objects.settings_theme_dropdown, value); + lv_dropdown_get_selected_str(objects.settings_theme_dropdown, buf1, sizeof(buf1)); + lv_snprintf(buf2, sizeof(buf2), _("Theme: %s"), buf1); + lv_label_set_text(objects.basic_settings_theme_label, buf2); + + // change theme and redraw UI + Themes::set(Themes::Theme(value)); + updateTheme(); +} - for (auto it2 : stats) { - if (it2.id == p.from || move) { - buf[0] = '\0'; - auto it = nodes.find(it2.id); // node may have been removed from nodes, so check if still there - if (it != nodes.end() && it->second) { - char *userData = (char *)&(it->second->LV_OBJ_IDX(node_lbs_idx)->user_data); - if (userData) { - buf[0] = userData[0]; - buf[1] = userData[1]; - buf[2] = userData[2]; - buf[3] = userData[3]; - buf[4] = '\0'; - } - } +/** + * @brief Save all data from node options panel + */ +void TFTView_320x240::storeNodeOptions(void) +{ + // store node filter options + meshtastic_NodeFilter &filter = db.uiConfig.node_filter; + db.uiConfig.has_node_filter = true; + filter.unknown_switch = lv_obj_has_state(objects.nodes_filter_unknown_switch, LV_STATE_CHECKED); + filter.offline_switch = lv_obj_has_state(objects.nodes_filter_offline_switch, LV_STATE_CHECKED); + filter.public_key_switch = lv_obj_has_state(objects.nodes_filter_public_key_switch, LV_STATE_CHECKED); + // filter.channel = lv_dropdown_get_selected(objects.nodes_filter_channel_dropdown); + filter.hops_away = lv_dropdown_get_selected(objects.nodes_filter_hops_dropdown); + // filter.mqtt_switch = lv_obj_has_state(objects.nodes_filter_mqtt_switch, LV_STATE_CHECKED); + filter.position_switch = lv_obj_has_state(objects.nodes_filter_position_switch, LV_STATE_CHECKED); + strncpy(filter.node_name, lv_textarea_get_text(objects.nodes_filter_name_area), sizeof(filter.node_name)); + + // store node highlight options + meshtastic_NodeHighlight &highlight = db.uiConfig.node_highlight; + db.uiConfig.has_node_highlight = true; + highlight.chat_switch = lv_obj_has_state(objects.nodes_hl_active_chat_switch, LV_STATE_CHECKED); + highlight.position_switch = lv_obj_has_state(objects.nodes_hl_position_switch, LV_STATE_CHECKED); + highlight.telemetry_switch = lv_obj_has_state(objects.nodes_hl_telemetry_switch, LV_STATE_CHECKED); + highlight.iaq_switch = lv_obj_has_state(objects.nodes_hliaq_switch, LV_STATE_CHECKED); + strncpy(highlight.node_name, lv_textarea_get_text(objects.nodes_hl_name_area), sizeof(highlight.node_name)); + + controller->storeUIConfig(db.uiConfig); +} - lv_table_set_cell_value(objects.statistics_table, row, 0, buf); - sprintf(buf, "%d", it2.tel); - lv_table_set_cell_value(objects.statistics_table, row, 1, buf); - sprintf(buf, "%d", it2.pos); - lv_table_set_cell_value(objects.statistics_table, row, 2, buf); - sprintf(buf, "%d", it2.inf); - lv_table_set_cell_value(objects.statistics_table, row, 3, buf); - sprintf(buf, "%d", it2.trc); - lv_table_set_cell_value(objects.statistics_table, row, 4, buf); - sprintf(buf, "%d", it2.nbr); - lv_table_set_cell_value(objects.statistics_table, row, 5, buf); - sprintf(buf, "%d", it2.sum); - lv_table_set_cell_value(objects.statistics_table, row, 6, buf); - - if (row != it2.row) { - it2.row = row; - move = true; - } else { - break; - } - } - row++; - if (row >= statisticTableRows) // fill rows till bottom of 320x240 display - break; - } +/** + * @brief erase chat and all its resources + */ +void TFTView_320x240::eraseChat(uint32_t channelOrNode) +{ + if (chats.find(channelOrNode) == chats.end()) { + ILOG_WARN("eraseChat: channelOrNode %d not found", channelOrNode); + return; + } + if (channelOrNode < c_max_channels) { + uint8_t ch = (uint8_t)channelOrNode; + if (state == MeshtasticView::eRunning) { + lv_obj_delete_delayed(chats.at(ch), 500); + } else { + lv_obj_del(chats.at(ch)); + } + lv_obj_del(channelGroup.at(ch)); + channelGroup[ch] = nullptr; + chats.erase(ch); + } else { + uint32_t nodeNum = channelOrNode; + if (state == MeshtasticView::eRunning) { + lv_obj_delete_delayed(chats.at(nodeNum), 500); + } else { + lv_obj_delete(chats.at(nodeNum)); } + lv_obj_del(messages.at(nodeNum)); + messages.erase(nodeNum); + chats.erase(nodeNum); + } +} - void TFTView_320x240::ui_event_statistics_table(lv_event_t * e) - { - lv_draw_task_t *draw_task = lv_event_get_draw_task(e); - lv_draw_dsc_base_t *base_dsc = (lv_draw_dsc_base_t *)lv_draw_task_get_draw_dsc(draw_task); - // if the cells are drawn... - if (base_dsc->part == LV_PART_ITEMS) { - // make the texts in the first cell blueish - lv_draw_fill_dsc_t *fill_draw_dsc = lv_draw_task_get_fill_dsc(draw_task); - if (fill_draw_dsc) { - uint32_t row = base_dsc->id1; - if (row == 0) { - fill_draw_dsc->color = lv_color_mix(lv_palette_main(LV_PALETTE_BLUE), fill_draw_dsc->color, LV_OPA_20); - } - // make every 2nd row grayish - else { - Themes::recolorTableRow(fill_draw_dsc, row % 2 == 0); - } - } - } +/** + * @brief clears all (persistent) chat messages + */ +void TFTView_320x240::clearChatHistory(void) +{ + for (auto &it : chats) { + lv_obj_delete(it.second); + if (it.first < c_max_channels) { + lv_obj_delete(channelGroup[it.first]); + channelGroup[it.first] = nullptr; + } else { + lv_obj_delete(messages[it.first]); } + } + chats.clear(); + messages.clear(); + updateActiveChats(); + updateNodesFiltered(true); + controller->removeTextMessages(0, 0, 0); +} - void TFTView_320x240::requestSetup(void) - { - ui_set_active(objects.settings_button, objects.initial_setup_panel, objects.top_setup_panel); - lv_dropdown_set_selected(objects.setup_region_dropdown, 0); - lv_obj_clear_flag(objects.initial_setup_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.setup_region_dropdown); - THIS->disablePanel(objects.controller_panel); - THIS->activeSettings = eSetup; - } - - /** - * update signal strength text and image for home screen - */ - void TFTView_320x240::updateSignalStrength(int32_t rssi, float snr) - { - // remember time we last heard a node - time(&lastHeard); - - if (rssi != 0 || snr != 0.0) { - char buf[40]; - sprintf(buf, "SNR: %.1f\nRSSI: %" PRId32, snr, rssi); - lv_label_set_text(objects.home_signal_label, buf); - - uint32_t pct = signalStrength2Percent(rssi, snr); - sprintf(buf, "(%d%%)", pct); - lv_label_set_text(objects.home_signal_pct_label, buf); - if (pct > 80) { - lv_obj_set_style_bg_image_src(objects.home_signal_button, &img_home_signal_button_image, - LV_PART_MAIN | LV_STATE_DEFAULT); - } else if (pct > 60) { - lv_obj_set_style_bg_image_src(objects.home_signal_button, &img_home_strong_signal_image, - LV_PART_MAIN | LV_STATE_DEFAULT); - } else if (pct > 40) { - lv_obj_set_style_bg_image_src(objects.home_signal_button, &img_home_good_signal_image, - LV_PART_MAIN | LV_STATE_DEFAULT); - } else if (pct > 20) { - lv_obj_set_style_bg_image_src(objects.home_signal_button, &img_home_fair_signal_image, - LV_PART_MAIN | LV_STATE_DEFAULT); - } else if (pct > 1) { - lv_obj_set_style_bg_image_src(objects.home_signal_button, &img_home_weak_signal_image, - LV_PART_MAIN | LV_STATE_DEFAULT); - } else { - lv_obj_set_style_bg_image_src(objects.home_signal_button, &img_home_no_signal_image, - LV_PART_MAIN | LV_STATE_DEFAULT); - } +/** + * @brief User widget OK button handling + * + * @param e + */ +void TFTView_320x240::ui_event_ok(lv_event_t *e) +{ + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED) { + switch (THIS->activeSettings) { + case eSetup: { + meshtastic_Config_LoRaConfig_RegionCode region = + (meshtastic_Config_LoRaConfig_RegionCode)(lv_dropdown_get_selected(objects.setup_region_dropdown) + 1); + + uint32_t numChannels = LoRaPresets::getNumChannels(region, THIS->db.config.lora.modem_preset); + // if (numChannels == 0) { + // // region not possible for selected preset, revert + // lv_dropdown_set_selected(objects.settings_region_dropdown, THIS->db.config.lora.region - 1); + // return; + // } + + if (region != THIS->db.config.lora.region) { + char buf1[10], buf2[30]; + lv_dropdown_get_selected_str(objects.setup_region_dropdown, buf1, sizeof(buf1)); + lv_snprintf(buf2, sizeof(buf2), _("Region: %s"), buf1); + lv_label_set_text(objects.basic_settings_region_label, buf2); + + meshtastic_Config_LoRaConfig &lora = THIS->db.config.lora; + uint32_t defaultSlot = lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET ? lora.channel_num : 0; + if (defaultSlot == 0) { + defaultSlot = + LoRaPresets::getDefaultSlot(region, THIS->db.config.lora.modem_preset, THIS->db.channel[0].settings.name); + } + lora.region = region; + lora.channel_num = (defaultSlot <= numChannels ? defaultSlot : 1); + THIS->controller->sendConfig(meshtastic_Config_LoRaConfig{lora}, THIS->ownNode); + } + + char buf[30]; + const char *userShort = lv_textarea_get_text(objects.setup_user_short_textarea); + const char *userLong = lv_textarea_get_text(objects.setup_user_long_textarea); + if (strcmp(userShort, THIS->db.short_name) || strcmp(userLong, THIS->db.long_name)) { + lv_snprintf(buf, sizeof(buf), _("User name: %s"), userShort); + lv_label_set_text(objects.basic_settings_user_label, buf); + lv_label_set_text(objects.user_name_short_label, userShort); + lv_label_set_text(objects.user_name_label, userLong); + strcpy(THIS->db.short_name, userShort); + strcpy(THIS->db.long_name, userLong); + meshtastic_User user{}; // TODO: don't overwrite is_licensed + strcpy(user.short_name, userShort); + strcpy(user.long_name, userLong); + THIS->controller->sendConfig(user, THIS->ownNode); } - } - - /** - * Translate proto modem preset enum value to numerical position in dropdown menu - */ - uint32_t TFTView_320x240::preset2val(meshtastic_Config_LoRaConfig_ModemPreset preset) - { - int32_t val[] = {0, -1, -1, 4, 3, 7, 5, 1, 6, 2}; + THIS->notifyReboot(true); - if (preset > (sizeof(val) / sizeof(val[0]) - 1) || val[preset] == -1) { - ILOG_WARN("unknown or deprecated preset value: %d", preset); - return 0; - } - return uint32_t(val[preset]); + lv_obj_add_flag(objects.initial_setup_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.home_button); + break; } - - /** - * Translate value from dropdown menu to modem preset proto enum - */ - meshtastic_Config_LoRaConfig_ModemPreset TFTView_320x240::val2preset(uint32_t val) - { - meshtastic_Config_LoRaConfig_ModemPreset preset[] = { - meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE, - meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, - meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST, - meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW}; - if (val > (sizeof(preset) / sizeof(preset[0]) - 1)) { - ILOG_ERROR("unknown preset value: %d", val); - return meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; - } - return preset[val]; + case eUsername: { + char buf[30]; + const char *userShort = lv_textarea_get_text(objects.settings_user_short_textarea); + const char *userLong = lv_textarea_get_text(objects.settings_user_long_textarea); + if (strcmp(userShort, THIS->db.short_name) || strcmp(userLong, THIS->db.long_name)) { + lv_snprintf(buf, sizeof(buf), _("User name: %s"), userShort); + lv_label_set_text(objects.basic_settings_user_label, buf); + lv_label_set_text(objects.user_name_short_label, userShort); + lv_label_set_text(objects.user_name_label, userLong); + strcpy(THIS->db.short_name, userShort); + strcpy(THIS->db.long_name, userLong); + meshtastic_User user{}; // TODO: don't overwrite is_licensed + strcpy(user.short_name, userShort); + strcpy(user.long_name, userLong); + THIS->controller->sendConfig(user, THIS->ownNode); + THIS->notifyReboot(true); + } + lv_obj_add_flag(objects.settings_username_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_user_button); + break; } + case eDeviceRole: { + meshtastic_Config_DeviceConfig &device = THIS->db.config.device; + meshtastic_Config_DeviceConfig_Role role = + THIS->val2role(lv_dropdown_get_selected(objects.settings_device_role_dropdown)); - /** - * Translate proto role enum value to numerical position in dropdown menu - */ - uint32_t TFTView_320x240::role2val(meshtastic_Config_DeviceConfig_Role role) - { -#ifdef USE_ROUTER_ROLE - int32_t val[] = {0, 1, 2, -1, 3, 4, 5, 6, 7, 8, 9}; -#else - int32_t val[] = {0, 1, -1, -1, -1, 2, 3, 4, 5, 6, 7}; -#endif - if (role > 10 || val[role] == -1) { - ILOG_WARN("unknown role value: %d", role); - return 0; + if (role != device.role) { + char buf1[30], buf2[40]; + lv_dropdown_get_selected_str(objects.settings_device_role_dropdown, buf1, sizeof(buf1)); + lv_snprintf(buf2, sizeof(buf2), _("Device Role: %s"), buf1); + lv_label_set_text(objects.basic_settings_role_label, buf2); + + device.role = role; + THIS->controller->sendConfig(meshtastic_Config_DeviceConfig{device}, THIS->ownNode); + THIS->notifyReboot(true); } - return uint32_t(val[role]); + lv_obj_add_flag(objects.settings_device_role_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_role_button); + break; } + case eRegion: { + meshtastic_Config_LoRaConfig_RegionCode region = + (meshtastic_Config_LoRaConfig_RegionCode)(lv_dropdown_get_selected(objects.settings_region_dropdown) + 1); - /** - * Translate value from dropdown menu to role proto enum - */ - meshtastic_Config_DeviceConfig_Role TFTView_320x240::val2role(uint32_t val) - { - meshtastic_Config_DeviceConfig_Role role[] = {meshtastic_Config_DeviceConfig_Role_CLIENT, - meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE, -#ifdef USE_ROUTER_ROLE - meshtastic_Config_DeviceConfig_Role_ROUTER, - meshtastic_Config_DeviceConfig_Role_REPEATER, -#endif - meshtastic_Config_DeviceConfig_Role_TRACKER, - meshtastic_Config_DeviceConfig_Role_SENSOR, - meshtastic_Config_DeviceConfig_Role_TAK, - meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN, - meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND, - meshtastic_Config_DeviceConfig_Role_TAK_TRACKER, - meshtastic_Config_DeviceConfig_Role_ROUTER_LATE}; - if (val > 10) { - ILOG_WARN("unknown role value: %d", val); - return meshtastic_Config_DeviceConfig_Role_CLIENT; + uint32_t numChannels = LoRaPresets::getNumChannels(region, THIS->db.config.lora.modem_preset); + if (numChannels == 0) { + // region not possible for selected preset, revert + lv_dropdown_set_selected(objects.settings_region_dropdown, THIS->db.config.lora.region - 1); + return; } - return role[val]; - } - /** - * Translate language proto enum value to (alphabetical) position in dropdown menu - */ - uint32_t TFTView_320x240::language2val(meshtastic_Language lang) - { - switch (lang) { - case meshtastic_Language_ENGLISH: - return 0; - case meshtastic_Language_FRENCH: - return 7; - case meshtastic_Language_GERMAN: - return 4; - case meshtastic_Language_ITALIAN: - return 8; - case meshtastic_Language_PORTUGUESE: - return 12; - case meshtastic_Language_SPANISH: - return 6; - case meshtastic_Language_SWEDISH: - return 17; - case meshtastic_Language_FINNISH: - return 16; - case meshtastic_Language_POLISH: - return 11; - case meshtastic_Language_TURKISH: - return 18; - case meshtastic_Language_SERBIAN: - return 15; - case meshtastic_Language_RUSSIAN: - return 13; - case meshtastic_Language_DUTCH: - return 9; - case meshtastic_Language_GREEK: - return 5; - case meshtastic_Language_NORWEGIAN: - return 10; - case meshtastic_Language_SLOVENIAN: - return 14; - case meshtastic_Language_UKRAINIAN: - return 19; - case meshtastic_Language_BULGARIAN: - return 1; - case meshtastic_Language_CZECH: - return 2; - case meshtastic_Language_DANISH: - return 3; - case meshtastic_Language_SIMPLIFIED_CHINESE: - return 20; - case meshtastic_Language_TRADITIONAL_CHINESE: - return 21; - default: - ILOG_WARN("unknown language uiconfig: %d", lang); + if (region != THIS->db.config.lora.region) { + char buf1[10], buf2[30]; + lv_dropdown_get_selected_str(objects.settings_region_dropdown, buf1, sizeof(buf1)); + lv_snprintf(buf2, sizeof(buf2), _("Region: %s"), buf1); + lv_label_set_text(objects.basic_settings_region_label, buf2); + + meshtastic_Config_LoRaConfig &lora = THIS->db.config.lora; + uint32_t defaultSlot = lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET ? lora.channel_num : 0; + if (defaultSlot == 0) { + defaultSlot = + LoRaPresets::getDefaultSlot(region, THIS->db.config.lora.modem_preset, THIS->db.channel[0].settings.name); + } + lora.region = region; + lora.channel_num = (defaultSlot <= numChannels ? defaultSlot : 1); + THIS->controller->sendConfig(meshtastic_Config_LoRaConfig{lora}, THIS->ownNode); + THIS->notifyReboot(true); } - return 0; + lv_obj_add_flag(objects.settings_region_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_region_button); + break; } + case eModemPreset: { + meshtastic_Config_LoRaConfig &lora = THIS->db.config.lora; + meshtastic_Config_LoRaConfig_ModemPreset preset = + THIS->val2preset(lv_dropdown_get_selected(objects.settings_modem_preset_dropdown)); + uint16_t channelNum = lv_slider_get_value(objects.frequency_slot_slider); + if (preset != lora.modem_preset || lora.channel_num != channelNum) { + char buf1[16], buf2[32]; + lv_dropdown_get_selected_str(objects.settings_modem_preset_dropdown, buf1, sizeof(buf1)); + lv_snprintf(buf2, sizeof(buf2), _("Modem Preset: %s"), buf1); + lv_label_set_text(objects.basic_settings_modem_preset_label, buf2); - /** - * Translate value from dropdown menu to language proto enum - */ - meshtastic_Language TFTView_320x240::val2language(uint32_t val) - { - switch (val) { - case 0: - return meshtastic_Language_ENGLISH; - case 7: - return meshtastic_Language_FRENCH; - case 4: - return meshtastic_Language_GERMAN; - case 8: - return meshtastic_Language_ITALIAN; - case 12: - return meshtastic_Language_PORTUGUESE; - case 6: - return meshtastic_Language_SPANISH; - case 17: - return meshtastic_Language_SWEDISH; - case 16: - return meshtastic_Language_FINNISH; - case 11: - return meshtastic_Language_POLISH; - case 18: - return meshtastic_Language_TURKISH; - case 15: - return meshtastic_Language_SERBIAN; - case 13: - return meshtastic_Language_RUSSIAN; - case 9: - return meshtastic_Language_DUTCH; - case 5: - return meshtastic_Language_GREEK; - case 10: - return meshtastic_Language_NORWEGIAN; - case 14: - return meshtastic_Language_SLOVENIAN; - case 19: - return meshtastic_Language_UKRAINIAN; - case 1: - return meshtastic_Language_BULGARIAN; - case 2: - return meshtastic_Language_CZECH; - case 3: - return meshtastic_Language_DANISH; - case 20: - return meshtastic_Language_SIMPLIFIED_CHINESE; - case 21: - return meshtastic_Language_TRADITIONAL_CHINESE; - default: - ILOG_WARN("unknown language val: %d", val); + lora.use_preset = true; + lora.modem_preset = preset; + lora.channel_num = channelNum; + THIS->controller->sendConfig(meshtastic_Config_LoRaConfig{lora}, THIS->ownNode); + THIS->notifyReboot(true); } - return meshtastic_Language_ENGLISH; + lv_obj_add_flag(objects.settings_modem_preset_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_modem_preset_button); + break; } - - /** - * @brief Set lv_i18n language - */ - void TFTView_320x240::setLocale(meshtastic_Language lang) - { - const char *locale = "en_US.UTF-8"; - switch (lang) { - case meshtastic_Language_ENGLISH: - lv_i18n_set_locale("en"); - break; - case meshtastic_Language_BULGARIAN: - lv_i18n_set_locale("bg"); - locale = "bg_BG.UTF-8"; - break; - case meshtastic_Language_GERMAN: - lv_i18n_set_locale("de"); - locale = "de_DE.UTF-8"; - break; - case meshtastic_Language_SPANISH: - lv_i18n_set_locale("es"); - locale = "es_ES.UTF-8"; - break; - case meshtastic_Language_FRENCH: - lv_i18n_set_locale("fr"); - locale = "fr_FR.UTF-8"; - break; - case meshtastic_Language_ITALIAN: - lv_i18n_set_locale("it"); - locale = "it_IT.UTF-8"; - break; - case meshtastic_Language_PORTUGUESE: - lv_i18n_set_locale("pt"); - locale = "pt_PT.UTF-8"; - break; - case meshtastic_Language_SWEDISH: - lv_i18n_set_locale("se"); - locale = "sv_SE.UTF-8"; - break; - case meshtastic_Language_FINNISH: - lv_i18n_set_locale("fi"); - locale = "fi_FI.UTF-8"; - break; - case meshtastic_Language_POLISH: - lv_i18n_set_locale("pl"); - locale = "pl_PL.UTF-8"; - break; - case meshtastic_Language_TURKISH: - lv_i18n_set_locale("tr"); - locale = "tr_TR.UTF-8"; - break; - case meshtastic_Language_SERBIAN: - lv_i18n_set_locale("sr"); - locale = "sr_RS.UTF-8"; - break; - case meshtastic_Language_DUTCH: - lv_i18n_set_locale("nl"); - locale = "nl_NL.UTF-8"; - break; - case meshtastic_Language_RUSSIAN: - lv_i18n_set_locale("ru"); - locale = "ru_RU.UTF-8"; - break; - case meshtastic_Language_GREEK: - lv_i18n_set_locale("el"); - locale = "el_GR.UTF-8"; - break; - case meshtastic_Language_NORWEGIAN: - lv_i18n_set_locale("no"); - locale = "no_NO.UTF-8"; - break; - case meshtastic_Language_SLOVENIAN: - lv_i18n_set_locale("sl"); - locale = "sl_SI.UTF-8"; - break; - case meshtastic_Language_UKRAINIAN: - lv_i18n_set_locale("uk"); - locale = "uk_UA.UTF-8"; - break; - case meshtastic_Language_CZECH: - lv_i18n_set_locale("cs"); - locale = "cs_CZ.UTF-8"; - break; - case meshtastic_Language_DANISH: - lv_i18n_set_locale("da"); - locale = "da_DK.UTF-8"; - break; - case meshtastic_Language_SIMPLIFIED_CHINESE: - lv_i18n_set_locale("cn"); - locale = "zh_CN.UTF-8"; - break; - case meshtastic_Language_TRADITIONAL_CHINESE: - lv_i18n_set_locale("tw"); - locale = "zh_TW.UTF-8"; - break; - default: - ILOG_WARN("Language %d not implemented", lang); - break; + case eChannel: { + for (int i = 0; i < c_max_channels; i++) { + // check if channel changed, then update and send to radio + if (memcmp(&THIS->db.channel[i], &THIS->channel_scratch[i], sizeof(THIS->channel_scratch[i])) != 0) { + THIS->channel_scratch[i].has_settings = true; + THIS->updateChannelConfig(THIS->channel_scratch[i]); + THIS->controller->sendConfig(THIS->channel_scratch[i], THIS->ownNode); + } } -#if defined(LOCALE_SUPPORT) - std::locale::global(std::locale(locale)); -#else - (void)locale; -#endif + int8_t ch = (signed long)THIS->ch_label[0]->user_data; + THIS->setChannelName(THIS->db.channel[ch]); + lv_obj_clear_state(objects.settings_channel_panel, LV_STATE_DISABLED); + lv_obj_add_flag(objects.settings_channel_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_channel_button); + delete[] THIS->channel_scratch; + break; } - - /** - * @brief Set language (using dropdown strings) - */ - void TFTView_320x240::setLanguage(meshtastic_Language lang) - { - char buf1[20], buf2[40]; - lv_dropdown_set_selected(objects.settings_language_dropdown, language2val(lang)); - lv_dropdown_get_selected_str(objects.settings_language_dropdown, buf1, sizeof(buf1)); - lv_snprintf(buf2, sizeof(buf2), _("Language: %s"), buf1); - lv_label_set_text(objects.basic_settings_language_label, buf2); + case eWifi: { + char buf[30]; + const char *ssid = lv_textarea_get_text(objects.settings_wifi_ssid_textarea); + const char *psk = lv_textarea_get_text(objects.settings_wifi_password_textarea); + if (strlen(ssid) == 0 || strlen(psk) == 0) + return; + lv_snprintf(buf, sizeof(buf), _("WiFi: %s"), ssid[0] ? ssid : _("")); + lv_label_set_text(objects.basic_settings_wifi_label, buf); + if (strcmp(THIS->db.config.network.wifi_ssid, ssid) != 0 || strcmp(THIS->db.config.network.wifi_psk, psk) != 0) { + strcpy(THIS->db.config.network.wifi_ssid, ssid); + strcpy(THIS->db.config.network.wifi_psk, psk); + THIS->db.config.network.wifi_enabled = true; + THIS->controller->sendConfig(meshtastic_Config_NetworkConfig{THIS->db.config.network}, THIS->ownNode); + THIS->notifyReboot(true); + } + THIS->enablePanel(objects.home_panel); + lv_obj_add_flag(objects.settings_wifi_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_wifi_button); + break; } + case eLanguage: { + uint32_t value = lv_dropdown_get_selected(objects.settings_language_dropdown); + meshtastic_Language lang = THIS->val2language(value); + if (lang != THIS->db.uiConfig.language) { + THIS->db.uiConfig.language = lang; + THIS->controller->storeUIConfig(THIS->db.uiConfig); + THIS->controller->requestReboot(3, THIS->ownNode); + THIS->notifyReboot(true); + } - /** - * @brief Set timeout - */ - void TFTView_320x240::setTimeout(uint32_t timeout) - { - char buf[32]; - if (timeout == 0) - lv_snprintf(buf, sizeof(buf), _("Screen Timeout: off")); - else - lv_snprintf(buf, sizeof(buf), _("Screen Timeout: %ds"), timeout); - lv_label_set_text(objects.basic_settings_timeout_label, buf); - THIS->displaydriver->setScreenTimeout(timeout); - } - - /** - * @brief Set brightness - */ - void TFTView_320x240::setBrightness(uint32_t brightness) - { - char buf[32]; - lv_snprintf(buf, sizeof(buf), _("Screen Brightness: %d%%"), uint16_t(round((brightness * 100) / 255.0))); - lv_label_set_text(objects.basic_settings_brightness_label, buf); - THIS->displaydriver->setBrightness((uint8_t)brightness); + lv_obj_add_flag(objects.settings_language_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_language_button); + break; } - - /** - * @brief Set theme to new value - */ - void TFTView_320x240::setTheme(uint32_t value) - { - char buf1[30], buf2[30]; - lv_dropdown_set_selected(objects.settings_theme_dropdown, value); - lv_dropdown_get_selected_str(objects.settings_theme_dropdown, buf1, sizeof(buf1)); - lv_snprintf(buf2, sizeof(buf2), _("Theme: %s"), buf1); - lv_label_set_text(objects.basic_settings_theme_label, buf2); - - // change theme and redraw UI - Themes::set(Themes::Theme(value)); - updateTheme(); + case eScreenTimeout: { + uint32_t value = lv_slider_get_value(objects.screen_timeout_slider); + if (value > 5) + value -= value % 5; + if (value != THIS->db.uiConfig.screen_timeout) { + THIS->setTimeout(value); + THIS->db.uiConfig.screen_timeout = value; + THIS->controller->storeUIConfig(THIS->db.uiConfig); + } + lv_obj_add_flag(objects.settings_screen_timeout_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_timeout_button); + break; } + case eScreenLock: { + const char *pin = lv_textarea_get_text(objects.settings_screen_lock_password_textarea); + bool screenLock = lv_obj_has_state(objects.settings_screen_lock_switch, LV_STATE_CHECKED); + bool settingsLock = lv_obj_has_state(objects.settings_settings_lock_switch, LV_STATE_CHECKED); + if ((screenLock || settingsLock) && (atol(pin) == 0 || strlen(pin) != 6)) + return; // require pin != "000000" + if ((screenLock != THIS->db.uiConfig.screen_lock) || settingsLock != THIS->db.uiConfig.settings_lock || + atol(pin) != THIS->db.uiConfig.pin_code) { + THIS->db.uiConfig.screen_lock = screenLock; + THIS->db.uiConfig.settings_lock = settingsLock; + THIS->db.uiConfig.pin_code = atol(pin); + THIS->controller->storeUIConfig(THIS->db.uiConfig); + } - /** - * @brief Save all data from node options panel - */ - void TFTView_320x240::storeNodeOptions(void) - { - // store node filter options - meshtastic_NodeFilter &filter = db.uiConfig.node_filter; - db.uiConfig.has_node_filter = true; - filter.unknown_switch = lv_obj_has_state(objects.nodes_filter_unknown_switch, LV_STATE_CHECKED); - filter.offline_switch = lv_obj_has_state(objects.nodes_filter_offline_switch, LV_STATE_CHECKED); - filter.public_key_switch = lv_obj_has_state(objects.nodes_filter_public_key_switch, LV_STATE_CHECKED); - // filter.channel = lv_dropdown_get_selected(objects.nodes_filter_channel_dropdown); - filter.hops_away = lv_dropdown_get_selected(objects.nodes_filter_hops_dropdown); - // filter.mqtt_switch = lv_obj_has_state(objects.nodes_filter_mqtt_switch, LV_STATE_CHECKED); - filter.position_switch = lv_obj_has_state(objects.nodes_filter_position_switch, LV_STATE_CHECKED); - strncpy(filter.node_name, lv_textarea_get_text(objects.nodes_filter_name_area), sizeof(filter.node_name)); - - // store node highlight options - meshtastic_NodeHighlight &highlight = db.uiConfig.node_highlight; - db.uiConfig.has_node_highlight = true; - highlight.chat_switch = lv_obj_has_state(objects.nodes_hl_active_chat_switch, LV_STATE_CHECKED); - highlight.position_switch = lv_obj_has_state(objects.nodes_hl_position_switch, LV_STATE_CHECKED); - highlight.telemetry_switch = lv_obj_has_state(objects.nodes_hl_telemetry_switch, LV_STATE_CHECKED); - highlight.iaq_switch = lv_obj_has_state(objects.nodes_hliaq_switch, LV_STATE_CHECKED); - strncpy(highlight.node_name, lv_textarea_get_text(objects.nodes_hl_name_area), sizeof(highlight.node_name)); + char buf[40]; + lv_snprintf(buf, 40, _("Lock: %s/%s"), screenLock ? _("on") : _("off"), settingsLock ? _("on") : _("off")); + lv_label_set_text(objects.basic_settings_screen_lock_label, buf); + lv_obj_add_flag(objects.settings_screen_lock_panel, LV_OBJ_FLAG_HIDDEN); - controller->storeUIConfig(db.uiConfig); + break; } - - /** - * @brief erase chat and all its resources - */ - void TFTView_320x240::eraseChat(uint32_t channelOrNode) - { - if (chats.find(channelOrNode) == chats.end()) { - ILOG_WARN("eraseChat: channelOrNode %d not found", channelOrNode); - return; - } - if (channelOrNode < c_max_channels) { - uint8_t ch = (uint8_t)channelOrNode; - if (state == MeshtasticView::eRunning) { - lv_obj_delete_delayed(chats.at(ch), 500); - } else { - lv_obj_del(chats.at(ch)); - } - lv_obj_del(channelGroup.at(ch)); - channelGroup[ch] = nullptr; - chats.erase(ch); - } else { - uint32_t nodeNum = channelOrNode; - if (state == MeshtasticView::eRunning) { - lv_obj_delete_delayed(chats.at(nodeNum), 500); - } else { - lv_obj_delete(chats.at(nodeNum)); - } - lv_obj_del(messages.at(nodeNum)); - messages.erase(nodeNum); - chats.erase(nodeNum); + case eScreenBrightness: { + int32_t value = lv_slider_get_value(objects.brightness_slider) * 255 / 100; + if (value != THIS->db.uiConfig.screen_brightness) { + THIS->setBrightness(value); + THIS->db.uiConfig.screen_brightness = value; + THIS->controller->storeUIConfig(THIS->db.uiConfig); } + lv_obj_add_flag(objects.settings_brightness_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_brightness_button); + break; } - - /** - * @brief clears all (persistent) chat messages - */ - void TFTView_320x240::clearChatHistory(void) - { - for (auto &it : chats) { - lv_obj_delete(it.second); - if (it.first < c_max_channels) { - lv_obj_delete(channelGroup[it.first]); - channelGroup[it.first] = nullptr; - } else { - lv_obj_delete(messages[it.first]); - } + case eTheme: { + uint32_t value = lv_dropdown_get_selected(objects.settings_theme_dropdown); + if (value != THIS->db.uiConfig.theme) { + THIS->setTheme(value); + THIS->db.uiConfig.theme = meshtastic_Theme(value); + THIS->controller->storeUIConfig(THIS->db.uiConfig); + lv_obj_set_style_bg_img_recolor(objects.settings_button, colorMesh, LV_PART_MAIN | LV_STATE_DEFAULT); } - chats.clear(); - messages.clear(); - updateActiveChats(); - updateNodesFiltered(true); - controller->removeTextMessages(0, 0, 0); - } - - /** - * @brief User widget OK button handling - * - * @param e - */ - void TFTView_320x240::ui_event_ok(lv_event_t * e) - { - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED) { - switch (THIS->activeSettings) { - case eSetup: { - meshtastic_Config_LoRaConfig_RegionCode region = - (meshtastic_Config_LoRaConfig_RegionCode)(lv_dropdown_get_selected(objects.setup_region_dropdown) + 1); - - uint32_t numChannels = LoRaPresets::getNumChannels(region, THIS->db.config.lora.modem_preset); - // if (numChannels == 0) { - // // region not possible for selected preset, revert - // lv_dropdown_set_selected(objects.settings_region_dropdown, THIS->db.config.lora.region - 1); - // return; - // } - - if (region != THIS->db.config.lora.region) { - char buf1[10], buf2[30]; - lv_dropdown_get_selected_str(objects.setup_region_dropdown, buf1, sizeof(buf1)); - lv_snprintf(buf2, sizeof(buf2), _("Region: %s"), buf1); - lv_label_set_text(objects.basic_settings_region_label, buf2); - - meshtastic_Config_LoRaConfig &lora = THIS->db.config.lora; - uint32_t defaultSlot = - lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET ? lora.channel_num : 0; - if (defaultSlot == 0) { - defaultSlot = LoRaPresets::getDefaultSlot(region, THIS->db.config.lora.modem_preset, - THIS->db.channel[0].settings.name); - } - lora.region = region; - lora.channel_num = (defaultSlot <= numChannels ? defaultSlot : 1); - THIS->controller->sendConfig(meshtastic_Config_LoRaConfig{lora}, THIS->ownNode); - } - - char buf[30]; - const char *userShort = lv_textarea_get_text(objects.setup_user_short_textarea); - const char *userLong = lv_textarea_get_text(objects.setup_user_long_textarea); - if (strcmp(userShort, THIS->db.short_name) || strcmp(userLong, THIS->db.long_name)) { - lv_snprintf(buf, sizeof(buf), _("User name: %s"), userShort); - lv_label_set_text(objects.basic_settings_user_label, buf); - lv_label_set_text(objects.user_name_short_label, userShort); - lv_label_set_text(objects.user_name_label, userLong); - strcpy(THIS->db.short_name, userShort); - strcpy(THIS->db.long_name, userLong); - meshtastic_User user{}; // TODO: don't overwrite is_licensed - strcpy(user.short_name, userShort); - strcpy(user.long_name, userLong); - THIS->controller->sendConfig(user, THIS->ownNode); - } - THIS->notifyReboot(true); - - lv_obj_add_flag(objects.initial_setup_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.home_button); - break; - } - case eUsername: { - char buf[30]; - const char *userShort = lv_textarea_get_text(objects.settings_user_short_textarea); - const char *userLong = lv_textarea_get_text(objects.settings_user_long_textarea); - if (strcmp(userShort, THIS->db.short_name) || strcmp(userLong, THIS->db.long_name)) { - lv_snprintf(buf, sizeof(buf), _("User name: %s"), userShort); - lv_label_set_text(objects.basic_settings_user_label, buf); - lv_label_set_text(objects.user_name_short_label, userShort); - lv_label_set_text(objects.user_name_label, userLong); - strcpy(THIS->db.short_name, userShort); - strcpy(THIS->db.long_name, userLong); - meshtastic_User user{}; // TODO: don't overwrite is_licensed - strcpy(user.short_name, userShort); - strcpy(user.long_name, userLong); - THIS->controller->sendConfig(user, THIS->ownNode); - THIS->notifyReboot(true); - } - lv_obj_add_flag(objects.settings_username_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_user_button); - break; - } - case eDeviceRole: { - meshtastic_Config_DeviceConfig &device = THIS->db.config.device; - meshtastic_Config_DeviceConfig_Role role = - THIS->val2role(lv_dropdown_get_selected(objects.settings_device_role_dropdown)); - - if (role != device.role) { - char buf1[30], buf2[40]; - lv_dropdown_get_selected_str(objects.settings_device_role_dropdown, buf1, sizeof(buf1)); - lv_snprintf(buf2, sizeof(buf2), _("Device Role: %s"), buf1); - lv_label_set_text(objects.basic_settings_role_label, buf2); - - device.role = role; - THIS->controller->sendConfig(meshtastic_Config_DeviceConfig{device}, THIS->ownNode); - THIS->notifyReboot(true); - } - lv_obj_add_flag(objects.settings_device_role_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_role_button); - break; - } - case eRegion: { - meshtastic_Config_LoRaConfig_RegionCode region = - (meshtastic_Config_LoRaConfig_RegionCode)(lv_dropdown_get_selected(objects.settings_region_dropdown) + 1); - - uint32_t numChannels = LoRaPresets::getNumChannels(region, THIS->db.config.lora.modem_preset); - if (numChannels == 0) { - // region not possible for selected preset, revert - lv_dropdown_set_selected(objects.settings_region_dropdown, THIS->db.config.lora.region - 1); - return; - } - - if (region != THIS->db.config.lora.region) { - char buf1[10], buf2[30]; - lv_dropdown_get_selected_str(objects.settings_region_dropdown, buf1, sizeof(buf1)); - lv_snprintf(buf2, sizeof(buf2), _("Region: %s"), buf1); - lv_label_set_text(objects.basic_settings_region_label, buf2); - - meshtastic_Config_LoRaConfig &lora = THIS->db.config.lora; - uint32_t defaultSlot = - lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET ? lora.channel_num : 0; - if (defaultSlot == 0) { - defaultSlot = LoRaPresets::getDefaultSlot(region, THIS->db.config.lora.modem_preset, - THIS->db.channel[0].settings.name); - } - lora.region = region; - lora.channel_num = (defaultSlot <= numChannels ? defaultSlot : 1); - THIS->controller->sendConfig(meshtastic_Config_LoRaConfig{lora}, THIS->ownNode); - THIS->notifyReboot(true); - } - lv_obj_add_flag(objects.settings_region_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_region_button); - break; - } - case eModemPreset: { - meshtastic_Config_LoRaConfig &lora = THIS->db.config.lora; - meshtastic_Config_LoRaConfig_ModemPreset preset = - THIS->val2preset(lv_dropdown_get_selected(objects.settings_modem_preset_dropdown)); - uint16_t channelNum = lv_slider_get_value(objects.frequency_slot_slider); - if (preset != lora.modem_preset || lora.channel_num != channelNum) { - char buf1[16], buf2[32]; - lv_dropdown_get_selected_str(objects.settings_modem_preset_dropdown, buf1, sizeof(buf1)); - lv_snprintf(buf2, sizeof(buf2), _("Modem Preset: %s"), buf1); - lv_label_set_text(objects.basic_settings_modem_preset_label, buf2); - - lora.use_preset = true; - lora.modem_preset = preset; - lora.channel_num = channelNum; - THIS->controller->sendConfig(meshtastic_Config_LoRaConfig{lora}, THIS->ownNode); - THIS->notifyReboot(true); - } - lv_obj_add_flag(objects.settings_modem_preset_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_modem_preset_button); - break; - } - case eChannel: { - for (int i = 0; i < c_max_channels; i++) { - // check if channel changed, then update and send to radio - if (memcmp(&THIS->db.channel[i], &THIS->channel_scratch[i], sizeof(THIS->channel_scratch[i])) != 0) { - THIS->channel_scratch[i].has_settings = true; - THIS->updateChannelConfig(THIS->channel_scratch[i]); - THIS->controller->sendConfig(THIS->channel_scratch[i], THIS->ownNode); - } - } - int8_t ch = (signed long)THIS->ch_label[0]->user_data; - THIS->setChannelName(THIS->db.channel[ch]); - lv_obj_clear_state(objects.settings_channel_panel, LV_STATE_DISABLED); - lv_obj_add_flag(objects.settings_channel_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_channel_button); - delete[] THIS->channel_scratch; - break; - } - case eWifi: { - char buf[30]; - const char *ssid = lv_textarea_get_text(objects.settings_wifi_ssid_textarea); - const char *psk = lv_textarea_get_text(objects.settings_wifi_password_textarea); - if (strlen(ssid) == 0 || strlen(psk) == 0) - return; - lv_snprintf(buf, sizeof(buf), _("WiFi: %s"), ssid[0] ? ssid : _("")); - lv_label_set_text(objects.basic_settings_wifi_label, buf); - if (strcmp(THIS->db.config.network.wifi_ssid, ssid) != 0 || - strcmp(THIS->db.config.network.wifi_psk, psk) != 0) { - strcpy(THIS->db.config.network.wifi_ssid, ssid); - strcpy(THIS->db.config.network.wifi_psk, psk); - THIS->db.config.network.wifi_enabled = true; - THIS->controller->sendConfig(meshtastic_Config_NetworkConfig{THIS->db.config.network}, THIS->ownNode); - THIS->notifyReboot(true); - } - THIS->enablePanel(objects.home_panel); - lv_obj_add_flag(objects.settings_wifi_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_wifi_button); - break; - } - case eLanguage: { - uint32_t value = lv_dropdown_get_selected(objects.settings_language_dropdown); - meshtastic_Language lang = THIS->val2language(value); - if (lang != THIS->db.uiConfig.language) { - THIS->db.uiConfig.language = lang; - THIS->controller->storeUIConfig(THIS->db.uiConfig); - THIS->controller->requestReboot(3, THIS->ownNode); - THIS->notifyReboot(true); - } + lv_obj_add_flag(objects.settings_theme_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_theme_button); + lv_obj_invalidate(objects.main_screen); + break; + } + case eInputControl: { + char new_val_kbd[10], new_val_ptr[10]; + lv_dropdown_get_selected_str(objects.settings_keyboard_input_dropdown, new_val_kbd, sizeof(new_val_kbd)); + lv_dropdown_get_selected_str(objects.settings_mouse_input_dropdown, new_val_ptr, sizeof(new_val_ptr)); - lv_obj_add_flag(objects.settings_language_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_language_button); - break; - } - case eScreenTimeout: { - uint32_t value = lv_slider_get_value(objects.screen_timeout_slider); - if (value > 5) - value -= value % 5; - if (value != THIS->db.uiConfig.screen_timeout) { - THIS->setTimeout(value); - THIS->db.uiConfig.screen_timeout = value; - THIS->controller->storeUIConfig(THIS->db.uiConfig); - } - lv_obj_add_flag(objects.settings_screen_timeout_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_timeout_button); - break; + bool error = false; + if (strcmp(THIS->old_val1_scratch, new_val_kbd) != 0) { + if (strcmp(THIS->old_val1_scratch, _("none")) != 0) { + THIS->inputdriver->releaseKeyboardDevice(); } - case eScreenLock: { - const char *pin = lv_textarea_get_text(objects.settings_screen_lock_password_textarea); - bool screenLock = lv_obj_has_state(objects.settings_screen_lock_switch, LV_STATE_CHECKED); - bool settingsLock = lv_obj_has_state(objects.settings_settings_lock_switch, LV_STATE_CHECKED); - if ((screenLock || settingsLock) && (atol(pin) == 0 || strlen(pin) != 6)) - return; // require pin != "000000" - if ((screenLock != THIS->db.uiConfig.screen_lock) || settingsLock != THIS->db.uiConfig.settings_lock || - atol(pin) != THIS->db.uiConfig.pin_code) { - THIS->db.uiConfig.screen_lock = screenLock; - THIS->db.uiConfig.settings_lock = settingsLock; - THIS->db.uiConfig.pin_code = atol(pin); - THIS->controller->storeUIConfig(THIS->db.uiConfig); - } - - char buf[40]; - lv_snprintf(buf, 40, _("Lock: %s/%s"), screenLock ? _("on") : _("off"), settingsLock ? _("on") : _("off")); - lv_label_set_text(objects.basic_settings_screen_lock_label, buf); - lv_obj_add_flag(objects.settings_screen_lock_panel, LV_OBJ_FLAG_HIDDEN); - - break; + if (strcmp(new_val_kbd, _("none")) != 0) { + error &= THIS->inputdriver->useKeyboardDevice(new_val_kbd); } - case eScreenBrightness: { - int32_t value = lv_slider_get_value(objects.brightness_slider) * 255 / 100; - if (value != THIS->db.uiConfig.screen_brightness) { - THIS->setBrightness(value); - THIS->db.uiConfig.screen_brightness = value; - THIS->controller->storeUIConfig(THIS->db.uiConfig); - } - lv_obj_add_flag(objects.settings_brightness_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_brightness_button); - break; + } + if (strcmp(THIS->old_val2_scratch, new_val_ptr) != 0) { + if (strcmp(THIS->old_val2_scratch, _("none")) != 0) { + THIS->inputdriver->releasePointerDevice(); } - case eTheme: { - uint32_t value = lv_dropdown_get_selected(objects.settings_theme_dropdown); - if (value != THIS->db.uiConfig.theme) { - THIS->setTheme(value); - THIS->db.uiConfig.theme = meshtastic_Theme(value); - THIS->controller->storeUIConfig(THIS->db.uiConfig); - lv_obj_set_style_bg_img_recolor(objects.settings_button, colorMesh, LV_PART_MAIN | LV_STATE_DEFAULT); - } - - lv_obj_add_flag(objects.settings_theme_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_theme_button); - lv_obj_invalidate(objects.main_screen); - break; + if (strcmp(new_val_ptr, _("none")) != 0) { + error &= THIS->inputdriver->usePointerDevice(new_val_ptr); } - case eInputControl: { - char new_val_kbd[10], new_val_ptr[10]; - lv_dropdown_get_selected_str(objects.settings_keyboard_input_dropdown, new_val_kbd, sizeof(new_val_kbd)); - lv_dropdown_get_selected_str(objects.settings_mouse_input_dropdown, new_val_ptr, sizeof(new_val_ptr)); - - bool error = false; - if (strcmp(THIS->old_val1_scratch, new_val_kbd) != 0) { - if (strcmp(THIS->old_val1_scratch, _("none")) != 0) { - THIS->inputdriver->releaseKeyboardDevice(); - } - if (strcmp(new_val_kbd, _("none")) != 0) { - error &= THIS->inputdriver->useKeyboardDevice(new_val_kbd); - } - } - if (strcmp(THIS->old_val2_scratch, new_val_ptr) != 0) { - if (strcmp(THIS->old_val2_scratch, _("none")) != 0) { - THIS->inputdriver->releasePointerDevice(); - } - if (strcmp(new_val_ptr, _("none")) != 0) { - error &= THIS->inputdriver->usePointerDevice(new_val_ptr); - } - } + } - THIS->setInputButtonLabel(); + THIS->setInputButtonLabel(); - if (error) { - ILOG_WARN("failed to use %s/%s", new_val_kbd, new_val_ptr); - return; - } + if (error) { + ILOG_WARN("failed to use %s/%s", new_val_kbd, new_val_ptr); + return; + } - std::string current_kbd = THIS->inputdriver->getCurrentKeyboardDevice(); - std::string current_ptr = THIS->inputdriver->getCurrentPointerDevice(); - if (strcmp(current_kbd.c_str(), _("none")) == 0 && strcmp(current_ptr.c_str(), _("none")) == 0 && - THIS->input_group) { - lv_group_delete(THIS->input_group); - THIS->input_group = nullptr; - } else if (strcmp(THIS->old_val1_scratch, current_kbd.c_str()) != 0 || - strcmp(THIS->old_val2_scratch, current_ptr.c_str()) != 0) { - THIS->setInputGroup(); - } + std::string current_kbd = THIS->inputdriver->getCurrentKeyboardDevice(); + std::string current_ptr = THIS->inputdriver->getCurrentPointerDevice(); + if (strcmp(current_kbd.c_str(), _("none")) == 0 && strcmp(current_ptr.c_str(), _("none")) == 0 && THIS->input_group) { + lv_group_delete(THIS->input_group); + THIS->input_group = nullptr; + } else if (strcmp(THIS->old_val1_scratch, current_kbd.c_str()) != 0 || + strcmp(THIS->old_val2_scratch, current_ptr.c_str()) != 0) { + THIS->setInputGroup(); + } - lv_obj_add_flag(objects.settings_input_control_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_input_button); - break; - } - case eAlertBuzzer: { - meshtastic_ModuleConfig_ExternalNotificationConfig &config = THIS->db.module_config.external_notification; - int tone = lv_dropdown_get_selected(objects.settings_ringtone_dropdown) + 1; - - bool silent = false; - bool alert_message = lv_obj_has_state(objects.settings_alert_buzzer_switch, LV_STATE_CHECKED); - if ((!config.enabled || !config.alert_message_buzzer) && alert_message) { - if (!config.enabled || !config.alert_message_buzzer || !config.use_pwm || !config.use_i2s_as_buzzer) { - config.enabled = true; - config.alert_message_buzzer = true; - config.use_pwm = true; - config.nag_timeout = 0; + lv_obj_add_flag(objects.settings_input_control_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_input_button); + break; + } + case eAlertBuzzer: { + meshtastic_ModuleConfig_ExternalNotificationConfig &config = THIS->db.module_config.external_notification; + int tone = lv_dropdown_get_selected(objects.settings_ringtone_dropdown) + 1; + + bool silent = false; + bool alert_message = lv_obj_has_state(objects.settings_alert_buzzer_switch, LV_STATE_CHECKED); + if ((!config.enabled || !config.alert_message_buzzer) && alert_message) { + if (!config.enabled || !config.alert_message_buzzer || !config.use_pwm || !config.use_i2s_as_buzzer) { + config.enabled = true; + config.alert_message_buzzer = true; + config.use_pwm = true; + config.nag_timeout = 0; #ifdef USE_I2S_BUZZER - config.use_i2s_as_buzzer = true; - config.use_pwm = false; + config.use_i2s_as_buzzer = true; + config.use_pwm = false; #endif - } - THIS->notifyReboot(true); - THIS->controller->sendConfig(meshtastic_ModuleConfig_ExternalNotificationConfig{config}, THIS->ownNode); - } else if (config.alert_message_buzzer && !alert_message) { - silent = true; - } - - THIS->controller->sendConfig(ringtone[silent ? 0 : tone].rtttl, THIS->ownNode); - THIS->db.uiConfig.ring_tone_id = tone; - THIS->db.silent = silent; - THIS->db.uiConfig.alert_enabled = !silent; - THIS->setBellText(THIS->db.uiConfig.alert_enabled, !silent); - THIS->controller->storeUIConfig(THIS->db.uiConfig); - - lv_obj_add_flag(objects.settings_alert_buzzer_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_alert_button); - break; - } - case eBackupRestore: { - uint32_t option = lv_dropdown_get_selected(objects.settings_backup_restore_dropdown); - if (lv_obj_has_state(objects.settings_backup_checkbox, LV_STATE_CHECKED)) { - THIS->backup(option); - } else if (lv_obj_has_state(objects.settings_restore_checkbox, LV_STATE_CHECKED)) { - THIS->restore(option); - } - lv_obj_add_flag(objects.settings_backup_restore_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_backup_restore_button); - break; - } - case eReset: { - uint32_t option = lv_dropdown_get_selected(objects.settings_reset_dropdown); - if (option == 2) { - THIS->clearChatHistory(); - } else { - THIS->notifyReboot(true); - THIS->controller->requestReset(option, THIS->ownNode); - } - lv_obj_add_flag(objects.settings_reset_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_reset_button); - break; - } - case eDisplayMode: { - meshtastic_Config_DisplayConfig &display = THIS->db.config.display; - meshtastic_Config_BluetoothConfig &bluetooth = THIS->db.config.bluetooth; - display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT; - bluetooth.enabled = true; - THIS->controller->sendConfig(meshtastic_Config_DisplayConfig{display}, THIS->ownNode); - THIS->controller->sendConfig(meshtastic_Config_BluetoothConfig{bluetooth}, THIS->ownNode); - THIS->controller->requestReboot(5, THIS->ownNode); - lv_screen_load_anim(objects.blank_screen, LV_SCR_LOAD_ANIM_FADE_OUT, 4000, 1000, false); - lv_obj_add_flag(objects.reboot_panel, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(objects.settings_reboot_panel, LV_OBJ_FLAG_HIDDEN); - break; } - case eModifyChannel: { - meshtastic_ChannelSettings_psk_t psk = {}; - const char *name = lv_textarea_get_text(objects.settings_modify_channel_name_textarea); - const char *base64 = lv_textarea_get_text(objects.settings_modify_channel_psk_textarea); - uint8_t btn_id = (unsigned long)objects.settings_modify_channel_name_textarea->user_data; - int8_t ch = (signed long)THIS->ch_label[btn_id]->user_data; - - if (strlen(base64) == 0 && strlen(name) == 0) { - // delete channel - THIS->channel_scratch[ch].role = meshtastic_Channel_Role_DISABLED; - THIS->channel_scratch[ch].settings.psk.size = 0; - memset(THIS->channel_scratch[ch].settings.name, 0, sizeof(THIS->channel_scratch[ch].settings.name)); - memset(THIS->channel_scratch[ch].settings.psk.bytes, 0, - sizeof(THIS->channel_scratch[ch].settings.psk.bytes)); - THIS->channel_scratch[ch].has_settings = false; - lv_label_set_text(THIS->ch_label[btn_id], _("")); - THIS->activeSettings = eChannel; - } else { - int paddings = (4 - strlen(base64) % 4) % 4; - while (paddings-- > 0) { - lv_textarea_add_text(objects.settings_modify_channel_psk_textarea, "="); - } + THIS->notifyReboot(true); + THIS->controller->sendConfig(meshtastic_ModuleConfig_ExternalNotificationConfig{config}, THIS->ownNode); + } else if (config.alert_message_buzzer && !alert_message) { + silent = true; + } - if (THIS->base64ToPsk(lv_textarea_get_text(objects.settings_modify_channel_psk_textarea), psk.bytes, - psk.size)) { - if (strlen(name) || psk.size) { - // TODO: fill temp storage -> user data - lv_label_set_text(THIS->ch_label[btn_id], name); - strcpy(THIS->channel_scratch[ch].settings.name, name); - memcpy(THIS->channel_scratch[ch].settings.psk.bytes, psk.bytes, 32); - THIS->channel_scratch[ch].settings.psk.size = psk.size; - THIS->activeSettings = eChannel; - } - } - THIS->channel_scratch[ch].role = - (ch == 0) ? meshtastic_Channel_Role_PRIMARY : meshtastic_Channel_Role_SECONDARY; - } - if (THIS->activeSettings == eChannel) { - lv_obj_add_flag(objects.settings_modify_channel_panel, LV_OBJ_FLAG_HIDDEN); - THIS->enablePanel(objects.settings_channel_panel); - lv_group_focus_obj(objects.settings_channel0_button); - } - return; - } - default: - ILOG_ERROR("Unhandled ok event"); - break; - } - THIS->enablePanel(objects.controller_panel); - THIS->enablePanel(objects.tab_page_basic_settings); - THIS->activeSettings = eNone; + THIS->controller->sendConfig(ringtone[silent ? 0 : tone].rtttl, THIS->ownNode); + THIS->db.uiConfig.ring_tone_id = tone; + THIS->db.silent = silent; + THIS->db.uiConfig.alert_enabled = !silent; + THIS->setBellText(THIS->db.uiConfig.alert_enabled, !silent); + THIS->controller->storeUIConfig(THIS->db.uiConfig); + + lv_obj_add_flag(objects.settings_alert_buzzer_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_alert_button); + break; + } + case eBackupRestore: { + uint32_t option = lv_dropdown_get_selected(objects.settings_backup_restore_dropdown); + if (lv_obj_has_state(objects.settings_backup_checkbox, LV_STATE_CHECKED)) { + THIS->backup(option); + } else if (lv_obj_has_state(objects.settings_restore_checkbox, LV_STATE_CHECKED)) { + THIS->restore(option); } + lv_obj_add_flag(objects.settings_backup_restore_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_backup_restore_button); + break; } - - /** - * @brief Cancel button user widget handling - * - * @param e - */ - void TFTView_320x240::ui_event_cancel(lv_event_t * e) - { - lv_event_code_t event_code = lv_event_get_code(e); - if (event_code == LV_EVENT_CLICKED) { - switch (THIS->activeSettings) { - case TFTView_320x240::eSetup: { - THIS->ui_set_active(objects.home_button, objects.home_panel, objects.top_panel); - // lv_obj_add_flag(objects.initial_setup_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.home_button); - break; - } - case TFTView_320x240::eUsername: { - lv_obj_add_flag(objects.settings_username_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_user_button); - break; - } - case TFTView_320x240::eDeviceRole: { - lv_obj_add_flag(objects.settings_device_role_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_role_button); - break; - } - case TFTView_320x240::eRegion: { - lv_obj_add_flag(objects.settings_region_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_region_button); - break; - } - case TFTView_320x240::eModemPreset: { - lv_obj_add_flag(objects.settings_modem_preset_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_modem_preset_button); - break; - } - case TFTView_320x240::eChannel: { - delete[] THIS->channel_scratch; - lv_obj_add_flag(objects.settings_channel_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_channel_button); - break; - } - case TFTView_320x240::eWifi: { - lv_obj_add_flag(objects.settings_wifi_panel, LV_OBJ_FLAG_HIDDEN); - THIS->enablePanel(objects.home_panel); - lv_group_focus_obj(objects.home_wlan_button); - break; - } - case TFTView_320x240::eLanguage: { - lv_obj_add_flag(objects.settings_language_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_language_button); - break; - } - case TFTView_320x240::eScreenTimeout: { - lv_obj_add_flag(objects.settings_screen_timeout_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_timeout_button); - break; - } - case eScreenLock: { - lv_obj_add_flag(objects.settings_screen_lock_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_screen_lock_button); - break; - } - case TFTView_320x240::eScreenBrightness: { - lv_obj_add_flag(objects.settings_brightness_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_brightness_button); - // revert to old brightness value - uint32_t old_brightness = THIS->db.uiConfig.screen_brightness; - THIS->displaydriver->setBrightness((uint8_t)old_brightness); - break; - } - case TFTView_320x240::eTheme: { - lv_obj_add_flag(objects.settings_theme_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_theme_button); - break; - } - case TFTView_320x240::eInputControl: { - lv_obj_add_flag(objects.settings_input_control_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_input_button); - break; - } - case TFTView_320x240::eAlertBuzzer: { - lv_obj_add_flag(objects.settings_alert_buzzer_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_alert_button); - break; - } - case TFTView_320x240::eBackupRestore: { - lv_obj_add_flag(objects.settings_backup_restore_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_backup_restore_button); - break; - } - case TFTView_320x240::eReset: { - lv_obj_add_flag(objects.settings_reset_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_reset_button); - break; - } - case TFTView_320x240::eDisplayMode: { - lv_obj_add_flag(objects.settings_reboot_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.basic_settings_reset_button); - break; - } - case TFTView_320x240::eModifyChannel: { - lv_obj_add_flag(objects.settings_modify_channel_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.settings_channel0_button); - THIS->enablePanel(objects.settings_channel_panel); - THIS->activeSettings = eChannel; - return; - } - default: - ILOG_ERROR("Unhandled cancel event"); - break; + case eReset: { + uint32_t option = lv_dropdown_get_selected(objects.settings_reset_dropdown); + if (option == 2) { + THIS->clearChatHistory(); + } else { + THIS->notifyReboot(true); + THIS->controller->requestReset(option, THIS->ownNode); + } + lv_obj_add_flag(objects.settings_reset_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_reset_button); + break; + } + case eDisplayMode: { + meshtastic_Config_DisplayConfig &display = THIS->db.config.display; + meshtastic_Config_BluetoothConfig &bluetooth = THIS->db.config.bluetooth; + display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT; + bluetooth.enabled = true; + THIS->controller->sendConfig(meshtastic_Config_DisplayConfig{display}, THIS->ownNode); + THIS->controller->sendConfig(meshtastic_Config_BluetoothConfig{bluetooth}, THIS->ownNode); + THIS->controller->requestReboot(5, THIS->ownNode); + lv_screen_load_anim(objects.blank_screen, LV_SCR_LOAD_ANIM_FADE_OUT, 4000, 1000, false); + lv_obj_add_flag(objects.reboot_panel, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(objects.settings_reboot_panel, LV_OBJ_FLAG_HIDDEN); + break; + } + case eModifyChannel: { + meshtastic_ChannelSettings_psk_t psk = {}; + const char *name = lv_textarea_get_text(objects.settings_modify_channel_name_textarea); + const char *base64 = lv_textarea_get_text(objects.settings_modify_channel_psk_textarea); + uint8_t btn_id = (unsigned long)objects.settings_modify_channel_name_textarea->user_data; + int8_t ch = (signed long)THIS->ch_label[btn_id]->user_data; + + if (strlen(base64) == 0 && strlen(name) == 0) { + // delete channel + THIS->channel_scratch[ch].role = meshtastic_Channel_Role_DISABLED; + THIS->channel_scratch[ch].settings.psk.size = 0; + memset(THIS->channel_scratch[ch].settings.name, 0, sizeof(THIS->channel_scratch[ch].settings.name)); + memset(THIS->channel_scratch[ch].settings.psk.bytes, 0, sizeof(THIS->channel_scratch[ch].settings.psk.bytes)); + THIS->channel_scratch[ch].has_settings = false; + lv_label_set_text(THIS->ch_label[btn_id], _("")); + THIS->activeSettings = eChannel; + } else { + int paddings = (4 - strlen(base64) % 4) % 4; + while (paddings-- > 0) { + lv_textarea_add_text(objects.settings_modify_channel_psk_textarea, "="); + } + + if (THIS->base64ToPsk(lv_textarea_get_text(objects.settings_modify_channel_psk_textarea), psk.bytes, psk.size)) { + if (strlen(name) || psk.size) { + // TODO: fill temp storage -> user data + lv_label_set_text(THIS->ch_label[btn_id], name); + strcpy(THIS->channel_scratch[ch].settings.name, name); + memcpy(THIS->channel_scratch[ch].settings.psk.bytes, psk.bytes, 32); + THIS->channel_scratch[ch].settings.psk.size = psk.size; + THIS->activeSettings = eChannel; + } } - - THIS->enablePanel(objects.controller_panel); - THIS->enablePanel(objects.tab_page_basic_settings); - THIS->activeSettings = eNone; + THIS->channel_scratch[ch].role = (ch == 0) ? meshtastic_Channel_Role_PRIMARY : meshtastic_Channel_Role_SECONDARY; + } + if (THIS->activeSettings == eChannel) { + lv_obj_add_flag(objects.settings_modify_channel_panel, LV_OBJ_FLAG_HIDDEN); + THIS->enablePanel(objects.settings_channel_panel); + lv_group_focus_obj(objects.settings_channel0_button); } + return; } - - // end button event handlers - - void TFTView_320x240::ui_event_screen_timeout_slider(lv_event_t * e) - { - lv_obj_t *slider = lv_event_get_target_obj(e); - char buf[20]; - uint32_t value = lv_slider_get_value(slider); - if (value > 5) - value -= value % 5; - if (value == 0) - lv_snprintf(buf, sizeof(buf), _("Timeout: off")); - else - lv_snprintf(buf, sizeof(buf), _("Timeout: %ds"), value); - lv_label_set_text(objects.settings_screen_timeout_label, buf); + default: + ILOG_ERROR("Unhandled ok event"); + break; } + THIS->enablePanel(objects.controller_panel); + THIS->enablePanel(objects.tab_page_basic_settings); + THIS->activeSettings = eNone; + } +} - void TFTView_320x240::ui_event_brightness_slider(lv_event_t * e) - { - lv_obj_t *slider = lv_event_get_target_obj(e); - char buf[20]; - lv_snprintf(buf, sizeof(buf), _("Brightness: %d%%"), (int)lv_slider_get_value(slider)); - lv_label_set_text(objects.settings_brightness_label, buf); - THIS->displaydriver->setBrightness((uint8_t)(lv_slider_get_value(slider) * 255 / 100)); +/** + * @brief Cancel button user widget handling + * + * @param e + */ +void TFTView_320x240::ui_event_cancel(lv_event_t *e) +{ + lv_event_code_t event_code = lv_event_get_code(e); + if (event_code == LV_EVENT_CLICKED) { + switch (THIS->activeSettings) { + case TFTView_320x240::eSetup: { + THIS->ui_set_active(objects.home_button, objects.home_panel, objects.top_panel); + // lv_obj_add_flag(objects.initial_setup_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.home_button); + break; } - - void TFTView_320x240::ui_event_frequency_slot_slider(lv_event_t * e) - { - lv_obj_t *slider = lv_event_get_target_obj(e); - char buf[40]; - uint32_t channel = (uint32_t)lv_slider_get_value(slider); - sprintf(buf, _("FrequencySlot: %d (%g MHz)"), channel, - LoRaPresets::getRadioFreq(THIS->db.config.lora.region, - THIS->val2preset(lv_dropdown_get_selected(objects.settings_modem_preset_dropdown)), - channel)); - lv_label_set_text(objects.frequency_slot_label, buf); + case TFTView_320x240::eUsername: { + lv_obj_add_flag(objects.settings_username_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_user_button); + break; + } + case TFTView_320x240::eDeviceRole: { + lv_obj_add_flag(objects.settings_device_role_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_role_button); + break; + } + case TFTView_320x240::eRegion: { + lv_obj_add_flag(objects.settings_region_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_region_button); + break; + } + case TFTView_320x240::eModemPreset: { + lv_obj_add_flag(objects.settings_modem_preset_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_modem_preset_button); + break; + } + case TFTView_320x240::eChannel: { + delete[] THIS->channel_scratch; + lv_obj_add_flag(objects.settings_channel_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_channel_button); + break; + } + case TFTView_320x240::eWifi: { + lv_obj_add_flag(objects.settings_wifi_panel, LV_OBJ_FLAG_HIDDEN); + THIS->enablePanel(objects.home_panel); + lv_group_focus_obj(objects.home_wlan_button); + break; + } + case TFTView_320x240::eLanguage: { + lv_obj_add_flag(objects.settings_language_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_language_button); + break; + } + case TFTView_320x240::eScreenTimeout: { + lv_obj_add_flag(objects.settings_screen_timeout_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_timeout_button); + break; + } + case eScreenLock: { + lv_obj_add_flag(objects.settings_screen_lock_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_screen_lock_button); + break; + } + case TFTView_320x240::eScreenBrightness: { + lv_obj_add_flag(objects.settings_brightness_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_brightness_button); + // revert to old brightness value + uint32_t old_brightness = THIS->db.uiConfig.screen_brightness; + THIS->displaydriver->setBrightness((uint8_t)old_brightness); + break; + } + case TFTView_320x240::eTheme: { + lv_obj_add_flag(objects.settings_theme_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_theme_button); + break; + } + case TFTView_320x240::eInputControl: { + lv_obj_add_flag(objects.settings_input_control_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_input_button); + break; + } + case TFTView_320x240::eAlertBuzzer: { + lv_obj_add_flag(objects.settings_alert_buzzer_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_alert_button); + break; + } + case TFTView_320x240::eBackupRestore: { + lv_obj_add_flag(objects.settings_backup_restore_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_backup_restore_button); + break; + } + case TFTView_320x240::eReset: { + lv_obj_add_flag(objects.settings_reset_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_reset_button); + break; + } + case TFTView_320x240::eDisplayMode: { + lv_obj_add_flag(objects.settings_reboot_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.basic_settings_reset_button); + break; + } + case TFTView_320x240::eModifyChannel: { + lv_obj_add_flag(objects.settings_modify_channel_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.settings_channel0_button); + THIS->enablePanel(objects.settings_channel_panel); + THIS->activeSettings = eChannel; + return; + } + default: + ILOG_ERROR("Unhandled cancel event"); + break; } - void TFTView_320x240::ui_event_modem_preset_dropdown(lv_event_t * e) - { - lv_obj_t *dropdown = lv_event_get_target_obj(e); - meshtastic_Config_LoRaConfig_ModemPreset preset = - (meshtastic_Config_LoRaConfig_ModemPreset)lv_dropdown_get_selected(dropdown); - uint32_t numChannels = LoRaPresets::getNumChannels(THIS->db.config.lora.region, preset); - if (numChannels == 0) { - // preset not possible for this region, revert - lv_dropdown_set_selected(dropdown, THIS->db.config.lora.modem_preset); - numChannels = LoRaPresets::getNumChannels(THIS->db.config.lora.region, THIS->db.config.lora.modem_preset); - return; - } + THIS->enablePanel(objects.controller_panel); + THIS->enablePanel(objects.tab_page_basic_settings); + THIS->activeSettings = eNone; + } +} - uint32_t channel = - LoRaPresets::getDefaultSlot(THIS->db.config.lora.region, preset, THIS->db.channel[0].settings.name); - if (channel > numChannels) - channel = 1; - lv_slider_set_range(objects.frequency_slot_slider, 1, numChannels); - lv_slider_set_value(objects.frequency_slot_slider, channel, LV_ANIM_ON); +// end button event handlers - char buf[40]; - sprintf(buf, _("FrequencySlot: %d (%g MHz)"), channel, - LoRaPresets::getRadioFreq(THIS->db.config.lora.region, preset, channel)); - lv_label_set_text(objects.frequency_slot_label, buf); - } +void TFTView_320x240::ui_event_screen_timeout_slider(lv_event_t *e) +{ + lv_obj_t *slider = lv_event_get_target_obj(e); + char buf[20]; + uint32_t value = lv_slider_get_value(slider); + if (value > 5) + value -= value % 5; + if (value == 0) + lv_snprintf(buf, sizeof(buf), _("Timeout: off")); + else + lv_snprintf(buf, sizeof(buf), _("Timeout: %ds"), value); + lv_label_set_text(objects.settings_screen_timeout_label, buf); +} - void TFTView_320x240::ui_event_setup_region_dropdown(lv_event_t * e) {} +void TFTView_320x240::ui_event_brightness_slider(lv_event_t *e) +{ + lv_obj_t *slider = lv_event_get_target_obj(e); + char buf[20]; + lv_snprintf(buf, sizeof(buf), _("Brightness: %d%%"), (int)lv_slider_get_value(slider)); + lv_label_set_text(objects.settings_brightness_label, buf); + THIS->displaydriver->setBrightness((uint8_t)(lv_slider_get_value(slider) * 255 / 100)); +} - // animations - void TFTView_320x240::ui_anim_node_panel_cb(void *var, int32_t v) - { - lv_obj_set_height((lv_obj_t *)var, v); - } +void TFTView_320x240::ui_event_frequency_slot_slider(lv_event_t *e) +{ + lv_obj_t *slider = lv_event_get_target_obj(e); + char buf[40]; + uint32_t channel = (uint32_t)lv_slider_get_value(slider); + sprintf(buf, _("FrequencySlot: %d (%g MHz)"), channel, + LoRaPresets::getRadioFreq(THIS->db.config.lora.region, + THIS->val2preset(lv_dropdown_get_selected(objects.settings_modem_preset_dropdown)), + channel)); + lv_label_set_text(objects.frequency_slot_label, buf); +} - void TFTView_320x240::ui_anim_radar_cb(void *var, int32_t r) - { - lv_img_set_angle(objects.radar_beam, r); - } - - /** - * @brief Dynamically show user widget - * First a panel is created where the widget is located in, then the widget is drawn. - * "active_widget" contains the surrounding panel which must be destroyed - * to remove the widget from the screen (e.g. by pressing OK/Cancel). - * - * @param func - */ - void TFTView_320x240::showUserWidget(UserWidgetFunc createWidget) - { - lv_obj_t *obj = lv_obj_create(objects.main_screen); - lv_obj_set_pos(obj, 39, 25); - lv_obj_set_size(obj, LV_PCT(88), LV_PCT(90)); - lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); - lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_bg_color(obj, colorDarkGray, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_radius(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - activeWidget = obj; +void TFTView_320x240::ui_event_modem_preset_dropdown(lv_event_t *e) +{ + lv_obj_t *dropdown = lv_event_get_target_obj(e); + meshtastic_Config_LoRaConfig_ModemPreset preset = + (meshtastic_Config_LoRaConfig_ModemPreset)lv_dropdown_get_selected(dropdown); + uint32_t numChannels = LoRaPresets::getNumChannels(THIS->db.config.lora.region, preset); + if (numChannels == 0) { + // preset not possible for this region, revert + lv_dropdown_set_selected(dropdown, THIS->db.config.lora.modem_preset); + numChannels = LoRaPresets::getNumChannels(THIS->db.config.lora.region, THIS->db.config.lora.modem_preset); + return; + } - createWidget(activeWidget, NULL, 0); - } + uint32_t channel = LoRaPresets::getDefaultSlot(THIS->db.config.lora.region, preset, THIS->db.channel[0].settings.name); + if (channel > numChannels) + channel = 1; + lv_slider_set_range(objects.frequency_slot_slider, 1, numChannels); + lv_slider_set_value(objects.frequency_slot_slider, channel, LV_ANIM_ON); - void TFTView_320x240::handleAddMessage(char *msg) - { - // retrieve nodeNum + channel from activeMsgContainer - uint32_t to = UINT32_MAX; - uint8_t ch = 0; - uint8_t hopLimit = db.config.lora.hop_limit; - uint32_t requestId; - uint32_t channelOrNode = (unsigned long)activeMsgContainer->user_data; - bool usePkc = false; + char buf[40]; + sprintf(buf, _("FrequencySlot: %d (%g MHz)"), channel, + LoRaPresets::getRadioFreq(THIS->db.config.lora.region, preset, channel)); + lv_label_set_text(objects.frequency_slot_label, buf); +} - auto callback = [this](const ResponseHandler::Request &req, ResponseHandler::EventType evt, int32_t pass) { - this->onTextMessageCallback(req, evt, pass); - }; +void TFTView_320x240::ui_event_setup_region_dropdown(lv_event_t *e) {} - if (channelOrNode < c_max_channels) { - ch = (uint8_t)channelOrNode; - requestId = requests.addRequest(ch, ResponseHandler::TextMessageRequest, (void *)(long)ch, callback); - } else { - ch = (uint8_t)(unsigned long)nodes[channelOrNode]->user_data; - to = channelOrNode; - usePkc = (unsigned long)nodes[to]->LV_OBJ_IDX(node_bat_idx)->user_data; // hasKey - requestId = requests.addRequest(to, ResponseHandler::TextMessageRequest, (void *)to, callback); - // trial: hoplimit optimization for direct text messages - int8_t hopsAway = (signed long)nodes[to]->LV_OBJ_IDX(node_sig_idx)->user_data; - if (hopsAway < 0) - hopsAway = db.config.lora.hop_limit; - hopLimit = (hopsAway < db.config.lora.hop_limit ? hopsAway + 1 : hopsAway); - } +// animations +void TFTView_320x240::ui_anim_node_panel_cb(void *var, int32_t v) +{ + lv_obj_set_height((lv_obj_t *)var, v); +} - // tweak to allow multiple lines in single line text area - for (int i = 0; i < strlen(msg); i++) - if (msg[i] == CR_REPLACEMENT) - msg[i] = '\n'; +void TFTView_320x240::ui_anim_radar_cb(void *var, int32_t r) +{ + lv_img_set_angle(objects.radar_beam, r); +} - controller->sendTextMessage(to, ch, hopLimit, actTime, requestId, usePkc, msg); - addMessage(activeMsgContainer, actTime, requestId, msg, LogMessage::eNone); - } +/** + * @brief Dynamically show user widget + * First a panel is created where the widget is located in, then the widget is drawn. + * "active_widget" contains the surrounding panel which must be destroyed + * to remove the widget from the screen (e.g. by pressing OK/Cancel). + * + * @param func + */ +void TFTView_320x240::showUserWidget(UserWidgetFunc createWidget) +{ + lv_obj_t *obj = lv_obj_create(objects.main_screen); + lv_obj_set_pos(obj, 39, 25); + lv_obj_set_size(obj, LV_PCT(88), LV_PCT(90)); + lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_color(obj, colorDarkGray, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_width(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_radius(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + activeWidget = obj; + + createWidget(activeWidget, NULL, 0); +} - /** - * display message that has just been written and sent out - */ - void TFTView_320x240::addMessage(lv_obj_t * container, uint32_t msgTime, uint32_t requestId, char *msg, - LogMessage::MsgStatus status) - { - lv_obj_t *hiddenPanel = lv_obj_create(container); - lv_obj_set_width(hiddenPanel, lv_pct(100)); - lv_obj_set_height(hiddenPanel, LV_SIZE_CONTENT); - lv_obj_set_align(hiddenPanel, LV_ALIGN_CENTER); - lv_obj_clear_flag(hiddenPanel, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_radius(hiddenPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - add_style_panel_style(hiddenPanel); - - lv_obj_set_style_border_width(hiddenPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_left(hiddenPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_right(hiddenPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_top(hiddenPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_bottom(hiddenPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - hiddenPanel->user_data = (void *)requestId; - - // add timestamp - char buf[284]; // 237 + 4 + 40 + 2 + 1 - buf[0] = '\0'; - uint32_t len = timestamp(buf, msgTime, status == LogMessage::eNone); - strcat(&buf[len], msg); - - lv_obj_t *textLabel = lv_label_create(hiddenPanel); - // calculate expected size of text bubble, to make it look nicer - lv_coord_t width = lv_txt_get_width(buf, strlen(buf), &ui_font_montserrat_12, 0); - lv_obj_set_width(textLabel, std::max(std::min(width, 200) + 10, 40)); - lv_obj_set_height(textLabel, LV_SIZE_CONTENT); - lv_obj_set_y(textLabel, 0); - lv_obj_set_align(textLabel, LV_ALIGN_RIGHT_MID); - lv_label_set_text(textLabel, buf); - - add_style_chat_message_style(textLabel); - - lv_obj_scroll_to_view(hiddenPanel, LV_ANIM_ON); - lv_obj_move_foreground(objects.message_input_area); - - switch (status) { - case LogMessage::eHeard: - lv_obj_set_style_border_color(textLabel, colorYellow, LV_PART_MAIN | LV_STATE_DEFAULT); - break; - case LogMessage::eAcked: - lv_obj_set_style_border_color(textLabel, colorBlueGreen, LV_PART_MAIN | LV_STATE_DEFAULT); - break; - case LogMessage::eFailed: - lv_obj_set_style_border_color(textLabel, colorRed, LV_PART_MAIN | LV_STATE_DEFAULT); - break; - default: - break; - } - } +void TFTView_320x240::handleAddMessage(char *msg) +{ + // retrieve nodeNum + channel from activeMsgContainer + uint32_t to = UINT32_MAX; + uint8_t ch = 0; + uint8_t hopLimit = db.config.lora.hop_limit; + uint32_t requestId; + uint32_t channelOrNode = (unsigned long)activeMsgContainer->user_data; + bool usePkc = false; + + auto callback = [this](const ResponseHandler::Request &req, ResponseHandler::EventType evt, int32_t pass) { + this->onTextMessageCallback(req, evt, pass); + }; - void TFTView_320x240::addNode(uint32_t nodeNum, uint8_t ch, const char *userShort, const char *userLong, - uint32_t lastHeard, eRole role, bool hasKey, bool unmessagable) - { - // lv_obj nodesPanel children | user data (4 bytes) - // ================================================== - // [0]: img | role - // [1]: btn | ll group - // [2]: lbl user long | nodeNum - // [3]: lbl user short | userShort (4 chars) - // [4]: lbl battery | hasKey - // [5]: lbl lastHeard | lastHeard / curtime - // [6]: lbl signal (or hops) | hops away - // [7]: lbl position 1 | lat - // [8]: lbl position 2 | lon - // [9]: lbl telemetry 1 | - // [10]: lbl telemetry 2 | iaq - // panel user_data: ch - - ILOG_DEBUG("addNode(%d): num=0x%08x, lastseen=%d, name=%s(%s), role=%d", nodeCount, nodeNum, lastHeard, userLong, - userShort, role); - while (nodeCount >= MAX_NUM_NODES_VIEW) { - purgeNode(nodeNum); - } + if (channelOrNode < c_max_channels) { + ch = (uint8_t)channelOrNode; + requestId = requests.addRequest(ch, ResponseHandler::TextMessageRequest, (void *)(long)ch, callback); + } else { + ch = (uint8_t)(unsigned long)nodes[channelOrNode]->user_data; + to = channelOrNode; + usePkc = (unsigned long)nodes[to]->LV_OBJ_IDX(node_bat_idx)->user_data; // hasKey + requestId = requests.addRequest(to, ResponseHandler::TextMessageRequest, (void *)to, callback); + // trial: hoplimit optimization for direct text messages + int8_t hopsAway = (signed long)nodes[to]->LV_OBJ_IDX(node_sig_idx)->user_data; + if (hopsAway < 0) + hopsAway = db.config.lora.hop_limit; + hopLimit = (hopsAway < db.config.lora.hop_limit ? hopsAway + 1 : hopsAway); + } - lv_obj_t *p = lv_obj_create(objects.nodes_panel); - lv_ll_t *lv_group_ll = &lv_group_get_default()->obj_ll; + // tweak to allow multiple lines in single line text area + for (int i = 0; i < strlen(msg); i++) + if (msg[i] == CR_REPLACEMENT) + msg[i] = '\n'; - p->user_data = (void *)(uint32_t)ch; - nodes[nodeNum] = p; - nodeCount++; - - // NodePanel - lv_obj_set_pos(p, LV_PCT(0), 0); - lv_obj_set_size(p, LV_PCT(100), 53); - lv_obj_set_align(p, LV_ALIGN_CENTER); - lv_obj_set_style_pad_top(p, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_bottom(p, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_remove_flag(p, lv_obj_flag_t(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_PRESS_LOCK | LV_OBJ_FLAG_CLICK_FOCUSABLE | - LV_OBJ_FLAG_GESTURE_BUBBLE | LV_OBJ_FLAG_SNAPPABLE | LV_OBJ_FLAG_SCROLLABLE)); - add_style_node_panel_style(p); - - // NodeImage - lv_obj_t *img = lv_img_create(p); - setNodeImage(nodeNum, role, unmessagable, img); - lv_obj_set_pos(img, -5, 3); - lv_obj_set_size(img, 32, 32); - lv_obj_clear_flag(img, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_radius(img, 6, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(img, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_opa(img, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(img, 2, LV_PART_MAIN | LV_STATE_DEFAULT); - if (!hasKey) { - lv_obj_set_style_border_color(img, colorRed, LV_PART_MAIN | LV_STATE_DEFAULT); - } - if (unmessagable) { - // node role icon is not clickable and replaced with a cancelled icon - img->user_data = (void *)eRole::unmessagable; - } else { - img->user_data = (void *)role; - } + controller->sendTextMessage(to, ch, hopLimit, actTime, requestId, usePkc, msg); + addMessage(activeMsgContainer, actTime, requestId, msg, LogMessage::eNone); +} - // NodeButton - lv_obj_t *nodeButton = lv_btn_create(p); - lv_obj_set_pos(nodeButton, 0, 0); - lv_obj_set_size(nodeButton, LV_PCT(106), LV_PCT(100)); - add_style_node_button_style(nodeButton); - lv_obj_set_align(nodeButton, LV_ALIGN_CENTER); - lv_obj_add_flag(nodeButton, LV_OBJ_FLAG_SCROLL_ON_FOCUS); - lv_obj_set_style_shadow_width(nodeButton, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_max_height(nodeButton, 132, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_min_height(nodeButton, 50, LV_PART_MAIN | LV_STATE_DEFAULT); - nodeButton->user_data = _lv_ll_get_tail(lv_group_ll); - - // UserNameLabel - lv_obj_t *ln_lbl = lv_label_create(p); - lv_obj_set_pos(ln_lbl, -5, 35); - lv_obj_set_size(ln_lbl, LV_PCT(80), LV_SIZE_CONTENT); - lv_label_set_long_mode(ln_lbl, LV_LABEL_LONG_SCROLL); - lv_label_set_text(ln_lbl, userLong); - ln_lbl->user_data = (void *)nodeNum; - lv_obj_set_style_align(ln_lbl, LV_ALIGN_TOP_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); - - // UserNameShortLabel - lv_obj_t *sn_lbl = lv_label_create(p); - lv_obj_set_pos(sn_lbl, 30, 10); - lv_obj_set_size(sn_lbl, LV_SIZE_CONTENT, LV_SIZE_CONTENT); - lv_label_set_long_mode(sn_lbl, LV_LABEL_LONG_WRAP); - lv_obj_set_style_align(sn_lbl, LV_ALIGN_TOP_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_font(sn_lbl, &ui_font_montserrat_14, LV_PART_MAIN | LV_STATE_DEFAULT); - // if short name contains only non-printable glyphs replace with short id - if (lv_txt_get_width(userShort, strlen(userShort), &ui_font_montserrat_14, 0) <= 4) { - lv_label_set_text_fmt(sn_lbl, "%04x", nodeNum & 0xffff); - } else { - lv_label_set_text(sn_lbl, userShort); - } - char *modUserShort = lv_label_get_text(sn_lbl); - - // keep a copy of the (4-byte) short name for use in many other widgets - char *userData = (char *)&(sn_lbl->user_data); - userData[0] = modUserShort[0]; - if (userData[0] == 0x00) - userData[0] = ' '; - userData[1] = modUserShort[1]; - if (userData[1] == 0x00) - userData[1] = ' '; - userData[2] = modUserShort[2]; - if (userData[2] == 0x00) - userData[2] = ' '; - userData[3] = modUserShort[3]; - if (userData[3] == 0x00) - userData[3] = ' '; - - // BatteryLabel - lv_obj_t *ui_BatteryLabel = lv_label_create(p); - lv_obj_set_pos(ui_BatteryLabel, 8, 17); - lv_obj_set_size(ui_BatteryLabel, LV_SIZE_CONTENT, LV_SIZE_CONTENT); - lv_obj_set_align(ui_BatteryLabel, LV_ALIGN_TOP_RIGHT); - lv_label_set_text(ui_BatteryLabel, ""); - lv_obj_set_style_text_align(ui_BatteryLabel, LV_TEXT_ALIGN_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); - ui_BatteryLabel->user_data = (void *)hasKey; - // LastHeardLabel - lv_obj_t *ui_lastHeardLabel = lv_label_create(p); - lv_obj_set_pos(ui_lastHeardLabel, 8, 33); - lv_obj_set_size(ui_lastHeardLabel, LV_SIZE_CONTENT, LV_SIZE_CONTENT); - lv_obj_set_style_align(ui_lastHeardLabel, LV_ALIGN_TOP_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_label_set_long_mode(ui_lastHeardLabel, LV_LABEL_LONG_CLIP); - - // TODO: devices without actual time will report all nodes as lastseen = now - if (lastHeard) { - lastHeard = std::min(curtime, (time_t)lastHeard); // adapt values too large +/** + * display message that has just been written and sent out + */ +void TFTView_320x240::addMessage(lv_obj_t *container, uint32_t msgTime, uint32_t requestId, char *msg, + LogMessage::MsgStatus status) +{ + lv_obj_t *hiddenPanel = lv_obj_create(container); + lv_obj_set_width(hiddenPanel, lv_pct(100)); + lv_obj_set_height(hiddenPanel, LV_SIZE_CONTENT); + lv_obj_set_align(hiddenPanel, LV_ALIGN_CENTER); + lv_obj_clear_flag(hiddenPanel, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_radius(hiddenPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + add_style_panel_style(hiddenPanel); + + lv_obj_set_style_border_width(hiddenPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_left(hiddenPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_right(hiddenPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_top(hiddenPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_bottom(hiddenPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + hiddenPanel->user_data = (void *)requestId; + + // add timestamp + char buf[284]; // 237 + 4 + 40 + 2 + 1 + buf[0] = '\0'; + uint32_t len = timestamp(buf, msgTime, status == LogMessage::eNone); + strcat(&buf[len], msg); + + lv_obj_t *textLabel = lv_label_create(hiddenPanel); + // calculate expected size of text bubble, to make it look nicer + lv_coord_t width = lv_txt_get_width(buf, strlen(buf), &ui_font_montserrat_12, 0); + lv_obj_set_width(textLabel, std::max(std::min(width, 200) + 10, 40)); + lv_obj_set_height(textLabel, LV_SIZE_CONTENT); + lv_obj_set_y(textLabel, 0); + lv_obj_set_align(textLabel, LV_ALIGN_RIGHT_MID); + lv_label_set_text(textLabel, buf); + + add_style_chat_message_style(textLabel); + + lv_obj_scroll_to_view(hiddenPanel, LV_ANIM_ON); + lv_obj_move_foreground(objects.message_input_area); + + switch (status) { + case LogMessage::eHeard: + lv_obj_set_style_border_color(textLabel, colorYellow, LV_PART_MAIN | LV_STATE_DEFAULT); + break; + case LogMessage::eAcked: + lv_obj_set_style_border_color(textLabel, colorBlueGreen, LV_PART_MAIN | LV_STATE_DEFAULT); + break; + case LogMessage::eFailed: + lv_obj_set_style_border_color(textLabel, colorRed, LV_PART_MAIN | LV_STATE_DEFAULT); + break; + default: + break; + } +} - char buf[20]; - bool isOnline = lastHeardToString(lastHeard, buf); - lv_label_set_text(ui_lastHeardLabel, buf); - if (isOnline) { - nodesOnline++; - } - } else { - lv_label_set_text(ui_lastHeardLabel, ""); - } +void TFTView_320x240::addNode(uint32_t nodeNum, uint8_t ch, const char *userShort, const char *userLong, uint32_t lastHeard, + eRole role, bool hasKey, bool unmessagable) +{ + // lv_obj nodesPanel children | user data (4 bytes) + // ================================================== + // [0]: img | role + // [1]: btn | ll group + // [2]: lbl user long | nodeNum + // [3]: lbl user short | userShort (4 chars) + // [4]: lbl battery | hasKey + // [5]: lbl lastHeard | lastHeard / curtime + // [6]: lbl signal (or hops) | hops away + // [7]: lbl position 1 | lat + // [8]: lbl position 2 | lon + // [9]: lbl telemetry 1 | + // [10]: lbl telemetry 2 | iaq + // panel user_data: ch + + ILOG_DEBUG("addNode(%d): num=0x%08x, lastseen=%d, name=%s(%s), role=%d", nodeCount, nodeNum, lastHeard, userLong, userShort, + role); + while (nodeCount >= MAX_NUM_NODES_VIEW) { + purgeNode(nodeNum); + } - lv_obj_set_style_text_align(ui_lastHeardLabel, LV_TEXT_ALIGN_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); - ui_lastHeardLabel->user_data = (void *)lastHeard; - // SignalLabel / hopsAway - lv_obj_t *ui_SignalLabel = lv_label_create(p); - lv_obj_set_width(ui_SignalLabel, LV_SIZE_CONTENT); - lv_obj_set_height(ui_SignalLabel, LV_SIZE_CONTENT); - lv_obj_set_pos(ui_SignalLabel, 8, 1); - lv_obj_set_align(ui_SignalLabel, LV_ALIGN_TOP_RIGHT); - lv_label_set_text(ui_SignalLabel, ""); - lv_obj_set_style_text_color(ui_SignalLabel, lv_color_hex(0xff5ded96), LV_PART_MAIN | LV_STATE_DEFAULT); - ui_SignalLabel->user_data = (void *)-1; // TODO viaMqtt; // used for filtering (applyNodesFilter) - // PositionLabel - lv_obj_t *ui_PositionLabel = lv_label_create(p); - lv_obj_set_pos(ui_PositionLabel, -5, 49); - lv_obj_set_size(ui_PositionLabel, 120, LV_SIZE_CONTENT); - lv_label_set_long_mode(ui_PositionLabel, LV_LABEL_LONG_CLIP); - lv_label_set_text(ui_PositionLabel, ""); - lv_obj_set_style_align(ui_PositionLabel, LV_ALIGN_TOP_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_color(ui_PositionLabel, colorBlueGreen, LV_PART_MAIN | LV_STATE_DEFAULT); - ui_PositionLabel->user_data = 0; // store latitude - // Position2Label - lv_obj_t *ui_Position2Label = lv_label_create(p); - lv_obj_set_pos(ui_Position2Label, -5, 63); - lv_obj_set_size(ui_Position2Label, 108, LV_SIZE_CONTENT); - lv_label_set_long_mode(ui_Position2Label, LV_LABEL_LONG_SCROLL); - lv_label_set_text(ui_Position2Label, ""); - lv_obj_set_style_align(ui_Position2Label, LV_ALIGN_TOP_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); - ui_Position2Label->user_data = 0; // store longitude - // Telemetry1Label - lv_obj_t *ui_Telemetry1Label = lv_label_create(p); - lv_obj_set_pos(ui_Telemetry1Label, 8, 49); - lv_obj_set_size(ui_Telemetry1Label, 130, LV_SIZE_CONTENT); - lv_label_set_long_mode(ui_Telemetry1Label, LV_LABEL_LONG_CLIP); - lv_label_set_text(ui_Telemetry1Label, ""); - lv_obj_set_style_align(ui_Telemetry1Label, LV_ALIGN_TOP_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_align(ui_Telemetry1Label, LV_TEXT_ALIGN_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); - // Telemetry2Label - lv_obj_t *ui_Telemetry2Label = lv_label_create(p); - lv_obj_set_pos(ui_Telemetry2Label, 8, 63); - lv_obj_set_size(ui_Telemetry2Label, 130, LV_SIZE_CONTENT); - lv_label_set_long_mode(ui_Telemetry2Label, LV_LABEL_LONG_CLIP); - lv_label_set_text(ui_Telemetry2Label, ""); - lv_obj_set_style_align(ui_Telemetry2Label, LV_ALIGN_TOP_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_align(ui_Telemetry2Label, LV_TEXT_ALIGN_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); - - // optimisation: hide all 6ix extended labels by default; enable only when set - // lv_obj_add_flag(ui_lastHeardLabel, LV_OBJ_FLAG_HIDDEN); // lastHeard - lv_obj_add_flag(ui_BatteryLabel, LV_OBJ_FLAG_HIDDEN); // Autohide battery - lv_obj_add_flag(ui_SignalLabel, LV_OBJ_FLAG_HIDDEN); // Autohide signal/hops - lv_obj_add_flag(ui_PositionLabel, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(ui_Position2Label, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(ui_Telemetry1Label, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(ui_Telemetry2Label, LV_OBJ_FLAG_HIDDEN); - - lv_obj_add_event_cb(nodeButton, ui_event_NodeButton, LV_EVENT_ALL, (void *)nodeNum); - - // move node into new position within nodePanel - if (lastHeard) { - lv_obj_t **children = objects.nodes_panel->spec_attr->children; - int i = objects.nodes_panel->spec_attr->child_cnt - 1; - while (i > 1) { - if (lastHeard <= (time_t)(children[i - 1]->LV_OBJ_IDX(node_lh_idx)->user_data)) - break; - i--; - } - if (i >= 1 && i < objects.nodes_panel->spec_attr->child_cnt - 1) { - lv_obj_move_to_index(p, i); - // re-arrange the group linked list by moving the new button (now at the tail) into the right position - void *after = children[i + 1]->LV_OBJ_IDX(node_btn_idx)->user_data; - _lv_ll_move_before(lv_group_ll, nodeButton->user_data, after); - } - } + lv_obj_t *p = lv_obj_create(objects.nodes_panel); + lv_ll_t *lv_group_ll = &lv_group_get_default()->obj_ll; - if (!nodesChanged) { - applyNodesFilter(nodeNum); - updateNodesStatus(); - } - } + p->user_data = (void *)(uint32_t)ch; + nodes[nodeNum] = p; + nodeCount++; + + // NodePanel + lv_obj_set_pos(p, LV_PCT(0), 0); + lv_obj_set_size(p, LV_PCT(100), 53); + lv_obj_set_align(p, LV_ALIGN_CENTER); + lv_obj_set_style_pad_top(p, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_bottom(p, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_remove_flag(p, lv_obj_flag_t(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_PRESS_LOCK | LV_OBJ_FLAG_CLICK_FOCUSABLE | + LV_OBJ_FLAG_GESTURE_BUBBLE | LV_OBJ_FLAG_SNAPPABLE | LV_OBJ_FLAG_SCROLLABLE)); + add_style_node_panel_style(p); + + // NodeImage + lv_obj_t *img = lv_img_create(p); + setNodeImage(nodeNum, role, unmessagable, img); + lv_obj_set_pos(img, -5, 3); + lv_obj_set_size(img, 32, 32); + lv_obj_clear_flag(img, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_radius(img, 6, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(img, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_opa(img, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_width(img, 2, LV_PART_MAIN | LV_STATE_DEFAULT); + if (!hasKey) { + lv_obj_set_style_border_color(img, colorRed, LV_PART_MAIN | LV_STATE_DEFAULT); + } + if (unmessagable) { + // node role icon is not clickable and replaced with a cancelled icon + img->user_data = (void *)eRole::unmessagable; + } else { + img->user_data = (void *)role; + } - void TFTView_320x240::setMyInfo(uint32_t nodeNum) - { - ownNode = nodeNum; + // NodeButton + lv_obj_t *nodeButton = lv_btn_create(p); + lv_obj_set_pos(nodeButton, 0, 0); + lv_obj_set_size(nodeButton, LV_PCT(106), LV_PCT(100)); + add_style_node_button_style(nodeButton); + lv_obj_set_align(nodeButton, LV_ALIGN_CENTER); + lv_obj_add_flag(nodeButton, LV_OBJ_FLAG_SCROLL_ON_FOCUS); + lv_obj_set_style_shadow_width(nodeButton, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_max_height(nodeButton, 132, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_min_height(nodeButton, 50, LV_PART_MAIN | LV_STATE_DEFAULT); + nodeButton->user_data = _lv_ll_get_tail(lv_group_ll); + + // UserNameLabel + lv_obj_t *ln_lbl = lv_label_create(p); + lv_obj_set_pos(ln_lbl, -5, 35); + lv_obj_set_size(ln_lbl, LV_PCT(80), LV_SIZE_CONTENT); + lv_label_set_long_mode(ln_lbl, LV_LABEL_LONG_SCROLL); + lv_label_set_text(ln_lbl, userLong); + ln_lbl->user_data = (void *)nodeNum; + lv_obj_set_style_align(ln_lbl, LV_ALIGN_TOP_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); + + // UserNameShortLabel + lv_obj_t *sn_lbl = lv_label_create(p); + lv_obj_set_pos(sn_lbl, 30, 10); + lv_obj_set_size(sn_lbl, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_label_set_long_mode(sn_lbl, LV_LABEL_LONG_WRAP); + lv_obj_set_style_align(sn_lbl, LV_ALIGN_TOP_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_font(sn_lbl, &ui_font_montserrat_14, LV_PART_MAIN | LV_STATE_DEFAULT); + // if short name contains only non-printable glyphs replace with short id + if (lv_txt_get_width(userShort, strlen(userShort), &ui_font_montserrat_14, 0) <= 4) { + lv_label_set_text_fmt(sn_lbl, "%04x", nodeNum & 0xffff); + } else { + lv_label_set_text(sn_lbl, userShort); + } + char *modUserShort = lv_label_get_text(sn_lbl); + + // keep a copy of the (4-byte) short name for use in many other widgets + char *userData = (char *)&(sn_lbl->user_data); + userData[0] = modUserShort[0]; + if (userData[0] == 0x00) + userData[0] = ' '; + userData[1] = modUserShort[1]; + if (userData[1] == 0x00) + userData[1] = ' '; + userData[2] = modUserShort[2]; + if (userData[2] == 0x00) + userData[2] = ' '; + userData[3] = modUserShort[3]; + if (userData[3] == 0x00) + userData[3] = ' '; + + // BatteryLabel + lv_obj_t *ui_BatteryLabel = lv_label_create(p); + lv_obj_set_pos(ui_BatteryLabel, 8, 17); + lv_obj_set_size(ui_BatteryLabel, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_align(ui_BatteryLabel, LV_ALIGN_TOP_RIGHT); + lv_label_set_text(ui_BatteryLabel, ""); + lv_obj_set_style_text_align(ui_BatteryLabel, LV_TEXT_ALIGN_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); + ui_BatteryLabel->user_data = (void *)hasKey; + // LastHeardLabel + lv_obj_t *ui_lastHeardLabel = lv_label_create(p); + lv_obj_set_pos(ui_lastHeardLabel, 8, 33); + lv_obj_set_size(ui_lastHeardLabel, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_style_align(ui_lastHeardLabel, LV_ALIGN_TOP_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_label_set_long_mode(ui_lastHeardLabel, LV_LABEL_LONG_CLIP); + + // TODO: devices without actual time will report all nodes as lastseen = now + if (lastHeard) { + lastHeard = std::min(curtime, (time_t)lastHeard); // adapt values too large + + char buf[20]; + bool isOnline = lastHeardToString(lastHeard, buf); + lv_label_set_text(ui_lastHeardLabel, buf); + if (isOnline) { + nodesOnline++; } + } else { + lv_label_set_text(ui_lastHeardLabel, ""); + } - void TFTView_320x240::setDeviceMetaData(int hw_model, const char *version, bool has_bluetooth, bool has_wifi, - bool has_eth, bool can_shutdown) - { + lv_obj_set_style_text_align(ui_lastHeardLabel, LV_TEXT_ALIGN_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); + ui_lastHeardLabel->user_data = (void *)lastHeard; + // SignalLabel / hopsAway + lv_obj_t *ui_SignalLabel = lv_label_create(p); + lv_obj_set_width(ui_SignalLabel, LV_SIZE_CONTENT); + lv_obj_set_height(ui_SignalLabel, LV_SIZE_CONTENT); + lv_obj_set_pos(ui_SignalLabel, 8, 1); + lv_obj_set_align(ui_SignalLabel, LV_ALIGN_TOP_RIGHT); + lv_label_set_text(ui_SignalLabel, ""); + lv_obj_set_style_text_color(ui_SignalLabel, lv_color_hex(0xff5ded96), LV_PART_MAIN | LV_STATE_DEFAULT); + ui_SignalLabel->user_data = (void *)-1; // TODO viaMqtt; // used for filtering (applyNodesFilter) + // PositionLabel + lv_obj_t *ui_PositionLabel = lv_label_create(p); + lv_obj_set_pos(ui_PositionLabel, -5, 49); + lv_obj_set_size(ui_PositionLabel, 120, LV_SIZE_CONTENT); + lv_label_set_long_mode(ui_PositionLabel, LV_LABEL_LONG_CLIP); + lv_label_set_text(ui_PositionLabel, ""); + lv_obj_set_style_align(ui_PositionLabel, LV_ALIGN_TOP_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_color(ui_PositionLabel, colorBlueGreen, LV_PART_MAIN | LV_STATE_DEFAULT); + ui_PositionLabel->user_data = 0; // store latitude + // Position2Label + lv_obj_t *ui_Position2Label = lv_label_create(p); + lv_obj_set_pos(ui_Position2Label, -5, 63); + lv_obj_set_size(ui_Position2Label, 108, LV_SIZE_CONTENT); + lv_label_set_long_mode(ui_Position2Label, LV_LABEL_LONG_SCROLL); + lv_label_set_text(ui_Position2Label, ""); + lv_obj_set_style_align(ui_Position2Label, LV_ALIGN_TOP_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); + ui_Position2Label->user_data = 0; // store longitude + // Telemetry1Label + lv_obj_t *ui_Telemetry1Label = lv_label_create(p); + lv_obj_set_pos(ui_Telemetry1Label, 8, 49); + lv_obj_set_size(ui_Telemetry1Label, 130, LV_SIZE_CONTENT); + lv_label_set_long_mode(ui_Telemetry1Label, LV_LABEL_LONG_CLIP); + lv_label_set_text(ui_Telemetry1Label, ""); + lv_obj_set_style_align(ui_Telemetry1Label, LV_ALIGN_TOP_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_align(ui_Telemetry1Label, LV_TEXT_ALIGN_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); + // Telemetry2Label + lv_obj_t *ui_Telemetry2Label = lv_label_create(p); + lv_obj_set_pos(ui_Telemetry2Label, 8, 63); + lv_obj_set_size(ui_Telemetry2Label, 130, LV_SIZE_CONTENT); + lv_label_set_long_mode(ui_Telemetry2Label, LV_LABEL_LONG_CLIP); + lv_label_set_text(ui_Telemetry2Label, ""); + lv_obj_set_style_align(ui_Telemetry2Label, LV_ALIGN_TOP_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_align(ui_Telemetry2Label, LV_TEXT_ALIGN_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); + + // optimisation: hide all 6ix extended labels by default; enable only when set + // lv_obj_add_flag(ui_lastHeardLabel, LV_OBJ_FLAG_HIDDEN); // lastHeard + lv_obj_add_flag(ui_BatteryLabel, LV_OBJ_FLAG_HIDDEN); // Autohide battery + lv_obj_add_flag(ui_SignalLabel, LV_OBJ_FLAG_HIDDEN); // Autohide signal/hops + lv_obj_add_flag(ui_PositionLabel, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(ui_Position2Label, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(ui_Telemetry1Label, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(ui_Telemetry2Label, LV_OBJ_FLAG_HIDDEN); + + lv_obj_add_event_cb(nodeButton, ui_event_NodeButton, LV_EVENT_ALL, (void *)nodeNum); + + // move node into new position within nodePanel + if (lastHeard) { + lv_obj_t **children = objects.nodes_panel->spec_attr->children; + int i = objects.nodes_panel->spec_attr->child_cnt - 1; + while (i > 1) { + if (lastHeard <= (time_t)(children[i - 1]->LV_OBJ_IDX(node_lh_idx)->user_data)) + break; + i--; } - - void TFTView_320x240::addOrUpdateNode(uint32_t nodeNum, uint8_t channel, uint32_t lastHeard, const meshtastic_User &cfg) - { - if (nodes.find(nodeNum) == nodes.end()) { - addNode(nodeNum, channel, cfg.short_name, cfg.long_name, lastHeard, (MeshtasticView::eRole)cfg.role, - cfg.public_key.size != 0, cfg.has_is_unmessagable && cfg.is_unmessagable); - } else { - updateNode(nodeNum, channel, cfg); - } + if (i >= 1 && i < objects.nodes_panel->spec_attr->child_cnt - 1) { + lv_obj_move_to_index(p, i); + // re-arrange the group linked list by moving the new button (now at the tail) into the right position + void *after = children[i + 1]->LV_OBJ_IDX(node_btn_idx)->user_data; + _lv_ll_move_before(lv_group_ll, nodeButton->user_data, after); } + } - /** - * @brief update node userName and image - * - * @param nodeNum - * @param ch - * @param userShort - * @param userLong - * @param lastHeard - * @param role - * @param viaMqtt - */ - // void TFTView_320x240::updateNode(uint32_t nodeNum, uint8_t ch, const char *userShort, const char *userLong, uint32_t - // lastHeard, - // eRole role, bool hasKey, bool viaMqtt) - void TFTView_320x240::updateNode(uint32_t nodeNum, uint8_t ch, const meshtastic_User &cfg) - { - db.user = cfg; - auto it = nodes.find(nodeNum); - if (it != nodes.end() && it->second) { - if (it->first == ownNode) { - // update related settings buttons and store role in image user data - char buf[30]; - lv_snprintf(buf, sizeof(buf), _("User name: %s"), cfg.short_name); - lv_label_set_text(objects.basic_settings_user_label, buf); - - char buf1[30], buf2[40]; - lv_dropdown_set_selected(objects.settings_device_role_dropdown, - role2val(meshtastic_Config_DeviceConfig_Role(cfg.role))); - lv_dropdown_get_selected_str(objects.settings_device_role_dropdown, buf1, sizeof(buf1)); - lv_snprintf(buf2, sizeof(buf2), _("Device Role: %s"), buf1); - lv_label_set_text(objects.basic_settings_role_label, buf2); - - // update DB - strcpy(db.short_name, cfg.short_name); - strcpy(db.long_name, cfg.long_name); - db.config.device.role = cfg.role; - } - lv_label_set_text(it->second->LV_OBJ_IDX(node_lbl_idx), cfg.long_name); - it->second->LV_OBJ_IDX(node_lbl_idx)->user_data = (void *)nodeNum; - lv_label_set_text(it->second->LV_OBJ_IDX(node_lbs_idx), cfg.short_name); - char *userData = (char *)&(it->second->LV_OBJ_IDX(node_lbs_idx)->user_data); - userData[0] = cfg.short_name[0]; - if (userData[0] == 0x00) - userData[0] = ' '; - userData[1] = cfg.short_name[1]; - if (userData[1] == 0x00) - userData[1] = ' '; - userData[2] = cfg.short_name[2]; - if (userData[2] == 0x00) - userData[2] = ' '; - userData[3] = cfg.short_name[3]; - if (userData[3] == 0x00) - userData[3] = ' '; - - setNodeImage(nodeNum, (MeshtasticView::eRole)cfg.role, cfg.has_is_unmessagable && cfg.is_unmessagable, - it->second->LV_OBJ_IDX(node_img_idx)); - - if (cfg.public_key.size != 0) { - // set border color to bg color - lv_color_t color = - lv_obj_get_style_bg_color(it->second->LV_OBJ_IDX(node_img_idx), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(it->second->LV_OBJ_IDX(node_img_idx), color, LV_PART_MAIN | LV_STATE_DEFAULT); - } else { - lv_obj_set_style_border_color(it->second->LV_OBJ_IDX(node_img_idx), colorRed, - LV_PART_MAIN | LV_STATE_DEFAULT); - } + if (!nodesChanged) { + applyNodesFilter(nodeNum); + updateNodesStatus(); + } +} - // update chat name - auto ct = chats.find(it->first); - if (ct != chats.end()) { - char buf[64]; - lv_snprintf(buf, sizeof(buf), "%s: %s", lv_label_get_text(it->second->LV_OBJ_IDX(node_lbs_idx)), - lv_label_get_text(it->second->LV_OBJ_IDX(node_lbl_idx))); - lv_label_set_text(ct->second->spec_attr->children[0], buf); - } - } +void TFTView_320x240::setMyInfo(uint32_t nodeNum) +{ + ownNode = nodeNum; +} + +void TFTView_320x240::setDeviceMetaData(int hw_model, const char *version, bool has_bluetooth, bool has_wifi, bool has_eth, + bool can_shutdown) +{ +} + +void TFTView_320x240::addOrUpdateNode(uint32_t nodeNum, uint8_t channel, uint32_t lastHeard, const meshtastic_User &cfg) +{ + if (nodes.find(nodeNum) == nodes.end()) { + addNode(nodeNum, channel, cfg.short_name, cfg.long_name, lastHeard, (MeshtasticView::eRole)cfg.role, + cfg.public_key.size != 0, cfg.has_is_unmessagable && cfg.is_unmessagable); + } else { + updateNode(nodeNum, channel, cfg); + } +} + +/** + * @brief update node userName and image + * + * @param nodeNum + * @param ch + * @param userShort + * @param userLong + * @param lastHeard + * @param role + * @param viaMqtt + */ +// void TFTView_320x240::updateNode(uint32_t nodeNum, uint8_t ch, const char *userShort, const char *userLong, uint32_t lastHeard, +// eRole role, bool hasKey, bool viaMqtt) +void TFTView_320x240::updateNode(uint32_t nodeNum, uint8_t ch, const meshtastic_User &cfg) +{ + db.user = cfg; + auto it = nodes.find(nodeNum); + if (it != nodes.end() && it->second) { + if (it->first == ownNode) { + // update related settings buttons and store role in image user data + char buf[30]; + lv_snprintf(buf, sizeof(buf), _("User name: %s"), cfg.short_name); + lv_label_set_text(objects.basic_settings_user_label, buf); + + char buf1[30], buf2[40]; + lv_dropdown_set_selected(objects.settings_device_role_dropdown, + role2val(meshtastic_Config_DeviceConfig_Role(cfg.role))); + lv_dropdown_get_selected_str(objects.settings_device_role_dropdown, buf1, sizeof(buf1)); + lv_snprintf(buf2, sizeof(buf2), _("Device Role: %s"), buf1); + lv_label_set_text(objects.basic_settings_role_label, buf2); + + // update DB + strcpy(db.short_name, cfg.short_name); + strcpy(db.long_name, cfg.long_name); + db.config.device.role = cfg.role; + } + lv_label_set_text(it->second->LV_OBJ_IDX(node_lbl_idx), cfg.long_name); + it->second->LV_OBJ_IDX(node_lbl_idx)->user_data = (void *)nodeNum; + lv_label_set_text(it->second->LV_OBJ_IDX(node_lbs_idx), cfg.short_name); + char *userData = (char *)&(it->second->LV_OBJ_IDX(node_lbs_idx)->user_data); + userData[0] = cfg.short_name[0]; + if (userData[0] == 0x00) + userData[0] = ' '; + userData[1] = cfg.short_name[1]; + if (userData[1] == 0x00) + userData[1] = ' '; + userData[2] = cfg.short_name[2]; + if (userData[2] == 0x00) + userData[2] = ' '; + userData[3] = cfg.short_name[3]; + if (userData[3] == 0x00) + userData[3] = ' '; + + setNodeImage(nodeNum, (MeshtasticView::eRole)cfg.role, cfg.has_is_unmessagable && cfg.is_unmessagable, + it->second->LV_OBJ_IDX(node_img_idx)); + + if (cfg.public_key.size != 0) { + // set border color to bg color + lv_color_t color = lv_obj_get_style_bg_color(it->second->LV_OBJ_IDX(node_img_idx), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_color(it->second->LV_OBJ_IDX(node_img_idx), color, LV_PART_MAIN | LV_STATE_DEFAULT); + } else { + lv_obj_set_style_border_color(it->second->LV_OBJ_IDX(node_img_idx), colorRed, LV_PART_MAIN | LV_STATE_DEFAULT); } - void TFTView_320x240::updatePosition(uint32_t nodeNum, int32_t lat, int32_t lon, int32_t alt, uint32_t sats, - uint32_t precision) - { - int32_t altU = abs(alt) < 10000 ? alt : 0; - char units[3] = {}; - if (db.config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_METRIC) { - units[0] = 'm'; - } else { - units[0] = 'f'; - units[1] = 't'; - altU = int32_t(float(altU) * 3.28084); - } - if (nodeNum == ownNode) { - char buf[64]; - int latSeconds = (int)round(lat * 1e-7 * 3600); - int latDegrees = latSeconds / 3600; - latSeconds = abs(latSeconds % 3600); - int latMinutes = latSeconds / 60; - latSeconds %= 60; - char latLetter = (lat > 0) ? 'N' : 'S'; - - int lonSeconds = (int)round(lon * 1e-7 * 3600); - int lonDegrees = lonSeconds / 3600; - lonSeconds = abs(lonSeconds % 3600); - int lonMinutes = lonSeconds / 60; - lonSeconds %= 60; - char lonLetter = (lon > 0) ? 'E' : 'W'; - - if (sats) - sprintf(buf, "%c%02i° %2i'%02i\" %u sats\n%c%02i° %2i'%02i\" %d%s", latLetter, abs(latDegrees), - latMinutes, latSeconds, sats, lonLetter, abs(lonDegrees), lonMinutes, lonSeconds, altU, units); - else - sprintf(buf, "%c%02i° %2i'%02i\"\n%c%02i° %2i'%02i\" %d%s", latLetter, abs(latDegrees), latMinutes, - latSeconds, lonLetter, abs(lonDegrees), lonMinutes, lonSeconds, altU, units); - - lv_label_set_text(objects.home_location_label, buf); - - if (lat != 0 && lon != 0) { - hasPosition = true; - myLatitude = lat; - myLongitude = lon; - - // go through existing node list and update distance - // TODO: need incremental update!? - for (auto &it : nodes) { - if (it.first != ownNode) { - int32_t nlat = (long)it.second->LV_OBJ_IDX(node_pos1_idx)->user_data; - int32_t nlon = (long)it.second->LV_OBJ_IDX(node_pos2_idx)->user_data; - if (nlat != 0 && nlon != 0) { - updateDistance(it.first, nlat, nlon); - } - } - } - // update own location on map - if (map) - map->setGpsPosition(lat * 1e-7, lon * 1e-7); - } - } else { - if (lat != 0 && lon != 0) { - if (hasPosition) { - updateDistance(nodeNum, lat, lon); + // update chat name + auto ct = chats.find(it->first); + if (ct != chats.end()) { + char buf[64]; + lv_snprintf(buf, sizeof(buf), "%s: %s", lv_label_get_text(it->second->LV_OBJ_IDX(node_lbs_idx)), + lv_label_get_text(it->second->LV_OBJ_IDX(node_lbl_idx))); + lv_label_set_text(ct->second->spec_attr->children[0], buf); + } + } +} + +void TFTView_320x240::updatePosition(uint32_t nodeNum, int32_t lat, int32_t lon, int32_t alt, uint32_t sats, uint32_t precision) +{ + int32_t altU = abs(alt) < 10000 ? alt : 0; + char units[3] = {}; + if (db.config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_METRIC) { + units[0] = 'm'; + } else { + units[0] = 'f'; + units[1] = 't'; + altU = int32_t(float(altU) * 3.28084); + } + if (nodeNum == ownNode) { + char buf[64]; + int latSeconds = (int)round(lat * 1e-7 * 3600); + int latDegrees = latSeconds / 3600; + latSeconds = abs(latSeconds % 3600); + int latMinutes = latSeconds / 60; + latSeconds %= 60; + char latLetter = (lat > 0) ? 'N' : 'S'; + + int lonSeconds = (int)round(lon * 1e-7 * 3600); + int lonDegrees = lonSeconds / 3600; + lonSeconds = abs(lonSeconds % 3600); + int lonMinutes = lonSeconds / 60; + lonSeconds %= 60; + char lonLetter = (lon > 0) ? 'E' : 'W'; + + if (sats) + sprintf(buf, "%c%02i° %2i'%02i\" %u sats\n%c%02i° %2i'%02i\" %d%s", latLetter, abs(latDegrees), latMinutes, + latSeconds, sats, lonLetter, abs(lonDegrees), lonMinutes, lonSeconds, altU, units); + else + sprintf(buf, "%c%02i° %2i'%02i\"\n%c%02i° %2i'%02i\" %d%s", latLetter, abs(latDegrees), latMinutes, latSeconds, + lonLetter, abs(lonDegrees), lonMinutes, lonSeconds, altU, units); + + lv_label_set_text(objects.home_location_label, buf); + + if (lat != 0 && lon != 0) { + hasPosition = true; + myLatitude = lat; + myLongitude = lon; + + // go through existing node list and update distance + // TODO: need incremental update!? + for (auto &it : nodes) { + if (it.first != ownNode) { + int32_t nlat = (long)it.second->LV_OBJ_IDX(node_pos1_idx)->user_data; + int32_t nlon = (long)it.second->LV_OBJ_IDX(node_pos2_idx)->user_data; + if (nlat != 0 && nlon != 0) { + updateDistance(it.first, nlat, nlon); } - addOrUpdateMap(nodeNum, lat, lon); } } - - if (lat != 0 && lon != 0) { - char buf[32]; - sprintf(buf, "%.5f %.5f", lat * 1e-7, lon * 1e-7); - lv_obj_t *panel = nodes[nodeNum]; - lv_label_set_text(panel->LV_OBJ_IDX(node_pos1_idx), buf); - if (sats) - sprintf(buf, "%d%s MSL %u sats", altU, units, sats); - sprintf(buf, "%d%s MSL", altU, units); - lv_label_set_text(panel->LV_OBJ_IDX(node_pos2_idx), buf); - // store lat/lon in user_data, because we need these values later to calculate the distance to us - panel->LV_OBJ_IDX(node_pos1_idx)->user_data = (void *)lat; - panel->LV_OBJ_IDX(node_pos2_idx)->user_data = (void *)lon; - lv_obj_remove_flag(panel->LV_OBJ_IDX(node_pos1_idx), LV_OBJ_FLAG_HIDDEN); - lv_obj_remove_flag(panel->LV_OBJ_IDX(node_pos2_idx), LV_OBJ_FLAG_HIDDEN); + // update own location on map + if (map) + map->setGpsPosition(lat * 1e-7, lon * 1e-7); + } + } else { + if (lat != 0 && lon != 0) { + if (hasPosition) { + updateDistance(nodeNum, lat, lon); } - - applyNodesFilter(nodeNum); + addOrUpdateMap(nodeNum, lat, lon); } + } - void TFTView_320x240::updateDistance(uint32_t nodeNum, int32_t lat, int32_t lon) - { - // if we know our position then calculate (simple) distance to other node in km - float dx = 71.5 * 1e-7 * (myLongitude - lon); - float dy = 111.3 * 1e-7 * (myLatitude - lat); - float dist = sqrt(dx * dx + dy * dy); + if (lat != 0 && lon != 0) { + char buf[32]; + sprintf(buf, "%.5f %.5f", lat * 1e-7, lon * 1e-7); + lv_obj_t *panel = nodes[nodeNum]; + lv_label_set_text(panel->LV_OBJ_IDX(node_pos1_idx), buf); + if (sats) + sprintf(buf, "%d%s MSL %u sats", altU, units, sats); + sprintf(buf, "%d%s MSL", altU, units); + lv_label_set_text(panel->LV_OBJ_IDX(node_pos2_idx), buf); + // store lat/lon in user_data, because we need these values later to calculate the distance to us + panel->LV_OBJ_IDX(node_pos1_idx)->user_data = (void *)lat; + panel->LV_OBJ_IDX(node_pos2_idx)->user_data = (void *)lon; + lv_obj_remove_flag(panel->LV_OBJ_IDX(node_pos1_idx), LV_OBJ_FLAG_HIDDEN); + lv_obj_remove_flag(panel->LV_OBJ_IDX(node_pos2_idx), LV_OBJ_FLAG_HIDDEN); + } - // add distance to user short field - char buf[32]; - char *userData = (char *)&(nodes[nodeNum]->LV_OBJ_IDX(node_lbs_idx)->user_data); - buf[0] = userData[0]; - buf[1] = userData[1]; - buf[2] = userData[2]; - buf[3] = userData[3]; - buf[4] = '\n'; - - if (db.config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_METRIC) { - if (dist > 1.0) - sprintf(&buf[5], "%.1f km ", dist); - else - sprintf(&buf[5], "%d m ", (uint32_t)round(dist * 1000)); - } else { - if (dist > 0.1) - sprintf(&buf[5], "%.1f mi ", round(dist * 0.621371)); - else - sprintf(&buf[5], "%d ft ", uint32_t(dist * 3280.84)); - } - // we used the userShort label to add the distance, so re-arrange a bit the position - lv_obj_t *userShort = nodes[nodeNum]->LV_OBJ_IDX(node_lbs_idx); - lv_label_set_text(userShort, buf); - lv_obj_set_pos(userShort, 30, -1); - } - - /** - * @brief Update battery level and air utilisation - * - * @param nodeNum - * @param bat_level - * @param voltage - * @param chUtil - * @param airUtil - */ - void TFTView_320x240::updateMetrics(uint32_t nodeNum, uint32_t bat_level, float voltage, float chUtil, float airUtil) - { - auto it = nodes.find(nodeNum); - if (it != nodes.end()) { - char buf[48]; - if (it->first == ownNode) { - sprintf(buf, _("Util %0.1f%% Air %0.1f%%"), chUtil, airUtil); - lv_label_set_text(it->second->LV_OBJ_IDX(node_sig_idx), buf); - - // update battery percentage and symbol - if (bat_level != 0 || voltage != 0) { - uint32_t shown_level = std::min(bat_level, (uint32_t)100); - sprintf(buf, "%d%%", shown_level); - bool alert = false; - - BatteryLevel level; - BatteryLevel::Status status = level.calcStatus(bat_level, voltage); - switch (status) { - case BatteryLevel::Plugged: - lv_obj_set_style_bg_image_src(objects.battery_image, &img_battery_plug_image, - LV_PART_MAIN | LV_STATE_DEFAULT); - if (shown_level == 100) - buf[0] = '\0'; - break; - case BatteryLevel::Charging: - lv_obj_set_style_bg_image_src(objects.battery_image, &img_battery_bolt_image, - LV_PART_MAIN | LV_STATE_DEFAULT); - break; - case BatteryLevel::Full: - lv_obj_set_style_bg_image_src(objects.battery_image, &img_battery_full_image, - LV_PART_MAIN | LV_STATE_DEFAULT); - break; - case BatteryLevel::Mid: - lv_obj_set_style_bg_image_src(objects.battery_image, &img_battery_mid_image, - LV_PART_MAIN | LV_STATE_DEFAULT); - break; - case BatteryLevel::Low: - lv_obj_set_style_bg_image_src(objects.battery_image, &img_battery_low_image, - LV_PART_MAIN | LV_STATE_DEFAULT); - break; - case BatteryLevel::Empty: - lv_obj_set_style_bg_image_src(objects.battery_image, &img_battery_empty_image, - LV_PART_MAIN | LV_STATE_DEFAULT); - break; - case BatteryLevel::Warn: - lv_obj_set_style_bg_image_src(objects.battery_image, &img_battery_empty_warn_image, - LV_PART_MAIN | LV_STATE_DEFAULT); - buf[0] = '\0'; - alert = true; - break; - default: - ILOG_ERROR("unhandled battery level %d", status); - break; - } - Themes::recolorTopLabel(objects.battery_percentage_label, alert); - lv_obj_set_style_bg_image_recolor_opa(objects.battery_image, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_label_set_text(objects.battery_percentage_label, buf); - } - } + applyNodesFilter(nodeNum); +} + +void TFTView_320x240::updateDistance(uint32_t nodeNum, int32_t lat, int32_t lon) +{ + // if we know our position then calculate (simple) distance to other node in km + float dx = 71.5 * 1e-7 * (myLongitude - lon); + float dy = 111.3 * 1e-7 * (myLatitude - lat); + float dist = sqrt(dx * dx + dy * dy); + + // add distance to user short field + char buf[32]; + char *userData = (char *)&(nodes[nodeNum]->LV_OBJ_IDX(node_lbs_idx)->user_data); + buf[0] = userData[0]; + buf[1] = userData[1]; + buf[2] = userData[2]; + buf[3] = userData[3]; + buf[4] = '\n'; + + if (db.config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_METRIC) { + if (dist > 1.0) + sprintf(&buf[5], "%.1f km ", dist); + else + sprintf(&buf[5], "%d m ", (uint32_t)round(dist * 1000)); + } else { + if (dist > 0.1) + sprintf(&buf[5], "%.1f mi ", round(dist * 0.621371)); + else + sprintf(&buf[5], "%d ft ", uint32_t(dist * 3280.84)); + } + // we used the userShort label to add the distance, so re-arrange a bit the position + lv_obj_t *userShort = nodes[nodeNum]->LV_OBJ_IDX(node_lbs_idx); + lv_label_set_text(userShort, buf); + lv_obj_set_pos(userShort, 30, -1); +} - if (bat_level != 0 || voltage != 0) { - bat_level = std::min(bat_level, (uint32_t)100); - sprintf(buf, "%d%% %0.2fV", bat_level, voltage); - lv_label_set_text(it->second->LV_OBJ_IDX(node_bat_idx), buf); - lv_obj_remove_flag(it->second->LV_OBJ_IDX(node_bat_idx), LV_OBJ_FLAG_HIDDEN); +/** + * @brief Update battery level and air utilisation + * + * @param nodeNum + * @param bat_level + * @param voltage + * @param chUtil + * @param airUtil + */ +void TFTView_320x240::updateMetrics(uint32_t nodeNum, uint32_t bat_level, float voltage, float chUtil, float airUtil) +{ + auto it = nodes.find(nodeNum); + if (it != nodes.end()) { + char buf[48]; + if (it->first == ownNode) { + sprintf(buf, _("Util %0.1f%% Air %0.1f%%"), chUtil, airUtil); + lv_label_set_text(it->second->LV_OBJ_IDX(node_sig_idx), buf); + + // update battery percentage and symbol + if (bat_level != 0 || voltage != 0) { + uint32_t shown_level = std::min(bat_level, (uint32_t)100); + sprintf(buf, "%d%%", shown_level); + bool alert = false; + + BatteryLevel level; + BatteryLevel::Status status = level.calcStatus(bat_level, voltage); + switch (status) { + case BatteryLevel::Plugged: + lv_obj_set_style_bg_image_src(objects.battery_image, &img_battery_plug_image, + LV_PART_MAIN | LV_STATE_DEFAULT); + if (shown_level == 100) + buf[0] = '\0'; + break; + case BatteryLevel::Charging: + lv_obj_set_style_bg_image_src(objects.battery_image, &img_battery_bolt_image, + LV_PART_MAIN | LV_STATE_DEFAULT); + break; + case BatteryLevel::Full: + lv_obj_set_style_bg_image_src(objects.battery_image, &img_battery_full_image, + LV_PART_MAIN | LV_STATE_DEFAULT); + break; + case BatteryLevel::Mid: + lv_obj_set_style_bg_image_src(objects.battery_image, &img_battery_mid_image, LV_PART_MAIN | LV_STATE_DEFAULT); + break; + case BatteryLevel::Low: + lv_obj_set_style_bg_image_src(objects.battery_image, &img_battery_low_image, LV_PART_MAIN | LV_STATE_DEFAULT); + break; + case BatteryLevel::Empty: + lv_obj_set_style_bg_image_src(objects.battery_image, &img_battery_empty_image, + LV_PART_MAIN | LV_STATE_DEFAULT); + break; + case BatteryLevel::Warn: + lv_obj_set_style_bg_image_src(objects.battery_image, &img_battery_empty_warn_image, + LV_PART_MAIN | LV_STATE_DEFAULT); + buf[0] = '\0'; + alert = true; + break; + default: + ILOG_ERROR("unhandled battery level %d", status); + break; } + Themes::recolorTopLabel(objects.battery_percentage_label, alert); + lv_obj_set_style_bg_image_recolor_opa(objects.battery_image, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_label_set_text(objects.battery_percentage_label, buf); } } - void TFTView_320x240::updateEnvironmentMetrics(uint32_t nodeNum, const meshtastic_EnvironmentMetrics &metrics) - { - auto it = nodes.find(nodeNum); - if (it != nodes.end()) { - char buf[50]; - if (db.config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_METRIC) { - if ((int)metrics.relative_humidity > 0) { - sprintf(buf, "%2.1f°C %d%% %3.1fhPa", metrics.temperature, (int)metrics.relative_humidity, - metrics.barometric_pressure); - } else { - sprintf(buf, "%2.1f°C %3.1fhPa", metrics.temperature, metrics.barometric_pressure); - } - } else { - if ((int)metrics.relative_humidity > 0) { - sprintf(buf, "%2.1f°F %d%% %3.1finHg", metrics.temperature * 9 / 5 + 32, (int)metrics.relative_humidity, - metrics.barometric_pressure / 33.86f); - } else { - sprintf(buf, "%2.1f°F %3.1finHg", metrics.temperature * 9 / 5 + 32, metrics.barometric_pressure / 33.86f); - } - } - lv_label_set_text(it->second->LV_OBJ_IDX(node_tm1_idx), buf); - lv_obj_remove_flag(it->second->LV_OBJ_IDX(node_tm1_idx), LV_OBJ_FLAG_HIDDEN); - - if (metrics.iaq > 0 && metrics.iaq < 1000) { - sprintf(buf, "IAQ: %d %.1fV %.1fmA", metrics.iaq, metrics.voltage, metrics.current); - lv_label_set_text(it->second->LV_OBJ_IDX(node_tm2_idx), buf); - it->second->LV_OBJ_IDX(node_tm2_idx)->user_data = (void *)(uint32_t)metrics.iaq; - lv_obj_remove_flag(it->second->LV_OBJ_IDX(node_tm2_idx), LV_OBJ_FLAG_HIDDEN); - } - applyNodesFilter(nodeNum); - } + if (bat_level != 0 || voltage != 0) { + bat_level = std::min(bat_level, (uint32_t)100); + sprintf(buf, "%d%% %0.2fV", bat_level, voltage); + lv_label_set_text(it->second->LV_OBJ_IDX(node_bat_idx), buf); + lv_obj_remove_flag(it->second->LV_OBJ_IDX(node_bat_idx), LV_OBJ_FLAG_HIDDEN); } + } +} - void TFTView_320x240::updateAirQualityMetrics(uint32_t nodeNum, const meshtastic_AirQualityMetrics &metrics) - { - auto it = nodes.find(nodeNum); - if (it != nodes.end() && it->first != ownNode) { - // TODO - // char buf[32]; - // sprintf(buf, "%d %d", metrics.particles_03um, metrics.pm100_environmental); - // lv_label_set_text(it->second->LV_OBJ_IDX(node_tm2_idx), buf); +void TFTView_320x240::updateEnvironmentMetrics(uint32_t nodeNum, const meshtastic_EnvironmentMetrics &metrics) +{ + auto it = nodes.find(nodeNum); + if (it != nodes.end()) { + char buf[50]; + if (db.config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_METRIC) { + if ((int)metrics.relative_humidity > 0) { + sprintf(buf, "%2.1f°C %d%% %3.1fhPa", metrics.temperature, (int)metrics.relative_humidity, + metrics.barometric_pressure); + } else { + sprintf(buf, "%2.1f°C %3.1fhPa", metrics.temperature, metrics.barometric_pressure); } - } - - void TFTView_320x240::updatePowerMetrics(uint32_t nodeNum, const meshtastic_PowerMetrics &metrics) - { - auto it = nodes.find(nodeNum); - if (it != nodes.end() && it->first != ownNode) { - // TODO - // char buf[32]; - // sprintf(buf, "%0.1fmA %0.2fV", metrics.ch1_current, metrics.ch1_voltage); - // lv_label_set_text(it->second->LV_OBJ_IDX(node_tm2_idx), buf); + } else { + if ((int)metrics.relative_humidity > 0) { + sprintf(buf, "%2.1f°F %d%% %3.1finHg", metrics.temperature * 9 / 5 + 32, (int)metrics.relative_humidity, + metrics.barometric_pressure / 33.86f); + } else { + sprintf(buf, "%2.1f°F %3.1finHg", metrics.temperature * 9 / 5 + 32, metrics.barometric_pressure / 33.86f); } } + lv_label_set_text(it->second->LV_OBJ_IDX(node_tm1_idx), buf); + lv_obj_remove_flag(it->second->LV_OBJ_IDX(node_tm1_idx), LV_OBJ_FLAG_HIDDEN); - /** - * update signal strength for direct neighbors - */ - void TFTView_320x240::updateSignalStrength(uint32_t nodeNum, int32_t rssi, float snr) - { - if (nodeNum != ownNode) { - auto it = nodes.find(nodeNum); - if (it != nodes.end()) { - char buf[32]; - if (rssi == 0 && snr == 0.0) { - buf[0] = '\0'; - } else { - sprintf(buf, "rssi: %d snr: %.1f", rssi, snr); - } - lv_label_set_text(it->second->LV_OBJ_IDX(node_sig_idx), buf); - it->second->LV_OBJ_IDX(node_sig_idx)->user_data = 0; - lv_obj_remove_flag(it->second->LV_OBJ_IDX(node_sig_idx), LV_OBJ_FLAG_HIDDEN); - } - } + if (metrics.iaq > 0 && metrics.iaq < 1000) { + sprintf(buf, "IAQ: %d %.1fV %.1fmA", metrics.iaq, metrics.voltage, metrics.current); + lv_label_set_text(it->second->LV_OBJ_IDX(node_tm2_idx), buf); + it->second->LV_OBJ_IDX(node_tm2_idx)->user_data = (void *)(uint32_t)metrics.iaq; + lv_obj_remove_flag(it->second->LV_OBJ_IDX(node_tm2_idx), LV_OBJ_FLAG_HIDDEN); } + applyNodesFilter(nodeNum); + } +} - void TFTView_320x240::updateHopsAway(uint32_t nodeNum, uint8_t hopsAway) - { - if (nodeNum != ownNode) { - auto it = nodes.find(nodeNum); - if (it != nodes.end()) { - char buf[32]; - sprintf(buf, _("hops: %d"), (int)hopsAway); - lv_label_set_text(it->second->LV_OBJ_IDX(node_sig_idx), buf); - it->second->LV_OBJ_IDX(node_sig_idx)->user_data = (void *)(unsigned long)hopsAway; - lv_obj_remove_flag(it->second->LV_OBJ_IDX(node_sig_idx), LV_OBJ_FLAG_HIDDEN); - } +void TFTView_320x240::updateAirQualityMetrics(uint32_t nodeNum, const meshtastic_AirQualityMetrics &metrics) +{ + auto it = nodes.find(nodeNum); + if (it != nodes.end() && it->first != ownNode) { + // TODO + // char buf[32]; + // sprintf(buf, "%d %d", metrics.particles_03um, metrics.pm100_environmental); + // lv_label_set_text(it->second->LV_OBJ_IDX(node_tm2_idx), buf); + } +} + +void TFTView_320x240::updatePowerMetrics(uint32_t nodeNum, const meshtastic_PowerMetrics &metrics) +{ + auto it = nodes.find(nodeNum); + if (it != nodes.end() && it->first != ownNode) { + // TODO + // char buf[32]; + // sprintf(buf, "%0.1fmA %0.2fV", metrics.ch1_current, metrics.ch1_voltage); + // lv_label_set_text(it->second->LV_OBJ_IDX(node_tm2_idx), buf); + } +} + +/** + * update signal strength for direct neighbors + */ +void TFTView_320x240::updateSignalStrength(uint32_t nodeNum, int32_t rssi, float snr) +{ + if (nodeNum != ownNode) { + auto it = nodes.find(nodeNum); + if (it != nodes.end()) { + char buf[32]; + if (rssi == 0 && snr == 0.0) { + buf[0] = '\0'; + } else { + sprintf(buf, "rssi: %d snr: %.1f", rssi, snr); } + lv_label_set_text(it->second->LV_OBJ_IDX(node_sig_idx), buf); + it->second->LV_OBJ_IDX(node_sig_idx)->user_data = 0; + lv_obj_remove_flag(it->second->LV_OBJ_IDX(node_sig_idx), LV_OBJ_FLAG_HIDDEN); } + } +} - void TFTView_320x240::updateConnectionStatus(const meshtastic_DeviceConnectionStatus &status) - { - db.connectionStatus = status; - if (status.has_wifi) { - if (db.config.network.wifi_enabled || db.config.network.eth_enabled) { - if (status.wifi.has_status) { - char buf[20]; - uint32_t ip = status.wifi.status.ip_address; - sprintf(buf, "%d.%d.%d.%d", ip & 0xff, (ip & 0xff00) >> 8, (ip & 0xff0000) >> 16, - (ip & 0xff000000) >> 24); - lv_label_set_text(objects.home_wlan_label, buf); - Themes::recolorButton(objects.home_wlan_button, true); - Themes::recolorText(objects.home_wlan_label, true); - if (status.wifi.status.is_connected) { - lv_obj_set_style_bg_img_src(objects.home_wlan_button, &img_home_wlan_button_image, - LV_PART_MAIN | LV_STATE_DEFAULT); - } else { - lv_obj_set_style_bg_img_src(objects.home_wlan_button, &img_home_wlan_off_image, - LV_PART_MAIN | LV_STATE_DEFAULT); - } +void TFTView_320x240::updateHopsAway(uint32_t nodeNum, uint8_t hopsAway) +{ + if (nodeNum != ownNode) { + auto it = nodes.find(nodeNum); + if (it != nodes.end()) { + char buf[32]; + sprintf(buf, _("hops: %d"), (int)hopsAway); + lv_label_set_text(it->second->LV_OBJ_IDX(node_sig_idx), buf); + it->second->LV_OBJ_IDX(node_sig_idx)->user_data = (void *)(unsigned long)hopsAway; + lv_obj_remove_flag(it->second->LV_OBJ_IDX(node_sig_idx), LV_OBJ_FLAG_HIDDEN); + } + } +} - if (status.wifi.status.is_mqtt_connected) { - Themes::recolorButton(objects.home_mqtt_button, true, 255); - Themes::recolorText(objects.home_mqtt_label, true); - } else { - Themes::recolorButton(objects.home_mqtt_button, db.module_config.mqtt.enabled); - Themes::recolorText(objects.home_mqtt_label, false); - } - } +void TFTView_320x240::updateConnectionStatus(const meshtastic_DeviceConnectionStatus &status) +{ + db.connectionStatus = status; + if (status.has_wifi) { + if (db.config.network.wifi_enabled || db.config.network.eth_enabled) { + if (status.wifi.has_status) { + char buf[20]; + uint32_t ip = status.wifi.status.ip_address; + sprintf(buf, "%d.%d.%d.%d", ip & 0xff, (ip & 0xff00) >> 8, (ip & 0xff0000) >> 16, (ip & 0xff000000) >> 24); + lv_label_set_text(objects.home_wlan_label, buf); + Themes::recolorButton(objects.home_wlan_button, true); + Themes::recolorText(objects.home_wlan_label, true); + if (status.wifi.status.is_connected) { + lv_obj_set_style_bg_img_src(objects.home_wlan_button, &img_home_wlan_button_image, + LV_PART_MAIN | LV_STATE_DEFAULT); } else { - Themes::recolorButton(objects.home_wlan_button, false); - Themes::recolorText(objects.home_wlan_label, false); - if (status.wifi.status.is_mqtt_connected) { - Themes::recolorButton(objects.home_mqtt_button, true, 255); - Themes::recolorText(objects.home_mqtt_label, true); - } else { - Themes::recolorButton(objects.home_mqtt_button, db.module_config.mqtt.enabled, 100); - Themes::recolorText(objects.home_mqtt_label, false); - } lv_obj_set_style_bg_img_src(objects.home_wlan_button, &img_home_wlan_off_image, LV_PART_MAIN | LV_STATE_DEFAULT); } - } else { - lv_obj_add_flag(objects.home_wlan_label, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(objects.home_wlan_button, LV_OBJ_FLAG_HIDDEN); - } - if (status.has_bluetooth) { - if (db.config.bluetooth.enabled) { - if (status.bluetooth.is_connected) { - char buf[20]; - uint32_t mac = ownNode; - lv_obj_set_style_text_color(objects.home_bluetooth_label, colorLightGray, - LV_PART_MAIN | LV_STATE_DEFAULT); - sprintf(buf, "??:??:%02x:%02x:%02x:%02x", mac & 0xff, (mac & 0xff00) >> 8, (mac & 0xff0000) >> 16, - (mac & 0xff000000) >> 24); - lv_label_set_text(objects.home_bluetooth_label, buf); - lv_obj_set_style_bg_opa(objects.home_bluetooth_button, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_img_src(objects.home_bluetooth_button, &img_home_bluetooth_on_button_image, - LV_PART_MAIN | LV_STATE_DEFAULT); - } else { - lv_obj_set_style_text_color(objects.home_bluetooth_label, colorMidGray, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_img_src(objects.home_bluetooth_button, &img_home_bluetooth_on_button_image, - LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_img_recolor_opa(objects.home_bluetooth_button, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - } + if (status.wifi.status.is_mqtt_connected) { + Themes::recolorButton(objects.home_mqtt_button, true, 255); + Themes::recolorText(objects.home_mqtt_label, true); } else { - lv_obj_set_style_text_color(objects.home_bluetooth_label, colorMidGray, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_img_src(objects.home_bluetooth_button, &img_home_bluetooth_off_button_image, - LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_img_recolor_opa(objects.home_bluetooth_button, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + Themes::recolorButton(objects.home_mqtt_button, db.module_config.mqtt.enabled); + Themes::recolorText(objects.home_mqtt_label, false); } - } else { - lv_obj_add_flag(objects.home_bluetooth_label, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(objects.home_bluetooth_button, LV_OBJ_FLAG_HIDDEN); } - - if (status.has_ethernet) { - if (status.ethernet.status.is_connected) { - char buf[20]; - uint32_t mac = ownNode; - sprintf(buf, "??:??:%02x:%02x:%02x:%02x", mac & 0xff000000, mac & 0xff0000, mac & 0xff00, mac & 0xff); - lv_label_set_text(objects.home_ethernet_label, buf); - lv_obj_set_style_text_color(objects.home_ethernet_label, colorLightGray, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(objects.home_ethernet_button, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - } else { - lv_obj_set_style_bg_img_recolor_opa(objects.home_ethernet_button, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_color(objects.home_ethernet_label, colorMidGray, LV_PART_MAIN | LV_STATE_DEFAULT); - } + } else { + Themes::recolorButton(objects.home_wlan_button, false); + Themes::recolorText(objects.home_wlan_label, false); + if (status.wifi.status.is_mqtt_connected) { + Themes::recolorButton(objects.home_mqtt_button, true, 255); + Themes::recolorText(objects.home_mqtt_label, true); } else { - lv_obj_add_flag(objects.home_ethernet_label, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(objects.home_ethernet_button, LV_OBJ_FLAG_HIDDEN); + Themes::recolorButton(objects.home_mqtt_button, db.module_config.mqtt.enabled, 100); + Themes::recolorText(objects.home_mqtt_label, false); } + lv_obj_set_style_bg_img_src(objects.home_wlan_button, &img_home_wlan_off_image, LV_PART_MAIN | LV_STATE_DEFAULT); } + } else { + lv_obj_add_flag(objects.home_wlan_label, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(objects.home_wlan_button, LV_OBJ_FLAG_HIDDEN); + } - // ResponseHandler callbacks - - void TFTView_320x240::onTextMessageCallback(const ResponseHandler::Request &req, ResponseHandler::EventType evt, - int32_t result) - { - ILOG_DEBUG("onTextMessageCallback: %d %d", evt, result); - if (evt == ResponseHandler::found) { - handleTextMessageResponse((unsigned long)req.cookie, req.id, false, result); - } else if (evt == ResponseHandler::removed) { - handleTextMessageResponse((unsigned long)req.cookie, req.id, true, result); + if (status.has_bluetooth) { + if (db.config.bluetooth.enabled) { + if (status.bluetooth.is_connected) { + char buf[20]; + uint32_t mac = ownNode; + lv_obj_set_style_text_color(objects.home_bluetooth_label, colorLightGray, LV_PART_MAIN | LV_STATE_DEFAULT); + sprintf(buf, "??:??:%02x:%02x:%02x:%02x", mac & 0xff, (mac & 0xff00) >> 8, (mac & 0xff0000) >> 16, + (mac & 0xff000000) >> 24); + lv_label_set_text(objects.home_bluetooth_label, buf); + lv_obj_set_style_bg_opa(objects.home_bluetooth_button, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_img_src(objects.home_bluetooth_button, &img_home_bluetooth_on_button_image, + LV_PART_MAIN | LV_STATE_DEFAULT); } else { - ILOG_DEBUG("onTextMessageCallback: timeout!"); + lv_obj_set_style_text_color(objects.home_bluetooth_label, colorMidGray, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_img_src(objects.home_bluetooth_button, &img_home_bluetooth_on_button_image, + LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_img_recolor_opa(objects.home_bluetooth_button, 255, LV_PART_MAIN | LV_STATE_DEFAULT); } + } else { + lv_obj_set_style_text_color(objects.home_bluetooth_label, colorMidGray, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_img_src(objects.home_bluetooth_button, &img_home_bluetooth_off_button_image, + LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_img_recolor_opa(objects.home_bluetooth_button, 255, LV_PART_MAIN | LV_STATE_DEFAULT); } + } else { + lv_obj_add_flag(objects.home_bluetooth_label, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(objects.home_bluetooth_button, LV_OBJ_FLAG_HIDDEN); + } - void TFTView_320x240::onPositionCallback(const ResponseHandler::Request &req, ResponseHandler::EventType evt, int32_t) {} - - void TFTView_320x240::onTracerouteCallback(const ResponseHandler::Request &req, ResponseHandler::EventType evt, int32_t) - { + if (status.has_ethernet) { + if (status.ethernet.status.is_connected) { + char buf[20]; + uint32_t mac = ownNode; + sprintf(buf, "??:??:%02x:%02x:%02x:%02x", mac & 0xff000000, mac & 0xff0000, mac & 0xff00, mac & 0xff); + lv_label_set_text(objects.home_ethernet_label, buf); + lv_obj_set_style_text_color(objects.home_ethernet_label, colorLightGray, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(objects.home_ethernet_button, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + } else { + lv_obj_set_style_bg_img_recolor_opa(objects.home_ethernet_button, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_color(objects.home_ethernet_label, colorMidGray, LV_PART_MAIN | LV_STATE_DEFAULT); } + } else { + lv_obj_add_flag(objects.home_ethernet_label, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(objects.home_ethernet_button, LV_OBJ_FLAG_HIDDEN); + } +} - /** - * handle response from routing - */ - void TFTView_320x240::handleResponse(uint32_t from, const uint32_t id, const meshtastic_Routing &routing, - const meshtastic_MeshPacket &p) - { - ResponseHandler::Request req{}; - bool ack = false; - if (from == ownNode) { - req = requests.findRequest(id); - } else { - req = requests.removeRequest(id); - ack = true; - } +// ResponseHandler callbacks - if (req.type == ResponseHandler::noRequest) { - ILOG_WARN("request id 0x%08x not valid (anymore)", id); - } else { - ILOG_DEBUG("handleResponse request id 0x%08x", id); - } - ILOG_DEBUG("routing tag variant: %d, error: %d", routing.which_variant, routing.error_reason); - switch (routing.which_variant) { - case meshtastic_Routing_error_reason_tag: { - if (routing.error_reason == meshtastic_Routing_Error_NONE) { - if (req.type == ResponseHandler::TraceRouteRequest) { - handleTraceRouteResponse(routing); - } else if (req.type == ResponseHandler::TextMessageRequest) { - handleTextMessageResponse((unsigned long)req.cookie, id, ack, false); - } else if (req.type == ResponseHandler::PositionRequest) { - handlePositionResponse(from, id, p.rx_rssi, p.rx_snr, p.hop_limit == p.hop_start); - } - } else if (routing.error_reason == meshtastic_Routing_Error_MAX_RETRANSMIT) { - ResponseHandler::Request req = requests.removeRequest(id); - if (req.type == ResponseHandler::TraceRouteRequest) { - handleTraceRouteResponse(routing); - } else if (req.type == ResponseHandler::TextMessageRequest) { - handleTextMessageResponse((unsigned long)req.cookie, id, ack, true); - } - } else if (routing.error_reason == meshtastic_Routing_Error_NO_RESPONSE) { - if (req.type == ResponseHandler::PositionRequest) { - handlePositionResponse(from, id, p.rx_rssi, p.rx_snr, p.hop_limit == p.hop_start); - } - } else if (routing.error_reason == meshtastic_Routing_Error_NO_CHANNEL || - routing.error_reason == meshtastic_Routing_Error_PKI_UNKNOWN_PUBKEY) { - if (req.type == ResponseHandler::TextMessageRequest) { - handleTextMessageResponse((unsigned long)req.cookie, id, ack, true); - // we probably have a wrong key; mark it as bad and don't use in future - if ((unsigned long)nodes[from]->LV_OBJ_IDX(node_bat_idx)->user_data == 1) { - ILOG_DEBUG("public key mismatch"); - nodes[from]->LV_OBJ_IDX(node_bat_idx)->user_data = (void *)2; - lv_obj_set_style_border_color(nodes[from]->LV_OBJ_IDX(node_img_idx), colorRed, - LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_image_src(objects.top_messages_node_image, &img_lock_slash_image, - LV_PART_MAIN | LV_STATE_DEFAULT); - } - } - } else { - ILOG_DEBUG("got Routing_Error %d", routing.error_reason); +void TFTView_320x240::onTextMessageCallback(const ResponseHandler::Request &req, ResponseHandler::EventType evt, int32_t result) +{ + ILOG_DEBUG("onTextMessageCallback: %d %d", evt, result); + if (evt == ResponseHandler::found) { + handleTextMessageResponse((unsigned long)req.cookie, req.id, false, result); + } else if (evt == ResponseHandler::removed) { + handleTextMessageResponse((unsigned long)req.cookie, req.id, true, result); + } else { + ILOG_DEBUG("onTextMessageCallback: timeout!"); + } +} + +void TFTView_320x240::onPositionCallback(const ResponseHandler::Request &req, ResponseHandler::EventType evt, int32_t) {} + +void TFTView_320x240::onTracerouteCallback(const ResponseHandler::Request &req, ResponseHandler::EventType evt, int32_t) {} + +/** + * handle response from routing + */ +void TFTView_320x240::handleResponse(uint32_t from, const uint32_t id, const meshtastic_Routing &routing, + const meshtastic_MeshPacket &p) +{ + ResponseHandler::Request req{}; + bool ack = false; + if (from == ownNode) { + req = requests.findRequest(id); + } else { + req = requests.removeRequest(id); + ack = true; + } + + if (req.type == ResponseHandler::noRequest) { + ILOG_WARN("request id 0x%08x not valid (anymore)", id); + } else { + ILOG_DEBUG("handleResponse request id 0x%08x", id); + } + ILOG_DEBUG("routing tag variant: %d, error: %d", routing.which_variant, routing.error_reason); + switch (routing.which_variant) { + case meshtastic_Routing_error_reason_tag: { + if (routing.error_reason == meshtastic_Routing_Error_NONE) { + if (req.type == ResponseHandler::TraceRouteRequest) { + handleTraceRouteResponse(routing); + } else if (req.type == ResponseHandler::TextMessageRequest) { + handleTextMessageResponse((unsigned long)req.cookie, id, ack, false); + } else if (req.type == ResponseHandler::PositionRequest) { + handlePositionResponse(from, id, p.rx_rssi, p.rx_snr, p.hop_limit == p.hop_start); + } + } else if (routing.error_reason == meshtastic_Routing_Error_MAX_RETRANSMIT) { + ResponseHandler::Request req = requests.removeRequest(id); + if (req.type == ResponseHandler::TraceRouteRequest) { + handleTraceRouteResponse(routing); + } else if (req.type == ResponseHandler::TextMessageRequest) { + handleTextMessageResponse((unsigned long)req.cookie, id, ack, true); + } + } else if (routing.error_reason == meshtastic_Routing_Error_NO_RESPONSE) { + if (req.type == ResponseHandler::PositionRequest) { + handlePositionResponse(from, id, p.rx_rssi, p.rx_snr, p.hop_limit == p.hop_start); + } + } else if (routing.error_reason == meshtastic_Routing_Error_NO_CHANNEL || + routing.error_reason == meshtastic_Routing_Error_PKI_UNKNOWN_PUBKEY) { + if (req.type == ResponseHandler::TextMessageRequest) { + handleTextMessageResponse((unsigned long)req.cookie, id, ack, true); + // we probably have a wrong key; mark it as bad and don't use in future + if ((unsigned long)nodes[from]->LV_OBJ_IDX(node_bat_idx)->user_data == 1) { + ILOG_DEBUG("public key mismatch"); + nodes[from]->LV_OBJ_IDX(node_bat_idx)->user_data = (void *)2; + lv_obj_set_style_border_color(nodes[from]->LV_OBJ_IDX(node_img_idx), colorRed, + LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_image_src(objects.top_messages_node_image, &img_lock_slash_image, + LV_PART_MAIN | LV_STATE_DEFAULT); } - break; - } - case meshtastic_Routing_route_request_tag: { - ILOG_ERROR("got meshtastic_Routing_route_request_tag"); - break; - } - case meshtastic_Routing_route_reply_tag: { - ILOG_DEBUG("got meshtastic_Routing_route_reply_tag"); - handleResponse(from, id, routing.route_reply); - break; - } - default: - ILOG_ERROR("unhandled meshtastic_Routing tag"); - break; } + } else { + ILOG_DEBUG("got Routing_Error %d", routing.error_reason); } + break; + } + case meshtastic_Routing_route_request_tag: { + ILOG_ERROR("got meshtastic_Routing_route_request_tag"); + break; + } + case meshtastic_Routing_route_reply_tag: { + ILOG_DEBUG("got meshtastic_Routing_route_reply_tag"); + handleResponse(from, id, routing.route_reply); + break; + } + default: + ILOG_ERROR("unhandled meshtastic_Routing tag"); + break; + } +} - /** - * Signal scanner - */ - void TFTView_320x240::scanSignal(uint32_t scanNo) - { - if (scans == 1 && spinnerButton) { - lv_label_set_text(objects.signal_scanner_start_label, _("Start")); - removeSpinner(); - } else { - uint32_t requestId; - uint32_t to = currentNode; - uint8_t ch = (uint8_t)(unsigned long)currentPanel->user_data; - requestId = requests.addRequest(to, ResponseHandler::PositionRequest, (void *)to); - controller->requestPosition(to, ch, requestId); - objects.signal_scanner_panel->user_data = (void *)requestId; - } - } +/** + * Signal scanner + */ +void TFTView_320x240::scanSignal(uint32_t scanNo) +{ + if (scans == 1 && spinnerButton) { + lv_label_set_text(objects.signal_scanner_start_label, _("Start")); + removeSpinner(); + } else { + uint32_t requestId; + uint32_t to = currentNode; + uint8_t ch = (uint8_t)(unsigned long)currentPanel->user_data; + requestId = requests.addRequest(to, ResponseHandler::PositionRequest, (void *)to); + controller->requestPosition(to, ch, requestId); + objects.signal_scanner_panel->user_data = (void *)requestId; + } +} - void TFTView_320x240::handlePositionResponse(uint32_t from, uint32_t request_id, int32_t rx_rssi, float rx_snr, - bool isNeighbor) - { - if (request_id == (unsigned long)objects.signal_scanner_panel->user_data) { - requests.removeRequest(request_id); - - if (from == currentNode && isNeighbor) { - char buf[20]; - sprintf(buf, "SNR\n%.1f", rx_snr); - lv_label_set_text(objects.signal_scanner_snr_label, buf); - sprintf(buf, "RSSI\n%d", rx_rssi); - lv_label_set_text(objects.signal_scanner_rssi_label, buf); - lv_slider_set_value(objects.snr_slider, rx_snr, LV_ANIM_ON); - lv_slider_set_value(objects.rssi_slider, rx_rssi, LV_ANIM_ON); - sprintf(buf, "%d%%", signalStrength2Percent(rx_rssi, rx_snr)); - lv_label_set_text(objects.signal_scanner_start_label, buf); - } - } else { - ILOG_DEBUG("handlePositionResponse: drop reply with not matching request 0x%08x", request_id); - } +void TFTView_320x240::handlePositionResponse(uint32_t from, uint32_t request_id, int32_t rx_rssi, float rx_snr, bool isNeighbor) +{ + if (request_id == (unsigned long)objects.signal_scanner_panel->user_data) { + requests.removeRequest(request_id); + + if (from == currentNode && isNeighbor) { + char buf[20]; + sprintf(buf, "SNR\n%.1f", rx_snr); + lv_label_set_text(objects.signal_scanner_snr_label, buf); + sprintf(buf, "RSSI\n%d", rx_rssi); + lv_label_set_text(objects.signal_scanner_rssi_label, buf); + lv_slider_set_value(objects.snr_slider, rx_snr, LV_ANIM_ON); + lv_slider_set_value(objects.rssi_slider, rx_rssi, LV_ANIM_ON); + sprintf(buf, "%d%%", signalStrength2Percent(rx_rssi, rx_snr)); + lv_label_set_text(objects.signal_scanner_start_label, buf); } + } else { + ILOG_DEBUG("handlePositionResponse: drop reply with not matching request 0x%08x", request_id); + } +} - /** - * Trace Route: handle ack or timeout - */ - void TFTView_320x240::handleTraceRouteResponse(const meshtastic_Routing &routing) - { - ILOG_DEBUG("handleTraceRouteResponse: route has %d hops", routing.route_reply.route_count); - if (routing.error_reason != meshtastic_Routing_Error_NONE) { - lv_label_set_text(objects.trace_route_start_label, _("Start")); - removeSpinner(); - } else { - // we got a first ACK to our route request - if (spinnerButton) { - lv_obj_set_style_outline_color(objects.trace_route_start_button, lv_color_hex(0xDBD251), - LV_PART_MAIN | LV_STATE_DEFAULT); - } - } +/** + * Trace Route: handle ack or timeout + */ +void TFTView_320x240::handleTraceRouteResponse(const meshtastic_Routing &routing) +{ + ILOG_DEBUG("handleTraceRouteResponse: route has %d hops", routing.route_reply.route_count); + if (routing.error_reason != meshtastic_Routing_Error_NONE) { + lv_label_set_text(objects.trace_route_start_label, _("Start")); + removeSpinner(); + } else { + // we got a first ACK to our route request + if (spinnerButton) { + lv_obj_set_style_outline_color(objects.trace_route_start_button, lv_color_hex(0xDBD251), + LV_PART_MAIN | LV_STATE_DEFAULT); } + } +} - void TFTView_320x240::handleResponse(uint32_t from, uint32_t id, const meshtastic_RouteDiscovery &route) - { - ILOG_DEBUG("handleResponse: trace route has %d / %d hops", route.route_count, route.route_back_count); - lv_obj_add_flag(objects.start_button_panel, LV_OBJ_FLAG_HIDDEN); - lv_obj_clear_flag(objects.hop_routes_panel, LV_OBJ_FLAG_HIDDEN); +void TFTView_320x240::handleResponse(uint32_t from, uint32_t id, const meshtastic_RouteDiscovery &route) +{ + ILOG_DEBUG("handleResponse: trace route has %d / %d hops", route.route_count, route.route_back_count); + lv_obj_add_flag(objects.start_button_panel, LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(objects.hop_routes_panel, LV_OBJ_FLAG_HIDDEN); - if (id && requests.findRequest(id).type == ResponseHandler::TraceRouteRequest) { - requests.removeRequest(id); - } + if (id && requests.findRequest(id).type == ResponseHandler::TraceRouteRequest) { + requests.removeRequest(id); + } - for (int i = route.route_count; i > 0; i--) { - addNodeToTraceRoute(route.route[i - 1], objects.route_towards_panel); - } + for (int i = route.route_count; i > 0; i--) { + addNodeToTraceRoute(route.route[i - 1], objects.route_towards_panel); + } - for (int i = 0; i < route.route_back_count; i++) { - addNodeToTraceRoute(route.route_back[i], objects.route_back_panel); - } + for (int i = 0; i < route.route_back_count; i++) { + addNodeToTraceRoute(route.route_back[i], objects.route_back_panel); + } - // route contains only intermediate nodes, so add our node - addNodeToTraceRoute(ownNode, objects.trace_route_panel); - } + // route contains only intermediate nodes, so add our node + addNodeToTraceRoute(ownNode, objects.trace_route_panel); +} - void TFTView_320x240::addNodeToTraceRoute(uint32_t nodeNum, lv_obj_t * panel) +void TFTView_320x240::addNodeToTraceRoute(uint32_t nodeNum, lv_obj_t *panel) +{ + // check if node exists, and get its panel + lv_obj_t *nodePanel = nullptr; + auto it = nodes.find(nodeNum); + if (it != nodes.end()) { + nodePanel = it->second; + } + lv_obj_t *btn = lv_btn_create(panel); + // objects.trace_route_to_button = btn; + lv_obj_set_pos(btn, 0, 0); + lv_obj_set_size(btn, LV_PCT(100), 38); + add_style_settings_button_style(btn); + lv_obj_set_style_align(btn, LV_ALIGN_TOP_MID, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_top(btn, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_bottom(btn, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_radius(btn, 6, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_shadow_width(btn, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_shadow_ofs_y(btn, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_width(btn, 1, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_color(btn, colorMidGray, LV_PART_MAIN | LV_STATE_DEFAULT); + { { - // check if node exists, and get its panel - lv_obj_t *nodePanel = nullptr; - auto it = nodes.find(nodeNum); - if (it != nodes.end()) { - nodePanel = it->second; + lv_obj_t *img = lv_img_create(btn); + if (nodePanel) { + setNodeImage(nodeNum, (MeshtasticView::eRole)(unsigned long)nodePanel->LV_OBJ_IDX(node_img_idx)->user_data, false, + img); + } else { + setNodeImage(0, eRole::unknown, false, img); } - lv_obj_t *btn = lv_btn_create(panel); - // objects.trace_route_to_button = btn; - lv_obj_set_pos(btn, 0, 0); - lv_obj_set_size(btn, LV_PCT(100), 38); - add_style_settings_button_style(btn); - lv_obj_set_style_align(btn, LV_ALIGN_TOP_MID, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_top(btn, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_bottom(btn, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_radius(btn, 6, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_shadow_width(btn, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_shadow_ofs_y(btn, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(btn, 1, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(btn, colorMidGray, LV_PART_MAIN | LV_STATE_DEFAULT); - { - { - lv_obj_t *img = lv_img_create(btn); - if (nodePanel) { - setNodeImage(nodeNum, - (MeshtasticView::eRole)(unsigned long)nodePanel->LV_OBJ_IDX(node_img_idx)->user_data, false, - img); - } else { - setNodeImage(0, eRole::unknown, false, img); - } - lv_obj_set_pos(img, -5, 3); - lv_obj_set_size(img, 32, 32); - lv_obj_clear_flag(img, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_border_width(img, 3, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_image_recolor_opa(img, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_align(img, LV_ALIGN_TOP_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_radius(img, 6, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(img, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - } - { - // TraceRouteToButtonLabel - lv_obj_t *label = lv_label_create(btn); - lv_obj_set_pos(label, 35, 10); - lv_obj_set_size(label, LV_PCT(80), LV_SIZE_CONTENT); - lv_label_set_long_mode(label, LV_LABEL_LONG_SCROLL); - if (nodePanel) { - if (nodeNum != ownNode) { - lv_obj_add_event_cb(btn, ui_event_trace_route_node, LV_EVENT_CLICKED, nodePanel); - lv_label_set_text(label, lv_label_get_text(nodePanel->LV_OBJ_IDX(node_lbs_idx))); - if (strlen(lv_label_get_text(label)) >= 5) - lv_obj_set_pos(label, 35, -1); - } else { - lv_label_set_text(label, lv_label_get_text(nodePanel->LV_OBJ_IDX(node_lbl_idx))); - } - } else { - char buf[20]; - if (nodeNum != UINT32_MAX) { - lv_snprintf(buf, 16, "!%08x", nodeNum); - lv_label_set_text(label, buf); - } else - lv_label_set_text(label, _("unknown")); - } - lv_obj_set_style_align(label, LV_ALIGN_TOP_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_pos(img, -5, 3); + lv_obj_set_size(img, 32, 32); + lv_obj_clear_flag(img, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_border_width(img, 3, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_image_recolor_opa(img, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_align(img, LV_ALIGN_TOP_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_radius(img, 6, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(img, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + } + { + // TraceRouteToButtonLabel + lv_obj_t *label = lv_label_create(btn); + lv_obj_set_pos(label, 35, 10); + lv_obj_set_size(label, LV_PCT(80), LV_SIZE_CONTENT); + lv_label_set_long_mode(label, LV_LABEL_LONG_SCROLL); + if (nodePanel) { + if (nodeNum != ownNode) { + lv_obj_add_event_cb(btn, ui_event_trace_route_node, LV_EVENT_CLICKED, nodePanel); + lv_label_set_text(label, lv_label_get_text(nodePanel->LV_OBJ_IDX(node_lbs_idx))); + if (strlen(lv_label_get_text(label)) >= 5) + lv_obj_set_pos(label, 35, -1); + } else { + lv_label_set_text(label, lv_label_get_text(nodePanel->LV_OBJ_IDX(node_lbl_idx))); } + } else { + char buf[20]; + if (nodeNum != UINT32_MAX) { + lv_snprintf(buf, 16, "!%08x", nodeNum); + lv_label_set_text(label, buf); + } else + lv_label_set_text(label, _("unknown")); } + lv_obj_set_style_align(label, LV_ALIGN_TOP_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); } + } +} - /** - * @brief purge oldest node from node list (and all its memory) - * @param nodeNum node that is being added and already contained in nodes[], so don't remove it! - */ - void TFTView_320x240::purgeNode(uint32_t nodeNum) - { - if (nodeCount <= 1) - return; +/** + * @brief purge oldest node from node list (and all its memory) + * @param nodeNum node that is being added and already contained in nodes[], so don't remove it! + */ +void TFTView_320x240::purgeNode(uint32_t nodeNum) +{ + if (nodeCount <= 1) + return; - lv_obj_t **children = objects.nodes_panel->spec_attr->children; - int last = objects.nodes_panel->spec_attr->child_cnt - 1; - int i = last; + lv_obj_t **children = objects.nodes_panel->spec_attr->children; + int last = objects.nodes_panel->spec_attr->child_cnt - 1; + int i = last; #ifndef ALWAYS_PURGE_OLDEST_NODE - time_t curr_time; + time_t curr_time; #ifdef ARCH_PORTDUINO - time(&curr_time); + time(&curr_time); #else - curr_time = actTime; + curr_time = actTime; #endif - // prefer purging older unknown nodes first (but not the brand new ones) - while ((eRole)(long)(children[i]->LV_OBJ_IDX(node_img_idx)->user_data) != eRole::unknown || - curr_time < (time_t)(children[i]->LV_OBJ_IDX(node_lh_idx)->user_data) + 120 || - (unsigned long)(children[i]->LV_OBJ_IDX(node_lbl_idx)->user_data) == nodeNum || - chats.find((unsigned long)(children[i]->LV_OBJ_IDX(node_lbl_idx)->user_data)) != chats.end()) { - if (i < (last + 1) / 5) { // keep 80% named nodes and 20% unknown (not fresh) nodes - i = last; - break; - } - i--; - } + // prefer purging older unknown nodes first (but not the brand new ones) + while ((eRole)(long)(children[i]->LV_OBJ_IDX(node_img_idx)->user_data) != eRole::unknown || + curr_time < (time_t)(children[i]->LV_OBJ_IDX(node_lh_idx)->user_data) + 120 || + (unsigned long)(children[i]->LV_OBJ_IDX(node_lbl_idx)->user_data) == nodeNum || + chats.find((unsigned long)(children[i]->LV_OBJ_IDX(node_lbl_idx)->user_data)) != chats.end()) { + if (i < (last + 1) / 5) { // keep 80% named nodes and 20% unknown (not fresh) nodes + i = last; + break; + } + i--; + } #endif - lv_obj_t *p = children[i]; - uint32_t oldest = (unsigned long)(p->LV_OBJ_IDX(node_lbl_idx)->user_data); - uint32_t lastHeard = (unsigned long)p->LV_OBJ_IDX(node_lh_idx)->user_data; - if (lastHeard > 0 && (curtime - lastHeard <= secs_until_offline)) - nodesOnline--; - - ILOG_INFO("removing oldest node 0x%08x", oldest); - lv_obj_delete(p); - { - auto it = messages.find(oldest); - if (it != messages.end()) { - lv_obj_delete(it->second); - messages.erase(oldest); - } - } + lv_obj_t *p = children[i]; + uint32_t oldest = (unsigned long)(p->LV_OBJ_IDX(node_lbl_idx)->user_data); + uint32_t lastHeard = (unsigned long)p->LV_OBJ_IDX(node_lh_idx)->user_data; + if (lastHeard > 0 && (curtime - lastHeard <= secs_until_offline)) + nodesOnline--; + + ILOG_INFO("removing oldest node 0x%08x", oldest); + lv_obj_delete(p); + { + auto it = messages.find(oldest); + if (it != messages.end()) { + lv_obj_delete(it->second); + messages.erase(oldest); + } + } - { - auto it = chats.find(oldest); - if (it != chats.end()) { - lv_obj_delete(it->second); - chats.erase(oldest); - updateActiveChats(); - } + { + auto it = chats.find(oldest); + if (it != chats.end()) { + lv_obj_delete(it->second); + chats.erase(oldest); + updateActiveChats(); + } + } + removeFromMap(oldest); + nodes.erase(oldest); + nodeCount--; + nodesChanged = true; // flag to force re-apply node filter +} + +/** + * @brief apply enabled filters and highlight node + * + * @param nodeNum + * @param reset : set true when filter has changed (to recalculate number of filtered nodes) + * @return true + * @return false + */ +bool TFTView_320x240::applyNodesFilter(uint32_t nodeNum, bool reset) +{ + lv_obj_t *panel = nodes[nodeNum]; + bool hide = false; + if (nodeNum != ownNode /* && filter.active*/) { // TODO + if (lv_obj_has_state(objects.nodes_filter_unknown_switch, LV_STATE_CHECKED)) { + if (lv_img_get_src(panel->LV_OBJ_IDX(node_img_idx)) == &img_circle_question_image) { + hide = true; } - removeFromMap(oldest); - nodes.erase(oldest); - nodeCount--; - nodesChanged = true; // flag to force re-apply node filter - } - - /** - * @brief apply enabled filters and highlight node - * - * @param nodeNum - * @param reset : set true when filter has changed (to recalculate number of filtered nodes) - * @return true - * @return false - */ - bool TFTView_320x240::applyNodesFilter(uint32_t nodeNum, bool reset) - { - lv_obj_t *panel = nodes[nodeNum]; - bool hide = false; - if (nodeNum != ownNode /* && filter.active*/) { // TODO - if (lv_obj_has_state(objects.nodes_filter_unknown_switch, LV_STATE_CHECKED)) { - if (lv_img_get_src(panel->LV_OBJ_IDX(node_img_idx)) == &img_circle_question_image) { - hide = true; - } - } - if (lv_obj_has_state(objects.nodes_filter_offline_switch, LV_STATE_CHECKED)) { - time_t lastHeard = (time_t)panel->LV_OBJ_IDX(node_lh_idx)->user_data; - if (lastHeard == 0 || curtime - lastHeard > secs_until_offline) - hide = true; - } - if (lv_obj_has_state(objects.nodes_filter_public_key_switch, LV_STATE_CHECKED)) { - bool hasKey = (unsigned long)panel->LV_OBJ_IDX(node_bat_idx)->user_data == 1; - if (!hasKey) - hide = true; - } - if (lv_dropdown_get_selected(objects.nodes_filter_channel_dropdown) != 0) { - int selected = lv_dropdown_get_selected(objects.nodes_filter_channel_dropdown); - if (selected != 0) { - uint8_t ch = (uint8_t)(unsigned long)panel->user_data; - if (selected - 1 != ch) - hide = true; - } - } - if (lv_dropdown_get_selected(objects.nodes_filter_hops_dropdown) != 0) { - int32_t hopsAway = (signed long)panel->LV_OBJ_IDX(node_sig_idx)->user_data; - int selected = lv_dropdown_get_selected(objects.nodes_filter_hops_dropdown) - 7; - if (hopsAway < 0) - hide = true; - else if (selected <= 0) { - if (hopsAway > -selected) - hide = true; - } else { - if (hopsAway < selected) - hide = true; - } - } + } + if (lv_obj_has_state(objects.nodes_filter_offline_switch, LV_STATE_CHECKED)) { + time_t lastHeard = (time_t)panel->LV_OBJ_IDX(node_lh_idx)->user_data; + if (lastHeard == 0 || curtime - lastHeard > secs_until_offline) + hide = true; + } + if (lv_obj_has_state(objects.nodes_filter_public_key_switch, LV_STATE_CHECKED)) { + bool hasKey = (unsigned long)panel->LV_OBJ_IDX(node_bat_idx)->user_data == 1; + if (!hasKey) + hide = true; + } + if (lv_dropdown_get_selected(objects.nodes_filter_channel_dropdown) != 0) { + int selected = lv_dropdown_get_selected(objects.nodes_filter_channel_dropdown); + if (selected != 0) { + uint8_t ch = (uint8_t)(unsigned long)panel->user_data; + if (selected - 1 != ch) + hide = true; + } + } + if (lv_dropdown_get_selected(objects.nodes_filter_hops_dropdown) != 0) { + int32_t hopsAway = (signed long)panel->LV_OBJ_IDX(node_sig_idx)->user_data; + int selected = lv_dropdown_get_selected(objects.nodes_filter_hops_dropdown) - 7; + if (hopsAway < 0) + hide = true; + else if (selected <= 0) { + if (hopsAway > -selected) + hide = true; + } else { + if (hopsAway < selected) + hide = true; + } + } #if 0 if (lv_obj_has_state(objects.nodes_filter_mqtt_switch, LV_STATE_CHECKED)) { bool viaMqtt = false; // TODO (unsigned long)panel->LV_OBJ_IDX(node_sig_idx)->user_data; @@ -5468,1778 +5550,1838 @@ void TFTView_320x240::ui_event_KeyboardButton(lv_event_t *e) hide = true; } #endif - if (lv_obj_has_state(objects.nodes_filter_position_switch, LV_STATE_CHECKED)) { - if (lv_label_get_text(panel->LV_OBJ_IDX(node_pos1_idx))[0] == '\0') - hide = true; - } - const char *name = lv_textarea_get_text(objects.nodes_filter_name_area); - if (name[0] != '\0') { - if (name[0] != '!') { // use '!' char to negate search result - if (!strcasestr(lv_label_get_text(panel->LV_OBJ_IDX(node_lbl_idx)), name) && - !strcasestr(lv_label_get_text(panel->LV_OBJ_IDX(node_lbs_idx)), name)) { - hide = true; - } - } else { - if (strcasestr(lv_label_get_text(panel->LV_OBJ_IDX(node_lbl_idx)), &name[1]) || - strcasestr(lv_label_get_text(panel->LV_OBJ_IDX(node_lbs_idx)), &name[1])) { - hide = true; - } - } - } - } - if (hide) { - if (reset || !lv_obj_has_flag(panel, LV_OBJ_FLAG_HIDDEN)) { - lv_obj_add_flag(panel, LV_OBJ_FLAG_HIDDEN); - nodesFiltered++; + if (lv_obj_has_state(objects.nodes_filter_position_switch, LV_STATE_CHECKED)) { + if (lv_label_get_text(panel->LV_OBJ_IDX(node_pos1_idx))[0] == '\0') + hide = true; + } + const char *name = lv_textarea_get_text(objects.nodes_filter_name_area); + if (name[0] != '\0') { + if (name[0] != '!') { // use '!' char to negate search result + if (!strcasestr(lv_label_get_text(panel->LV_OBJ_IDX(node_lbl_idx)), name) && + !strcasestr(lv_label_get_text(panel->LV_OBJ_IDX(node_lbs_idx)), name)) { + hide = true; } } else { - lv_obj_clear_flag(panel, LV_OBJ_FLAG_HIDDEN); - } - - // hide node location if filtered - if (map) - map->update(nodeNum, hide); - - bool highlight = false; - if (true /*highlight.active*/) { // TODO - if (lv_obj_has_state(objects.nodes_hl_active_chat_switch, LV_STATE_CHECKED)) { - auto it = chats.find(nodeNum); - if (it != nodes.end()) { - lv_obj_set_style_border_color(panel, colorOrange, LV_PART_MAIN | LV_STATE_DEFAULT); - highlight = true; - } - } - if (lv_obj_has_state(objects.nodes_hl_position_switch, LV_STATE_CHECKED)) { - if (lv_label_get_text(panel->LV_OBJ_IDX(node_pos1_idx))[0] != '\0') { - lv_obj_set_style_border_color(panel, colorBlueGreen, LV_PART_MAIN | LV_STATE_DEFAULT); - highlight = true; - } - } - if (lv_obj_has_state(objects.nodes_hl_telemetry_switch, LV_STATE_CHECKED)) { - if (lv_label_get_text(panel->LV_OBJ_IDX(node_tm1_idx))[0] != '\0') { - lv_obj_set_style_border_color(panel, colorBlue, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(panel, 2, LV_PART_MAIN | LV_STATE_DEFAULT); - highlight = true; - } - } - if (lv_obj_has_state(objects.nodes_hliaq_switch, LV_STATE_CHECKED)) { - if (lv_label_get_text(panel->LV_OBJ_IDX(node_tm2_idx))[0] != '\0') { - uint32_t iaq = (unsigned long)panel->LV_OBJ_IDX(node_tm2_idx)->user_data; - // IAQ color code - lv_color_t fg, bg; - if (iaq <= 50) { - fg = lv_color_hex(0x00000000); - bg = lv_color_hex(0x000ce810); - } else if (iaq <= 100) { - fg = lv_color_hex(0x00000000); - bg = lv_color_hex(0x00faf646); - } else if (iaq <= 150) { - fg = lv_color_hex(0x00000000); - bg = lv_color_hex(0x00f98204); - } else if (iaq <= 200) { - fg = lv_color_hex(0x00000000); - bg = lv_color_hex(0x00e42104); - } else if (iaq <= 300) { - fg = lv_color_hex(0xffffffff); - bg = lv_color_hex(0x009b2970); - } else { - fg = lv_color_hex(0xffffffff); - bg = lv_color_hex(0x001d1414); - } - lv_obj_set_style_text_color(panel->LV_OBJ_IDX(node_tm2_idx), fg, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(panel->LV_OBJ_IDX(node_tm2_idx), bg, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(panel->LV_OBJ_IDX(node_tm2_idx), 255, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(panel, bg, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(panel, 2, LV_PART_MAIN | LV_STATE_DEFAULT); - highlight = true; - } - } - const char *name = lv_textarea_get_text(objects.nodes_hl_name_area); - if (name[0] != '\0') { - if (strcasestr(lv_label_get_text(panel->LV_OBJ_IDX(node_lbl_idx)), name) || - strcasestr(lv_label_get_text(panel->LV_OBJ_IDX(node_lbs_idx)), name)) { - lv_obj_set_style_border_color(panel, colorMesh, LV_PART_MAIN | LV_STATE_DEFAULT); - highlight = true; - } + if (strcasestr(lv_label_get_text(panel->LV_OBJ_IDX(node_lbl_idx)), &name[1]) || + strcasestr(lv_label_get_text(panel->LV_OBJ_IDX(node_lbs_idx)), &name[1])) { + hide = true; } } - if (!highlight) { - lv_obj_set_style_border_color(panel, colorMidGray, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(panel, 1, LV_PART_MAIN | LV_STATE_DEFAULT); - } - return hide; // TODO || filter.active; } + } + if (hide) { + if (reset || !lv_obj_has_flag(panel, LV_OBJ_FLAG_HIDDEN)) { + lv_obj_add_flag(panel, LV_OBJ_FLAG_HIDDEN); + nodesFiltered++; + } + } else { + lv_obj_clear_flag(panel, LV_OBJ_FLAG_HIDDEN); + } - void TFTView_320x240::messageAlert(const char *alert, bool show) - { - lv_label_set_text(objects.alert_label, alert); - if (show) - lv_obj_clear_flag(objects.alert_panel, LV_OBJ_FLAG_HIDDEN); - else - lv_obj_add_flag(objects.alert_panel, LV_OBJ_FLAG_HIDDEN); - } - - /** - * @brief mark the sent message as either heard or acknowledged or failed - * - * @param channelOrNode - * @param id - * @param ack - */ - void TFTView_320x240::handleTextMessageResponse(uint32_t channelOrNode, const uint32_t id, bool ack, bool err) - { - lv_obj_t *msgContainer; - if (channelOrNode < c_max_channels) { - msgContainer = channelGroup[(uint8_t)channelOrNode]; - ack = true; // treat messages sent to group channel same as ack - } else { - msgContainer = messages[channelOrNode]; - } - if (!msgContainer) { - ILOG_WARN("received unexpected response nodeNum/channel 0x%08x for request id 0x%08x", channelOrNode, id); - return; - } - // go through all hiddenPanels and search for requestId - uint16_t i = msgContainer->spec_attr->child_cnt; - while (i-- > 0) { - lv_obj_t *panel = msgContainer->spec_attr->children[i]; - uint32_t requestId = (unsigned long)panel->user_data; - if (requestId == id) { - // now give the textlabel border another color - lv_obj_t *textLabel = panel->spec_attr->children[0]; - lv_obj_set_style_border_color(textLabel, - err ? colorRed - : ack ? colorBlueGreen - : colorYellow, - LV_PART_MAIN | LV_STATE_DEFAULT); + // hide node location if filtered + if (map) + map->update(nodeNum, hide); - // store message - break; + bool highlight = false; + if (true /*highlight.active*/) { // TODO + if (lv_obj_has_state(objects.nodes_hl_active_chat_switch, LV_STATE_CHECKED)) { + auto it = chats.find(nodeNum); + if (it != nodes.end()) { + lv_obj_set_style_border_color(panel, colorOrange, LV_PART_MAIN | LV_STATE_DEFAULT); + highlight = true; + } + } + if (lv_obj_has_state(objects.nodes_hl_position_switch, LV_STATE_CHECKED)) { + if (lv_label_get_text(panel->LV_OBJ_IDX(node_pos1_idx))[0] != '\0') { + lv_obj_set_style_border_color(panel, colorBlueGreen, LV_PART_MAIN | LV_STATE_DEFAULT); + highlight = true; + } + } + if (lv_obj_has_state(objects.nodes_hl_telemetry_switch, LV_STATE_CHECKED)) { + if (lv_label_get_text(panel->LV_OBJ_IDX(node_tm1_idx))[0] != '\0') { + lv_obj_set_style_border_color(panel, colorBlue, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_width(panel, 2, LV_PART_MAIN | LV_STATE_DEFAULT); + highlight = true; + } + } + if (lv_obj_has_state(objects.nodes_hliaq_switch, LV_STATE_CHECKED)) { + if (lv_label_get_text(panel->LV_OBJ_IDX(node_tm2_idx))[0] != '\0') { + uint32_t iaq = (unsigned long)panel->LV_OBJ_IDX(node_tm2_idx)->user_data; + // IAQ color code + lv_color_t fg, bg; + if (iaq <= 50) { + fg = lv_color_hex(0x00000000); + bg = lv_color_hex(0x000ce810); + } else if (iaq <= 100) { + fg = lv_color_hex(0x00000000); + bg = lv_color_hex(0x00faf646); + } else if (iaq <= 150) { + fg = lv_color_hex(0x00000000); + bg = lv_color_hex(0x00f98204); + } else if (iaq <= 200) { + fg = lv_color_hex(0x00000000); + bg = lv_color_hex(0x00e42104); + } else if (iaq <= 300) { + fg = lv_color_hex(0xffffffff); + bg = lv_color_hex(0x009b2970); + } else { + fg = lv_color_hex(0xffffffff); + bg = lv_color_hex(0x001d1414); } + lv_obj_set_style_text_color(panel->LV_OBJ_IDX(node_tm2_idx), fg, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(panel->LV_OBJ_IDX(node_tm2_idx), bg, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(panel->LV_OBJ_IDX(node_tm2_idx), 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_color(panel, bg, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_width(panel, 2, LV_PART_MAIN | LV_STATE_DEFAULT); + highlight = true; } } - - void TFTView_320x240::packetReceived(const meshtastic_MeshPacket &p) - { - MeshtasticView::packetReceived(p); - - // try update time from packet - if (!VALID_TIME(actTime) && VALID_TIME(p.rx_time)) - updateTime(p.rx_time); - - if (detectorRunning) { - packetDetected(p); - } - if (packetLogEnabled) { - writePacketLog(p); - } - if (p.from != ownNode) { - updateSignalStrength(p.rx_rssi, p.rx_snr); + const char *name = lv_textarea_get_text(objects.nodes_hl_name_area); + if (name[0] != '\0') { + if (strcasestr(lv_label_get_text(panel->LV_OBJ_IDX(node_lbl_idx)), name) || + strcasestr(lv_label_get_text(panel->LV_OBJ_IDX(node_lbs_idx)), name)) { + lv_obj_set_style_border_color(panel, colorMesh, LV_PART_MAIN | LV_STATE_DEFAULT); + highlight = true; } - updateStatistics(p); } + } + if (!highlight) { + lv_obj_set_style_border_color(panel, colorMidGray, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_width(panel, 1, LV_PART_MAIN | LV_STATE_DEFAULT); + } + return hide; // TODO || filter.active; +} - void TFTView_320x240::notifyConnected(const char *info) - { - if (state == MeshtasticView::eBooting) { - updateBootMessage(info); - } else { - if (state == MeshtasticView::eDisconnected) { - messageAlert(_("Connected!"), true); - // force re-sync with node - THIS->controller->setConfigRequested(true); - } - state = MeshtasticView::eRunning; - } - } +void TFTView_320x240::messageAlert(const char *alert, bool show) +{ + lv_label_set_text(objects.alert_label, alert); + if (show) + lv_obj_clear_flag(objects.alert_panel, LV_OBJ_FLAG_HIDDEN); + else + lv_obj_add_flag(objects.alert_panel, LV_OBJ_FLAG_HIDDEN); +} - void TFTView_320x240::notifyDisconnected(const char *info) - { - if (state == MeshtasticView::eBooting) { - updateBootMessage(info); - } else { - if (state == MeshtasticView::eRunning) { - messageAlert(_("Disconnected!"), true); - } - state = MeshtasticView::eDisconnected; - } +/** + * @brief mark the sent message as either heard or acknowledged or failed + * + * @param channelOrNode + * @param id + * @param ack + */ +void TFTView_320x240::handleTextMessageResponse(uint32_t channelOrNode, const uint32_t id, bool ack, bool err) +{ + lv_obj_t *msgContainer; + if (channelOrNode < c_max_channels) { + msgContainer = channelGroup[(uint8_t)channelOrNode]; + ack = true; // treat messages sent to group channel same as ack + } else { + msgContainer = messages[channelOrNode]; + } + if (!msgContainer) { + ILOG_WARN("received unexpected response nodeNum/channel 0x%08x for request id 0x%08x", channelOrNode, id); + return; + } + // go through all hiddenPanels and search for requestId + uint16_t i = msgContainer->spec_attr->child_cnt; + while (i-- > 0) { + lv_obj_t *panel = msgContainer->spec_attr->children[i]; + uint32_t requestId = (unsigned long)panel->user_data; + if (requestId == id) { + // now give the textlabel border another color + lv_obj_t *textLabel = panel->spec_attr->children[0]; + lv_obj_set_style_border_color(textLabel, + err ? colorRed + : ack ? colorBlueGreen + : colorYellow, + LV_PART_MAIN | LV_STATE_DEFAULT); + + // store message + break; } + } +} - void TFTView_320x240::notifyResync(bool show) - { - if (controller->isStandalone()) { - if (show) - notifyReboot(true); - } else { - messageAlert(_("Resync ..."), show); - if (!show) { - lv_screen_load_anim(objects.main_screen, LV_SCR_LOAD_ANIM_NONE, 0, 0, false); - } - } - } +void TFTView_320x240::packetReceived(const meshtastic_MeshPacket &p) +{ + MeshtasticView::packetReceived(p); - void TFTView_320x240::notifyReboot(bool show) - { - messageAlert(_("Rebooting ..."), show); - if (controller->isStandalone()) { - lv_timer_create(timer_event_reboot, 8000, NULL); - } - } + // try update time from packet + if (!VALID_TIME(actTime) && VALID_TIME(p.rx_time)) + updateTime(p.rx_time); - void TFTView_320x240::notifyShutdown(void) - { - messageAlert(_("Shutting down ..."), true); - } + if (detectorRunning) { + packetDetected(p); + } + if (packetLogEnabled) { + writePacketLog(p); + } + if (p.from != ownNode) { + updateSignalStrength(p.rx_rssi, p.rx_snr); + } + updateStatistics(p); +} - void TFTView_320x240::blankScreen(bool enable) - { - ILOG_DEBUG("%s screen (%s)", enable ? "blank" : "unblank", screenLocked ? "locked" : "timeout"); - if (enable) - lv_screen_load_anim(objects.blank_screen, LV_SCR_LOAD_ANIM_FADE_OUT, 1000, 0, false); - else { - if (objects.main_screen) - lv_screen_load_anim(objects.main_screen, LV_SCR_LOAD_ANIM_NONE, 0, 0, false); - else - lv_screen_load_anim(objects.boot_screen, LV_SCR_LOAD_ANIM_NONE, 0, 0, false); - } +void TFTView_320x240::notifyConnected(const char *info) +{ + if (state == MeshtasticView::eBooting) { + updateBootMessage(info); + } else { + if (state == MeshtasticView::eDisconnected) { + messageAlert(_("Connected!"), true); + // force re-sync with node + THIS->controller->setConfigRequested(true); } + state = MeshtasticView::eRunning; + } +} - void TFTView_320x240::screenSaving(bool enabled) - { - if (enabled) { - // overlay main screen with blank screen to prevent accidentally pressing buttons - lv_screen_load_anim(objects.blank_screen, LV_SCR_LOAD_ANIM_FADE_OUT, 0, 0, false); - lv_group_focus_obj(objects.blank_screen_button); - screenLocked = true; - screenUnlockRequest = false; - } else { - if (THIS->db.uiConfig.screen_lock) { - ILOG_DEBUG("showing lock screen"); - lv_screen_load_anim(objects.lock_screen, LV_SCR_LOAD_ANIM_NONE, 0, 0, false); - } else if (objects.main_screen) { - ILOG_DEBUG("showing main screen"); - lv_screen_load_anim(objects.main_screen, LV_SCR_LOAD_ANIM_NONE, 0, 0, false); - if (THIS->activeSettings != eNone) { - lv_event_t e = {.code = LV_EVENT_CLICKED}; - ui_event_cancel(&e); - } - screenLocked = false; - } else { - ILOG_DEBUG("showing boot screen"); - lv_screen_load_anim(objects.boot_screen, LV_SCR_LOAD_ANIM_NONE, 0, 0, false); - screenLocked = false; - } - } +void TFTView_320x240::notifyDisconnected(const char *info) +{ + if (state == MeshtasticView::eBooting) { + updateBootMessage(info); + } else { + if (state == MeshtasticView::eRunning) { + messageAlert(_("Disconnected!"), true); } + state = MeshtasticView::eDisconnected; + } +} - bool TFTView_320x240::isScreenLocked(void) - { - return screenLocked && !screenUnlockRequest; +void TFTView_320x240::notifyResync(bool show) +{ + if (controller->isStandalone()) { + if (show) + notifyReboot(true); + } else { + messageAlert(_("Resync ..."), show); + if (!show) { + lv_screen_load_anim(objects.main_screen, LV_SCR_LOAD_ANIM_NONE, 0, 0, false); } + } +} - void TFTView_320x240::updateChannelConfig(const meshtastic_Channel &ch) - { - static lv_obj_t *btn[c_max_channels] = {objects.channel_button0, objects.channel_button1, objects.channel_button2, - objects.channel_button3, objects.channel_button4, objects.channel_button5, - objects.channel_button6, objects.channel_button7}; - db.channel[ch.index] = ch; - - if (ch.role != meshtastic_Channel_Role_DISABLED) { - setChannelName(ch); - - lv_obj_set_width(btn[ch.index], lv_pct(70)); - lv_obj_set_style_pad_left(btn[ch.index], 8, LV_PART_MAIN | LV_STATE_DEFAULT); - - lv_obj_t *lockImage = NULL; - if (lv_obj_get_child_cnt(btn[ch.index]) == 1) - lockImage = lv_img_create(btn[ch.index]); - else - lockImage = lv_obj_get_child(btn[ch.index], 1); - - uint32_t recolor = 0; - - if (memcmp(ch.settings.psk.bytes, "\001\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000", 16) == 0) { - lv_image_set_src(lockImage, &img_groups_key_image); - recolor = 0xF2E459; // yellow - } else if (memcmp(ch.settings.psk.bytes, "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000", - 16) == 0) { - lv_image_set_src(lockImage, &img_groups_unlock_image); - recolor = 0xF72B2B; // reddish - } else { - lv_image_set_src(lockImage, &img_groups_lock_image); - recolor = 0x1EC174; // green - } - lv_obj_set_width(lockImage, LV_SIZE_CONTENT); /// 1 - lv_obj_set_height(lockImage, LV_SIZE_CONTENT); /// 1 - lv_obj_set_align(lockImage, LV_ALIGN_LEFT_MID); - lv_obj_add_flag(lockImage, LV_OBJ_FLAG_ADV_HITTEST); /// Flags - lv_obj_clear_flag(lockImage, LV_OBJ_FLAG_SCROLLABLE); /// Flags - lv_obj_set_style_img_recolor(lockImage, lv_color_hex(recolor), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_img_recolor_opa(lockImage, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - - lv_obj_t *bellImage = NULL; - if (lv_obj_get_child_cnt(btn[ch.index]) < 3) - bellImage = lv_img_create(btn[ch.index]); - else - bellImage = lv_obj_get_child(btn[ch.index], 2); - lv_obj_set_width(bellImage, LV_SIZE_CONTENT); /// 1 - lv_obj_set_height(bellImage, LV_SIZE_CONTENT); /// 1 - lv_obj_set_align(bellImage, LV_ALIGN_RIGHT_MID); - lv_obj_add_flag(bellImage, LV_OBJ_FLAG_ADV_HITTEST); /// Flags - lv_obj_clear_flag(bellImage, LV_OBJ_FLAG_SCROLLABLE); /// Flags - lv_obj_set_style_img_recolor_opa(bellImage, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - updateGroupChannel(ch.index); - } else { - // display smaller button with just the channel number - char buf[10]; - lv_snprintf(buf, sizeof(buf), "%d", ch.index); - lv_label_set_text(channel[ch.index], buf); - lv_obj_set_width(btn[ch.index], lv_pct(30)); +void TFTView_320x240::notifyReboot(bool show) +{ + messageAlert(_("Rebooting ..."), show); + if (controller->isStandalone()) { + lv_timer_create(timer_event_reboot, 8000, NULL); + } +} - if (lv_obj_get_child_cnt(btn[ch.index]) == 2) { - lv_obj_delete(lv_obj_get_child(btn[ch.index], 1)); - } - } - } +void TFTView_320x240::notifyShutdown(void) +{ + messageAlert(_("Shutting down ..."), true); +} - // redraw bell icons and color - void TFTView_320x240::updateGroupChannel(uint8_t chId) - { - static lv_obj_t *btn[c_max_channels] = {objects.channel_button0, objects.channel_button1, objects.channel_button2, - objects.channel_button3, objects.channel_button4, objects.channel_button5, - objects.channel_button6, objects.channel_button7}; - - lv_obj_t *bellImage = lv_obj_get_child(btn[chId], 2); - if (db.channel[chId].settings.module_settings.is_muted) { - lv_obj_set_style_img_recolor(bellImage, lv_color_hex(0xffab0000), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_image_set_src(bellImage, &img_groups_bell_slash_image); - } else { - Themes::recolorImage(bellImage, true); - lv_image_set_src(bellImage, &img_groups_bell_image); - } +void TFTView_320x240::blankScreen(bool enable) +{ + ILOG_DEBUG("%s screen (%s)", enable ? "blank" : "unblank", screenLocked ? "locked" : "timeout"); + if (enable) + lv_screen_load_anim(objects.blank_screen, LV_SCR_LOAD_ANIM_FADE_OUT, 1000, 0, false); + else { + if (objects.main_screen) + lv_screen_load_anim(objects.main_screen, LV_SCR_LOAD_ANIM_NONE, 0, 0, false); + else + lv_screen_load_anim(objects.boot_screen, LV_SCR_LOAD_ANIM_NONE, 0, 0, false); + } +} + +void TFTView_320x240::screenSaving(bool enabled) +{ + if (enabled) { + // overlay main screen with blank screen to prevent accidentally pressing buttons + lv_screen_load_anim(objects.blank_screen, LV_SCR_LOAD_ANIM_FADE_OUT, 0, 0, false); + lv_group_focus_obj(objects.blank_screen_button); + screenLocked = true; + screenUnlockRequest = false; + } else { + if (THIS->db.uiConfig.screen_lock) { + ILOG_DEBUG("showing lock screen"); + lv_screen_load_anim(objects.lock_screen, LV_SCR_LOAD_ANIM_NONE, 0, 0, false); + } else if (objects.main_screen) { + ILOG_DEBUG("showing main screen"); + lv_screen_load_anim(objects.main_screen, LV_SCR_LOAD_ANIM_NONE, 0, 0, false); + if (THIS->activeSettings != eNone) { + lv_event_t e = {.code = LV_EVENT_CLICKED}; + ui_event_cancel(&e); + } + screenLocked = false; + } else { + ILOG_DEBUG("showing boot screen"); + lv_screen_load_anim(objects.boot_screen, LV_SCR_LOAD_ANIM_NONE, 0, 0, false); + screenLocked = false; } + } +} - void TFTView_320x240::updateDeviceConfig(const meshtastic_Config_DeviceConfig &cfg) - { - db.config.device = cfg; - db.config.has_device = true; +bool TFTView_320x240::isScreenLocked(void) +{ + return screenLocked && !screenUnlockRequest; +} - char buf1[30], buf2[40]; - lv_dropdown_set_selected(objects.settings_device_role_dropdown, role2val(cfg.role)); - lv_dropdown_get_selected_str(objects.settings_device_role_dropdown, buf1, sizeof(buf1)); - lv_snprintf(buf2, sizeof(buf2), _("Device Role: %s"), buf1); - lv_label_set_text(objects.basic_settings_role_label, buf2); - } +void TFTView_320x240::updateChannelConfig(const meshtastic_Channel &ch) +{ + static lv_obj_t *btn[c_max_channels] = {objects.channel_button0, objects.channel_button1, objects.channel_button2, + objects.channel_button3, objects.channel_button4, objects.channel_button5, + objects.channel_button6, objects.channel_button7}; + db.channel[ch.index] = ch; + + if (ch.role != meshtastic_Channel_Role_DISABLED) { + setChannelName(ch); + + lv_obj_set_width(btn[ch.index], lv_pct(70)); + lv_obj_set_style_pad_left(btn[ch.index], 8, LV_PART_MAIN | LV_STATE_DEFAULT); + + lv_obj_t *lockImage = NULL; + if (lv_obj_get_child_cnt(btn[ch.index]) == 1) + lockImage = lv_img_create(btn[ch.index]); + else + lockImage = lv_obj_get_child(btn[ch.index], 1); + + uint32_t recolor = 0; + + if (memcmp(ch.settings.psk.bytes, "\001\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000", 16) == 0) { + lv_image_set_src(lockImage, &img_groups_key_image); + recolor = 0xF2E459; // yellow + } else if (memcmp(ch.settings.psk.bytes, "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000", 16) == 0) { + lv_image_set_src(lockImage, &img_groups_unlock_image); + recolor = 0xF72B2B; // reddish + } else { + lv_image_set_src(lockImage, &img_groups_lock_image); + recolor = 0x1EC174; // green + } + lv_obj_set_width(lockImage, LV_SIZE_CONTENT); /// 1 + lv_obj_set_height(lockImage, LV_SIZE_CONTENT); /// 1 + lv_obj_set_align(lockImage, LV_ALIGN_LEFT_MID); + lv_obj_add_flag(lockImage, LV_OBJ_FLAG_ADV_HITTEST); /// Flags + lv_obj_clear_flag(lockImage, LV_OBJ_FLAG_SCROLLABLE); /// Flags + lv_obj_set_style_img_recolor(lockImage, lv_color_hex(recolor), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_img_recolor_opa(lockImage, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + + lv_obj_t *bellImage = NULL; + if (lv_obj_get_child_cnt(btn[ch.index]) < 3) + bellImage = lv_img_create(btn[ch.index]); + else + bellImage = lv_obj_get_child(btn[ch.index], 2); + lv_obj_set_width(bellImage, LV_SIZE_CONTENT); /// 1 + lv_obj_set_height(bellImage, LV_SIZE_CONTENT); /// 1 + lv_obj_set_align(bellImage, LV_ALIGN_RIGHT_MID); + lv_obj_add_flag(bellImage, LV_OBJ_FLAG_ADV_HITTEST); /// Flags + lv_obj_clear_flag(bellImage, LV_OBJ_FLAG_SCROLLABLE); /// Flags + lv_obj_set_style_img_recolor_opa(bellImage, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + updateGroupChannel(ch.index); + } else { + // display smaller button with just the channel number + char buf[10]; + lv_snprintf(buf, sizeof(buf), "%d", ch.index); + lv_label_set_text(channel[ch.index], buf); + lv_obj_set_width(btn[ch.index], lv_pct(30)); - void TFTView_320x240::updatePositionConfig(const meshtastic_Config_PositionConfig &cfg) - { - db.config.position = cfg; - db.config.has_position = true; - if (cfg.gps_mode != meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) { - if (cfg.fixed_position && db.uiConfig.map_data.has_home) { - updatePosition(ownNode, db.uiConfig.map_data.home.latitude, db.uiConfig.map_data.home.longitude, 0, 0, 0); - } - // grey out text to indicate it's a fixed position vs. actual GPS position - Themes::recolorText(objects.home_location_label, !cfg.fixed_position); - } - Themes::recolorButton(objects.home_location_button, cfg.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED); + if (lv_obj_get_child_cnt(btn[ch.index]) == 2) { + lv_obj_delete(lv_obj_get_child(btn[ch.index], 1)); } + } +} - void TFTView_320x240::updatePowerConfig(const meshtastic_Config_PowerConfig &cfg) - { - db.config.power = cfg; - db.config.has_power = true; - } +// redraw bell icons and color +void TFTView_320x240::updateGroupChannel(uint8_t chId) +{ + static lv_obj_t *btn[c_max_channels] = {objects.channel_button0, objects.channel_button1, objects.channel_button2, + objects.channel_button3, objects.channel_button4, objects.channel_button5, + objects.channel_button6, objects.channel_button7}; + + lv_obj_t *bellImage = lv_obj_get_child(btn[chId], 2); + if (db.channel[chId].settings.module_settings.is_muted) { + lv_obj_set_style_img_recolor(bellImage, lv_color_hex(0xffab0000), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_image_set_src(bellImage, &img_groups_bell_slash_image); + } else { + Themes::recolorImage(bellImage, true); + lv_image_set_src(bellImage, &img_groups_bell_image); + } +} - void TFTView_320x240::updateNetworkConfig(const meshtastic_Config_NetworkConfig &cfg) - { - db.config.network = cfg; - db.config.has_network = true; +void TFTView_320x240::updateDeviceConfig(const meshtastic_Config_DeviceConfig &cfg) +{ + db.config.device = cfg; + db.config.has_device = true; + + char buf1[30], buf2[40]; + lv_dropdown_set_selected(objects.settings_device_role_dropdown, role2val(cfg.role)); + lv_dropdown_get_selected_str(objects.settings_device_role_dropdown, buf1, sizeof(buf1)); + lv_snprintf(buf2, sizeof(buf2), _("Device Role: %s"), buf1); + lv_label_set_text(objects.basic_settings_role_label, buf2); +} - char buf[40]; - lv_snprintf(buf, sizeof(buf), _("WiFi: %s"), cfg.wifi_ssid[0] ? cfg.wifi_ssid : _("")); - lv_label_set_text(objects.basic_settings_wifi_label, buf); - } +void TFTView_320x240::updatePositionConfig(const meshtastic_Config_PositionConfig &cfg) +{ + db.config.position = cfg; + db.config.has_position = true; + if (cfg.gps_mode != meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) { + if (cfg.fixed_position && db.uiConfig.map_data.has_home) { + updatePosition(ownNode, db.uiConfig.map_data.home.latitude, db.uiConfig.map_data.home.longitude, 0, 0, 0); + } + // grey out text to indicate it's a fixed position vs. actual GPS position + Themes::recolorText(objects.home_location_label, !cfg.fixed_position); + } + Themes::recolorButton(objects.home_location_button, cfg.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED); +} - void TFTView_320x240::updateDisplayConfig(const meshtastic_Config_DisplayConfig &cfg) - { - db.config.display = cfg; - db.config.has_display = true; - if (!controller->isStandalone() && cfg.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { - meshtastic_Config_DisplayConfig &display = db.config.display; - display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR; - THIS->controller->sendConfig(meshtastic_Config_DisplayConfig{display}, THIS->ownNode); - } - } +void TFTView_320x240::updatePowerConfig(const meshtastic_Config_PowerConfig &cfg) +{ + db.config.power = cfg; + db.config.has_power = true; +} - void TFTView_320x240::updateLoRaConfig(const meshtastic_Config_LoRaConfig &cfg) - { - db.config.lora = cfg; - db.config.has_lora = true; - - if (cfg.use_preset) { - // This must be run before displaying LoRa frequency as channel of 0 ("calculate from hash") leads to an integer - // underflow - if (!db.config.lora.channel_num) { - db.config.lora.channel_num = LoRaPresets::getDefaultSlot( - db.config.lora.region, THIS->db.config.lora.modem_preset, THIS->db.channel[0].settings.name); - } - char buf1[20], buf2[32]; - lv_dropdown_set_selected(objects.settings_modem_preset_dropdown, preset2val(cfg.modem_preset)); - lv_dropdown_get_selected_str(objects.settings_modem_preset_dropdown, buf1, sizeof(buf1)); - lv_snprintf(buf2, sizeof(buf2), _("Modem Preset: %s"), buf1); - lv_label_set_text(objects.basic_settings_modem_preset_label, buf2); +void TFTView_320x240::updateNetworkConfig(const meshtastic_Config_NetworkConfig &cfg) +{ + db.config.network = cfg; + db.config.has_network = true; - uint32_t numChannels = LoRaPresets::getNumChannels(cfg.region, cfg.modem_preset); - lv_slider_set_range(objects.frequency_slot_slider, 1, numChannels); - lv_slider_set_value(objects.frequency_slot_slider, db.config.lora.channel_num, LV_ANIM_OFF); - } else { - lv_label_set_text(objects.basic_settings_modem_preset_label, _("Modem Preset: custom")); - } + char buf[40]; + lv_snprintf(buf, sizeof(buf), _("WiFi: %s"), cfg.wifi_ssid[0] ? cfg.wifi_ssid : _("")); + lv_label_set_text(objects.basic_settings_wifi_label, buf); +} - char region[30]; - lv_snprintf(region, sizeof(region), _("Region: %s"), LoRaPresets::loRaRegionToString(cfg.region)); - lv_label_set_text(objects.basic_settings_region_label, region); +void TFTView_320x240::updateDisplayConfig(const meshtastic_Config_DisplayConfig &cfg) +{ + db.config.display = cfg; + db.config.has_display = true; + if (!controller->isStandalone() && cfg.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + meshtastic_Config_DisplayConfig &display = db.config.display; + display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR; + THIS->controller->sendConfig(meshtastic_Config_DisplayConfig{display}, THIS->ownNode); + } +} - showLoRaFrequency(db.config.lora); +void TFTView_320x240::updateLoRaConfig(const meshtastic_Config_LoRaConfig &cfg) +{ + db.config.lora = cfg; + db.config.has_lora = true; + + if (cfg.use_preset) { + // This must be run before displaying LoRa frequency as channel of 0 ("calculate from hash") leads to an integer underflow + if (!db.config.lora.channel_num) { + db.config.lora.channel_num = LoRaPresets::getDefaultSlot(db.config.lora.region, THIS->db.config.lora.modem_preset, + THIS->db.channel[0].settings.name); + } + char buf1[20], buf2[32]; + lv_dropdown_set_selected(objects.settings_modem_preset_dropdown, preset2val(cfg.modem_preset)); + lv_dropdown_get_selected_str(objects.settings_modem_preset_dropdown, buf1, sizeof(buf1)); + lv_snprintf(buf2, sizeof(buf2), _("Modem Preset: %s"), buf1); + lv_label_set_text(objects.basic_settings_modem_preset_label, buf2); + + uint32_t numChannels = LoRaPresets::getNumChannels(cfg.region, cfg.modem_preset); + lv_slider_set_range(objects.frequency_slot_slider, 1, numChannels); + lv_slider_set_value(objects.frequency_slot_slider, db.config.lora.channel_num, LV_ANIM_OFF); + } else { + lv_label_set_text(objects.basic_settings_modem_preset_label, _("Modem Preset: custom")); + } - if (db.config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_UNSET) { - // update channel names again now that region is known - for (int i = 0; i < c_max_channels; i++) { - if (db.channel[i].has_settings && db.channel[i].role != meshtastic_Channel_Role_DISABLED) { - setChannelName(db.channel[i]); - } - } - } else { - requestSetup(); - } - } + char region[30]; + lv_snprintf(region, sizeof(region), _("Region: %s"), LoRaPresets::loRaRegionToString(cfg.region)); + lv_label_set_text(objects.basic_settings_region_label, region); - void TFTView_320x240::showLoRaFrequency(const meshtastic_Config_LoRaConfig &cfg) - { - char loraFreq[48]; - if (!cfg.region) { - strcpy(loraFreq, _("region unset")); - } else if (cfg.use_preset) { - float frequency = LoRaPresets::getRadioFreq(cfg.region, cfg.modem_preset, cfg.channel_num) + cfg.frequency_offset; - sprintf(loraFreq, "LoRa %g MHz\n[%s kHz]", frequency, LoRaPresets::getBandwidthString(cfg.modem_preset)); - lv_obj_remove_state(objects.basic_settings_modem_preset_button, LV_STATE_DISABLED); - } else { - float frequency = cfg.override_frequency + cfg.frequency_offset; - sprintf(loraFreq, "LoRa %g MHz\n[%d kHz]", frequency, cfg.bandwidth); - lv_obj_add_state(objects.basic_settings_modem_preset_button, LV_STATE_DISABLED); - } + showLoRaFrequency(db.config.lora); - lv_label_set_text(objects.home_lora_label, loraFreq); - Themes::recolorButton(objects.home_lora_button, cfg.tx_enabled); - Themes::recolorText(objects.home_lora_label, cfg.tx_enabled); - if (!cfg.tx_enabled) { - lv_obj_clear_flag(objects.top_lora_tx_panel, LV_OBJ_FLAG_HIDDEN); - } else { - lv_obj_add_flag(objects.top_lora_tx_panel, LV_OBJ_FLAG_HIDDEN); + if (db.config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_UNSET) { + // update channel names again now that region is known + for (int i = 0; i < c_max_channels; i++) { + if (db.channel[i].has_settings && db.channel[i].role != meshtastic_Channel_Role_DISABLED) { + setChannelName(db.channel[i]); } } + } else { + requestSetup(); + } +} - void TFTView_320x240::setBellText(bool banner, bool sound) - { - if (banner && sound) { - lv_label_set_text(objects.home_bell_label, _("Banner & Sound")); - } else if (banner) { - lv_label_set_text(objects.home_bell_label, _("Banner only")); - } else if (sound) { - lv_label_set_text(objects.home_bell_label, _("Sound only")); - } else { - lv_label_set_text(objects.home_bell_label, _("silent")); - } - - char buf[40]; - lv_snprintf(buf, sizeof(buf), _("Message Alert: %s"), - db.module_config.external_notification.alert_message_buzzer - ? (!sound ? _("silent") : ringtone[db.uiConfig.ring_tone_id].name) - : "off"); - lv_label_set_text(objects.basic_settings_alert_label, buf); +void TFTView_320x240::showLoRaFrequency(const meshtastic_Config_LoRaConfig &cfg) +{ + char loraFreq[48]; + if (!cfg.region) { + strcpy(loraFreq, _("region unset")); + } else if (cfg.use_preset) { + float frequency = LoRaPresets::getRadioFreq(cfg.region, cfg.modem_preset, cfg.channel_num) + cfg.frequency_offset; + sprintf(loraFreq, "LoRa %g MHz\n[%s kHz]", frequency, LoRaPresets::getBandwidthString(cfg.modem_preset)); + lv_obj_remove_state(objects.basic_settings_modem_preset_button, LV_STATE_DISABLED); + } else { + float frequency = cfg.override_frequency + cfg.frequency_offset; + sprintf(loraFreq, "LoRa %g MHz\n[%d kHz]", frequency, cfg.bandwidth); + lv_obj_add_state(objects.basic_settings_modem_preset_button, LV_STATE_DISABLED); + } - Themes::recolorButton(objects.home_bell_button, banner || sound); - Themes::recolorText(objects.home_bell_label, banner || sound); - } + lv_label_set_text(objects.home_lora_label, loraFreq); + Themes::recolorButton(objects.home_lora_button, cfg.tx_enabled); + Themes::recolorText(objects.home_lora_label, cfg.tx_enabled); + if (!cfg.tx_enabled) { + lv_obj_clear_flag(objects.top_lora_tx_panel, LV_OBJ_FLAG_HIDDEN); + } else { + lv_obj_add_flag(objects.top_lora_tx_panel, LV_OBJ_FLAG_HIDDEN); + } +} - /** - * auto set primary(secondary) channel name (based on region) - */ - void TFTView_320x240::setChannelName(const meshtastic_Channel &ch) - { - char buf[40]; - if (ch.role == meshtastic_Channel_Role_PRIMARY) { - sprintf(buf, _("Channel: %s"), - strlen(ch.settings.name) ? ch.settings.name - : db.config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET - ? ("") - : LoRaPresets::modemPresetToString(db.config.lora.modem_preset)); - lv_label_set_text(objects.basic_settings_channel_label, buf); - - sprintf(buf, "*%s", - strlen(ch.settings.name) ? ch.settings.name - : db.config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET - ? ("") - : LoRaPresets::modemPresetToString(db.config.lora.modem_preset)); - } else { - if (ch.settings.name[0] == '\0' && ch.settings.psk.size == 1 && ch.settings.psk.bytes[0] == 0x01) { - sprintf(buf, "%s", LoRaPresets::modemPresetToString(db.config.lora.modem_preset)); - } else { - strcpy(buf, ch.settings.name); - } - } +void TFTView_320x240::setBellText(bool banner, bool sound) +{ + if (banner && sound) { + lv_label_set_text(objects.home_bell_label, _("Banner & Sound")); + } else if (banner) { + lv_label_set_text(objects.home_bell_label, _("Banner only")); + } else if (sound) { + lv_label_set_text(objects.home_bell_label, _("Sound only")); + } else { + lv_label_set_text(objects.home_bell_label, _("silent")); + } - lv_label_set_text(channel[ch.index], buf); + char buf[40]; + lv_snprintf(buf, sizeof(buf), _("Message Alert: %s"), + db.module_config.external_notification.alert_message_buzzer + ? (!sound ? _("silent") : ringtone[db.uiConfig.ring_tone_id].name) + : "off"); + lv_label_set_text(objects.basic_settings_alert_label, buf); + + Themes::recolorButton(objects.home_bell_button, banner || sound); + Themes::recolorText(objects.home_bell_label, banner || sound); +} - // rename chat - auto it = chats.find(ch.index); - if (it != chats.end()) { - char buf2[64]; - sprintf(buf2, "%d: %s", (int)ch.index, buf); - lv_label_set_text(it->second->spec_attr->children[0], buf2); - } +/** + * auto set primary(secondary) channel name (based on region) + */ +void TFTView_320x240::setChannelName(const meshtastic_Channel &ch) +{ + char buf[40]; + if (ch.role == meshtastic_Channel_Role_PRIMARY) { + sprintf(buf, _("Channel: %s"), + strlen(ch.settings.name) ? ch.settings.name + : db.config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET + ? ("") + : LoRaPresets::modemPresetToString(db.config.lora.modem_preset)); + lv_label_set_text(objects.basic_settings_channel_label, buf); + + sprintf(buf, "*%s", + strlen(ch.settings.name) ? ch.settings.name + : db.config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET + ? ("") + : LoRaPresets::modemPresetToString(db.config.lora.modem_preset)); + } else { + if (ch.settings.name[0] == '\0' && ch.settings.psk.size == 1 && ch.settings.psk.bytes[0] == 0x01) { + sprintf(buf, "%s", LoRaPresets::modemPresetToString(db.config.lora.modem_preset)); + } else { + strcpy(buf, ch.settings.name); } + } - void TFTView_320x240::backup(uint32_t option) - { + lv_label_set_text(channel[ch.index], buf); + + // rename chat + auto it = chats.find(ch.index); + if (it != chats.end()) { + char buf2[64]; + sprintf(buf2, "%d: %s", (int)ch.index, buf); + lv_label_set_text(it->second->spec_attr->children[0], buf2); + } +} + +void TFTView_320x240::backup(uint32_t option) +{ #if defined(HAS_SDCARD) || defined(HAS_SD_MMC) || defined(ARCH_PORTDUINO) - meshtastic_Config_SecurityConfig_public_key_t &pubkey = db.config.security.public_key; - meshtastic_Config_SecurityConfig_private_key_t &privkey = db.config.security.private_key; + meshtastic_Config_SecurityConfig_public_key_t &pubkey = db.config.security.public_key; + meshtastic_Config_SecurityConfig_private_key_t &privkey = db.config.security.private_key; - std::stringstream path; - path << "/keys/" << std::hex << std::setw(8) << std::setfill('0') << ownNode << ".yml"; + std::stringstream path; + path << "/keys/" << std::hex << std::setw(8) << std::setfill('0') << ownNode << ".yml"; #if defined(ARCH_PORTDUINO) || defined(HAS_SD_MMC) - SDFs.mkdir("/keys"); - File sd = SDFs.open(path.str().c_str(), FILE_WRITE); + SDFs.mkdir("/keys"); + File sd = SDFs.open(path.str().c_str(), FILE_WRITE); #else - SDFs.mkdir("/keys"); - FsFile sd = SDFs.open(path.str().c_str(), O_RDWR | O_CREAT); + SDFs.mkdir("/keys"); + FsFile sd = SDFs.open(path.str().c_str(), O_RDWR | O_CREAT); #endif - if (sd) { - sd.println("config:"); - sd.println(" security:"); - sd.print(" privateKey: base64:"); - sd.println(pskToBase64(privkey.bytes, privkey.size).c_str()); - sd.print(" publicKey: base64:"); - sd.println(pskToBase64(pubkey.bytes, pubkey.size).c_str()); - ILOG_INFO("backup pub/priv keys done."); - } else { - ILOG_ERROR("open file %s for backup failed", path.str().c_str()); - messageAlert(_("Failed to write keys!"), true); - } - sd.close(); + if (sd) { + sd.println("config:"); + sd.println(" security:"); + sd.print(" privateKey: base64:"); + sd.println(pskToBase64(privkey.bytes, privkey.size).c_str()); + sd.print(" publicKey: base64:"); + sd.println(pskToBase64(pubkey.bytes, pubkey.size).c_str()); + ILOG_INFO("backup pub/priv keys done."); + } else { + ILOG_ERROR("open file %s for backup failed", path.str().c_str()); + messageAlert(_("Failed to write keys!"), true); + } + sd.close(); #endif - } +} - void TFTView_320x240::restore(uint32_t option) - { +void TFTView_320x240::restore(uint32_t option) +{ #if defined(HAS_SDCARD) || defined(HAS_SD_MMC) || defined(ARCH_PORTDUINO) - meshtastic_Config_SecurityConfig_public_key_t &pubkey = db.config.security.public_key; - meshtastic_Config_SecurityConfig_private_key_t &privkey = db.config.security.private_key; + meshtastic_Config_SecurityConfig_public_key_t &pubkey = db.config.security.public_key; + meshtastic_Config_SecurityConfig_private_key_t &privkey = db.config.security.private_key; - std::stringstream path; - path << "/keys/" << std::hex << std::setw(8) << std::setfill('0') << ownNode << ".yml"; + std::stringstream path; + path << "/keys/" << std::hex << std::setw(8) << std::setfill('0') << ownNode << ".yml"; #if defined(ARCH_PORTDUINO) || defined(HAS_SD_MMC) - File sd = SDFs.open(path.str().c_str(), FILE_READ); + File sd = SDFs.open(path.str().c_str(), FILE_READ); #else - FsFile sd = SDFs.open(path.str().c_str(), O_RDONLY); + FsFile sd = SDFs.open(path.str().c_str(), O_RDONLY); #endif - if (sd) { - // TODO: improve parsing file contents - sd.readStringUntil('\n'); // config: - sd.readStringUntil('\n'); // security: - String privKey = sd.readStringUntil('\n'); // privateKey: base64: - String pubKey = sd.readStringUntil('\n'); // publicKey: base64: - if (privKey.indexOf("privateKey:") > 0 && pubKey.indexOf("publicKey:") > 0) { - String b64priv = privKey.substring(privKey.lastIndexOf(":") + 1); - String b64pub = pubKey.substring(pubKey.lastIndexOf(":") + 1); - b64priv.trim(); - b64pub.trim(); - if (base64ToPsk(b64priv.c_str(), privkey.bytes, privkey.size) && - base64ToPsk(b64pub.c_str(), pubkey.bytes, pubkey.size) && - controller->sendConfig(meshtastic_Config_SecurityConfig{db.config.security})) { - ILOG_INFO("restore pub/priv keys sent to radio"); - } else { - ILOG_ERROR("decoding keys failed"); - messageAlert(_("Failed to restore keys!"), true); - } - } else { - ILOG_ERROR("file %s contents don't match backup", path.str().c_str()); - messageAlert(_("Failed to parse keys!"), true); - } + if (sd) { + // TODO: improve parsing file contents + sd.readStringUntil('\n'); // config: + sd.readStringUntil('\n'); // security: + String privKey = sd.readStringUntil('\n'); // privateKey: base64: + String pubKey = sd.readStringUntil('\n'); // publicKey: base64: + if (privKey.indexOf("privateKey:") > 0 && pubKey.indexOf("publicKey:") > 0) { + String b64priv = privKey.substring(privKey.lastIndexOf(":") + 1); + String b64pub = pubKey.substring(pubKey.lastIndexOf(":") + 1); + b64priv.trim(); + b64pub.trim(); + if (base64ToPsk(b64priv.c_str(), privkey.bytes, privkey.size) && + base64ToPsk(b64pub.c_str(), pubkey.bytes, pubkey.size) && + controller->sendConfig(meshtastic_Config_SecurityConfig{db.config.security})) { + ILOG_INFO("restore pub/priv keys sent to radio"); } else { - ILOG_ERROR("open file %s failed", path.str().c_str()); - messageAlert(_("Failed to retrieve keys!"), true); + ILOG_ERROR("decoding keys failed"); + messageAlert(_("Failed to restore keys!"), true); } - sd.close(); -#endif + } else { + ILOG_ERROR("file %s contents don't match backup", path.str().c_str()); + messageAlert(_("Failed to parse keys!"), true); } + } else { + ILOG_ERROR("open file %s failed", path.str().c_str()); + messageAlert(_("Failed to retrieve keys!"), true); + } + sd.close(); +#endif +} - /** - * @brief write local time stamp into buffer - * if date is not current also add day/month - * Note: time string ends with linefeed - * - * @param buf allocated buffer - * @param datetime date/time to write - * @param update update with actual time, otherwise using time from parameter 'time' - * @return length of time string - */ - uint32_t TFTView_320x240::timestamp(char *buf, uint32_t datetime, bool update) - { - time_t local = datetime; - if (update) { +/** + * @brief write local time stamp into buffer + * if date is not current also add day/month + * Note: time string ends with linefeed + * + * @param buf allocated buffer + * @param datetime date/time to write + * @param update update with actual time, otherwise using time from parameter 'time' + * @return length of time string + */ +uint32_t TFTView_320x240::timestamp(char *buf, uint32_t datetime, bool update) +{ + time_t local = datetime; + if (update) { #ifdef ARCH_PORTDUINO - time(&local); + time(&local); #else - if (VALID_TIME(actTime)) - local = actTime; + if (VALID_TIME(actTime)) + local = actTime; #endif - } - if (VALID_TIME(local)) { - std::tm date_tm{}; - localtime_r(&local, &date_tm); - if (!update) - return strftime(buf, 20, "%y/%m/%d %R\n", &date_tm); - else - return strftime(buf, 20, "%R\n", &date_tm); - } else - return 0; - } - - /** - * calculate percentage value from rssi and snr - * Note: ranges are based on the axis values of the signal scanner - */ - int32_t TFTView_320x240::signalStrength2Percent(int32_t rx_rssi, float rx_snr) - { + } + if (VALID_TIME(local)) { + std::tm date_tm{}; + localtime_r(&local, &date_tm); + if (!update) + return strftime(buf, 20, "%y/%m/%d %R\n", &date_tm); + else + return strftime(buf, 20, "%R\n", &date_tm); + } else + return 0; +} + +/** + * calculate percentage value from rssi and snr + * Note: ranges are based on the axis values of the signal scanner + */ +int32_t TFTView_320x240::signalStrength2Percent(int32_t rx_rssi, float rx_snr) +{ #if defined(USE_SX127x) - int p_snr = ((std::max(rx_snr, -19.0f) + 19.0f) / 33.0f) * 100.0f; // range -19..14 - int p_rssi = ((std::max(rx_rssi, -145L) + 145) * 100) / 90; // range -145..-55 + int p_snr = ((std::max(rx_snr, -19.0f) + 19.0f) / 33.0f) * 100.0f; // range -19..14 + int p_rssi = ((std::max(rx_rssi, -145L) + 145) * 100) / 90; // range -145..-55 #else - int p_snr = ((std::max(rx_snr, -18.0f) + 18.0f) / 26.0f) * 100.0f; // range -18..8 - int p_rssi = ((std::max(rx_rssi, -125) + 125) * 100) / 100; // range -125..-25 + int p_snr = ((std::max(rx_snr, -18.0f) + 18.0f) / 26.0f) * 100.0f; // range -18..8 + int p_rssi = ((std::max(rx_rssi, -125) + 125) * 100) / 100; // range -125..-25 #endif - return std::min((p_snr + p_rssi * 2) / 3, 100); - } + return std::min((p_snr + p_rssi * 2) / 3, 100); +} - void TFTView_320x240::updateBluetoothConfig(const meshtastic_Config_BluetoothConfig &cfg, uint32_t id) - { - db.config.bluetooth = cfg; - db.config.has_bluetooth = true; +void TFTView_320x240::updateBluetoothConfig(const meshtastic_Config_BluetoothConfig &cfg, uint32_t id) +{ + db.config.bluetooth = cfg; + db.config.has_bluetooth = true; - if (ownNode == 0) { - ownNode = id; - } + if (ownNode == 0) { + ownNode = id; + } - if (state <= MeshtasticView::eBootScreenDone && state != MeshtasticView::eWaitingForReboot) { - enterProgrammingMode(); - } - } + if (state <= MeshtasticView::eBootScreenDone && state != MeshtasticView::eWaitingForReboot) { + enterProgrammingMode(); + } +} - void TFTView_320x240::updateSecurityConfig(const meshtastic_Config_SecurityConfig &cfg) - { - db.config.security = cfg; - db.config.has_security = true; +void TFTView_320x240::updateSecurityConfig(const meshtastic_Config_SecurityConfig &cfg) +{ + db.config.security = cfg; + db.config.has_security = true; - // display public key in qr code label - char buf[64]; - lv_snprintf(buf, sizeof(buf), "%s", pskToBase64((uint8_t *)cfg.public_key.bytes, cfg.public_key.size).c_str()); - lv_label_set_text(objects.home_qr_label, buf); - } + // display public key in qr code label + char buf[64]; + lv_snprintf(buf, sizeof(buf), "%s", pskToBase64((uint8_t *)cfg.public_key.bytes, cfg.public_key.size).c_str()); + lv_label_set_text(objects.home_qr_label, buf); +} - void TFTView_320x240::updateSessionKeyConfig(const meshtastic_Config_SessionkeyConfig &cfg) - { - // TODO - } +void TFTView_320x240::updateSessionKeyConfig(const meshtastic_Config_SessionkeyConfig &cfg) +{ + // TODO +} - /// ---- module updates ---- +/// ---- module updates ---- - void TFTView_320x240::updateMQTTModule(const meshtastic_ModuleConfig_MQTTConfig &cfg) - { - db.module_config.mqtt = cfg; - db.module_config.has_mqtt = true; +void TFTView_320x240::updateMQTTModule(const meshtastic_ModuleConfig_MQTTConfig &cfg) +{ + db.module_config.mqtt = cfg; + db.module_config.has_mqtt = true; - char buf[32]; - lv_snprintf(buf, sizeof(buf), "%s", db.module_config.mqtt.root); - lv_label_set_text(objects.home_mqtt_label, buf); + char buf[32]; + lv_snprintf(buf, sizeof(buf), "%s", db.module_config.mqtt.root); + lv_label_set_text(objects.home_mqtt_label, buf); - if (!db.module_config.mqtt.enabled) { - Themes::recolorButton(objects.home_mqtt_button, false); - Themes::recolorText(objects.home_mqtt_label, false); - } - } + if (!db.module_config.mqtt.enabled) { + Themes::recolorButton(objects.home_mqtt_button, false); + Themes::recolorText(objects.home_mqtt_label, false); + } +} - void TFTView_320x240::updateExtNotificationModule(const meshtastic_ModuleConfig_ExternalNotificationConfig &cfg) - { - db.module_config.external_notification = cfg; - db.module_config.has_external_notification = true; +void TFTView_320x240::updateExtNotificationModule(const meshtastic_ModuleConfig_ExternalNotificationConfig &cfg) +{ + db.module_config.external_notification = cfg; + db.module_config.has_external_notification = true; + + char buf[32]; + lv_snprintf(buf, sizeof(buf), _("Message Alert: %s"), + db.module_config.external_notification.alert_message_buzzer && db.module_config.external_notification.enabled + ? _("on") + : _("off")); + lv_label_set_text(objects.basic_settings_alert_label, buf); +} - char buf[32]; - lv_snprintf(buf, sizeof(buf), _("Message Alert: %s"), - db.module_config.external_notification.alert_message_buzzer && - db.module_config.external_notification.enabled - ? _("on") - : _("off")); - lv_label_set_text(objects.basic_settings_alert_label, buf); +void TFTView_320x240::updateRingtone(const char rtttl[231]) +{ + // retrieving ringtone index for dropdown + uint16_t rtIndex = 0; + for (int i = 0; i < numRingtones; i++) { + if (strncmp(ringtone[i].rtttl, rtttl, 16) == 0) { + rtIndex = i; + break; } + } + if (rtIndex != 0) + db.uiConfig.ring_tone_id = rtIndex; + if (db.uiConfig.ring_tone_id == 0) + db.uiConfig.ring_tone_id = 1; - void TFTView_320x240::updateRingtone(const char rtttl[231]) - { - // retrieving ringtone index for dropdown - uint16_t rtIndex = 0; - for (int i = 0; i < numRingtones; i++) { - if (strncmp(ringtone[i].rtttl, rtttl, 16) == 0) { - rtIndex = i; - break; - } - } - if (rtIndex != 0) - db.uiConfig.ring_tone_id = rtIndex; - if (db.uiConfig.ring_tone_id == 0) - db.uiConfig.ring_tone_id = 1; - - // update home panel bell text - setBellText(db.uiConfig.alert_enabled, !db.silent); - bool off = !db.uiConfig.alert_enabled && db.silent; - Themes::recolorButton(objects.home_bell_button, !off); - Themes::recolorText(objects.home_bell_label, !off); - objects.home_bell_button->user_data = (void *)off; - } + // update home panel bell text + setBellText(db.uiConfig.alert_enabled, !db.silent); + bool off = !db.uiConfig.alert_enabled && db.silent; + Themes::recolorButton(objects.home_bell_button, !off); + Themes::recolorText(objects.home_bell_label, !off); + objects.home_bell_button->user_data = (void *)off; +} - void TFTView_320x240::updateTime(uint32_t timeVal) - { - time_t localtime; - time(&localtime); +void TFTView_320x240::updateTime(uint32_t timeVal) +{ + time_t localtime; + time(&localtime); - if (VALID_TIME(localtime)) { - if (actTime != localtime) { - ILOG_DEBUG("update (local)time: %d -> %d", actTime, localtime); - actTime = localtime; - } - } else { - if (timeVal > actTime) { - ILOG_DEBUG("update (act)time: %d -> %d", actTime, timeVal); - actTime = timeVal; - } - } + if (VALID_TIME(localtime)) { + if (actTime != localtime) { + ILOG_DEBUG("update (local)time: %d -> %d", actTime, localtime); + actTime = localtime; + } + } else { + if (timeVal > actTime) { + ILOG_DEBUG("update (act)time: %d -> %d", actTime, timeVal); + actTime = timeVal; } + } +} - /** - * @brief Create a new container for a node or group channel if it does not exist - * - * @param from - * @param to: UINT32_MAX for broadcast, ownNode (= us) otherwise - * @param channel - */ - lv_obj_t *TFTView_320x240::newMessageContainer(uint32_t from, uint32_t to, uint8_t ch) - { - if (to == UINT32_MAX || from == 0) { - if (channelGroup[ch] != nullptr) - return channelGroup[ch]; - } else { - auto it = messages.find(from); - if (it != messages.end() && it->second) - return it->second; - } +/** + * @brief Create a new container for a node or group channel if it does not exist + * + * @param from + * @param to: UINT32_MAX for broadcast, ownNode (= us) otherwise + * @param channel + */ +lv_obj_t *TFTView_320x240::newMessageContainer(uint32_t from, uint32_t to, uint8_t ch) +{ + if (to == UINT32_MAX || from == 0) { + if (channelGroup[ch] != nullptr) + return channelGroup[ch]; + } else { + auto it = messages.find(from); + if (it != messages.end() && it->second) + return it->second; + } - // create container for new messages - lv_obj_t *container = lv_obj_create(objects.messages_panel); - lv_obj_remove_style_all(container); - lv_obj_set_width(container, lv_pct(100)); - lv_obj_set_height(container, lv_pct(88)); - lv_obj_set_x(container, 0); - lv_obj_set_y(container, 0); - lv_obj_set_align(container, LV_ALIGN_TOP_MID); - lv_obj_set_flex_flow(container, LV_FLEX_FLOW_COLUMN); - lv_obj_set_flex_align(container, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START); - lv_obj_clear_flag(container, - lv_obj_flag_t(LV_OBJ_FLAG_PRESS_LOCK | LV_OBJ_FLAG_CLICK_FOCUSABLE | LV_OBJ_FLAG_GESTURE_BUBBLE | - LV_OBJ_FLAG_SNAPPABLE | LV_OBJ_FLAG_SCROLL_ELASTIC)); /// Flags - lv_obj_set_scrollbar_mode(container, LV_SCROLLBAR_MODE_ACTIVE); - lv_obj_set_scroll_dir(container, LV_DIR_VER); - lv_obj_set_style_pad_left(container, 6, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_right(container, 6, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_top(container, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_bottom(container, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_row(container, 6, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_column(container, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - - // store new message container - if (to == UINT32_MAX || from == 0) { - channelGroup[ch] = container; - } else { - messages[from] = container; - } + // create container for new messages + lv_obj_t *container = lv_obj_create(objects.messages_panel); + lv_obj_remove_style_all(container); + lv_obj_set_width(container, lv_pct(100)); + lv_obj_set_height(container, lv_pct(88)); + lv_obj_set_x(container, 0); + lv_obj_set_y(container, 0); + lv_obj_set_align(container, LV_ALIGN_TOP_MID); + lv_obj_set_flex_flow(container, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(container, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START); + lv_obj_clear_flag(container, lv_obj_flag_t(LV_OBJ_FLAG_PRESS_LOCK | LV_OBJ_FLAG_CLICK_FOCUSABLE | LV_OBJ_FLAG_GESTURE_BUBBLE | + LV_OBJ_FLAG_SNAPPABLE | LV_OBJ_FLAG_SCROLL_ELASTIC)); /// Flags + lv_obj_set_scrollbar_mode(container, LV_SCROLLBAR_MODE_ACTIVE); + lv_obj_set_scroll_dir(container, LV_DIR_VER); + lv_obj_set_style_pad_left(container, 6, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_right(container, 6, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_top(container, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_bottom(container, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_row(container, 6, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_column(container, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + + // store new message container + if (to == UINT32_MAX || from == 0) { + channelGroup[ch] = container; + } else { + messages[from] = container; + } - // optionally add chat to chatPanel to access the container - addChat(from, to, ch); + // optionally add chat to chatPanel to access the container + addChat(from, to, ch); - return container; - } + return container; +} - /** - * @brief insert a mew message that arrived into a or container - * - * @param from source node - * @param to destination node - * @param ch channel - * @param size length of msg - * @param msg text message - * @param time in/out: message time (maybe overwritten when 0) - * @param restore if restoring then skip banners and highlight - */ - void TFTView_320x240::newMessage(uint32_t from, uint32_t to, uint8_t ch, const char *msg, uint32_t &msgTime, bool restore) - { - ILOG_DEBUG("newMessage: from:0x%08x, to:0x%08x, ch:%d, time:%d", from, to, ch, msgTime); - int pos = 0; - char buf[284]; // 237 + 4 + 40 + 2 + 1 - lv_obj_t *container = nullptr; - if (to == UINT32_MAX) { // message for group, prepend short name to msg - if (nodes.find(from) == nodes.end()) { - pos += sprintf(buf, "%04x ", from & 0xffff); - } else { - // original short name is held in userData, extract it and add msg - char *userData = (char *)&(nodes[from]->LV_OBJ_IDX(node_lbs_idx)->user_data); - while (pos < 4 && userData[pos] != 0) { - buf[pos] = userData[pos]; - pos++; - } - } - buf[pos++] = ' '; - container = channelGroup[ch]; - } else { // message for us - container = messages[from]; +/** + * @brief insert a mew message that arrived into a or container + * + * @param from source node + * @param to destination node + * @param ch channel + * @param size length of msg + * @param msg text message + * @param time in/out: message time (maybe overwritten when 0) + * @param restore if restoring then skip banners and highlight + */ +void TFTView_320x240::newMessage(uint32_t from, uint32_t to, uint8_t ch, const char *msg, uint32_t &msgTime, bool restore) +{ + ILOG_DEBUG("newMessage: from:0x%08x, to:0x%08x, ch:%d, time:%d", from, to, ch, msgTime); + int pos = 0; + char buf[284]; // 237 + 4 + 40 + 2 + 1 + lv_obj_t *container = nullptr; + if (to == UINT32_MAX) { // message for group, prepend short name to msg + if (nodes.find(from) == nodes.end()) { + pos += sprintf(buf, "%04x ", from & 0xffff); + } else { + // original short name is held in userData, extract it and add msg + char *userData = (char *)&(nodes[from]->LV_OBJ_IDX(node_lbs_idx)->user_data); + while (pos < 4 && userData[pos] != 0) { + buf[pos] = userData[pos]; + pos++; } + } + buf[pos++] = ' '; + container = channelGroup[ch]; + } else { // message for us + container = messages[from]; + } - // if it's the first message we need a container - if (!container) { - container = newMessageContainer(from, to, ch); - } + // if it's the first message we need a container + if (!container) { + container = newMessageContainer(from, to, ch); + } - pos += timestamp(&buf[pos], msgTime, !restore); - sprintf(&buf[pos], "%s", msg); + pos += timestamp(&buf[pos], msgTime, !restore); + sprintf(&buf[pos], "%s", msg); - // place message into container - newMessage(from, container, ch, buf); + // place message into container + newMessage(from, container, ch, buf); - if (!restore) { - // display msg popup if not already viewing the messages - if (container != activeMsgContainer || activePanel != objects.messages_panel) { - unreadMessages++; - updateUnreadMessages(); - if (activePanel != objects.messages_panel && db.uiConfig.alert_enabled && - !db.channel[ch].settings.module_settings.is_muted) { - showMessagePopup(from, to, ch, lv_label_get_text(nodes[from]->LV_OBJ_IDX(node_lbl_idx))); - } - lv_obj_add_flag(container, LV_OBJ_FLAG_HIDDEN); - } - if (container != activeMsgContainer) - highlightChat(from, to, ch); - } else { - if (container != activeMsgContainer) - lv_obj_add_flag(container, LV_OBJ_FLAG_HIDDEN); + if (!restore) { + // display msg popup if not already viewing the messages + if (container != activeMsgContainer || activePanel != objects.messages_panel) { + unreadMessages++; + updateUnreadMessages(); + if (activePanel != objects.messages_panel && db.uiConfig.alert_enabled && + !db.channel[ch].settings.module_settings.is_muted) { + showMessagePopup(from, to, ch, lv_label_get_text(nodes[from]->LV_OBJ_IDX(node_lbl_idx))); } + lv_obj_add_flag(container, LV_OBJ_FLAG_HIDDEN); } + if (container != activeMsgContainer) + highlightChat(from, to, ch); + } else { + if (container != activeMsgContainer) + lv_obj_add_flag(container, LV_OBJ_FLAG_HIDDEN); + } +} - /** - * @brief Display message bubble in related message container - * - * @param nodeNum - * @param container - * @param ch - * @param msg - */ - void TFTView_320x240::newMessage(uint32_t nodeNum, lv_obj_t * container, uint8_t ch, const char *msg) - { - lv_obj_t *hiddenPanel = lv_obj_create(container); - lv_obj_set_width(hiddenPanel, lv_pct(100)); - lv_obj_set_height(hiddenPanel, LV_SIZE_CONTENT); /// 50 - lv_obj_set_align(hiddenPanel, LV_ALIGN_CENTER); - lv_obj_clear_flag(hiddenPanel, LV_OBJ_FLAG_SCROLLABLE); /// Flags - lv_obj_set_style_radius(hiddenPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - add_style_panel_style(hiddenPanel); - lv_obj_set_style_pad_left(hiddenPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_right(hiddenPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_top(hiddenPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_bottom(hiddenPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - - lv_obj_t *msgLabel = lv_label_create(hiddenPanel); - // calculate expected size of text bubble, to make it look nicer - lv_coord_t width = lv_txt_get_width(msg, strlen(msg), &ui_font_montserrat_14, 0); - lv_obj_set_width(msgLabel, std::max(std::min((int32_t)(width), 160) + 10, 40)); - lv_obj_set_height(msgLabel, LV_SIZE_CONTENT); - lv_obj_set_align(msgLabel, LV_ALIGN_LEFT_MID); - lv_label_set_text(msgLabel, msg); - add_style_new_message_style(msgLabel); - - if (state == MeshtasticView::eRunning) { - lv_obj_scroll_to_view(hiddenPanel, LV_ANIM_ON); - lv_obj_move_foreground(objects.message_input_area); - } - lv_obj_add_event_cb(hiddenPanel, ui_event_chatNodeButton, LV_EVENT_CLICKED, (void *)nodeNum); - } +/** + * @brief Display message bubble in related message container + * + * @param nodeNum + * @param container + * @param ch + * @param msg + */ +void TFTView_320x240::newMessage(uint32_t nodeNum, lv_obj_t *container, uint8_t ch, const char *msg) +{ + lv_obj_t *hiddenPanel = lv_obj_create(container); + lv_obj_set_width(hiddenPanel, lv_pct(100)); + lv_obj_set_height(hiddenPanel, LV_SIZE_CONTENT); /// 50 + lv_obj_set_align(hiddenPanel, LV_ALIGN_CENTER); + lv_obj_clear_flag(hiddenPanel, LV_OBJ_FLAG_SCROLLABLE); /// Flags + lv_obj_set_style_radius(hiddenPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + add_style_panel_style(hiddenPanel); + lv_obj_set_style_pad_left(hiddenPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_right(hiddenPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_top(hiddenPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_bottom(hiddenPanel, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + + lv_obj_t *msgLabel = lv_label_create(hiddenPanel); + // calculate expected size of text bubble, to make it look nicer + lv_coord_t width = lv_txt_get_width(msg, strlen(msg), &ui_font_montserrat_14, 0); + lv_obj_set_width(msgLabel, std::max(std::min((int32_t)(width), 160) + 10, 40)); + lv_obj_set_height(msgLabel, LV_SIZE_CONTENT); + lv_obj_set_align(msgLabel, LV_ALIGN_LEFT_MID); + lv_label_set_text(msgLabel, msg); + add_style_new_message_style(msgLabel); + + if (state == MeshtasticView::eRunning) { + lv_obj_scroll_to_view(hiddenPanel, LV_ANIM_ON); + lv_obj_move_foreground(objects.message_input_area); + } + lv_obj_add_event_cb(hiddenPanel, ui_event_chatNodeButton, LV_EVENT_CLICKED, (void *)nodeNum); +} - /** - * restore messages from persistent log - */ - void TFTView_320x240::restoreMessage(const LogMessage &msg) - { - //((uint8_t *)msg.bytes)[msg._size] = 0; - // ILOG_DEBUG("restoring msg from:0x%08x, to:0x%08x, ch:%d, time:%d, status:%d, trash:%d, size:%d, '%s'", msg.from, - // msg.to, - // msg.ch, msg.time, (int)msg.status, msg.trashFlag, msg._size, msg.bytes); - - if (msg.from == ownNode) { - lv_obj_t *container = nullptr; - if (msg.to == UINT32_MAX) { - if (msg.trashFlag && chats.find(msg.ch) != chats.end()) { - ILOG_DEBUG("trashFlag set for channel %d", msg.ch); - eraseChat(msg.ch); - return; - } else { - container = newMessageContainer(msg.from, msg.to, msg.ch); - } - } else { - if (nodes.find(msg.to) != nodes.end()) { - if (msg.trashFlag && chats.find(msg.to) != chats.end()) { - ILOG_DEBUG("trashFlag set for node %08x", msg.to); - eraseChat(msg.to); - return; - } else { - container = newMessageContainer(msg.to, msg.from, msg.ch); - } - } else { - ILOG_DEBUG("to node 0x%08x not in db", msg.to); - MeshtasticView::addOrUpdateNode(msg.to, msg.ch, 0, eRole::unknown, false, false); - } - } - if (container) { - if (container != activeMsgContainer) - lv_obj_add_flag(container, LV_OBJ_FLAG_HIDDEN); - addMessage(container, msg.time, 0, (char *)msg.bytes, msg.status); - } - } else if (nodes.find(msg.from) != nodes.end()) { - if (msg.trashFlag && chats.find(msg.from) != chats.end()) { - ILOG_DEBUG("trashFlag set for node %08x", msg.from); - eraseChat(msg.from); +/** + * restore messages from persistent log + */ +void TFTView_320x240::restoreMessage(const LogMessage &msg) +{ + //((uint8_t *)msg.bytes)[msg._size] = 0; + // ILOG_DEBUG("restoring msg from:0x%08x, to:0x%08x, ch:%d, time:%d, status:%d, trash:%d, size:%d, '%s'", msg.from, msg.to, + // msg.ch, msg.time, (int)msg.status, msg.trashFlag, msg._size, msg.bytes); + + if (msg.from == ownNode) { + lv_obj_t *container = nullptr; + if (msg.to == UINT32_MAX) { + if (msg.trashFlag && chats.find(msg.ch) != chats.end()) { + ILOG_DEBUG("trashFlag set for channel %d", msg.ch); + eraseChat(msg.ch); + return; + } else { + container = newMessageContainer(msg.from, msg.to, msg.ch); + } + } else { + if (nodes.find(msg.to) != nodes.end()) { + if (msg.trashFlag && chats.find(msg.to) != chats.end()) { + ILOG_DEBUG("trashFlag set for node %08x", msg.to); + eraseChat(msg.to); return; } else { - uint32_t time = msg.time ? msg.time : UINT32_MAX; // don't overwrite 0 with actual time - newMessage(msg.from, msg.to, msg.ch, (const char *)msg.bytes, time); + container = newMessageContainer(msg.to, msg.from, msg.ch); } } else { - int pos = 0; - char buf[284]; // 237 + 4 + 40 + 2 + 1 - if (msg.to != UINT32_MAX) { - // from node not in db - ILOG_DEBUG("from node 0x%08x not in db", msg.from); - MeshtasticView::addOrUpdateNode(msg.from, msg.ch, 0, eRole::unknown, false, false); - } else { - ILOG_DEBUG("from node 0x%08x not in db and no need to insert", msg.from); - pos += sprintf(buf, "%04x ", msg.from & 0xffff); - } - uint32_t len = timestamp(buf + pos, msg.time, false); - memcpy(buf + pos + len, msg.bytes, msg.length()); - buf[pos + len + msg.length()] = 0; - - lv_obj_t *container = newMessageContainer(msg.from, msg.to, msg.ch); - lv_obj_add_flag(container, LV_OBJ_FLAG_HIDDEN); - newMessage(msg.from, container, msg.ch, buf); + ILOG_DEBUG("to node 0x%08x not in db", msg.to); + MeshtasticView::addOrUpdateNode(msg.to, msg.ch, 0, eRole::unknown, false, false); } } + if (container) { + if (container != activeMsgContainer) + lv_obj_add_flag(container, LV_OBJ_FLAG_HIDDEN); + addMessage(container, msg.time, 0, (char *)msg.bytes, msg.status); + } + } else if (nodes.find(msg.from) != nodes.end()) { + if (msg.trashFlag && chats.find(msg.from) != chats.end()) { + ILOG_DEBUG("trashFlag set for node %08x", msg.from); + eraseChat(msg.from); + return; + } else { + uint32_t time = msg.time ? msg.time : UINT32_MAX; // don't overwrite 0 with actual time + newMessage(msg.from, msg.to, msg.ch, (const char *)msg.bytes, time); + } + } else { + int pos = 0; + char buf[284]; // 237 + 4 + 40 + 2 + 1 + if (msg.to != UINT32_MAX) { + // from node not in db + ILOG_DEBUG("from node 0x%08x not in db", msg.from); + MeshtasticView::addOrUpdateNode(msg.from, msg.ch, 0, eRole::unknown, false, false); + } else { + ILOG_DEBUG("from node 0x%08x not in db and no need to insert", msg.from); + pos += sprintf(buf, "%04x ", msg.from & 0xffff); + } + uint32_t len = timestamp(buf + pos, msg.time, false); + memcpy(buf + pos + len, msg.bytes, msg.length()); + buf[pos + len + msg.length()] = 0; - /** - * @brief Add a new chat to the chat panel to access the message container - * - * @param from - * @param to - * @param ch - */ - void TFTView_320x240::addChat(uint32_t from, uint32_t to, uint8_t ch) - { - uint32_t index = ((to == UINT32_MAX || from == 0) ? ch : from); - auto it = chats.find(index); - if (it != chats.end()) - return; + lv_obj_t *container = newMessageContainer(msg.from, msg.to, msg.ch); + lv_obj_add_flag(container, LV_OBJ_FLAG_HIDDEN); + newMessage(msg.from, container, msg.ch, buf); + } +} - lv_obj_t *chatDelBtn = nullptr; - lv_obj_t *parent_obj = objects.chats_panel; - - // ChatsButton - lv_obj_t *chatBtn = lv_btn_create(parent_obj); - lv_obj_set_pos(chatBtn, 0, 0); - lv_obj_set_size(chatBtn, LV_PCT(100), buttonSize); - lv_obj_add_flag(chatBtn, LV_OBJ_FLAG_SCROLL_ON_FOCUS); - lv_obj_clear_flag(chatBtn, LV_OBJ_FLAG_SCROLLABLE); - add_style_home_button_style(chatBtn); - lv_obj_set_style_align(chatBtn, LV_ALIGN_TOP_MID, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(chatBtn, colorMidGray, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_width(chatBtn, 1, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_shadow_ofs_x(chatBtn, 1, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_shadow_ofs_y(chatBtn, 2, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_radius(chatBtn, 6, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_left(chatBtn, 3, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_right(chatBtn, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_top(chatBtn, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_bottom(chatBtn, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_row(chatBtn, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_pad_column(chatBtn, 0, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(chatBtn, lv_color_hex(0xff141723), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_move_to_index(chatBtn, 0); +/** + * @brief Add a new chat to the chat panel to access the message container + * + * @param from + * @param to + * @param ch + */ +void TFTView_320x240::addChat(uint32_t from, uint32_t to, uint8_t ch) +{ + uint32_t index = ((to == UINT32_MAX || from == 0) ? ch : from); + auto it = chats.find(index); + if (it != chats.end()) + return; - char buf[64]; - if (to == UINT32_MAX || from == 0) { - sprintf(buf, "%d: %s", (int)ch, lv_label_get_text(channel[ch])); - } else { - auto it = nodes.find(from); - if (it != nodes.end()) { - sprintf(buf, "%s: %s", lv_label_get_text(it->second->LV_OBJ_IDX(node_lbs_idx)), - lv_label_get_text(it->second->LV_OBJ_IDX(node_lbl_idx))); - } else { - sprintf(buf, "!%08x", from); - } - } + lv_obj_t *chatDelBtn = nullptr; + lv_obj_t *parent_obj = objects.chats_panel; + + // ChatsButton + lv_obj_t *chatBtn = lv_btn_create(parent_obj); + lv_obj_set_pos(chatBtn, 0, 0); + lv_obj_set_size(chatBtn, LV_PCT(100), buttonSize); + lv_obj_add_flag(chatBtn, LV_OBJ_FLAG_SCROLL_ON_FOCUS); + lv_obj_clear_flag(chatBtn, LV_OBJ_FLAG_SCROLLABLE); + add_style_home_button_style(chatBtn); + lv_obj_set_style_align(chatBtn, LV_ALIGN_TOP_MID, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_color(chatBtn, colorMidGray, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_width(chatBtn, 1, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_shadow_ofs_x(chatBtn, 1, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_shadow_ofs_y(chatBtn, 2, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_radius(chatBtn, 6, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_left(chatBtn, 3, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_right(chatBtn, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_top(chatBtn, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_bottom(chatBtn, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_row(chatBtn, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_pad_column(chatBtn, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(chatBtn, lv_color_hex(0xff141723), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_move_to_index(chatBtn, 0); + + char buf[64]; + if (to == UINT32_MAX || from == 0) { + sprintf(buf, "%d: %s", (int)ch, lv_label_get_text(channel[ch])); + } else { + auto it = nodes.find(from); + if (it != nodes.end()) { + sprintf(buf, "%s: %s", lv_label_get_text(it->second->LV_OBJ_IDX(node_lbs_idx)), + lv_label_get_text(it->second->LV_OBJ_IDX(node_lbl_idx))); + } else { + sprintf(buf, "!%08x", from); + } + } + { + lv_obj_t *parent_obj = chatBtn; + { + // ChatsButtonLabel + lv_obj_t *obj = lv_label_create(parent_obj); + objects.chats_button_label = obj; + lv_obj_set_pos(obj, 0, 0); + lv_obj_set_size(obj, LV_PCT(100), LV_SIZE_CONTENT); + lv_label_set_long_mode(obj, LV_LABEL_LONG_DOT); + lv_label_set_text(obj, buf); + lv_obj_set_style_align(obj, LV_ALIGN_LEFT_MID, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_align(obj, LV_TEXT_ALIGN_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); + } + { + // ChatDelButton + lv_obj_t *obj = lv_btn_create(parent_obj); + chatDelBtn = obj; + lv_obj_set_pos(obj, -3, -1); + lv_obj_set_size(obj, 40, 23); + lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_align(obj, LV_ALIGN_RIGHT_MID, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(obj, colorDarkRed, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); { - lv_obj_t *parent_obj = chatBtn; - { - // ChatsButtonLabel - lv_obj_t *obj = lv_label_create(parent_obj); - objects.chats_button_label = obj; - lv_obj_set_pos(obj, 0, 0); - lv_obj_set_size(obj, LV_PCT(100), LV_SIZE_CONTENT); - lv_label_set_long_mode(obj, LV_LABEL_LONG_DOT); - lv_label_set_text(obj, buf); - lv_obj_set_style_align(obj, LV_ALIGN_LEFT_MID, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_align(obj, LV_TEXT_ALIGN_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); - } + lv_obj_t *parent_obj = obj; { - // ChatDelButton - lv_obj_t *obj = lv_btn_create(parent_obj); - chatDelBtn = obj; - lv_obj_set_pos(obj, -3, -1); - lv_obj_set_size(obj, 40, 23); - lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_align(obj, LV_ALIGN_RIGHT_MID, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(obj, colorDarkRed, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); - { - lv_obj_t *parent_obj = obj; - { - // DelLabel - lv_obj_t *chatDelBtn = lv_label_create(parent_obj); - lv_obj_set_pos(chatDelBtn, 0, 0); - lv_obj_set_size(chatDelBtn, LV_SIZE_CONTENT, LV_SIZE_CONTENT); - lv_label_set_text(chatDelBtn, _("DEL")); - lv_obj_set_style_align(chatDelBtn, LV_ALIGN_CENTER, LV_PART_MAIN | LV_STATE_DEFAULT); - } - } + // DelLabel + lv_obj_t *chatDelBtn = lv_label_create(parent_obj); + lv_obj_set_pos(chatDelBtn, 0, 0); + lv_obj_set_size(chatDelBtn, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_label_set_text(chatDelBtn, _("DEL")); + lv_obj_set_style_align(chatDelBtn, LV_ALIGN_CENTER, LV_PART_MAIN | LV_STATE_DEFAULT); } } + } + } - chats[index] = chatBtn; - updateActiveChats(); - if (index > c_max_channels) { - if (nodes.find(index) != nodes.end()) - applyNodesFilter(index); - } + chats[index] = chatBtn; + updateActiveChats(); + if (index > c_max_channels) { + if (nodes.find(index) != nodes.end()) + applyNodesFilter(index); + } - lv_obj_add_event_cb(chatBtn, ui_event_ChatButton, LV_EVENT_ALL, (void *)index); - lv_obj_add_event_cb(chatDelBtn, ui_event_ChatDelButton, LV_EVENT_CLICKED, (void *)index); - } + lv_obj_add_event_cb(chatBtn, ui_event_ChatButton, LV_EVENT_ALL, (void *)index); + lv_obj_add_event_cb(chatDelBtn, ui_event_ChatDelButton, LV_EVENT_CLICKED, (void *)index); +} - void TFTView_320x240::highlightChat(uint32_t from, uint32_t to, uint8_t ch) - { - uint32_t index = ((to == UINT32_MAX || from == 0) ? ch : from); - auto it = chats.find(index); - if (it != chats.end()) { - // mark chat in color - lv_obj_set_style_border_color(it->second, colorOrange, LV_PART_MAIN | LV_STATE_DEFAULT); - } - } +void TFTView_320x240::highlightChat(uint32_t from, uint32_t to, uint8_t ch) +{ + uint32_t index = ((to == UINT32_MAX || from == 0) ? ch : from); + auto it = chats.find(index); + if (it != chats.end()) { + // mark chat in color + lv_obj_set_style_border_color(it->second, colorOrange, LV_PART_MAIN | LV_STATE_DEFAULT); + } +} - void TFTView_320x240::updateActiveChats(void) - { - char buf[48]; - sprintf(buf, _p("%d active chat(s)", chats.size()), chats.size()); - lv_label_set_text(objects.top_chats_label, buf); - } +void TFTView_320x240::updateActiveChats(void) +{ + char buf[48]; + sprintf(buf, _p("%d active chat(s)", chats.size()), chats.size()); + lv_label_set_text(objects.top_chats_label, buf); +} - /** - * @brief Display banner showing to be patient while restoring messages - */ - void TFTView_320x240::notifyRestoreMessages(int32_t percentage) - { - lv_bar_set_value(objects.message_restore_bar, percentage, LV_ANIM_OFF); - } +/** + * @brief Display banner showing to be patient while restoring messages + */ +void TFTView_320x240::notifyRestoreMessages(int32_t percentage) +{ + lv_bar_set_value(objects.message_restore_bar, percentage, LV_ANIM_OFF); +} - void TFTView_320x240::notifyMessagesRestored(void) - { - MeshtasticView::notifyMessagesRestored(); - lv_obj_add_flag(objects.msg_restore_panel, LV_OBJ_FLAG_HIDDEN); - updateActiveChats(); - updateNodesFiltered(true); - } +void TFTView_320x240::notifyMessagesRestored(void) +{ + MeshtasticView::notifyMessagesRestored(); + lv_obj_add_flag(objects.msg_restore_panel, LV_OBJ_FLAG_HIDDEN); + updateActiveChats(); + updateNodesFiltered(true); +} - /** - * @brief display new message popup panel - * - * @param from sender (NULL for removing popup) - * @param to individual or group message - * @param ch received channel - */ - void TFTView_320x240::showMessagePopup(uint32_t from, uint32_t to, uint8_t ch, const char *name) - { - if (name) { - static char buf[64]; - sprintf(buf, _("New message from \n%s"), name); - buf[38] = '\0'; // cut too long userName - lv_label_set_text(objects.msg_popup_label, buf); - if (to == UINT32_MAX) - objects.msg_popup_button->user_data = (void *)(uint32_t)ch; // store the channel in the button's data - else - objects.msg_popup_button->user_data = (void *)from; // store the node in the button's data - lv_obj_clear_flag(objects.msg_popup_panel, LV_OBJ_FLAG_HIDDEN); - - if (db.module_config.external_notification.alert_message) - lv_disp_trig_activity(NULL); - - lv_group_focus_obj(objects.msg_popup_button); - } - } +/** + * @brief display new message popup panel + * + * @param from sender (NULL for removing popup) + * @param to individual or group message + * @param ch received channel + */ +void TFTView_320x240::showMessagePopup(uint32_t from, uint32_t to, uint8_t ch, const char *name) +{ + if (name) { + static char buf[64]; + sprintf(buf, _("New message from \n%s"), name); + buf[38] = '\0'; // cut too long userName + lv_label_set_text(objects.msg_popup_label, buf); + if (to == UINT32_MAX) + objects.msg_popup_button->user_data = (void *)(uint32_t)ch; // store the channel in the button's data + else + objects.msg_popup_button->user_data = (void *)from; // store the node in the button's data + lv_obj_clear_flag(objects.msg_popup_panel, LV_OBJ_FLAG_HIDDEN); + + if (db.module_config.external_notification.alert_message) + lv_disp_trig_activity(NULL); + + lv_group_focus_obj(objects.msg_popup_button); + } +} - void TFTView_320x240::hideMessagePopup(void) - { - lv_obj_add_flag(objects.msg_popup_panel, LV_OBJ_FLAG_HIDDEN); - } +void TFTView_320x240::hideMessagePopup(void) +{ + lv_obj_add_flag(objects.msg_popup_panel, LV_OBJ_FLAG_HIDDEN); +} - /** - * @brief Display messages of a group channel - * - * @param ch - */ - void TFTView_320x240::showMessages(uint8_t ch) - { - if (!messagesRestored) { - // display message restoration progress banner - lv_obj_clear_flag(objects.msg_popup_panel, LV_OBJ_FLAG_HIDDEN); - lv_group_focus_obj(objects.msg_popup_button); - return; - } +/** + * @brief Display messages of a group channel + * + * @param ch + */ +void TFTView_320x240::showMessages(uint8_t ch) +{ + if (!messagesRestored) { + // display message restoration progress banner + lv_obj_clear_flag(objects.msg_popup_panel, LV_OBJ_FLAG_HIDDEN); + lv_group_focus_obj(objects.msg_popup_button); + return; + } - lv_obj_add_flag(activeMsgContainer, LV_OBJ_FLAG_HIDDEN); - activeMsgContainer = channelGroup[ch]; - if (!activeMsgContainer) { - activeMsgContainer = newMessageContainer(0, UINT32_MAX, ch); - } + lv_obj_add_flag(activeMsgContainer, LV_OBJ_FLAG_HIDDEN); + activeMsgContainer = channelGroup[ch]; + if (!activeMsgContainer) { + activeMsgContainer = newMessageContainer(0, UINT32_MAX, ch); + } - activeMsgContainer->user_data = (void *)(uint32_t)ch; - lv_obj_clear_flag(activeMsgContainer, LV_OBJ_FLAG_HIDDEN); - lv_label_set_text(objects.top_group_chat_label, lv_label_get_text(channel[ch])); - ui_set_active(objects.messages_button, objects.messages_panel, objects.top_group_chat_panel); - } + activeMsgContainer->user_data = (void *)(uint32_t)ch; + lv_obj_clear_flag(activeMsgContainer, LV_OBJ_FLAG_HIDDEN); + lv_label_set_text(objects.top_group_chat_label, lv_label_get_text(channel[ch])); + ui_set_active(objects.messages_button, objects.messages_panel, objects.top_group_chat_panel); +} - /** - * @brief Display messages from a node - * - * @param nodeNum - */ - void TFTView_320x240::showMessages(uint32_t nodeNum) - { - lv_obj_add_flag(activeMsgContainer, LV_OBJ_FLAG_HIDDEN); - activeMsgContainer = messages[nodeNum]; - if (!activeMsgContainer) { - activeMsgContainer = newMessageContainer(nodeNum, 0, 0); - } - activeMsgContainer->user_data = (void *)nodeNum; - lv_obj_clear_flag(activeMsgContainer, LV_OBJ_FLAG_HIDDEN); - lv_obj_t *p = nodes[nodeNum]; - if (p) { - lv_label_set_text(objects.top_messages_node_label, lv_label_get_text(p->LV_OBJ_IDX(node_lbl_idx))); - ui_set_active(objects.messages_button, objects.messages_panel, objects.top_messages_panel); - switch ((unsigned long)p->LV_OBJ_IDX(node_bat_idx)->user_data) { - case 0: - lv_obj_set_style_bg_image_src(objects.top_messages_node_image, &img_lock_channel_image, - LV_PART_MAIN | LV_STATE_DEFAULT); - break; - case 1: - lv_obj_set_style_bg_image_src(objects.top_messages_node_image, &img_lock_secure_image, - LV_PART_MAIN | LV_STATE_DEFAULT); - break; - default: - lv_obj_set_style_bg_image_src(objects.top_messages_node_image, &img_lock_slash_image, - LV_PART_MAIN | LV_STATE_DEFAULT); - break; - } - unreadMessages = 0; // TODO: not all messages may be actually read - updateUnreadMessages(); - } else { - // TODO: log error - } +/** + * @brief Display messages from a node + * + * @param nodeNum + */ +void TFTView_320x240::showMessages(uint32_t nodeNum) +{ + lv_obj_add_flag(activeMsgContainer, LV_OBJ_FLAG_HIDDEN); + activeMsgContainer = messages[nodeNum]; + if (!activeMsgContainer) { + activeMsgContainer = newMessageContainer(nodeNum, 0, 0); + } + activeMsgContainer->user_data = (void *)nodeNum; + lv_obj_clear_flag(activeMsgContainer, LV_OBJ_FLAG_HIDDEN); + lv_obj_t *p = nodes[nodeNum]; + if (p) { + lv_label_set_text(objects.top_messages_node_label, lv_label_get_text(p->LV_OBJ_IDX(node_lbl_idx))); + ui_set_active(objects.messages_button, objects.messages_panel, objects.top_messages_panel); + switch ((unsigned long)p->LV_OBJ_IDX(node_bat_idx)->user_data) { + case 0: + lv_obj_set_style_bg_image_src(objects.top_messages_node_image, &img_lock_channel_image, + LV_PART_MAIN | LV_STATE_DEFAULT); + break; + case 1: + lv_obj_set_style_bg_image_src(objects.top_messages_node_image, &img_lock_secure_image, + LV_PART_MAIN | LV_STATE_DEFAULT); + break; + default: + lv_obj_set_style_bg_image_src(objects.top_messages_node_image, &img_lock_slash_image, + LV_PART_MAIN | LV_STATE_DEFAULT); + break; } + unreadMessages = 0; // TODO: not all messages may be actually read + updateUnreadMessages(); + } else { + // TODO: log error + } +} - /** - * @brief Place keyboard at a suitable space above or below the text input area - * - * @param textArea - */ - void TFTView_320x240::showKeyboard(lv_obj_t * textArea) - { - // REMOVED: Virtual keyboard not needed for T-Deck - // This function is no longer used - return; - } +/** + * @brief Place keyboard at a suitable space above or below the text input area + * + * @param textArea + */ +void TFTView_320x240::showKeyboard(lv_obj_t *textArea) +{ + lv_area_t text_coords, kb_coords; + lv_obj_get_coords(textArea, &text_coords); + lv_obj_get_coords(objects.keyboard, &kb_coords); + uint32_t kb_h = kb_coords.y2 - kb_coords.y1; + uint32_t v = lv_display_get_vertical_resolution(displaydriver->getDisplay()); - void TFTView_320x240::hideKeyboard(lv_obj_t * panel) - { - // REMOVED: Virtual keyboard not needed for T-Deck - // This function is no longer used - return; + if (textArea == objects.message_input_area) { + // if keyboard is to be shown in message input area then scroll the panel using animation + static auto panelAnimCB = [](void *var, int32_t v) { lv_obj_set_y((lv_obj_t *)var, v); }; + static auto kbdAnimCB = [](void *var, int32_t v) { lv_obj_set_y((lv_obj_t *)var, v); }; + + static lv_anim_t a1; + lv_area_t panel_coords; + lv_obj_get_coords(objects.messages_panel, &panel_coords); + + lv_anim_init(&a1); + lv_anim_set_var(&a1, objects.messages_panel); + lv_anim_set_exec_cb(&a1, panelAnimCB); + lv_anim_set_values(&a1, panel_coords.y1, panel_coords.y1 - kb_h); + lv_anim_set_duration(&a1, 300); + lv_anim_set_path_cb(&a1, lv_anim_path_linear); + lv_anim_start(&a1); + + static lv_anim_t a2; + lv_anim_init(&a2); + lv_anim_set_var(&a2, objects.keyboard); + lv_anim_set_exec_cb(&a2, kbdAnimCB); + lv_anim_set_values(&a2, v, v - kb_h); + lv_anim_set_duration(&a2, 300); + lv_anim_set_path_cb(&a2, lv_anim_path_linear); + lv_anim_start(&a2); + } else { + if (text_coords.y1 > kb_h + 30) { + // if enough place above put under top panel + lv_obj_set_pos(objects.keyboard, 0, 28); + } else if ((text_coords.y1 + 10) > v / 2) { + // if text area is at lower half then place above text area + lv_obj_set_pos(objects.keyboard, 0, text_coords.y1 - kb_h - 2); + } else { + // place below text area + lv_obj_set_pos(objects.keyboard, 0, text_coords.y2 + 3); } + } + lv_keyboard_set_textarea(objects.keyboard, textArea); +} - lv_obj_t *TFTView_320x240::showQrCode(lv_obj_t * parent, const char *data) - { - lv_color_t bg_color = colorMesh; - lv_color_t fg_color = lv_palette_darken(LV_PALETTE_BLUE, 4); - qr = lv_qrcode_create(parent); - int32_t size = std::min(lv_obj_get_width(parent), lv_obj_get_height(parent)) - 8; - lv_qrcode_set_size(qr, size); - lv_qrcode_set_dark_color(qr, fg_color); - lv_qrcode_set_light_color(qr, bg_color); - lv_qrcode_update(qr, data, strlen(data)); - lv_obj_center(qr); - lv_obj_set_style_border_color(qr, fg_color, 0); - lv_obj_set_style_border_width(qr, 4, 0); - return qr; - } - - /** - * Enable underlying panel, buttons and scrollbar after it was disabled - */ - void TFTView_320x240::enablePanel(lv_obj_t * panel) - { - lv_obj_clear_state(panel, LV_STATE_DISABLED); - lv_obj_set_scrollbar_mode(panel, LV_SCROLLBAR_MODE_AUTO); - lv_obj_add_flag(panel, LV_OBJ_FLAG_SCROLLABLE); +void TFTView_320x240::hideKeyboard(lv_obj_t *panel) +{ + lv_area_t kb_coords; + lv_obj_get_coords(objects.keyboard, &kb_coords); + uint32_t kb_h = kb_coords.y2 - kb_coords.y1; + + if (panel == objects.messages_panel) { + static auto panelAnimCB = [](void *var, int32_t v) { lv_obj_set_y((lv_obj_t *)var, v); }; + static auto kbdAnimCB = [](void *var, int32_t v) { lv_obj_set_y((lv_obj_t *)var, v); }; + static auto deleted_cb = [](_lv_anim_t *) { lv_obj_add_flag(objects.keyboard, LV_OBJ_FLAG_HIDDEN); }; + + static lv_anim_t a1; + lv_area_t panel_coords; + lv_obj_get_coords(panel, &panel_coords); + + lv_anim_init(&a1); + lv_anim_set_var(&a1, panel); + lv_anim_set_exec_cb(&a1, panelAnimCB); + lv_anim_set_values(&a1, panel_coords.y1, panel_coords.y1 + kb_h); + lv_anim_set_duration(&a1, 300); + lv_anim_set_path_cb(&a1, lv_anim_path_linear); + lv_anim_start(&a1); + + static lv_anim_t a2; + lv_anim_init(&a2); + lv_anim_set_var(&a2, objects.keyboard); + lv_anim_set_exec_cb(&a2, kbdAnimCB); + lv_anim_set_values(&a2, kb_coords.y1, kb_coords.y1 + kb_h); + lv_anim_set_duration(&a2, 300); + lv_anim_set_path_cb(&a2, lv_anim_path_linear); + lv_anim_set_deleted_cb(&a2, deleted_cb); + lv_anim_start(&a2); + } +} - auto enableButtons = [](lv_obj_t *obj, void *) -> lv_obj_tree_walk_res_t { - if (obj->class_p == &lv_button_class) { - lv_obj_clear_state(obj, LV_STATE_DISABLED); - } - return LV_OBJ_TREE_WALK_NEXT; - }; +lv_obj_t *TFTView_320x240::showQrCode(lv_obj_t *parent, const char *data) +{ + lv_color_t bg_color = colorMesh; + lv_color_t fg_color = lv_palette_darken(LV_PALETTE_BLUE, 4); + qr = lv_qrcode_create(parent); + int32_t size = std::min(lv_obj_get_width(parent), lv_obj_get_height(parent)) - 8; + lv_qrcode_set_size(qr, size); + lv_qrcode_set_dark_color(qr, fg_color); + lv_qrcode_set_light_color(qr, bg_color); + lv_qrcode_update(qr, data, strlen(data)); + lv_obj_center(qr); + lv_obj_set_style_border_color(qr, fg_color, 0); + lv_obj_set_style_border_width(qr, 4, 0); + return qr; +} + +/** + * Enable underlying panel, buttons and scrollbar after it was disabled + */ +void TFTView_320x240::enablePanel(lv_obj_t *panel) +{ + lv_obj_clear_state(panel, LV_STATE_DISABLED); + lv_obj_set_scrollbar_mode(panel, LV_SCROLLBAR_MODE_AUTO); + lv_obj_add_flag(panel, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_tree_walk(panel, enableButtons, NULL); + auto enableButtons = [](lv_obj_t *obj, void *) -> lv_obj_tree_walk_res_t { + if (obj->class_p == &lv_button_class) { + lv_obj_clear_state(obj, LV_STATE_DISABLED); } + return LV_OBJ_TREE_WALK_NEXT; + }; - /** - * Disable underlying panel with it's children buttons and scrollbar - */ - void TFTView_320x240::disablePanel(lv_obj_t * panel) - { - lv_obj_add_state(panel, LV_STATE_DISABLED); - lv_obj_set_scrollbar_mode(panel, LV_SCROLLBAR_MODE_OFF); - lv_obj_clear_flag(panel, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_tree_walk(panel, enableButtons, NULL); +} - auto disableButtons = [](lv_obj_t *obj, void *) -> lv_obj_tree_walk_res_t { - if (obj->class_p == &lv_button_class) { - lv_obj_add_state(obj, LV_STATE_DISABLED); - } - return LV_OBJ_TREE_WALK_NEXT; - }; +/** + * Disable underlying panel with it's children buttons and scrollbar + */ +void TFTView_320x240::disablePanel(lv_obj_t *panel) +{ + lv_obj_add_state(panel, LV_STATE_DISABLED); + lv_obj_set_scrollbar_mode(panel, LV_SCROLLBAR_MODE_OFF); + lv_obj_clear_flag(panel, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_tree_walk(panel, disableButtons, NULL); + auto disableButtons = [](lv_obj_t *obj, void *) -> lv_obj_tree_walk_res_t { + if (obj->class_p == &lv_button_class) { + lv_obj_add_state(obj, LV_STATE_DISABLED); } + return LV_OBJ_TREE_WALK_NEXT; + }; - /** - * Set focus to first button of a panel - */ - void TFTView_320x240::setGroupFocus(lv_obj_t * panel) - { - if (panel == objects.home_panel) { - lv_group_focus_obj(objects.home_mail_button); - } else if (panel == objects.nodes_panel) { - lv_group_focus_obj(objects.node_button); - } else if (panel == objects.groups_panel) { - lv_group_focus_obj(objects.channel_button0); - } else if (panel == objects.messages_panel) { - lv_group_focus_obj(objects.message_input_area); - } else if (panel == objects.chats_panel) { - if (chats.size() > 0) { - lv_group_focus_obj(panel->spec_attr->children[1]); // TODO: does not work - } - } else if (panel == objects.map_panel) { + lv_obj_tree_walk(panel, disableButtons, NULL); +} - } else if (panel == objects.settings_screen_lock_panel) { - lv_group_focus_obj(objects.screen_lock_button_matrix); - } else if (panel == objects.controller_panel) { - lv_group_focus_obj(objects.basic_settings_user_button); - } else { - for (int i = 0; i < lv_obj_get_child_count(panel); i++) { - if (panel->spec_attr->children[i]->class_p == &lv_button_class) { - lv_group_focus_obj(panel->spec_attr->children[i]); - break; - } - } +/** + * Set focus to first button of a panel + */ +void TFTView_320x240::setGroupFocus(lv_obj_t *panel) +{ + if (panel == objects.home_panel) { + lv_group_focus_obj(objects.home_mail_button); + } else if (panel == objects.nodes_panel) { + lv_group_focus_obj(objects.node_button); + } else if (panel == objects.groups_panel) { + lv_group_focus_obj(objects.channel_button0); + } else if (panel == objects.messages_panel) { + lv_group_focus_obj(objects.message_input_area); + } else if (panel == objects.chats_panel) { + if (chats.size() > 0) { + lv_group_focus_obj(panel->spec_attr->children[1]); // TODO: does not work + } + } else if (panel == objects.map_panel) { + } else if (panel == objects.settings_screen_lock_panel) { + lv_group_focus_obj(objects.screen_lock_button_matrix); + } else if (panel == objects.controller_panel) { + lv_group_focus_obj(objects.basic_settings_user_button); + } else { + for (int i = 0; i < lv_obj_get_child_count(panel); i++) { + if (panel->spec_attr->children[i]->class_p == &lv_button_class) { + lv_group_focus_obj(panel->spec_attr->children[i]); + break; } } + } +} - /** - * input group used by keyboard and/or pointer for dynamic assignment - */ - void TFTView_320x240::setInputGroup(void) - { - lv_group_t *group = lv_group_get_default(); +/** + * input group used by keyboard and/or pointer for dynamic assignment + */ +void TFTView_320x240::setInputGroup(void) +{ + lv_group_t *group = lv_group_get_default(); - if (group && inputdriver->hasKeyboardDevice()) - lv_indev_set_group(inputdriver->getKeyboard(), group); + if (group && inputdriver->hasKeyboardDevice()) + lv_indev_set_group(inputdriver->getKeyboard(), group); - if (group && inputdriver->hasPointerDevice()) - lv_indev_set_group(inputdriver->getPointer(), group); - } + if (group && inputdriver->hasPointerDevice()) + lv_indev_set_group(inputdriver->getPointer(), group); +} - void TFTView_320x240::setInputButtonLabel(void) - { - // update input button label - std::string current_kbd = inputdriver->getCurrentKeyboardDevice(); - std::string current_ptr = inputdriver->getCurrentPointerDevice(); +void TFTView_320x240::setInputButtonLabel(void) +{ + // update input button label + std::string current_kbd = inputdriver->getCurrentKeyboardDevice(); + std::string current_ptr = inputdriver->getCurrentPointerDevice(); - char label[40]; - lv_snprintf(label, sizeof(label), _("Input Control: %s/%s"), current_ptr.c_str(), current_kbd.c_str()); - lv_label_set_text(objects.basic_settings_input_label, label); - } - // -------- helpers -------- + char label[40]; + lv_snprintf(label, sizeof(label), _("Input Control: %s/%s"), current_ptr.c_str(), current_kbd.c_str()); + lv_label_set_text(objects.basic_settings_input_label, label); +} +// -------- helpers -------- - void TFTView_320x240::removeNode(uint32_t nodeNum) - { - auto it = nodes.find(nodeNum); - if (it != nodes.end()) { - } - } +void TFTView_320x240::removeNode(uint32_t nodeNum) +{ + auto it = nodes.find(nodeNum); + if (it != nodes.end()) { + } +} - void TFTView_320x240::setNodeImage(uint32_t nodeNum, eRole role, bool unmessagable, lv_obj_t *img) - { - uint32_t bgColor, fgColor; - std::tie(bgColor, fgColor) = nodeColor(nodeNum); - if (unmessagable) { - lv_image_set_src(img, &img_unmessagable_image); - lv_obj_set_style_border_color(img, lv_color_hex(bgColor), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(img, lv_color_hex(0x202020), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_img_recolor(img, lv_color_hex(0xFF5555), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_img_recolor_opa(img, 255, LV_PART_MAIN | LV_STATE_DEFAULT); - return; - } else { - switch (role) { - case client: - case client_mute: - case client_hidden: - case tak: { - lv_image_set_src(img, &img_node_client_image); - break; - } - case router_client: { - lv_image_set_src(img, &img_top_nodes_image); - break; - } - case repeater: - case router: - case router_late: { - lv_image_set_src(img, &img_node_router_image); - break; - } - case tracker: - case sensor: - case lost_and_found: - case tak_tracker: { - lv_image_set_src(img, &img_node_sensor_image); - break; - } - case unknown: { - lv_image_set_src(img, &img_circle_question_image); - break; - } - default: - lv_image_set_src(img, &img_node_client_image); - break; - } - } - lv_obj_set_style_bg_color(img, lv_color_hex(bgColor), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(img, lv_color_hex(bgColor), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_img_recolor_opa(img, fgColor ? 0 : 255, LV_PART_MAIN | LV_STATE_DEFAULT); +void TFTView_320x240::setNodeImage(uint32_t nodeNum, eRole role, bool unmessagable, lv_obj_t *img) +{ + uint32_t bgColor, fgColor; + std::tie(bgColor, fgColor) = nodeColor(nodeNum); + if (unmessagable) { + lv_image_set_src(img, &img_unmessagable_image); + lv_obj_set_style_border_color(img, lv_color_hex(bgColor), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(img, lv_color_hex(0x202020), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_img_recolor(img, lv_color_hex(0xFF5555), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_img_recolor_opa(img, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + return; + } else { + switch (role) { + case client: + case client_mute: + case client_hidden: + case tak: { + lv_image_set_src(img, &img_node_client_image); + break; + } + case router_client: { + lv_image_set_src(img, &img_top_nodes_image); + break; + } + case repeater: + case router: + case router_late: { + lv_image_set_src(img, &img_node_router_image); + break; + } + case tracker: + case sensor: + case lost_and_found: + case tak_tracker: { + lv_image_set_src(img, &img_node_sensor_image); + break; + } + case unknown: { + lv_image_set_src(img, &img_circle_question_image); + break; + } + default: + lv_image_set_src(img, &img_node_client_image); + break; } + } + lv_obj_set_style_bg_color(img, lv_color_hex(bgColor), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_color(img, lv_color_hex(bgColor), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_img_recolor_opa(img, fgColor ? 0 : 255, LV_PART_MAIN | LV_STATE_DEFAULT); +} - void TFTView_320x240::updateNodesStatus(void) - { - char buf[40]; - lv_snprintf(buf, sizeof(buf), _p("%d of %d nodes online", nodeCount), nodesOnline, nodeCount); - lv_label_set_text(objects.home_nodes_label, buf); - - if (nodesFiltered) - lv_snprintf(buf, sizeof(buf), _("Filter: %d of %d nodes"), nodeCount - nodesFiltered, nodeCount); - lv_label_set_text(objects.top_nodes_online_label, buf); - } - - /** - * @brief Dynamically update all nodes filter and highlight - * Because the update can take quite some time (tens of ms) it is done in smaller - * chunks of 10 nodes per invocation, so it must be periodically called - * TODO: check for side effects if new nodes are inserted or removed during filter processing - * @param reset indicates to start update from beginning of node list otherwise - * continue with iterator position or skip if done - */ - void TFTView_320x240::updateNodesFiltered(bool reset) - { - static auto it = nodes.begin(); - if (reset || nodesChanged) { - nodesFiltered = 0; - nodesChanged = false; - processingFilter = true; - it = nodes.begin(); - } +void TFTView_320x240::updateNodesStatus(void) +{ + char buf[40]; + lv_snprintf(buf, sizeof(buf), _p("%d of %d nodes online", nodeCount), nodesOnline, nodeCount); + lv_label_set_text(objects.home_nodes_label, buf); - for (int i = 0; i < 10 && it != nodes.end(); i++) { - applyNodesFilter(it->first, true); - it++; - } + if (nodesFiltered) + lv_snprintf(buf, sizeof(buf), _("Filter: %d of %d nodes"), nodeCount - nodesFiltered, nodeCount); + lv_label_set_text(objects.top_nodes_online_label, buf); +} - if (it == nodes.end()) { - processingFilter = false; - } - updateNodesStatus(); - } +/** + * @brief Dynamically update all nodes filter and highlight + * Because the update can take quite some time (tens of ms) it is done in smaller + * chunks of 10 nodes per invocation, so it must be periodically called + * TODO: check for side effects if new nodes are inserted or removed during filter processing + * @param reset indicates to start update from beginning of node list otherwise + * continue with iterator position or skip if done + */ +void TFTView_320x240::updateNodesFiltered(bool reset) +{ + static auto it = nodes.begin(); + if (reset || nodesChanged) { + nodesFiltered = 0; + nodesChanged = false; + processingFilter = true; + it = nodes.begin(); + } - /** - * @brief Update last heard display/user_data/counter to current time - * - * @param nodeNum - */ - void TFTView_320x240::updateLastHeard(uint32_t nodeNum) - { - auto it = nodes.find(nodeNum); - if (it != nodes.end() && it->second) { - time_t lastHeard = (time_t)it->second->LV_OBJ_IDX(node_lh_idx)->user_data; - it->second->LV_OBJ_IDX(node_lh_idx)->user_data = (void *)curtime; - lv_label_set_text(it->second->LV_OBJ_IDX(node_lh_idx), _("now")); - if (it->first != ownNode) { - if (lastHeard > 0 && curtime - lastHeard >= secs_until_offline) { - nodesOnline++; - applyNodesFilter(nodeNum); - updateNodesStatus(); - } - // move to top position - lv_obj_move_to_index(it->second, 1); - - // re-arrange the group linked list i.e. move the node after the top position - lv_ll_t *lv_group_ll = &lv_group_get_default()->obj_ll; - void *act = it->second->LV_OBJ_IDX(node_btn_idx)->user_data; - if (lv_group_ll && act) - _lv_ll_move_before(lv_group_ll, act, _lv_ll_get_next(lv_group_ll, topNodeLL)); - } - } - } + for (int i = 0; i < 10 && it != nodes.end(); i++) { + applyNodesFilter(it->first, true); + it++; + } - /** - * @brief update last heard display for all nodes; also update nodes online - * - */ - void TFTView_320x240::updateAllLastHeard(void) - { - uint16_t online = 0; - time_t lastHeard; - for (auto it : nodes) { - char buf[32]; - if (it.first == ownNode) { // own node is always now, so do update - lastHeard = curtime; - it.second->LV_OBJ_IDX(node_lh_idx)->user_data = (void *)lastHeard; - } else { - lastHeard = (time_t)it.second->LV_OBJ_IDX(node_lh_idx)->user_data; - } - if (lastHeard) { - bool isOnline = lastHeardToString(lastHeard, buf); - lv_label_set_text(it.second->LV_OBJ_IDX(node_lh_idx), buf); - if (isOnline) - online++; - } + if (it == nodes.end()) { + processingFilter = false; + } + updateNodesStatus(); +} + +/** + * @brief Update last heard display/user_data/counter to current time + * + * @param nodeNum + */ +void TFTView_320x240::updateLastHeard(uint32_t nodeNum) +{ + auto it = nodes.find(nodeNum); + if (it != nodes.end() && it->second) { + time_t lastHeard = (time_t)it->second->LV_OBJ_IDX(node_lh_idx)->user_data; + it->second->LV_OBJ_IDX(node_lh_idx)->user_data = (void *)curtime; + lv_label_set_text(it->second->LV_OBJ_IDX(node_lh_idx), _("now")); + if (it->first != ownNode) { + if (lastHeard > 0 && curtime - lastHeard >= secs_until_offline) { + nodesOnline++; + applyNodesFilter(nodeNum); + updateNodesStatus(); } - nodesOnline = online; - updateNodesFiltered(true); - updateNodesStatus(); + // move to top position + lv_obj_move_to_index(it->second, 1); + + // re-arrange the group linked list i.e. move the node after the top position + lv_ll_t *lv_group_ll = &lv_group_get_default()->obj_ll; + void *act = it->second->LV_OBJ_IDX(node_btn_idx)->user_data; + if (lv_group_ll && act) + _lv_ll_move_before(lv_group_ll, act, _lv_ll_get_next(lv_group_ll, topNodeLL)); } + } +} - void TFTView_320x240::updateUnreadMessages(void) - { - char buf[64]; - if (unreadMessages > 0) { - sprintf(buf, unreadMessages == 1 ? _("%d new message") : _("%d new messages"), unreadMessages); - lv_obj_set_style_bg_img_src(objects.home_mail_button, &img_home_mail_unread_button_image, - LV_PART_MAIN | LV_STATE_DEFAULT); - } else { - strcpy(buf, _("no new messages")); - lv_obj_set_style_bg_img_src(objects.home_mail_button, &img_home_mail_button_image, - LV_PART_MAIN | LV_STATE_DEFAULT); - } - lv_label_set_text(objects.home_mail_label, buf); +/** + * @brief update last heard display for all nodes; also update nodes online + * + */ +void TFTView_320x240::updateAllLastHeard(void) +{ + uint16_t online = 0; + time_t lastHeard; + for (auto it : nodes) { + char buf[32]; + if (it.first == ownNode) { // own node is always now, so do update + lastHeard = curtime; + it.second->LV_OBJ_IDX(node_lh_idx)->user_data = (void *)lastHeard; + } else { + lastHeard = (time_t)it.second->LV_OBJ_IDX(node_lh_idx)->user_data; + } + if (lastHeard) { + bool isOnline = lastHeardToString(lastHeard, buf); + lv_label_set_text(it.second->LV_OBJ_IDX(node_lh_idx), buf); + if (isOnline) + online++; } + } + nodesOnline = online; + updateNodesFiltered(true); + updateNodesStatus(); +} - /** - * @brief Called once a second to update time label - * - */ - void TFTView_320x240::updateTime(void) - { - char buf[80]; - time_t curr_time; +void TFTView_320x240::updateUnreadMessages(void) +{ + char buf[64]; + if (unreadMessages > 0) { + sprintf(buf, unreadMessages == 1 ? _("%d new message") : _("%d new messages"), unreadMessages); + lv_obj_set_style_bg_img_src(objects.home_mail_button, &img_home_mail_unread_button_image, + LV_PART_MAIN | LV_STATE_DEFAULT); + } else { + strcpy(buf, _("no new messages")); + lv_obj_set_style_bg_img_src(objects.home_mail_button, &img_home_mail_button_image, LV_PART_MAIN | LV_STATE_DEFAULT); + } + lv_label_set_text(objects.home_mail_label, buf); +} + +/** + * @brief Called once a second to update time label + * + */ +void TFTView_320x240::updateTime(void) +{ + char buf[80]; + time_t curr_time; #ifdef ARCH_PORTDUINO - time(&curr_time); + time(&curr_time); #else - curr_time = actTime; + curr_time = actTime; #endif - tm *curr_tm = localtime(&curr_time); + tm *curr_tm = localtime(&curr_time); - int len = 0; - if (VALID_TIME(curr_time) && (unsigned long)objects.home_time_button->user_data == 0) { - if (db.config.display.use_12h_clock) { - len = strftime(buf, 40, "%I:%M:%S %p\n%a %d-%b-%g", curr_tm); - } else { - len = strftime(buf, 40, "%T %Z%z\n%a %d-%b-%g", curr_tm); - } - } else { - uint32_t uptime = millis() / 1000; - int hours = uptime / 3600; - uptime -= hours * 3600; - int minutes = uptime / 60; - int seconds = uptime - minutes * 60; - - sprintf(&buf[len], _("uptime: %02d:%02d:%02d"), hours, minutes, seconds); - } - lv_label_set_text(objects.home_time_label, buf); + int len = 0; + if (VALID_TIME(curr_time) && (unsigned long)objects.home_time_button->user_data == 0) { + if (db.config.display.use_12h_clock) { + len = strftime(buf, 40, "%I:%M:%S %p\n%a %d-%b-%g", curr_tm); + } else { + len = strftime(buf, 40, "%T %Z%z\n%a %d-%b-%g", curr_tm); } + } else { + uint32_t uptime = millis() / 1000; + int hours = uptime / 3600; + uptime -= hours * 3600; + int minutes = uptime / 60; + int seconds = uptime - minutes * 60; - bool TFTView_320x240::updateSDCard(void) - { - formatSD = false; - if (sdCard) { - delete sdCard; - sdCard = nullptr; - } + sprintf(&buf[len], _("uptime: %02d:%02d:%02d"), hours, minutes, seconds); + } + lv_label_set_text(objects.home_time_label, buf); +} + +bool TFTView_320x240::updateSDCard(void) +{ + formatSD = false; + if (sdCard) { + delete sdCard; + sdCard = nullptr; + } #ifdef HAS_SDCARD - char buf[64]; + char buf[64]; #ifdef HAS_SD_MMC - sdCard = new SDCard; + sdCard = new SDCard; #else - sdCard = new SdFsCard; + sdCard = new SdFsCard; #endif - ISdCard::ErrorType err = ISdCard::ErrorType::eNoError; - if (sdCard->init() && sdCard->cardType() != ISdCard::eNone) { - ILOG_DEBUG("SdCard init successful, card type: %d", sdCard->cardType()); - ISdCard::CardType cardType = sdCard->cardType(); - ISdCard::FatType fatType = sdCard->fatType(); - uint32_t usedSpace = sdCard->usedBytes() / (1024 * 1024); - uint32_t totalSpace = sdCard->cardSize() / (1024 * 1024); - uint32_t totalSpaceGB = (sdCard->cardSize() + 500000000ULL) / (1000ULL * 1000ULL * 1000ULL); - - sprintf(buf, _("%s: %d GB (%s)\nUsed: %0.2f GB (%d%%)"), - cardType == ISdCard::eMMC ? "MMC" - : cardType == ISdCard::eSD ? "SDSC" - : cardType == ISdCard::eSDHC ? "SDHC" - : cardType == ISdCard::eSDXC ? "SDXC" - : "UNKN", - totalSpaceGB, - fatType == ISdCard::eExFat ? "exFAT" - : fatType == ISdCard::eFat32 ? "FAT32" - : fatType == ISdCard::eFat16 ? "FAT16" - : "???", - float(sdCard->usedBytes()) / 1024.0f / 1024.0f / 1024.0f, - totalSpace ? ((usedSpace * 100) + totalSpace / 2) / totalSpace : 0); - Themes::recolorButton(objects.home_sd_card_button, true); - Themes::recolorText(objects.home_sd_card_label, true); - cardDetected = true; - } else { - ILOG_DEBUG("SdFsCard init failed"); - err = sdCard->errorType(); - delete sdCard; - sdCard = nullptr; - } + ISdCard::ErrorType err = ISdCard::ErrorType::eNoError; + if (sdCard->init() && sdCard->cardType() != ISdCard::eNone) { + ILOG_DEBUG("SdCard init successful, card type: %d", sdCard->cardType()); + ISdCard::CardType cardType = sdCard->cardType(); + ISdCard::FatType fatType = sdCard->fatType(); + uint32_t usedSpace = sdCard->usedBytes() / (1024 * 1024); + uint32_t totalSpace = sdCard->cardSize() / (1024 * 1024); + uint32_t totalSpaceGB = (sdCard->cardSize() + 500000000ULL) / (1000ULL * 1000ULL * 1000ULL); + + sprintf(buf, _("%s: %d GB (%s)\nUsed: %0.2f GB (%d%%)"), + cardType == ISdCard::eMMC ? "MMC" + : cardType == ISdCard::eSD ? "SDSC" + : cardType == ISdCard::eSDHC ? "SDHC" + : cardType == ISdCard::eSDXC ? "SDXC" + : "UNKN", + totalSpaceGB, + fatType == ISdCard::eExFat ? "exFAT" + : fatType == ISdCard::eFat32 ? "FAT32" + : fatType == ISdCard::eFat16 ? "FAT16" + : "???", + float(sdCard->usedBytes()) / 1024.0f / 1024.0f / 1024.0f, + totalSpace ? ((usedSpace * 100) + totalSpace / 2) / totalSpace : 0); + Themes::recolorButton(objects.home_sd_card_button, true); + Themes::recolorText(objects.home_sd_card_label, true); + cardDetected = true; + } else { + ILOG_DEBUG("SdFsCard init failed"); + err = sdCard->errorType(); + delete sdCard; + sdCard = nullptr; + } - if (!cardDetected || err != ISdCard::ErrorType::eNoError) { - switch (err) { - case ISdCard::ErrorType::eSlotEmpty: - ILOG_ERROR("SD card slot empty"); - lv_snprintf(buf, sizeof(buf), _("SD slot empty")); - break; - case ISdCard::ErrorType::eFormatError: - ILOG_ERROR("SD invalid format"); - lv_snprintf(buf, sizeof(buf), _("SD invalid format")); - formatSD = true; - break; - case ISdCard::ErrorType::eNoMbrError: - ILOG_ERROR("SD mbr not found"); - lv_snprintf(buf, sizeof(buf), _("SD mbr not found")); - formatSD = true; - break; - case ISdCard::ErrorType::eCardError: - ILOG_ERROR("SD card error"); - lv_snprintf(buf, sizeof(buf), _("SD card error")); - break; - default: - ILOG_ERROR("SD unknown error"); - lv_snprintf(buf, sizeof(buf), _("SD unknown error")); - break; - } - Themes::recolorButton(objects.home_sd_card_button, false); - Themes::recolorText(objects.home_sd_card_label, false); - // allow backup/restore only if there is an SD card detected - lv_obj_add_state(objects.basic_settings_backup_restore_button, LV_STATE_DISABLED); - } else { - // enable backup/restore - lv_obj_clear_state(objects.basic_settings_backup_restore_button, LV_STATE_DISABLED); - } - lv_label_set_text(objects.home_sd_card_label, buf); + if (!cardDetected || err != ISdCard::ErrorType::eNoError) { + switch (err) { + case ISdCard::ErrorType::eSlotEmpty: + ILOG_ERROR("SD card slot empty"); + lv_snprintf(buf, sizeof(buf), _("SD slot empty")); + break; + case ISdCard::ErrorType::eFormatError: + ILOG_ERROR("SD invalid format"); + lv_snprintf(buf, sizeof(buf), _("SD invalid format")); + formatSD = true; + break; + case ISdCard::ErrorType::eNoMbrError: + ILOG_ERROR("SD mbr not found"); + lv_snprintf(buf, sizeof(buf), _("SD mbr not found")); + formatSD = true; + break; + case ISdCard::ErrorType::eCardError: + ILOG_ERROR("SD card error"); + lv_snprintf(buf, sizeof(buf), _("SD card error")); + break; + default: + ILOG_ERROR("SD unknown error"); + lv_snprintf(buf, sizeof(buf), _("SD unknown error")); + break; + } + Themes::recolorButton(objects.home_sd_card_button, false); + Themes::recolorText(objects.home_sd_card_label, false); + // allow backup/restore only if there is an SD card detected + lv_obj_add_state(objects.basic_settings_backup_restore_button, LV_STATE_DISABLED); + } else { + // enable backup/restore + lv_obj_clear_state(objects.basic_settings_backup_restore_button, LV_STATE_DISABLED); + } + lv_label_set_text(objects.home_sd_card_label, buf); #else - lv_obj_add_flag(objects.home_sd_card_button, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(objects.home_sd_card_label, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(objects.home_sd_card_button, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(objects.home_sd_card_label, LV_OBJ_FLAG_HIDDEN); #if defined(ARCH_PORTDUINO) - cardDetected = true; // use PortduinoFS instead - sdCard = new SDCard; + cardDetected = true; // use PortduinoFS instead + sdCard = new SDCard; #endif #endif - if (!sdCard) - sdCard = new NoSdCard; - return cardDetected; - } + if (!sdCard) + sdCard = new NoSdCard; + return cardDetected; +} - void TFTView_320x240::formatSDCard(void) - { - if (sdCard) { - delete sdCard; - sdCard = nullptr; - } +void TFTView_320x240::formatSDCard(void) +{ + if (sdCard) { + delete sdCard; + sdCard = nullptr; + } #ifdef HAS_SDCARD #ifdef HAS_SD_MMC - sdCard = new SDCard; + sdCard = new SDCard; #else - sdCard = new SdFsCard; + sdCard = new SdFsCard; #endif - ILOG_DEBUG("formatting SD card"); - if (sdCard->format()) { - updateSDCard(); - } else { - lv_label_set_text(objects.home_sd_card_label, "SD format failed"); - } + ILOG_DEBUG("formatting SD card"); + if (sdCard->format()) { + updateSDCard(); + } else { + lv_label_set_text(objects.home_sd_card_label, "SD format failed"); + } #endif - if (!sdCard) - sdCard = new NoSdCard; - } + if (!sdCard) + sdCard = new NoSdCard; +} - void TFTView_320x240::updateFreeMem(void) - { - // only update if HomePanel is active (since this is some critical code that did crash sporadically) - if (activePanel == objects.home_panel && (unsigned long)objects.home_memory_button->user_data) { - char buf[64]; - uint32_t freeHeap = 0; - uint32_t freeHeap_pct = 0; +void TFTView_320x240::updateFreeMem(void) +{ + // only update if HomePanel is active (since this is some critical code that did crash sporadically) + if (activePanel == objects.home_panel && (unsigned long)objects.home_memory_button->user_data) { + char buf[64]; + uint32_t freeHeap = 0; + uint32_t freeHeap_pct = 0; - lv_mem_monitor_t mon; - lv_mem_monitor(&mon); + lv_mem_monitor_t mon; + lv_mem_monitor(&mon); #ifdef ARDUINO_ARCH_ESP32 - freeHeap = ESP.getFreeHeap(); - freeHeap_pct = 100 * freeHeap / ESP.getHeapSize(); - sprintf(buf, _("Heap: %d (%d%%)\nLVGL: %d (%d%%)"), freeHeap, freeHeap_pct, mon.free_size, 100 - mon.used_pct); + freeHeap = ESP.getFreeHeap(); + freeHeap_pct = 100 * freeHeap / ESP.getHeapSize(); + sprintf(buf, _("Heap: %d (%d%%)\nLVGL: %d (%d%%)"), freeHeap, freeHeap_pct, mon.free_size, 100 - mon.used_pct); #elif defined(ARCH_PORTDUINO) - static uint32_t totalMem = LinuxHelper::getTotalMem(); - if (totalMem != 0) { - freeHeap = LinuxHelper::getAvailableMem(); - freeHeap_pct = 100 * freeHeap / totalMem; - } - sprintf(buf, _("Heap: %d (%d%%)\nLVGL: %d (%d%%)"), freeHeap, freeHeap_pct, mon.free_size / 1024, - 100 - mon.used_pct); + static uint32_t totalMem = LinuxHelper::getTotalMem(); + if (totalMem != 0) { + freeHeap = LinuxHelper::getAvailableMem(); + freeHeap_pct = 100 * freeHeap / totalMem; + } + sprintf(buf, _("Heap: %d (%d%%)\nLVGL: %d (%d%%)"), freeHeap, freeHeap_pct, mon.free_size / 1024, 100 - mon.used_pct); #else - buf[0] = '\0'; + buf[0] = '\0'; #endif - lv_label_set_text(objects.home_memory_label, buf); - } - } - - void TFTView_320x240::task_handler(void) - { - MeshtasticView::task_handler(); + lv_label_set_text(objects.home_memory_label, buf); + } +} - if (screensInitialised) { - if (map) - map->task_handler(); +void TFTView_320x240::task_handler(void) +{ + MeshtasticView::task_handler(); - if (curtime - lastrun1 >= 1) { // call every 1s - if (map) { - updateLocationMap(THIS->map->getObjectsOnMap()); - } + if (screensInitialised) { + if (map) + map->task_handler(); - lastrun1 = curtime; - actTime++; - updateTime(); + if (curtime - lastrun1 >= 1) { // call every 1s + if (map) { + updateLocationMap(THIS->map->getObjectsOnMap()); + } - if (curtime - lastrun5 >= 5) { // call every 5s - lastrun5 = curtime; - if (scans > 0 && activePanel == objects.signal_scanner_panel) { - scanSignal(scans); - scans--; - } - if (startTime) { - if (curtime - startTime > 30) { - lv_label_set_text(objects.trace_route_start_label, _("Start")); - lv_obj_set_style_outline_color(objects.trace_route_start_button, colorMesh, - LV_PART_MAIN | LV_STATE_DEFAULT); - removeSpinner(); - } else { - char buf[16]; - sprintf(buf, "%ds", ((35 - (curtime - startTime)) / 5) * 5); - lv_label_set_text(objects.trace_route_start_label, buf); - } - } - } - if (curtime - lastrun10 >= 10) { // call every 10s - lastrun10 = curtime; - updateFreeMem(); + lastrun1 = curtime; + actTime++; + updateTime(); - if ((db.config.network.wifi_enabled || db.module_config.mqtt.enabled) && - !displaydriver->isPowersaving()) { - controller->requestDeviceConnectionStatus(); - } + if (curtime - lastrun5 >= 5) { // call every 5s + lastrun5 = curtime; + if (scans > 0 && activePanel == objects.signal_scanner_panel) { + scanSignal(scans); + scans--; + } + if (startTime) { + if (curtime - startTime > 30) { + lv_label_set_text(objects.trace_route_start_label, _("Start")); + lv_obj_set_style_outline_color(objects.trace_route_start_button, colorMesh, + LV_PART_MAIN | LV_STATE_DEFAULT); + removeSpinner(); + } else { + char buf[16]; + sprintf(buf, "%ds", ((35 - (curtime - startTime)) / 5) * 5); + lv_label_set_text(objects.trace_route_start_label, buf); } - if (curtime - lastrun60 >= 60) { // call every 60s - lastrun60 = curtime; - updateAllLastHeard(); + } + } + if (curtime - lastrun10 >= 10) { // call every 10s + lastrun10 = curtime; + updateFreeMem(); - if (detectorRunning) { - controller->sendPing(); - } + if ((db.config.network.wifi_enabled || db.module_config.mqtt.enabled) && !displaydriver->isPowersaving()) { + controller->requestDeviceConnectionStatus(); + } + } + if (curtime - lastrun60 >= 60) { // call every 60s + lastrun60 = curtime; + updateAllLastHeard(); - // if we didn't hear any node for 1h assume we have no signal - if (curtime - lastHeard > secs_until_offline) { - lv_obj_set_style_bg_image_src(objects.home_signal_button, &img_home_no_signal_image, - LV_PART_MAIN | LV_STATE_DEFAULT); - lv_label_set_text(objects.home_signal_label, _("no signal")); - lv_label_set_text(objects.home_signal_pct_label, ""); - } - } + if (detectorRunning) { + controller->sendPing(); } - if (processingFilter || nodesChanged) { - updateNodesFiltered(nodesChanged); + + // if we didn't hear any node for 1h assume we have no signal + if (curtime - lastHeard > secs_until_offline) { + lv_obj_set_style_bg_image_src(objects.home_signal_button, &img_home_no_signal_image, + LV_PART_MAIN | LV_STATE_DEFAULT); + lv_label_set_text(objects.home_signal_label, _("no signal")); + lv_label_set_text(objects.home_signal_pct_label, ""); } } } + if (processingFilter || nodesChanged) { + updateNodesFiltered(nodesChanged); + } + } +} - // === lvgl C style callbacks === +// === lvgl C style callbacks === - extern "C" { +extern "C" { - void action_on_boot_screen_displayed(lv_event_t *e) - { - ILOG_DEBUG("action_on_boot_screen_displayed()"); - } - } +void action_on_boot_screen_displayed(lv_event_t *e) +{ + ILOG_DEBUG("action_on_boot_screen_displayed()"); +} +} #endif From 23d4a360e6ca3adcc582a0d809b78b80d97df5ff Mon Sep 17 00:00:00 2001 From: Juan Pena Date: Fri, 30 Jan 2026 20:15:04 -0500 Subject: [PATCH 10/14] Correcting to move back to master --- include/graphics/view/TFT/TFTView_320x240.h | 3 + source/graphics/TFT/TFTView_320x240.cpp | 150 +++++++++++++++++++- 2 files changed, 146 insertions(+), 7 deletions(-) diff --git a/include/graphics/view/TFT/TFTView_320x240.h b/include/graphics/view/TFT/TFTView_320x240.h index ba6cb4bf..441eceda 100644 --- a/include/graphics/view/TFT/TFTView_320x240.h +++ b/include/graphics/view/TFT/TFTView_320x240.h @@ -379,6 +379,7 @@ class TFTView_320x240 : public MeshtasticView static void ui_event_mapNodeButton(lv_event_t *e); static void ui_event_chatNodeButton(lv_event_t *e); static void ui_event_positionButton(lv_event_t *e); + static void ui_event_nodesPanelScroll(lv_event_t *e); // animations static void ui_anim_node_panel_cb(void *var, int32_t v); @@ -399,6 +400,8 @@ class TFTView_320x240 : public MeshtasticView uint32_t nodesFiltered; // no. hidden nodes in node list bool nodesChanged; // true if nodes changed (added or purged) bool processingFilter; // indicates that filtering is ongoing + uint16_t nodesScrollDisplayLimit; // current node display limit for infinite scroll + bool nodesScrollLoadingMore; // prevent loading multiple batches simultaneously bool packetLogEnabled; // display received packets bool detectorRunning; // meshDetector is active bool cardDetected; // SD has been detected diff --git a/source/graphics/TFT/TFTView_320x240.cpp b/source/graphics/TFT/TFTView_320x240.cpp index cba543e2..f323d38a 100644 --- a/source/graphics/TFT/TFTView_320x240.cpp +++ b/source/graphics/TFT/TFTView_320x240.cpp @@ -74,7 +74,7 @@ constexpr lv_color_t colorRed = LV_COLOR_HEX(0xff5555); constexpr lv_color_t colorDarkRed = LV_COLOR_HEX(0xa70a0a); constexpr lv_color_t colorOrange = LV_COLOR_HEX(0xff8c04); constexpr lv_color_t colorYellow = LV_COLOR_HEX(0xdbd251); -constexpr lv_color_t colorBlueGreen = LV_COLOR_HEX(0x5ded96); +constexpr lv_color_t colorBlueGreen = LV_COLOR_HEX(0x05f6cb); constexpr lv_color_t colorBlue = LV_COLOR_HEX(0x436C70); constexpr lv_color_t colorGray = LV_COLOR_HEX(0x757575); constexpr lv_color_t colorLightGray = LV_COLOR_HEX(0xAAFBFF); @@ -137,8 +137,8 @@ TFTView_320x240 *TFTView_320x240::instance(const DisplayDriverConfig &cfg) TFTView_320x240::TFTView_320x240(const DisplayDriverConfig *cfg, DisplayDriver *driver) : MeshtasticView(cfg, driver, new ViewController), screensInitialised(false), nodesFiltered(0), nodesChanged(true), - processingFilter(false), packetLogEnabled(false), detectorRunning(false), cardDetected(false), formatSD(false), - packetCounter(0), actTime(0), uptime(0), lastHeard(0), hasPosition(false), myLatitude(0), myLongitude(0), + processingFilter(false), nodesScrollDisplayLimit(15), packetLogEnabled(false), detectorRunning(false), cardDetected(false), + formatSD(false), packetCounter(0), actTime(0), uptime(0), lastHeard(0), hasPosition(false), myLatitude(0), myLongitude(0), topNodeLL(nullptr), scans(0), selectedHops(0), chooseNodeSignalScanner(false), chooseNodeTraceRoute(false), qr(nullptr), db{} { @@ -729,6 +729,9 @@ void TFTView_320x240::ui_events_init(void) // node and channel buttons lv_obj_add_event_cb(objects.node_button, ui_event_NodeButton, LV_EVENT_ALL, (void *)ownNode); + // nodes panel - infinite scroll support + lv_obj_add_event_cb(objects.nodes_panel, TFTView_320x240::ui_event_nodesPanelScroll, LV_EVENT_SCROLL, this); + // 8 channel buttons lv_obj_add_event_cb(objects.channel_button0, ui_event_ChannelButton, LV_EVENT_ALL, (void *)0); lv_obj_add_event_cb(objects.channel_button1, ui_event_ChannelButton, LV_EVENT_ALL, (void *)1); @@ -2379,6 +2382,132 @@ void TFTView_320x240::ui_event_positionButton(lv_event_t *e) } } +/** + * @brief Hard object culling for nodes panel + * Actually DELETE nodes outside viewport buffer, recreate when scrolling back + * Dramatically reduces LVGL object count and layout cost + * Trade-off: slight delay when recreating nodes (but faster overall) + */ +void TFTView_320x240::ui_event_nodesPanelScroll(lv_event_t *e) +{ + auto *self = static_cast(lv_event_get_user_data(e)); + if (!self) + return; + + lv_event_code_t event_code = lv_event_get_code(e); + lv_obj_t *panel = (lv_obj_t *)lv_event_get_target(e); + + // Only load more nodes when scroll ends (not during continuous scroll) + if (event_code == LV_EVENT_SCROLL_END) { + const lv_coord_t LOAD_DISTANCE = 400; + + // Load more nodes when near bottom + if (lv_obj_get_scroll_bottom(panel) < LOAD_DISTANCE && self->nodesScrollDisplayLimit < MAX_NUM_NODES_VIEW) { + if (!self->nodesScrollLoadingMore) { + self->nodesScrollLoadingMore = true; + + uint16_t new_limit = self->nodesScrollDisplayLimit + 10; + if (new_limit > MAX_NUM_NODES_VIEW) { + new_limit = MAX_NUM_NODES_VIEW; + } + + self->nodesScrollDisplayLimit = new_limit; + self->updateNodesFiltered(false); + lv_obj_update_layout(panel); + + ILOG_DEBUG("Loaded nodes batch, now displaying %d nodes", self->nodesScrollDisplayLimit); + + self->nodesScrollLoadingMore = false; + } + } + return; + } + + // During continuous scroll: only hide/show visible nodes (cheap operation) + if (event_code != LV_EVENT_SCROLL) { + return; + } + + // Prevent re-entry during hide/show updates + static bool scroll_running = false; + if (scroll_running) + return; + scroll_running = true; + + const int NODE_BUFFER = 3; // Keep 3 nodes visible on each side of viewport + + // Hide nodes with 3-node buffer strategy + lv_coord_t scroll_y = lv_obj_get_scroll_y(panel); + lv_coord_t panel_h = lv_obj_get_height(panel); + + // Find first and last visible nodes + int first_visible_idx = -1; + int last_visible_idx = -1; + int node_idx = 0; + + for (auto &node_pair : self->nodes) { + lv_obj_t *node_obj = node_pair.second; + if (!node_obj) + continue; + + // Only process up to display limit for performance + if (node_idx >= self->nodesScrollDisplayLimit) + break; + + lv_coord_t node_y = lv_obj_get_y(node_obj); + lv_coord_t node_h = lv_obj_get_height(node_obj); + + // Check if node is within viewport + if (node_y + node_h >= scroll_y && node_y <= scroll_y + panel_h) { + if (first_visible_idx == -1) { + first_visible_idx = node_idx; + } + last_visible_idx = node_idx; + } + + node_idx++; + } + + // Calculate buffer range: 3 nodes before first visible, 3 nodes after last visible + // If no visible nodes, show buffer around top + int buffer_start = 0; + int buffer_end = self->nodesScrollDisplayLimit - 1; + + if (first_visible_idx != -1) { + buffer_start = (first_visible_idx > NODE_BUFFER) ? (first_visible_idx - NODE_BUFFER) : 0; + buffer_end = last_visible_idx + NODE_BUFFER; + } + + // Now hide/show based on buffer range + node_idx = 0; + for (auto &node_pair : self->nodes) { + lv_obj_t *node_obj = node_pair.second; + if (!node_obj) + continue; + + bool in_buffer = (node_idx < self->nodesScrollDisplayLimit) && (node_idx >= buffer_start && node_idx <= buffer_end); + + if (in_buffer) { + // Show this node + if (lv_obj_has_flag(node_obj, LV_OBJ_FLAG_HIDDEN)) { + lv_obj_clear_flag(node_obj, LV_OBJ_FLAG_HIDDEN); + } + } else { + // Hide this node + if (!lv_obj_has_flag(node_obj, LV_OBJ_FLAG_HIDDEN)) { + lv_obj_add_flag(node_obj, LV_OBJ_FLAG_HIDDEN); + } + } + + node_idx++; + + if (node_idx >= MAX_NUM_NODES_VIEW) + break; + } + + scroll_running = false; +} + void TFTView_320x240::ui_screen_event_cb(lv_event_t *e) { if (THIS->activePanel == objects.map_panel) { @@ -4651,7 +4780,6 @@ void TFTView_320x240::addNode(uint32_t nodeNum, uint8_t ch, const char *userShor lv_obj_set_pos(ui_SignalLabel, 8, 1); lv_obj_set_align(ui_SignalLabel, LV_ALIGN_TOP_RIGHT); lv_label_set_text(ui_SignalLabel, ""); - lv_obj_set_style_text_color(ui_SignalLabel, lv_color_hex(0xff5ded96), LV_PART_MAIN | LV_STATE_DEFAULT); ui_SignalLabel->user_data = (void *)-1; // TODO viaMqtt; // used for filtering (applyNodesFilter) // PositionLabel lv_obj_t *ui_PositionLabel = lv_label_create(p); @@ -7038,19 +7166,27 @@ void TFTView_320x240::updateNodesStatus(void) void TFTView_320x240::updateNodesFiltered(bool reset) { static auto it = nodes.begin(); + static uint16_t processedCount = 0; // Track how many nodes we've processed in current session + if (reset || nodesChanged) { nodesFiltered = 0; nodesChanged = false; processingFilter = true; it = nodes.begin(); + processedCount = 0; } - for (int i = 0; i < 10 && it != nodes.end(); i++) { + // Process nodes in batches of 10, but respect the display limit for infinite scroll + uint16_t batchSize = 10; + uint16_t processLimit = nodesScrollDisplayLimit; // Limit based on infinite scroll display count + + for (int i = 0; i < batchSize && it != nodes.end() && processedCount < processLimit; i++) { applyNodesFilter(it->first, true); it++; + processedCount++; } - if (it == nodes.end()) { + if (it == nodes.end() || processedCount >= processLimit) { processingFilter = false; } updateNodesStatus(); @@ -7384,4 +7520,4 @@ void action_on_boot_screen_displayed(lv_event_t *e) } } -#endif +#endif \ No newline at end of file From 8a6a0cef1713e93c9d6060d3062bb4146c9ac8d5 Mon Sep 17 00:00:00 2001 From: Juan Pena Date: Fri, 30 Jan 2026 20:21:59 -0500 Subject: [PATCH 11/14] Fixing things --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index dd70c94d..50f9d3e3 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,6 @@

Meshtastic device-ui library

A versatile UI library for the meshtastic® project

-

This is meant to be a faster and more cut down version

-

ONLY FOR T-DECK & T-DECK PLUS

-

This has be edited somewhat by me @limitlezz

-

Real Credit goes to the Meshtastic UI Devs

From 563d9e08d19aeb52d4d0c5fe98949100478c3101 Mon Sep 17 00:00:00 2001 From: Juan Pena Date: Fri, 30 Jan 2026 20:24:02 -0500 Subject: [PATCH 12/14] Fix 2 --- generated/ui_320x240/images.c | 3 +- generated/ui_320x240/images.h | 33 +++---- generated/ui_320x240/screens.c | 151 ++++++++++++++++++++++++--------- 3 files changed, 130 insertions(+), 57 deletions(-) diff --git a/generated/ui_320x240/images.c b/generated/ui_320x240/images.c index 6941bf59..02cfd575 100644 --- a/generated/ui_320x240/images.c +++ b/generated/ui_320x240/images.c @@ -1,6 +1,6 @@ #include "images.h" -const ext_img_desc_t images[89] = { +const ext_img_desc_t images[90] = { { "meshtastic_boot_logo_image", &img_meshtastic_boot_logo_image }, { "settings_button_image", &img_settings_button_image }, { "map_button_image", &img_map_button_image }, @@ -15,6 +15,7 @@ const ext_img_desc_t images[89] = { { "home_bluetooth_on_button_image", &img_home_bluetooth_on_button_image }, { "home_memory_button", &img_home_memory_button }, { "node_client_image", &img_node_client_image }, + { "keyboard_image", &img_keyboard_image }, { "top_nodes_image", &img_top_nodes_image }, { "top_group_image", &img_top_group_image }, { "top_chats_image", &img_top_chats_image }, diff --git a/generated/ui_320x240/images.h b/generated/ui_320x240/images.h index 823a579d..a235d45f 100644 --- a/generated/ui_320x240/images.h +++ b/generated/ui_320x240/images.h @@ -1,12 +1,12 @@ -#ifndef EEZ_LVGL_UI_IMAGES_H -#define EEZ_LVGL_UI_IMAGES_H - -#include "lvgl.h" - -#ifdef __cplusplus -extern "C" { -#endif - +#ifndef EEZ_LVGL_UI_IMAGES_H +#define EEZ_LVGL_UI_IMAGES_H + +#include "lvgl.h" + +#ifdef __cplusplus +extern "C" { +#endif + extern const lv_img_dsc_t img_meshtastic_boot_logo_image; extern const lv_img_dsc_t img_settings_button_image; extern const lv_img_dsc_t img_map_button_image; @@ -21,6 +21,7 @@ extern const lv_img_dsc_t img_home_wlan_button_image; extern const lv_img_dsc_t img_home_bluetooth_on_button_image; extern const lv_img_dsc_t img_home_memory_button; extern const lv_img_dsc_t img_node_client_image; +extern const lv_img_dsc_t img_keyboard_image; extern const lv_img_dsc_t img_top_nodes_image; extern const lv_img_dsc_t img_top_group_image; extern const lv_img_dsc_t img_top_chats_image; @@ -105,11 +106,11 @@ typedef struct _ext_img_desc_t { } ext_img_desc_t; #endif -extern const ext_img_desc_t images[89]; - - -#ifdef __cplusplus -} -#endif - +extern const ext_img_desc_t images[90]; + + +#ifdef __cplusplus +} +#endif + #endif /*EEZ_LVGL_UI_IMAGES_H*/ \ No newline at end of file diff --git a/generated/ui_320x240/screens.c b/generated/ui_320x240/screens.c index 7677c244..1900cabf 100644 --- a/generated/ui_320x240/screens.c +++ b/generated/ui_320x240/screens.c @@ -915,8 +915,7 @@ void create_screen_main_screen() { lv_label_set_text(obj, ""); lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_style_align(obj, LV_ALIGN_TOP_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_align(obj, LV_TEXT_ALIGN_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_color(obj, lv_color_hex(0xff5ded96), LV_PART_MAIN | LV_STATE_DEFAULT); // Changes Signal / hops Color to green + lv_obj_set_style_text_align(obj, LV_TEXT_ALIGN_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); } { // PositionLabel @@ -928,7 +927,7 @@ void create_screen_main_screen() { lv_label_set_text(obj, ""); lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_style_align(obj, LV_ALIGN_TOP_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_color(obj, lv_color_hex(0xff05f6cb), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_color(obj, lv_color_hex(0xff05f6cb), LV_PART_MAIN | LV_STATE_DEFAULT); } { // Position2Label @@ -939,7 +938,7 @@ void create_screen_main_screen() { lv_label_set_long_mode(obj, LV_LABEL_LONG_SCROLL); lv_label_set_text(obj, ""); lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_align(obj, LV_ALIGN_TOP_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_align(obj, LV_ALIGN_TOP_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT); } { // Telemetry1Label @@ -1249,7 +1248,7 @@ void create_screen_main_screen() { lv_obj_t *obj = lv_textarea_create(parent_obj); objects.message_input_area = obj; lv_obj_set_pos(obj, 6, -1); - lv_obj_set_size(obj, LV_PCT(96), 25); // Adjusted width to fit within MessagesPanel was 85 + lv_obj_set_size(obj, LV_PCT(85), 25); lv_textarea_set_max_length(obj, 220); lv_textarea_set_placeholder_text(obj, "Enter Text ..."); lv_textarea_set_one_line(obj, true); @@ -1264,15 +1263,18 @@ void create_screen_main_screen() { lv_obj_set_style_pad_left(obj, 3, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_max_height(obj, 25, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_pad_bottom(obj, 2, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_bg_color(obj, lv_color_hex(0xff1f2a37), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_text_color(obj, lv_color_hex(0xffffffff), LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(obj, lv_color_hex(0xff1f2a37), LV_PART_MAIN | LV_STATE_DEFAULT); } { // KeyboardButton_0 lv_obj_t *obj = lv_btn_create(parent_obj); objects.keyboard_button_0 = obj; - lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_pos(obj, -5, -2); + lv_obj_set_size(obj, 25, 24); + lv_obj_set_style_align(obj, LV_ALIGN_BOTTOM_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(obj, lv_color_hex(0xffffffff), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_image_src(obj, &img_keyboard_image, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_image_recolor(obj, lv_color_hex(0xff373737), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_image_recolor_opa(obj, 255, LV_PART_MAIN | LV_STATE_DEFAULT); } } } @@ -3120,7 +3122,7 @@ void create_screen_main_screen() { lv_obj_t *obj = lv_textarea_create(parent_obj); objects.settings_user_short_textarea = obj; lv_obj_set_pos(obj, 0, 18); - lv_obj_set_size(obj, LV_PCT(50), 25); // was 42 + lv_obj_set_size(obj, LV_PCT(42), 25); lv_textarea_set_max_length(obj, 4); lv_textarea_set_one_line(obj, true); lv_textarea_set_password_mode(obj, false); @@ -3134,7 +3136,13 @@ void create_screen_main_screen() { // KeyboardButton_1 lv_obj_t *obj = lv_btn_create(parent_obj); objects.keyboard_button_1 = obj; - lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_pos(obj, 163, 18); + lv_obj_set_size(obj, 25, 24); + lv_obj_set_style_align(obj, LV_ALIGN_DEFAULT, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(obj, lv_color_hex(0xffffffff), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_image_src(obj, &img_keyboard_image, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_image_recolor(obj, lv_color_hex(0xff373737), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_image_recolor_opa(obj, 255, LV_PART_MAIN | LV_STATE_DEFAULT); } { lv_obj_t *obj = lv_label_create(parent_obj); @@ -3150,7 +3158,7 @@ void create_screen_main_screen() { lv_obj_t *obj = lv_textarea_create(parent_obj); objects.settings_user_long_textarea = obj; lv_obj_set_pos(obj, 0, 75); - lv_obj_set_size(obj, LV_PCT(100), 25); // was 88 + lv_obj_set_size(obj, LV_PCT(88), 25); lv_textarea_set_max_length(obj, 39); lv_textarea_set_one_line(obj, true); lv_textarea_set_password_mode(obj, false); @@ -3164,7 +3172,13 @@ void create_screen_main_screen() { // KeyboardButton_2 lv_obj_t *obj = lv_btn_create(parent_obj); objects.keyboard_button_2 = obj; - lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_pos(obj, 200, 75); + lv_obj_set_size(obj, 25, 24); + lv_obj_set_style_align(obj, LV_ALIGN_DEFAULT, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(obj, lv_color_hex(0xffffffff), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_image_src(obj, &img_keyboard_image, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_image_recolor(obj, lv_color_hex(0xff373737), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_image_recolor_opa(obj, 255, LV_PART_MAIN | LV_STATE_DEFAULT); } { lv_obj_t *obj = lv_obj_create(parent_obj); @@ -3700,7 +3714,7 @@ void create_screen_main_screen() { lv_obj_t *obj = lv_textarea_create(parent_obj); objects.settings_wifi_ssid_textarea = obj; lv_obj_set_pos(obj, 0, 18); - lv_obj_set_size(obj, LV_PCT(100), 25); // was 88 + lv_obj_set_size(obj, LV_PCT(88), 25); lv_textarea_set_max_length(obj, 32); lv_textarea_set_one_line(obj, true); lv_textarea_set_password_mode(obj, false); @@ -3714,7 +3728,13 @@ void create_screen_main_screen() { // KeyboardButton_8 lv_obj_t *obj = lv_btn_create(parent_obj); objects.keyboard_button_8 = obj; - lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_pos(obj, 200, 18); + lv_obj_set_size(obj, 25, 24); + lv_obj_set_style_align(obj, LV_ALIGN_DEFAULT, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(obj, lv_color_hex(0xffffffff), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_image_src(obj, &img_keyboard_image, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_image_recolor(obj, lv_color_hex(0xff373737), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_image_recolor_opa(obj, 255, LV_PART_MAIN | LV_STATE_DEFAULT); } { lv_obj_t *obj = lv_label_create(parent_obj); @@ -3730,7 +3750,7 @@ void create_screen_main_screen() { lv_obj_t *obj = lv_textarea_create(parent_obj); objects.settings_wifi_password_textarea = obj; lv_obj_set_pos(obj, 0, 75); - lv_obj_set_size(obj, LV_PCT(100), 25); // was 88 + lv_obj_set_size(obj, LV_PCT(88), 25); lv_textarea_set_max_length(obj, 64); lv_textarea_set_one_line(obj, true); lv_textarea_set_password_mode(obj, false); @@ -3744,7 +3764,13 @@ void create_screen_main_screen() { // KeyboardButton_9 lv_obj_t *obj = lv_btn_create(parent_obj); objects.keyboard_button_9 = obj; - lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_pos(obj, 200, 75); + lv_obj_set_size(obj, 25, 24); + lv_obj_set_style_align(obj, LV_ALIGN_DEFAULT, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(obj, lv_color_hex(0xffffffff), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_image_src(obj, &img_keyboard_image, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_image_recolor(obj, lv_color_hex(0xff373737), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_image_recolor_opa(obj, 255, LV_PART_MAIN | LV_STATE_DEFAULT); } { lv_obj_t *obj = lv_obj_create(parent_obj); @@ -4012,8 +4038,8 @@ void create_screen_main_screen() { // SettingsScreenLockPasswordTextarea lv_obj_t *obj = lv_textarea_create(parent_obj); objects.settings_screen_lock_password_textarea = obj; - lv_obj_set_pos(obj, -20, 70); // was -30 - lv_obj_set_size(obj, LV_PCT(70), 25); // was 40 + lv_obj_set_pos(obj, -30, 70); + lv_obj_set_size(obj, LV_PCT(40), 25); lv_textarea_set_accepted_chars(obj, "1234567890"); lv_textarea_set_max_length(obj, 6); lv_textarea_set_placeholder_text(obj, "123456"); @@ -4044,7 +4070,13 @@ void create_screen_main_screen() { // KeyboardButton_7 lv_obj_t *obj = lv_btn_create(parent_obj); objects.keyboard_button_7 = obj; - lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_pos(obj, 2, 70); + lv_obj_set_size(obj, 25, 24); + lv_obj_set_style_align(obj, LV_ALIGN_TOP_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(obj, lv_color_hex(0xffffffff), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_image_src(obj, &img_keyboard_image, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_image_recolor(obj, lv_color_hex(0xff373737), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_image_recolor_opa(obj, 255, LV_PART_MAIN | LV_STATE_DEFAULT); } } } @@ -4450,7 +4482,7 @@ void create_screen_main_screen() { lv_obj_t *obj = lv_label_create(parent_obj); objects.obj19 = obj; lv_obj_set_pos(obj, 0, 0); - lv_obj_set_size(obj, LV_PCT(100), LV_SIZE_CONTENT); // was 200 no PCT + lv_obj_set_size(obj, 200, LV_SIZE_CONTENT); lv_label_set_long_mode(obj, LV_LABEL_LONG_CLIP); lv_label_set_text(obj, _("Channel Name")); lv_obj_set_style_text_color(obj, lv_color_hex(0xff7ff5f9), LV_PART_MAIN | LV_STATE_DEFAULT); @@ -4463,7 +4495,7 @@ void create_screen_main_screen() { lv_obj_t *obj = lv_textarea_create(parent_obj); objects.settings_modify_channel_name_textarea = obj; lv_obj_set_pos(obj, 0, 18); - lv_obj_set_size(obj, LV_PCT(100), 25); + lv_obj_set_size(obj, LV_PCT(90), 25); lv_textarea_set_max_length(obj, 11); lv_textarea_set_placeholder_text(obj, "LongFast"); lv_textarea_set_one_line(obj, true); @@ -4478,13 +4510,19 @@ void create_screen_main_screen() { // KeyboardButton_3 lv_obj_t *obj = lv_btn_create(parent_obj); objects.keyboard_button_3 = obj; - lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_pos(obj, 224, 18); + lv_obj_set_size(obj, 25, 24); + lv_obj_set_style_align(obj, LV_ALIGN_DEFAULT, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(obj, lv_color_hex(0xffffffff), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_image_src(obj, &img_keyboard_image, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_image_recolor(obj, lv_color_hex(0xff373737), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_image_recolor_opa(obj, 255, LV_PART_MAIN | LV_STATE_DEFAULT); } { lv_obj_t *obj = lv_label_create(parent_obj); objects.obj20 = obj; - lv_obj_set_pos(obj, 0, 60); - lv_obj_set_size(obj, LV_PCT(100), LV_SIZE_CONTENT); // was 200 no PCT + lv_obj_set_pos(obj, 0, 60); + lv_obj_set_size(obj, 200, LV_SIZE_CONTENT); lv_label_set_long_mode(obj, LV_LABEL_LONG_CLIP); lv_label_set_text(obj, _("Pre-shared Key")); lv_obj_set_style_text_color(obj, lv_color_hex(0xff7ff5f9), LV_PART_MAIN | LV_STATE_DEFAULT); @@ -4496,7 +4534,7 @@ void create_screen_main_screen() { lv_obj_t *obj = lv_textarea_create(parent_obj); objects.settings_modify_channel_psk_textarea = obj; lv_obj_set_pos(obj, 0, 80); - lv_obj_set_size(obj, LV_PCT(100), 25); + lv_obj_set_size(obj, LV_PCT(90), 25); lv_textarea_set_accepted_chars(obj, "1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+=/"); lv_textarea_set_max_length(obj, 44); lv_textarea_set_placeholder_text(obj, "AQ=="); @@ -4516,7 +4554,7 @@ void create_screen_main_screen() { // SettingsModifyChannelKeyGenerateButton lv_obj_t *obj = lv_btn_create(parent_obj); objects.settings_modify_channel_key_generate_button = obj; - lv_obj_set_pos(obj, 224, 48); // was 54 + lv_obj_set_pos(obj, 224, 54); lv_obj_set_size(obj, 25, 24); lv_obj_set_style_align(obj, LV_ALIGN_DEFAULT, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_color(obj, lv_color_hex(0xffffffff), LV_PART_MAIN | LV_STATE_DEFAULT); @@ -4528,7 +4566,13 @@ void create_screen_main_screen() { // KeyboardButton_4 lv_obj_t *obj = lv_btn_create(parent_obj); objects.keyboard_button_4 = obj; - lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_pos(obj, 224, 80); + lv_obj_set_size(obj, 25, 24); + lv_obj_set_style_align(obj, LV_ALIGN_DEFAULT, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(obj, lv_color_hex(0xffffffff), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_image_src(obj, &img_keyboard_image, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_image_recolor(obj, lv_color_hex(0xff373737), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_image_recolor_opa(obj, 255, LV_PART_MAIN | LV_STATE_DEFAULT); } { // SettingsModifyTrashButton @@ -4870,8 +4914,8 @@ void create_screen_main_screen() { // NodesFilterNameArea lv_obj_t *obj = lv_textarea_create(parent_obj); objects.nodes_filter_name_area = obj; - lv_obj_set_pos(obj, -8, -2); // was 40 - lv_obj_set_size(obj, LV_PCT(70), 24); // was 60 + lv_obj_set_pos(obj, -40, -2); + lv_obj_set_size(obj, LV_PCT(60), 24); lv_textarea_set_max_length(obj, 15); lv_textarea_set_placeholder_text(obj, "!Enter Filter ..."); lv_textarea_set_one_line(obj, true); @@ -4891,7 +4935,15 @@ void create_screen_main_screen() { // KeyboardButton_5 lv_obj_t *obj = lv_btn_create(parent_obj); objects.keyboard_button_5 = obj; - lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_pos(obj, -10, -2); + lv_obj_set_size(obj, 25, 24); + lv_obj_set_style_align(obj, LV_ALIGN_BOTTOM_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(obj, lv_color_hex(0xffffffff), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_image_src(obj, &img_keyboard_image, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_image_recolor(obj, lv_color_hex(0xff373737), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_image_recolor_opa(obj, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_shadow_width(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_shadow_ofs_y(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT); } } } @@ -5054,8 +5106,8 @@ void create_screen_main_screen() { // NodesHLNameArea lv_obj_t *obj = lv_textarea_create(parent_obj); objects.nodes_hl_name_area = obj; - lv_obj_set_pos(obj, -8, -2); - lv_obj_set_size(obj, LV_PCT(70), 24); + lv_obj_set_pos(obj, -40, -2); + lv_obj_set_size(obj, LV_PCT(60), 24); lv_textarea_set_max_length(obj, 15); lv_textarea_set_placeholder_text(obj, "Enter Filter ..."); lv_textarea_set_one_line(obj, true); @@ -5075,7 +5127,15 @@ void create_screen_main_screen() { // KeyboardButton_6 lv_obj_t *obj = lv_btn_create(parent_obj); objects.keyboard_button_6 = obj; - lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_pos(obj, -10, -2); + lv_obj_set_size(obj, 25, 24); + lv_obj_set_style_align(obj, LV_ALIGN_BOTTOM_RIGHT, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(obj, lv_color_hex(0xffffffff), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_image_src(obj, &img_keyboard_image, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_image_recolor(obj, lv_color_hex(0xff373737), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_image_recolor_opa(obj, 255, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_shadow_width(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_shadow_ofs_y(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT); } } } @@ -6155,7 +6215,13 @@ void create_screen_main_screen() { // KeyboardButton_10 lv_obj_t *obj = lv_btn_create(parent_obj); objects.keyboard_button_10 = obj; - lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_pos(obj, 200, 72); + lv_obj_set_size(obj, 25, 24); + lv_obj_set_style_align(obj, LV_ALIGN_DEFAULT, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(obj, lv_color_hex(0xffffffff), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_image_src(obj, &img_keyboard_image, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_image_recolor(obj, lv_color_hex(0xff373737), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_image_recolor_opa(obj, 255, LV_PART_MAIN | LV_STATE_DEFAULT); } { lv_obj_t *obj = lv_label_create(parent_obj); @@ -6171,7 +6237,7 @@ void create_screen_main_screen() { lv_obj_t *obj = lv_textarea_create(parent_obj); objects.setup_user_long_textarea = obj; lv_obj_set_pos(obj, 0, 119); - lv_obj_set_size(obj, LV_PCT(96), 25); + lv_obj_set_size(obj, LV_PCT(88), 25); lv_textarea_set_max_length(obj, 39); lv_textarea_set_one_line(obj, true); lv_textarea_set_password_mode(obj, false); @@ -6185,7 +6251,13 @@ void create_screen_main_screen() { // KeyboardButton_11 lv_obj_t *obj = lv_btn_create(parent_obj); objects.keyboard_button_11 = obj; - lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_pos(obj, 200, 121); + lv_obj_set_size(obj, 25, 24); + lv_obj_set_style_align(obj, LV_ALIGN_DEFAULT, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(obj, lv_color_hex(0xffffffff), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_image_src(obj, &img_keyboard_image, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_image_recolor(obj, lv_color_hex(0xff373737), LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_bg_image_recolor_opa(obj, 255, LV_PART_MAIN | LV_STATE_DEFAULT); } { lv_obj_t *obj = lv_obj_create(parent_obj); @@ -6243,7 +6315,7 @@ void create_screen_main_screen() { // Keyboard lv_obj_t *obj = lv_keyboard_create(parent_obj); objects.keyboard = obj; - /*lv_obj_set_pos(obj, 1, 28); + lv_obj_set_pos(obj, 1, 28); lv_obj_set_size(obj, LV_PCT(100), LV_PCT(50)); lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE); @@ -6252,8 +6324,7 @@ void create_screen_main_screen() { lv_obj_set_style_text_font(obj, &lv_font_montserrat_16, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_color(obj, lv_color_hex(0xffccd1d8), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_max_height(obj, 300, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_max_width(obj, 600, LV_PART_MAIN | LV_STATE_DEFAULT);*/ - lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_style_max_width(obj, 600, LV_PART_MAIN | LV_STATE_DEFAULT); } } From 0855320a96765e7ecba80a73f5ae6309b7c3c79a Mon Sep 17 00:00:00 2001 From: Juan Pena Date: Fri, 30 Jan 2026 20:25:36 -0500 Subject: [PATCH 13/14] Fix 3 --- generated/ui_320x240/screens.h | 148 ++++++++++++++++----------------- include/lv_conf.h | 4 +- source/graphics/TFT/Themes.cpp | 90 ++++++++++---------- 3 files changed, 121 insertions(+), 121 deletions(-) diff --git a/generated/ui_320x240/screens.h b/generated/ui_320x240/screens.h index c9deab19..ebd6e864 100644 --- a/generated/ui_320x240/screens.h +++ b/generated/ui_320x240/screens.h @@ -1,70 +1,70 @@ -#ifndef EEZ_LVGL_UI_SCREENS_H -#define EEZ_LVGL_UI_SCREENS_H - -#include "lvgl.h" - -#ifdef __cplusplus -extern "C" { -#endif - - -// advanced settings -extern lv_obj_t * ui_AdvancedSettingsPanel; -extern lv_obj_t * ui_SettingsTabView; -extern lv_obj_t * ui_TabPageGeneral; -extern lv_obj_t * ui_GeneralLanguageButton; -extern lv_obj_t * ui_LanguageLabel; -extern lv_obj_t * ui_GeneralTimezoneButton; -extern lv_obj_t * ui_TimezoneLabel; -extern lv_obj_t * ui_GeneralScreenButton; -extern lv_obj_t * ui_ScreenLabel; -extern lv_obj_t * ui_GeneralMapsButton; -extern lv_obj_t * ui_MapsLabel; -extern lv_obj_t * ui_GeneralAudioButton; -extern lv_obj_t * ui_AudioLabel1; -extern lv_obj_t * ui_TabPageRadio; -extern lv_obj_t * ui_RadioBluetoothButton; -extern lv_obj_t * ui_BluetoothLabel; -extern lv_obj_t * ui_RadioDeviceButton; -extern lv_obj_t * ui_DeviceLabel; -extern lv_obj_t * ui_RadioDisplayButton; -extern lv_obj_t * ui_DisplayLabel; -extern lv_obj_t * ui_RadioLoRaButton; -extern lv_obj_t * ui_LoRaLabel; -extern lv_obj_t * ui_RadioNetworkButton; -extern lv_obj_t * ui_NetworkLabel; -extern lv_obj_t * ui_RadioPositionButton; -extern lv_obj_t * ui_PositionLabel; -extern lv_obj_t * ui_RadioPowerButton; -extern lv_obj_t * ui_PowerLabel; -extern lv_obj_t * ui_TabPageModules; -extern lv_obj_t * ui_ModuleCannedMsgButton; -extern lv_obj_t * ui_CannedMsgLabel; -extern lv_obj_t * ui_ModuleSaFButton; -extern lv_obj_t * ui_StoreAndForwardLabel; -extern lv_obj_t * ui_ModuleTelemetryButton; -extern lv_obj_t * ui_TelemetryLabel; -extern lv_obj_t * ui_ModuleMQTTButton; -extern lv_obj_t * ui_MQTTLabel; -extern lv_obj_t * ui_ModuleRangeTestButton; -extern lv_obj_t * ui_RangeTestLabel; -extern lv_obj_t * ui_ModuleAudioButton; -extern lv_obj_t * ui_AudioLabel; -extern lv_obj_t * ui_ModuleSerialButton; -extern lv_obj_t * ui_SerialLabel; -extern lv_obj_t * ui_ModuleExtNotificationButton; -extern lv_obj_t * ui_ExtNotificationLabel; -extern lv_obj_t * ui_ModuleNeighborInfoButton; -extern lv_obj_t * ui_NeighborInfoLabel; -extern lv_obj_t * ui_ModuleAmbientLightingButton; -extern lv_obj_t * ui_AmbientLightingLabel; -extern lv_obj_t * ui_ModuleDetectionSensorButton; -extern lv_obj_t * ui_DetectionSensorLabel; -extern lv_obj_t * ui_ModuleRemoteHardwareButton; -extern lv_obj_t * ui_RemoteHardwareLabel; - -void create_tabview_settings(void); - +#ifndef EEZ_LVGL_UI_SCREENS_H +#define EEZ_LVGL_UI_SCREENS_H + +#include "lvgl.h" + +#ifdef __cplusplus +extern "C" { +#endif + + +// advanced settings +extern lv_obj_t * ui_AdvancedSettingsPanel; +extern lv_obj_t * ui_SettingsTabView; +extern lv_obj_t * ui_TabPageGeneral; +extern lv_obj_t * ui_GeneralLanguageButton; +extern lv_obj_t * ui_LanguageLabel; +extern lv_obj_t * ui_GeneralTimezoneButton; +extern lv_obj_t * ui_TimezoneLabel; +extern lv_obj_t * ui_GeneralScreenButton; +extern lv_obj_t * ui_ScreenLabel; +extern lv_obj_t * ui_GeneralMapsButton; +extern lv_obj_t * ui_MapsLabel; +extern lv_obj_t * ui_GeneralAudioButton; +extern lv_obj_t * ui_AudioLabel1; +extern lv_obj_t * ui_TabPageRadio; +extern lv_obj_t * ui_RadioBluetoothButton; +extern lv_obj_t * ui_BluetoothLabel; +extern lv_obj_t * ui_RadioDeviceButton; +extern lv_obj_t * ui_DeviceLabel; +extern lv_obj_t * ui_RadioDisplayButton; +extern lv_obj_t * ui_DisplayLabel; +extern lv_obj_t * ui_RadioLoRaButton; +extern lv_obj_t * ui_LoRaLabel; +extern lv_obj_t * ui_RadioNetworkButton; +extern lv_obj_t * ui_NetworkLabel; +extern lv_obj_t * ui_RadioPositionButton; +extern lv_obj_t * ui_PositionLabel; +extern lv_obj_t * ui_RadioPowerButton; +extern lv_obj_t * ui_PowerLabel; +extern lv_obj_t * ui_TabPageModules; +extern lv_obj_t * ui_ModuleCannedMsgButton; +extern lv_obj_t * ui_CannedMsgLabel; +extern lv_obj_t * ui_ModuleSaFButton; +extern lv_obj_t * ui_StoreAndForwardLabel; +extern lv_obj_t * ui_ModuleTelemetryButton; +extern lv_obj_t * ui_TelemetryLabel; +extern lv_obj_t * ui_ModuleMQTTButton; +extern lv_obj_t * ui_MQTTLabel; +extern lv_obj_t * ui_ModuleRangeTestButton; +extern lv_obj_t * ui_RangeTestLabel; +extern lv_obj_t * ui_ModuleAudioButton; +extern lv_obj_t * ui_AudioLabel; +extern lv_obj_t * ui_ModuleSerialButton; +extern lv_obj_t * ui_SerialLabel; +extern lv_obj_t * ui_ModuleExtNotificationButton; +extern lv_obj_t * ui_ExtNotificationLabel; +extern lv_obj_t * ui_ModuleNeighborInfoButton; +extern lv_obj_t * ui_NeighborInfoLabel; +extern lv_obj_t * ui_ModuleAmbientLightingButton; +extern lv_obj_t * ui_AmbientLightingLabel; +extern lv_obj_t * ui_ModuleDetectionSensorButton; +extern lv_obj_t * ui_DetectionSensorLabel; +extern lv_obj_t * ui_ModuleRemoteHardwareButton; +extern lv_obj_t * ui_RemoteHardwareLabel; + +void create_tabview_settings(void); + typedef struct _objects_t { lv_obj_t *boot_screen; lv_obj_t *main_screen; @@ -580,15 +580,15 @@ void tick_screen_calibration_screen(); void create_user_widget_ok_cancel_widget(lv_obj_t *parent_obj, int startWidgetIndex); void tick_user_widget_ok_cancel_widget(int startWidgetIndex); - + void tick_screen_by_id(enum ScreensEnum screenId); void tick_screen(int screen_index); void create_screens(); - - -#ifdef __cplusplus -} -#endif - + + +#ifdef __cplusplus +} +#endif + #endif /*EEZ_LVGL_UI_SCREENS_H*/ \ No newline at end of file diff --git a/include/lv_conf.h b/include/lv_conf.h index d01a5c07..bb20143d 100644 --- a/include/lv_conf.h +++ b/include/lv_conf.h @@ -83,7 +83,7 @@ *====================*/ /*Default display refresh, input device read and animation step period.*/ -#define LV_DEF_REFR_PERIOD 33 /*[ms]*/ //Switch to 30 fps +#define LV_DEF_REFR_PERIOD 40 /*[ms]*/ /*Default Dot Per Inch. Used to initialize default sizes such as widgets sized, style paddings. *(Not so important, you can adjust it to modify default sizes and spaces)*/ @@ -630,7 +630,7 @@ #define LV_USE_IMAGEBUTTON 1 -#define LV_USE_KEYBOARD 0 // Removed: Virtual keyboard not needed for T-Deck +#define LV_USE_KEYBOARD 1 #define LV_USE_LABEL 1 #if LV_USE_LABEL diff --git a/source/graphics/TFT/Themes.cpp b/source/graphics/TFT/Themes.cpp index 28f4e112..71521443 100644 --- a/source/graphics/TFT/Themes.cpp +++ b/source/graphics/TFT/Themes.cpp @@ -95,84 +95,84 @@ enum ThemeColor { uint32_t themeColor[][2] = { // dark, light - {0xff020712, 0xfff4f4f4}, // eMainScreenStyle - {0xff0f1423, 0xff67ea94}, // eTopPanelBg 0d0f1b + {0xff303030, 0xfff4f4f4}, // eMainScreenStyle + {0xff436C70, 0xff67ea94}, // eTopPanelBg {0xffE0E0E0, 0xff212121}, // eTopPanelText - {0xff0f1423, 0xff67ea94}, // eTopImageBg + {0xff436C70, 0xff67ea94}, // eTopImageBg {0xffffffff, 0xff212121}, // eTopImageRecolor {255, 255}, // eTopImageRecolorOpa {0xffffffff, 0xff212121}, // ePositiveImageRecolor, - {0xff020712, 0xfff4f4f0}, // ePanelBg - {0xff020712, 0xfffafafa}, // ePanelPressedBg + {0xff303030, 0xfff4f4f0}, // ePanelBg + {0xff303030, 0xfffafafa}, // ePanelPressedBg {0xfff0f0f0, 0xff212121}, // ePanelText - {0xff020712, 0xff67ea94}, // ePanelBorder - {0xff141723, 0xffffffff}, // eNodePanelBg BG of the whole pannel - {0xff141723, 0xff979797}, // eNodePanelBorder + {0xff67ea94, 0xff67ea94}, // ePanelBorder + {0xff404040, 0xffffffff}, // eNodePanelBg + {0xff808080, 0xff979797}, // eNodePanelBorder {0xfff0f0f0, 0xff212121}, // eNodePanelText - {0xff141723, 0xffffffff}, // eNodeButtonBg + {0xff404040, 0xffffffff}, // eNodeButtonBg {0, 0}, // eNodeButtonBgOpa - {0xff0a0f1c, 0xffffffff}, // eButtonPanelBg - {0xff0a0f1c, 0xffeaeae0}, // eMainButtonBg + {0xff585858, 0xffffffff}, // eButtonPanelBg + {0xff585858, 0xffeaeae0}, // eMainButtonBg {0xffaafbff, 0xff101010}, // eMainButtonText {0xff67ea94, 0xff67ea94}, // eMainButtonBorder {0xff9e9e9e, 0xffc0c0c0}, // eMainButtonShadow {0xff67ea94, 0xff757575}, // eMainButtonImageRecolor {0, 255}, // eMainButtonImageRecolorOpa - {0xff020712, 0xfffafaf4}, // eHomeContainerBg + {0xff303030, 0xfffafaf4}, // eHomeContainerBg {0xff67EA94, 0xffaaaaaa}, // eHomeContainerBorder {0xff2B824A, 0xff999999}, // eHomeContainerShadow {0xffaafbff, 0xff294337}, // eHomeContainerText - {0xff020712, 0xffffffff}, // eHomeButtonBg + {0xff303030, 0xffffffff}, // eHomeButtonBg {0xffffffff, 0xff101010}, // eHomeButtonText - {0xff020712, 0xffd0d0d0}, // eHomeButtonBorder + {0xff303030, 0xffd0d0d0}, // eHomeButtonBorder {0xff606060, 0xff57a6b3}, // eHomeButtonImageRecolor {0, 255}, // eHomeButtonImageRecolorOpa - {0xff141723, 0xfffafaf4}, // eChannelButtonBg //CHAT Button BG - {0xff141723, 0xffD0D0D0}, // eChannelButtonBorder + {0xff404040, 0xfffafaf4}, // eChannelButtonBg + {0xffA0A0A0, 0xffD0D0D0}, // eChannelButtonBorder {0xffffffff, 0xff101010}, // eChannelButtonText - {0xff020712, 0xfff0f0f0}, // eSettingsPanelBg + {0xff303030, 0xfff0f0f0}, // eSettingsPanelBg {0xffaafbff, 0xff003c9f}, // eSettingsPanelText {0, 0xff979797}, // eSettingsPanelBorder {0, 0xff7e7e7e}, // eSettingsPanelShadow {250, 250}, // eSettingsPanelBgOpa - {0xff141723, 0xffeaeae0}, // eSettingsButtonBg + {0xff505050, 0xffeaeae0}, // eSettingsButtonBg {0xffaafbff, 0xff294337}, // eSettingsButtonText - {0xff020712, 0xffd0d0d0}, // eSettingsButtonBorder + {0xff303030, 0xffd0d0d0}, // eSettingsButtonBorder {0, 0xff67ea94}, // eSettingsButtonImageRecolor {0, 255}, // eSettingsButtonImageRecolorOpa - {0xff141723, 0xffffffff}, // eSettingsLabelBg - {0xff141723, 0xff808080}, // eSettingsLabelBorder - {0xff020712, 0xfff4f4f4}, // eTabViewBg // Tabview background - {0xffaafbff, 0xff003c9f}, // eTabViewText // Tabview text color - {0xff020712, 0xffe0e0e0}, // eTabButtonDefaultBg //Tab buttons in tabview like filters and settings - {0xff020712, 0xffffffff}, // eTabButtonActiveBg //Tab buttons in tabview like filters and settings - {0xff67ea94, 0xffaafbff}, // eTabButtonPressedBg //Tab buttons in tabview like filters and settings - {0xffA0A0A0, 0xff606060}, // eTabButtonDefaultText //Tab buttons in tabview like filters and settings - {0xffffffff, 0xff101010}, // eTabButtonActiveText //Tab buttons in tabview like filters and settings - {0xffffffff, 0xffffffff}, // eTabButtonPressedText //Tab buttons in tabview like filters and settings - {0xff505050, 0xffb0b0b0}, // eTabButtonDefaultBorder //Tab buttons in tabview like filters and settings - {0xff141723, 0xfffbfce9}, // eChatMessageBg // Maybe in chats page each chat BG?? Rn Testing + {0xff404040, 0xffffffff}, // eSettingsLabelBg + {0xff404040, 0xff808080}, // eSettingsLabelBorder + {0xff303030, 0xfff4f4f4}, // eTabViewBg + {0xffaafbff, 0xff003c9f}, // eTabViewText + {0xff303030, 0xffe0e0e0}, // eTabButtonDefaultBg + {0xff303030, 0xffffffff}, // eTabButtonActiveBg + {0xff67ea94, 0xffaafbff}, // eTabButtonPressedBg + {0xffA0A0A0, 0xff606060}, // eTabButtonDefaultText + {0xffffffff, 0xff101010}, // eTabButtonActiveText + {0xffffffff, 0xffffffff}, // eTabButtonPressedText + {0xff505050, 0xffb0b0b0}, // eTabButtonDefaultBorder + {0xff303030, 0xfffbfce9}, // eChatMessageBg {255, 255}, // eChatMessageBgOpa {0xffffffff, 0xff294337}, // eChatMessageText - {0xff141723, 0xff888888}, // eChatMessageBorder // Maybe in chats page each chat border?? Rn Testing - {0xff141723, 0xffffffff}, // eNewMessageBg // Every individual chat new message BG + {0xff707070, 0xff888888}, // eChatMessageBorder + {0xff404040, 0xffffffff}, // eNewMessageBg {255, 255}, // eNewMessageBgOpa {0xffd0d0d0, 0xff294337}, // eNewMessageText - {0xff141723, 0xff888888}, // eNewMessageBorder // Every individual chat new message border - {0xff020712, 0xfffbfbfb}, // eAlertPanelBg - {0xff020712, 0xfff4f4f4}, // eBtnMatrixBorderMain // Lock Screen - {0xff67ea94, 0xff67ea94}, // eBtnMatrixBorderItems //Lock Screen - {0xff606060, 0xfffffff8}, // eBtnMatrixBgItems // Lock Screen - {0xffaafbff, 0xff212121}, // eBtnMatrixTextItems // Lock Screen - {0xff5ded96, 0xff212121}, // eBatteryPercentageText // changed + {0xff808080, 0xff888888}, // eNewMessageBorder + {0xff303030, 0xfffbfbfb}, // eAlertPanelBg + {0xff303030, 0xfff4f4f4}, // eBtnMatrixBorderMain + {0xff67ea94, 0xff67ea94}, // eBtnMatrixBorderItems + {0xff606060, 0xfffffff8}, // eBtnMatrixBgItems + {0xffaafbff, 0xff212121}, // eBtnMatrixTextItems + {0xffaafbff, 0xff212121}, // eBatteryPercentageText {0xffaafbff, 0xff003c9f}, // eColorTextLabel - {0xff404040, 0xffe0e0e0}, // eSpinnerMainArc // Arc at boot screen - {0xff67ea94, 0xff67ea94}, // eSpinnerIndicatorArc // Arc in things like scanner + {0xff404040, 0xffe0e0e0}, // eSpinnerMainArc + {0xff67ea94, 0xff67ea94}, // eSpinnerIndicatorArc {0xffaafbff, 0xff212121}, // eTableHeadingText, - {0xff020712, 0xfff4f4f0}, // eTableHeadingBg + {0xff303030, 0xfff4f4f0}, // eTableHeadingBg {0xffaafbff, 0xff212121}, // eTableItemText, - {0xff141723, 0xfff4f4f0}, // eTableItemBg - {0xff020712, 0xffd4d4d0}, // eTableItemDarkBg was 020712 testing rn + {0xff505050, 0xfff4f4f0}, // eTableItemBg + {0xff303030, 0xffd4d4d0}, // eTableItemDarkBg {0xff404040, 0xffe0e0e0}, // eTableBorder {0xff404040, 0xffe0e0e0} // eTableCellBorder }; From 19b68302f0b9f59cd15a1745de17c00679519d3b Mon Sep 17 00:00:00 2001 From: Juan Pena Date: Fri, 30 Jan 2026 21:11:28 -0500 Subject: [PATCH 14/14] Fix 4 --- source/graphics/TFT/TFTView_320x240.cpp | 88 ++++++++++--------------- 1 file changed, 36 insertions(+), 52 deletions(-) diff --git a/source/graphics/TFT/TFTView_320x240.cpp b/source/graphics/TFT/TFTView_320x240.cpp index f323d38a..0c08383f 100644 --- a/source/graphics/TFT/TFTView_320x240.cpp +++ b/source/graphics/TFT/TFTView_320x240.cpp @@ -730,7 +730,7 @@ void TFTView_320x240::ui_events_init(void) lv_obj_add_event_cb(objects.node_button, ui_event_NodeButton, LV_EVENT_ALL, (void *)ownNode); // nodes panel - infinite scroll support - lv_obj_add_event_cb(objects.nodes_panel, TFTView_320x240::ui_event_nodesPanelScroll, LV_EVENT_SCROLL, this); + lv_obj_add_event_cb(objects.nodes_panel, this->ui_event_nodesPanelScroll, LV_EVENT_SCROLL, NULL); // 8 channel buttons lv_obj_add_event_cb(objects.channel_button0, ui_event_ChannelButton, LV_EVENT_ALL, (void *)0); @@ -2395,54 +2395,51 @@ void TFTView_320x240::ui_event_nodesPanelScroll(lv_event_t *e) return; lv_event_code_t event_code = lv_event_get_code(e); - lv_obj_t *panel = (lv_obj_t *)lv_event_get_target(e); - // Only load more nodes when scroll ends (not during continuous scroll) + // Only do expensive updates on scroll END, not during continuous scroll if (event_code == LV_EVENT_SCROLL_END) { + // Load more nodes if near bottom + lv_obj_t *panel = (lv_obj_t *)lv_event_get_target(e); const lv_coord_t LOAD_DISTANCE = 400; - // Load more nodes when near bottom - if (lv_obj_get_scroll_bottom(panel) < LOAD_DISTANCE && self->nodesScrollDisplayLimit < MAX_NUM_NODES_VIEW) { - if (!self->nodesScrollLoadingMore) { - self->nodesScrollLoadingMore = true; + if (lv_obj_get_scroll_bottom(panel) < LOAD_DISTANCE && self->nodesScrollDisplayLimit < MAX_NUM_NODES_VIEW && + !self->nodesScrollLoadingMore) { + self->nodesScrollLoadingMore = true; - uint16_t new_limit = self->nodesScrollDisplayLimit + 10; - if (new_limit > MAX_NUM_NODES_VIEW) { - new_limit = MAX_NUM_NODES_VIEW; - } + uint16_t new_limit = self->nodesScrollDisplayLimit + 10; + if (new_limit > MAX_NUM_NODES_VIEW) { + new_limit = MAX_NUM_NODES_VIEW; + } - self->nodesScrollDisplayLimit = new_limit; - self->updateNodesFiltered(false); - lv_obj_update_layout(panel); + self->nodesScrollDisplayLimit = new_limit; + lv_obj_update_layout(panel); - ILOG_DEBUG("Loaded nodes batch, now displaying %d nodes", self->nodesScrollDisplayLimit); + ILOG_DEBUG("Loaded nodes batch, now displaying %d nodes", self->nodesScrollDisplayLimit); - self->nodesScrollLoadingMore = false; - } + self->nodesScrollLoadingMore = false; } + return; } - // During continuous scroll: only hide/show visible nodes (cheap operation) + // During continuous scroll: only hide/show visible nodes (no text/style updates) if (event_code != LV_EVENT_SCROLL) { return; } + lv_obj_t *panel = (lv_obj_t *)lv_event_get_target(e); + // Prevent re-entry during hide/show updates static bool scroll_running = false; if (scroll_running) return; scroll_running = true; - const int NODE_BUFFER = 3; // Keep 3 nodes visible on each side of viewport - - // Hide nodes with 3-node buffer strategy + // Hide nodes with 0-node buffer strategy - SINGLE PASS lv_coord_t scroll_y = lv_obj_get_scroll_y(panel); lv_coord_t panel_h = lv_obj_get_height(panel); - // Find first and last visible nodes - int first_visible_idx = -1; - int last_visible_idx = -1; + // Find first visible node AND hide/show in single loop int node_idx = 0; for (auto &node_pair : self->nodes) { @@ -2458,51 +2455,38 @@ void TFTView_320x240::ui_event_nodesPanelScroll(lv_event_t *e) lv_coord_t node_h = lv_obj_get_height(node_obj); // Check if node is within viewport - if (node_y + node_h >= scroll_y && node_y <= scroll_y + panel_h) { - if (first_visible_idx == -1) { - first_visible_idx = node_idx; + bool is_visible = (node_y + node_h >= scroll_y && node_y <= scroll_y + panel_h); + + if (is_visible) { + // Show this node + if (lv_obj_has_flag(node_obj, LV_OBJ_FLAG_HIDDEN)) { + lv_obj_clear_flag(node_obj, LV_OBJ_FLAG_HIDDEN); + } + } else { + // Hide this node + if (!lv_obj_has_flag(node_obj, LV_OBJ_FLAG_HIDDEN)) { + lv_obj_add_flag(node_obj, LV_OBJ_FLAG_HIDDEN); } - last_visible_idx = node_idx; } node_idx++; } - // Calculate buffer range: 3 nodes before first visible, 3 nodes after last visible - // If no visible nodes, show buffer around top - int buffer_start = 0; - int buffer_end = self->nodesScrollDisplayLimit - 1; - - if (first_visible_idx != -1) { - buffer_start = (first_visible_idx > NODE_BUFFER) ? (first_visible_idx - NODE_BUFFER) : 0; - buffer_end = last_visible_idx + NODE_BUFFER; - } - - // Now hide/show based on buffer range - node_idx = 0; + // Hide all nodes beyond display limit for (auto &node_pair : self->nodes) { lv_obj_t *node_obj = node_pair.second; if (!node_obj) continue; - bool in_buffer = (node_idx < self->nodesScrollDisplayLimit) && (node_idx >= buffer_start && node_idx <= buffer_end); + if (node_idx >= MAX_NUM_NODES_VIEW) + break; - if (in_buffer) { - // Show this node - if (lv_obj_has_flag(node_obj, LV_OBJ_FLAG_HIDDEN)) { - lv_obj_clear_flag(node_obj, LV_OBJ_FLAG_HIDDEN); - } - } else { - // Hide this node + if (node_idx >= self->nodesScrollDisplayLimit) { if (!lv_obj_has_flag(node_obj, LV_OBJ_FLAG_HIDDEN)) { lv_obj_add_flag(node_obj, LV_OBJ_FLAG_HIDDEN); } } - node_idx++; - - if (node_idx >= MAX_NUM_NODES_VIEW) - break; } scroll_running = false;