diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 00000000..7a63f990 Binary files /dev/null and b/.DS_Store differ diff --git a/.github/.DS_Store b/.github/.DS_Store new file mode 100644 index 00000000..32b2e7ea Binary files /dev/null and b/.github/.DS_Store differ diff --git a/.trunk/.DS_Store b/.trunk/.DS_Store new file mode 100644 index 00000000..33da6af0 Binary files /dev/null and b/.trunk/.DS_Store differ diff --git a/generated/.DS_Store b/generated/.DS_Store new file mode 100644 index 00000000..e17b0867 Binary files /dev/null and b/generated/.DS_Store differ diff --git a/include/.DS_Store b/include/.DS_Store new file mode 100644 index 00000000..f8a8c17b Binary files /dev/null and b/include/.DS_Store differ 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/.DS_Store b/source/.DS_Store new file mode 100644 index 00000000..4a142a18 Binary files /dev/null and b/source/.DS_Store differ diff --git a/source/graphics/TFT/TFTView_320x240.cpp b/source/graphics/TFT/TFTView_320x240.cpp index e65e60bf..0c08383f 100644 --- a/source/graphics/TFT/TFTView_320x240.cpp +++ b/source/graphics/TFT/TFTView_320x240.cpp @@ -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{} { @@ -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) { @@ -728,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, 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); lv_obj_add_event_cb(objects.channel_button1, ui_event_ChannelButton, LV_EVENT_ALL, (void *)1); @@ -918,7 +922,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 @@ -2378,6 +2382,116 @@ 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); + + // 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; + + 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; + } + + self->nodesScrollDisplayLimit = new_limit; + 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 (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; + + // 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 visible node AND hide/show in single loop + 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 + 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); + } + } + + node_idx++; + } + + // Hide all nodes beyond display limit + for (auto &node_pair : self->nodes) { + lv_obj_t *node_obj = node_pair.second; + if (!node_obj) + continue; + + if (node_idx >= MAX_NUM_NODES_VIEW) + break; + + 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++; + } + + scroll_running = false; +} + void TFTView_320x240::ui_screen_event_cb(lv_event_t *e) { if (THIS->activePanel == objects.map_panel) { @@ -4685,6 +4799,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); // 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 @@ -4882,6 +5005,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 +5116,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 +5142,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 +5192,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 +5206,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); } } } @@ -6546,6 +6676,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]; @@ -6899,7 +7030,6 @@ void TFTView_320x240::setGroupFocus(lv_obj_t *panel) 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) { @@ -7020,19 +7150,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(); @@ -7366,4 +7504,4 @@ void action_on_boot_screen_displayed(lv_event_t *e) } } -#endif +#endif \ No newline at end of file