From 3cc6d2c2c078acba5a4c932102c013c69803b3f2 Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Wed, 30 Sep 2026 21:22:08 +1000 Subject: [PATCH] Max: Timezones world clock page --- examples/companion_radio/DataStore.cpp | 10 ++ examples/companion_radio/NodePrefs.h | 9 +- examples/companion_radio/main.cpp | 12 +- examples/companion_radio/ui-new/UITask.cpp | 198 ++++++++++++++++++++- examples/companion_radio/ui-new/UITask.h | 4 + 5 files changed, 228 insertions(+), 5 deletions(-) diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index 38317198..8199473a 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -285,6 +285,12 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no if (file.read((uint8_t *)_prefs.canned_msgs, sizeof(_prefs.canned_msgs)) != sizeof(_prefs.canned_msgs)) { memset(_prefs.canned_msgs, 0, sizeof(_prefs.canned_msgs)); // default: all slots empty } + if (file.read((uint8_t *)&_prefs.clock_slot_a, sizeof(_prefs.clock_slot_a)) != sizeof(_prefs.clock_slot_a)) { + _prefs.clock_slot_a = 0; // default: Zone 1 at UTC+0 + } + if (file.read((uint8_t *)&_prefs.clock_slot_b, sizeof(_prefs.clock_slot_b)) != sizeof(_prefs.clock_slot_b)) { + _prefs.clock_slot_b = 0; // default: Zone 2 at UTC+0 + } // Clamp to valid ranges if (_prefs.dark_mode > 1) _prefs.dark_mode = 0; @@ -300,6 +306,8 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no if (_prefs.lora_antenna > 1) _prefs.lora_antenna = 0; if (_prefs.backlight_brightness_pct < 5 || _prefs.backlight_brightness_pct > 100) _prefs.backlight_brightness_pct = 100; if (_prefs.kb_backlight_pct < 5 || _prefs.kb_backlight_pct > 100) _prefs.kb_backlight_pct = 50; + if (_prefs.clock_slot_a < -12 || _prefs.clock_slot_a > 14) _prefs.clock_slot_a = 0; + if (_prefs.clock_slot_b < -12 || _prefs.clock_slot_b > 14) _prefs.clock_slot_b = 0; // Force NUL termination on each canned message slot in case of garbage for (int i = 0; i < (int)(sizeof(_prefs.canned_msgs) / sizeof(_prefs.canned_msgs[0])); i++) { _prefs.canned_msgs[i][sizeof(_prefs.canned_msgs[0]) - 1] = '\0'; @@ -368,6 +376,8 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_ file.write((uint8_t *)&_prefs.backlight_brightness_pct, sizeof(_prefs.backlight_brightness_pct)); // 175 file.write((uint8_t *)&_prefs.kb_backlight_pct, sizeof(_prefs.kb_backlight_pct)); // 176 file.write((uint8_t *)_prefs.canned_msgs, sizeof(_prefs.canned_msgs)); // 177 + file.write((uint8_t *)&_prefs.clock_slot_a, sizeof(_prefs.clock_slot_a)); // 1520 + file.write((uint8_t *)&_prefs.clock_slot_b, sizeof(_prefs.clock_slot_b)); // 1521 file.close(); } diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index bbd92808..8aaa0a44 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -87,9 +87,16 @@ struct NodePrefs { // persisted to file // Ten pre-written messages, edited in Settings -> Canned Messages and sent // from the channel screen (Max: speech-bubble capacitive button). Each slot // holds up to 133 chars + NUL; an empty string marks an unused slot. Older - // prefs files short-read this final field and keep the all-empty default. + // prefs files short-read this field and keep the all-empty default. char canned_msgs[CANNED_MSG_SLOTS][CANNED_MSG_LEN]; + // --- Timezones home page (world clock) --- + // UTC offsets (-12..+14) of the page's two extra zones; its Home row is + // utc_offset_hours. Older prefs files short-read these final fields and + // keep the default 0 (UTC+0). + int8_t clock_slot_a; // Zone 1 UTC offset, default 0 + int8_t clock_slot_b; // Zone 2 UTC offset, default 0 + // --- Font helpers (inline, no overhead) --- // Returns the DisplayDriver text-size index for "small/body" text. // T-Deck Pro: 0 = built-in 6×8 (or 7pt with custom fonts), 1 = 9pt. diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 0110ee50..20dfaa60 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -1569,6 +1569,14 @@ static void openMapScreen() { // Home screen: long press = activate current page action // (BLE toggle, send advert, hibernate, GPS toggle, etc.) if (ui_task.isOnHomeScreen()) { + // Timezones page: select the zone row under the finger first, so the + // Enter opens that row's editor. While the editor is open the row is + // left alone and the Enter saves. + if (ui_task.isHomeOnTimezonesPage()) { + int vx, vy; + touchToVirtual(x, y, vx, vy); + ui_task.selectHomeTimezoneRowAt(vy); + } return (char)KEY_ENTER; } @@ -4647,6 +4655,7 @@ void handleKeyboardInput() { || ui_task.isOnAlarmScreen() #endif || ui_task.isHomeOnShutdownPage() + || ui_task.isHomeOnTimezonesPage() || ui_task.isOnRxLogScreen() ) { ui_task.injectKey('s'); // Pass directly for scrolling @@ -4696,6 +4705,7 @@ void handleKeyboardInput() { #ifdef MECK_AUDIO_VARIANT || ui_task.isOnAlarmScreen() #endif + || ui_task.isHomeOnTimezonesPage() || ui_task.isOnRxLogScreen() ) { ui_task.injectKey('w'); // Pass directly for scrolling @@ -5427,4 +5437,4 @@ void audio_eof_mp3(const char *info) { } #endif // !HAS_4G_MODEM -#endif // LilyGo_TDeck_Pro +#endif // LilyGo_TDeck_Pro \ No newline at end of file diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 5e5916ce..24173875 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -161,6 +161,7 @@ class HomeScreen : public UIScreen { BATTERY, #endif SHUTDOWN, + TIMEZONES, // world clock; last page, so A or a right swipe from the tiles reaches it Count // keep as last }; @@ -176,6 +177,11 @@ class HomeScreen : public UIScreen { bool _poweroff_msg_shown; // true = "powering off..." already displayed once bool _editing_utc; int8_t _saved_utc_offset; // for cancel/undo + // Timezones page: selected row (0 = Home, 1 = Zone 1, 2 = Zone 2) and + // the zone offset editor, which holds the edited value until saved. + uint8_t _tz_sel; + bool _tz_editing; + int8_t _tz_edit_val; AdvertPath recent[UI_RECENT_LIST_SIZE]; @@ -284,6 +290,156 @@ void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts, } #endif + // ========================================================================== + // Timezones home page (world clock) -- ported from the watch and Meck-P4 + // + // Row 0 = Home (the device utc_offset_hours), rows 1/2 = the two saved + // zones (prefs clock_slot_a/b). Each row shows the zone label and offset, + // the local HH:MM, a +/-1D day marker when the zone sits on a different + // calendar day to Home, and city codes for the offset (table copied from + // Meck-P4, which copied it from the watch). W/S or a long-press picks a + // row and Enter opens its offset editor; in the editor W/S or taps adjust + // the offset, Enter or a long-press saves, Q cancels. Editing Home + // changes the device UTC offset, as on the watch and P4. + // ========================================================================== + + // Row 0 label y and row pitch, in virtual coordinates. Also used by + // selectTimezoneRowAt() for the touch long-press hit test. + enum { TZ_ROW_TOP = 16, TZ_ROW_H = 34 }; + + static const char* tzRowLabel(int r) { + return (r == 1) ? "Zone 1" : (r == 2) ? "Zone 2" : "Home"; + } + + static const char* tzCities(int8_t off) { + static const char* const CITIES[27] = { + "BIT/HWL", /* -12 */ "PPG/NIU/MDY", /* -11 */ "HNL/PPT/RAR", /* -10 */ + "ANC/JNU", /* -9 */ "LAX/VAN/SEA", /* -8 */ "PHX/HMO/MZT", /* -7 */ + "MEX/REG/GUA", /* -6 */ "BOG/LIM/PTY", /* -5 */ "CCS/LPB/SDQ", /* -4 */ + "BUE/SAO/MVD", /* -3 */ "FEN/SGS", /* -2 */ "RAI/PDL", /* -1 */ + "REY/ACC/DKR", /* 0 */ "LOS/ALG/TUN", /* +1 */ "JNB/KRT/HRE", /* +2 */ + "NBO/MOW/ADD", /* +3 */ "DXB/BAK/TBS", /* +4 */ "KHI/TAS/SVX", /* +5 */ + "DAC/ALA/OMS", /* +6 */ "BKK/JKT/HAN", /* +7 */ "PER/BEI/HKG", /* +8 */ + "TYO/SEL/YKS", /* +9 */ "BNE/POM/VLA", /* +10 */ "NOU/HIR/VLI", /* +11 */ + "SUV/TRW/MAJ", /* +12 */ "TBU/APW", /* +13 */ "CXI" /* +14 */ + }; + int i = (int)off + 12; + if (i < 0 || i > 26) return ""; + return CITIES[i]; + } + + int8_t tzRowOffset(int r) const { + if (r == 1) return _node_prefs->clock_slot_a; + if (r == 2) return _node_prefs->clock_slot_b; + return _node_prefs->utc_offset_hours; + } + + // Same clock-validity test as the header clock: before the clock is set + // the page shows "Clock not set" and its keys do nothing. + bool tzClockValid() const { return _rtc->getCurrentTime() > 1700000000; } + + void renderTimezonesPage(DisplayDriver& display) { + char buf[24]; + + if (_tz_editing) { + // Offset editor, laid out like the watch's edit page: row name, the + // offset being edited, its city codes, then the key and touch hints. + display.setTextSize(1); + display.setColor(DisplayDriver::GREEN); + display.drawTextCentered(display.width() / 2, 18, tzRowLabel(_tz_sel)); + display.setTextSize(5); + display.setColor(DisplayDriver::LIGHT); + snprintf(buf, sizeof(buf), "UTC%+d", (int)_tz_edit_val); + display.drawTextCentered(display.width() / 2, 48, buf); + display.setTextSize(1); + display.setColor(DisplayDriver::GREEN); + display.drawTextCentered(display.width() / 2, 64, tzCities(_tz_edit_val)); + display.setTextSize(_node_prefs->smallTextSize()); + display.drawTextCentered(display.width() / 2, 84, "W/S:adj Enter:ok Q:cancel"); + display.drawTextCentered(display.width() / 2, 96, "Tap:-/+ Hold:save"); + display.setTextSize(1); + return; + } + + if (!tzClockValid()) { + display.setTextSize(1); + display.setColor(DisplayDriver::GREEN); + display.drawTextCentered(display.width() / 2, 50, "Clock not set"); + return; + } + + const uint32_t now = _rtc->getCurrentTime(); + const int32_t home_local = (int32_t)now + ((int32_t)tzRowOffset(0) * 3600); + for (int r = 0; r < 3; r++) { + const int ry = TZ_ROW_TOP + r * TZ_ROW_H; + const int8_t off = tzRowOffset(r); + const int32_t local = (int32_t)now + ((int32_t)off * 3600); + int hrs = (local / 3600) % 24; + if (hrs < 0) hrs += 24; + int mins = (local / 60) % 60; + if (mins < 0) mins += 60; + + // Label line: selection marker, zone name and offset + display.setTextSize(1); + display.setColor(DisplayDriver::GREEN); + if (r == _tz_sel) display.drawTextLeftAlign(0, ry, ">"); + snprintf(buf, sizeof(buf), "%s UTC%+d", tzRowLabel(r), (int)off); + display.drawTextLeftAlign(7, ry, buf); + + // Local time, large + display.setTextSize(5); + display.setColor(DisplayDriver::LIGHT); + snprintf(buf, sizeof(buf), "%02d:%02d", hrs, mins); + display.drawTextCentered(display.width() / 2, ry + 17, buf); + + // Day marker (left) and city codes (right) under the time + display.setTextSize(1); + display.setColor(DisplayDriver::GREEN); + const int dayDiff = (int)((local / 86400) - (home_local / 86400)); + if (dayDiff != 0) { + snprintf(buf, sizeof(buf), "%+dD", dayDiff); + display.drawTextLeftAlign(7, ry + 27, buf); + } + display.drawTextRightAlign(display.width() - 1 - EINK_X_OFFSET, ry + 27, tzCities(off)); + } + } + + // Keys on the Timezones page. Returns true when the key is used here; + // anything else falls through to the page cycling in handleInput(). + // Keyboard W/S arrive as 'w'/'s' because main.cpp passes them through on + // this page (as it passes S through on the Hibernate page). Touch taps + // arrive as KEY_PREV (left half) / KEY_NEXT (right half) and a long-press + // as KEY_ENTER, after selectTimezoneRowAt() has picked the row. + bool handleTimezonesKey(char c) { + if (_tz_editing) { + // Zone offset editor: intercept all keys. Q and Shift+Del never get + // here: main.cpp sends them to UITask::gotoHomeScreen(), whose + // cancelEditing() closes the editor without saving. + if (c == 'w' || c == KEY_NEXT) { + if (_tz_edit_val < 14) _tz_edit_val++; + } else if (c == 's' || c == KEY_PREV) { + if (_tz_edit_val > -12) _tz_edit_val--; + } else if (c == KEY_ENTER) { + if (_tz_sel == 1) _node_prefs->clock_slot_a = _tz_edit_val; + else if (_tz_sel == 2) _node_prefs->clock_slot_b = _tz_edit_val; + else _node_prefs->utc_offset_hours = _tz_edit_val; + the_mesh.savePrefs(); + Serial.printf("Timezones: %s = UTC%+d\n", tzRowLabel(_tz_sel), (int)_tz_edit_val); + _tz_editing = false; + } + return true; + } + if (!tzClockValid()) return false; + if (c == 'w') { _tz_sel = (_tz_sel + 2) % 3; return true; } + if (c == 's') { _tz_sel = (_tz_sel + 1) % 3; return true; } + if (c == KEY_ENTER) { + _tz_edit_val = tzRowOffset(_tz_sel); + _tz_editing = true; + return true; + } + return false; + } + CayenneLPP sensors_lpp; int sensors_nb = 0; bool sensors_scroll = false; @@ -315,17 +471,34 @@ public: HomeScreen(UITask* task, mesh::RTCClock* rtc, SensorManager* sensors, NodePrefs* node_prefs) : _task(task), _rtc(rtc), _sensors(sensors), _node_prefs(node_prefs), _page(0), _shutdown_init(false), _shutdown_at(0), _poweroff_selected(false), _poweroff_confirm(false), - _poweroff_msg_shown(false), _editing_utc(false), _saved_utc_offset(0), sensors_lpp(200) { } + _poweroff_msg_shown(false), _editing_utc(false), _saved_utc_offset(0), + _tz_sel(0), _tz_editing(false), _tz_edit_val(0), sensors_lpp(200) { } bool isEditingUTC() const { return _editing_utc; } bool isFirstPage() const { return _page == HomePage::FIRST; } bool isOnRecentPage() const { return _page == HomePage::RECENT; } bool isOnShutdownPage() const { return _page == HomePage::SHUTDOWN; } + bool isOnTimezonesPage() const { return _page == HomePage::TIMEZONES; } + bool isEditingTimezone() const { return _tz_editing; } + // Touch long-press on the Timezones page (main.cpp mapTouchLongPress): + // select the zone row under the finger before the Enter that opens its + // editor. vy comes from touchToVirtual(), which lands 5 units below the + // render y of the same point (setCursor adds 5), so row r's label, time + // and city codes sit in the band from TZ_ROW_TOP + r * TZ_ROW_H to the + // next row's. + void selectTimezoneRowAt(int vy) { + if (_tz_editing) return; // in the editor a long-press saves instead + int r = (vy - TZ_ROW_TOP) / TZ_ROW_H; + if (vy < TZ_ROW_TOP) r = 0; + if (r > 2) r = 2; + _tz_sel = (uint8_t)r; + } void cancelEditing() { if (_editing_utc) { _node_prefs->utc_offset_hours = _saved_utc_offset; _editing_utc = false; } + _tz_editing = false; // Timezones zone editor: discard the unsaved value } void poll() override { @@ -1089,7 +1262,10 @@ public: display.drawTextCentered(display.width() / 2, y2, line2); } } - return _editing_utc ? 700 : 5000; + else if (_page == HomePage::TIMEZONES) { + renderTimezonesPage(display); + } + return (_editing_utc || _tz_editing) ? 700 : 5000; } bool handleInput(char c) override { @@ -1127,6 +1303,13 @@ public: return true; // Consume all other keys while editing } + // Timezones page: W/S row select, Enter to edit, and the zone editor's + // keys (see handleTimezonesKey). Keys it does not use fall through to + // the page cycling below. + if (_page == HomePage::TIMEZONES && handleTimezonesKey(c)) { + return true; + } + // SHUTDOWN page -- intercept up/down and Enter before page cycling if (_page == HomePage::SHUTDOWN) { if (_poweroff_confirm) { @@ -2408,7 +2591,8 @@ void UITask::gotoHomeScreen() { } bool UITask::isEditingHomeScreen() const { - return curr == home && ((HomeScreen *) home)->isEditingUTC(); + return curr == home && (((HomeScreen *) home)->isEditingUTC() || + ((HomeScreen *) home)->isEditingTimezone()); } bool UITask::isHomeOnRecentPage() const { @@ -2419,6 +2603,14 @@ bool UITask::isHomeOnShutdownPage() const { return curr == home && ((HomeScreen *) home)->isOnShutdownPage(); } +bool UITask::isHomeOnTimezonesPage() const { + return curr == home && ((HomeScreen *) home)->isOnTimezonesPage(); +} + +void UITask::selectHomeTimezoneRowAt(int vy) { + if (curr == home) ((HomeScreen *) home)->selectTimezoneRowAt(vy); +} + void UITask::gotoChannelScreen(bool resetDmView) { ChannelScreen* cs = (ChannelScreen*)channel_screen; // If currently showing DM view, reset to channel 0 (unless caller opts out) diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index c3d10c82..fe1f356a 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -291,6 +291,10 @@ public: // Check if home screen is showing the Recent Adverts page bool isHomeOnRecentPage() const; bool isHomeOnShutdownPage() const; + // Check if home screen is showing the Timezones (world clock) page + bool isHomeOnTimezonesPage() const; + // Touch long-press on the Timezones page: select the zone row at virtual y + void selectHomeTimezoneRowAt(int vy); // Inject a key press from external source (e.g., keyboard) void injectKey(char c);