rtc: write the RTC time only for years 2000-2099

ZephCore writes the year as two BCD digits with no century, as y % 100.
A time before 2000, accepted by a node whose clock had no time yet, was
stored as a year near the end of the century and restored at the next
boot above the 2025 floor: `time 900000000` (1998) came back as 2098.

rtc_time_block() now fails outside 2000-2099, and zephcore_rtc_save()
then logs and writes no time, for every chip; the chip keeps the time it
has. An RV3028 write retry stops if its run-on time leaves the range.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Pieter ViljoenandClaude Opus 5.5 committed 2026-10-07 07:09:40 -07:00
1 parent a2e8d5f440
commit 9b68b7e1ae
5 files changed
+30 -18

No files matched your search

+1 -1
View File
@@ -860,7 +860,7 @@ Repeaters and room servers default to `CONFIG_ZEPHCORE_REPEATER_GPS_INTERVAL_SEC
**Telemetry** (`REQ_TYPE_GET_TELEMETRY_DATA`, every role, upstream's shape): battery on channel 1, then `sensors.querySensors()` (`adapters/sensors/ZephyrSensorManager.cpp`): GPS position on channel 1 while the GPS is on and has a fix, each environment sensor and power-monitor channel found at boot on its own channel from 2 up (probe order), board-local analog sensors (T1000-E) on channel 1; then the MCU temperature on channel 1. Servers give guests battery + MCU temperature only and honour the requester's inverse permission mask, as upstream. Encoded by `helpers/compat/CayenneLPP.h`: upstream's library API and wire format, but values round to the nearest step where the library truncates.
**Clock** (`adapters/clock/ZephyrRTCClock`): `setCurrentTime()` also writes the hardware RTC when the board has one (coalesced on the system work queue), as upstream's `AutoDiscoverRTCClock` writes its chip; `seedCurrentTime()` is the boot restore, not written back. A chip is adopted at boot only once it is identified (`adapters/clock/ZephyrRTCDiscover.c`), because other parts can answer at the same address: a device is passed over when two reads each show a bit set that the chip's data sheet fixes at zero, or a time field out of range, and a time is restored only from a read in which every field is valid. The time is written in each chip's own encoding (the RX8130CE takes a one-hot weekday, `weekday-one-hot` in its descriptor), and an RV-3028 whose descriptor carries `rv3028-eeprom-config` (the RAK12002 on a RAK4631) has that configuration, its backup switchover, stored in the chip's EEPROM. Without a hardware RTC, the last time read is kept in no-init memory (RTC slow memory on ESP32) and restored after a reset that keeps RAM (reboot, crash, watchdog), as upstream's `ESP32RTCClock`; a power cycle starts from 1970 again.
**Clock** (`adapters/clock/ZephyrRTCClock`): `setCurrentTime()` also writes the hardware RTC when the board has one (coalesced on the system work queue; a time outside 2000-2099 is not written), as upstream's `AutoDiscoverRTCClock` writes its chip; `seedCurrentTime()` is the boot restore, not written back. A chip is adopted at boot only once it is identified (`adapters/clock/ZephyrRTCDiscover.c`), because other parts can answer at the same address: a device is passed over when two reads each show a bit set that the chip's data sheet fixes at zero, or a time field out of range, and a time is restored only from a read in which every field is valid. The time is written in each chip's own encoding (the RX8130CE takes a one-hot weekday, `weekday-one-hot` in its descriptor), and an RV-3028 whose descriptor carries `rv3028-eeprom-config` (the RAK12002 on a RAK4631) has that configuration, its backup switchover, stored in the chip's EEPROM. Without a hardware RTC, the last time read is kept in no-init memory (RTC slow memory on ESP32) and restored after a reset that keeps RAM (reboot, crash, watchdog), as upstream's `ESP32RTCClock`; a power cycle starts from 1970 again.
**LED master switch** (`helpers/led_gate.{c,h}`, `set leds on|off`, all roles): one process-wide
flag every LED driver consults — heartbeat and unread-message LEDs in `helpers/ui/ui_common.c`, the
+3 -2
View File
@@ -911,8 +911,9 @@ config ZEPHCORE_RTC_AUTODISCOVER
backed RTC opt in by including boards/common/rtc-i2c.dtsi or
declaring their own node; if a chip holds a valid time it is
restored at boot — shown tagged "L" until the next external sync.
On every GPS/app/CLI sync the time is written back so it survives
power-off. A node with rv3028-eeprom-config has its time read and
On every GPS/app/CLI sync to a time within 2000-2099 the time is
written back so it survives power-off. A node with
rv3028-eeprom-config has its time read and
written with the chip's backup switch flag cleared before and
checked after, and, when its chip is the one adopted, a read
identifies it, and a checked time read just before the store shows
+16 -5
View File
@@ -224,8 +224,9 @@ static enum rtc_verdict rtc_identify(const struct rtc_desc *d, uint8_t blk[7])
return RTC_FOUND;
}
/* The 7-byte time block for an epoch, in d's register order. */
static void rtc_time_block(const struct rtc_desc *d, uint32_t epoch, uint8_t blk[7])
/* The 7-byte time block for an epoch, in d's register order. False outside
* 2000-2099: the year is written as two BCD digits with no century. */
static bool rtc_time_block(const struct rtc_desc *d, uint32_t epoch, uint8_t blk[7])
{
int y;
unsigned m, day;
@@ -236,6 +237,9 @@ static void rtc_time_block(const struct rtc_desc *d, uint32_t epoch, uint8_t blk
unsigned sec = rem % 60;
unsigned dow = (unsigned)(((epoch / 86400) + 4) % 7); /* 1970-01-01 = Thu */
if (y < 2000 || y > 2099) {
return false;
}
blk[0] = BIN2BCD(sec);
blk[1] = BIN2BCD(min);
blk[2] = BIN2BCD(hour);
@@ -244,6 +248,7 @@ static void rtc_time_block(const struct rtc_desc *d, uint32_t epoch, uint8_t blk
blk[d->date_index == 4 ? 3 : 4] = d->week_one_hot ? (uint8_t)BIT(dow) : (uint8_t)dow;
blk[5] = BIN2BCD(m);
blk[6] = BIN2BCD((unsigned)(y % 100));
return true;
}
/* Clear the power-loss flag (for chips whose flag is a separate reg;
@@ -537,8 +542,11 @@ static void rv3028_save_retry_fn(struct k_work *work)
uint8_t blk[7];
ARG_UNUSED(work);
rtc_time_block(s_active, s_save_epoch +
(uint32_t)((k_uptime_get() - s_save_at) / 1000), blk);
if (!rtc_time_block(s_active, s_save_epoch +
(uint32_t)((k_uptime_get() - s_save_at) / 1000), blk)) {
LOG_WRN("RTC %s: time outside 2000-2099 not written", s_active->name);
return;
}
if (!rv3028_steady(s_active, blk, true)) {
if (++s_save_tries < RTC_SAVE_RETRIES) {
k_work_schedule(&s_save_retry, RTC_SAVE_RETRY);
@@ -675,7 +683,10 @@ void zephcore_rtc_save(uint32_t epoch)
const struct rtc_desc *d = s_active;
uint8_t blk[7];
rtc_time_block(d, epoch, blk);
if (!rtc_time_block(d, epoch, blk)) {
LOG_WRN("RTC %s: time outside 2000-2099 not written", d->name);
return;
}
#if RTC_RV3028_CFG
if (d->cfg != NULL) {
(void)k_work_cancel_delayable(&s_save_retry);
+3 -3
View File
@@ -10,8 +10,8 @@
*
* If a present chip holds a valid time, zephcore_rtc_restore() returns it so
* the soft clock can be seeded at boot (shown tagged "L" — local). Every
* ZephyrRTCClock::setCurrentTime() writes it back via zephcore_rtc_save(), so
* time survives the next power-off.
* ZephyrRTCClock::setCurrentTime() within 2000-2099 writes it back via
* zephcore_rtc_save(), so time survives the next power-off.
*/
#pragma once
@@ -40,7 +40,7 @@ bool zephcore_rtc_restore(uint32_t *epoch_out);
* power-loss flag. No-op if no RTC was discovered. Intended for real syncs
* (GPS/app/CLI), not per-packet clock nudges. Call only from the system work
* queue: an unconfirmed write is repeated there and shares state with this
* call.
* call. A time outside 2000-2099 is not written.
*/
void zephcore_rtc_save(uint32_t epoch);
@@ -81,10 +81,10 @@ properties:
refresh. The store is retried up to 3 times, 10 minutes apart,
identifying the chip again first; a boot whose read could not
identify it cleanly waits for those retries. The time is read at
boot and written on each sync with the chip's backup switch flag
cleared before and checked after. A write sets year 00h first and
the real year last, so a write cut by a power loss reads as "time
not yet set", and a write not confirmed is repeated every 5 s, up
to 12 times. Set it only on a board whose backup supply is known:
trickle charging suits a supercapacitor or rechargeable cell,
never a primary cell.
boot and written on each sync within 2000-2099 with the chip's
backup switch flag cleared before and checked after. A write sets
year 00h first and the real year last, so a write cut by a power
loss reads as "time not yet set", and a write not confirmed is
repeated every 5 s, up to 12 times. Set it only on a board whose
backup supply is known: trickle charging suits a supercapacitor or
rechargeable cell, never a primary cell.