refactor(ui): extract shared helpers, dedup screens, GPS shutdown via provider

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 <noreply@anthropic.com>
This commit is contained in:
MarekZegare4
2026-06-14 13:21:59 +02:00
co-authored by Claude Sonnet 4.6
parent 1521be3803
commit 151892e1ce
9 changed files with 76 additions and 134 deletions
+1 -8
View File
@@ -24,14 +24,7 @@ inline void draw(DisplayDriver& d,
const int hdr = d.headerH(); const int hdr = d.headerH();
// Title bar: label, inverted (matches the Nearby/discover detail style). // Title bar: label, inverted (matches the Nearby/discover detail style).
char title[24]; d.drawInvertedHeader((label && label[0]) ? label : "Navigate");
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());
if (!own_valid) { if (!own_valid) {
d.drawTextCentered(cx, hdr + (d.height() - hdr) / 2 - d.getLineHeight(), "No GPS fix"); d.drawTextCentered(cx, hdr + (d.height() - hdr) / 2 - d.getLineHeight(), "No GPS fix");
+19 -51
View File
@@ -87,10 +87,15 @@ class NearbyScreen : public UIScreen {
// Geographic helpers live in GeoUtils.h (geo::) — shared with Waypoints / // Geographic helpers live in GeoUtils.h (geo::) — shared with Waypoints /
// trail course-over-ground. Call sites use geo:: directly. // trail course-over-ground. Call sites use geo:: directly.
// Global metric/imperial preference for distance display. bool useImperial() const { return _task && _task->useImperial(); }
bool useImperial() const {
NodePrefs* p = _task ? _task->getNodePrefs() : nullptr; // Save the currently-selected list entry as a waypoint (its name as label).
return p && p->units_imperial; // 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) { static void fmtAge(char* buf, int n, uint32_t lastmod) {
@@ -310,14 +315,7 @@ class NearbyScreen : public UIScreen {
char label[32]; char label[32];
if (r.name[0]) { strncpy(label, r.name, 31); label[31] = '\0'; } if (r.name[0]) { strncpy(label, r.name, 31); label[31] = '\0'; }
else { snprintf(label, sizeof(label), "[%s]", fullType); } else { snprintf(label, sizeof(label), "[%s]", fullType); }
char filtered[32]; display.drawInvertedHeader(label);
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);
char b64[48]; char b64[48];
pubKeyToBase64(r.pub_key, b64, sizeof(b64)); pubKeyToBase64(r.pub_key, b64, sizeof(b64));
@@ -365,13 +363,7 @@ class NearbyScreen : public UIScreen {
const Entry& e = _entries[_sel]; const Entry& e = _entries[_sel];
const int hdr = display.headerH(); const int hdr = display.headerH();
display.setColor(DisplayDriver::LIGHT); display.drawInvertedHeader(e.name);
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);
int step = display.lineStep(); int step = display.lineStep();
if (step * 5 > display.height() - hdr) step = (display.height() - hdr) / 5; 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.drawTextEllipsized(3, y + lh + 2, display.width() - 6, sig);
} }
display.setColor(DisplayDriver::LIGHT); display.drawScrollArrows(d_start_y, d_start_y + (_d_visible - 1) * d_item_h,
int cw = display.getCharWidth(); _dscroll > 0, _dscroll + _d_visible < _dresult_count);
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"); }
} }
updatePingMenuState(); updatePingMenuState();
@@ -665,12 +653,8 @@ public:
display.print(right); display.print(right);
} }
display.setColor(DisplayDriver::LIGHT); display.drawScrollArrows(start_y, start_y + (_visible - 1) * item_h,
int cw = display.getCharWidth(); _scroll > 0, _scroll + _visible < _count);
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"); }
} }
if (_ctx_menu.active) { if (_ctx_menu.active) {
@@ -709,16 +693,8 @@ public:
uint8_t pk[PUB_KEY_SIZE]; uint8_t pk[PUB_KEY_SIZE];
openPingMenu(); openPingMenu();
if (selectedStoredPubKey(pk)) startPingForKey(pk); if (selectedStoredPubKey(pk)) startPingForKey(pk);
} else if (idx == 2 && _sel < _count) { // Save waypoint } else if (idx == 2) { // Save waypoint
const Entry& e = _entries[_sel]; saveSelectedWaypoint();
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);
}
} }
} }
return true; return true;
@@ -754,16 +730,8 @@ public:
const Entry& e = _entries[_sel]; const Entry& e = _entries[_sel];
if (e.lat_e6 != 0 || e.lon_e6 != 0) _nav = true; if (e.lat_e6 != 0 || e.lon_e6 != 0) _nav = true;
else _task->showAlert("No node GPS", 1000); else _task->showAlert("No node GPS", 1000);
} else if (sel == 2 && _sel < _count) { } else if (sel == 2) { // Save waypoint
const Entry& e = _entries[_sel]; saveSelectedWaypoint();
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);
}
} }
} }
return true; return true;
@@ -715,9 +715,8 @@ public:
} }
} }
} }
display.setColor(DisplayDriver::LIGHT); display.drawScrollArrows(start_y, start_y + (_visible-1)*item_h,
if (_contact_scroll > 0) { display.setCursor(display.width() - cw, start_y); display.print("^"); } _contact_scroll > 0, _contact_scroll + _visible < _num_contacts);
if (_contact_scroll + _visible < _num_contacts) { display.setCursor(display.width() - cw, start_y + (_visible-1)*item_h); display.print("v"); }
// Context menu overlay // Context menu overlay
if (_ctx_menu.active) _ctx_menu.render(display); if (_ctx_menu.active) _ctx_menu.render(display);
@@ -755,9 +754,8 @@ public:
} }
} }
} }
display.setColor(DisplayDriver::LIGHT); display.drawScrollArrows(start_y, start_y + (_visible-1)*item_h,
if (_channel_scroll > 0) { display.setCursor(display.width() - cw, start_y); display.print("^"); } _channel_scroll > 0, _channel_scroll + _visible < _num_channels);
if (_channel_scroll + _visible < _num_channels) { display.setCursor(display.width() - cw, start_y + (_visible-1)*item_h); display.print("v"); }
// Context menu overlay // Context menu overlay
if (_ctx_menu.active) _ctx_menu.render(display); 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.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; 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.drawScrollArrows(hist_start_y + 1, arrow_y,
display.print("v"); _dm_hist_scroll > 0, _dm_hist_scroll + _hist_visible < dm_count);
} }
bool compose_sel = (_dm_hist_sel == -1); bool compose_sel = (_dm_hist_sel == -1);
@@ -1014,15 +1007,10 @@ public:
} }
// scroll hints // 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; 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.drawScrollArrows(hist_start_y + 1, arrow_y,
display.print("v"); _hist_scroll > 0, _hist_scroll + _hist_visible < ch_hist_count);
} }
// small compose button (bottom-left, always bordered, inverted when selected) // 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.drawTextEllipsized(cw + 2, y, display.width() - cw - 4, tmpl);
} }
} }
display.setColor(DisplayDriver::LIGHT); display.drawScrollArrows(start_y, start_y + (_visible-1)*item_h,
if (_msg_scroll > 0) { display.setCursor(display.width() - cw, start_y); display.print("^"); } _msg_scroll > 0, _msg_scroll + _visible < total_msg_items);
if (_msg_scroll + _visible < total_msg_items) { display.setCursor(display.width() - cw, start_y + (_visible-1)*item_h); display.print("v"); }
} }
return 2000; return 2000;
} }
@@ -619,17 +619,8 @@ public:
renderItem(display, _vis[_scroll + i], start_y + i * item_h); renderItem(display, _vis[_scroll + i], start_y + i * item_h);
} }
// scroll indicators display.drawScrollArrows(start_y, start_y + (_visible - 1) * item_h,
if (_scroll > 0) { _scroll > 0, _scroll + _visible < _vis_count);
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");
}
return 2000; return 2000;
} }
@@ -39,11 +39,8 @@ public:
display.setCursor(cw + 2, y); display.setCursor(cw + 2, y);
display.print(ITEMS[idx]); display.print(ITEMS[idx]);
} }
display.setColor(DisplayDriver::LIGHT); display.drawScrollArrows(start_y, start_y + (vis - 1) * item_h,
if (_scroll > 0) _scroll > 0, _scroll + vis < ITEM_COUNT);
{ 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"); }
return 500; return 500;
} }
+6 -23
View File
@@ -406,11 +406,7 @@ private:
bool ownPos(int32_t& lat, int32_t& lon) const { return _task->currentLocation(lat, lon); } bool ownPos(int32_t& lat, int32_t& lon) const { return _task->currentLocation(lat, lon); }
// Global metric/imperial preference for distance display. bool useImperial() const { return _task && _task->useImperial(); }
bool useImperial() const {
NodePrefs* p = _task->getNodePrefs();
return p && p->units_imperial;
}
void handleMarkHere() { void handleMarkHere() {
int32_t lat, lon; int32_t lat, lon;
@@ -683,15 +679,9 @@ private:
display.print(buf); display.print(buf);
} }
int cw = display.getCharWidth(); display.drawScrollArrows(y0, y0 + (visible - 1) * step,
if (_summary_scroll > 0) { _summary_scroll > 0,
display.setCursor(display.width() - cw, y0); _summary_scroll + visible < SUMMARY_ITEM_COUNT);
display.print("^");
}
if (_summary_scroll + visible < SUMMARY_ITEM_COUNT) {
display.setCursor(display.width() - cw, y0 + (visible - 1) * step);
display.print("v");
}
} }
void renderList(DisplayDriver& display) { void renderList(DisplayDriver& display) {
@@ -743,15 +733,8 @@ private:
display.print(row); display.print(row);
} }
int cw = display.getCharWidth(); display.drawScrollArrows(top, top + (visible - 1) * step,
if (_list_scroll > 0) { _list_scroll > 0, _list_scroll + visible < total);
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");
}
} }
void renderMap(DisplayDriver& display) { void renderMap(DisplayDriver& display) {
+9 -11
View File
@@ -1519,17 +1519,15 @@ void UITask::shutdown(bool restart){
} else { } else {
_display->turnOff(); _display->turnOff();
radio_driver.powerOff(); radio_driver.powerOff();
#ifdef PIN_GPS_EN // Power GPS down through its provider before SYSTEMOFF — GPIO pins retain
// Power off GPS before SYSTEMOFF — GPIO pins retain state in NRF52 SYSTEMOFF, // state in NRF52 SYSTEMOFF, so otherwise the module keeps draining the
// so without this the GPS stays powered and drains the battery. // battery. The provider handles the enable + reset pins and the correct
// gps_enabled is already persisted to flash; applyGpsPrefs() restores it on next boot. // active level. gps_enabled is persisted; applyGpsPrefs() restores it on
// Use the same active level as MicroNMEALocationProvider::stop() (default active-high). // the next boot.
#ifndef PIN_GPS_EN_ACTIVE if (_sensors) {
#define PIN_GPS_EN_ACTIVE HIGH LocationProvider* loc = _sensors->getLocationProvider();
#endif if (loc) loc->stop();
pinMode(PIN_GPS_EN, OUTPUT); }
digitalWrite(PIN_GPS_EN, !PIN_GPS_EN_ACTIVE);
#endif
_board->powerOff(); _board->powerOff();
} }
} }
+2
View File
@@ -135,6 +135,8 @@ public:
void onBLEDisconnected() override { _next_refresh = 0; } void onBLEDisconnected() override { _next_refresh = 0; }
NodePrefs* getNodePrefs() const { return _node_prefs; } 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(); } uint16_t getBattMilliVolts() const { return _batt_mv > 0 ? _batt_mv : AbstractUITask::getBattMilliVolts(); }
void gotoHomeScreen() { setCurrScreen(home); } void gotoHomeScreen() { setCurrScreen(home); }
void gotoSettingsScreen(); void gotoSettingsScreen();
+23
View File
@@ -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. // Advance a UTF-8 pointer by one codepoint, returning the decoded value.
// Invalid sequences return 0xFFFD and consume trailing continuation bytes. // Invalid sequences return 0xFFFD and consume trailing continuation bytes.
static uint32_t decodeCodepoint(const uint8_t*& p) { static uint32_t decodeCodepoint(const uint8_t*& p) {