From f508c14dbb9ab2be1c2ed9fe57b62878c06d830e Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Fri, 5 Jun 2026 14:56:11 +0200 Subject: [PATCH] display improvements --- zephcore/CMakeLists.txt | 1 + zephcore/Kconfig | 12 ++++ zephcore/helpers/CommonCLI.cpp | 3 + zephcore/helpers/time_sync.h | 19 ++++- zephcore/helpers/ui-button/time_sync.c | 42 +++++++++++ zephcore/helpers/ui-button/ui_pages.c | 88 +++++++++++++++--------- zephcore/helpers/ui-joystick/time_sync.c | 26 +++++++ 7 files changed, 156 insertions(+), 35 deletions(-) create mode 100644 zephcore/helpers/ui-button/time_sync.c diff --git a/zephcore/CMakeLists.txt b/zephcore/CMakeLists.txt index 37cbf4a..1f30842 100644 --- a/zephcore/CMakeLists.txt +++ b/zephcore/CMakeLists.txt @@ -698,6 +698,7 @@ endif() if(CONFIG_ZEPHCORE_UI_DESIGN_BUTTON) target_sources(app PRIVATE helpers/ui-button/ui_task.c + helpers/ui-button/time_sync.c ) target_include_directories(app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/helpers/ui-button diff --git a/zephcore/Kconfig b/zephcore/Kconfig index faa7efe..2477f63 100644 --- a/zephcore/Kconfig +++ b/zephcore/Kconfig @@ -620,6 +620,18 @@ config ZEPHCORE_UI_DISPLAY_AUTO_OFF_MS Time in milliseconds before the display turns off after the last user interaction. Set to 0 to disable. +config ZEPHCORE_UI_TIME_SOURCE_FRESH_HOURS + int "Clock source tag freshness window (hours)" + default 12 + help + The top-bar clock shows a one-character source tag: G (GPS), + A (app), N (network/SNTP). If the most recent sync from one of + those sources is older than this many hours, the tag falls back + to L (local) — the RTC is free-running on its own oscillator. + Set to 0 to keep the last external source tag indefinitely + (no freshness expiry). Drift on a 50 ppm crystal is ~4 s/day, + so this is a trust/freshness signal, not an accuracy limit. + config ZEPHCORE_AUTO_SHUTDOWN_MILLIVOLTS int "Low-battery auto-shutdown threshold (mV, 0 = disabled)" default 3300 if SOC_SERIES_NRF52 diff --git a/zephcore/helpers/CommonCLI.cpp b/zephcore/helpers/CommonCLI.cpp index 1831751..5a171c0 100644 --- a/zephcore/helpers/CommonCLI.cpp +++ b/zephcore/helpers/CommonCLI.cpp @@ -5,6 +5,7 @@ #include "CommonCLI.h" #include "battery_curve.h" +#include #include #include #include @@ -331,6 +332,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch uint32_t curr = getRTCClock()->getCurrentTime(); if (sender_timestamp > curr) { getRTCClock()->setCurrentTime(sender_timestamp + 1); + time_sync_report(TIME_SYNC_CLI); uint32_t now = getRTCClock()->getCurrentTime(); time_t t = (time_t)now; struct tm *tm = gmtime(&t); @@ -350,6 +352,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch uint32_t curr = getRTCClock()->getCurrentTime(); if (secs > curr) { getRTCClock()->setCurrentTime(secs); + time_sync_report(TIME_SYNC_CLI); time_t t = (time_t)secs; struct tm *tm = gmtime(&t); snprintf(reply, CLI_REPLY_SIZE, "OK - clock set: %02d:%02d - %d/%d/%d UTC", diff --git a/zephcore/helpers/time_sync.h b/zephcore/helpers/time_sync.h index 88c9eb5..3d8e7fe 100644 --- a/zephcore/helpers/time_sync.h +++ b/zephcore/helpers/time_sync.h @@ -19,10 +19,14 @@ enum time_sync_source { TIME_SYNC_CLI, }; -#if IS_ENABLED(CONFIG_ZEPHCORE_UI_DESIGN_JOYSTICK) +#if IS_ENABLED(CONFIG_ZEPHCORE_UI_DESIGN_JOYSTICK) || \ + IS_ENABLED(CONFIG_ZEPHCORE_UI_DESIGN_BUTTON) void time_sync_report(enum time_sync_source src); -const char *time_sync_display_label(void); + +/* Current effective time source (prefers live GPS sync). TIME_SYNC_NONE + * if the RTC has never been set. */ +enum time_sync_source time_sync_get_source(void); #else @@ -31,7 +35,16 @@ static inline void time_sync_report(enum time_sync_source src) ARG_UNUSED(src); } -#endif /* CONFIG_ZEPHCORE_UI_DESIGN_JOYSTICK */ +static inline enum time_sync_source time_sync_get_source(void) +{ + return TIME_SYNC_NONE; +} + +#endif /* CONFIG_ZEPHCORE_UI_DESIGN_JOYSTICK || _BUTTON */ + +#if IS_ENABLED(CONFIG_ZEPHCORE_UI_DESIGN_JOYSTICK) +const char *time_sync_display_label(void); +#endif #ifdef __cplusplus } diff --git a/zephcore/helpers/ui-button/time_sync.c b/zephcore/helpers/ui-button/time_sync.c new file mode 100644 index 0000000..11bef0e --- /dev/null +++ b/zephcore/helpers/ui-button/time_sync.c @@ -0,0 +1,42 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Button UI: track which source last set the RTC, for the top-bar clock tag. + * + * The tag reflects *freshness*: an external sync (GPS / app / network) is + * shown only while it is recent (CONFIG_ZEPHCORE_UI_TIME_SOURCE_FRESH_HOURS). + * Once it ages out — or after a reboot, before any sync — the source reads as + * local (the RTC is free-running on its own oscillator). + */ + +#include +#include + +static enum time_sync_source s_last = TIME_SYNC_NONE; +static int64_t s_last_uptime; /* k_uptime_get() at the last report */ + +void time_sync_report(enum time_sync_source src) +{ + s_last = src; + s_last_uptime = k_uptime_get(); +} + +enum time_sync_source time_sync_get_source(void) +{ + /* Local/manual (CLI) and "never synced" are both just local time. */ + if (s_last == TIME_SYNC_NONE || s_last == TIME_SYNC_CLI) { + return TIME_SYNC_CLI; + } + +#if CONFIG_ZEPHCORE_UI_TIME_SOURCE_FRESH_HOURS > 0 + int64_t age_ms = k_uptime_get() - s_last_uptime; + int64_t window_ms = + (int64_t)CONFIG_ZEPHCORE_UI_TIME_SOURCE_FRESH_HOURS * 3600 * 1000; + + if (age_ms > window_ms) { + /* External sync has gone stale — fall back to local. */ + return TIME_SYNC_CLI; + } +#endif + + return s_last; /* GPS / APP / WIFI, still fresh */ +} diff --git a/zephcore/helpers/ui-button/ui_pages.c b/zephcore/helpers/ui-button/ui_pages.c index 97406e9..ab972e6 100644 --- a/zephcore/helpers/ui-button/ui_pages.c +++ b/zephcore/helpers/ui-button/ui_pages.c @@ -18,6 +18,7 @@ #include "ui_task.h" #include "display.h" +#include #include #include @@ -123,49 +124,72 @@ static uint8_t calc_battery_pct(uint16_t mv) /* ========== Helper: Top Bar (Node Name + Battery) ========== */ +/* One-char tag for where the displayed clock was last freshly synced from + * (see time_sync_get_source() for the freshness window). Always returns a + * letter — falls back to 'L' (local) when no recent external sync. */ +static char time_source_tag(enum time_sync_source src) +{ + switch (src) { + case TIME_SYNC_GPS: return 'G'; /* GPS fix */ + case TIME_SYNC_APP: return 'A'; /* phone/companion app */ + case TIME_SYNC_WIFI: return 'N'; /* network (SNTP) */ + default: return 'L'; /* local: manual/CLI or stale/none */ + } +} + static void render_top_bar(void) { - /* Node name on the left */ - if (state.node_name[0]) { - /* Truncate to fit left side (leave room for battery) */ - char name[16]; + /* Right side: "HH:MM XX%" — clock (with 1-char source tag) then + * battery, right-aligned. Built first so the node name can be clipped + * to the space that remains on the left, preventing overlap. */ + char right[16] = ""; + int pos = 0; - strncpy(name, state.node_name, sizeof(name) - 1); - name[sizeof(name) - 1] = '\0'; - mc_display_text(0, TOP_BAR_Y, name, false); + /* 24h clock from RTC epoch (only if time has been synced). + * Before sync, getCurrentTime() returns bare uptime (~seconds), + * so check for a sane epoch (after Jan 1 2025 = 1735689600). */ + if (state.rtc_epoch > 1735689600) { + uint32_t day_sec = state.rtc_epoch % 86400; + uint8_t hh = day_sec / 3600; + uint8_t mm = (day_sec % 3600) / 60; + + /* Source tag: G=GPS, A=app, N=network, L=local (always set). */ + char src = time_source_tag(time_sync_get_source()); + + pos = snprintf(right, sizeof(right), "%02u:%02u%c ", hh, mm, src); } - /* Right side: "HH:MM XX%" — clock then battery, right-aligned */ - { - char right[16] = ""; - int pos = 0; + /* Battery percentage */ + if (state.battery_mv > 0) { + uint8_t pct = state.battery_pct; - /* 24h clock from RTC epoch (only if time has been synced). - * Before sync, getCurrentTime() returns bare uptime (~seconds), - * so check for a sane epoch (after Jan 1 2025 = 1735689600). */ - if (state.rtc_epoch > 1735689600) { - uint32_t day_sec = state.rtc_epoch % 86400; - uint8_t hh = day_sec / 3600; - uint8_t mm = (day_sec % 3600) / 60; - - pos = snprintf(right, sizeof(right), "%02u:%02u ", hh, mm); + if (pct == 0) { + pct = calc_battery_pct(state.battery_mv); } + snprintf(right + pos, sizeof(right) - pos, "%u%%", pct); + } - /* Battery percentage */ - if (state.battery_mv > 0) { - uint8_t pct = state.battery_pct; + int right_x = DISP_W; + if (right[0]) { + right_x = DISP_W - ((int)strlen(right) * FONT_W); + mc_display_text(right_x, TOP_BAR_Y, right, false); + } - if (pct == 0) { - pct = calc_battery_pct(state.battery_mv); - } - snprintf(right + pos, sizeof(right) - pos, "%u%%", pct); + /* Node name on the left, clipped to the gap before the right block + * (one char-width of padding so it never touches the clock). */ + if (state.node_name[0]) { + char name[16]; + int max_chars = (right_x - FONT_W) / FONT_W; + + if (max_chars > (int)sizeof(name) - 1) { + max_chars = sizeof(name) - 1; } - - if (right[0]) { - int x = DISP_W - ((int)strlen(right) * FONT_W); - - mc_display_text(x, TOP_BAR_Y, right, false); + if (max_chars < 0) { + max_chars = 0; } + strncpy(name, state.node_name, max_chars); + name[max_chars] = '\0'; + mc_display_text(0, TOP_BAR_Y, name, false); } } diff --git a/zephcore/helpers/ui-joystick/time_sync.c b/zephcore/helpers/ui-joystick/time_sync.c index 4b6a41f..e324d17 100644 --- a/zephcore/helpers/ui-joystick/time_sync.c +++ b/zephcore/helpers/ui-joystick/time_sync.c @@ -4,9 +4,11 @@ #include #include +#include #include static enum time_sync_source s_last = TIME_SYNC_NONE; +static int64_t s_last_uptime; /* k_uptime_get() at the last report */ static const char *source_short_name(enum time_sync_source src) { @@ -22,6 +24,30 @@ static const char *source_short_name(enum time_sync_source src) void time_sync_report(enum time_sync_source src) { s_last = src; + s_last_uptime = k_uptime_get(); +} + +/* Freshness-windowed source for the top-bar clock tag. Local/manual (CLI) + * and "never synced" both read as local; an external sync ages out after + * CONFIG_ZEPHCORE_UI_TIME_SOURCE_FRESH_HOURS. (Distinct from the System > + * Info > Time label below, which is not time-windowed.) */ +enum time_sync_source time_sync_get_source(void) +{ + if (s_last == TIME_SYNC_NONE || s_last == TIME_SYNC_CLI) { + return TIME_SYNC_CLI; + } + +#if CONFIG_ZEPHCORE_UI_TIME_SOURCE_FRESH_HOURS > 0 + int64_t age_ms = k_uptime_get() - s_last_uptime; + int64_t window_ms = + (int64_t)CONFIG_ZEPHCORE_UI_TIME_SOURCE_FRESH_HOURS * 3600 * 1000; + + if (age_ms > window_ms) { + return TIME_SYNC_CLI; + } +#endif + + return s_last; } const char *time_sync_display_label(void)