From 24474f67fa939fce068a1729521baaff0354e0b3 Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Thu, 14 May 2026 11:28:36 +0200 Subject: [PATCH] feat: NearbyScreen, configurable clock dashboard, memory optimizations - Add NearbyScreen: GPS contact list with haversine distance sort, type filter (ALL/Chat/Rpt/Room/Snsr), detail view with ping (RTT via ACK poll), node scan via advert(), context menu - Add DashboardConfigScreen: configure up to 3 data fields on the clock page (Battery, Temp, Humidity, Pressure, GPS, Altitude, Lux, CO2, Contacts); entered via long-press ENTER on clock page - Rework HP_CLOCK layout: full-screen clock (size 2 at y=0), date at y=19, separator at y=28, three data field rows at y=31/41/51; hide node name/battery indicator on clock page - Persist dashboard_fields[3] in NodePrefs and DataStore - Add isAckPending() to MyMesh for NearbyScreen ping detection - Wire NearbyScreen into ToolsScreen and UITask - Reduce NearbyScreen entry buffer from MAX_CONTACTS(100) to 32, saving ~3.3 KB RAM - Switch haversine/bearing math from double to float (sinf/cosf/atan2f), eliminating double libm and saving ~5-10 KB flash Co-Authored-By: Claude Sonnet 4.6 --- examples/companion_radio/DataStore.cpp | 4 + examples/companion_radio/MyMesh.h | 6 + examples/companion_radio/NodePrefs.h | 1 + .../ui-new/DashboardConfigScreen.h | 86 +++++ .../companion_radio/ui-new/NearbyScreen.h | 353 ++++++++++++++++++ examples/companion_radio/ui-new/ToolsScreen.h | 5 +- examples/companion_radio/ui-new/UITask.cpp | 125 ++++++- examples/companion_radio/ui-new/UITask.h | 4 + 8 files changed, 565 insertions(+), 19 deletions(-) create mode 100644 examples/companion_radio/ui-new/DashboardConfigScreen.h create mode 100644 examples/companion_radio/ui-new/NearbyScreen.h diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index 75380404..4f5476cf 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -266,6 +266,9 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no file.read((uint8_t *)&_prefs.buzzer_auto, sizeof(_prefs.buzzer_auto)); if (file.available()) { file.read((uint8_t *)_prefs.dm_notif, sizeof(_prefs.dm_notif)); + if (file.available()) { + file.read((uint8_t *)_prefs.dashboard_fields, sizeof(_prefs.dashboard_fields)); + } } } } @@ -338,6 +341,7 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_ file.write((uint8_t *)&_prefs.clock_hide_seconds, sizeof(_prefs.clock_hide_seconds)); file.write((uint8_t *)&_prefs.buzzer_auto, sizeof(_prefs.buzzer_auto)); file.write((uint8_t *)_prefs.dm_notif, sizeof(_prefs.dm_notif)); + file.write((uint8_t *)_prefs.dashboard_fields, sizeof(_prefs.dashboard_fields)); file.close(); } diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 240fdbcc..5066b4bf 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -168,6 +168,12 @@ public: void savePrefs() { _store->savePrefs(_prefs, sensors.node_lat, sensors.node_lon); } void saveRTCTime() { _store->saveRTCTime(); } + bool isAckPending(uint32_t expected_ack) const { + for (int i = 0; i < EXPECTED_ACK_TABLE_SIZE; i++) + if (expected_ack_table[i].ack == expected_ack) return true; + return false; + } + #if ENV_INCLUDE_GPS == 1 void applyGpsPrefs() { sensors.setSettingValue("gps", _prefs.gps_enabled ? "1" : "0"); diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 2e67665f..05fbbd66 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -60,4 +60,5 @@ struct NodePrefs { // persisted to file struct DmNotifEntry { uint8_t prefix[4]; uint8_t state; }; // state: 0=default,1=muted,2=force-on static const int DM_NOTIF_TABLE_MAX = 16; DmNotifEntry dm_notif[DM_NOTIF_TABLE_MAX]; // 16*5 = 80 bytes + uint8_t dashboard_fields[3]; // 0=None,1=Batt,2=Temp,3=Hum,4=Pres,5=GPS,6=Alt,7=Lux,8=CO2,9=Nodes }; \ No newline at end of file diff --git a/examples/companion_radio/ui-new/DashboardConfigScreen.h b/examples/companion_radio/ui-new/DashboardConfigScreen.h new file mode 100644 index 00000000..1cbc007a --- /dev/null +++ b/examples/companion_radio/ui-new/DashboardConfigScreen.h @@ -0,0 +1,86 @@ +#pragma once +// Configures which data fields appear on the clock home page. +// Included by UITask.cpp after BotScreen.h. + +// Field type constants — used here and in UITask.cpp HP_CLOCK render. +static const uint8_t DASH_NONE = 0; +static const uint8_t DASH_BATT = 1; +static const uint8_t DASH_TEMP = 2; +static const uint8_t DASH_HUM = 3; +static const uint8_t DASH_PRES = 4; +static const uint8_t DASH_GPS = 5; +static const uint8_t DASH_ALT = 6; +static const uint8_t DASH_LUX = 7; +static const uint8_t DASH_CO2 = 8; +static const uint8_t DASH_NODES = 9; +static const uint8_t DASH_COUNT = 10; + +class DashboardConfigScreen : public UIScreen { + UITask* _task; + NodePrefs* _prefs; + + static const int FIELD_SLOTS = 3; + static const int ITEM_H = 13; + static const int START_Y = 13; + static const int VAL_X = 62; + + static const char* OPTION_NAMES[DASH_COUNT]; + + int _sel; + + void cycle(int slot, int dir) { + uint8_t& f = _prefs->dashboard_fields[slot]; + f = (uint8_t)((f + DASH_COUNT + dir) % DASH_COUNT); + } + +public: + DashboardConfigScreen(UITask* task, NodePrefs* prefs) : _task(task), _prefs(prefs) {} + + void enter() { _sel = 0; } + + int render(DisplayDriver& display) override { + display.setTextSize(1); + display.setColor(DisplayDriver::LIGHT); + display.drawTextCentered(display.width() / 2, 0, "CLOCK FIELDS"); + display.fillRect(0, 10, display.width(), 1); + + static const char* labels[] = { "Field 1", "Field 2", "Field 3" }; + for (int i = 0; i < FIELD_SLOTS; i++) { + int y = START_Y + i * ITEM_H; + bool sel = (i == _sel); + if (sel) { + display.setColor(DisplayDriver::LIGHT); + display.fillRect(0, y - 1, display.width(), ITEM_H); + display.setColor(DisplayDriver::DARK); + } else { + display.setColor(DisplayDriver::LIGHT); + } + display.setCursor(2, y); + display.print(labels[i]); + display.setCursor(VAL_X, y); + uint8_t f = _prefs->dashboard_fields[i]; + display.print(OPTION_NAMES[f < DASH_COUNT ? f : DASH_NONE]); + display.setColor(DisplayDriver::LIGHT); + } + return 500; + } + + bool handleInput(char c) override { + if (c == KEY_CANCEL || c == KEY_CONTEXT_MENU) { + the_mesh.savePrefs(); + _task->gotoHomeScreen(); + return true; + } + if (c == KEY_UP && _sel > 0) { _sel--; return true; } + if (c == KEY_DOWN && _sel < FIELD_SLOTS - 1){ _sel++; return true; } + if (c == KEY_LEFT || c == KEY_PREV) { cycle(_sel, -1); return true; } + if (c == KEY_RIGHT || c == KEY_NEXT) { cycle(_sel, 1); return true; } + if (c == KEY_ENTER) { cycle(_sel, 1); return true; } + return false; + } +}; + +const char* DashboardConfigScreen::OPTION_NAMES[DASH_COUNT] = { + "None", "Battery", "Temp", "Humidity", "Pressure", + "GPS", "Altitude", "Lux", "CO2", "Contacts" +}; diff --git a/examples/companion_radio/ui-new/NearbyScreen.h b/examples/companion_radio/ui-new/NearbyScreen.h new file mode 100644 index 00000000..3ee57a1e --- /dev/null +++ b/examples/companion_radio/ui-new/NearbyScreen.h @@ -0,0 +1,353 @@ +#pragma once +#include + +#ifndef M_PI + #define M_PI 3.14159265358979323846 +#endif + +class NearbyScreen : public UIScreen { + UITask* _task; + + static const int VISIBLE = 4; + static const int ITEM_H = 12; + static const int START_Y = 12; + static const int DIST_COL = 86; + + static const int FILTER_COUNT = 5; + static const char* FILTER_LABELS[FILTER_COUNT]; + static const uint8_t FILTER_TYPES[FILTER_COUNT]; + + struct Entry { + char name[32]; + int32_t lat_e6, lon_e6; + float dist_km; + uint8_t type; + int contact_idx; + }; + + static const int MAX_NEARBY = 32; + Entry _entries[MAX_NEARBY]; + int _count; + int _sel; + int _scroll; + bool _detail; + int32_t _own_lat, _own_lon; + bool _own_gps; + uint8_t _filter; + + // scan state + bool _scanning; + unsigned long _scan_started_ms; + static const unsigned long SCAN_DURATION_MS = 4000UL; + + // context menu (list view) + PopupMenu _ctx_menu; + + // ping state (detail view) + enum PingState { PING_IDLE, PING_WAIT, PING_OK, PING_TIMEOUT }; + PingState _ping_state; + uint32_t _ping_expected_ack; + unsigned long _ping_start_ms; + unsigned long _ping_rtt_ms; + static const unsigned long PING_TIMEOUT_MS = 30000UL; + + static float haversineKm(int32_t lat1, int32_t lon1, int32_t lat2, int32_t lon2) { + static const float DEG2RAD = (float)M_PI / 180.0f; + float la1 = lat1 * (1e-6f * DEG2RAD); + float la2 = lat2 * (1e-6f * DEG2RAD); + float dla = (lat2 - lat1) * (1e-6f * DEG2RAD); + float dlo = (lon2 - lon1) * (1e-6f * DEG2RAD); + float a = sinf(dla/2)*sinf(dla/2) + cosf(la1)*cosf(la2)*sinf(dlo/2)*sinf(dlo/2); + return 6371.0f * 2.0f * asinf(sqrtf(a)); + } + + static int bearingDeg(int32_t lat1, int32_t lon1, int32_t lat2, int32_t lon2) { + static const float DEG2RAD = (float)M_PI / 180.0f; + float la1 = lat1 * (1e-6f * DEG2RAD); + float la2 = lat2 * (1e-6f * DEG2RAD); + float dlo = (lon2 - lon1) * (1e-6f * DEG2RAD); + float b = atan2f(sinf(dlo)*cosf(la2), + cosf(la1)*sinf(la2) - sinf(la1)*cosf(la2)*cosf(dlo)) * (180.0f / (float)M_PI); + if (b < 0.0f) b += 360.0f; + return (int)(b + 0.5f) % 360; + } + + static void fmtDist(char* buf, int n, float km) { + if (km < 1.0f) snprintf(buf, n, "%dm", (int)(km * 1000 + 0.5f)); + else if (km < 100.0f) snprintf(buf, n, "%.1fkm", km); + else snprintf(buf, n, "%dkm", (int)(km + 0.5f)); + } + + static const char* typeName(uint8_t t) { + switch (t) { + case ADV_TYPE_CHAT: return "Chat"; + case ADV_TYPE_REPEATER: return "Repeater"; + case ADV_TYPE_ROOM: return "Room"; + case ADV_TYPE_SENSOR: return "Sensor"; + default: return "Unknown"; + } + } + + void startScan() { + the_mesh.advert(); + _scanning = true; + _scan_started_ms = millis(); + } + + void refresh() { + _count = 0; + _own_gps = false; + _own_lat = _own_lon = 0; + +#if ENV_INCLUDE_GPS == 1 + LocationProvider* loc = sensors.getLocationProvider(); + if (loc && loc->isValid()) { + _own_lat = loc->getLatitude(); + _own_lon = loc->getLongitude(); + _own_gps = true; + } +#endif + + int nc = the_mesh.getNumContacts(); + for (int i = 0; i < nc && _count < MAX_NEARBY; i++) { + ContactInfo ci; + if (!the_mesh.getContactByIdx(i, ci)) continue; + if (ci.gps_lat == 0 && ci.gps_lon == 0) continue; + if (_filter > 0 && ci.type != FILTER_TYPES[_filter]) continue; + + Entry& e = _entries[_count++]; + strncpy(e.name, ci.name, sizeof(e.name) - 1); + e.name[sizeof(e.name) - 1] = '\0'; + e.lat_e6 = ci.gps_lat; + e.lon_e6 = ci.gps_lon; + e.dist_km = _own_gps ? haversineKm(_own_lat, _own_lon, ci.gps_lat, ci.gps_lon) : -1.0f; + e.type = ci.type; + e.contact_idx = i; + } + + if (_own_gps) { + for (int i = 0; i < _count - 1; i++) { + int best = i; + for (int j = i + 1; j < _count; j++) + if (_entries[j].dist_km < _entries[best].dist_km) best = j; + if (best != i) { Entry tmp = _entries[i]; _entries[i] = _entries[best]; _entries[best] = tmp; } + } + } + + if (_count == 0) { + _sel = _scroll = 0; + } else if (_sel >= _count) { + _sel = _count - 1; + if (_scroll > _sel) _scroll = _sel; + } + } + +public: + NearbyScreen(UITask* task) + : _task(task), _filter(0), _scanning(false), _ping_state(PING_IDLE) {} + + void enter() { + _sel = _scroll = 0; + _detail = false; + _filter = 0; + _scanning = false; + _ctx_menu.active = false; + _ping_state = PING_IDLE; + refresh(); + } + + int render(DisplayDriver& display) override { + display.setTextSize(1); + + // poll scan timer — only refresh list when not in detail view + if (_scanning && !_detail && millis() - _scan_started_ms >= SCAN_DURATION_MS) { + _scanning = false; + refresh(); + } + + // poll ping ack + if (_ping_state == PING_WAIT) { + if (!the_mesh.isAckPending(_ping_expected_ack)) { + _ping_rtt_ms = millis() - _ping_start_ms; + _ping_state = PING_OK; + } else if (millis() - _ping_start_ms >= PING_TIMEOUT_MS) { + _ping_state = PING_TIMEOUT; + } + } + + // ── detail view ────────────────────────────────────────────────────────── + if (_detail && _sel < _count) { + const Entry& e = _entries[_sel]; + + display.setColor(DisplayDriver::LIGHT); + display.fillRect(0, 0, display.width(), 10); + 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); + + char buf[32]; + snprintf(buf, sizeof(buf), "Lat: %.5f", e.lat_e6 / 1e6); + display.setCursor(2, 13); display.print(buf); + + snprintf(buf, sizeof(buf), "Lon: %.5f", e.lon_e6 / 1e6); + display.setCursor(2, 22); display.print(buf); + + if (e.dist_km >= 0.0f) { + char dist[12]; + fmtDist(dist, sizeof(dist), e.dist_km); + snprintf(buf, sizeof(buf), "Dist:%s %d\xb0", dist, bearingDeg(_own_lat, _own_lon, e.lat_e6, e.lon_e6)); + display.setCursor(2, 31); display.print(buf); + } else { + display.setCursor(2, 31); display.print("Dist: no own GPS"); + } + + display.setCursor(2, 40); + switch (_ping_state) { + case PING_IDLE: display.print("[OK]=Ping"); break; + case PING_WAIT: display.print("Pinging..."); break; + case PING_OK: snprintf(buf,sizeof(buf),"RTT:%lums",_ping_rtt_ms); + display.print(buf); break; + case PING_TIMEOUT: display.print("Ping timeout"); break; + } + + snprintf(buf, sizeof(buf), "Type:%s", typeName(e.type)); + display.setCursor(2, 49); display.print(buf); + + return (_ping_state == PING_WAIT) ? 100 : 2000; + } + + // ── list view ──────────────────────────────────────────────────────────── + display.setColor(DisplayDriver::LIGHT); + if (_scanning) { + display.drawTextCentered(display.width() / 2, 0, "SCANNING..."); + } else { + char title[22]; + snprintf(title, sizeof(title), "NEARBY[%s]", FILTER_LABELS[_filter]); + display.drawTextCentered(display.width() / 2, 0, title); + } + display.fillRect(0, 10, display.width(), 1); + + if (_count == 0) { + display.drawTextCentered(display.width() / 2, 28, _scanning ? "Waiting..." : "No GPS contacts"); + display.drawTextCentered(display.width() / 2, 40, "[M]=Scan"); + } else { + for (int i = 0; i < VISIBLE && (_scroll + i) < _count; i++) { + int idx = _scroll + i; + bool sel = (idx == _sel); + int y = START_Y + i * ITEM_H; + const Entry& e = _entries[idx]; + + if (sel) { + display.setColor(DisplayDriver::LIGHT); + display.fillRect(0, y - 1, display.width(), ITEM_H - 1); + display.setColor(DisplayDriver::DARK); + } else { + display.setColor(DisplayDriver::LIGHT); + } + + char filt[32]; + display.translateUTF8ToBlocks(filt, e.name, sizeof(filt)); + display.drawTextEllipsized(2, y, DIST_COL - 4, filt); + + display.setColor(sel ? DisplayDriver::DARK : DisplayDriver::LIGHT); + char dist[10]; + if (e.dist_km >= 0.0f) fmtDist(dist, sizeof(dist), e.dist_km); + else strncpy(dist, "?GPS", sizeof(dist)); + display.setCursor(DIST_COL, y); + display.print(dist); + } + + display.setColor(DisplayDriver::LIGHT); + if (_scroll > 0) + { display.setCursor(display.width() - 6, START_Y); display.print("^"); } + if (_scroll + VISIBLE < _count) + { display.setCursor(display.width() - 6, START_Y + (VISIBLE - 1) * ITEM_H); display.print("v"); } + } + + // context menu overlay — drawn on top regardless of list state + if (_ctx_menu.active) { + _ctx_menu.render(display); + return 50; + } + + return _scanning ? 100 : (_count == 0 ? 3000 : 2000); + } + + bool handleInput(char c) override { + // ── detail view input ──────────────────────────────────────────────────── + if (_detail) { + if (c == KEY_CANCEL || c == KEY_CONTEXT_MENU) { + _detail = false; + _ping_state = PING_IDLE; + return true; + } + if (c == KEY_ENTER && _ping_state == PING_IDLE) { + ContactInfo ci; + if (the_mesh.getContactByIdx(_entries[_sel].contact_idx, ci)) { + uint32_t expected_ack, est_timeout; + int res = the_mesh.sendMessage(ci, rtc_clock.getCurrentTime(), 0, "", expected_ack, est_timeout); + if (res != MSG_SEND_FAILED && expected_ack != 0) { + _ping_expected_ack = expected_ack; + _ping_start_ms = millis(); + _ping_state = PING_WAIT; + } + } + return true; + } + return true; + } + + // ── list view — context menu ───────────────────────────────────────────── + if (_ctx_menu.active) { + auto res = _ctx_menu.handleInput(c); + if (res == PopupMenu::SELECTED) { + if (_ctx_menu.selectedIndex() == 0) { + startScan(); + } else { + _task->gotoToolsScreen(); + } + } + return true; + } + + // ── list view — normal input ───────────────────────────────────────────── + if (c == KEY_CANCEL) { _task->gotoToolsScreen(); return true; } + if (c == KEY_CONTEXT_MENU) { + _ctx_menu.begin("Options", 2); + _ctx_menu.addItem("Scan for nodes"); + _ctx_menu.addItem("Back"); + return true; + } + if (c == KEY_UP && _sel > 0) { + _sel--; + if (_sel < _scroll) _scroll = _sel; + return true; + } + if (c == KEY_DOWN && _sel < _count - 1) { + _sel++; + if (_sel >= _scroll + VISIBLE) _scroll = _sel - VISIBLE + 1; + return true; + } + if (c == KEY_ENTER && _count > 0) { + _detail = true; + _ping_state = PING_IDLE; + return true; + } + if (c == KEY_LEFT) { + _filter = (_filter + FILTER_COUNT - 1) % FILTER_COUNT; + refresh(); + return true; + } + if (c == KEY_RIGHT) { + _filter = (_filter + 1) % FILTER_COUNT; + refresh(); + return true; + } + return false; + } +}; + +const char* NearbyScreen::FILTER_LABELS[5] = { "ALL", "Chat", "Rpt", "Room", "Snsr" }; +const uint8_t NearbyScreen::FILTER_TYPES[5] = { 0, ADV_TYPE_CHAT, ADV_TYPE_REPEATER, ADV_TYPE_ROOM, ADV_TYPE_SENSOR }; diff --git a/examples/companion_radio/ui-new/ToolsScreen.h b/examples/companion_radio/ui-new/ToolsScreen.h index d2cf363e..2bb600ee 100644 --- a/examples/companion_radio/ui-new/ToolsScreen.h +++ b/examples/companion_radio/ui-new/ToolsScreen.h @@ -6,7 +6,7 @@ class ToolsScreen : public UIScreen { UITask* _task; int _sel; - static const int ITEM_COUNT = 2; + static const int ITEM_COUNT = 3; static const char* ITEMS[ITEM_COUNT]; public: @@ -44,8 +44,9 @@ public: if (c == KEY_ENTER) { if (_sel == 0) { _task->gotoRingtoneEditor(); return true; } if (_sel == 1) { _task->gotoBotScreen(); return true; } + if (_sel == 2) { _task->gotoNearbyScreen(); return true; } } return false; } }; -const char* ToolsScreen::ITEMS[2] = { "Ringtone Editor", "Auto-Reply Bot" }; +const char* ToolsScreen::ITEMS[3] = { "Ringtone Editor", "Auto-Reply Bot", "Nearby Nodes" }; diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 34527069..a93584cb 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -1592,6 +1592,8 @@ public: // ── Custom screens (separate files to ease upstream merges) ─────────────────── #include "RingtoneEditorScreen.h" #include "BotScreen.h" +#include "NearbyScreen.h" +#include "DashboardConfigScreen.h" #include "ToolsScreen.h" // ── HomeScreen ──────────────────────────────────────────────────────────────── @@ -1787,16 +1789,16 @@ public: int render(DisplayDriver& display) override { char tmp[80]; - // node name - display.setTextSize(1); - display.setColor(DisplayDriver::LIGHT); - char filtered_name[sizeof(_node_prefs->node_name)]; - display.translateUTF8ToBlocks(filtered_name, _node_prefs->node_name, sizeof(filtered_name)); - display.setCursor(0, 0); - display.print(filtered_name); - - // battery voltage - renderBatteryIndicator(display, _task->getBattMilliVolts()); + // node name + battery — hidden on CLOCK page (full screen used for dashboard) + if (_page != CLOCK) { + display.setTextSize(1); + display.setColor(DisplayDriver::LIGHT); + char filtered_name[sizeof(_node_prefs->node_name)]; + display.translateUTF8ToBlocks(filtered_name, _node_prefs->node_name, sizeof(filtered_name)); + display.setCursor(0, 0); + display.print(filtered_name); + renderBatteryIndicator(display, _task->getBattMilliVolts()); + } // ensure current page is visible (e.g. after settings change) if (!isPageVisible(_page)) _page = navPage(_page, +1); @@ -1842,17 +1844,90 @@ public: sprintf(buf, "%02d:%02d:%02d", ti->tm_hour, ti->tm_min, ti->tm_sec); else sprintf(buf, "%02d:%02d", ti->tm_hour, ti->tm_min); - display.drawTextCentered(display.width() / 2, 18, buf); + display.drawTextCentered(display.width() / 2, 0, buf); display.setTextSize(1); - const char* wd[] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"}; - const char* mo[] = {"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"}; + static const char* wd[] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"}; + static const char* mo[] = {"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"}; sprintf(buf, "%s %d %s %d", wd[ti->tm_wday], ti->tm_mday, mo[ti->tm_mon], 1900 + ti->tm_year); - display.drawTextCentered(display.width() / 2, 40, buf); + display.drawTextCentered(display.width() / 2, 19, buf); - if (tz >= 0) sprintf(buf, "UTC+%d", (int)tz); - else sprintf(buf, "UTC%d", (int)tz); - display.drawTextCentered(display.width() / 2, 54, buf); + display.fillRect(0, 28, display.width(), 1); + + // dashboard data fields + if (_node_prefs) { + refresh_sensors(); + static const int FIELD_Y[] = { 31, 41, 51 }; + for (int fi = 0; fi < 3; fi++) { + uint8_t field = _node_prefs->dashboard_fields[fi]; + if (field == DASH_NONE) continue; + + char label[10], val[20]; + label[0] = '\0'; + val[0] = '\0'; + + if (field == DASH_BATT) { + strcpy(label, "Batt"); + uint16_t mv = _task->getBattMilliVolts(); + if (mv > 0) snprintf(val, sizeof(val), "%u.%02uV", mv/1000, (mv%1000)/10); + else strcpy(val, "--"); + } else if (field == DASH_GPS) { + strcpy(label, "GPS"); +#if ENV_INCLUDE_GPS == 1 + LocationProvider* loc = sensors.getLocationProvider(); + if (loc && loc->isValid()) + snprintf(val, sizeof(val), "%.3f %.3f", + loc->getLatitude()/1000000.0f, loc->getLongitude()/1000000.0f); + else + strcpy(val, "no fix"); +#else + strcpy(val, "--"); +#endif + } else if (field == DASH_NODES) { + strcpy(label, "Nodes"); + snprintf(val, sizeof(val), "%d", the_mesh.getNumContacts()); + } else { + uint8_t lpp_type = 0; + switch (field) { + case DASH_TEMP: strcpy(label, "Temp"); lpp_type = LPP_TEMPERATURE; break; + case DASH_HUM: strcpy(label, "Hum"); lpp_type = LPP_RELATIVE_HUMIDITY; break; + case DASH_PRES: strcpy(label, "Pres"); lpp_type = LPP_BAROMETRIC_PRESSURE; break; + case DASH_ALT: strcpy(label, "Alt"); lpp_type = LPP_ALTITUDE; break; + case DASH_LUX: strcpy(label, "Lux"); lpp_type = LPP_LUMINOSITY; break; + case DASH_CO2: strcpy(label, "CO2"); lpp_type = LPP_CONCENTRATION; break; + } + if (lpp_type) { + LPPReader r(sensors_lpp.getBuffer(), sensors_lpp.getSize()); + uint8_t ch, type; + while (r.readHeader(ch, type)) { + if (type == lpp_type) { + float v; + switch (lpp_type) { + case LPP_TEMPERATURE: r.readTemperature(v); snprintf(val, sizeof(val), "%.1f\xf8""C", v); break; + case LPP_RELATIVE_HUMIDITY: r.readRelativeHumidity(v); snprintf(val, sizeof(val), "%.0f%%", v); break; + case LPP_BAROMETRIC_PRESSURE: r.readPressure(v); snprintf(val, sizeof(val), "%.0fhPa", v); break; + case LPP_ALTITUDE: r.readAltitude(v); snprintf(val, sizeof(val), "%.0fm", v); break; + case LPP_LUMINOSITY: r.readLuminosity(v); snprintf(val, sizeof(val), "%.0flux", v); break; + case LPP_CONCENTRATION: r.readConcentration(v); snprintf(val, sizeof(val), "%.0fppm", v); break; + } + break; + } + r.skipData(type); + } + } + if (!val[0]) strcpy(val, "--"); + } + + if (val[0] && label[0]) { + display.setColor(DisplayDriver::LIGHT); + display.setCursor(0, FIELD_Y[fi]); + display.print(label); + int vw = display.getTextWidth(val); + display.setCursor(display.width() - vw - 1, FIELD_Y[fi]); + display.print(val); + } + } + } } } else if (_page == HomePage::RECENT) { the_mesh.getRecentlyHeard(recent, UI_RECENT_LIST_SIZE); @@ -2122,6 +2197,10 @@ public: _shutdown_init = true; // need to wait for button to be released return true; } + if (c == KEY_CONTEXT_MENU && _page == HomePage::CLOCK) { + _task->gotoDashboardConfig(); + return true; + } return false; } }; @@ -2171,6 +2250,8 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no tools_screen = new ToolsScreen(this); ringtone_edit = new RingtoneEditorScreen(this, node_prefs); bot_screen = new BotScreen(this, node_prefs); + nearby_screen = new NearbyScreen(this); + dashboard_config = new DashboardConfigScreen(this, node_prefs); setCurrScreen(splash); applyBrightness(); @@ -2195,6 +2276,16 @@ void UITask::gotoBotScreen() { setCurrScreen(bot_screen); } +void UITask::gotoNearbyScreen() { + ((NearbyScreen*)nearby_screen)->enter(); + setCurrScreen(nearby_screen); +} + +void UITask::gotoDashboardConfig() { + ((DashboardConfigScreen*)dashboard_config)->enter(); + setCurrScreen(dashboard_config); +} + void UITask::playMelody(const char* melody) { #ifdef PIN_BUZZER buzzer.playForced(melody); diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index af95bce1..388500b7 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -63,6 +63,8 @@ class UITask : public AbstractUITask { UIScreen* tools_screen; UIScreen* ringtone_edit; UIScreen* bot_screen; + UIScreen* nearby_screen; + UIScreen* dashboard_config; UIScreen* curr; void userLedHandler(); @@ -98,6 +100,8 @@ public: void gotoToolsScreen(); void gotoRingtoneEditor(); void gotoBotScreen(); + void gotoNearbyScreen(); + void gotoDashboardConfig(); void playMelody(const char* melody); void stopMelody(); bool isMelodyPlaying();