From cdc2ff25c9e4589a451afe815f30c58b0b9ca93f Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Thu, 29 Oct 2026 14:09:25 +0100 Subject: [PATCH] feat: summer time - Settings > Summer time (Europe, North America, Australia, New Zealand) puts the clock an hour ahead and back by itself; every local time goes through tzHoursAt(), the alarm keeps its time across the change (a skipped time rings an hour later, a repeated one once) Co-Authored-By: Claude Opus 5.5 --- docs/solo/settings.md | 8 ++- docs/solo/tools.md | 4 +- examples/companion_radio/DataStore.cpp | 1 + examples/companion_radio/MyMeshBot.h | 11 ++-- examples/companion_radio/NodePrefs.h | 51 ++++++++++++++++- .../companion_radio/ui-core/ClockEngine.h | 56 +++++++++++-------- .../companion_radio/ui-core/MessageText.h | 5 +- .../companion_radio/ui-core/SettingsCli.h | 2 +- .../companion_radio/ui-core/SettingsSchema.h | 5 ++ examples/companion_radio/ui-lvgl/BotScreen.h | 2 +- examples/companion_radio/ui-lvgl/MapScreen.h | 2 +- .../companion_radio/ui-lvgl/SettingsPages.h | 4 +- examples/companion_radio/ui-lvgl/Widgets.h | 4 +- examples/companion_radio/ui-new/BotScreen.h | 3 +- .../companion_radio/ui-new/ClockToolsScreen.h | 2 +- examples/companion_radio/ui-new/HomeScreen.h | 6 +- examples/companion_radio/ui-new/TrailScreen.h | 3 +- 17 files changed, 119 insertions(+), 50 deletions(-) diff --git a/docs/solo/settings.md b/docs/solo/settings.md index b7793c73..64664ac4 100644 --- a/docs/solo/settings.md +++ b/docs/solo/settings.md @@ -7,7 +7,7 @@ board supports are shown. | Section | What's there | | ------- | ------------ | -| **Display** | Brightness, auto-off, wake on message, the message alert (banner, corner envelope or off), lock screen, the lock clock's look and the [PIN](./lock.md), battery as icon / % / volts, time zone, clock format and seconds; on e-ink also rotation and full refresh, and on a 4.26" panel the text and icon size | +| **Display** | Brightness, auto-off, wake on message, the message alert (banner, corner envelope or off), lock screen, the lock clock's look and the [PIN](./lock.md), battery as icon / % / volts, time zone and summer time, clock format and seconds; on e-ink also rotation and full refresh, and on a 4.26" panel the text and icon size | | **Sound** | Buzzer on / off / auto (quiet while the app is connected), volume, quiet hours, the melody for messages, channels and adverts | | **Home Pages** | Order of the home pages, and which are shown | | **Radio** | TX power, preset, frequency, SF / BW / CR, saved presets, Auto pwr, the scope list, then the advanced options (below) | @@ -26,6 +26,10 @@ A few notes: apart. The default one is used for direct messages and repeating, and a channel follows it unless it picks its own (see [Messages](./messages.md#channels)). Anyone who types the same name gets the same scope; it isn't encryption. +- **Summer time** puts the clock an hour ahead and back again by itself, by + the rule of your region: Europe (the last Sundays of March and October), + North America, south-east Australia or New Zealand. The time zone stays + the winter one, e.g. UTC+1 for Poland. The alarm and quiet hours follow it. - **Low battery** shuts the device down at the voltage you choose, which is also 0 % on the battery indicator. - **Batt curve** sets the cell voltage at 0, 10 … 100 % for a battery that @@ -124,7 +128,7 @@ so its documentation applies: | `agc.reset.interval` | Seconds between radio gain resets (in steps of 4, `0` off), with `get` / `set` | | `get name`, `set name …` | The device name | | `get pin`, `set pin 123456` | The Bluetooth PIN, `0` for a random one; used from the next start | -| `get tz.offset`, `set tz.offset 2` | Time zone, in hours from UTC | +| `get tz.offset`, `set tz.offset 1` | Time zone, in hours from UTC (the winter one when summer time is on) | | `get lat`, `set lat …`, `get lon`, `set lon …` | The fixed position | | `ver`, `board` | Firmware version, hardware | | `reboot`, `poweroff` | Restart or turn off | diff --git a/docs/solo/tools.md b/docs/solo/tools.md index 0af838f0..0cd5f951 100644 --- a/docs/solo/tools.md +++ b/docs/solo/tools.md @@ -18,8 +18,8 @@ The **Clock** home page shows the time with a bar under it that fills with the seconds (Settings › Display › Clock seconds), the date, and up to three fields you choose (hold Enter): battery, temperature, humidity, pressure, altitude (barometric or GPS), light, CO₂, GPS position, satellites, contacts, unread -messages. The time comes from GPS or the phone app; the time zone is in -Settings. +messages. The time comes from GPS or the phone app; the time zone and summer +time are in Settings. **Enter** on the clock opens the clock tools: diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index bc0157ae..5465da1f 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -349,6 +349,7 @@ static void sanitize(NodePrefs& p) { flag(p.fav_sort, 1); flag(p.msg_wake, 1); if (p.contact_expiry_idx >= NodePrefs::CONTACT_EXPIRY_COUNT) p.contact_expiry_idx = 0; + if (p.dst_rule >= NodePrefs::DST_COUNT) p.dst_rule = NodePrefs::DST_OFF; if (p.display_brightness_pct > 100) p.display_brightness_pct = 0; flag(p.quiet_hours, 0); if (p.quiet_from > 23) p.quiet_from = 22; diff --git a/examples/companion_radio/MyMeshBot.h b/examples/companion_radio/MyMeshBot.h index 1a2f1e9e..808d29a5 100644 --- a/examples/companion_radio/MyMeshBot.h +++ b/examples/companion_radio/MyMeshBot.h @@ -124,7 +124,8 @@ void MyMesh::botDmRecord(const uint8_t* pubkey) { // wraps past midnight. bool MyMesh::botInQuietHours() const { int h; - if (!localHour(getRTCClock()->getCurrentTime(), _prefs.tz_offset_hours, h)) return false; // clock not set -- don't suppress + const uint32_t now = getRTCClock()->getCurrentTime(); + if (!localHour(now, tzHoursAt(_prefs, now), h)) return false; // clock not set -- don't suppress return hourInWindow(h, _prefs.bot_quiet_start, _prefs.bot_quiet_end); } @@ -188,7 +189,7 @@ void MyMesh::tryBotReplyDM(const ContactInfo& from, const char* text, uint8_t ho expandMsg(_prefs.bot_reply_dm, expanded, sizeof(expanded), sensors.node_lat, sensors.node_lon, sensors.node_lat != 0.0 || sensors.node_lon != 0.0, - ts, _prefs.tz_offset_hours, + ts, tzHoursAt(_prefs, ts), &sensors, (float)board.getBattMilliVolts() / 1000.0f, from.name, hops, board.getMCUTemperature(), &s_bot_in); uint32_t expected_ack, est_timeout; @@ -230,7 +231,7 @@ void MyMesh::tryBotReplyChannel(uint8_t channel_idx, const char* text, uint8_t h expandMsg(_prefs.bot_reply_ch, expanded, sizeof(expanded), sensors.node_lat, sensors.node_lon, sensors.node_lat != 0.0 || sensors.node_lon != 0.0, - ts, _prefs.tz_offset_hours, + ts, tzHoursAt(_prefs, ts), &sensors, (float)board.getBattMilliVolts() / 1000.0f, sender_name, hops, board.getMCUTemperature(), &s_bot_in); // Anti-loop: don't echo a message identical to our own reply (e.g. another bot @@ -275,7 +276,7 @@ void MyMesh::tryBotReplyRoom(const ContactInfo& from, const uint8_t* sender_pref expandMsg(_prefs.bot_reply_room, expanded, sizeof(expanded), sensors.node_lat, sensors.node_lon, sensors.node_lat != 0.0 || sensors.node_lon != 0.0, - ts, _prefs.tz_offset_hours, + ts, tzHoursAt(_prefs, ts), &sensors, (float)board.getBattMilliVolts() / 1000.0f, sender_name, hops, board.getMCUTemperature(), &s_bot_in); // Anti-loop, mirrors the channel path: don't echo a message identical to @@ -437,7 +438,7 @@ bool MyMesh::botCommandReply(const char* cmd, const char* arg, const char* arg2, expandMsg(tmpl, out, out_len, sensors.node_lat, sensors.node_lon, sensors.node_lat != 0.0 || sensors.node_lon != 0.0, - ts, _prefs.tz_offset_hours, + ts, tzHoursAt(_prefs, ts), &sensors, (float)board.getBattMilliVolts() / 1000.0f, sender_name, hops, board.getMCUTemperature(), &s_bot_in); if (strchr(out, '{')) strcpy(out, "n/a"); // requested sensor not present diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 2de5f1f7..70256754 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -268,6 +268,11 @@ struct NodePrefsFields { // ── Clock & alarm ───────────────────────────────────────────────────── uint8_t clock_hide_seconds; // 0=show HH:MM:SS/refresh 1s (default), 1=hide/refresh 60s uint8_t clock_12h; // 0=24h (default), 1=12h with AM/PM + // Summer time: the rule that puts the clock an hour ahead for part of the + // year, on top of tz_offset_hours (then the standard, winter offset). Every + // local time goes through tzHoursAt() below. + enum : uint8_t { DST_OFF, DST_EU, DST_US, DST_AU, DST_NZ, DST_COUNT }; + uint8_t dst_rule; // Alarm clock — a wake alarm, configured from the Clock page (Enter › Alarm). // Stored as a local time-of-day; the actual fire instant is (re)computed as an // absolute time in tickBackground(), which is what makes it robust to RTC @@ -830,7 +835,7 @@ class NodePrefs : public NodePrefsFields, public ConfigSerializer { s.def("kb_main", keyboard_main_alphabet); s.def("kb_cardkb", keyboard_cardkb_compact); } void clockSolo(Section& s) { - s.def("hide_sec", clock_hide_seconds); s.def("h12", clock_12h); + s.def("hide_sec", clock_hide_seconds); s.def("h12", clock_12h); s.def("dst", dst_rule); s.def("alarm", alarm_on); s.def("alarm_h", alarm_hour); s.def("alarm_m", alarm_min); s.def("alarm_rep", alarm_repeat_mask); } @@ -996,6 +1001,48 @@ static inline bool hourInWindow(int h, int start, int end) { if (start == end) return false; return start < end ? (h >= start && h < end) : (h >= start || h < end); } +// Days since 1970-01-01 of a calendar date (H. Hinnant's days_from_civil). +static inline int32_t daysFromCivil(int y, int m, int d) { + y -= m <= 2; + const int era = (y >= 0 ? y : y - 399) / 400; + const int yoe = y - era * 400; + const int doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; + return era * 146097 + yoe * 365 + yoe / 4 - yoe / 100 + doy - 719468; +} +// The day (since 1970) of the n-th Sunday of a month, n = 1..4, 5 = the last. +static inline int32_t nthSunday(int y, int m, int n) { + const int32_t first = daysFromCivil(y, m, 1); + int32_t d = first + (3 - first % 7 + 7) % 7 + (n - 1) * 7; // 1970-01-01 was a Thursday + if (n == 5 && d >= daysFromCivil(m == 12 ? y + 1 : y, m == 12 ? 1 : m + 1, 1)) d -= 7; + return d; +} +// Summer time in force at `utc`. The changes, per NodePrefs::dst_rule: Europe +// (EU / UK) last Sunday of March - last Sunday of October at 01:00 UTC; North +// America second Sunday of March - first Sunday of November; south-east +// Australia first Sunday of October - first Sunday of April; New Zealand last +// Sunday of September - first Sunday of April; those three forward at 02:00, +// back at 02:00 summer time in America and at 03:00 in the other two. +static inline bool inSummerTime(const NodePrefs& p, uint32_t utc) { + // Hours in local standard time, or UTC with `utc` set. + struct Rule { uint8_t on_mon, on_nth, on_hour, off_mon, off_nth, off_hour, utc; }; + static const Rule RULES[NodePrefs::DST_COUNT - 1] = { + { 3, 5, 1, 10, 5, 1, 1 }, { 3, 2, 2, 11, 1, 1, 0 }, { 10, 1, 2, 4, 1, 2, 0 }, { 9, 5, 2, 4, 1, 2, 0 }, + }; + if (p.dst_rule == NodePrefs::DST_OFF || p.dst_rule >= NodePrefs::DST_COUNT || utc < 1000000000UL) return false; + const Rule& r = RULES[p.dst_rule - 1]; + const int64_t std_off = r.utc ? 0 : (int64_t)p.tz_offset_hours * 3600; + time_t lt = (time_t)((int64_t)utc + (int64_t)p.tz_offset_hours * 3600); + const int y = gmtime(<)->tm_year + 1900; + const int64_t on = (int64_t)nthSunday(y, r.on_mon, r.on_nth) * 86400 + r.on_hour * 3600 - std_off; + const int64_t off = (int64_t)nthSunday(y, r.off_mon, r.off_nth) * 86400 + r.off_hour * 3600 - std_off; + const int64_t t = utc; + return on < off ? (t >= on && t < off) : (t >= on || t < off); // southern summers span New Year +} +// The offset from UTC in force at `utc`, in hours: the time zone plus summer time. +static inline int8_t tzHoursAt(const NodePrefs& p, uint32_t utc) { + return (int8_t)(p.tz_offset_hours + (inSummerTime(p, utc) ? 1 : 0)); +} +static inline int8_t tzHoursAt(const NodePrefs* p, uint32_t utc) { return p ? tzHoursAt(*p, utc) : 0; } // The local hour from a UTC time; false while the clock isn't set. static inline bool localHour(uint32_t utc, int8_t tz_hours, int& h) { if (utc < 1000000000UL) return false; @@ -1012,7 +1059,7 @@ static inline bool localTm(uint32_t utc, int8_t tz_hours, struct tm& out) { // Settings > Sound > Quiet hours in force at `utc` (never with the clock unset). static inline bool inQuietHours(const NodePrefs& p, uint32_t utc) { int h; - return p.quiet_hours && localHour(utc, p.tz_offset_hours, h) && hourInWindow(h, p.quiet_from, p.quiet_to); + return p.quiet_hours && localHour(utc, tzHoursAt(p, utc), h) && hourInWindow(h, p.quiet_from, p.quiet_to); } // Bounds for a usable repeater radio profile. The frequency range is passed in diff --git a/examples/companion_radio/ui-core/ClockEngine.h b/examples/companion_radio/ui-core/ClockEngine.h index 84eb93b2..e64b6b82 100644 --- a/examples/companion_radio/ui-core/ClockEngine.h +++ b/examples/companion_radio/ui-core/ClockEngine.h @@ -64,34 +64,44 @@ private: _events->push(UiEventType::ClockAlert, label); } + // The UTC instant a local wall time first happens. Summer time starting + // skips an hour: a time inside it comes as long after the jump as it was + // meant to be after its start (02:30 rings at 03:30). Summer time ending + // repeats one: the first of the two counts. + int64_t wallToUtc(int64_t local) const { + const int8_t tz = _prefs->tz_offset_hours; + for (int8_t h = tz + 1; h >= tz; h--) { // the summer offset gives the earlier instant + int64_t u = local - (int64_t)h * 3600; + if (tzHoursAt(_prefs, (uint32_t)u) == h) return u; + } + return local - (int64_t)tz * 3600; // skipped + } + // Next absolute wall instant matching alarm_hour:alarm_min in local time, // strictly after now_wall (an alarm set to the current minute waits a day). - // With alarm_repeat_mask == 0 that's just tomorrow's occurrence (one-shot). - // With a repeat mask set, scan today..+6 days for the next weekday whose bit - // is set (struct tm's tm_wday convention, same as the mask) -- today counts - // only if its time hasn't already passed. + // With alarm_repeat_mask == 0 that's just the next occurrence (one-shot). + // With a repeat mask set, scan today..+7 days for the next weekday whose bit + // is set (struct tm's tm_wday convention, same as the mask). Each day's time + // is turned into UTC with that day's offset, so summer time starting or + // ending in between doesn't move it. uint32_t computeAlarmNextFire(uint32_t now_wall) const { - int tz = _prefs->tz_offset_hours; - int64_t now_local = (int64_t)now_wall + (int64_t)tz * 3600; + int64_t now_local = (int64_t)now_wall + (int64_t)tzHoursAt(_prefs, now_wall) * 3600; time_t t = (time_t)now_local; struct tm* ti = gmtime(&t); int64_t sod = ti->tm_hour * 3600 + ti->tm_min * 60 + ti->tm_sec; // secs since local midnight int64_t midnight = now_local - sod; int64_t time_of_day = (int64_t)_prefs->alarm_hour * 3600 + (int64_t)_prefs->alarm_min * 60; + const int wday = ti->tm_wday; uint8_t mask = _prefs->alarm_repeat_mask; - if (mask != 0) { - for (int d = 0; d < 7; d++) { - if (mask & (1 << ((ti->tm_wday + d) % 7))) { - int64_t target = midnight + (int64_t)d * 86400 + time_of_day; - if (target > now_local) return (uint32_t)(target - (int64_t)tz * 3600); - } - } - // Mask had no bit set (shouldn't happen -- the UI only offers non-empty - // presets) -- fall through to the one-shot calculation so it still fires. + // A mask with no weekday bit (shouldn't happen -- the UI only offers + // non-empty presets) falls back to one-shot so it still fires. + if ((mask & 0x7F) == 0) mask = 0x7F; + for (int d = 0; d <= 7; d++) { + if (!(mask & (1 << ((wday + d) % 7)))) continue; + int64_t u = wallToUtc(midnight + (int64_t)d * 86400 + time_of_day); + if (u > (int64_t)now_wall) return (uint32_t)u; } - int64_t target = midnight + time_of_day; - if (target <= now_local) target += 86400; - return (uint32_t)(target - (int64_t)tz * 3600); + return (uint32_t)wallToUtc(midnight + 86400 + time_of_day); // not reached } void evaluateAlarm() { @@ -101,9 +111,11 @@ private: _alarm_check_ms = now_ms; uint32_t now_wall = rtc_clock.getCurrentTime(); if (now_wall < 1000000000UL) return; // need a real time sync first - // A time zone change (Settings, CLI tz.offset, the app) moves the local - // alarm time: reschedule here rather than trusting every caller to say so. - if (_alarm_tz != _prefs->tz_offset_hours) { _alarm_tz = _prefs->tz_offset_hours; _alarm_next_fire = 0; } + // A time zone change (Settings, CLI tz.offset, the app) or summer time + // starting / ending moves the local alarm time: reschedule here rather + // than trusting every caller to say so. + const int8_t tz = tzHoursAt(_prefs, now_wall); + if (_alarm_tz != tz) { _alarm_tz = tz; _alarm_next_fire = 0; } if (_alarm_next_fire == 0) _alarm_next_fire = computeAlarmNextFire(now_wall); if (now_wall < _alarm_next_fire) return; if (now_wall - _alarm_next_fire < ALARM_CATCHUP_SECS) { @@ -127,7 +139,7 @@ private: NodePrefs* _prefs = nullptr; UiEventQueue* _events = nullptr; uint32_t _alarm_next_fire = 0; // unix; 0 = (re)compute lazily once time is valid - int8_t _alarm_tz = 0; // the tz_offset_hours _alarm_next_fire was computed with + int8_t _alarm_tz = 0; // the offset (tzHoursAt) _alarm_next_fire was computed with uint32_t _alarm_check_ms = 0; // throttle the wall-clock read to ~2 Hz bool _timer_running = false; uint32_t _timer_deadline_ms = 0; diff --git a/examples/companion_radio/ui-core/MessageText.h b/examples/companion_radio/ui-core/MessageText.h index b7e74bd4..00511c4a 100644 --- a/examples/companion_radio/ui-core/MessageText.h +++ b/examples/companion_radio/ui-core/MessageText.h @@ -88,8 +88,9 @@ static void expand(const char* tmpl, char* out, int n, const NodePrefs* p) { } #endif float batt = (float)board.getBattMilliVolts() / 1000.0f; - ::expandMsg(tmpl, out, n, lat, lon, gps_valid, rtc_clock.getCurrentTime(), - p ? p->tz_offset_hours : 0, &sensors, batt, nullptr, -1, board.getMCUTemperature()); + const uint32_t now = rtc_clock.getCurrentTime(); + ::expandMsg(tmpl, out, n, lat, lon, gps_valid, now, + tzHoursAt(p, now), &sensors, batt, nullptr, -1, board.getMCUTemperature()); } // A typed message: a reply's leading "@[nick] " stays as it is (a nick may diff --git a/examples/companion_radio/ui-core/SettingsCli.h b/examples/companion_radio/ui-core/SettingsCli.h index a6432cdd..f0ac7880 100644 --- a/examples/companion_radio/ui-core/SettingsCli.h +++ b/examples/companion_radio/ui-core/SettingsCli.h @@ -26,7 +26,7 @@ static const Key KEYS[] = { K("display.alert", msg_alert), K("lock", auto_lock), K("lock.clock", lock_compact), K("display.text.size", ui_text_scale), K("display.icon.size", ui_icon_scale), K("batt.display", batt_display_mode), K("batt.shutdown", low_batt_mv), K("gps.interval", gps_interval), - K("clock.12h", clock_12h), K("clock.seconds", clock_hide_seconds), K("units.imperial", units_imperial), + K("clock.12h", clock_12h), K("clock.summer", dst_rule), K("clock.seconds", clock_hide_seconds), K("units.imperial", units_imperial), K("sound.volume", buzzer_volume), K("quiet", quiet_hours), K("quiet.from", quiet_from), K("quiet.until", quiet_to), K("melody.dm", notif_melody_dm), K("melody.channel", notif_melody_ch), K("melody.advert", notif_melody_ad), K("advert.sound", advert_sound_scope), diff --git a/examples/companion_radio/ui-core/SettingsSchema.h b/examples/companion_radio/ui-core/SettingsSchema.h index 01f1cd40..b392faa9 100644 --- a/examples/companion_radio/ui-core/SettingsSchema.h +++ b/examples/companion_radio/ui-core/SettingsSchema.h @@ -157,6 +157,10 @@ static void optLockLook(uint8_t v, char* b, int n, const NodePrefs&) { snprintf( static void optExpiry(uint8_t v, char* b, int n, const NodePrefs&) { snprintf(b, n, "%s", NodePrefs::contactExpiryLabel(v)); } +static void optDst(uint8_t v, char* b, int n, const NodePrefs&) { + static const char* L[NodePrefs::DST_COUNT] = { "Off", "Europe", "North America", "Australia", "New Zealand" }; + snprintf(b, n, "%s", L[v < NodePrefs::DST_COUNT ? v : 0]); +} static void optChSort(uint8_t v, char* b, int n, const NodePrefs&) { static const char* L[NodePrefs::CH_SORT_COUNT] = { "Slots", "Newest", "Name" }; snprintf(b, n, "%s", L[v < NodePrefs::CH_SORT_COUNT ? v : 0]); @@ -276,6 +280,7 @@ static const Setting ALL[] = { MAP("GPS power saving", "Sleep between fixes", SEC_POWER, gps_interval, GPS_DUTY, optGpsDuty, applyGpsDuty), MAP("Time zone", "For the clock and the alarm", SEC_TIME, tz_offset_hours, TZ, optTz, nullptr), + IDX("Summer time", "An hour ahead, changed by itself", SEC_TIME, dst_rule, NodePrefs::DST_COUNT, optDst, nullptr), SW("12-hour clock", "AM / PM instead of 24 h", SEC_TIME, clock_12h, nullptr), MAP("Clock seconds", "On the home clock", SEC_TIME, clock_hide_seconds, INVERTED, nullptr, nullptr), diff --git a/examples/companion_radio/ui-lvgl/BotScreen.h b/examples/companion_radio/ui-lvgl/BotScreen.h index 8088133d..333e173d 100644 --- a/examples/companion_radio/ui-lvgl/BotScreen.h +++ b/examples/companion_radio/ui-lvgl/BotScreen.h @@ -100,7 +100,7 @@ void UITask::buildBot() { if (log.count() == 0) label(g, "No replies yet", THEME_FONT_BODY, theme::TEXT_MUTED); for (int i = 0; i < log.count(); i++) { const BotLogEntry& e = log.at(i); - const uint32_t t = e.ts + (int32_t)p->tz_offset_hours * 3600; + const uint32_t t = e.ts + (int32_t)tzHoursAt(p, e.ts) * 3600; static const char* const WHERE[] = { "DM", "channel", "room" }; char hd[64]; snprintf(hd, sizeof(hd), "%02u:%02u %s (%s)", (unsigned)(t / 3600 % 24), (unsigned)(t / 60 % 60), diff --git a/examples/companion_radio/ui-lvgl/MapScreen.h b/examples/companion_radio/ui-lvgl/MapScreen.h index 5af85520..29a24264 100644 --- a/examples/companion_radio/ui-lvgl/MapScreen.h +++ b/examples/companion_radio/ui-lvgl/MapScreen.h @@ -1378,7 +1378,7 @@ void UITask::flowHistoryPopup() { const bool named = f.node[0] >= 0 && the_mesh.getContactByIdx(f.node[0], c) && c.name[0]; snprintf(title, sizeof(title), "%s%s%s%.24s", FLOW_KINDS[h.kind < PacketFlow::K_COUNT ? h.kind : PacketFlow::K_OTHER], named ? " " : "", named && !f.origin ? "via " : "", named ? c.name : ""); - const uint32_t t = h.ts + (int32_t)_prefs->tz_offset_hours * 3600; + const uint32_t t = h.ts + (int32_t)tzHoursAt(_prefs, h.ts) * 3600; int o = snprintf(sub, sizeof(sub), "%02u:%02u:%02u ", (unsigned)(t / 3600 % 24), (unsigned)(t / 60 % 60), (unsigned)(t % 60)); if (h.hops == 0) o += snprintf(sub + o, sizeof(sub) - o, "direct"); else o += snprintf(sub + o, sizeof(sub) - o, "%u hop%s", (unsigned)h.hops, h.hops == 1 ? "" : "s"); diff --git a/examples/companion_radio/ui-lvgl/SettingsPages.h b/examples/companion_radio/ui-lvgl/SettingsPages.h index ed0c3b90..d0192ed2 100644 --- a/examples/companion_radio/ui-lvgl/SettingsPages.h +++ b/examples/companion_radio/ui-lvgl/SettingsPages.h @@ -632,8 +632,8 @@ void UITask::settingsGroup(int i) { g = group(body, "SYSTEM"); listRow(g, LV_SYMBOL_EDIT " Name", the_mesh.getNodeName(), onNodeName, NULL); if (_prefs) { - snprintf(sub, sizeof(sub), "%s, %s", settingText(*_prefs, SETTING(tz_offset_hours), v1, sizeof(v1)), - _prefs->clock_12h ? "12 h" : "24 h"); + snprintf(sub, sizeof(sub), "%s%s, %s", settingText(*_prefs, SETTING(tz_offset_hours), v1, sizeof(v1)), + inSummerTime(*_prefs, rtc_clock.getCurrentTime()) ? " summer" : "", _prefs->clock_12h ? "12 h" : "24 h"); listRow(g, UI_SYMBOL_CLOCK " Time", sub, onOpenSchemaPage, (void*)(uintptr_t)PG_TIME); schemaRow(g, SETTING(units_imperial)); } diff --git a/examples/companion_radio/ui-lvgl/Widgets.h b/examples/companion_radio/ui-lvgl/Widgets.h index ed93ab6e..66e00cdc 100644 --- a/examples/companion_radio/ui-lvgl/Widgets.h +++ b/examples/companion_radio/ui-lvgl/Widgets.h @@ -1166,9 +1166,9 @@ static void tileBadge(lv_obj_t* tile, int n) { lv_obj_align(lv_obj_get_child(tile, -1), LV_ALIGN_TOP_RIGHT, -2, 2); } -// Local time (NodePrefs::tz_offset_hours), now or at `utc`; false before the clock is set. +// Local time (time zone + summer time), now or at `utc`; false before the clock is set. static bool localTime(const NodePrefs* p, struct tm& out, uint32_t utc = rtc_clock.getCurrentTime()) { - return localTm(utc, p ? p->tz_offset_hours : 0, out); + return localTm(utc, tzHoursAt(p, utc), out); } static const char* const MONTHS[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; diff --git a/examples/companion_radio/ui-new/BotScreen.h b/examples/companion_radio/ui-new/BotScreen.h index b9cb4c4d..34d02613 100644 --- a/examples/companion_radio/ui-new/BotScreen.h +++ b/examples/companion_radio/ui-new/BotScreen.h @@ -153,12 +153,11 @@ public: display.drawTextCentered(display.width() / 2, display.height() / 2, "No replies yet"); return 2000; } - const int8_t tz = _prefs ? _prefs->tz_offset_hours : 0; int mq_delay = 0; drawList(display, log.count(), _sel, _scroll, [&](int i, int y, bool sel, int reserve) { drawRowSelection(display, y, sel, reserve); char line[200]; - botcfg::logLine(log.at(i), tz, line, sizeof(line)); + botcfg::logLine(log.at(i), tzHoursAt(_prefs, log.at(i).ts), line, sizeof(line)); char filt[200]; display.translateUTF8ToBlocks(filt, line, sizeof(filt)); int r = display.drawTextEllipsized(2, y, display.width() - 2 - reserve, filt, sel); diff --git a/examples/companion_radio/ui-new/ClockToolsScreen.h b/examples/companion_radio/ui-new/ClockToolsScreen.h index 257c935b..f49ff2e0 100644 --- a/examples/companion_radio/ui-new/ClockToolsScreen.h +++ b/examples/companion_radio/ui-new/ClockToolsScreen.h @@ -263,7 +263,7 @@ class ClockToolsScreen : public UIScreen { // screen), the grid goes tight and the month and year stand beside it. int renderCalendar(DisplayDriver& d) { struct tm t; - if (!localTm(rtc_clock.getCurrentTime(), _prefs ? _prefs->tz_offset_hours : 0, t) || t.tm_year < 120) { + if (!localTm(rtc_clock.getCurrentTime(), tzHoursAt(_prefs, rtc_clock.getCurrentTime()), t) || t.tm_year < 120) { drawScreenHeader(d, "Calendar"); d.drawTextCentered(d.width() / 2, d.height() / 2 - d.getLineHeight() / 2, "Clock not set"); return 60000; diff --git a/examples/companion_radio/ui-new/HomeScreen.h b/examples/companion_radio/ui-new/HomeScreen.h index 2becdd85..a41f109d 100644 --- a/examples/companion_radio/ui-new/HomeScreen.h +++ b/examples/companion_radio/ui-new/HomeScreen.h @@ -1002,7 +1002,7 @@ public: void drawClockPage(DisplayDriver& d, uint32_t unix_ts) { struct tm ti; - localTm(unix_ts, _node_prefs ? _node_prefs->tz_offset_hours : 0, ti); + localTm(unix_ts, tzHoursAt(_node_prefs, unix_ts), ti); const bool h12 = _node_prefs && _node_prefs->clock_12h; const bool show_sec = !Features::IS_EINK && (!_node_prefs || !_node_prefs->clock_hide_seconds); const int W = d.width(), H = d.height(); @@ -1086,7 +1086,7 @@ public: const uint32_t unix_ts = _rtc->getCurrentTime(); const bool synced = unix_ts >= 1000000000UL; struct tm ti; - if (synced) localTm(unix_ts, _node_prefs ? _node_prefs->tz_offset_hours : 0, ti); + if (synced) localTm(unix_ts, tzHoursAt(_node_prefs, unix_ts), ti); const bool h12 = _node_prefs && _node_prefs->clock_12h; const bool compact = _node_prefs && _node_prefs->lock_compact && W > H; // The month and the rows under the big clock: on a tall screen (portrait @@ -1338,7 +1338,7 @@ public: uint32_t now_ts = _rtc->getCurrentTime(); if (now_ts >= 1000000000UL) { struct tm lt; - localTm(now_ts, _node_prefs->tz_offset_hours, lt); + localTm(now_ts, tzHoursAt(_node_prefs, now_ts), lt); int hh = lt.tm_hour; if (_node_prefs->clock_12h) { hh %= 12; if (hh == 0) hh = 12; } snprintf(filtered_name, sizeof(filtered_name), "%d:%02d", hh, lt.tm_min); diff --git a/examples/companion_radio/ui-new/TrailScreen.h b/examples/companion_radio/ui-new/TrailScreen.h index 6adaf999..f668ca75 100644 --- a/examples/companion_radio/ui-new/TrailScreen.h +++ b/examples/companion_radio/ui-new/TrailScreen.h @@ -632,14 +632,13 @@ private: _list_max_scroll = max_scroll; NodePrefs* p = _task->getNodePrefs(); - int32_t tz_off = (int32_t)(p ? p->tz_offset_hours : 0) * 3600; for (int i = 0; i < visible; i++) { int idx = total - 1 - (_list_scroll + i); if (idx < 0) break; const TrailPoint& pt = _store->at(idx); - time_t lt = (time_t)((int32_t)pt.ts + tz_off); + time_t lt = (time_t)((int32_t)pt.ts + (int32_t)tzHoursAt(p, pt.ts) * 3600); struct tm* ti = gmtime(<); char dist[12];