From 151892e1ceb6543a02fe47dc7bc941b30d72d2ad Mon Sep 17 00:00:00 2001 From: MarekZegare4 Date: Sun, 14 Jun 2026 13:21:59 +0200 Subject: [PATCH] refactor(ui): extract shared helpers, dedup screens, GPS shutdown via provider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pull repeated UI idioms into DisplayDriver and remove duplicated logic: - DisplayDriver: add drawScrollArrows() and drawInvertedHeader(); replaces 11 copy-pasted scroll-arrow blocks across 5 screens and 3 inverted title-bar blocks (NearbyScreen x2, NavView). Standardises the detail-view separator. - useImperial(): single source on UITask; NearbyScreen/TrailScreen delegate instead of each re-reading NodePrefs.units_imperial. - NearbyScreen: extract saveSelectedWaypoint() (two identical blocks -> one); drop the redundant local label buffer (WaypointStore truncates). - UITask::shutdown(): power GPS off through LocationProvider::stop() instead of poking PIN_GPS_EN directly — centralises enable/reset-pin + active-level logic. Net -58 lines. Verified: GAT562 (SSD1306) and WioTrackerL1Eink (GxEPD) solo builds compile clean. Co-Authored-By: Claude Sonnet 4.6 --- examples/companion_radio/ui-new/NavView.h | 9 +-- .../companion_radio/ui-new/NearbyScreen.h | 70 +++++-------------- .../companion_radio/ui-new/QuickMsgScreen.h | 37 ++++------ .../companion_radio/ui-new/SettingsScreen.h | 13 +--- examples/companion_radio/ui-new/ToolsScreen.h | 7 +- examples/companion_radio/ui-new/TrailScreen.h | 29 ++------ examples/companion_radio/ui-new/UITask.cpp | 20 +++--- examples/companion_radio/ui-new/UITask.h | 2 + src/helpers/ui/DisplayDriver.h | 23 ++++++ 9 files changed, 76 insertions(+), 134 deletions(-) diff --git a/examples/companion_radio/ui-new/NavView.h b/examples/companion_radio/ui-new/NavView.h index b3e9549e..ff8c67e9 100644 --- a/examples/companion_radio/ui-new/NavView.h +++ b/examples/companion_radio/ui-new/NavView.h @@ -24,14 +24,7 @@ inline void draw(DisplayDriver& d, const int hdr = d.headerH(); // Title bar: label, inverted (matches the Nearby/discover detail style). - char title[24]; - d.translateUTF8ToBlocks(title, (label && label[0]) ? label : "Navigate", sizeof(title)); - d.setColor(DisplayDriver::LIGHT); - d.fillRect(0, 0, d.width(), hdr - 1); - d.setColor(DisplayDriver::DARK); - d.drawTextEllipsized(2, 1, d.width() - 4, title); - d.setColor(DisplayDriver::LIGHT); - d.fillRect(0, hdr - 1, d.width(), d.sepH()); + d.drawInvertedHeader((label && label[0]) ? label : "Navigate"); if (!own_valid) { d.drawTextCentered(cx, hdr + (d.height() - hdr) / 2 - d.getLineHeight(), "No GPS fix"); diff --git a/examples/companion_radio/ui-new/NearbyScreen.h b/examples/companion_radio/ui-new/NearbyScreen.h index ed0d05be..24cdeaee 100644 --- a/examples/companion_radio/ui-new/NearbyScreen.h +++ b/examples/companion_radio/ui-new/NearbyScreen.h @@ -87,10 +87,15 @@ class NearbyScreen : public UIScreen { // Geographic helpers live in GeoUtils.h (geo::) — shared with Waypoints / // trail course-over-ground. Call sites use geo:: directly. - // Global metric/imperial preference for distance display. - bool useImperial() const { - NodePrefs* p = _task ? _task->getNodePrefs() : nullptr; - return p && p->units_imperial; + bool useImperial() const { return _task && _task->useImperial(); } + + // Save the currently-selected list entry as a waypoint (its name as label). + // Shared by the list context menu and the detail Options menu. + void saveSelectedWaypoint() { + if (_sel >= _count) return; + const Entry& e = _entries[_sel]; + if (e.lat_e6 == 0 && e.lon_e6 == 0) { _task->showAlert("No node GPS", 1000); return; } + _task->addWaypoint(e.lat_e6, e.lon_e6, e.name); // WaypointStore truncates the label } static void fmtAge(char* buf, int n, uint32_t lastmod) { @@ -310,14 +315,7 @@ class NearbyScreen : public UIScreen { char label[32]; if (r.name[0]) { strncpy(label, r.name, 31); label[31] = '\0'; } else { snprintf(label, sizeof(label), "[%s]", fullType); } - char filtered[32]; - display.translateUTF8ToBlocks(filtered, label, sizeof(filtered)); - display.setColor(DisplayDriver::LIGHT); - display.fillRect(0, 0, display.width(), hdr - 1); - display.setColor(DisplayDriver::DARK); - display.drawTextEllipsized(2, 1, display.width() - 4, filtered); - display.setColor(DisplayDriver::LIGHT); - display.fillRect(0, hdr - 1, display.width(), 1); + display.drawInvertedHeader(label); char b64[48]; pubKeyToBase64(r.pub_key, b64, sizeof(b64)); @@ -365,13 +363,7 @@ class NearbyScreen : public UIScreen { const Entry& e = _entries[_sel]; const int hdr = display.headerH(); - display.setColor(DisplayDriver::LIGHT); - display.fillRect(0, 0, display.width(), hdr - 1); - display.setColor(DisplayDriver::DARK); - char filtered[32]; - display.translateUTF8ToBlocks(filtered, e.name, sizeof(filtered)); - display.drawTextEllipsized(2, 1, display.width() - 4, filtered); - display.setColor(DisplayDriver::LIGHT); + display.drawInvertedHeader(e.name); int step = display.lineStep(); if (step * 5 > display.height() - hdr) step = (display.height() - hdr) / 5; @@ -486,12 +478,8 @@ class NearbyScreen : public UIScreen { display.drawTextEllipsized(3, y + lh + 2, display.width() - 6, sig); } - display.setColor(DisplayDriver::LIGHT); - int cw = display.getCharWidth(); - if (_dscroll > 0) - { display.setCursor(display.width() - cw, d_start_y); display.print("^"); } - if (_dscroll + _d_visible < _dresult_count) - { display.setCursor(display.width() - cw, d_start_y + (_d_visible - 1) * d_item_h); display.print("v"); } + display.drawScrollArrows(d_start_y, d_start_y + (_d_visible - 1) * d_item_h, + _dscroll > 0, _dscroll + _d_visible < _dresult_count); } updatePingMenuState(); @@ -665,12 +653,8 @@ public: display.print(right); } - display.setColor(DisplayDriver::LIGHT); - int cw = display.getCharWidth(); - if (_scroll > 0) - { display.setCursor(display.width() - cw, start_y); display.print("^"); } - if (_scroll + _visible < _count) - { display.setCursor(display.width() - cw, start_y + (_visible - 1) * item_h); display.print("v"); } + display.drawScrollArrows(start_y, start_y + (_visible - 1) * item_h, + _scroll > 0, _scroll + _visible < _count); } if (_ctx_menu.active) { @@ -709,16 +693,8 @@ public: uint8_t pk[PUB_KEY_SIZE]; openPingMenu(); if (selectedStoredPubKey(pk)) startPingForKey(pk); - } else if (idx == 2 && _sel < _count) { // Save waypoint - const Entry& e = _entries[_sel]; - if (e.lat_e6 == 0 && e.lon_e6 == 0) { - _task->showAlert("No node GPS", 1000); - } else { - char label[WAYPOINT_LABEL_LEN]; - strncpy(label, e.name, WAYPOINT_LABEL_LEN - 1); - label[WAYPOINT_LABEL_LEN - 1] = '\0'; - _task->addWaypoint(e.lat_e6, e.lon_e6, label); - } + } else if (idx == 2) { // Save waypoint + saveSelectedWaypoint(); } } return true; @@ -754,16 +730,8 @@ public: const Entry& e = _entries[_sel]; if (e.lat_e6 != 0 || e.lon_e6 != 0) _nav = true; else _task->showAlert("No node GPS", 1000); - } else if (sel == 2 && _sel < _count) { - const Entry& e = _entries[_sel]; - if (e.lat_e6 == 0 && e.lon_e6 == 0) { - _task->showAlert("No node GPS", 1000); - } else { - char label[WAYPOINT_LABEL_LEN]; - strncpy(label, e.name, WAYPOINT_LABEL_LEN - 1); - label[WAYPOINT_LABEL_LEN - 1] = '\0'; - _task->addWaypoint(e.lat_e6, e.lon_e6, label); - } + } else if (sel == 2) { // Save waypoint + saveSelectedWaypoint(); } } return true; diff --git a/examples/companion_radio/ui-new/QuickMsgScreen.h b/examples/companion_radio/ui-new/QuickMsgScreen.h index 2343fb67..3bef5d50 100644 --- a/examples/companion_radio/ui-new/QuickMsgScreen.h +++ b/examples/companion_radio/ui-new/QuickMsgScreen.h @@ -715,9 +715,8 @@ public: } } } - display.setColor(DisplayDriver::LIGHT); - if (_contact_scroll > 0) { display.setCursor(display.width() - cw, start_y); display.print("^"); } - if (_contact_scroll + _visible < _num_contacts) { display.setCursor(display.width() - cw, start_y + (_visible-1)*item_h); display.print("v"); } + display.drawScrollArrows(start_y, start_y + (_visible-1)*item_h, + _contact_scroll > 0, _contact_scroll + _visible < _num_contacts); // Context menu overlay if (_ctx_menu.active) _ctx_menu.render(display); @@ -755,9 +754,8 @@ public: } } } - display.setColor(DisplayDriver::LIGHT); - if (_channel_scroll > 0) { display.setCursor(display.width() - cw, start_y); display.print("^"); } - if (_channel_scroll + _visible < _num_channels) { display.setCursor(display.width() - cw, start_y + (_visible-1)*item_h); display.print("v"); } + display.drawScrollArrows(start_y, start_y + (_visible-1)*item_h, + _channel_scroll > 0, _channel_scroll + _visible < _num_channels); // Context menu overlay if (_ctx_menu.active) _ctx_menu.render(display); @@ -865,15 +863,10 @@ public: display.drawTextCentered(display.width()/2, display.height()/2, "No messages yet"); } - display.setColor(DisplayDriver::LIGHT); - if (_dm_hist_scroll > 0) { - display.setCursor(display.width() - cw, hist_start_y + 1); - display.print("^"); - } - if (_dm_hist_scroll + _hist_visible < dm_count) { + { int arrow_y = (n_vis > 0) ? box_ys[n_vis - 1] + box_hs[n_vis - 1] - lh : hist_start_y; - display.setCursor(display.width() - cw, arrow_y); - display.print("v"); + display.drawScrollArrows(hist_start_y + 1, arrow_y, + _dm_hist_scroll > 0, _dm_hist_scroll + _hist_visible < dm_count); } bool compose_sel = (_dm_hist_sel == -1); @@ -1014,15 +1007,10 @@ public: } // scroll hints - display.setColor(DisplayDriver::LIGHT); - if (_hist_scroll > 0) { - display.setCursor(display.width() - cw, hist_start_y + 1); - display.print("^"); - } - if (_hist_scroll + _hist_visible < ch_hist_count) { + { int arrow_y = (n_vis > 0) ? box_ys[n_vis - 1] + box_hs[n_vis - 1] - lh : hist_start_y; - display.setCursor(display.width() - cw, arrow_y); - display.print("v"); + display.drawScrollArrows(hist_start_y + 1, arrow_y, + _hist_scroll > 0, _hist_scroll + _hist_visible < ch_hist_count); } // small compose button (bottom-left, always bordered, inverted when selected) @@ -1086,9 +1074,8 @@ public: display.drawTextEllipsized(cw + 2, y, display.width() - cw - 4, tmpl); } } - display.setColor(DisplayDriver::LIGHT); - if (_msg_scroll > 0) { display.setCursor(display.width() - cw, start_y); display.print("^"); } - if (_msg_scroll + _visible < total_msg_items) { display.setCursor(display.width() - cw, start_y + (_visible-1)*item_h); display.print("v"); } + display.drawScrollArrows(start_y, start_y + (_visible-1)*item_h, + _msg_scroll > 0, _msg_scroll + _visible < total_msg_items); } return 2000; } diff --git a/examples/companion_radio/ui-new/SettingsScreen.h b/examples/companion_radio/ui-new/SettingsScreen.h index 676a567e..4432374b 100644 --- a/examples/companion_radio/ui-new/SettingsScreen.h +++ b/examples/companion_radio/ui-new/SettingsScreen.h @@ -619,17 +619,8 @@ public: renderItem(display, _vis[_scroll + i], start_y + i * item_h); } - // scroll indicators - if (_scroll > 0) { - display.setColor(DisplayDriver::LIGHT); - display.setCursor(display.width() - display.getCharWidth(), start_y); - display.print("^"); - } - if (_scroll + _visible < _vis_count) { - display.setColor(DisplayDriver::LIGHT); - display.setCursor(display.width() - display.getCharWidth(), start_y + (_visible - 1) * item_h); - display.print("v"); - } + display.drawScrollArrows(start_y, start_y + (_visible - 1) * item_h, + _scroll > 0, _scroll + _visible < _vis_count); return 2000; } diff --git a/examples/companion_radio/ui-new/ToolsScreen.h b/examples/companion_radio/ui-new/ToolsScreen.h index f48b20b0..271dcf71 100644 --- a/examples/companion_radio/ui-new/ToolsScreen.h +++ b/examples/companion_radio/ui-new/ToolsScreen.h @@ -39,11 +39,8 @@ public: display.setCursor(cw + 2, y); display.print(ITEMS[idx]); } - display.setColor(DisplayDriver::LIGHT); - if (_scroll > 0) - { display.setCursor(display.width() - cw, start_y); display.print("^"); } - if (_scroll + vis < ITEM_COUNT) - { display.setCursor(display.width() - cw, start_y + (vis - 1) * item_h); display.print("v"); } + display.drawScrollArrows(start_y, start_y + (vis - 1) * item_h, + _scroll > 0, _scroll + vis < ITEM_COUNT); return 500; } diff --git a/examples/companion_radio/ui-new/TrailScreen.h b/examples/companion_radio/ui-new/TrailScreen.h index 6e90c1c3..631d8473 100644 --- a/examples/companion_radio/ui-new/TrailScreen.h +++ b/examples/companion_radio/ui-new/TrailScreen.h @@ -406,11 +406,7 @@ private: bool ownPos(int32_t& lat, int32_t& lon) const { return _task->currentLocation(lat, lon); } - // Global metric/imperial preference for distance display. - bool useImperial() const { - NodePrefs* p = _task->getNodePrefs(); - return p && p->units_imperial; - } + bool useImperial() const { return _task && _task->useImperial(); } void handleMarkHere() { int32_t lat, lon; @@ -683,15 +679,9 @@ private: display.print(buf); } - int cw = display.getCharWidth(); - if (_summary_scroll > 0) { - display.setCursor(display.width() - cw, y0); - display.print("^"); - } - if (_summary_scroll + visible < SUMMARY_ITEM_COUNT) { - display.setCursor(display.width() - cw, y0 + (visible - 1) * step); - display.print("v"); - } + display.drawScrollArrows(y0, y0 + (visible - 1) * step, + _summary_scroll > 0, + _summary_scroll + visible < SUMMARY_ITEM_COUNT); } void renderList(DisplayDriver& display) { @@ -743,15 +733,8 @@ private: display.print(row); } - int cw = display.getCharWidth(); - if (_list_scroll > 0) { - display.setCursor(display.width() - cw, top); - display.print("^"); - } - if (_list_scroll + visible < total) { - display.setCursor(display.width() - cw, top + (visible - 1) * step); - display.print("v"); - } + display.drawScrollArrows(top, top + (visible - 1) * step, + _list_scroll > 0, _list_scroll + visible < total); } void renderMap(DisplayDriver& display) { diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 01f867bd..895db1cc 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -1519,17 +1519,15 @@ void UITask::shutdown(bool restart){ } else { _display->turnOff(); radio_driver.powerOff(); -#ifdef PIN_GPS_EN - // Power off GPS before SYSTEMOFF — GPIO pins retain state in NRF52 SYSTEMOFF, - // so without this the GPS stays powered and drains the battery. - // gps_enabled is already persisted to flash; applyGpsPrefs() restores it on next boot. - // Use the same active level as MicroNMEALocationProvider::stop() (default active-high). - #ifndef PIN_GPS_EN_ACTIVE - #define PIN_GPS_EN_ACTIVE HIGH - #endif - pinMode(PIN_GPS_EN, OUTPUT); - digitalWrite(PIN_GPS_EN, !PIN_GPS_EN_ACTIVE); -#endif + // Power GPS down through its provider before SYSTEMOFF — GPIO pins retain + // state in NRF52 SYSTEMOFF, so otherwise the module keeps draining the + // battery. The provider handles the enable + reset pins and the correct + // active level. gps_enabled is persisted; applyGpsPrefs() restores it on + // the next boot. + if (_sensors) { + LocationProvider* loc = _sensors->getLocationProvider(); + if (loc) loc->stop(); + } _board->powerOff(); } } diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 8fd6075f..5b945ccf 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -135,6 +135,8 @@ public: void onBLEDisconnected() override { _next_refresh = 0; } NodePrefs* getNodePrefs() const { return _node_prefs; } + // Global metric/imperial preference for distance/speed display. + bool useImperial() const { return _node_prefs && _node_prefs->units_imperial; } uint16_t getBattMilliVolts() const { return _batt_mv > 0 ? _batt_mv : AbstractUITask::getBattMilliVolts(); } void gotoHomeScreen() { setCurrScreen(home); } void gotoSettingsScreen(); diff --git a/src/helpers/ui/DisplayDriver.h b/src/helpers/ui/DisplayDriver.h index d2716d38..85f9f709 100644 --- a/src/helpers/ui/DisplayDriver.h +++ b/src/helpers/ui/DisplayDriver.h @@ -162,6 +162,29 @@ public: } } + // Up/down scroll indicators in the right-edge column. top_y is the first + // row's y, bottom_y the last visible row's y. Replaces the 4-line + // setCursor/print("^")/setCursor/print("v") block in every scrollable list. + void drawScrollArrows(int top_y, int bottom_y, bool more_up, bool more_down) { + int x = width() - getCharWidth(); + setColor(LIGHT); + if (more_up) { setCursor(x, top_y); print("^"); } + if (more_down) { setCursor(x, bottom_y); print("v"); } + } + + // Inverted title bar: light background, dark ellipsized label, then the + // standard separator line. The label is UTF-8 translated by + // drawTextEllipsized. Leaves ink colour LIGHT for following content. + void drawInvertedHeader(const char* label) { + int hdr = headerH(); + setColor(LIGHT); + fillRect(0, 0, width(), hdr - 1); + setColor(DARK); + drawTextEllipsized(2, 1, width() - 4, (label && label[0]) ? label : ""); + setColor(LIGHT); + fillRect(0, hdr - 1, width(), sepH()); + } + // Advance a UTF-8 pointer by one codepoint, returning the decoded value. // Invalid sequences return 0xFFFD and consume trailing continuation bytes. static uint32_t decodeCodepoint(const uint8_t*& p) {