upstream dev bump

This commit is contained in:
liquidraver committed 2026-10-22 15:01:02 +02:00
1 parent c89ea27ae8
commit 6f7f807e82
20 files changed
+278 -58

No files matched your search

+1 -1
View File
@@ -33,7 +33,7 @@ list(APPEND BOARD_ROOT ${CMAKE_CURRENT_SOURCE_DIR})
# Service advertises (CONFIG_BT_DIS_FW_REV_STR, generated from it below), what the
# GitHub release is tagged/named, and what the Mesh America catalog uses as its
# version key. The release workflow reads this line directly — keep its form.
set(ZEPHCORE_FIRMWARE_VERSION "1.18.0-zephcore")
set(ZEPHCORE_FIRMWARE_VERSION "1.18.1-zephcore")
# ========== Helpers ==========
# Append FILE to the overlay list once, keeping its FIRST position. Position is
+13 -7
View File
@@ -1125,7 +1125,9 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, const char* command, cha
} else if (memcmp(config, "input.rotate", 12) == 0) {
snprintf(reply, CLI_REPLY_SIZE, "> %d", zephcore_input_is_flipped() ? 1 : 0);
} else if (memcmp(config, "tz.offset", 9) == 0) {
snprintf(reply, CLI_REPLY_SIZE, "> %d", (int)_prefs->tz_offset);
char hours[TZ_OFFSET_TEXT_LEN];
tz_offset_format_hours(tz_offset_minutes(_prefs->tz_offset), hours, sizeof(hours));
snprintf(reply, CLI_REPLY_SIZE, "> %s", hours);
} else if (memcmp(config, "gps diag", 8) == 0) {
/* What the last module-configuration attempt actually did.
* Self-limits on the remote path like cad.stats/meshtimesync:
@@ -1714,19 +1716,23 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, const char* command, cha
strcpy(reply, "Error: must be 0, 1, on, or off");
}
} else if (memcmp(config, "tz.offset ", 10) == 0) {
// Whole-hour offset from UTC for the on-device clock display only; the RTC,
// `clock` and `time <epoch>` stay UTC. Whole hours as upstream.
long val;
if (!cliNum(&config[10], cliDefaults()->tz_offset, &val)) {
// Hours from UTC in quarter-hour steps, for the on-device clock display
// only; the RTC, `clock` and `time <epoch>` stay UTC.
float val;
if (!cliFloat(&config[10], cliDefaults()->tz_offset, &val)) {
snprintf(reply, CLI_REPLY_SIZE, "Error: expected %d..%d or default",
TZ_OFFSET_MIN, TZ_OFFSET_MAX);
} else if (val < TZ_OFFSET_MIN || val > TZ_OFFSET_MAX) {
snprintf(reply, CLI_REPLY_SIZE, "Error: offset range is %d..%d",
TZ_OFFSET_MIN, TZ_OFFSET_MAX);
} else if (!tz_offset_valid(val)) {
strcpy(reply, "Error: offset must be whole quarter hours (e.g. 5.5, 5.75)");
} else {
_prefs->tz_offset = (int8_t)val;
char hours[TZ_OFFSET_TEXT_LEN];
_prefs->tz_offset = tz_offset_sanitize(val);
savePrefs();
snprintf(reply, CLI_REPLY_SIZE, "OK - tz.offset=%d", (int)val);
tz_offset_format_hours(tz_offset_minutes(_prefs->tz_offset), hours, sizeof(hours));
snprintf(reply, CLI_REPLY_SIZE, "OK - tz.offset=%s", hours);
}
} else if (memcmp(config, "gps diag", 8) == 0) {
// set gps diag <0|1|on|off> — arm module-configuration reporting.
+10 -11
View File
@@ -17,6 +17,9 @@
* and this header is C++; see the note there. */
#include "led_gate.h"
/* TZ_OFFSET_MIN / TZ_OFFSET_MAX and the quarter-hour helpers for tz_offset. */
#include "tz_offset.h"
/* ── Accepted radio parameter ranges ──
* One definition for everything that accepts or validates a radio preset
* (CLIs, the companion protocol, the load-time checks): a setter must never
@@ -62,11 +65,6 @@
* ConfigureSideDetectors limit of 3. */
#define EXTRA_SF_MAX 3
/* Display timezone offset range, whole hours from UTC. Matches upstream
* MeshCore's `set tz.offset` so the same command works against both trees. */
#define TZ_OFFSET_MIN (-12)
#define TZ_OFFSET_MAX 14
struct NodePrefs {
/* ---- Common fields (both roles) ---- */
float airtime_factor;
@@ -149,10 +147,10 @@ struct NodePrefs {
uint8_t display_rotate; // 1 = panel rotated 180 degrees
uint8_t input_rotate; // 1 = joystick up/down and left/right swapped
/* Whole-hour offset from UTC, -12..+14, applied only when formatting a clock
* for the local display. It must never reach RTCClock or any timestamp that
* leaves this node. */
int8_t tz_offset;
/* Hours from UTC, -12..+14 in quarter hours (float, as upstream), applied
* only when formatting a clock for the local display. It must never reach
* RTCClock or any timestamp that leaves this node. See tz_offset.h. */
float tz_offset;
/* The family base detPeak that cad_offset was learned against (0 = never
* stored). Lets the firmware re-anchor the offset at boot when the driver's
@@ -354,8 +352,9 @@ static inline void sanitizeNodePrefs(NodePrefs* p) {
/* Neutral, not the boundary — a garbage offset is not a request to run
* the CAD staircase at one end of its range. */
if (p->cad_offset < CAD_OFFSET_MIN || p->cad_offset > CAD_OFFSET_MAX) p->cad_offset = 0;
/* UTC, not a boundary zone, for a byte that carries no user intent. */
if (p->tz_offset < TZ_OFFSET_MIN || p->tz_offset > TZ_OFFSET_MAX) p->tz_offset = 0;
/* UTC, not a boundary zone, for a value that carries no user intent;
* an in-range value snaps to the nearest quarter hour. */
p->tz_offset = tz_offset_sanitize(p->tz_offset);
if (p->probe_interval != 0 && p->probe_interval < 10) p->probe_interval = 10;
/* 6-digit BLE passkey — anything else is rejected at pairing time and
+14 -4
View File
@@ -17,6 +17,9 @@ struct Wr {
size_t off;
void put(const void *src, size_t n) { memcpy(b + off, src, n); off += n; }
void zeros(size_t n) { memset(b + off, 0, n); off += n; }
/* tz_offset: both layouts hold signed whole hours in one byte, so a
* fractional offset is truncated toward zero. */
void tz(float hours) { int8_t v = (int8_t)tz_offset_sanitize(hours); put(&v, 1); }
};
struct Rd {
@@ -36,6 +39,13 @@ struct Rd {
}
off += n;
}
/* The one-byte whole-hours tz_offset into the float field. */
void tz(float &hours) {
if (off < len) {
hours = (int8_t)b[off];
}
off += 1;
}
};
} // namespace
@@ -103,7 +113,7 @@ size_t companionPrefsEncode(const NodePrefs &p, uint8_t *buf, size_t cap)
w.put(&p.display_rotate, 1);
w.put(&p.input_rotate, 1);
w.put(&p.cad_base, 1);
w.put(&p.tz_offset, 1);
w.tz(p.tz_offset);
w.put(&p.leds_radio_mode, 1);
w.put(&p.leds_hb_mode, 1);
w.put(&p.wifi_enabled, 1);
@@ -184,7 +194,7 @@ bool companionPrefsDecode(NodePrefs &prefs, const uint8_t *buf, size_t len)
/* cad_base 0 = "no base recorded": setCadParams() then leaves the stored
* offset alone. */
r.take(&p.cad_base, 1);
r.take(&p.tz_offset, 1);
r.tz(p.tz_offset);
r.take(&p.leds_radio_mode, 1);
r.take(&p.leds_hb_mode, 1);
r.take(&p.wifi_enabled, 1);
@@ -276,7 +286,7 @@ size_t serverPrefsEncode(const NodePrefs &prefs, uint8_t *buf, size_t cap)
w.put(&prefs.cad_base, sizeof(prefs.cad_base));
/* Display timezone offset (offset 308) — signed whole hours from UTC,
* applied only when formatting the on-device clock */
w.put(&prefs.tz_offset, sizeof(prefs.tz_offset));
w.tz(prefs.tz_offset);
/* LED activity/heartbeat modes (offsets 309-310) */
w.put(&prefs.leds_radio_mode, sizeof(prefs.leds_radio_mode));
w.put(&prefs.leds_hb_mode, sizeof(prefs.leds_hb_mode));
@@ -352,7 +362,7 @@ void serverPrefsDecode(NodePrefs &prefs, const uint8_t *buf, size_t len)
* offset alone, since a node upgrading across a table change cannot know
* which base its offset came from. */
r.get(&prefs.cad_base, sizeof(prefs.cad_base));
r.get(&prefs.tz_offset, sizeof(prefs.tz_offset));
r.tz(prefs.tz_offset);
r.get(&prefs.leds_radio_mode, sizeof(prefs.leds_radio_mode));
r.get(&prefs.leds_hb_mode, sizeof(prefs.leds_hb_mode));
+79
View File
@@ -0,0 +1,79 @@
/*
* SPDX-License-Identifier: MIT
* Display timezone offset: validation and text, shared by the CLI, both UIs
* and the prefs loaders (C and C++).
*
* NodePrefs::tz_offset holds float hours from UTC, upstream MeshCore's type and
* prefs.json value. ZephCore accepts quarter hours only and every consumer
* works in whole minutes. The offset is applied only when a clock is formatted
* for the local display; it never reaches RTCClock or a timestamp.
*/
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
/* Range in hours, as upstream's `set tz.offset`. */
#define TZ_OFFSET_MIN (-12)
#define TZ_OFFSET_MAX 14
/* Buffer size for tz_offset_format_hours() and tz_offset_format_label(). */
#define TZ_OFFSET_TEXT_LEN 12
/* Nearest quarter hour; 0 (UTC) for NaN or a value outside the range. */
static inline float tz_offset_sanitize(float hours)
{
if (hours != hours || hours < TZ_OFFSET_MIN || hours > TZ_OFFSET_MAX) {
return 0.0f;
}
float q = hours * 4.0f;
return (float)(int32_t)(q < 0.0f ? q - 0.5f : q + 0.5f) * 0.25f;
}
/* In range and a whole number of quarter hours (within 0.0001 h). */
static inline bool tz_offset_valid(float hours)
{
if (hours != hours || hours < TZ_OFFSET_MIN || hours > TZ_OFFSET_MAX) {
return false;
}
float d = hours - tz_offset_sanitize(hours);
return d > -0.0001f && d < 0.0001f;
}
/* Offset in whole minutes, from a sanitized value. */
static inline int16_t tz_offset_minutes(float hours)
{
float m = tz_offset_sanitize(hours) * 60.0f;
return (int16_t)(m < 0.0f ? m - 0.5f : m + 0.5f);
}
/* Decimal hours, exact for every quarter hour: "0.0", "2.0", "-3.5", "5.75".
* Same text as upstream's "%.1f" wherever that is exact. */
static inline void tz_offset_format_hours(int16_t minutes, char *buf, size_t n)
{
static const char *const frac[4] = { "0", "25", "5", "75" };
unsigned a = (unsigned)(minutes < 0 ? -minutes : minutes);
snprintf(buf, n, "%s%u.%s", minutes < 0 ? "-" : "", a / 60, frac[(a % 60) / 15]);
}
/* Zone label for a clock display, at most 8 characters: "UTC", "UTC+2",
* "UTC-3:30", and "UTC+1245" where the h:mm form would be longer. */
static inline void tz_offset_format_label(int16_t minutes, char *buf, size_t n)
{
unsigned a = (unsigned)(minutes < 0 ? -minutes : minutes);
char sign = minutes < 0 ? '-' : '+';
if (a == 0) {
snprintf(buf, n, "UTC");
} else if (a % 60 == 0) {
snprintf(buf, n, "UTC%c%u", sign, a / 60);
} else if (a / 60 < 10) {
snprintf(buf, n, "UTC%c%u:%02u", sign, a / 60, a % 60);
} else {
snprintf(buf, n, "UTC%c%02u%02u", sign, a / 60, a % 60);
}
}
+8 -9
View File
@@ -19,6 +19,7 @@
#include "ui_mesh_actions.h"
#include "display.h"
#include <helpers/buzzer_gate.h>
#include <helpers/tz_offset.h>
#include <time_sync.h>
#include <ZephyrGPSManager.h>
@@ -200,19 +201,17 @@ static int current_page_idx;
* silent mesh-wide mute. Never write these values back into state. */
static uint32_t local_epoch(void)
{
return state.rtc_epoch + (int32_t)state.tz_offset * 3600;
return state.rtc_epoch + (int32_t)state.tz_offset_min * 60;
}
/* The zone the displayed digits are actually in: "UTC", "UTC+2", "UTC-11".
/* The zone the displayed digits are actually in: "UTC", "UTC+2", "UTC-3:30",
* at most 8 characters (tz_offset_format_label()).
* Static buffer -- every caller is on the UI thread inside ui_pages_render(). */
static const char *tz_label(void)
{
static char label[8];
static char label[TZ_OFFSET_TEXT_LEN];
if (state.tz_offset == 0) {
return "UTC";
}
snprintf(label, sizeof(label), "UTC%+d", (int)state.tz_offset);
tz_offset_format_label(state.tz_offset_min, label, sizeof(label));
return label;
}
@@ -1713,8 +1712,8 @@ static void render_status(void)
uint8_t mm = (day_sec % 3600) / 60;
uint8_t ss = day_sec % 60;
/* "T:" rather than "Time:" so a two-digit zone still fits the
* 20-column budget of the 200x200 e-paper boards, which pair
/* "T:" rather than "Time:" so an 8-character zone label still fits
* the 20-column budget of the 200x200 e-paper boards, which pair
* CONFIG_ZEPHCORE_DISPLAY_LARGE_FONT (10x16) with this page. */
snprintf(buf, sizeof(buf), "T: %02u:%02u:%02u %s", hh, mm, ss,
tz_label());
+1 -1
View File
@@ -45,7 +45,7 @@ struct ui_state {
uint16_t battery_mv;
uint8_t battery_pct;
uint32_t rtc_epoch; /* Unix epoch from RTC, ALWAYS UTC (0 = not set) */
int8_t tz_offset; /* whole hours from UTC, applied at format time
int16_t tz_offset_min; /* minutes from UTC, applied at format time
* only -- rtc_epoch itself is never shifted */
/* Messages page */
+3 -3
View File
@@ -1035,14 +1035,14 @@ void ui_set_clock(uint32_t epoch)
}
}
void ui_set_tz(int8_t hours)
void ui_set_tz(int16_t minutes)
{
struct ui_state *s = get_state();
if (s->tz_offset == hours) {
if (s->tz_offset_min == minutes) {
return;
}
s->tz_offset = hours;
s->tz_offset_min = minutes;
/* EPD only redraws on demand, so a timezone change would otherwise not
* appear until something else forced a repaint. */
@@ -260,7 +260,7 @@ extern "C" void ui_set_ble_enabled(bool enabled)
extern "C" void ui_set_gps_data(bool, uint8_t, int32_t, int32_t, int32_t) {}
extern "C" void ui_set_clock(uint32_t) {}
/* The joystick UI reads tz_offset straight off NodePrefs at render time. */
extern "C" void ui_set_tz(int8_t) {}
extern "C" void ui_set_tz(int16_t) {}
extern "C" void ui_add_recent(const char *, int16_t, uint32_t) {}
extern "C" void ui_set_node_name(const char *) {}
extern "C" void ui_clear_recent(void) {}
@@ -349,25 +349,21 @@ int SystemTimeScreen::render(JoystickDisplay &display)
/* The RTC is UTC; the offset is applied here, at format time only. It
* must never reach the clock itself -- see NodePrefs::tz_offset. */
NodePrefs *prefs = _task->getPrefs();
int8_t tz = prefs ? prefs->tz_offset : 0;
int16_t tz_min = prefs ? tz_offset_minutes(prefs->tz_offset) : 0;
uint32_t epoch = _rtc->getCurrentTime();
/* Before a sync getCurrentTime() returns bare uptime, and a negative
* offset would underflow it into a garbage far-future date. Same
* "time is set" threshold (2025-01-01) the other UI uses. */
if (epoch > 1735689600) {
epoch += (int32_t)tz * 3600;
epoch += (int32_t)tz_min * 60;
} else {
tz = 0;
tz_min = 0;
}
char timeText[16], dateText[16], tzText[8];
char timeText[16], dateText[16], tzText[TZ_OFFSET_TEXT_LEN];
formatUnixDateTime(epoch, timeText, sizeof(timeText), dateText, sizeof(dateText));
if (tz == 0) {
strcpy(tzText, "UTC");
} else {
snprintf(tzText, sizeof(tzText), "UTC%+d", (int)tz);
}
tz_offset_format_label(tz_min, tzText, sizeof(tzText));
const char *labels[4] = { "Time:", "Date:", "TZ:", "Source:" };
const char *values[4] = { timeText, dateText, tzText,
+2 -2
View File
@@ -77,9 +77,9 @@ WEAK void ui_set_clock(uint32_t epoch)
ARG_UNUSED(epoch);
}
WEAK void ui_set_tz(int8_t hours)
WEAK void ui_set_tz(int16_t minutes)
{
ARG_UNUSED(hours);
ARG_UNUSED(minutes);
}
WEAK void ui_add_recent(const char *name, int16_t rssi, uint32_t age_s)
+1 -1
View File
@@ -352,7 +352,7 @@ extern "C" void mesh_housekeeping_ui_refresh(void)
* display offset is a separate push so nothing downstream is tempted to
* bake a timezone into a timestamp (see NodePrefs::tz_offset). */
ui_set_clock(s_rtc_clock->getCurrentTime());
ui_set_tz(s_mesh->prefs.tz_offset);
ui_set_tz(tz_offset_minutes(s_mesh->prefs.tz_offset));
ui_push_radio_state(*s_lora_radio);
/* The app and the CLI change it too. */
+6 -5
View File
@@ -119,13 +119,14 @@ void ui_set_battery(uint16_t mv, uint8_t pct);
void ui_set_clock(uint32_t epoch);
/**
* Set the whole-hour offset from UTC used when formatting the on-device
* clock. Display only: the epoch handed to ui_set_clock() stays UTC, and
* nothing here may ever reach RTCClock or a packet timestamp.
* Set the offset from UTC used when formatting the on-device clock. Display
* only: the epoch handed to ui_set_clock() stays UTC, and nothing here may
* ever reach RTCClock or a packet timestamp.
*
* @param hours Offset from UTC, TZ_OFFSET_MIN..TZ_OFFSET_MAX (0 = UTC)
* @param minutes Offset from UTC in minutes, a multiple of 15 within
* TZ_OFFSET_MIN..TZ_OFFSET_MAX hours (0 = UTC)
*/
void ui_set_tz(int8_t hours);
void ui_set_tz(int16_t minutes);
/**
* Record a recently heard contact for the "recent" page.
+4
View File
@@ -563,6 +563,10 @@ static void server_event_loop(void)
#if ZEPHCORE_HAS_UI
ui_set_clock(rtc_clock.getCurrentTime());
/* Display offset only; the epoch above stays UTC. */
if (server_role) {
ui_set_tz(tz_offset_minutes(server_role->prefs->tz_offset));
}
#ifdef ZEPHCORE_LORA
/* Refresh live radio state (noise floor, TX power