From a1cd2536ed4d7a9555e7b6c041a9d18235d90480 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 6 Oct 2026 08:51:53 -0700 Subject: [PATCH 01/16] rtc: mark an RV3028 time write with year A0h, then write it in one burst The RV3028 time write set year 00h, burst-wrote 00h-05h, then wrote the real year, so a write cut short left year 2000 and the next boot took no time from it. That split the write into three accesses against the manual's one-access rule (Application Manual Rev 1.4, 4.5), relying on the Seconds write restarting the prescaler, and used a real date as the "not set" mark. The write now sets 06h to A0h, which is not BCD, then writes all seven registers 00h-06h in one burst, year last. The chip keeps each byte as it is acknowledged, so a write cut short still leaves the mark. On read at an rv3028-eeprom-config descriptor, a year of A0h-FEh reads as "time not yet set": a New Year counts a left mark on (A0h to A1h, A9h to B0h) and never into BCD. FFh stays a failed read, since a read cut short returns 1s and the year is the last byte. The BSF bracket and the 5 s retry around the write are unchanged, and so is the 2025 floor, so a power-on-reset chip reading 2000-01-01 still gives no time. The same write order and year handling are used by Zephyr and MeshCore for the RV3028; the three are kept consistent in behaviour. Link: https://github.com/zephyrproject-rtos/zephyr/pull/121389 Link: https://github.com/meshcore-dev/MeshCore/pull/3421 Link: https://github.com/liquidraver/ZephCore/pull/98 Co-Authored-By: Claude Opus 5.5 --- zephcore/adapters/clock/ZephyrRTCDiscover.c | 21 +++++++++++++++------ zephcore/adapters/clock/ZephyrRTCDiscover.h | 9 ++++++--- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index 7f0b6b56..a591480f 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -281,15 +281,15 @@ static void rtc_clear_power_flag(const struct rtc_desc *d) #define RV3028_REG_CFG_FIRST 0x35 /* EEPROM Clkout */ #define RV3028_REG_CFG_LAST 0x37 /* EEPROM Backup */ #define RV3028_BACKUP_BSM 0x0C /* 37h switchover mode; 00 = disabled */ +#define RV3028_YEAR_UNSET 0xA0 /* not BCD; a year of A0h-FEh reads "not set" */ -/* Year 00h first and the real year last: a power loss, which no retry can - * repeat, cutting the write after the first byte and before the last leaves - * year 2000, so the next boot takes no time from it. */ +/* Mark the year unset, then write all seven registers in one access (4.5), + * year last: the chip keeps each byte as it is acknowledged, so a write cut + * short leaves the mark and the next boot takes no time from it. */ static bool rv3028_write_time(const struct rtc_desc *d, const uint8_t blk[7]) { - return i2c_reg_write_byte(d->bus, d->addr, d->time_reg + 6, 0x00) == 0 && - i2c_burst_write(d->bus, d->addr, d->time_reg, blk, 6) == 0 && - i2c_reg_write_byte(d->bus, d->addr, d->time_reg + 6, blk[6]) == 0; + return i2c_reg_write_byte(d->bus, d->addr, d->time_reg + 6, RV3028_YEAR_UNSET) == 0 && + i2c_burst_write(d->bus, d->addr, d->time_reg, blk, 7) == 0; } /* Read or write the time block with BSF cleared before and checked after. A @@ -628,6 +628,15 @@ static bool rtc_probe(uint32_t *epoch_out) uint8_t sb = blk[0] & 0x7F, mb = blk[1] & 0x7F, hb = blk[2] & 0x3F; uint8_t db = blk[d->date_index] & 0x3F, ob = blk[5] & 0x1F, yb = blk[6]; +#if RTC_RV3028_CFG + /* A New Year counts a left mark on (A0h to A1h, A9h to B0h), never into + * BCD. FFh is a read cut short. */ + if (d->cfg != NULL && yb >= RV3028_YEAR_UNSET && yb != 0xFF) { + LOG_INF("%s present, time not yet set (year %02Xh)", d->name, yb); + continue; + } +#endif + /* Identity leaves the hours and year unchecked, and may have let the * other fields pass on a flag read since; a time needs them all. */ if (!rtc_fields_ok(d, blk) || !bcd_field_ok(hb, 23) || !bcd_field_ok(yb, 99)) { diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.h b/zephcore/adapters/clock/ZephyrRTCDiscover.h index c2d22aeb..02d876ca 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.h +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -40,7 +40,9 @@ extern "C" { * * At a descriptor with rv3028-eeprom-config, the time is read once more * with the chip's backup switch flag (BSF) cleared before and checked - * after, and is not taken if the read failed or BSF was set. A chip adopted + * after, and is not taken if the read failed or BSF was set. A year of + * A0h-FEh there, the mark a cut write leaves, reads as "time not yet set"; + * FFh is a failed read. A chip adopted * on a read that identified it (not on a failed second read), with BSF * clear, has that config stored at once: after the power-on refresh (up to * ~66 ms), backup switchover is turned off while the EEPROM is read, each @@ -65,8 +67,9 @@ bool zephcore_rtc_restore(uint32_t *epoch_out); * A device ruled out on its second read is not probed again for that reason. * * At a descriptor with rv3028-eeprom-config the time is written with BSF - * cleared before and checked after, year 00h first and the real year last, - * so a write cut by a power loss reads at boot as "time not yet set". A + * cleared before and checked after: year A0h first, then all seven + * registers in one burst, year last, so a write cut short reads at boot as + * "time not yet set". A * write not confirmed is repeated every 5 s on the system work queue, with * the time run on, until one is confirmed, a newer save replaces it, or 12 * repeats (13 attempts in all) have failed. Call only from the system work From 2f5c639baaa0f1b046b65ce9b4ba4317cb4f122c Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 6 Oct 2026 09:00:39 -0700 Subject: [PATCH 02/16] dts: describe the RV3028 A0h year mark in the rtc-i2c binding The binding still described the old write order (year 00h first, the real year last). Co-Authored-By: Claude Opus 5.5 --- zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml index efde7677..688beaee 100644 --- a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml +++ b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml @@ -82,9 +82,9 @@ properties: 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: + cleared before and checked after. A write sets the year to A0h, + then all seven registers in one burst, year last, so a write cut + short 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. From ed8802f558773f28716374f477e45154f98d732a Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 6 Oct 2026 09:04:25 -0700 Subject: [PATCH 03/16] rtc: say a write cut after the A0h mark leaves it, not any cut write Local review: a write cut on the A0h byte itself leaves the old year and the old, intact time, so the docs now say "cut after the mark". The read comment no longer says a counted-on mark never reaches BCD, since only A0h, A9h and FFh were rolled over. The header lines the first edit left as fragments are rewrapped, and so is the binding text. Co-Authored-By: Claude Opus 5.5 --- zephcore/adapters/clock/ZephyrRTCDiscover.c | 10 ++++----- zephcore/adapters/clock/ZephyrRTCDiscover.h | 22 +++++++++---------- .../dts/bindings/rtc/zephcore,rtc-i2c.yaml | 8 +++---- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index a591480f..21ad45d8 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -283,9 +283,9 @@ static void rtc_clear_power_flag(const struct rtc_desc *d) #define RV3028_BACKUP_BSM 0x0C /* 37h switchover mode; 00 = disabled */ #define RV3028_YEAR_UNSET 0xA0 /* not BCD; a year of A0h-FEh reads "not set" */ -/* Mark the year unset, then write all seven registers in one access (4.5), - * year last: the chip keeps each byte as it is acknowledged, so a write cut - * short leaves the mark and the next boot takes no time from it. */ +/* Mark the year A0h, then write all seven registers in one access (4.5), + * year last. The chip keeps each byte as it is acknowledged, so a write cut + * after the mark leaves it, and the next boot takes no time from it. */ static bool rv3028_write_time(const struct rtc_desc *d, const uint8_t blk[7]) { return i2c_reg_write_byte(d->bus, d->addr, d->time_reg + 6, RV3028_YEAR_UNSET) == 0 && @@ -629,8 +629,8 @@ static bool rtc_probe(uint32_t *epoch_out) uint8_t db = blk[d->date_index] & 0x3F, ob = blk[5] & 0x1F, yb = blk[6]; #if RTC_RV3028_CFG - /* A New Year counts a left mark on (A0h to A1h, A9h to B0h), never into - * BCD. FFh is a read cut short. */ + /* A New Year counts a left mark on (A0h to A1h, A9h to B0h). FFh is a + * read cut short. */ if (d->cfg != NULL && yb >= RV3028_YEAR_UNSET && yb != 0xFF) { LOG_INF("%s present, time not yet set (year %02Xh)", d->name, yb); continue; diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.h b/zephcore/adapters/clock/ZephyrRTCDiscover.h index 02d876ca..d933d0ed 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.h +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -40,9 +40,7 @@ extern "C" { * * At a descriptor with rv3028-eeprom-config, the time is read once more * with the chip's backup switch flag (BSF) cleared before and checked - * after, and is not taken if the read failed or BSF was set. A year of - * A0h-FEh there, the mark a cut write leaves, reads as "time not yet set"; - * FFh is a failed read. A chip adopted + * after, and is not taken if the read failed or BSF was set. A chip adopted * on a read that identified it (not on a failed second read), with BSF * clear, has that config stored at once: after the power-on refresh (up to * ~66 ms), backup switchover is turned off while the EEPROM is read, each @@ -54,7 +52,10 @@ extern "C" { * otherwise switchover stays as the store left it, off once the store has * disabled it, until a retry or that refresh. A failed store, or one * skipped at boot, is retried on the system work queue every 10 minutes, at - * most 3 times, each retry identifying the chip again first. + * most 3 times, each retry identifying the chip again first. At such a + * descriptor a year of A0h-FEh is the mark a cut time write leaves, as + * written or counted on by a New Year, and reads as "time not yet set"; FFh + * is a failed read. */ bool zephcore_rtc_restore(uint32_t *epoch_out); @@ -67,13 +68,12 @@ bool zephcore_rtc_restore(uint32_t *epoch_out); * A device ruled out on its second read is not probed again for that reason. * * At a descriptor with rv3028-eeprom-config the time is written with BSF - * cleared before and checked after: year A0h first, then all seven - * registers in one burst, year last, so a write cut short reads at boot as - * "time not yet set". A - * write not confirmed is repeated every 5 s on the system work queue, with - * the time run on, until one is confirmed, a newer save replaces it, or 12 - * repeats (13 attempts in all) have failed. Call only from the system work - * queue: the repeat shares state with this call. + * cleared before and checked after: the year marked A0h, then all seven + * registers in one burst, so a write cut after the mark reads at boot as + * "time not yet set". A write not confirmed is repeated every 5 s on the + * system work queue, with the time run on, until one is confirmed, a newer + * save replaces it, or 12 repeats (13 attempts in all) have failed. Call + * only from the system work queue: the repeat shares state with this call. */ void zephcore_rtc_save(uint32_t epoch); diff --git a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml index 688beaee..1fdae05e 100644 --- a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml +++ b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml @@ -84,7 +84,7 @@ properties: boot and written on each sync with the chip's backup switch flag cleared before and checked after. A write sets the year to A0h, then all seven registers in one burst, year last, so a write cut - short 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. + after the mark 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. From b9fb1246a2cac36fa6033f4f106bd9a370852dfe Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 6 Oct 2026 09:08:55 -0700 Subject: [PATCH 04/16] rtc: say the A0h-FEh range holds the mark, not that every value is one Local review: a counted-on mark skips xAh-xFh, and another tool can write any of them, so the header no longer calls the whole range the mark. The behaviour is unchanged. Co-Authored-By: Claude Opus 5.5 --- zephcore/adapters/clock/ZephyrRTCDiscover.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.h b/zephcore/adapters/clock/ZephyrRTCDiscover.h index d933d0ed..561fb916 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.h +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -53,9 +53,9 @@ extern "C" { * disabled it, until a retry or that refresh. A failed store, or one * skipped at boot, is retried on the system work queue every 10 minutes, at * most 3 times, each retry identifying the chip again first. At such a - * descriptor a year of A0h-FEh is the mark a cut time write leaves, as - * written or counted on by a New Year, and reads as "time not yet set"; FFh - * is a failed read. + * descriptor a year of A0h-FEh reads as "time not yet set": that range holds + * the A0h mark a cut time write leaves and what a New Year counts it on to. + * FFh is a failed read. */ bool zephcore_rtc_restore(uint32_t *epoch_out); From e3f48f8af627c1e07b959a49223e4e99fbed1771 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 6 Oct 2026 09:21:00 -0700 Subject: [PATCH 05/16] rtc: refuse to write a year outside 2000-2099 The year register holds two BCD digits and no century, and the save encoded y % 100. So a time before 2000 was stored as a year near the end of the century: a CLI `time 900000000` (1998), accepted by a node whose clock had no time yet, was written as year 98 and restored at the next boot as 2098, past the 2025 floor. A year from 2100 was stored as 2100-2106 minus a century, which the floor did reject. On the RV3028, BCD-encoding 2100 as a three-digit year would also equal the A0h mark, which is why Zephyr and MeshCore bound the write to 2000-2099 too. rtc_time_block() now fails outside 2000-2099, and the save writes nothing then, for every chip. At an rv3028-eeprom-config descriptor it writes the A0h mark instead and drops any pending retry, so the next boot takes no time rather than the older time the chip still holds. A retry whose run-on time crosses 2099 does the same. Other chips keep their older time. Local review: the docs now say a write cut between the mark and the year byte gives no time, rather than any cut after the mark, and that a mark gives no time rather than always logging "time not yet set", since a set power-loss flag or a switchover logs first. Co-Authored-By: Claude Opus 5.5 --- zephcore/adapters/clock/ZephyrRTCDiscover.c | 41 ++++++++++++++++--- zephcore/adapters/clock/ZephyrRTCDiscover.h | 21 ++++++---- .../dts/bindings/rtc/zephcore,rtc-i2c.yaml | 10 +++-- 3 files changed, 54 insertions(+), 18 deletions(-) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index 21ad45d8..4a4967ad 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -229,8 +229,10 @@ 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 register has two BCD digits and no century, so any + * other year would be stored as one inside that range. */ +static bool rtc_time_block(const struct rtc_desc *d, uint32_t epoch, uint8_t blk[7]) { int y; unsigned m, day; @@ -241,6 +243,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); @@ -249,6 +254,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; @@ -285,13 +291,23 @@ static void rtc_clear_power_flag(const struct rtc_desc *d) /* Mark the year A0h, then write all seven registers in one access (4.5), * year last. The chip keeps each byte as it is acknowledged, so a write cut - * after the mark leaves it, and the next boot takes no time from it. */ + * between the mark and the year byte leaves the mark, and the next boot + * takes no time from it. */ static bool rv3028_write_time(const struct rtc_desc *d, const uint8_t blk[7]) { return i2c_reg_write_byte(d->bus, d->addr, d->time_reg + 6, RV3028_YEAR_UNSET) == 0 && i2c_burst_write(d->bus, d->addr, d->time_reg, blk, 7) == 0; } +/* A time outside 2000-2099 is not written. The year is marked instead, so the + * next boot takes no time rather than the older one the chip still holds. */ +static void rv3028_refuse_time(const struct rtc_desc *d) +{ + (void)i2c_reg_write_byte(d->bus, d->addr, d->time_reg + 6, RV3028_YEAR_UNSET); + LOG_WRN("RTC %s: time outside 2000-2099 not written, year marked unset", + d->name); +} + /* Read or write the time block with BSF cleared before and checked after. A * switchover to backup disables the chip's I2C mid-transfer (4.2): a read's * tail comes back 0xFF, which inside the year byte can still be valid BCD, @@ -548,8 +564,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)) { + rv3028_refuse_time(s_active); + return; + } if (!rv3028_steady(s_active, blk, true)) { if (++s_save_tries < RTC_SAVE_RETRIES) { k_work_schedule(&s_save_retry, RTC_SAVE_RETRY); @@ -695,7 +714,17 @@ 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)) { +#if RTC_RV3028_CFG + if (d->cfg != NULL) { + (void)k_work_cancel_delayable(&s_save_retry); + rv3028_refuse_time(d); + return; + } +#endif + 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); diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.h b/zephcore/adapters/clock/ZephyrRTCDiscover.h index 561fb916..5bcce38f 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.h +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -53,9 +53,8 @@ extern "C" { * disabled it, until a retry or that refresh. A failed store, or one * skipped at boot, is retried on the system work queue every 10 minutes, at * most 3 times, each retry identifying the chip again first. At such a - * descriptor a year of A0h-FEh reads as "time not yet set": that range holds - * the A0h mark a cut time write leaves and what a New Year counts it on to. - * FFh is a failed read. + * descriptor no time is taken from a year of A0h-FEh, the range that holds + * the A0h mark and what a New Year counts it on to. FFh is a failed read. */ bool zephcore_rtc_restore(uint32_t *epoch_out); @@ -69,11 +68,17 @@ bool zephcore_rtc_restore(uint32_t *epoch_out); * * At a descriptor with rv3028-eeprom-config the time is written with BSF * cleared before and checked after: the year marked A0h, then all seven - * registers in one burst, so a write cut after the mark reads at boot as - * "time not yet set". A write not confirmed is repeated every 5 s on the - * system work queue, with the time run on, until one is confirmed, a newer - * save replaces it, or 12 repeats (13 attempts in all) have failed. Call - * only from the system work queue: the repeat shares state with this call. + * registers in one burst, so a write cut between the mark and the year byte + * gives no time at the next boot. A write not confirmed is repeated every + * 5 s on the system work queue, with the time run on, until one is + * confirmed, a newer save replaces it, or 12 repeats (13 attempts in all) + * have failed. Call only from the system work queue: the repeat shares + * state with this call. + * + * A time outside 2000-2099 is not written, since the chip's two-digit year + * would store it as one inside that range. The chip keeps its older time, + * except at an rv3028-eeprom-config descriptor, where its year is marked + * A0h so that the next boot takes no time. */ void zephcore_rtc_save(uint32_t epoch); diff --git a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml index 1fdae05e..72f17b11 100644 --- a/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml +++ b/zephcore/dts/bindings/rtc/zephcore,rtc-i2c.yaml @@ -84,7 +84,9 @@ properties: boot and written on each sync with the chip's backup switch flag cleared before and checked after. A write sets the year to A0h, then all seven registers in one burst, year last, so a write cut - after the mark 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. + between the mark and the year byte gives no time at the next boot, + and a write not confirmed is repeated every 5 s, up to 12 times. A + time outside 2000-2099 is not written; the year is marked A0h + instead. Set it only on a board whose backup supply is known: + trickle charging suits a supercapacitor or rechargeable cell, + never a primary cell. From b0abaff0772f8d9b7a5a8a6818076c79f5105b27 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 6 Oct 2026 09:28:29 -0700 Subject: [PATCH 06/16] rtc: confirm and repeat the RV3028 year mark like a time write Local review of the range check: the refusal ignored whether its A0h write landed, logged "year marked unset" either way and had already dropped any pending retry, so a failed mark left the chip's older time for the next boot to restore. The mark is now checked and, if it fails, repeated every 5 s up to the same 13 attempts, sharing the time write's repeat state; a newer save replaces it either way. The header says only a time within 2000-2099 is saved, and the 2000-2099 reason is ZephCore's two-digit year, since the DS3231 and PCF8563 have a century bit that ZephCore does not use. Co-Authored-By: Claude Opus 5.5 --- zephcore/adapters/clock/ZephyrRTCDiscover.c | 59 +++++++++++---------- zephcore/adapters/clock/ZephyrRTCDiscover.h | 17 +++--- 2 files changed, 40 insertions(+), 36 deletions(-) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index 4a4967ad..3487b137 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -230,7 +230,7 @@ static enum rtc_verdict rtc_identify(const struct rtc_desc *d, uint8_t blk[7]) } /* The 7-byte time block for an epoch, in d's register order. False outside - * 2000-2099: the year register has two BCD digits and no century, so any + * 2000-2099: the year is kept as two BCD digits with no century, so any * other year would be stored as one inside that range. */ static bool rtc_time_block(const struct rtc_desc *d, uint32_t epoch, uint8_t blk[7]) { @@ -299,13 +299,11 @@ static bool rv3028_write_time(const struct rtc_desc *d, const uint8_t blk[7]) i2c_burst_write(d->bus, d->addr, d->time_reg, blk, 7) == 0; } -/* A time outside 2000-2099 is not written. The year is marked instead, so the - * next boot takes no time rather than the older one the chip still holds. */ -static void rv3028_refuse_time(const struct rtc_desc *d) +/* For a time outside 2000-2099, which is not written: mark the year alone, + * so the next boot takes no time rather than the older one the chip holds. */ +static bool rv3028_mark_year(const struct rtc_desc *d) { - (void)i2c_reg_write_byte(d->bus, d->addr, d->time_reg + 6, RV3028_YEAR_UNSET); - LOG_WRN("RTC %s: time outside 2000-2099 not written, year marked unset", - d->name); + return i2c_reg_write_byte(d->bus, d->addr, d->time_reg + 6, RV3028_YEAR_UNSET) == 0; } /* Read or write the time block with BSF cleared before and checked after. A @@ -556,6 +554,7 @@ static void rv3028_cfg_retry_fn(struct k_work *work) static uint32_t s_save_epoch; static int64_t s_save_at; static uint8_t s_save_tries; +static bool s_save_mark; /* the repeat writes the year mark, not a time */ static void rv3028_save_retry_fn(struct k_work *work); static K_WORK_DELAYABLE_DEFINE(s_save_retry, rv3028_save_retry_fn); @@ -564,20 +563,25 @@ static void rv3028_save_retry_fn(struct k_work *work) uint8_t blk[7]; ARG_UNUSED(work); - if (!rtc_time_block(s_active, s_save_epoch + + if (!s_save_mark && + !rtc_time_block(s_active, s_save_epoch + (uint32_t)((k_uptime_get() - s_save_at) / 1000), blk)) { - rv3028_refuse_time(s_active); - return; + s_save_mark = true; /* the run-on time left 2000-2099 */ } - if (!rv3028_steady(s_active, blk, true)) { + if (s_save_mark ? !rv3028_mark_year(s_active) : !rv3028_steady(s_active, blk, true)) { if (++s_save_tries < RTC_SAVE_RETRIES) { k_work_schedule(&s_save_retry, RTC_SAVE_RETRY); } else { - LOG_WRN("RTC %s: time write not confirmed after %u tries", - s_active->name, s_save_tries + 1U); + LOG_WRN("RTC %s: %s not confirmed after %u tries", s_active->name, + s_save_mark ? "year mark" : "time write", s_save_tries + 1U); } return; } + if (s_save_mark) { + LOG_WRN("RTC %s: time outside 2000-2099 not written, year marked unset", + s_active->name); + return; + } rtc_clear_power_flag(s_active); LOG_INF("RTC %s: time write confirmed on retry", s_active->name); } @@ -714,32 +718,31 @@ void zephcore_rtc_save(uint32_t epoch) const struct rtc_desc *d = s_active; uint8_t blk[7]; - if (!rtc_time_block(d, epoch, blk)) { -#if RTC_RV3028_CFG - if (d->cfg != NULL) { - (void)k_work_cancel_delayable(&s_save_retry); - rv3028_refuse_time(d); - return; - } -#endif - LOG_WRN("RTC %s: time outside 2000-2099 not written", d->name); - return; - } + bool in_range = rtc_time_block(d, epoch, blk); #if RTC_RV3028_CFG if (d->cfg != NULL) { (void)k_work_cancel_delayable(&s_save_retry); s_save_epoch = epoch; s_save_at = k_uptime_get(); s_save_tries = 0; - if (!rv3028_steady(d, blk, true)) { - LOG_WRN("RTC %s: time write not confirmed, repeating every 5 s", - d->name); + s_save_mark = !in_range; + if (s_save_mark ? !rv3028_mark_year(d) : !rv3028_steady(d, blk, true)) { + LOG_WRN("RTC %s: %s not confirmed, repeating every 5 s", d->name, + s_save_mark ? "year mark" : "time write"); k_work_schedule(&s_save_retry, RTC_SAVE_RETRY); return; } + if (s_save_mark) { + LOG_WRN("RTC %s: time outside 2000-2099 not written, year marked " + "unset", d->name); + return; + } } else #endif - if (i2c_burst_write(d->bus, d->addr, d->time_reg, blk, sizeof(blk)) != 0) { + if (!in_range) { + LOG_WRN("RTC %s: time outside 2000-2099 not written", d->name); + return; + } else if (i2c_burst_write(d->bus, d->addr, d->time_reg, blk, sizeof(blk)) != 0) { LOG_WRN("RTC %s: time write failed", d->name); return; } diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.h b/zephcore/adapters/clock/ZephyrRTCDiscover.h index 5bcce38f..ca3b5267 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.h +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -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 @@ -59,8 +59,8 @@ extern "C" { bool zephcore_rtc_restore(uint32_t *epoch_out); /* - * Persist an authoritative epoch to the discovered RTC chip and clear its - * power-loss flag. No-op if no RTC was discovered. Safe to call often, but + * Persist an authoritative epoch within 2000-2099 to the discovered RTC chip + * and clear its power-loss flag. No-op if no RTC was discovered. Safe to call often, but * intended only for real syncs (GPS/app/CLI), not per-packet clock nudges. * If restore adopted nothing but skipped a candidate whose first read was all * 0xFF, the first save probes again, once per boot, in the caller's context. @@ -75,10 +75,11 @@ bool zephcore_rtc_restore(uint32_t *epoch_out); * have failed. Call only from the system work queue: the repeat shares * state with this call. * - * A time outside 2000-2099 is not written, since the chip's two-digit year - * would store it as one inside that range. The chip keeps its older time, - * except at an rv3028-eeprom-config descriptor, where its year is marked - * A0h so that the next boot takes no time. + * A time outside 2000-2099 is not written, since the year is kept as two + * BCD digits with no century, and the chip keeps its older time. At an + * rv3028-eeprom-config descriptor the year is marked A0h instead, so the + * next boot takes no time; the mark is confirmed and repeated like a time + * write, and a repeat whose run-on time leaves 2000-2099 marks instead. */ void zephcore_rtc_save(uint32_t epoch); From fdf2332477b7d4be16b9d0eb5d132dd140d5f3cd Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 6 Oct 2026 09:40:54 -0700 Subject: [PATCH 07/16] rtc: bracket the RV3028 year mark with BSF like a time write Local review: the mark was confirmed by its acknowledgement alone, without the BSF clear-before / check-after a time write gets, so a switchover during it (5.10) could leave the byte unstored while the log said "year marked unset". It now takes the same bracket, and a failure is repeated. A repeat that switches from writing the time to marking restarts the try count, so the final warning counts mark attempts only. Co-Authored-By: Claude Opus 5.5 --- zephcore/adapters/clock/ZephyrRTCDiscover.c | 12 ++++++++++-- zephcore/adapters/clock/ZephyrRTCDiscover.h | 5 +++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index 3487b137..cb5c93a7 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -300,10 +300,17 @@ static bool rv3028_write_time(const struct rtc_desc *d, const uint8_t blk[7]) } /* For a time outside 2000-2099, which is not written: mark the year alone, - * so the next boot takes no time rather than the older one the chip holds. */ + * so the next boot takes no time rather than the older one the chip holds. + * BSF is cleared before and checked after, as for a time write. */ static bool rv3028_mark_year(const struct rtc_desc *d) { - return i2c_reg_write_byte(d->bus, d->addr, d->time_reg + 6, RV3028_YEAR_UNSET) == 0; + uint8_t st; + + return i2c_reg_update_byte(d->bus, d->addr, RV3028_REG_STATUS, + RV3028_STATUS_BSF, 0) == 0 && + i2c_reg_write_byte(d->bus, d->addr, d->time_reg + 6, RV3028_YEAR_UNSET) == 0 && + i2c_reg_read_byte(d->bus, d->addr, RV3028_REG_STATUS, &st) == 0 && + !(st & RV3028_STATUS_BSF); } /* Read or write the time block with BSF cleared before and checked after. A @@ -567,6 +574,7 @@ static void rv3028_save_retry_fn(struct k_work *work) !rtc_time_block(s_active, s_save_epoch + (uint32_t)((k_uptime_get() - s_save_at) / 1000), blk)) { s_save_mark = true; /* the run-on time left 2000-2099 */ + s_save_tries = 0; } if (s_save_mark ? !rv3028_mark_year(s_active) : !rv3028_steady(s_active, blk, true)) { if (++s_save_tries < RTC_SAVE_RETRIES) { diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.h b/zephcore/adapters/clock/ZephyrRTCDiscover.h index ca3b5267..c64495e0 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.h +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -60,8 +60,9 @@ bool zephcore_rtc_restore(uint32_t *epoch_out); /* * Persist an authoritative epoch within 2000-2099 to the discovered RTC chip - * and clear its power-loss flag. No-op if no RTC was discovered. Safe to call often, but - * intended only for real syncs (GPS/app/CLI), not per-packet clock nudges. + * and clear its power-loss flag. No-op if no RTC was discovered. Safe to + * call often, but intended only for real syncs (GPS/app/CLI), not + * per-packet clock nudges. * If restore adopted nothing but skipped a candidate whose first read was all * 0xFF, the first save probes again, once per boot, in the caller's context. * A device ruled out on its second read is not probed again for that reason. From b4aebca58c3795b8db09f1bd727fdf3b928ddc47 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 6 Oct 2026 09:51:04 -0700 Subject: [PATCH 08/16] rtc: keep one 13-attempt cap when a repeat switches to marking Local review of fdf2332: restarting the try count when a repeat switched from writing the time to marking let one save repeat up to 23 times, against the documented 13 attempts, and its final log overstated the mark attempts by one. The count now runs across both, and the final log says "save not confirmed" rather than naming one kind. Co-Authored-By: Claude Opus 5.5 --- zephcore/adapters/clock/ZephyrRTCDiscover.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index cb5c93a7..0943f058 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -574,14 +574,13 @@ static void rv3028_save_retry_fn(struct k_work *work) !rtc_time_block(s_active, s_save_epoch + (uint32_t)((k_uptime_get() - s_save_at) / 1000), blk)) { s_save_mark = true; /* the run-on time left 2000-2099 */ - s_save_tries = 0; } if (s_save_mark ? !rv3028_mark_year(s_active) : !rv3028_steady(s_active, blk, true)) { if (++s_save_tries < RTC_SAVE_RETRIES) { k_work_schedule(&s_save_retry, RTC_SAVE_RETRY); } else { - LOG_WRN("RTC %s: %s not confirmed after %u tries", s_active->name, - s_save_mark ? "year mark" : "time write", s_save_tries + 1U); + LOG_WRN("RTC %s: save not confirmed after %u tries", + s_active->name, s_save_tries + 1U); } return; } From e53d3a04ed9aea2fcc43f547af5b39787822802a Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 6 Oct 2026 09:57:57 -0700 Subject: [PATCH 09/16] rtc: say the 2000-2099 limit is ZephCore's year encoding Local review: the DS3231 and PCF8563 have a century bit, which ZephCore writes as 0 and masks off on read, so the limit is the encoding rather than every chip. Co-Authored-By: Claude Opus 5.5 --- zephcore/adapters/clock/ZephyrRTCDiscover.c | 4 ++-- zephcore/adapters/clock/ZephyrRTCDiscover.h | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index 0943f058..96926424 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -230,8 +230,8 @@ static enum rtc_verdict rtc_identify(const struct rtc_desc *d, uint8_t blk[7]) } /* The 7-byte time block for an epoch, in d's register order. False outside - * 2000-2099: the year is kept as two BCD digits with no century, so any - * other year would be stored as one inside that range. */ + * 2000-2099: ZephCore writes the year as two BCD digits and no century bit, + * so any other year would be stored as one inside that range. */ static bool rtc_time_block(const struct rtc_desc *d, uint32_t epoch, uint8_t blk[7]) { int y; diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.h b/zephcore/adapters/clock/ZephyrRTCDiscover.h index c64495e0..353e6a4e 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.h +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -76,11 +76,11 @@ bool zephcore_rtc_restore(uint32_t *epoch_out); * have failed. Call only from the system work queue: the repeat shares * state with this call. * - * A time outside 2000-2099 is not written, since the year is kept as two - * BCD digits with no century, and the chip keeps its older time. At an - * rv3028-eeprom-config descriptor the year is marked A0h instead, so the - * next boot takes no time; the mark is confirmed and repeated like a time - * write, and a repeat whose run-on time leaves 2000-2099 marks instead. + * A time outside 2000-2099 is not written, since ZephCore writes the year + * as two BCD digits and no century bit, and the chip keeps its older time. + * At an rv3028-eeprom-config descriptor the year is marked A0h instead, so + * the next boot takes no time; the mark is confirmed and repeated like a + * time write, and a repeat whose run-on time leaves 2000-2099 marks instead. */ void zephcore_rtc_save(uint32_t epoch); From 42eea4f9068db3a39ab9b5061cd4038973afc40e Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Tue, 6 Oct 2026 20:17:53 +0200 Subject: [PATCH 10/16] usb fix --- zephcore/adapters/usb/ZephyrCompanionUSB.cpp | 61 ++++++++++++++++---- zephcore/adapters/usb/ZephyrCompanionUSB.h | 8 ++- zephcore/adapters/usb/ZephyrUSBCDC.cpp | 50 +++++++++++++--- zephcore/adapters/usb/ZephyrUSBCDC.h | 16 ++++- 4 files changed, 113 insertions(+), 22 deletions(-) diff --git a/zephcore/adapters/usb/ZephyrCompanionUSB.cpp b/zephcore/adapters/usb/ZephyrCompanionUSB.cpp index c0885ea1..9b8eaf0d 100644 --- a/zephcore/adapters/usb/ZephyrCompanionUSB.cpp +++ b/zephcore/adapters/usb/ZephyrCompanionUSB.cpp @@ -100,9 +100,16 @@ static const struct companion_link_cbs *s_link; K_MSGQ_DEFINE(usb_recv_queue, sizeof(struct frame), USB_RECV_QUEUE_DEPTH, 4); /* A session starts on the first traffic after open (a binary frame or a CLI - * line) and ends when the host drops DTR. */ + * line) and ends when the host drops DTR or goes away without one (VBUS loss, + * bus reset). */ static bool usb_session_active; +/* The session can carry frames: it is open and the bus is not suspended (host + * asleep with the port open). Main hears each edge as a connect or a + * disconnect, so a suspended session is not a client: nothing is queued for + * it, and it does not hold up the last-client cleanup. */ +static bool usb_link_up; + /* CLI text line callback — fired when a complete line arrives in text mode. */ static void (*s_cli_line_cb)(const char *line); static char usb_text_line[USB_TEXT_LINE_MAX]; @@ -189,6 +196,31 @@ static void usb_uart_isr(const struct device *dev, void *user_data) } } +/* Recompute usb_link_up and tell main about an edge. Arduino shows "connected" + * for serial transports too, so a text session raises the edges as well. */ +static void usb_link_update(void) +{ + bool up = usb_session_active; + +#if COMPANION_HAS_DTR + up = up && !zephcore_usbd_is_suspended(); +#endif + if (up == usb_link_up) { + return; + } + usb_link_up = up; + if (!s_link) { + return; + } + if (up) { + if (s_link->on_connected) { + s_link->on_connected(); + } + } else if (s_link->on_disconnected) { + s_link->on_disconnected(); + } +} + /* Start a session on its first inbound traffic — a binary frame or a complete * CLI line. The official client opens with CMD_DEVICE_QUERY (0x16), not * CMD_APP_START, so any first traffic starts it. Other transports (BLE, WiFi) @@ -202,10 +234,7 @@ static void usb_session_begin(uint8_t log_tag, bool is_text) usb_session_is_text = is_text; LOG_INF("usb_rx: first traffic 0x%02x, session started (%s)", log_tag, is_text ? "text" : "binary"); - /* Arduino shows "connected" for serial transports too */ - if (s_link && s_link->on_connected) { - s_link->on_connected(); - } + usb_link_update(); } /* USB RX work - parses V3 frames from ring buffer */ @@ -347,7 +376,8 @@ static void usb_rx_work_fn(struct k_work *work) #if COMPANION_HAS_DTR /* DTR-transition callback from the shared ZephyrUSBCDC module. - * On drop: host closed the port → reset parser, hand control back to BLE. + * On drop (the host closed the port, or went away: VBUS loss, bus reset): + * reset the parser and end the session. * CDC-only — a plain-UART backend has no DTR and never registers this. */ static void on_dtr_change(bool dtr_active) { @@ -355,7 +385,6 @@ static void on_dtr_change(bool dtr_active) return; } LOG_INF("usb_dtr: DTR dropped, USB disconnected"); - bool was_active = usb_session_active; usb_session_active = false; ring_buf_reset(&usb_ring_buf); @@ -375,8 +404,19 @@ static void on_dtr_change(bool dtr_active) /* The session is over: main runs its per-session cleanup (contact dump, * sync, sign buffer) once no other transport is still connected. */ - if (was_active && s_link && s_link->on_disconnected) { - s_link->on_disconnected(); + usb_link_update(); +} + +/* Bus suspend, resume or reset, from the shared ZephyrUSBCDC module. */ +static void on_bus_change(void) +{ + bool was_up = usb_link_up; + + usb_link_update(); + if (usb_link_up && !was_up) { + /* Resumed. The class does not restart TX by itself: frames queued + * before the suspend wait for this kick. */ + uart_irq_tx_enable(usb_dev); } } #endif /* COMPANION_HAS_DTR */ @@ -455,7 +495,7 @@ size_t zephcore_usb_companion_recv(uint8_t *dest) bool zephcore_usb_companion_is_connected(void) { - return usb_session_active && !usb_session_is_text; + return usb_link_up && !usb_session_is_text; } /* Only a connected session is ever busy: MultiSerialInterface asks every @@ -516,6 +556,7 @@ void zephcore_usb_companion_init(const struct companion_link_cbs *link) * shared usbd_msg_callback — no polling work needed. Plain-UART * backends have no DTR; their session reset is protocol-driven. */ zephcore_usbd_set_dtr_cb(on_dtr_change); + zephcore_usbd_set_bus_cb(on_bus_change); #endif } else { LOG_WRN("Companion UART not ready"); diff --git a/zephcore/adapters/usb/ZephyrCompanionUSB.h b/zephcore/adapters/usb/ZephyrCompanionUSB.h index a71f4027..8cd25629 100644 --- a/zephcore/adapters/usb/ZephyrCompanionUSB.h +++ b/zephcore/adapters/usb/ZephyrCompanionUSB.h @@ -21,8 +21,9 @@ extern "C" { /** * Initialize the transport: ring buffers, UART ISR, DTR tracking. - * @param link frame waiting / TX drained / session start / session end - * (DTR drop). Raised from the ISR and sysworkq: post events only. + * @param link frame waiting / TX drained / session start (also a bus resume) + * / session end (DTR drop, VBUS loss, bus reset; also a bus + * suspend). Raised from the ISR and sysworkq: post events only. */ void zephcore_usb_companion_init(const struct companion_link_cbs *link); @@ -36,7 +37,8 @@ size_t zephcore_usb_companion_write_frame(const uint8_t *src, size_t len); /** Take the next received binary frame into dest (MAX_FRAME_SIZE). Returns its length, 0 if none. */ size_t zephcore_usb_companion_recv(uint8_t *dest); -/** A binary companion session is open (a text-CLI session is not a client). */ +/** A binary companion session is open and the bus is not suspended (a + * text-CLI session is not a client). */ bool zephcore_usb_companion_is_connected(void); /** The TX ring cannot take another full-size frame. */ diff --git a/zephcore/adapters/usb/ZephyrUSBCDC.cpp b/zephcore/adapters/usb/ZephyrUSBCDC.cpp index b5eafa22..ef3a3494 100644 --- a/zephcore/adapters/usb/ZephyrUSBCDC.cpp +++ b/zephcore/adapters/usb/ZephyrUSBCDC.cpp @@ -8,8 +8,9 @@ * SET_LINE_CODING requests reach us as events (no DTR polling) * - Emits a k_event when DTR transitions high — the boot path waits on it * instead of a fixed sleep - * - Fires a user callback on every DTR transition (companion: reset RX + - * flip active_iface on drop) + * - Fires a user callback on every DTR transition (companion: end the + * session on drop), and reports VBUS loss and a bus reset as a drop + * - Fires a user callback on bus suspend, resume and reset * - Handles the Arduino-style 1200-baud touch → reboot-to-bootloader flow * for nRF52 boards */ @@ -71,6 +72,7 @@ static K_EVENT_DEFINE(usb_cdc_events); static bool s_dtr_active; static bool s_initialized; static zephcore_usbd_cdc_dtr_cb_t s_dtr_cb; +static zephcore_usbd_cdc_bus_cb_t s_bus_cb; /* ZephyrBoard for bootloader-magic write before reset. The class is stateless * (only static methods over GPREGRET + sys_reboot), so a local instance here @@ -86,6 +88,17 @@ static void enter_bootloader(void) CODE_UNREACHABLE; } +/* The host went away without a DTR=0: report it as a DTR drop. */ +static void host_gone(void) +{ + if (s_dtr_active) { + s_dtr_active = false; + if (s_dtr_cb) { + s_dtr_cb(false); + } + } +} + /* ===== USBD message callback ===== */ static void usbd_msg_callback(struct usbd_context *const ctx, const struct usbd_msg *msg) @@ -142,11 +155,24 @@ static void usbd_msg_callback(struct usbd_context *const ctx, * connection until reboot). Treat VBUS loss as a DTR drop so the * interface is handed back to BLE. */ LOG_INF("CDC ACM: VBUS removed (device unplug)"); - if (s_dtr_active) { - s_dtr_active = false; - if (s_dtr_cb) { - s_dtr_cb(false); - } + host_gone(); + } else if (msg->type == USBD_MSG_RESET) { + /* Bus reset. At enumeration no port is open and this does nothing. + * Later it means the host is enumerating us again (a hub upstream + * was reset, or the host lost the device): the port it had open is + * gone, and no DTR=0 says so, since the class keeps the line state + * of the old session. VBUS can stay up throughout, so treat it as + * a DTR drop too. A reset also ends a suspend, with no RESUME. */ + LOG_INF("CDC ACM: bus reset"); + host_gone(); + if (s_bus_cb) { + s_bus_cb(); + } + } else if (msg->type == USBD_MSG_SUSPEND || msg->type == USBD_MSG_RESUME) { + LOG_INF("CDC ACM: bus %s", + msg->type == USBD_MSG_SUSPEND ? "suspended" : "resumed"); + if (s_bus_cb) { + s_bus_cb(); } } } @@ -268,6 +294,16 @@ extern "C" void zephcore_usbd_set_dtr_cb(zephcore_usbd_cdc_dtr_cb_t cb) s_dtr_cb = cb; } +extern "C" bool zephcore_usbd_is_suspended(void) +{ + return s_initialized && usbd_is_suspended(&zephcore_usbd); +} + +extern "C" void zephcore_usbd_set_bus_cb(zephcore_usbd_cdc_bus_cb_t cb) +{ + s_bus_cb = cb; +} + extern "C" void zephcore_usbd_detach(void) { if (!s_initialized) { diff --git a/zephcore/adapters/usb/ZephyrUSBCDC.h b/zephcore/adapters/usb/ZephyrUSBCDC.h index dc7e7321..12a23390 100644 --- a/zephcore/adapters/usb/ZephyrUSBCDC.h +++ b/zephcore/adapters/usb/ZephyrUSBCDC.h @@ -20,9 +20,14 @@ extern "C" { #endif /* Per-transition DTR callback. Fires on every host CONTROL_LINE_STATE change, - * value = current DTR level. NULL is allowed (cb cleared). */ + * value = current DTR level, and with false when the host goes away without + * one (VBUS loss, bus reset). NULL is allowed (cb cleared). */ typedef void (*zephcore_usbd_cdc_dtr_cb_t)(bool dtr_active); +/* Bus state callback: the bus was suspended, resumed or reset. Read the state + * with zephcore_usbd_is_suspended(). NULL is allowed (cb cleared). */ +typedef void (*zephcore_usbd_cdc_bus_cb_t)(void); + /* Initialize USB device + CDC ACM class and enumerate. * Idempotent; subsequent calls are no-ops. * Returns 0 on success or a negative errno. */ @@ -37,9 +42,16 @@ int zephcore_usbd_wait_dtr(uint32_t timeout_ms); bool zephcore_usbd_is_dtr_active(void); /* Register a callback fired on every DTR transition. - * Companion uses this to reset its RX state and flip active_iface on drop. */ + * Companion uses this to end its session on a drop. */ void zephcore_usbd_set_dtr_cb(zephcore_usbd_cdc_dtr_cb_t cb); +/* The host has suspended the bus (asleep with the port open): nothing we send + * leaves the device until it resumes. The USB stack's own state. */ +bool zephcore_usbd_is_suspended(void); + +/* Register a callback fired on a bus suspend, resume or reset. */ +void zephcore_usbd_set_bus_cb(zephcore_usbd_cdc_bus_cb_t cb); + /* Detach from the USB bus (drop the D+ pull-up) so the host sees an unplug. * Call before a reset: a soft reset leaves the PHY attached on some SoCs, and * the host then keeps a stale endpoint open across the reboot. No-op if the From 8297d88bf7f84708152392107d83fbf439f9b701 Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Tue, 6 Oct 2026 21:23:55 +0200 Subject: [PATCH 11/16] formatter update, bump and contact iterator fix --- docs/Repeater_CLI_commands.md | 3 + formatter/README.md | 44 ++++- formatter/SoftDevice_v6_formatter.uf2 | Bin 100864 -> 95744 bytes formatter/SoftDevice_v6_formatter.zip | Bin 51158 -> 48664 bytes formatter/SoftDevice_v7_formatter.uf2 | Bin 97792 -> 98304 bytes formatter/SoftDevice_v7_formatter.zip | Bin 49680 -> 49723 bytes releasenotes/RELEASE_NOTES_1.17.7-zephcore.md | 78 ++++++--- zephcore/CMakeLists.txt | 2 +- zephcore/src/main_companion.cpp | 9 +- zephcore/tools/formatter/src/main.c | 14 +- zephcore/tools/formatter/src/qspi_probe.c | 161 +++++++++++++++--- zephcore/tools/formatter/src/qspi_probe.h | 3 +- 12 files changed, 257 insertions(+), 57 deletions(-) create mode 100644 docs/Repeater_CLI_commands.md diff --git a/docs/Repeater_CLI_commands.md b/docs/Repeater_CLI_commands.md new file mode 100644 index 00000000..908129e8 --- /dev/null +++ b/docs/Repeater_CLI_commands.md @@ -0,0 +1,3 @@ +# Repeater CLI Commands + +This page has moved: the CLI reference for every role is now [CLI_commands.md](CLI_commands.md). diff --git a/formatter/README.md b/formatter/README.md index 57750da3..3b54e580 100644 --- a/formatter/README.md +++ b/formatter/README.md @@ -1,6 +1,7 @@ nRF formatter tools -- QSPI is formatted for all supported boards +- Erases the internal LittleFS and BLE-bond NVS partitions, and the external QSPI flash on + every supported board that has one (see [QSPI flash](#qspi-flash) below) - Watch out for softdevice version! Flashing the wrong version can corrupt the node and you'll need a full bootloader reflash with adafruit-nrfutil! - You can check what softdevice version you use if you open INFO_UF2.TXT on the storage drive when in DFU mode. Bootloader should say "sxxx 6.x.x" for v6 and "sxxx 7.x.x" for v7 - That warning applies to the **`.uf2`** files, which have no version guard. The **`.zip`** packages carry an `--sd-req` guard (v6 = `0x00B6`, v7 = `0x0123`), so the bootloader *rejects* a mismatched package instead of corrupting the node. @@ -11,8 +12,8 @@ nRF formatter tools | SoftDevice | Boards | |------------|--------| -| **v6** | RAK4631, RAK3401 1W, ThinkNode M1/M3/M6, RAK WisMesh Tag, LilyGo T-Echo, LilyGo T-Impulse Plus, ProMicro SX1262, Heltec T114, Heltec T096, GAT562 30s, muzi works R1 Neo | -| **v7** | Wio Tracker L1, T1000-E, Ikoka Nano 30dBm, SenseCAP Solar, XIAO nRF52840 | +| **v6** | RAK4631, RAK3401 1W, ThinkNode M1/M3/M6, RAK WisMesh Tag, LilyGo T-Echo, LilyGo T-Impulse Plus, ProMicro SX1262, ProMicro LR2021, Heltec T114, Heltec T096, Heltec Mesh Node T1, GAT562 30s, muzi works R1 Neo | +| **v7** | Wio Tracker L1, Wio Tracker L1 Pro 1W, T1000-E, MeshTracker X1, Ikoka Nano 30dBm, SenseCAP Solar, XIAO nRF52840 | - **`.uf2`** — manual drag-and-drop onto the UF2 mass-storage drive. - **`.zip`** — Adafruit DFU package. Used by the Mesh America configurator as ZephCore's @@ -46,5 +47,40 @@ prints `Formatter DFU zip: ENABLED (sd-req=...)` when wired up, and silently ski zip otherwise. `--sd-req` is read automatically from the build board's `board.conf` (`CONFIG_ZEPHCORE_SD_FWID`), so it always matches the SoftDevice being targeted. +## QSPI flash + +The formatter does not know which board it is running on. It tries each known QSPI pin +set in turn, and the first one where a flash answers its JEDEC ID gets a full chip erase. + +| Pin set | Boards | Flash rail pin | +|---------|--------|----------------| +| Wio/XIAO/Ikoka/SenseCap | Wio Tracker L1, Wio Tracker L1 Pro 1W, XIAO nRF52840, Ikoka Nano 30dBm, SenseCAP Solar | — | +| ThinkNode M1 / TEcho | ThinkNode M1, LilyGo T-Echo | P0.12 | +| ThinkNode M6 | ThinkNode M6 (units with the flash fitted) | P0.21 | +| RAK4631 / GAT562 | RAK base boards with QSPI flash | — | +| LilyGo TEcho Lite | LilyGo T-Echo Lite | — | +| Nano G2 Ultra | Nano G2 Ultra | — | +| LilyGo T-Impulse Plus | LilyGo T-Impulse Plus (v6 build only) | P0.14 | +| MeshTracker X1 | MeshTracker X1 (v7 build only) | P0.15 | + +- A flash left in deep power-down is woken first. Repeaters, room servers and observers + put it to sleep, and the reset into the formatter does not wake it. +- Where the flash sits behind a switched supply rail, the formatter raises that pin and + gives the part time to start; nothing else powers it after the bootloader. +- The erase is checked: the log shows the first bytes of the flash before the erase, and + the erase is reported as failed unless they read back blank afterwards. +- The last lines of the log say what happened: `All partitions erased successfully + (internal flash + QSPI)`, or that the internal flash was erased and no QSPI flash + answered. The second is normal on a board without one (T1000-E, RAK4631 on a base + board without flash, ...). On a board from the table above it means the QSPI was + **not** erased. + +Verified on hardware: Wio Tracker L1 (as companion and as repeater) and T1000-E. The +MeshTracker X1, T-Impulse Plus and ThinkNode M1 / T-Echo rail entries follow the board +devicetrees and have not been run on those boards. + To support a new board, add its QSPI pin mapping to `known_boards[]` in -`zephcore/tools/formatter/src/qspi_probe.c` — no DTS or Kconfig changes needed. +`zephcore/tools/formatter/src/qspi_probe.c` — no DTS or Kconfig changes needed. Set +`pwr_pin` if a GPIO switches the flash's supply, and `sd` if the board exists on one +SoftDevice only: every probed pin set drives its pins on every board that build runs on, +so a set limited to its own build stays off unrelated boards. diff --git a/formatter/SoftDevice_v6_formatter.uf2 b/formatter/SoftDevice_v6_formatter.uf2 index 010c40cc89142e60e42af6938423e882f7677a2a..d144603eda98c77eaf7188d1082a0295e6903ca5 100644 GIT binary patch delta 35550 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 delta 38776 zcmb@vd3=*q7B_yMB}>ySr3FG;;7L=Uw19zvRzV4A`*ca62s+|6hDA*QrHZ4dNCHI# z6sLX`@&D7Y_^03BIesxSuH8GT9%d73WI`@Koi*7?1Ey`Nt{pPOg9 z%em*CbMCq4oO|o}kH)iETWxZGObD@Mz40~~O@xmtS#sbkOT4%aOvv)PzwX}e^kw(M zk-UFe&XWI!Q+`7^_GFIO}veFyMWpyHwmB?|2 z-fq>$J#dNthRB1I?-jo3`%A2a-(Oi&SwtEnflYCbza$quFTNn^|2kOY#bF}JvB^Hq z!-Yj>YC>y*<=#(Q1uh|^>k>j-TfHs6IWF#qt-N{gtlc$7I(rmLTtcVmD}5*Fz?b~3 zg;{4>O!`)J5`SWAA$w+bE6e5->}rYOUavXO+0&KKnVuWhN#ufNu3)D<<;=KYao4x1 zx%`QpcJ|EPR+bxAkeQm_%-TkHyKKZV*}2?Ve^GwcE`A?aD7FSj_TxSP)kbor`-X`W z2j2k)Nm4u?+eaE70$eu4T$vE!E+>ST9tw7Ta{@;}CLyN124L)~0U$zZTwJG4u{RTY zb>SUn-ZI6ts#g9)HNXZWB)j0X7CpPYhUQ1^4CD<0vi!f%#IXBLtA2pHxpZ;m?W+Uj5MzcIK@4I1!6-5JOeLo{ER62S1s9kykJ;;szO7eaBZ z6GCxqAcvkA%?S*;Swcd(_5+Z^S2eUA)WiOEkY6KFYIC4rey_>Gt^vM06v zvO@kuT??DwYSC~^9+~jE?_f&;JHLfvm{43-Mo8CI*35AoHUoMw@y`L9_;H{YFS_(T zTFDpB1gy1B?~fO`H;L@({IHCl;T)#8|c($VwUH=choi zcoF@SF7R1Wfp*#e%8QHDN2`-O+EjO2(WAwW&H7RK?j@~RC*P`&G5f`du15e{xl;3l znmxkzuw#%cB*sB_?X;OhRvyXBiJ9eNvTgDg9LW(kMc9~<3p*&-G&URGM zPhkN{M$f%vWYlqm+^Poi)!fIAE16T4<2P%{0sLC0wB3)(1L#zYH+f;yKXK(W0n0+s9B6p4gN`GuG3zlZH?2u?w?t#m9MB& zzCO|fVDt9}@-P1_A2t8sxYCP(_e#ooKBWdz{|<%b)(w z+wXf!?z-{Cp9RJje-bEf4^?XA?cKzJqO_wE^Dk>~KlxhCmC&Em;HNzoy#VOibGo!s ziDS-4JJ)s|j_TjL8ju^ZqlHK z-k?W~zqKV*36`q1LF*dYmc7W!Ng)9le^FTsO-GMT?o_%0`v4W4Pc_f+VwP^G7}v;* zjcX{B2QbE<*jZ7=Flq{AK{KlA*|wx`ofMnwSdFaH*yPO~0YDN}leOqHvumy*$fO~r zf+aPhB7I}b7=X|rv8E{4 z&)8WXlba%54m7!9 zi6O$?BqHdF^`-g}eJy>zNJZs%s=uhW(dWAwNW)Q{2sG~_BRO&i_XE&i8o*olvJ^S; zK-O{MR<9q^S7LFD3w3m#y2M+4a>avY|IqzYMI5ekx?in`#dTIUGhXBWtxdC9Uu?Wy zD~sJ`F-xq!m}leVdG)P;+MLRfn~~-tRU=s&)L=KD0>#gWo%N%1ASKCdke7Gt9A+1x zYD_c8Pj>9RU)QQ9pzPJ80gOZ_H|@l5jK8GhPnziwCl#*;<7wrX?mdft&}KAV14y{; zq|7~whwHe;@p$U$8kCtebI;-}y1|Xvcy8}{aLJyn+Fi!HK;+p{1!lWQ_&{t#Fu57wrl!n0^#1S&k;m4fEBYq`cWJo&o@ zXAW{~TimE+Pi&NVE0F*T|*|gwZ{YGzohAP;NURQZ%AAkN79jU zk>(;9YloK&M|CfE?OME*v4*RDv1^2|Yw=lzYaE6z>$*k?DP_ABr*T~45IlRj%=mgc zo|5tOcvprHhp*f4Y{c_JT^4*jho^*MUMnx|8Zh+f1LoRFQ;ORMmZ9)f0gfF=`;opx zYDO}qb0i0;vNp3!D`pnQ1A|3?@eDtu(TsLxbPYv2sT!_vB%W^QG6|`GV!I}>F&)p7 zx?WwfYq5#fg?o_GHNv}Vv8A2l?^-ZSFe2aWD3|g8GLGm+aAQDBY2iI*#`;opvqKKu$cDLgm z*M|MP_-UJ7I{*(O`dNUCdb(Grva&n>K&!i4>u2RSPM?$PzXk1`=_I4ZiBuA{jo7H@ zJTW=s^7Eb_FLJWMvv%bM`nS%gD{F`onNcl)l45a|R&MB|IolR*)A5aksN=m(V##i5 zTTF78v-aYvfwskW#*izGGw|uHPU+h83C1!0b2V&Err+$EgqP2CN-NScgc-h%+VqUM zZdvo3jAhOCjF?UAnUs=xK3(10p^@n~MoBmoux zsNG$z@!>7QKyTEmnx^fG4_xs~-+EFF)=Iyn4=zhAULQzwB$gP9hqf#3zqcA$eZw$} zcwVPFu&hnbei%l1GBm^KNVg$9jSW4o)$9!o`ORlZm2xWi?aJ#+4G-my5No!IYKppn0MKbkkCWP_`>r=!yIqUb7@ zt;ARWbUp96D325bPp{IEP!5W{ue4%=^CoBYvbZwA>03rDC-0ZqAuMQ>JesHQN=z-LFC;-tDkd&~PUr{L!Ig5wj2V*b5Fxw(ktq}mmR4n&COmlPd1N3YcCfzmBV`MT zby?(*9G9Oj8@Z2^9oFa2RR(uEtrYtMh9_$P+o63hX#S6i)ARKH1aTxR3RE%i7Fy0a zU8lX%y)#DW?P>mzS;Nj)T*K@`{1z7@Y2^nxce%KHPCBhr&;GbgH_BJT+_)RdQszxH zcucjY$Fwprag)zc(ajpMECZiZQB?B|4OTjv8FYB`ggf3*g-T=nitt#+mJ-XL6>9Kx z)dI;Lq_)~MDopX`u5e&w=U1wQ zkD&5P_M?~AqU08oENrT9X*)@h8Z1&7>$w2gLRxf5<;}YEFI^Vffh#0&rq6I1*O9qC zaCYKU`s*H<>|?Gw5jfCF+(o_vt)6>k(qDJGf3^~5UFRxwY2_m!o4l?vrm6qU0s3aP zt=%eo4RwI4K7%e=SI{mpQ@7YBp8;w~1hM^Pw)U1G!i7NpTf+-@O)72e#M+COoc`s` znE@HAkZ(<;Z@`-;Lbi5~@acdz4~4Sw8v=BkeLcA=nB3gTZGjJ!E!Gc}orKAKFhG}C zM|$V8*@HO}9w{QLgs@5aS{5sSE#Pqd^!hCzUe3Xnnw-*#e3|8>%E}`8E)#l~zE)eJ zEvDM=hpl7|d&QHX!AcA7ACf4dOfmAUbv9^MtRpjn?IdA3r_TdMe?tH0 zQ(Ad?zXpp5?xkumU5t?*?fm`{xgWam`hMMb$p(@$!x~GRMtHWjnco!H&VDEveg$bMtpSob%NAhrbL&^IAkZ6gyt7 z5Ah<&YLL#2Go$PN&V?1D)|_`X2WXeCf8l(dQafnofUa9(Z;i?)l9@T0)G76(RnHc= z8rtrBan40D{q_;|izNLBl(C?nzB^aLWTeinWeqP$L5%P&mGvTj5X5rP;!VBR~)e7P6-X4 zNwOyUlPjhkxIvsK3gVU`4p^SpoijVLY4yg>YqawB-5TJk$w%`yp`^=I<@%y-yKA|Z zmVh{#VJeE`+}C|6P>nXU@-N*N+~{gTx5lwdewt04O$0i>pL1OZoOFGT?#Ds;f{Myx zSLMncy#$3OUK}FYzuUv){1Ijf$m6U1aH`O3BOm+zOZBCT05 z)jEBnCnwIIh1_wfE%N=vmvlrPvzFR%4Vi0C=&otZ*`UuEFp`t|iP$D<*EYM7UAq>{ zb$zbz*0CaiIc1Nc5sh9o_-oH^O~vT(^3cy4E9+{!W5t~nFk1C6zy*=Vuk8m&{on>~ z5-gmgFCu5D_I_IIkA{zXX3G&KWm$jtn+a>Hn&{AS`BXO0$#Z5!cEvi z7#J(bvdObnGgA#Yznx}Yo1<9dp(|PPEA$M4sElRPr<3;$-tYGrdYel>VY7vxFq)Y*ndn|4-k6_VnKiZE(LzP2APX*yja z9rr~ckC^2pjU5XjhmG`>vQ(gZr|tO*C#qL zcVr#&FW5-3QyN$cZQ5$pK=*?IspsQ>ULL7G)XF5f+gicKP-N`**4=*BJ`8|9&UXk6 z5Ao5QHu>8%v=fhZkgRY0@BKn^mF!!%k;(ZDxLInQ-*${!i|fU}K_z?V`vB>@+ka1J zwK5}2TyuLUV?NtCbvBdB`Z1PyQv-9v$hR9i@&gfIF21v1W*f; z-rfG)J3A|wj9~_?>_qeK_EYafhIC8-xH6Kl)7$xd%vgX9e5&tI(31eVflYkPPqKIV zZyxAzxS^j`Xn(r9DPUq4un&Ap#?9i<0IeqQ4wL;txM%q=k-QS1EN{q}e)Y9?BII51 zR6i+4Nt#~I_r71gI`aJ$T+eT0QRCC^(18V}r?=O=(~s%{eekl0={`EDn$|eOY=E{; zIlpm>-E=|?21Co_Snky@(zm}OIj>E*_Cg>+9Pf8~#2*4P#M!WbHTUfLg<_vFPkD2y z<(|-nP>gRZ20#t2K)yp!`AT_aT0b5L$Z_pE)xR)P6VSscy*(rix2|Hz%ShAR{e8U- z_cRUc^Z(wj_OJw<(A&_7*-91KKd4ODsgXbJ5xfkBkAG+WGSXyrFC$}#oV~`d;%s47 z!&z|-e&>ibbi~tB`U{Ns^1dEJE5kC?j2K}$Bbb_GO>e*OHpP)o;rCNKja0TPL|B7W z11sJhMufBHv=X=fFo-b5KX$|Uja%*Rs~PWno=TDQKu`KF=M)uwf-Bw!FQT15i9b#64U5xa~@aAmsslZu9FKk5d4;{(24@)VfNcN4WXrA9_*6!BH zN4nV&>Q(34hBuAbKPnf0j&C)^=~0U@!(Pa=vP|yjO@y6vPiT!oOL1a^&R9tUJMrm4 zs1^-(h=yvQ(I~Q6RRej()=w*tsxb7kv3H2OTVnMZ0H%>Y=q_)b$1xJSeY?oD%wwfO zGXyx3IKBO;x0!8Q@1l8XyC3~`y24B?Z%$6-^D-|*uw>B4EFC`&v$a>g&giF=bs*ezq6fboXlR=3tOiX69WrK< zM!un&*>rRt3~A-zoqBJU$X)22 z#EnTZ&$!!tdvtO^h^imc+b`{9!a3oY#?!GqfxsM{`^pbtq^zEYO^esyl8mMGrdLy^Ww2y=VGk(7p%7H8x}5D(_8oF zb+14n)aIP^>2gj}ZOjdIj5PeSiY3R8enVy4AL0fzk+M*D!(#9xLYfTF;KX! zLRThPANzD+)#XI!XaFWrr9D70nDCp=Lwj(aBr2fArPh3z=a@I?{7Fthv*`m(;7rG>!(i&j5Y0WP| ziEr0Se=Nv0+@lUF8CtBDhjmJu3da29=cUZKNN7Uo0OQr6f^lv$@N^XB25QESAHk6( zTMH003(TFBaE5t>lp`0CupOhgQBD?{9-|IuC2tGhma-T78**mPR~i zXHSw$L9DD_=*kj@xw?bjbd#*BKGWpZK(vj@TJ5dWkF~m==+b6pyH@v(_#D?JWvOsN zr#V%BvC?J+U2*b=4=AlfyxDf%gEmB)`MAEaem_=p$3ec-Bmea!G6;;Eky4DWAm0x= zSn*;Vw;c;%=5D7xx9TD+#BE8Q{*G|lQ=NuV_sy>PlznnNy?xRaQ~u!Om%FLEFV;EN z*&3jY(8j1YC9l@8g0Coa2^B()-j|Ux%-0iK-jme3Vly^ashxtz%bj zf4u5pt$bfchNw4ZK@!dCKppjtbycg6wb@v1Fid3Zqxp|!h%x5h!rGp`HaRNSX7ZQP>|8>xN1nmRev5KdO*C=z4k7ICK=(xMI$gYvY_#>I}FWq`jpf z*JjD*oEpb5Wobc_CukEX3BXe&Yct`Rj?_o(Qv96Qkx?@LJvka2h*n7FGUiLu%Lb34 z&{Y;4Q&PqwETlI%95&}UhuUAp#>3(vbJA%p7=I|lX zo_IQ8R)S-qE6o*~f6SH)0@V6ri`griA2#RPlnm=cXH03b%aWh$)H^(=mp4aUBbD{E zXS7Gr-jTGQNRv{_X?f9d$|&n~f}hBVEEK+0kG;3sbJIsfyUbompL9Ygn0$avD9`i* zrVqTZ_%$jJN0~?rDp4XXmn9k>qbxQx4H4v|V z=%4NAcV{Oi3^?GZxWMA^FrB}zQ3a7PN8U_1Tv`7lgsO+yaWPU5bntv?<@8Pselt1+ zlz?xmlMxl~-n#r%rxEKw-8k`%^vZY#lW}&DW7VCorNlbYRxFPQCU>~0830cWhI+F4 znj&b5G|NOLVZJDe53#lHAWncxBuGUeU^z zL$nL2m2pjRj`%V{?-18Qa%r2M5Q9(;eu}Z43qz!p&xKgr1p>4++Um;Qo^-1}GK|Pz z3R_h-4)YQ0ZjND<8uTXF%fr3Y3VW%W8cbKAfE&pO5u{%gU#gXLJuoPTh@1=?elkpS zle=vwaE6A+Orlx{-@2CJi#hpfh=U?ayKF?`d1!>%P>USeiuJIgnw;9!wL~YM?4}a? z>q}Y-$y_GuxUVm9lVP$hxXCeH`f^UJp{(;qrQLI5$<*Rrg1kLFAxe-pmg{6rm42Bs zB3&zAA0k<|`kBcw<4nGomVzYxxD6{OI_xrBM8Yj1oT&PcwD^{SNe^@R^26LSum#0L z919H;H}x+7*F!*326#vki7*m#H6a}^Jb$3}2>>_E#1JxJz<}6@7EeJ{W%MKy6%q1- zYOIZeZb8Z6Ooy;aD_`5qh#XvkGrFngE)J+Q*1L=p>DtoRRUavFQ}WwA%js3WN)3Ji zoB{J7H`KE#iq)iIR@VE$1fb6@>6SlC_~ROt@8xh>Upnwh%Mh?MwKDFX`7|tyD4mCN zJ;}fnyB>EF_h86v%3D-ZoT!(tQ*>gUZbIWfitVBUQCQDYxwD3{c3-EZ_&@e@XCxZt zHIT`Z-bRH?PHrd54SvS_u`+IOV>pqN^_t=aJWq*g3oirm>r^JJ0EQH*oR6iNWNb&+`pk@`D;}Hd z^*pYd+_=(#g>GDxLXI{n9{y;}Mnz8=m9;6K<1WV7sH~gZs63yE_)g3Anzt2$wNa_g zY*aQcvNvlp-d5s-Mr8}0cZ8oe)l|$fysac)?zG9XJDozzOHl~*f_X!4Q_nrn+f>r#VC*U`Xety~ta z@F7%a>#cBhUxih@SqD(Xf_|zP*1adt5BRVM;LPVWGh`2=1>goP1zix?Mi?8rCc{cj zx+eyPmp&67pvuXER&28y{HYk$)XJ15PIhy2Q0PFk2U1EIOLApyWqJF~g>Nf*3{~Z1 z`t@ri&V7`H`mdGmS43#$l=!J6ql`eM9Az*nujwQa6+nydE-KJo3cVV9FZ66M@tW%a z@IP9Oy#Uii!3otPRx76J=l~{zga`K&16Mb78gYNF+X&V((AhO&jqJmVds9S2OoC8| z639@6m~E)_m`bUau^hI=l%C6n4<*rLgAauvW0f!sPjV&Oa-E&OgjpGEOrb@1=iHNu zb)r6pZJ?4Ifu7^2?}YXiY(7InSv*W+d4TIg_l)bpGGQ&Ipl*aQ?(|MBpW>JXZDCnk zBA=d*Fi8FLhbPXmT~?E)I>!i~D12deaHjf+Lh{F#Ca-wTn=Q@!qs3}=K-rKM8e$s&W1MYMU9Oi`6F{jkv zTOoZ&do{CJ%fA=!gth4xVO41I$2;On(p>^vRG)>6tt%2)KfV8Jh%`41@ zkkc@6RD5erDz}&qNV-doL zne!T^OH!O!lCZ$U{;fy4VL@in2c55KG>wZePDUXWCEk^mEf~)KqqE%6M<7!C;sHy< z>IEXRM*4Pvg`LwSbuGwDxlmIedw7$}40`!bH&d+v-AR)dO%rs8oiIDXu}^Xh%w13( z%wAhYGsAv?``t~w4J=8wM9D7AkzShHJeJF9NES&TH5@%PvxCJ9WRmtQ8s&ZhSfp5` z7^a#bRdVzXu{J9b#W2M-of;`K`GD!!cs~w_k|oCeN^R)rD={HWTcPaHj1>V9$!z9$ zjr?G@M&8n`h0{?DehhJgFj~UPQYaU`QFr^rcdY2z^WCNyP`v2PkH1i5?Pp}n#}xTr zg#vgebm|udCdS9RJu(x+Y`-V8zGGQ{m-Wn42N`>}@1$}RQ_ldLJ~$Ak_l@Rxc|u5f z|GxVq!8Kc;08_Ft)?#uSV1XHeX^fNA@Zu!Av%tM2LJ4^TDB(-*;1WM(v*ZKB@*%GM z3>oK7q*{O6QS-XGlFNUxf?wjG$EVY4)M7)j;~JL9283N0>*=@4fmBfrM3peQHb}08 z{bc`9!!iHxeBZMHvilbgh4y!I*Og2IZZB3enTZ$ z&F6mKt1?GTu0l(3zwrAWb8x!lEIh%G!X^SgOqXf z8d+8MqWPip*eGQ!dofRPFE-yi^nWTF7p{zqE%gH*lTZU{3FBj2bbYRoUr?>yp5U)4 zjDP^HC-^1w#;Ax(lP)bDbzh1D(rHktGa1v@Fy|mgIDwf8x{|1hD&HM0SFiTrShyuX zA>IVj61^S@P`NL#buVaK(z#@RJkDCw=IV;vemw=m&C-jxIB0 zmN}L8Bv%23O?5hb)}DlrU$@!Z7`1uI;s9q%Y;=qkMtP z^687BL^9l)19jPY#8hNGQc3ab7ATU92g5}(*7Oz~S`v#GC6=da;5cWh4|J4&Qi)4J znoQp%`H+8gG;F-=)mcM4idsTa${iMX3c z7mTJUpDLb-yPWu*i8T2H_wU9?lPo-O6OC!0b|Sb{UseAF>!>Gx0IfU` zTN)J-w0YigIj|BqScRbOz>O zxC>vAZg_aKo!+vJSSPf=aM;}`T1|0fQ5-hfVDSuNvbI=Th7i;qEkd{jr&b=*<#bYc zOB%jI-cv^DwTH9WHA&Jp4`=s5n*7%Y)890J=_TditPyk+*8oB~YBW4UD}UQLQPg2F z)5ur5rJEkfcK=h=%I|mTFuT121CJG2-f~qv{b{ zo7?aalr4Ym&?`fL3>d|J~M|?(6(Kf|3@*a95g^ zkH$`>>3(x5Vp^={blInNyKWF^9Zz%&v8MUhzw8DA=rf`+vWA`8?P4CIQW_WwYdIF+EFS{DJME_fyIt2~d>5j^@6YP?%k|MF%YoWP zj0a~#GxAL3pBU|@$$tYk12pvqx*>;yCNYw|Glq5YJ|>4NHZ{=rEV?lZ+<@1Xm8K;OzN-u# zRM7C`xSWd9G~ik{bolMUt+%`Ich>^)i4s@1AYJrvySdG3@UY5lXU6ETVx&90@#gclRJB%Q1cJ)Gf?A&EV^L@$zgxt5~6W>TjlBQDtG=EsXWI0Ie=Qyx^wR#BI*7sn~*frAp|V zV0EQ~R-}4$rM&qgg(Ow=dKfB;ssL^jY%0;2ust&Jo^|kHxeox{AR`JheWADDMfe$_ zAHCo?Bi4nyFKlqEQ-e=nR}2xlfG=8xHo#GQFi}iJ-m_G-eV9%wQG7(6!?mNFzBU#@ zke|z9WUsG+pAh1w7Sx2gqf*l8N^hgP% ziH@WdH+wOVN!NQlW^^l=F`sb~7$_=>PAZw^3|Ll<;fni-=$Z?*)Y2quTIT-uG zQ)&~y@gIcJW>z0wl*x>t3c{^W4oD8?FT6+|CfN~Bjm~Elh?$v&9Xg#UvdTC6jS({d z8c9z|C^1yh;j>>hJ_6Igy=w5M9-)pAG?-kOX+gh+TDt*Yj#H^;@Y@vNlVIVDj$R*L z+<%XV7;%7z3D(Xp&39=kNG68AaHLBsGLTt$n5lNa)S7sX*?xm-*yARb8WiB=*PUDP z__Y;GjuruboR5ka4noM7x#w{%C+Kf^oaD6o$3C9qWyi|?1apq-z?C885BqWu?L6e3 zN}oFgH-BFCpifV)?gtR~JjDH-zq~y?cSaNLm)(yyf4=9W|3=64c>bw;flr5=^Y3xe z_vN4Z^Bm)wimnbqxb=r9MYz$(HGldoz9d@T^f{<^=GCM4T8{kl%lG-mI|kEwroZJM z=P))M7<^^^HHD}$yHv90Bg;?M{MDZoU9@lPXC=NV3put@#cb|~(zyJOu=DgIWohBd za8N#jL^62`u!VQ}86c{O!ySwn>ama^4y)`63f-*G75s0PAU+W=IetL;4*NC#YB_`7 zow-n^+J{?C29h0q+$~N1CNRkH719@ha(DZ^+@}Gf<73Er&%%#-X_V|w#_p4DW&_kX)Gf`aEtFSv=8u^aU?88x2^0&aqu)2B)>Z+h$G@5=d4ONE8_t8$~rmKFc zXu{*sAqS#mt^}fW7DsH+w2&3__^a=5#qi37uJ;i`O=W1oCD`%;gY3R(%DA-ly7 z@%;BIB>BJV7X-DQH$1Y@hGaz-m*sN|vfC{#2rorLN1+Y6zTo*ad4r1*+r+sQ*Ay+v}Y zx`!_x~fTbF}cZD&+xftx%{xxV3Y$c zgKILNvB7O%jQYX*hWpmN5o#_QK+Pw0?5R_VZQ^qW2sWGBZl?EY@S7ftYuH5+A;6)I zlb6{0_uK1c2q&Xhp%^)NiENRo*A5B$c&Xh^Jo4AWZ(yvr8nn4n_A z7f)USI!S&j;jkd>p%D<-qC7T05k{$wB2Qnd2Jh{m8!3*aQuiKxZ$wN9V_E3zMaI_j zaZ$>mW0n@u_KiGGCtw(MJZ3DfdWT06p82j1jZ*xM{teG3;Vb?~shpybpHWQSX^v^7 z$CT$sYh*W`Zg6yhJx!xD@LJ4+tnk>8Za$%0xXG`~8#KR-!`7nkUF>WOYctu^`7P z+edJjg56|VjtF#@xqxrc{pR!@p13m0<5JyH#$tKM;1HZ7%hX`PrZR^zyUya$6_cF$ zi$ zeTbJ^Rec@pZP`QAF7z1bfH==elGbdvmc`}$4Oyml;YBoh?|3|;?)Hah7ijN|_4pXt zkilC~r6fF`)i=J$-U$A09)N$T_<4a{t4fQXpTt^|q`yA@U?wZX(Lvv-G%$;YrDL|2UhhyP-wQ+7$5iR!3wc@PACQIuJsl9fI1XRO zLiLCaDSc0+q)uGFzqf%m(uF9xZd3T}XB0m9^O`DApD{xwGm24ukt#W>=dd@Z(!uJb zlCUX5`&N%;w`TJI0Cx`raMK7wMGtkG@$$VrQspLFYC#XTovJ+S1f8GB`PNUiQ6-6& z@9&Ym*fc3~X3v7aAJFR^uvgG4-?waMoDFUFAWnoH%(HoHABiW|Mo@g~?@+w?QFmwX z3a07KpsyQ57^Y!5qMaDg-9R@;C%?Si3GG;Kp%g(?*F3zZ4Bx79u2?TqikOgk`p!rR z4TdE?e*rc=l((;iRdtYJ^o{_DZYkOY4{Uk+2E5aQQQ?&XM{Fr_#)e-%U&+heQIGhZ ze#OY-9qeGj8O`B*FCgD#)#Qb;8h-n;hE`gQw*kD3?ajMOW5C-@c>A52<<&SM1jq`M z1QF{4P^Te9x{>18jRPJ`VB_j8E`yz)RAQf>BsKhbhVU2o>Wwh517U&-C^jCXh>b0c zEg_cA?i!>>!j?=9ALy8dgOJ?sMNoX(02CX;(P0qn1EOxUjvX!@2XRCrXs9(O&(<0A z1GoYTSgpZWS8a4 zv^}$*$>72sq90U7wsq2oi!*?5?FI)iO29QH34ES!D|=@)0nw za6TE;gecn#>%Six_2|8izVYZ1ei#J&oE?BErW_JcJban|ylehohel#7P)-&gu4FD%Hj*6V zWdVZwzg?DJl@E%7t_8o|MV$fhI^J8lFqz8^6D#FXS97v>j5&Bl?+6 z%?~uv3Up<65R7jR;r5p^LO0_!lC9oY?0c(KcJH5oAeTFyVpxon`EJ=nkSjU{ccv=Iv{>Tzh;scusphfU0PR#==7*90BL zU~Rm}L$MT$;E892zD>!a7dN?{vD;3Jb{({*`o4iR*RSIiMSo3Y1-6x8 zVt+bh6+cltv$0bE5rNl>2*-a9+8tdMzUF$<+k|Wy>@_=I@tP~SU_r(C`I4RSTX3~3 zW^yeHh!8y}<==sw6_$!E2tjpLcv5%Hr{!)7wFD@c$NF?7>uJ0xnK_?!>3T5(gS({r zOJFdAd=Ju&bcPr8q5(a5Mli1i3j4UieK;Zt?i?TrXcU`yF&R76_nPa~3c9@51sF$# zq1**1`36hHECJ~&WfEbpV#6tSM0DGd=4*aK@GI@GK76>wI2%C)75SB#=v3CgJe|dKYcGi zMx6+d@$LR- q?_W3O&rMAk2@L@PeETx-V*zn*#=7J&f3^VnZ6JP7Y6x?@~4^!cR zRR6Mjoa+s{&QB~KUt)8kq!tK}aaVk&E|E#e&O4lzl?E~zJ}%DkNFPoRmRBDknY#y& z47u;`PCh*J)0Mr>&FECld_g>}c$~aEN!1o#59G{OKMk`rUBs)_j{;HXm*b>@FpByN zZ@lXuP-KUDjpH_h?DL(R)8g8$<9&j7uWPObdkyGO29aZg5LSa3;nrr3z2c)gbb^xQ10OINylbaD&38(2>>i%U%Z>0D0O;Ag{1ucOT`~1n z@^Z%qJ_!76b{H+AhX)<*r;I9R6q3q%IvB~ch1Eizhi|Q`z{^Zq+qdk7(hW}|2%OjY zqcV%qU{oaG`~unA`JVu-s}jb|-@{8Rbl8UZj=R0lQOa0JEEzJ(9!DH_#v{AJ{oqa%+1@U`1htYjNcU3QGK&|5F=%$(v%SLhl=A%S zD|M`yZ+)Hc@)K~ySNaTm>kbW#^&Gg5r6EEY_Y9y6AQdXA0y<+QjC(j#hYG40;OEV( z9i6?U``f@NrCP{w9ao;WzKrocj?Ma~l-IFAf<+Hrx5C?ic`6^%e~WX8E6Rvj2$miQ zWsA#wS_>V4z3Ei5v&FSdH?+}8J+vrP?d4p=yuxdDy==eHkNdHc{SUixoz-^YUyhZA zP0>$PgFQX8QCfmFdQM}->i2monTm0@L>nbJt9=CLsjLFXuUpS(rAFi>eZqvNtJ>8T zISGwwT8%^vp6cn7wNm`i*hx_hq%IKaTn>4pZ+OW0d4Lhqys0jem@K}B2nlQ!xA;M@ zi!dT@!Ev`Y&%MsN%{~GSmQorKLXOsaOIz0+YxA(_jhDTOmsl*$Z-KVub)R0c6j1&9 zqptIC5A)o4XRd3PGYi%&BAgGTJ3m#bZNE}R)M%UFZPaj0X}TX9L4g`#$;Ezn%9b=T zmX}+OG-2YGYuE4RM`F!>{E~$vyn<09$1jZ{7=B#GkXsLAJx-zvq!0Vq!giT4>S5)l z=%L!VdM(m*&_b+qjgENN`(b|HOf`{79k+VNx{|_+o#w73kEeNet*2wXYc-|ZP2I

Bt%5;AQ6O20+Qz6 zLGeu(3Tkc)^Zx({sJu$|eHTX7Y{XWZz>t6KCZqRLxrIa25NOmzC4{z>(Hea#!};9P zdNXe5{!btZE*VYZ%jMNqWn^&=S-M~{(OUqEM z>z`fe_g}ie(~ES)H-x9X2jFugSpisy!~8xtm#UTvyM7GIH1sN8H|go<3YBj5`lMeq z%aC;}JxIm4b@gh}I*)TtI(lhHE~}XY?ZbW37poBK}C#YGOHhDK$6$e#Wr{%TdZ%y+rY8|4FunQppA6GO#Kc^(Ck8 zcZAQ_aX>0XdnCEw_9AqlOlzPDxm5Y?BL*U$2;DnB2!@q@eb5QM5E$(s6QB38V z?Zn5|{m1p3UFS<(DG7&C^hACuWD%Fl@1sg7{)>LJ=k>1W`d3j{&|`f>WYb~VFzphD z%~?3)XU(KKZRyPwn_Ms1=~0mj8lqrE0nDp4mAwk(FV?l%p3;ztZA~SvSm*7|U9O)h zvg-zw!Zd7-b)KJZXxb!Q^X^zDk^g{x@@rDSjG^%RR!&kA%ibKOV793!`<9YX^?rqq6H#$I|&xjWQdgeP^8T~y-Pz2U z*DA8T(f<9ZgVIc7CyE+8{Q;mh3piD&)x;jVq|J!X^#3*@U3ecyTpbxQgvb*+?(Hkh zeXzlP79-q8f$q;DRPh&219}5YoLO9AcG9Hv- z!p!T~LXVINj}bk&j~Aa^k1-gaHgYS6=`@W|O{)H)QgtQF3JXCv%TvmPe(7!yJZn3&UwX?9lW%i#I=I&9&ey$8gJ6`8yd%g)b~l-Z1Y zT%iVdkSu1nS>2IZthAP@C5K;Ie*y=tM7t6tJ>G&tX@C69{kvXM2gTIxPTi))e&H0! zxIRD`h0c7GS!$GZ(D6x3O`qH9LLXOCTf)g&%FX&6m^1Fju`Au_-v0HsuU8VWD+hrU z4V-B78%oyM<9)qkb27dEq}_u@Gs9+=VweajF?PJ;XeyF%UOrDv%eT-mJc{=AyoEn} zudI!O2VulY>@iVIu-<5I#kr*zi|P(NEQZ@|o#tII0>QXAgR;_gaww7SJycmw`IOc{ z%TN4*nk+yG{Wh{YUQBctOLw@8n3uJn(r@~VWe}<6w3SKNj&|}clG6rpK}8gG)LB-Z z)6$r^Yr6&U-(kd1!lMnaFw|3(5TF*7{bgb$GfuKH2O>Lb|J6?n2*h*6Vl(A`57CUI z;Sm(I)>gu-cqfV*e4H!ad-4)*_Fdu&2MwFiqG67OpCYR|Iw_G{EAU(fy!A_bPUqxid(g8B$4%bpRgwbUq zTAh^QybJQ-0d=iC!JiDo-B}mr1>k>)szvx2j~&{?+!M@FqB?#Sj5T2C7w4opTjI#v zLi9~|8wVV`g+=%^ICK0o$bq7cqCI?`{Ey1+Y9!C+hyiCY%-5;$A{kA_oQAKey!q!< zv%MIJve)dZT5w>++1FE-wj^lLy@=n6&u>Y94em3niBKNP%~^^W5wJh?5fU`6A~pEq z2lGw&EfxW6b05Xt(t&!+Hqo)5DV4y`Svooj3`kcRtVXQXTD zK9VW?CPFd~4j`F|Bc$OCST%Ks&LNVnL`V0BUDV%tU&p(##! z;fqa@@YRidehVr7hXIa~yS_Ru3E$kn{xj+;_;G;Ty6c-&>~>Z9?wbNh@J;IjOp5<` zU=63(C72NFOUGXEwr~RrQh2xGF z44bxQyp5x&&p-$5pDv`Pf29-{)$2@r%ctC(>(1)*r>=(* zt_I81$YEquNZxzrpVIi2kMaGSF6IoQTGET6^}n9BzvU#32|B4OZ@$3mPcY&Q5FvDH3_UOxi=3Y8!`C}{ z@(R}4btkN_TVjJ*k2(DI7U{9GH?ym{q%Y1+n-uo}72b&lb+!DPH!FNB^HN9fh!Qzh znf}KLSSd1M>r7&0(Q|gAKh|ZBMz+xv)2{;v<)-f*WImDJ{%$1msr2=Cqr#hzXUsOLKzfE5EMfUdC@ zJ~!sM@~i4YSXRh7-2|WZnrjf#sFO_-^b<_Hi6H6;Mg*>j$z52*j$$m)9gv4%o`{a_ zR@K-<4Gjxj7CI^1H^zITcp)&td7rZ*z+$UMN6@CYZ^YmzfYzY-7M2QLb1D#)O#`A1 z_~&?8$Ft)MK4u)xjC*FawFZttr!e*EeF zg#`Z`9YCuWHIlF#9gMzGWIYDUGDVpZ;1G1)i=Ng2tdrezy}LGUomO18E;H3y@p{c# z`_na<4U5uVuNj}VyJicb6HH>R%Tkc+j9teS#Cj_0D-Tw!e|nv^;=ieH*DOzykRw^F zJYo=;;c$U9R31#g7K@M9O;ili|C73_Ch~m}azJNkFxEQO#jU%L`g+Y~d&0@of=xBg ztt&3|7(ArnMCzuRHFo9%b#`+F&v`<=AXOF_Yh&St zQG@@Lo^7&R`^fv8yy$)GrEqlw_Z-sKb_7o=RK9ftmBR=U6gq-C5qt17sVtiR?t*ut zTN|YBnnw5aPKv)8n;m86WiK|gmA@!7L~|~9x3WmciaE2t6|02POy2&GB>bGB?+9)U z2j@#S{hSe_$BuQqRSh;v5B)rt@4(7K+VHc$2I{1}KijE36E<=3*6!fX4L!j*hpLsO zJ1cB;)I=wMHa=2&g7dnw z#JqZXbj&&3kW;MO4FmVDP%J~id%Ea3K+S$e%*^0hvj`_Y))6^kh6$es^F7GnG0oXP zWw?GJT>mB&Ii_0A>Lxb+pVqzwys0W(d#&6W+7we-u#^HxZ-y3XC@oY#OFFqYg83P@FRaK`FEpg>i7^cqSEaxQq^pT3hiqwa8A} zv^np$cap|A=Q+=F{{P?4v$A*AT6^90x4yO3x4!RPXF6~uxr#jECtX=*vJbkip3o-$MycMZDov_UQFgmk3@@a0U8Y|SJDM; zg`E>B3!f_Vnz6D@!jj=epOLOZM%)r&qzo#eF1>#SxMs1hoW;SGYY6p;w za8Giaw|$zBEXPyb{379-i`p*LKgHlh#{A}HX8K19DUED$s%nQg2_h0o;Hj#Dorj;X z_rD#=to@-AZ)h@R>$A@o0~l@0(ykzpL41|F>%f0kMJK2l4u63)m1s^wG4yIf6Ub4Eu zswHvlnh9&jD*pGuLaSlF&MS7Rth#-y003X@C!5He`+*}<0rr*Pt#2{|W#2d^F~K{- ze#Q0?Y#d@lAtq~h-(oF!pZ0|;XyIQ?$hmNGP1kDuUf9UB7pnX1=7HtGelMM?PuO4Y zVg(J9A~(VyX}e9gSNl?9u+VhEGq^%s@nNfB^emr72X<8t!OyOqV4c-^<=f&=%2y`SF7i-k=0se@2(Z55<+W<}AtZQhN1fKi3@P=;@ z1RIx+ka)v5hCX!v3)h8jY*!)vIqV#7Z#}{Szk&`NFENg4Y0eS;(L%l~@B-3p^9LI- zL{>g3upBX{^`QL9CAQV@^I|ADzAfxsS*d3LH9O4*Ey-OoU@AxR)Fs|^T|;<-SL_<$ zhL{>0p6!7lW!7jQKEfCuBQ3Lh-poW8W=8yqWxq?!F7&6lkVK6K6Wd$4H8|e&A1x^X z&u!^4nW+SjyE?teIC{G+3nxdZ9j?l8$F{Fw-tBe7HUGSND-6F?y{`H_=mM{;&KFoh z!EyQ3*mn_=jd5g7=^dBTORXqy(1uG8s5HsgjmHj8FWDfCkOgrcK)uZhzi#;H>wBbs zh153KmUcylvn8A}qb%8T6G8n^8Wa zY(LNP{vlK{({XBVFu{5abURSC%m?LKyILxQz0OW)f0OIj?#^_pU0?-tK1jmVv%!pM zhn;4u6adB*C_hDWn%~1n=zB=#U}@6maYC+pDppN*t3rNT@$I6Vo{k#vI1Q-A-+CPN zX>zmXz;1m>b);E~4}jlPFDPxK9bAu-;P-C;&&*U0C5QH67M3QW+OEbV-4wY^ZPioI zbmveUj!D3?htB}*3i_ol6!=t!yoY;rx=!h_Q>w$>S9=G6-lv}Oe%yQiizYs(ztwl8 z+FQ4KtI;_(WB(Dk%wi~&XBI?DdYT4Bu=P@;OLv=TK)v#h=2&Xynbw3Cr}DepYUdCq znPAk7ftv8}GT60Ne9Z<#jWB2)rg1u|d!=(>qfdd61+0ziSHAU3-M(@ep4Mt?U@a+0 z?Fo*uLMh7X?2{Z9e4~7m?Q!3v%$L1>3`}X3cc#}1U?=UJrj@kj5uZ}=EbO1+w1p<- zRGaCkr3pvUIMba>{&!7YBXe_Dy4pm(T=u<4q*ysEIAtPhw`t~rGeGohhr#=`Sw53wWERDMWxsC1h0B$I=bQ^26kC=zIMz4?udRcs z$TYATEwX(Ts^4aS!v!i?c8~PsiD&KQ+vWrf!ygG6484*OU>x8Jm=~Uj3KMvWn4Hd& zVy7XJ&7lUkXZPxlchyPme!AeO^&{+*&7-|G%yWX$4a{b=fR7?k(#j^>w}@YBc&TX8 z@vMGzIP8YbjrBt_Eu?jgf<0L zr^`8bdj5(bBSM0MdFH?<2`ZzAu6+l{;TiE=9UtfpLb-}ND+Pn|KA~XaR(%^_;C& zLGD@{=)|%KL9R!-7WXa{r=dBFmFqmV;UOru(bjzHAXh-}896VUTJJ#{Ci2Pb82tE=1E&p7~#(QO&` z=HGFdSV|%|%AMr_BREQ|Vcg#-9e8J42kx^MwU2slv^ehP!+f+iV4v zCfFoCtH^Zz95P|Ag-RNf9&0FVq;9QluT!baM!d7G$Ksn^eXj1lt+ouZINSs_sFp+A zOQB(PrFxfKWm|^lh$j-k#yEjb$-I!Y*Qph5sQ(c+d{1V%(WX-QbFkLidR$(-$?t)< zv}DRg`%f@sD~fW_1rg#=`!#9uC#!`eI@Qh6W2pxnQtNyohK9$@s+DiB@Hh=5YG-DA z@@P`7Lh^usQW%Z$m6LjmKKdf>g+alTDoA5eFiN+H&B_-Kh6K4Y&=}Yw6(1)`j>1#+ zNWV(P#)rHbS{szcc8h|M=ct})5T7^lOr9kkGV)P|e*Uu^%=kk~8g{*6pEajl`K-I+ zKC9NBdh_%X-}!%N;iPL}Y7;w+{0`AHfKMC3Hk?Kx7>71gb!bx6t6Y^hdR1AxQJ$V70ZtTaStzgZLdI_MjYfKJ^CR zU0me$4kzUUhO$A8iC@+RIj2>wMzuXCj!Wm}q`P~k-~a_0IGV);>3qbXk-dj~RJyP< z4b$Aca!SUF;;ZSrPxS2mL7|@)bz^dMBNR_5= zc^FX7$*B&ySg}!oZ%BjacY@Q3xEQ7C3pO}9%G3p`tT8-IO!$i9(-(6x_@E4b&t9CH zk?yfTnI$ZJ5AZtITj~v05(+Mg(=&L^ZC`h>tS!OACZ#s%;^quKT5ex4RKPPcqcmu+ zcT^LcCsp}q&lDdI{H0U;GZILu>G4FG;w7;&gWu7g*cQz8%yYJPN;p-sFq7w*4hJp1 zijCQT1SB=Mil_8oZdLUZk7B(R6jx^QoMAg4*&G9OP)N(bBA$a9C}nCv%cNptj(zo1 zNoiOyu3^QuGkJbuN9dA|zI-qwwq^3MqG=dU=YH|7VfWEJPAGD!i-7^5nkD2rG-m7`KNXTj2U)!Z z>y1F88d%l@w!+7x*D#23Ndly{e{h7+)bqJMg-k=Pwwr5mYSI9_!u`L7a2@{{&r&Vy z(<`>P>*osUZ{Ag7Lsf2DS{ejeJXy4fb{^dw!j;349nAzB1tTe*T@LW9>Q75~lc_(4Ag;mE@ItaCu zsna3CFR9uos4$FxV6APCNH<=y{WJn6VMYdqS#1yLOU*4IIq@pfuNFO=E4Gw`K>l#r5g{ z1Wpo00#_yA(@C!>V$NTVQ(4XjjeMH#BPVtzS0}p8;2(U<*vNm>5kmpUzwMJ~)LGai zq(~nD=iA8X6%IggMR06HF){@;eR)rxpg%)sUUygh?iE9TH*saf)SD4<8y8P*(EZfJ z;FKXtchXB{A2-u$46nH=Rn8PaZ9lip=9??mVHQuv21im=s<3x!v|dIYHQiV0smcCG zHxGvjD8~DJev@QBy&D$&5vhJjX5p~xS>{?`V=zM7x|K7c7!L~csN3h!uzpaOS^ zk%PNOA%~aa$N@G}sUK)>zX*%s6nPsZPSF{DWyLZOzAlVA9q&SGT(ZH z8>&)-57rGP^d%_0=~GjL9qTgDfZJCX0e1xGfE2Tj0bIbHlR`KG13($9pxytFU<4vk z>M{NX?FW2`HHsD{_*t?EJ}iW~i27jJouvhKt;HlsZJqS{PG^n4&DXFV{5)I@SnF&j znXFfB$lj1t@fsYA*GrZPAGfj!+RNT`m^520_mFtEKC@rB{PI zTb~u7nF(<;1N7P4BTk&HoXc^f-dPx3zvie0%#dj`E9my04VujK%(JgMP~{awUl)S2 z-~wE>tjteZp*rUn;aBdh zG`rqY4zYYwn%y{t-$4^?mM8Jg zyb?z<;!pB;x+vuFEWTOXp2wg5DM{uTkiPVoeRSc(?4$-1 zHrdq(QG-S0cU5*vYmRqIBaWxzKTuZ!gO5X|gbBC)ShqAj7$aCQ*~a@6tZ84UTRM0g ze2Wky&H8*@^Z8&gS6OyIzlOR8?J#voe{B+TEIgkt6Q^4E5Y?feSYzQkY(WSUMEe(&4w=B;?n_Qfe^{!nJ8kn9ACWvQR|!o!94!Er4j;hs1mH`9K_$ z^l(0(z%AnLd_I|fDE=*MADeOy%gkD`v`d7`P1 z*YWqo*9-ZCLA|{OyT_jQ7EM7!`OJy1RHrDUi=oQ+X=zs>y=vu0RnPT`A6t2DOs_#q zzMm)AVvP`(++z_=2iKSRgX>4zIufk6aF`O$-V9CoLyMKY6f(-(72Lt19I#7z;gncZ z#1{_9?WLwVu3vyb z1Fe__qJavivS51Tlcdagv9y>^H_&c%ZwuLe{ty;-6!Y|Z{spJi>42=PDhB=k9Dob` zwnnTQ=&TZLds>oqU)uh=VBuv>vv$)cN(YoEBIo+zBWNZjYbxpLoLX18|vYjF6IS3;Rj0LK?te84$ z+S4G+EXCaDo)b~+p3+96IC(68SVQ#__l@Ow&;mY(p_eRe;;3R8Pl~B5EUsdKQ=Mgy zJIV(Wm$5)w{XLFD&hH$2!l9~Ycc;#-KW557nKnhS>3XE8owr0jj+=7#bW7KQ#9_(e zX*(2uyQQ5Y0kHOmwiO#APATP?4pNxhrI^l{lBtec`jqrlzRAMx$XX3YWDfHG z8;s||(v>jFA@ex~n>N{C&IZsU4+E_J6zor9uMVd_kb>m_pB}d3PxTpeipkS?k|9Qz zmLk9SQ7IqkHeTY6D&a2DE`a4hR6F-#OQN8{+)-7L==icF3r~p^Do4KTXMd3e%#ovwRg&N~AwK+mmGvbI!V>duN`5h@E$ff}eomj{PZ2h3T08vkh zeGimdC)K+uBdS+%_#6&Z zfNq@K+166(1~n=N$m3kMu0@X0MvSw&A|@6wnJs_4Tl!-RZxub3xXi-Rr*T=8VqnxF{`H=tvc{>T3%4oWjWNRz8(Tapba}hq`jy1k|{oI897fB5oN7%^qB@5qf9CN3fZ{9808ltp8 z3840IbVLRwss7&pAJJo+Uqw~1M5;1NuF4zT+|>6r^z<<2yoDX)#6TtKne4Xji>~5k z$I&(Nh?{dTSkUVH79ZI^r8Q{A;=8QRYuv8JPX3Jegn0LbWu z>{azooL4QJ_jOCZBRLBaI*Oxp@R2KiUQT+I5cI}X7lhComi`oGP_3F1Zf`+`sgVvh z0z3LJb7E!hwQ4wRL|sS05@AOTWUp@Ng{C-ana7igqtxA}eoKcq47I1()BG6~gZ!*O zf3+JsBpO+L_x)B@X0?l^a$c(GmL3m_^U84~oW}l1YvSI4P{5fD^#G2{0-*$|+cSN{ zd@x}W4)8kQI3TFobnFE72@L+{$_>p+;J>NjKx~38EA(u^CRp?AHoaOKvrJ^B_Le`j zrZ38(vF*o~7^vYNDsq9xdD7r$jo4lq#k!?OaTFDRJC=SS_LTGNdon8S$6j+f1pB#R z!l@y<#ITg#OA1SgX!h_aa8q-))O~6?;_PX{p?AZl1vLh2*1!TqTskSTP!&T@#Lrto zlX#AZlQC@WFQ$dv93}j4HKw4r9*o@Snc!3J)?uBDtEyfJe=Tw+KLSsri+~jAS{b?T zirfi=bLh*;dm|1h_ZOd1?)^xel(9_uVdo+*yovkkI39|wQPr}5R7PG9ljp~=3JM(y z70(b*YF`ENz;P%QdyLGqiXeG$J%O+^Xq>J`SOdatAZ*}kO4ySKvqZv9AuJddHUMER zq}hP?jv@@&ai(Kil=s#RQ@hzjyr@r8v(pGngX08$hHEQcoQJTP2zw9ynQ*VrG7y&q zVV98T9=NyR??}L@5p+TauE_|ai{KMZAdH?jN80O7wEGxZ7T|gps%0xvv}!i`w{n|& zrjXCVxHh3E(WudTv5uKWkZ=Ome;5A$;$N?PPWS)!g#UL1)yg#^Ch?gVS`^1Q|EFY} zUX|RmM5djg|NHiNxC;jr0TFgI+y;C$bC=4^p2WlCSKxub?QjR+{s4Cp?jaE2McnE7 za32nj?^Ll4IJycVcY5C4reb9g!aIrQ4RG7B^;3rZ+ix*zd>7B-XfGF+7GcpVxvd{E zN8s?b_`V~=(Mib-9KwcI>JK2<->TRrxc~8Aku2_mf1_emaKC_i5^h7}`55jWz+HxG zgH{WA?!v-cIRP=WjAxpoFE3m%0!W!N?|X3a)Q4x9Ecc8qvdTVqeGdO+g1yRF zvxw4~hh=-;4#CR!@-3Oi-xkk1_&~Abab6OOl{y4%h88=Cwut5?E_obrO^&$4QZtok z4L^$JplI{sqk$+XwZ8HqODyMH8mAEVR9xJ$8B?CHvKiCM9>i_c ziuntf_`yP+(|abnVymi_uaqMc!6){wsFeQAT(ta2 z=hCVg_GFcFsd!}$H%%-q96hSEWYl77ana8fjvKe2WbC*Fru_W;M7Cnhil=LrS1)6h zrK@UcMzH*!)znt4%xCr)ri$97KXWc#6{|ri@SUgzO>s34iv)ge0*7slR+k{U1fGMH zM>ujJ9(#SW(mNRULY%Iel_LI!z>5r*A~b&%cgi40qj9Q9$>4Nc26)R_zFv!?FyP=% z1(2`ik5%j@<_dJpM1OxX5-xj0e>6($Tf@XTHN5bi1k|8Vc}0oAmGEKkk4z(?Z_4+x z=s}uD{F1BU`!&2*E~i;3=evj$B_fn_wfp#JX8KVA=<&x({;S_5@K(A;MtBy@@rZ{# z4d;Q|47UyLc{nfJKDYyLZ@~HB=v@(G*vrp>AHijR3trZy(y#^J^v!MZvXoN3hse=V zCIwXN0StOyMh3i>T2(0c=9Di`5OS0-UH#!}(!a;i+-j^gVGVfupe>$DxI9e5oU_`kn$8~7MpJSL=eJ3-0K3I*m8mPvs8~2ATkD+9U61yT} bRO~-J47@h*+B+cnAsi-_DH?07FY!MCU`pYhNcb zbLK3cbI#{{_A~!}imU5ohgG;ZK8?`7`L7AL44d%!z27i|eB&RskkL$;m~WdTtzw?{ z#@?+;c|?AP5Srpq{^2_+#fR>_YKiABzN?Ax?v*TgHo%gbas6Y8HT{R5zYn~pSufqw zUv~JtEcw4C@BdeU|DWG`|F6)YbA!TIlNDwflV@q!?g$408I?u0#*KRi3EXQ${P*6VQ7b>D@`BON2pW_x zjsEBj@wgG9--V9FaU_CdjOWNiq=LrsE}xB6(p|cOadC2n+R&q)r0ro$XA_+5Go8;l%G-kz@9$t3{;bJJ zOj`Mcp4L#Y5DH13>MWBB8WYQi`KVGb^{6r(iA;3<#&JiL$x}6g8h%wR@Sji$#+<;r zOxD?+vefD>XqJA}E%eIe&lkTi`-QS|%9@uq?$yfqedRLIzb14%e4k%yO$_Ke8U2Ay zeUhd#QPUOtD@K*ww^B|aN#Ev*+whdx_xi(Xo-y#WD@8UYCAO5y4fb?_l}9miS-fTat=)F^j$^P5tcCFmgO~wH3JJfKh%3vgg zci`z&q$x;?&2BY3T&-?;7+rqX3jnk`A66|)Kn<@_Q>X3FV+F= z@Uu|=T))}vy!fIM)&EfXSd>@_0+`mJ#Ul^z>$8-%Cubu$ZKuxeV~llnUp%Qjwy&c2 zlYLrwQ!lqu4RdO`pL=#TdRW)%C@;hHQEx99%6GJ!{n*j%iRyCVNbNu{}o=H(TzPpA$}vuJ*)S-vNVCQ zXl!insnD3>6QT01hwr`JoKyUE&&5u*Cwa-fo=X?Ca(b^CUKdR?b3J;g4vWJDiBYq+ z`GWkE8r~6ALbuL(H<(`B7-BBB-bb?MDrTGd&0oFP1Q)a%A+@aC2WlEk)9>);d^T9zJVO*JF zdJ$=<&=MmhMZ7RbFk};xR}|u8rH?glQCM4^z`Gd9B#44WzDK20iL~5uCyUDI?#hA6 z`bJ*(^-^+`(Oh*YrJ9qIK))FIVTE>A*Q1qtdwW9>bU40EF4An~1&eN&5Py=%O>CJZ z8#J?Iqh@r~h@ON8Xq*tI%d;8Qx$D$agV{@|BmkF_k?+^ zW)JpXt$DLq)^Sc*%bBa_8$l4H-h^xHB(ETNppzsO1)K282COAU;o~BV^YQ+1(t3KX zexJGOwDwOY&9Z^buHpdtogUL>a+U9l%E+}$1EEMniv=Q=nFcw7ItnqqFo-dhyDLo2^= zh@*EspP}P5@x=HRuFoNDKw5y5J(wfDMvX977$(#KX>RM`1R_`TN?*pOd(ZWd7Cj+A zMXs1DC8m@NEehzHEeXV!W+p~mvA$GaqHm=6^Luo~Iy^OB(A(+r?1QA`Fi(uM?3y%= z9Kbz&UWNNgbeJfd_G{(#$O&*vlSoG!ihwZUnKyqcCgb^BIWo>fzV%Qhdm8sAA}@Ic z;r@7p8LtoidP(z`R{mEcuK3kX1EZBcj+g|C(0rlN&dVJKNXs;gY&lXr(i=!;220Nj z&i2O1$9o!vI|O3VPmhz&_UyV#*QqDK<#nV5LnU;)t=P`v+Z1QPY@e_R6Qh-@BU90F zb>xj@-)Jo-wX!dgk7r*bIp>XKBXk<%JQ%Sd=fOxyj`jNe%j$H)kau@v9P;jt+`oMP zvf)|*Ik&qbmSQRzER9e!Wdc6){w1k7T&p4H3{Yf4scOpHC^gNQGF{ex`>mz0~sgLYf_AE0UU66})J2s0>C6LbD8Tl zu?BSgqf2@%8NCrCmX*EE{hi)&eUOzCIla{o)XFwxFFs2ehyFyd1>SpMdypNl8ch>+yze7Y}~YNEB)I7QBawEeFYkcQ)rn> z%eL#ZCpEIFmKD=qVz#wjT1Kqg8I62dEiUc~U0Sw?TtbOo)LAI;iz@9&zA7y@cy=Fa z9TU{ZU#KQ=Dqei9N}a>Dx|T7#H=pjU<%e+p^j^=fkQI$YPP23K-P za`CV(#S1dP>RX0`h&`a*A6j`y&o)z(8V{Vh1+%;f=|iM%kO&0YB%}#QW%P=?LZo|A zqz_UohTbRxzL`+$yQBwy{5B%>rHs*1rRZ?3l$&Z~?^GmL>c0oqLR{G1OXS@OCm)MS z6VfK>4=YdDDvLIGBu`qN_v38v?#3ZPLTRF_*1djZhI=q}=nKj?;jv3`)x^lT>WYjY zL?oM-nKENhBh;-;9+uF0mLy_iT!!q7#bDbQ0q=AqEcMj5|KzS+IjGF&KBhcDi1~etNV;u^F`hQSqc=;Ev}wpx@75O5(!fJ3DJCL7 zBotuQ#g%f!jETtN0YZ2qNU8BTaJ#C%XiDwVQr*1EZhF6sNi_(xjQhYi)FC8c2u1$P za$X=~o^-Hh4Itw3I=5J`f1aOos?!baiAg!ZQI_n}qs6RtP0~mp+Nhc8S4!>S6N=t9 zTKw#iZrrhbi72{cR&hHagHMf?M`?yQskma78S;_xTm>`fBc*{bQxhkDPU$TBSxBlM zdaZY-YOFLg^WbIr#)ML0PFbynUr-I5LzA=teMwNMHNJmImtE^1XZ6kRDRtyMrNPG7 zE&{J;9Xk1dz5MpFJ4#L{wKlzYNw>JEw^L zY6Y?(B-1tBbzX(ox%vgO7hHdHIa7-Th%*WQ5}56CXPF1Y7!)=n3T0rv$It<7{yE3t zi;tu6$5mo1ZJnbePB+V=xKyW8Hb7f?&NfHkr#oc^_W*U$;m{dz<)u`1-(`~I2qd1u zb=0K5zRSL(DfspJMhywXcQWyY{G*|Loy2=>U|*;2wm;Ed@5LarS))d-h}z}q3VWB& zz_<426h#Z1XEFVB;_0+6o^{pfes)H|#q^SXsky(jEoyOwLjCu7QJeGIf%}}O-P_-= z@zg+8Cc5%fTpoBz8|awWJ`QEequ6+-17aIpQD)+&%5%m~_I-*qEti?N=R@x+b@um_ z1_LvBd8k}gZSM|nlbt}n*isRFG9fGrbW4V04OzObh#1TC%>}?g&c&CeyQS4mndPL) z$|9QgJ%ppU5-O4E8<|eG6BKEJ{@8n+WG;Kzmu<~sPjeu2eKGNTMj0_qPm+E}pXAlc zfrB5%(XEh^&Grxw18hA!uYXwU$}urLY<|YF%ttfxeHmt%gn4_!Zb9Ym+$C%`Qe-app3#e-e{i=L_y>2Ip6G8Y5%jsu#_y+D3_VHO{BTp%A)QUA9fee*+*$!!f*J$%%y z?cwGXy6Ziu{<@+9;be&F;oY&m5}*{V&9JhSMbe&(tazWE810TE$a&Jqj4@uig}3e{ zW==|2n=kuxJfz_vf?Y1#@UUNxNqAJfZ|xZ!cj9?n(#ia_%bScsGUc}A8u_JnOV`%w z^e27R#Gnl&MYW*+-BVk2L=IOGt6lD@`bw8_GQUP|9TdDMriwe3%oEQqnJ7TDl&gBV zT8r1OhRf8fR-4cr;{K|Jf9l<)SSnsAC*~otJL$-r4=&A29Twow)T*kL@J}D{i$v%xdFRH=1zaD$fSmT0Aw@ho=fU;zCLx?eNP_D6c z;(9p*!ZJ*5K8(a6jC#)OE~+jTcOX3*lJXmN#c z>7^s=23+s=qN8ofI3MjS8AaH<6w2W=3)(fB^7d4dw@bUH!NX*0JY_vst`oWsm?Tmr--EJm(e+-;4dnhnh@9_c} zn-j`0ps`H|#aox1I(H#e9!xy{{hxumMAk!0ENMvuS7-AU3@pzL0mk~$7R*a-d2C*2 z*BLRflZ`FLGnmPA|4bp#rjaN0aL=eelH6-icZIFIUDE4iR)55MiKG^{bNU}c{gbcl z_6e;aOs(&Nw!A&vd|P+5q6-kS{xzeZmo&1oPyhUx5HoGCb!ZIGBr=)QlOg#38~*9_ z_y4OIx&W4*bU$Os43N?9hE6DzK1w{4z+1{ywRH-yd7qtSX?a#CiLKtB057q;5uj@a zgCWt#A9cTiN~?U|$FLQqp-zTngmpqcimi?MQ2tmTpzwRY-Et5nm}eap2%!`c(qnBM z4RAG%nwg$s8ArrL8OJi}(Ix3jjNk6KS+KNZ2o+vH_)PaHC2{XTz!ewFtvSEtS%>$x zWqUr!sKC3IyKfJjRd`Y*W-d={I;)Vq?V%C@?8~AA8ajFX0)-z%axVt-tBK{;z_J=^ z&C4BJ;xbQ%Sf8=0gJWwQ+ge!R>3y`V#oeLUFdDia3ONK0OQey#kMyj=7-n?cvqvMR z^!~8rS!IaraMQC&ns19@L^9b&@Te5cIE!q0@@q zxJ8Mx9df1G50z5YLvn0^gf>5DFb(Qph)({on;WVAcHJKRYX<8%CGbAKbCY24wiwKH zvzShnu^q1`V1N5i$@T`O(JIi1tix2U8tBTn=RhZ$bVQ&NXo01cO;{-G>`2hFuhG$6 zihL@Rw=dx0Iu@|fEK@LDW|C%feYAVH!WroMpCX5Uq?Y~i_LPyVoWy1+gIlu{FKc8J z_9sSR#!*voQwSrsQ4lcd+Jm7?g~_ZCJ_lRWzyd+pV=y*_q#Fi(Y{##5FI`Dm%`1s@ zMptn6qU!yZiP!tvXS>(B^b&zlF2{C$1>aewubZWBA?9YN$ddo&N0V~(!Qqd_7h*QO z#^<3BGm}u%n0@zl2NTE?J3_P|#n7#o_N_7G)W{cObZy+p)w+)*->|N}ADo zMll%r8=}q3jUe-XXlC?o^WF2%h-vp3g*2R@#K%U*ykIvaBlCBIMEjCs^zOw0FcWvv zeP8%r^6T79(XF?a;>>lscWiNps&&S11>W5$4{dj-U6Xg8*_Q;mY}@_q7ADu*HEy@L zP*Se-oeXi<`5M{PbIlL;tv#(9dh$EaOADdgt*3jn|6YHt(BRR^#VR9kwY2fIyMAZU zx3+w{Wg=L%Mn2R{sb}S`G2heL8u{yP?)h}TMewTOZ9Tt)$oLsuOLpbPMo_5g@#5O= z8$6r`JkO9U^NhDEVV73U8yMT{T}-Ux1hAMHUC}o}RNhn}UUDp%0TY)dNkiu9yqB-p zMdv8;29*vL_iO{_qt9J$P^PT0YCs9|^o9nHMt-kbBfXF_+*{j?O%-F26Hw*f2f1g- zHE`#cK7onr=RP&uauDWlof`fG_O*o7TC3qLLy&`I^f@EMzzzLkpi3miba%zF;->f8 z^!XbgknzQaLsTBr$@R^gd)|_+(3+S)sm?Tx1@q2dP>kP6%xkPy{#xbgGiif%^Rj7v zYAAjj>Y-^_u&D|SJ+JUKt@Ve1&U*e7^D<1}vS%GjZb90Fv=Zq}q#a0K`B-xGdX~7C z#f#ZX);aVoOP0_U64nNlk;YrJMu82gVSCS~z>gn$KMs*>hLR(VJXn2O>K#4Y`&u{2 z=1d(xv=uh9%yZ0ZltIyzd=76|(>Ea|h8NFtZ;O?A)k90sGXDnA$@fs<@Hmtpdfl#@ zaPpKg-DU&bJcTVuO#OFun?|5_Z#MNWd}EJBSlvXi$-zXWXT}(FH1eJ(MU5Nx{ z->7mir8C5C*P6D*=Frx-X|Kdg7@D5p$|xb*%Da3^h8J^C42G8sgV{DYD#eYRdwmd- zVTZ;@t)bgsa8O{1rT14-K_e@~m1qPXdY%BGxwvVgQa~&i>)>u)F!*;O|3=v93t*Sn z>XTj@o8`R#=gn_{^vRu}D~(k)YN=s=oiTeoR&02gSzL$tUL;DGlCwI^32GZP0SP#H z_d9f5X{piT{Mfph#Up;3K(c8WTEg?rVlhw5c5k8U=BDc=ikeP6bHr0u#KD58^UD9} z`94(HTnt^Q47fyH5+IT(z~1bXVl?p@E1&OUg^!!KdJs+K7PmgX>HcVfRvz&I5Coffp!`DwmCM z0IFsaX2a~MTOl^p02C@Bj0x)G?%s4LLK=CC3O!Tb9&&g-6lc}Eoxyr)-pqJCllq65 zY-*QEwDIySQCfAkqct$iYxUEq)&W*dJ{n=E!tl6GKHjTuI=J?}rZ^YHN+x^W1p?m9 z1)UdQO|XPx3QcRwW5>zXzWt#Jj9i+KH#A)hFIS*T`Lz#NglQ|N-ADl{EQptG$txa4 zWW7c!yZV@%4fdO~E8BT(X5eJgQ0Z`<**m!SutEwTo)l2+_Assq#hjRL=iF18;&7Lu zy`d%FZgz68&Kyxz6jGF-P4659h$>n8E?oB_{l0E-F8E=xxa>JiB^U3X2v2C%5hZOr zz0x(TETT`55R@xB>I@!O?EXq~B%gJN7dkN<&S~S7SfNUwzE=KsFA8t$M~rzRnH;T^ z?;F{N;_M2X#F{UB)NCwq0X zVNh7bn7MA!X%)@w3}c1mB2n`pqqViX@L7GR53ZdIhD z@j2d46c9Dg7rLq7&p2QKMZa2wx6Ra_^U{IJW*w+PBDE!9uHgbl<;{?FsEztSlLgl| zlsbP|lM@PP(usN@&Ah{oJ5hVyzXR{f7@w>OkA0Z5bK|d0mBK=4z`y z(M7V;#bh-0GzG{s)))z8&TnL-Q->{4hG9_LCx)*0TvV zlytYYa|0+wVFbiGXVQ(zIi<^YUCFfK>&kWVU695N#_14aon7nUYqWD8LjZnHs8-VL z)Xk!i52(^j6K>@-^5d%1G@;P@07^Z;J*N0n&?Hx`|P(w{OCsna&pdk{+_^t?mGf{8l$x*m;ACstkTD)n#%Yen65)NC;ubuQriA4mfF-4uiK!jyqLjT;+8T)VY4s;b z?EV4dLUW`#(Ff53|IJ?BpN-siqD{K86Wuz)NlHQg3hV+6eG2r`L~@E z>3c|L?|j$y1VHoUK%h4sS2o}phii4tapj4$W<_7&XlJsY-uI>wkKEd{eY0erCZU_VWP#3W)l#`*I6Nwb z=z5IK7F{QZeqy59N(#nIl~RrDi2BC!E%QNlD)!RyYIt(r7Z96%>!WpsqKZYV7~>x8v1DcufRAwzPN7-CEkQ|gvt@ zvC@Ago4q4lBnLcnmKxrr4nymUx~V%`BNs*w`)O!U^97}9TlXvBe|A3}PMR>7;zhEdWpQ|zo>k-nPpCs4PLDh;1HDQi*pQ?88F1cvttdwQiBPEen4 zS%eHxBOgYv*VGA2MEcuQJ1g``r>Ev5|Jd^qe5bc#I~a@^DE*Bz`05eNm(sYaOPJTC z2d*}=6%ncaYMZ{UhpB~H)*~IidOEL_`&22l;Ho%{+!|#;?(9-&!RTwXfQo`iL8e@t zvN!!btlfr0A*SF{QcKtIc3dlvKMqMTNv{=*EX(N8$TNDhm`OD}EW!v{m~=R~T+N4_ z+}m|ww+*wD59?6>_1uqi*6zowIbqxP#lUFoKJg<1tCO)uO1;Lxb8>rBy7`*7y%a$x z-2@@5>KC|&l%}kX-=9``Oc^(%4g2wZT&b(>m|`>O^SA&NqG^E0Ae119hfQ`WKiS^L z{rgH<7QOxqH%3}kdx(}?DaRA)Jh+0ORGzxJkz~&j$f|19^Ks%^8_xI^xh`v_= z$n%aXwuyRc8a#?ug!SuULf_$RlIsalvClgo_l>(E_f3Xafcw!gHlXP_d8Ogsn@jye zbGfnRNUlH|n}m93Ck=8W8AoMEjXa^x24Dn5Bj?4y8v6&ZhP&c}c4_A{lQ+eMeU*~o zPIeK~aJRt~OTf4rdy}X%AxC@ZkcKE!1184XLg-`Mb|{+kdMI=Q&?UzCFtb;70!1;3 zRus(&8X4Ku&6n}BcR9%Mg610(ep+3T68b&^0a{1~FMKvxt8ajDfiYzZ!z=s3C%|W$ zFBt57;TI1Xng^kgQCKp@{COK|ULoj8eCB1;l2oXXuj{ikL05DTvxwlU@1p!(NKKtA zLSy0u8XQxh@QIodJ8VvtK3E!HjWEZ=$n>Jh<`$VHET$W4;4@Jip?lfT$a#2(HEQ@R z)#%Zb@HS2k^$^QTEoRt`w=bc*zA%o)ef+-s8Y(=?EOS}F2DIvAxhWoRA;S1%+k zM5_|{a@2?c(m|==&s!)ZGBkVs#uCm|?_IV?#|Bw!mE2}3(}QAi|9b~X=m2U5lk7%J z%L&>zZHUO{-UV7Xk1L5_!-N-zq(aNj6{iX_92H^*XNDBqT^51s*0qXlNKrq~F z9DpbSh!_hSWYEm{LzPAU0U*@5G^K>{1f9TS&$&Qm%#3MS{Zc}H(}WURvCI9by9QF^ zojs3?WURs|W`YjR3<_G-WDZ^~#yUNql|e;>ia%%v%xIJ;1e0U*BU_D-e~49v&D-Z> zfm(VqwD8b~{Wkc&#`K<0(x)tKIswh3)DPKsBJL(0Rnj-AVWWB!W(vH|L7IGw`{87N zhfIK(a4tB@PgM=-6Eh)HmX(WoPQ&!j-ve*H7_wtlCfV>)jAQ_!=^JVE*FTg3zqYt9 zJT%G|_l7@;BEOkNp7n%m@YRoaR=Q`#7ze#&T?uZ+oQfy{(o%^gNO5J6JBA)iHZ+A| zErNpvcf%N=7~NXgqPX1@fk?}0l&#E`PS40=2b(30{l<*J=$uaVAYmvd&nb1Hpo=lC z%RSQl_Pm9|`!w=FRR{XJ8!^y6=rlw=u2Q>u0`}&U>Tj9X70N+vF(V&~Z60~uN_jK? z8+Fp-x4*;wE)?fr=u*QGkClav_V-8;`$+cdUTOHutf9}v>MB)gc&S>^^w7z>$1=A35mf2VlG;FBueG9Q^DsI)^I>1a(kcTb!33^@qN^uCWDOWd zWz$YkBY)q=3kwjor!ApRAN6Gm8k5%87k)ef@`8oou`n+^#}c`*P#h{Wh}mlxxNjcc zJFDF>gjntlybW(XAEcd4#!}wcx<}YAd9D&f(tkqPd&tOqLl2 zAnL9SzTMg2y_!0axMgu1f>p=BcQqquDy1Q-XLWfe8pLZ6ysnWS>>Fy!46uJ|7#LC^ zY_r49Hi%3%efwhHJE8rZhgWI>`w<$D*20)s^N|o|eh-+~;5Zd(5U;^B>-xw<%hzXg zd*$XU&6Z)#PGU|uLj&ftv-eA^=0@^!2!2OI%+kPE>3nIS{uPRM2F5Nu?kbDF^3aei zeeL?wsGS4ve2FpaD&M$ws4z-+Ua>duKqa4aZP{&X0AfVh0(M74x?DEbTZxjiwLaBL z8c^HTnBqz)I}UsOi8hipH{IF6b1CUb#ep_raCQ=Ie#X)c0@qgrPkVGUE?Z4K*l5E-QP#RypOj7q>wkO4TkEJ81AS*so zTbDk}RoF?GBd~gb)(#joQm_Aa8(F1>yQ4=k7kBXdhj^P*5^N*JW9fw*guI8l#8M0a z5i$^2$LU+ru|(mmISn(^}6x!LSn*^*{nDf@L)nlo<_ecCwhFZ^5RM|z~tC=I*8z*eM5xi_rIo1#*s zl9ykKR`}_4e{`&<>+m5??66{(LR0_yKveqjhJq2tfeah;6kNVI5yjsGVR)s*SV=+M zAmz+AvCUDbX#QMQic0nKpJX^G??!X{`+XB*oxIy8-E^ZpPE7DKrKxUxGtDzJQXYF;;hjr#I*70vDtURiYVePY#S86#IikF) z26>CIq8Tn?q)|lfSnd`67rGq>#>ye&cP&zm%6*3nA;Xt}KO}e~b zs`p+@hoRqI4r9%K5ZO3SK|bQGuTnjRm({uzjlKL@Y)DMjyQ811FlT0tJgZ0aVmflfj`8zyEF6lY5pOyC_5|;k5uu)EpX;4vx74&}2yP9hnR63}5 zBC5Qk^jjg`QR$HZY6_KS_J(hIr#~RE4r^usKz~BI3u!7+HBv!iybEUt?2HY+6@Pb- zQw7a#(dOsYo`tH*%QaE`A(HfHu1VYh?twDGCO8F^X6RJO~Jhr9bDJ>k@DW2v;s4CAu2PP?~qK+gY9NIHA- zRps4%Hz>C0x8Cuu(22Hq^W1ihF^U{G>w}7I^2Z^4NxF@Zhjzn~4JS4+=4dgVKP2NI zVmt`1U}fXb>xa%BR#GSZd0{@&B-Jh)<*f##`54PeCJ|$>hb4z|iP75#{t_-$$GfT! zdOE&r{OqM3XBW9|>Ec$n1hZjnIsc&ffu#@Zcwhruy-Sukx>~cBEZG3I3M#JjhHs02 zI_2+Ja(+o~czJhk_+(^c@yIeFf7*vR?~M%^?We2Iuf5?RDkJ}0!77*id2zZ+BazXJ zc~}W~hPtpmZ{y~H0nSfzAwX5Wq0hWVD^Ty6Q82I4$Ria>^zTszmC}vc8`ef{i0MXO zgy>*sR3x2<0~ov!S2{>K5gk}qUp_God6UHgOIsJEC4SbIHn}S##^H!SoaA4W=JoYa z&(FrsXbFg*1DY4b+%G{oBEhW5moe4UCqygV2YV@Sc%2c!(kay+h_n5p-B$64yUN4l zS1l$+YI00&#iD7s;Ar}Ijnt5U4UP;(a!5)ZT^qy0U>)sPViHwW=RifmG3oL4n=gdJGRJc-3A{xrqpJFC5 zxkuYrQ&JEmZD&@dG;M#RN~wnIWLgYcuYpG-cy7~?wo%!%9x#4k)h}cRtwZQq&zLUxZ_x1j~HBTcr z{w)U|V5YHwMv;vFA&BL8C#yIc;Tc3ufL=<$If5=e2w_YEf*;MJCWj5i1~K5TjZ_$^&rggZ9kt&ceG>)D1suItv)N~rO#I(f! zSI5|3o~Nye&exf{o^&ul%#hcJqYR5N@qkjI6N1s6UAZ5+3@>l&9Vhj%eeb8v2b zi_^`RjHTGu6MBJ{7@ZNAZEK#>&avSDjY`E@BXFIrS2{@1uR#W+z&=u;hc# z1ToWP%Lshtmojh50z~mDunp1SwYEDKQ{p}b4mC)@*{{2;{%hcFqRIx5haI9TtTYcG zS~(8auA^rTF|AXPu0{_7d-epP>t`QiO?>BDgonk0SXKr2&Rq#7lwB3K%z7w5#y=Q1 zIk4tdFJ1I_y6DzbAGDFzKTz}yl==|4CCZpVUa;C}4b(<(w)-1|-8C4kA`NPP1}H`Y z+D|BNAyknC_;1-duzEcFZVcDqUM|v|Zb2l+?|^lPr?lYetn);>yF=Wr%R~f-7m_Nf zcer=@S&^7({Z8?Dhcn3Gd|v^G32&rEdIanTKSrDjG20dWG^(=EZfQN*?O6?b0%NvS z9i&}80wCYAoz`kk;vJzm{UnOO=11;gCBE|&wnRwA`tYdh7UuGkl{}i4MK+C52Yd=m^R4ElX{DR$rnI2lg-l2OA2P!*>Dmh$jhJg z_+XSjyzeUtgYL*BADeaZqF?b7v)TO>rt7|@kJ`o-Qej!_IS)%5&z*PYi#yyF8a)8A znB~T)2eX=4S&~C zIhYt_gu)<3!FaT7EE)VfMox~lVs6j!hcf*;HqqtXv5pD=*Y_MnjDH-s4TJLnq-As?;)||8yrG{@zb7Wk zBa_mC^V(UBHb~*0u2P#K-N`d$J@cNMd3N0rzQh31@o6t(l);<8pIWR@m=C?Nq5OMb zC>!_U3{~wgVFcYwRsGG=I5bY;7hk}=c8e>l~}%giicZadHEAi)e3G6MY#({}X$v%VKNdHc19 zY>fjxiU=&i{KuT;RGj;%2R=Z`USWWX=cz=S?Cs|=PTi!9dq)Y`GEQN2F5nKg4bAsX zGSpYCBclf+cLtmE?7I7S^Wiq_e2`8zx9WrYh=~|yMIJ$H(t4bIh{ zYfPsqK~H$D(of%Ywxewsb%X1TvW`%eV7v=-9_(O)Zy_rUs%NC}8Hx2o%7eyE#m%0u z(4#@j!41B8k@GKFx%4hLZDQ(vzhr^%fK>nu;Ec*Y+?J7l7|gZ)cOJ zh ziEoG*YmRC8;2A0Xt|S;~JQ2DE{gFFr_$R`9~O;Bpbt*t{(F3inUaDRGqdKcRK^L2`=? zW==cdK(dPH8C;!EEojjCM>z8`lj`-41BwBe0LR=KGF)dAJ)sV%j3~VuRx%0XFddY4 zWCYK)j5V;1)jDLJ%~*xa#U8<7yQG1$i{K)$DZVR)yJFaaQ&qD^u1UvtC@4jx z(omn0?BY5e)2;B+^7E82v8YxK!eL@bws3wJEwf0W?}*oY(*-i2=mME|B_Ia8J_bMS zgm&=3VU{RV${fI%wCjhs8pU*Rr-P^160_8xPIiWS(wd}LpyE8(9%7Bd&qaB~D!6ip zr9EI>b5al>JE-Uh%nW_j`=jBd2P%uU z;dy^-h4HPx?)wg%j>c=AQx4ZcQ8a^WsHYXap%p3w+5RsCXndmZ_OY#~l)4fk z;@k{y!ikGJaI7tJX-6E#Ei6i$aAuU~bdjh!wz)^rR`FrPV=@t2N7 zFhkbtq&tSoau)*EYgF$BM^Z2u216!4Vc>HT^spua{hX6eM~R7(rz%ulu)sWkJ^fUD z#)^)?Y{e`9I%!XUh`T~$^aQvNMJyMc9Xex5|0W-p3szjRUk%S!hq)Gk80!Pa+SrKz z?f$dFXLe}3uvODXm|{k6a8OMP(9KT_Q1=3L_cF{A4!(uTRjDz(LuppS1u7>$fp{SV z*QiuaA3`o*32Dc@SUxPmfSDZudLli4nVGS`v0x@;espipGL$MzynfQ`bBg!l+DYX$ zH!&v)R`v7er^GqsEwE2C`9v4;sCzhJTH&l!?>k$26xr>$ta* zum@Ta1m~>b!iE+n2uykV`85Up^iF*U`+j0r(rrdmX4*hIk6a@z&f(iiw_J< zbYtz}5!tI(!$%M&eK0_qjd%Vt2=9+6-aW_7dJ5c!p@2~7u$C$f8>+z;5tP&L14d3D z_|U1ROF&2WN2j?OemTmQ9%~~L52w!rme3ZcnUe**+1z1#~API051+2H9qqf{b+nhWy$b))igqv5B|>FsJy1avt%orjME~A zGaiFY;E&92UN!KMo9F8)+RrC1Nd*s%g}YG;Le&vqKtmuB)#y^D5hC@_o318GZz~U zF{VgZIVg2LlIWVI=LG#I%3yT!PdIx<(HNb9J-2+bWpnv&>OGqs@U`+| z1KM$UIQ9lpelDMDxXM77{N;$`$KvC#FPlwK=~dWwfk@_jsKw4EsM&WG!tKtY<9(3v zJ7FmKiyLPzy#lS)9-_Zsz~qO4JD#)-+-)RhLow7J0EOSo0|a)QKERAij745hzb>t> zZ*^{Frq%CrR+*bjLL|Hv9T}R9f*dMntoE;?u@hG;FPrh*2i553 z1${FdX?~aJbeD<5tQTDF?(mUr$^WR4U3@?~{-|Rjhn}}r&&^;8xBG^>D~qbCS&^Am z<(rqqxeco;i>_!;ysa<%dP_81ER9)PzA#C1IaI5xAcrsc09gU3LNt5^$2ZjQ`)Y&7 z-h_}0jd28hiiW>|eYl{R9&$OW8+wA3@zEX2FOZab!ee8H-r9qVkdsZ%H?Z=zs`TaB z4EFV?)Uh^0SkR}%!6yb$1{z^+(~gxcKix+JAJNeWJN@(x7maVF*GKvq?xHp;dLQY7 zAw1UmU(kC^RC0MQFfU1yeK#{{(v!Xyy_K-44?#0&J$AnxVPl!1*`Ftvz^f|lH2kQb z(Pu&=2b!wsrExLzAQX|e!fa+hm{NE61UyZ2Q4{!XtUgMv#AX19YPcOHacV$&(6yW&oCse5sU0> zXU%UaSJ~T7o*^p3QIHo({(ixrkL=LWJb)!zyDOy{d!Ws2- zL>kn26&JUakq1ZOo~`vZHKU=OXqdv2aH$$mKv~X_PAT;8$nBMr+qIm&uL&RaYK01z-VuZ% z-TBxYd`;)TW6v@b(%i=v@!jD!dL;kjS9yS<0!vRs? zBUraS0AKkGPJ%=P@eON*xS{=4jBCmfe+wZ5dmuD@xbQv=O=lL%UW8%q^h`nv>tiic zO4*wxO!xxvOcnmmfjRRKNJp9H{lMwAAT#+q20NX6AEE&rK5Wrpc=wUy+mt(YE=sfq z$u?_@C)tr-64M#J3zENC(aJ=D=FN}-n=-vuH*S82NSmuE+4a%GC|7?O|vc?#fgtUmd(eZ9hS>?=OSDH zUpYl7J)+tdf#fL7@gQJ6e1zs)%lK?inuN89!p&NZa8=L@$@^bIS`zoEhQA5vJ;uP8k}YQ z-Xl24hWM4It$y~6)HW?)_jZSq-+>_c|MHG|M|y9wdw69LJ=j`dOlgu}?XN6ij5Oy+ zn_imx#5LZ6MozGcnVwt3UE+?1spEN@GTuhr%sU?Nh%^1>HTg{t10(58BCvB(w{aB} zV+tBmh0@i$XO?L681wi*cHYnc-3uwL|EkvIMS{!yXT=QB;oe@&uB(5j>RvVckeV&% z5vh)&u_|rh(`sVRDV_t&ybW->pe=nE8ErMV^n(lQQ!EvKdX!q-J2;gj8(&T7&Z9?{cDcctyukW{oK z+gpb!KeZ4KXQFqZ7JTH+la5auFb~q_b1IcG7?T=4(?au^Sg8}Z&x{=`t;?rM{7l5Q zLXl4)dHqNCDvPKB8-T-#em0KeKnIOKg@c@o{5}{U*?-6}GN7%QmidBXr?+J!q}4=Q z6oN=x2Si3;lCS`J(I~}QGr5ph&%+&2K>A1VnGy-#j;|ohY&eCZ=Tv8`Y^+eKr!Nvm z;)5v$4oxE>L=9_H!+s`qN7M^V^OdqhT0a{83s8Tj-&td~T3YO8=lXolB5`g#&V^oQ z;~rpQLhV)1l758LL799ceAPRZ&6SO=)h267@bhZUV;4tybmJ{8y3x7l<`|+HjSvYY zj-cS*0uL9Y&77W)wDie^adlijEYCAb?#AKZE$7eII z(FURrjA*mF(JVcW9SQG^eEDDcsqK6YYj?MjVADh5+|0iZl9RxUcNTiQDbPO)GrUnM{#OM1&x1z8yYO8Sh@t}l``O+U|H~^HZXY60jb(n*`L)D) zEqqrChzaLl<(U+L% zAQDf)S{@TWNbrey`)ie{#_f(+1pR;w09~N|jum>r+7t~>i~jv~YEu}|8VRRFA`m{u zpgiXd@|eEf@UIBlx?;>sYpzwUi#3)|<80Dny1Q`-kZuqMqEaiH8?dPc_nILRgfSNu zauJQu&c_7J%QF2mr!PF74&(m-#3I}WK*D{0zuoLz+C` z;SUrU7z`%Sw{rl-eFGqtsc%v?Qa)^?Phhr|e@#Ujt!KUfB|ZV2ZQ6`!rKj?0S1j1i z);K+dHSdY9WPFlwd&a|=+Z;^2r6mno+Rei$Mt#-ynSYXZbm6t$Ch9uvkx#(Gkdc&K0bXSpxvCc%W7+%AlW(?Y9be8GO}LT0yM81u|+D)c7*aAll@3 z#^H=#LyVW?b^=c+vH1l~(~jVRkv`^phS0XDts@C>9V5(pjm2>CDVkaU0nf z>D}8K@ZSK7kpZVemae5M?8cJ)N%?(+moD4UM88+CJVt^MM1}X;mH1XJR=*a&e-r#R zl%@=tPOLu%DH`^-T-EEf_|5pP6!l8<;T%SPh1I>q{#SrTlbj)B$UtJ$9I|pQ5wi0LET}pOV*4RqTK!TE92E2VZzT{{TgAdS)JH8bB+O z55&}+hn4VK(ZwE(P~w-Go*v1rd`CL{bQWWjetUYnDIsw55;0s3rOA3$7f9_oeA}-9 zgfzi9$Qav02H}EammVxU{Agv-d-qos;Y(#61A0EBx|{TCab%ez+Ts>NzC$q^pfbq; z>{uG$pGbonLF#+M>CKG%OA{l1+9YYV{fViP3b)-nYHJTg@SV_lTAPY-2dA!Z0b6vF zGR#)343qw~ZL~KDYV=(!rByGfUxk(;(C(WPG>67os^Pz>$3l-O{)!(%g*W1`6%iWYW2aI`<`7X= zupwWN-Cpu=vSmkOYkG5=Z)QuIFRA4ZzGUboQt91eEu)*G)48^N;mr1X`(#?ivmr%U zgw0t{I>$G(KRmY+rPq3ud+M=o9kozCtv7v7y1S`QsrU0^?Wr*}_g2B92<7-aA85}` zeB|CqFVz6s0xX0NAaARePd~J%Btq}s7E8N6OArPZ{b8bIc;}o1(JS8aABQYD;&J@6 zT&@Z;vA4;c#sBfp!sIW;4`3Uc+>(XEO^A?DwE3)Z&*roI-wtI=kB5hgXMNpbsNP(D z@6h+v?~SkW!pVD(mv>~u>>Y0j*Y_*#=qM6e{rp77e8KML|IzV~VDR&=Ix^-ghgC!o zoCOeDZ#wrAYy(EOg_@hoa}VcfV#lw0MKnHsoYxhMOzIj%nPWc1Gh1HFXW z%gsl&N8SwhrSFgPPmav@B=j;e@C*x%p=Q{lJ=gLNv3j4>K6crXs;@Q6(_y%<&x!OT z$j=G4Z!Fb(*#g0dNm-aWtQ2a^s#L$UHpqHKgM8|Vg_o*8i#2G+RvdF-ManGkdOB1| z#1f6Jn=x6z2|bJB@o2^&fzXcZ1g|FcLH1Uwi4i~*aUTSM6DMa%$hG>RAR;K020El_ zs=_h4MQ;Yh-jLJFqf}XxVxfAltBzj65LnMBl05@L{xRIoTFWSvj=Gv5nhq9>4te6C zo6!P+!XM=JOjg#lH35}H4jhKc6jes#+CC`L*U`Un^Jcl&uVI-ft4rXJV47^?rK2Tg z5L+VLR``SkOo^q=-I`K|;bO_c`=LF!1bkG~Z`zuZH$jB$69itsG!Y|-F%d@+Un!9pjp(JfyKLzH z;uBx0Bc%ao3Z_S%WWzaBf_WX;_?a;kFdom)N~w*?xjCf$HC#58sQPh!qQ!|oNiV%F z25ihD@yya&0(lF#*r)qid)O_eq(>X5mLJV7serW)Q z*6_>%Qqrux(zDWV+H%LBSbo|_dLp!})SeSFP)#dol#NUFF(0@6GJXCioRVn!4nPg^ zdAOVkNRQym3Y$Ra@4#Gw!Lp+$8NO$^{g<;n^!y5-FvX*k0~O|IaG}3ITSD7M9RC2R z%8Jpypz$ntE6s0^rfT%#l}Q8t0#$;b^kI-?kRm}qS_wr0s1@k0g0uT!bg~NauYd#v zZwzT)>nmQh7?3IgEoT*JaHi2B7!+n4U@Csto5m|n&+)`vR~U@uY!cu>afrm%*M~G_ z0lY*g*BFL|(h$>>3x)-9nhrbX95|XJonMHupbWx_pu|Vwx}v74C>U8%hlI3x6gXyr zbS6Cot$jWi5{G=7f@On*2%ZiTOqB?+REBMEoh;DO_VMx4`5rJD9OlOsC^5UqTVa7$ zyI6dfbfsS%iz!e*i0bPrGSjw$+Cdr_MTR>#BE03T@f};!MhZaQNkvJPDJ!2rjF;{@ zZXoTHfV6XLLNSQOh+jJRb}=|^cszOJ0magfLfKQ9sw!4+d7-&2T6i{9*@vGbCXEsE;2>7+ z-W(P8(WYc5q-Tw#@1D>Z_e+&H<_@)V^#!V$s@eM-?mq0UNVz*a(ILO|#06gZX2t{ZIKQM0o2`cK#4qXYar>KP9&G4QX4Z5Cto>SH z=0P7$iIH0r<5Ht7YV_K>eeeJ~(Q9(21*AXXT;mw}7?$h7P)kWPgO-LjJSsC2kCzGZ zLKhp~+%asMgVGCFSkiOxKvTmn9Uag0m=*B-O>)sl{dE0HAw6fPdC6K{=AV>B4rF6jpMfAd;#7d?*(R zg6eYtN#&!P2}sL&uwKLv`?=3P%t|YIy!*=%-iP3`pj(!&layUK@O)X*2xH1mK%#4z zt?q-rx%WF}k6{Vw8`@ts`oY(7k)PaE_XVWuy?kN!L#lvu?Ji%_U4h6kSo(Koc?7&u zll1GqR0ub2YfjPIMibICgF@7deK>+iZ#Cq~dTOM#N_XMMIl*c32)u|Wzf>{xM{Q;5 zK}S@@h1T-Z2D(>xNzeLbaMPF3%FhPgxt*+EKs@wR$OofeD#WJ#L>?5@})eA_Ky&4>c1f0@+z^(W30)~pW}Xzt)RV&M@c#l_zwQXp6?TTM)$YlBZDBey zuBC8?M&Yr>&~+Ri$3XC~_uv?&1`^l?m`jwwumNj%u^J}L6g~j1HjJwPt^&A^!?;o4 z%)sf8ZxwL+fcpyeq!bh|x7Z!V{Q$UwQ*ft{??ZS-K8SoPqCHUEgtbs=TPR}}kSo+7 z;cxfec{uz)}2%heNo>+67iW8j7EQn{x{H zubyuw;Q!wvA-VqBw+O$3?ty~-kN8i+k7=R-o{7l+)73NC72k0OAHVp&blrE*|4%*Q z#$c|#BCs6v>Ptri77f^l$0Ln+1XhX2+*bs)9!Mi#7ho?D0AmX0@KAV!3@~OZ~;c?aaIX(r0|DGWLu6zF^uycsJ=?{hYTa$&QApSB4 za&Y}EJhv6$sw^JAj&}~r!N+N4rX(gO%?h$|#Pb*C=0!!svRcH`XQifPWX@zJ#Few> z&3#A+vK@%4Vk6brvx96O;_8PZXJ@1ZS$ilw+oPTx7x~ZllaM$wX_h!WDgupB$W6kJ zL0~k8<)$Idj%wupp|L)VF&&ft%j7!z2PM&~C_y(Jj3tEc#|)z+#$qC=tZqy{3VUZL zJ#4=0nVg9)U<#$dL;0s+1~PY0U8-Z3A!+2x`O!NKM8F6KG0)u zZUfWSx6 zfav`+N{11YTuCR`@fpBn!25uk038|tNMQRAIRNMeP^*R$`0buM9x=;ebD11Aqg8CQ z8l7e_M_jwUraC)c!>S)dm>fo@sX=tQ>+4NUXQSI{-IXssZWm2E!}%#+xR7R}Q^P_T zmWejI%_JIa&SsMX`Lk?x$9B|aw~0ovF5Yf*G>FX>m)Y)iA$jO4Vr?N?uwa2$X1BX6wk_f&`7?#$ zn)PCx+hT1HcP!3dl3$SLs4vW0R8*L^W$EI&rG-n2OeOUti$rA9oW1eZOUmVy>-A#2 z-Hi-3J6dcK9VVBu^ozTWM~=hh21)P<~$y5mQ@v_B)p+*ohj z#;o+qo&8Ha8%&Pv7Ca`l%4n4v>D=XXnYL@#M%y-dQ+3jVz;3kavt35{ZiO^7y4JU<31a$_Gb1d z2ATBD;uL6Tm$vd#7Z=3P3W!}&&{*fGm-6D; zz(@WPo~nyAJpR(-y#J#5dq2f8f`D=oF$H-MH6`mwWW5&7s=c2za|PxY0cDxUdiy`91gEwj)l3Od}kqQ@M|U#&M5V3Sphu3RGI+FDSl*<#sEBlmO*oe zTJ*>l0=tfaeAP_MMd!BwNib0{UCr`Cn9+%Do|@bfg@=)}XyOez74_P2fj#qvz-Hc> zdF#PXrjAs7=-?w))8;f@t9eu*B(m@LnkvtKC;xjJ>HDhR`iAO66~B2Vnm_$cN*U2m ofpI_(Oms2P{pOj}zWZNYZ7{lO9*rQEka%0*gJrlr2`pp(1)|l&lmGw# delta 33108 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 D!bL)e diff --git a/formatter/SoftDevice_v7_formatter.uf2 b/formatter/SoftDevice_v7_formatter.uf2 index 3f0b4d829b0ab4a0aa8228da13a0e6eda9fa26d7..251ffcb96060dbacba3aca7f8dd168929c0f4c4c 100644 GIT binary patch delta 35634 zcmb@vd3;k<`UiaOlBF9>H|Pf3GzGc<1`1jQB_wcZ8?YcQh&l$`(!xj;#zAFFpr8ds ztCtySt4*y^MbCa~a@Ao7v?eFt>|9U^Kd`^?x zdzR-s&vTyhZ0G3i)wtc-+H_YNA;gmRxA)xnMEJdnB@4c1$({HdUQp%#{P(wk_w?cG z>!WouHn8OXLvj6oY4rbNo0zLmOGf6L6k^6)#fU*+3X>FO22+sNT5k<_y_r?>%uSp2 zB#7MGL_QYg#bOUD=8K6XT6v}_h?+g-7Oo+oWpdTPK9zWwX+-``(TM3DLw8Cdo=;0gi(mWqds0h`0Hw1( z1;73M^yv@%7{fxxs8h>jx9yIcBTDu-Mj&$R?V-`laj0^lKZ24llPo3haW}Mq*pyNH z{#d;HcYjPt!2j(8j(h>Ie@5v=i4{2V4N5P{1eE(x-V~&Q*oll^x+8W*!RNuouzspG z%oxwb**a(2Ua^*U`lkG$i(&Y4Mu8Z$^4i{Ze~IY#OCQDNr4~2E&nKp%O7XQvm6<4H zitV={ptNy$gFtcIQDxe-8c_|rr51ZmD#hbZqCpmG>rB3{&{5nXwZ~c{AwJV3m%m!_ z+Je{SpI6qsv1yN1PUtU}iT-V|>xtDKU17Xe-_7U`bn6o}-SL_p-(N7(-o90GGD-X{ z-?2Mb4$#%UZ%hzYu0mJ>$zvkPAKG|sTQlu zu}2g#fiXR6X;bvN>BMOH?JG-k61ve>nN2I73X}1e;R9+QMP)GOIS=61TPV{}mYE!C zAVaNbegZIjL?P1dd_v7*ylP;LnsV(f?<$!mpSA-U+cAIP<42X;=_~LzZ~D9VYo6YP zztg4{J^@TlUxB|fr*ji*m)=b|>Qagyq}@~Osez9E;^zA>z;lrS=zw4Pqf~h&Y`gSV z8=4QReY~IK6?=i}ktJi=_V(qKcP8ba*vzNT?PY{Ii#wLo9^bpL2H4_=+sO*l!X@oqo&vfg9Q)_5nN zGp6sfe|*VFe|gUnkKS*}E%_jPshbTaE#Diyd`T-`>Qw{l`{PYqSTEIqVw{f%nmsKS zkj`Ob`t@#OM@bUGm=**0fb&%>^QFXC$NOgjlgFRPvG5dmdxa zINeb-&{*HZi@#n@suoPumy>Hac@|_OBR`?g;U4YP%3t^O`nO`hNgeV$&9l6iryC{4 zo?`OjTj$CK&0JZ~jH`a;a#9VD^$eS@k#(9_i(Km#FyxcHQEo{XUC>6NVo8&C|7m8& zVnvWiYX>07TFa7u|B)pxqP&aJ@<-{xu@5OqLA$d;kY*}E8)%Umag|R-PtYbFs zHCEHUXY?98(RWlbd@L?*H&tLQ*P5O+$vVy^YdKRjeItsZ^uOeyTvG%{Qc&zW&`lEO z`I_;U^%j;2;<0&{{xfv?L%eN>y9#k~4ee6THS9H2pV8iQ$|M`uoN5m9e6Yg!ESc;+ zD^;cDj{Y>#FB|FioR{RZ`d@dF{3G53{B4swsS|R)3%~2H*SfA0yl;|)fi5QcpKPb( z;2cD#nx|w-{i!*LPg_|pQsOi?F|91AL?_=AJl4#LDWxMy7?Vy;3e`3< zCL%E+Ylh$F;NVx6dwukYnVA}{Qf+6?LT^I_7e2OH>{xDu|67?U%#sGx62*MNi5 zh@dOcm+4FOP4xLSy}A+|ezjcGTj=izACXox6;e^RC!dR8Ae1ts1kiW1;2o&~0P*4( zk5)b!I*Co*=%lm%I0T7|-@NHNXA*ult)T; zTKUaTOvzi_21YBt5i*K-V#~!U3on1tOj`eqiK-?pj$D(=ky$7Y=Sokf=PupQ-mgmYo?v0Dk3di8?4JV>Cv_;c;stJTGti`!mxr#!xXVNDR(`9^JEfJE zhKlfeX(%c8-OABA4W29tnM;OvW?~ab9R5n$8RTVa2}646TSK zH-+*_sD5)}h%!9mb({7rPs!!l4Y_CitK>9IHrk$z43Zxji?;cp)ZBfQ4O%*^5TZ;a zgwk@8?e&#kYuS_am9@IqQ+LHXGc>wWvw;yJ?~ztzPA=E9`N{uC`vzd*Wt6W_zC#J2 zB#q<96cj^K?tHEMeb9tHzYp%He3_YvG0p}B{5~5TBkZU=%g|AM!O_Den;y5M4OpWZ z4Sh9MNT0u>GK-@XKMPV$|14-k(>o(i4h7Tj#k!SmZl-m1)Ce%6?dGc;> z1fIMbl!WY5*Qb7v29PfUgioM6kMcIkF_d3WV#aesX_`1+3-PLzpAHtF>C=GzEI%6{ zJQbu{%TvKo=sZh9f%-!*7f=2WOc%xiguiN1@N7k7jw^zHU$LVyosYqj<-zQd>(Sq` z(ZP|P@*ZN_u{=XaL4!LZoh%O0mAg0?rFq7X`jJVoq)9{%WkISQmIbqfm@=u-*5k5C z%ZaJ5N@*D>eC*#**}z{yt?PnR3A`?tEp$cbGaa*;k3#A5JAAH1c^>5*6iOq=_}P{A zd!1XboqTp#uO;I)L9(+h*=5_;?JC#%SUH~47aDv}34+Veb!<>8Yl04Z(*(iwVl3S= zXRCoJhYY&;t??o=j!gWYm&)PGW40vdWHe@3CI#~Fo~V$JQBD@h@DRx1xJgY395gPieirKF_Cf4TB5avAN8DRa^8 zm?AZfnVg#MJGYlD91nnZDn=(=Xn$9vZ^z^?b0uc%sE0r5i$J`7G#LE&-l?91ZXL@Q z1t_SA{fX`MmB~G>a*Y>H83x+?g?>$YL*>3J?pe|aHL#(d6A$}o3u63{enJTT?1#)$ zl1kR&bBSWGCzU3ZjOtNbkRq(UbqM$|adQWO&j24RRQ}*!by?503~*yN=rI~f`%;ug zQMRG%KskcaPCxPfG>S%Q67mhb{S5Z?xDxkeJ@#)kB>f_ckEe!WTNV+<$b6zC!imJ zfw3EMGMAwp79Ve4TG8OFw~lG$?67ifSqkNZ)}!zA_6BFQHI_=`>KPvG!26|@IX%R> zba{+i4rx4CH??$&Qxe9fW+#?|FeW7}952Mv7Cd^V zkPOZ)1*S2bUGlKmN$bW>bG_IK8IcKorp zjjCc6nIWGkuPkJyex@`M=GypaUr`ayei>Q%yj=s^aofOb#|>kEy1dD*ovKh}Xd&2z z^-XbQ#FV^N4ZNlrx<@8zz4}t0QY#$3tjnpjl5_f&kCi&|vC?Q}%$F#;q79`JCRTp` z{0B-;Dz#?4^T=hLsm{7-54m*Etp+|;OPvI*neKQd-*;YFv;Mr|^wcO&ilL{k3)L-F zh=prdvS7*0ca}4?pk0hn{6b{vZ<%WvP}ES_5HHS$+Hf2L(C%Ncwq1G#oj;=zp{#w8 z5hhiOc0^PhFDx}3u9y+RVL-uTn_ z96Qy!_li4lI{iB~%^Taz#2Si@`uBDd*A3pi-R^sDqJLeNeDER&8o5BV$TbTsJ#GWv z9(`g|i*4sXwl6QcbQ3APmd%}fCb|XFsempdYF|XF$EtZYzd!n&SIxHX2EOM7*}X39U6SA@^^{xc2CP%bkuulf%wb(X_Q zqk);W!as-|DWPCUohX-8^PvF|v;@o?p)Jv4xwF7wToP;$SRHj(>-v^FFf?c9%hDav zTAR#rQdQMF`fLrPU`#31)b&kFx5EZ$d5!+~$KB)>_KG{Fuz)?om1yMuMC_Q>>xnQi zhDiUJSaiGVP%1P^)}EWgh6yAOX{En6JfXGc8ksO#lv$ber>r7(rb*_wJ>ko8)jayX zxJhT<@I-Ddj~eGHfWO>$N179oTIIuJAsh=o1<3HiY=t@MsLBEdaLOS$swjZk(29HUTpVl#(u~((6>{?8R#KK zPF3D`B3F8Ha)y23LAr6cRiV9Ni+#+B4YY3Ip9;22tjsE+VBeC-hPgSZ_SCYj%m%|^ z+m1&I|8(Keul%&0Nwh+p;^ccU3duVn`6rul)+qN?jN5R*(dnm9U;q4tVk$jGEg-qM zo4(WRI^Y$+DWZXFMUAx*T% z^EW=>(F29QS64rER>z%uRhM|G=&2RWf|x}0igJy-qP6U)TAlusyD;8oM$IK^arE5_ zFY1UKs3wILxu^O+y5v(uTl9qqzDv#&=Z@uzofno*5sBc^$P4X*rgxi?xA4t!VoH-8iANTFa(Q;jC@+Vu)>N;OH)+(sKYM2^8Eqr-Bh_3T$vf}6 zu(h$mPfUW%GfCa13{_v8dV8!MJ=}qj@9hl5vfG#r>>HDv5Rr6>4cKiEu8nJBQD?aKB62zY4O@nMj{#12KU28d|u;^Ev!5}h#e{6+9A<>{$h$ef>d1nHw)k5 z*$QH0NozdN#}+IZpy#b#rttNxKvsTv1X&cG%!rE4MyPi$(3|d=EykNQa&MUXv-%Us zzX5Gmn#(&SyJ_d(kEQ7^Z}x@C67@ysL6yE2bw zHUI|F9Uoh7y;IC<%@h}+mwUsfmH0geyNMK>8Rx2e|MN`xbY=K{|2c&x)y}LHDb42; zvZvEuDq;uCQ@oAcynczok0tq+y!y2y?^kc-mclJ>baC;O6<#3C;% zsqU?cfMPVCQ4Hptz~W9ZtL8l=#~fwD)sflG>bdQGtEhk5=|7|Bg{?}A`6GLZ<)boc z#Yxt!&_i$Y8H@>C4AIGZ!rU13w;SKrzilWyuXqphJ2wL(22%S_^|p3in?D4c=5{~*-%6OI3rcP3*CN@TN@5$)LuD=-TCGovsQX_z3k*^il< zDF}lr$Mt+vu8bKbSAdb)gZ?aq$yz9W)j>=e`Lz&~0+TO-^DjaO31NBvXZL-pNV{nj zDV){g+kIEfzAMD#`t8fzPucYnfy*`5d|@TuJzrlpSKmrZElQ3cdG7pEV!qy&@u%21 zz_jqHk}J5Fsi-RC{J!4Gc(a^cep-<-_lqFOixI|#vnnEEXZ5_c`&C5{fS8WoeO)XgAp*JRImIik%yW7SQm1$L%)#eldDA?mnxK#><>w$pG;&(+^*^qD>WpsWsqZ0>^9bE)KMAk(JnG388!L2j zvcia5E$#fNU4#09R?a){-PS44A2jmQVJe=hc8&jmw${jh4|A`kd-9+|Yz+V6CzEFN zEZ>zMnLz9aMUNLZ{Lol&m0@b53?mF<7_I`Fu^~z38COr-F0I@X*>Y#~EZD`M8`y0s z1*ZR9KUJtzwC7lI4DLxyqK4d}b6vT97sX%jU8=?`3U9-fOn>*hOSK10p#~DSz|dHs zk)I7~M5%U8x@#%EkjUPB4Vrv?kb9Y2kLaQCbNDC!(XR$t4|yD_TR+MTez9SbWXT;_CCrdWq$3iCDxhYHt_MaQw2Drb0TDB&3d5{#9;=4$l+s=}MK zg+F?Ag%?gUZy<6aZ+?*_e?oa1!GkfaE(~Vu_y;a}3d^znQ?kS%# zMz}{Sh^$WySfE^J4Dz17V}6puM1~knKNDf4d&Aozb>6C=wP>BMA=HX0s3Lp<79stt*Ije!v@+9dhOj=3WlfCc0ky<7 z`f50nzOtA#BCYgZGvywQ?CRBsYnvM@v~s*EJzbKM{kTe*>-P5xM4fTFpDJqIezsm~ zd=ZG2J}oJT`K&jibX18Rp44DTo@6X5%GoXFM+taaySV)bmZtAWKRK;!v=c zz9?>D#h6l!=*G~&eqJnT-lPU?x~gP%d%akp|bquO`q27 z9~_~oNlSx4>xG;Rpkc;*lXE>NaFx$$j`+x9PwOC*>)Pd<}7z?5| z8El@DT!Jp%X65(#S@Bpi*8ow>+~d#}4Nl*+ExICehpou!iyQ$^?kKBs4VjLZvPI5C zV0BXD#_-w_%IKVYB}5}(agN1~%YHg89Ut8Yh9NDegI{9Ta9Y&*8zC(eDuJ80)D?WA3-SR;uyXC#icd}?o1Cv8N zi}7Y&o}ki}yRGfs8Lq-K4<&;RuyXSAA(om)&*%ocqot!~wqZWh+1?m`{}@n|2*6KAZfn(OO$rw0puW{F{qWSy|uN-VzP1Yo*hwE&Y`SFM{P_3uvM((O?>V|8KQBF z9U;GLE@x45>7_qRK>$Ddh>|*qeo}f@&7;49e$uYB3~!^MjTT9d+>+*kp8;O6)JaOD zQWchvR&MM=W`PDZ z#i1`tahhx?4xQbNK6xW`omaI`;3?396CmG_cBP5aV(yTs4T`?#jz(1 zvR;?%BT#8xB5b$Y%3jHydR&<^O``Jk=_qXQF!$-C8*d#I%K}~LbCs5zNz(FL?`C#N zt+!4wYzgw1Ol>eP!eH+NU%D+dN`UgA9X`RHwKkJ~QPh%f-p83r-81RWF8+80Bojtos zhY`-+PrV}sHBV$*=&l$8%xR{=h;~RjGc+aG=Qy+07JB0CBq!aOgwF1!02wC;{eeSa zVKghNK^_q`R=z%@lkH);_DHJ+?RNpgOrD{M5i`7OUW|8VbE2IU)18PD2P4y~if@m% zC(I`~nN{)jSmaU^(N6CV5AvjxaLce=4S5~>bVCalfdgqkK^EWqemV*rjURxxPd(2t zB7=CUsg*!R>zg$8j4BO9s)t9spnJe4qD6fj3KcN=rxMDLh}6rNxDb(3oE$iYlPC6C z1oei%86^3rQ|Y$LcW-3)5>DO&kNjjf%M2~AVDNYhLcLe>?AnUW@L;7owA~vanJ!4( zOU4xl!ShOw`^M54B{!DqWNqJR#URXt>Tc`VfPlD#I|ilyW3fg_znZO(RUci61>nU& z_M>4vt-L{zN|yePM}Se0-dQ^5mdDV;W8Bk<+Z;n>O*Lty$auAZCGHF&?7{xyO(wz( z*sKVF=0gst7D9PEpsI|X>_J^ZzEut27bs`wKW8A3ZEGBnI=!5aWTsEBnLatW_+7+5M=$a??bBoOwq~T^l5ZD zvGCMCO01$Ci9{2rkXRpGi2FeBYR_IOi*l2D$fL17MmVBOgqWe_sxjtzC5r5cK+E8q z3GddFw4(AjD(wCa$iL}jB1(;2rOFK5aO%}Qt^O2=e7E2!HPbanOgV7%>GZ2lH>%CL z`6oj944 zP3{ThxzrX#ztGyrR&H9 zJ;X?@#BcgZ4s{l2ppv;K@vXN(TrS*0>#KojegA@m(iUmc?m{EWK%>J~8(r#;R=usS z**^qEMidO@^S!fWH=_X z2%eeVPv5`*4j=mI#n9_lnnIhdBu>f)gnvW!LKH$e{oDI>Vy%+y9_%9xZM(u2py9P} zGQRH%XAUfs>m%s;OFy~CSd4~q`y=Xu>0OJV?3@iQJbd3!P$;k4B@Ea6@GL|!yV|4vVci%r|gksZ(qH_?YA%6H>+--x)_+6Ve58J9YdAez%dfmHQbuW6t}`&X}@l=UtU>^K`aX zo$*H`4rWW$0II`avrDotK*@akpdx6W{ruIux|GJKlrWPaDIwK9I1QrsxFQ{U@TN?y z>`@p~wI$mIU)aLBAgL|Tk#1&vbGIZov$O9Cy4>TN!%JGsF4EvcWNz4T}ZVI*VCDpY(+@H|eMF>qthpA0oscWTKyMCDl2a|980D zE(w()^N#dTrAeC^B6*epwUbFsc_RutH5Ug-C)HL?)GzO4YGH!+N=GYi8uLoXXs6B! zBJXv>nIJGmk0qy*VZXrlo$ZpaGFA7E%0m2Gq!}y6nOPu;Vvb^wDpL{t;}OImAE@#w8Czu@IFfqR1&y{mL=UAHo_rrdJD!tASBLM%2Ph$jcc@5#m0s zo{PK9pk^OjkkQZcKT+z=QW?>P&{bIkw)W7;pBT6{JHu}99Mf=bXmfCtpO^K_3_Hoc z$9qC)o0ife2@mHn`=!!{=S87(3B!f&+QpPUgeN9M%G}h+h{RS5poF|G|9WI)q06`-*jEPa6&J~Fh#pp&tnF=P6wgEg%bb){}oC3R`eL2hP$UUPYm+e%DM@&Ta|;zdEnZVLQ~3{IoZ=o@?LK>AxcueoN8dtjiIv`;uQ@woMe_cr%Q4$ zCuu4XMV+A~7fBjWnURUl$VrNT8MnAFO*QaUD;3d(md$@>31@Gpyi3RWSS-!UEy$t7 zk`glh?*NGy0Olx@qoIJ<2plz6B+@zc)fx=(4*OI2UgFGlNVuomL%at-X97qF|Jl<^>E5an znVdxz$*kEC!#t|}6g6K{YA&%m{^{5P6T#g3o|9A+Id<4|c1^(u^M7oeb$LK#6Gw~Yn4^d@+D{G<1o5pb4!Pb%rt?`u8@i?z%H zyJrf%O*yKhZ&Cw-dK8W^yw63McAWe16djN$WW3BZ=Y4ZMPg6{$Ar5WCnE+N!>OBMh zR&_`cZ##!5d8tRSUO_%(|z+)bY1!Xc=Ro_JOOvP}qM_bYt(Dw5sy@7x9Q@x(% z^N0PlK+}zHOUs`aZ>3*Zdz^zYr656wv^voRDXE&bGr~_y4(y5&EpiOsL)P+%kl@hD zUxpkG%5kK%3U!^S(icw@u(4^Tg*$&XIjuFzzd` zL};jfmHaIK8_mbT-zYfIAr#vP7~C1i1kHwc{Zp^hxo(W`_r6@2<9AuLT^fCyFPX}E zuxI!b$xT3)5B3dV_p!LFLG03Tlxj6_pSrO5@jESms7;#j)Mzc8I_CVlrw;3NA`y->V91IN&6*R$ApA z&F)mC=iLR+P39=lNA7fXqbhyvF4JctcbUBzr$Zv?7mp8#4U?$&*MN*mP>P%8I(Is2 zk*E7MeD_t&JE5Y~!0EIEnaSEx`fG)fBWjFVp)c^qARL_F2p$~qF@Tnv<~T=+jn15P z3__~U?3vqXO(S`~_kMuX9o|Rbg<2@w-oD)FSa(>-Wi(RVhTQuvI87m@J4t4kgl9&N^pL4fM*(mRrMyfLYv6lyI5n~Z-M_R(;WtUyk z?VihD*|}PCrDpm!u+s|;re?6N9NZ(JcM`>t3LV~(dIe=0jR zOIw#d%08!?Fh}6l25twujVU*O-9grEO?F>|&}JYu|!I-R=YMmtv+>1CR|uS=?lrZ?Z$27adhfigKXU(?%!| z_4;u~I#>pJ0p9UNEOmYrWm5eO9#wZz5)|~_Kw*E9&>K+tusKXOc^K0U$7C?e(Y_=* zGj5z?v_m7$2&sYC*ZN1;>6}K`2g&5xA0^YAVPv8;5}-X1fqtV(cvPv@h3G1WA^|qy zU?4=u2c?2C4V?%@HAVTPj(~0Ae1KB)vM%lv-LGt(d}J*S?p`TQrYOZh-=F1l<07sQ zlN5M zxW*veTstz8T z*^A1KD9?>Yx&VGnv4^qUr;pRXg}DS0!fi!abX@u2CZDn-dD&%-JB;sG{6T5*wjD!R z1J4gzwcc&%<~K111Du=b&Qyp$SQ&Y5whC3zot3_0VGUFxw~(KXeOZPK*!GvnzthOu z73RiEzTy@QGP_A$$jgB3mOk>Ee`lmSeQEsT^KtG(?}&Ru1LIcTOgo(aNEo~19|_#{ zt+XBhwo%=g7~x7Y#=v`mu#PjyBMEichG<8$$_a%x9oG#KAmu=m=}Lw%oix8LJ(I%3 z+YO$jkLNVs`3TkU=qV~g%g$8ksk$6?Ppb4rT?wy|A5f%U>Za1)IrRn6USAxB?fLa{ z_&oqlTDeh>_SYMj@1#%a?~;U9gm@$^DAZr1k@K(G^hF;=!6_RC&O#5ZGeeo+)OER$ zn%t%srqdE_yF`&zzEV7T9x{(pVfRAkEQuF+*@$gn5c8?h$FGb9sB;zRhgW7w!fUDN z8thb@QHz|~IjDHrH%=yVnOrhy5G_)|UxpEO{%cRL&#Ka~*K%UZ)F}X&mqk_Td2OO3 zG^VDHQ@IANkn^&Wbv~x>KR!}#6pK+M+Sm5cv2BE_0%36s_3{%&kbc{pF% zEp2U-B;k!*{pO&)xi@hCz)1$WA=JlZ4z^@WrU(yFa)&|gNl;SW80k6y#Oj-<_ndRa zd6=>khrWeAGc*kl+O}M$J5}DZ5w_N4#o!qe$w}P-Z)kbX7Ch@v(mk1WBG61#N95^q zc=|1#rrDXuQ)A@m^LTm!PYnZgzt_+nYVXA3V|W}lPK})VVk94Qg zir&G>pD3&h19jqGTGQ?6Ws8w9l3r2=AHETze5lOzbX6R=tmjM~7EuD~zT1QP?<&_u zycQ=ytf=>JYq{oxvV@W;Xz_Z4E!Dt>>O^{6j|O?<*}Z{34dl;H1l>FU_A?Y2r4=O! z^px&?b4(f1n-u-D%YGA38uD9EwW_JWUNFCK0h9Cep3JIU*6b4;JT~8`H0*Ik9Y25k zg7M;RE8lJAQ%*2)WzaZ(m$d-jqU`hP=-k1H{~;I-Tptw5q|GM8)TH&=JyioH?>Bzw z-oPdW;=O3c@(as*11|=917T%M$(Z>>{%0TXAC4df{ijD9OKB=2|4jkG=ilvG zmTuQbWE^7}RZ9Mh{dxIU9o%B*Ah)I31*na;_L-oMZ;J(by|;qKZMjV6dJ<+Z-;&^XJ;k29Om-!Z4qVZ`frCir%nyp>zj~K zL0|T?DSQHaJ!kvW;MMT4gu#~6($D@Mv5K{3BZStAR=FiW(PbY7}uj~W93gykM+9a zg${Ok*W=C z{SD_@PkY6K18G7#Mi&h-b#?k{NO~|;Tv*0TF)pVj!HY8rU{-XAC3<5@H#V{$o@%FLjq%>q8Oxb`GzWea5<@jq?|Olm!sH+A zV2z1B)HI)6mE2taWVKQQ#n-qD(JBLv<5c<0M>@vl&~`+);E#1cS9J*iI6pU%P0p8s zBMg5IjzA{oPGoXwMT3bu3Mx99DKgJK^t_ev0eu1v5H15zFaaroE8cr<)jrteyu7r3 zqVsRo(LQ4Oq=PM@jQnq)9Ib`FfF04+(C$wXlbt?)I-H1xNgD>Ik#=<+XZ>X2c`xj) zsVR;qcHSPcmzFwA>u9zN+<=^E&Cw20Lp@JCSeeNGR(ClrtV(I#@#IFO#{7w5fJe%0 zUfq1OW9)+jPU)k{M)Oe|3L^5iVLgsj68Y=i&Hgctql(+{3-~mRRMWI8vwIl&u4_u7 zOzGMb^}6%U=R78-6<$b=+h*k)`c|8RL70`7d1O*i+zVJ5=9WdB92@Y`;}u{Gob96Y zOGifIuRaD+=kAKG$oj3KzRiIo4c@uYn(h6@6P-7mCK-2ilDA3yd$c@}e>WswKaH}> z?ZeoG&aigj1IviKE(|wRNhH5Ye)J=62RpUiiVVP_Y6rE_sko!8kwS-GFXV27nA_-_ z=3!=TbWpJgH689=tZtt7(cg5r4GK>jZIeIl{l)L19Vj?JJ;XM|L`k3N{c`vm#$TPo zgKs&aWQtY6$2d_{YkqJU-JQmV854}7oOc|Bp6e0Rrv@sKM}0(@M`;=0;c{H?v3?-_ zel3WUb{z&D5EUnJmTkd7*2s4cE+#2q_mGU)kBChexL@PtyLZK%RCX=AXYS)(GU<=r zQxP6p>L{cv6-yy4T=Pi>dFK;F-$4;AP$e8`+Gg|E_yo_Ka^#0Jn5n3fGKM8#Q#Uv0n-1~svBqsqF zhybDn{@uUdALjrN<~qz402n^?Gh(JE#c2>TMUOuPXJcr8JYohVSU5in$Q`BW2qs4l z>>Rml(O6I3tFuZEePEhaXU6eOA5?`t#KTD2-v0pjGrA*}-E8)$OCH5TOeV*FfYa5@ zpLI-_Lk)==`=mo3q-Qo-B5PEmZHK>C!?kDW&YuEjYKUow>U)Q%UWC(7R0dwx#MDOi z$N?eDyp4-GRDO*1znX|t?FdCzEM2>Fo1*+iQ+3q9_pJ^+qKQqLk@W%nuVVrk@hb2{ zj&|e|a1uTX$!+`c9!GfS5@XP}G6HbzBGeL9DT`hQCCp46L0@%fAA%QS8)bG&@HS6A ztWqj6CiJa%Jj=6VGd*CqV?BLh>N|>~cQM#o8Jy!It!J>xuTEfNR3Z7;nIw-%O!Y19 zWHnkJoz6^^#v!QKnJvfanU5XJ%j=i(S8soCeGwHEONWUH1BBo|dl^B-HjS;ib#n?% ztwv`3^}wt*?ZKh!+ELFnPU7W?-rG8L9QPUwK0JXpN2Ov#1F$bRb*1zW z$^{40uw(fRVj&cQ=>{yMKVz_;|Ia}GiVQ?e9vvN`ChA@x<7YbmZcX=oB>DG@wi3Co zH!9O8?y`c+wHN&3n`=-zVwJ3COJ?JFTqxj=iiyj!v&CCFc}@p4Zlsv${dFK(*EF<& z3=X6R+zq|p{`+sh@sAjAz7yH6AX_A9g6hXVhmoQ|wG6oAV|i*f49FszZtA8zW5pa9 z2fDkLaBUqUi(FF;4b|((xDj|di%ooa{c7IS)}g%(dxC>o^T}%5FcsJ``B1Mt3fq`DjOG-G~N3))Bhb2@hch!#=ms%}NOkj5H}T zzJW*u@GDXEJsemW(!lX^tGmI;dG1?$jYpU1B!g0_swy3Qm^J->n z{+Cg{xO*61v|)?1J;d2JJn4oORTqh#98Hxz_-OpS>($#qbd8Vl=^3DgdZBL+i$Gwg z!lgw1Yq-Zx4X9Xqy%Pvy4lA_=9HS;WoCk3)TI3+e6i%w4G1LYpQiLMmX1YMBxl!Dd zE`5A7OM0p8+EgBw$vm*ExMhp=TW_-STx1ast?&=y;@bc(^(TCT#?sw~)*!!jB3c9@ zHH({Sg9d?HM+z8`Kg-E3j$6aa4@s8~O)ADF-D?t8pzkPV=Ic)HOmqBiQQ!LgKdis{ zOwbfwvCc#|Tk{N=h3(rl;k>f8)-Enq(pSA^CEgLuXOwsc&fb{!;UXAnn7kT3&}~Tk zTuF6kz4UUPZ-S#d$q;3K?Khya9oBT(S@pZkZ)`pee<%f#2LE{zbPdw`OB9P| zRMnKYpb)w?=289%=D|1r?0namx$d}@_nqDBDqzmMQBmO8fjLoLyD3a*7ELu(1LxrA zqFggfbz*9ywxhRP%gbE&fvffA9@=6(3;y{JmMyJMnMG;(FC=k1(h4aS3bEA;-sICd zMB71hAPKar&S`J}EgPb`->c3%fHONp9lBII(1Phd2u6Zw7B!G|C_2#37>F(d1-2R5 z4)|)gbsP>|lLF+ujVI$v_ZImExurLlw}4x>Ub^G3y^xom39Vcfb-E}m4Z7@8o1ju3 z3$9;X2!$ZVx55d9cX763rQ>JGcQ|EeRU957!j=CnrM0G}I_`FS<6plT+H?c~i#iDh zVLOlBPofiRL=E=fSAxZmeUNa^0Me^=m$Na`epb;F8k^7Tr{6}EPK6{-$DT1Jh~MVS zb?_Y5T4kQiT!W>~9#LvtLgK)U3)n1y*XT-b>Q-mzTR*_xFTA`wHzZ^VNOaQSs8;*h zj|Tu7L>QF=xGbSNojuZ_&53diz#2%M9(UPo_eh+yVKge~;1P)!J_qW#&PA^&O7hO5kADx?geAk zrQ;Scx)fT#cO=bp$3=3@yo+SY zVD=-;eUHxilOaZ?)1;%)5g!LC6sqkHuw&}YY4#>(x^t(M2OHcPjH=qmd3H=)ml(;r zTnukjWHe+0(9Lj732xUg=$3@vilI>;`I}QT=v^mN}6DSwsM+zDP5W zaE}p=>!&;Qp8i1dfvS1A(j#AH&1aSYoB`X1CV8D#F1mBise$wA_4Z6BxdHMnXKqF* zB5Jo}obDiZM51mP$gJwp>1eXTd8MrumcI#NYRD)$8KwCB!zg}6;Tzjw0+M>9jafcd zc*}%#^p~;%xiqtoyoRIHq7VLTz-*{RpEI(`@mKAI8(`(EPQ_~`+O~X$zm5keHO>r!B0J_`ay~90~Ukr(f_(&=Z!#ro@ zTdDq;60r=G7Bx_;^71mcfI0d7Aobp)k&B>i1R8J>h&B)iL-xAU0u??hbBT4yY`VA8 z1*M=46*E(Ap1R}42 zxCrx*8u+z;l}v1A4{)FSvh~q!!X9Xi6K!)d#Eq>sNUZYC3+swK8=PJxzCA*Z5unFp zTmXddC+KaqfkL0dA+;0tP)>1xp0PB1tp<+t$CA8*UfOM}ExKU-IkaH@;X`!S80%Q% zU@M9pZLkBWT3t&`w~aI~j?B=;AAyQqOpy|Z5{gcDc!rW1c%z>$JKjO2w587mFVP;T z_n+2X9o-QIh*|Bt)51Z8kCnE26J3v~IJY#_1f_#VhT#s!6f7%qZyxkdYOLQlitZ;m zFN}kK(t2HiEsN!1C9_2Gz&2CT(qHYSiG$G z>xU5({QK9BOTy`)e$Phxt^7azp5Cw743G2%sr&Rbl5qBFZ7JdFVX>>?v%AOOHq{SF z*T=+CW6>k;tBC`01L~fTTW;$fOC;f&X;DD_GYpVhzxh%Uz8!c$^_;WAI=%JV;+YG) zyfb0!q^w``<8Z^@@{Gkb^r#Q-8&asKa;^sQRP&r^I0M_3`7~Tz(XCPPew(@=RlP2q z@A{0ptLCguf0E`=tAWMp4H2DQ4M^$(cl{xY@A?Gq&*@@LQVT&1*wm?TCDCuy>f}gG z>MT(Mm!&D~6L_<#y&5{23i2ZIPaYnIGm_WkWw6_*ffN0WR^6GY8T9+3YEtCczoaMI z*E8QreeEVm_+H44Q)eUJNDVyHe|_Zhcm2h4Hd=MZ&G7O@@%7))DT8EI1HJvmNVO%> zL*FlArc2G=-)J@*qVm6LUNEriVB&sV6aO~u(}`nVR0F@M(W?UaJyr8KT=^5%oG#+R z5aEaG24l-9?a>KT4V!>;`wtf8BWe2&W0@A|A3uz9&0c2sh%p8OV-C^-MjCO6XOSn) zY4g~fMV@TjammRqgeM{zKr@2U5IUqc{*Az#{(*?XSek@7Aj?)foA+$_Z|Wn@!s68N z6TI4q1-O5Jmks%RzG1R~FhwhHf)UFJ7rd}Jba4Wz;mhXxA<6m{qBB^urXqNMY|U^d zJvawhUo43l)m|IPi@p?E~L7k#qitXc>T!e-kJ0k59*tOfs9^yCu1<+Y9&TS2nJ5!)bS=#HE-Dm%Rh)7 zx_k74Ax*LMa`_A9*5w~a{hcGF$vsP9rFVW7*lRzOzWmub6*o&d>uYYwWaiYnGaOa(s%uy$ zGo#wQIGb}A)>h3MwE1~+U*MhA{y@nAN%&=c6a!4M>xU>Gt^pzjN1|@0EB;z#C270t z#*~U7+4}=$`{}|wtTt9ynyHIQ7)^im2fjrRsJMlmH9n^sd6Jd)!x#ArOq)>Pp2$h? zP9G!Y=FpR)9PEkcH6BdlwDEsI7uO#(XTi60fOjGTyo76GR+`W1rksLfjcL%CPvTk; zobc1)bUP#efb7ma%{x}vJyiSu0OV&em>dP_z-nO)9=AP1jZtOJ`6y& z0MM!kpwyX3k2`n!%cVQyv?wk*hKa|O^8SQDyqU$m!0XbaOS1-1mhw{+WseO*nY8j! zCX*^XbE!Vjll}yR0@iC9;7bil^LvEsEjSFl5O--)kYDWOaSAyPd>o3LLLTf^O88|c zLlkiHA+R@cDpx_AVAg7&6B(4$(f;Js4t|Jqz(EINJf~x*!x7B1zh~vrqLmtqRX2}L z81tAO@>54tE$*{{orc6!>I5L&dD=-{wlLNE3aL3Wb&gZI10jRfchdf`H(!@urnz{%$oV3$pi z+dpd+TH6-#gSqnY?TOI${wb08d-}X{a5zi>$Q%?WA|Tk4f5EH9eCIvkB!%C4>F}72 zqZ=}V7c-C{BjBDLVt_(tg|G{FgFsd-_93<~LF92@beweOZ`YeK>+tl)<_DWS*u_vlFKrP36TxDR9cetUt)t>b`fsw2g zJDTqh>9+=42CAd;k*D%S+&Cy98y2O)yVx+Hs@{-(7I=DI9SYEnA z$PG^p-({Xk! zDJb{-wTd3K)O8nDFxSP|3YV#SNiL6h6L+1$;HHJ+K21YIg*7Tk-*<2T7G2a8N^c>ji%SYpkvYU&O4RH-~#)slApVgU8 z=Hm05ZtTfAMQg58I6+@Wq=fFtT+EA%JR%tLa&2=9`q_ywDX4LmYmj4`Z5v~g{7;Oj zubS4mK+EY{-?Dl9Tmw^NDCDGrK9aY|n-GbWo^{qcrgmx>-gm|^zLV8vIm;vJ{jd1D ziTq4MUlHFYm4g#L>I-}#b%(A=XR#JyVDK%&-B-o%j8lIHO|6e|!j)k%p}2PpQzqTf zYh*r^9`CiW#(wEg?{Z1#zp9$0{2WEkx?$*%iu<$UKE)B6g@_*_A4$5me-ZB7^z^U# zv%p+!DJ6u4@|P(S^CAP&rmCe@DTevA@XvPU9_G70zs-zhmj3xO=FaT`JJ>ipdeorY zMr6M>f!->EBx*HqeEU3>S+QiN!nNrhxw!j(QC(n*O+} zuq^a^1*Tt&Y<{YQ{bBn(Jd?$Iw*7IQ$)9iwR&1w_Y%7c?8y37TjhNrx`^fe}w3s5& zXS23{jy9FoH9h+{k)Exg^7MAqdkGP6eut#yjd z+#caZ>Td@pnnQk@v#uf(DBs?#Wya2DTd6Mkw;=L~;XGB%RW0rcBJXT1hDPVEhzEqP z^e<_igIUno=E`o~6q)gg6)CnA_Z7Dc)$>IDHwfFq3^V$=LB??WzqX|Fr){xTIm-&H zR)+Q+H|O+rm!28T&THQOnx4sIuW#P|p`LMMK$C?OT__|sO9)_A&7+DES8=36zug|o zj2=W@%B3jsHVi}Fc4sV;jq^O5XmMs$+~eHk-0=iW7uu#wGSlRs9gkHwXM0TRike}{ z1k;ds{ZQVZi#1dEOqGV=T=g|8R<8FD{ca> zNXH#z%pd9R^D5OW8KW9F+e)7^kyxKrG9&9XBv6cjH5G5}5B#LYbiqUsr;1CE3fB`X+%jzrDZGFv zY%z(>=1V0Q*b=N!W;dRO7Ee7nRZR<(nwfVw$KXCi1BafC=|HZAiTI_qhjq-5GGN%p zL|zj0V)Jdv{P_Q;wyOb)^0?MBy9+EV#6>|7Q9ph}esy(?LHx-Ay66)dQ86K!HY~8Z zs|ef${7ItF_!EL{gE4h%ylHKXt=9gyT3bn_V3RiOZG8$!(-?h{Xlz2%Hh~iT7ItCp zd%ka1*4%ra`}97y!!vx}&fhn4=FH5QGv}P4B1hyJfbY1wsMV64(P>F7ddBQr=Qs>g zDho~8EnL}E&4A$l$DkSMNPkc+g>{9xvkz@c&K}!yu|?~&IHouwax*$3re)&8Zw}MF zBw5y3lc_;p1_@Lb*1^)ez2fov_}WNFP$^aTrs%wnCkN58>9&fNdqU7M`KDG2YivsD zov1)>PpVR}sFVYqbcGx`3%cu)J1T{rpihl}KBWr$1Qj5QNrUoVJb${otc3UfJNkdK z$6(T9_p(p-YYcaTt`j0>9U#7%`U;91I_Fe}u0L48CHUl{-RkEb90>;jRnkKiRS04~ z8c`$<0=keXO=I2Pt3FZ{Z>8P$n)d5ES#~~7`^#7|jLh^E5E7=on%!zf%tP3WQP`l| zat?PGXrU{9XG`SA@muBxfIiohy+zp>rtb}$9ul@j^HF+dY|Hg(ZoXbcdoP+D{c^!@ zozmWo9@=W{1+xe#=3iF~Sy)DBqeatNiKi^uFMqSM?yS|poQ9n`bX<(J*^zao%`;be zsZh~e46ujP{70+x*6j8XdH%?;s=ZApX7bfRInOf)@-Q74v^_ci)hB=E&jgkbZusRm zEQt%1!VIlN?Rs>AinUXOgs--l7+m){sZ z9;o!`K&6-ae~f;YS4^1FY~}k`yhl`HxO0satU3ce|J*)6kqlF`RhOO7v=%uRw_b3( zlKA_?s>CNJy;#T(XLQC{Q<|)gO(fKNXZVlq*-g_H=eLtALPz2IS9t1P&q|YY^>lT& zqtv;wb!;n}6q!h|`wKtqq4o31$jx4|e5UI;2m+PfG>UYY4tXdY%evLWaxeFg2p_ud zc{Xhj#C1`%#yY7L6;zJnqsF#es9u*yw}1pkl87;e;EKusX=rGa(fF=U za({1=(ZTY-CI_K!VOk~pqAvr7lHhMN`GZj<*aOf6rC)%9nd}waC7ujSRjj`*3|#h9 z?Jn&ejj|S;@^!mQ!B93rL$`7#VL~}NauFeP023-V9&IO^W1Eyk01~^1f5{VdRjcH0 zL*?J^xz-!!iCMt1qyFAQylyHw2$!Zob%uE^WQGAB3pGx4PErn2hvj%krPIQ+&9~J3 z3Q&=jCHoox zQPfW3%_sj9*Zy4JLIZ+KT^ZvoG!4a`Q}(P+dgRN)HYW_WDI;kdAEl$ksqtLmi$%|P z5VMl_Y&_3RHgvx}z>K$h6=9QNg8~y+-RNVqOBjd+@c^t3rBT z2&7X%kaX>+E56WJR}AaSP9rLN-e2CX-wE46S{A;v(`YXAq#ChP?mGs1Iiyt?O~Uq= zZhdk>2k(3Bm%Q(Y7*61+LQ3RGe3w|5$RCsTD`p{%V?%qjeJKisY>o93Oa{++CYV~_ zgWWG;68I7aImI;q_Mu`r zd^E#qmyjf$I4{iVy_^NWm7^Hz*$*5+A4Vv`IEdxe#z@*$e_# z1#OO)md10VqK7ZJB|tiMSUjG_ALcj2$uw>bHcG^#gkt>rX*`9W6?__>Dx`Ft7x{v( zyPb0H5w>)`*?4ZmD?hK;DWbc@M(qrUn6!I}Nyk=UTssS~@Zys~ms)p~E;7Hui7B`l<2+hUMin)k2@31rj1b z*1FcA?d#GKLs45!1QQ*==$uvuQtXkWW-RhY=wlIlroQKB1(8Y+k{WA<{)pN=(?W<0 zI`v|GU2In&r6VTzRtQM>O#{8LkVS09S@310f}>wQx^W!79^U;e2y4^VDYdZ!T8S0( ztH`kq-(k{i*kvJkzdEqaV#u~Zw1qYoe(bf0-|;Y3ADA(~Lco$IZ=6^Gq<3auy*}u_ zA8VInUAr+`S)q^y)$_gTl^=bT3|O(gUkpRn5QoGGV8_!Al)`>2Dzo_g;g=L@Y!p{A zLOc1}S^OD+3zKGRuDvQsbGW40I4HK{@XX-ph=|F$Cw&fgB_=w0o|@ObzaB@>DVOVU zW(UDgonp^W%$~s;RB&d8t~B4D!L6ci1~(;XumnRhZrty256xR1DONc%2#5v?i-{+Q z$4$H>ydNZaSm4->sNxyikR2R5{BVk6;`Rja=ude`e%J%k5@9esp^;>HkB|PZ5^tOM zgV7&=Ns@mh@2MgABfpSl^A|>eVB!XAusp@~U|rDmV07nv$dyd*F*{PMhrkE?tsk8n zkQsD3^q6mq{@D&wyV0hD7ISQi@XY4vX*VD&f1?+8++PA){D)6#`-vvpIZ5(vW<6WNEv3+ZCo3<1xSH zF|L8JjwN*+B4;k2q8pMw{grrlF3+0M;^zzRK%vnuf7B^M0+kgVd5?ZH(9%;20 zh~A^H@knd;i{?C@hnf}p#rZs*J__r(+%P=|>(>T~QomJb=zgZ~^B&!;3tDJjLRfnn zc+$k``>WvdXwu5(yHQZjh&Z0l^Hp?uAQYWG%%`d^;HQo6;fO zzo)GWc+S|#Bhl7oYx)^lsKiJDwr4_G#D6_n(FNR)9<1nrXCR5&+jT=bme>zEu)O^8 z6s(v8mt(^RALP=+WNRD*l`i-)99^EL3%Wf|&$0|em}L2!mGO0FXXH8MzH$# zt#}UVOk*8kFBIVza<(=6_!tcJ>0St&&4Cih93bm%Y-}RQxF<}FM}_sr8Ae-Z3E41& zy%cwNocZ0V$FReWPHFB3Nx z^K^|bgk4j}4NHT)@E)YzAeLLc!RD2{!(=>NxxB2vEBF0sQTa*dXXSkUNgEAA+V&dH zN@=LwE>bg~9fPM=9I^7moN;HNa{8iIp5meDq76zo`wDrtSKg>d4dK)dO5Yj=#cy+h zO?=zRlfFW@Zi0yXfe3B)Vn?K#_5Dpb=_D$!jr6vp$nVe$u5mAMd>@wB7A)eE6NBxfb@+>9H0ybhcq|r$S z4WcxV!)0b@hL8-Axvn?@7Ig?Zs)RMi=>mg+@QJALKJ|>;`^6M26GB!!b-I1o@*L zTu@3HDg@X(U|i?JexjX_Iw%`~Ys|#7^$06F@m(LKl?{5q=V-LMW}? zV##3X(_T(&P*aL>Xa~;ook1*q_4e}X2#2I+v$_?oE3WN(ltrnB*HpkLT$YBw1#Jsd zMf>!+i2L47F!|)wp46v<+LKYs(dk5c8Jv2~$IR(kq*ND)&HbtykV89$U_f zj9`Z13mIcLOs?#L;xn~Fc$bCHNpzjCL8-B!L65rX0c!b|dIfBw?eobIo`m*S&@-^v z=2u%n0uo3O*zxNeOgdJmQMU3RZE}Xdf%MB?$No5hdF3C1v*?xo;LUMx+!}{DunEGY zW32mCHNK!czhiG`Lo7Y)I&A z`hKK83CZ9bY*MCKKkChal8OGTSDrbdHpc!d>|yco-;oz{*lfymw z4xg+&cSb2sNUL}P9}x3a@nZfb@zqs4bCCuV?Re{In4^2|M$`!+?V_M=fTlpqeQXOD z;HkUHa}-qtQm3NkdF7Vjla78^2i4vj#l45Qp(t2C9n-(PyHMZbidfvatLS7ofd=>S zW!&jmdoxmdz59(D3Dzu{^-ri5R(R#d!F74JC$Hv`$S#G=C$D^BgsfY%nW&>WU>(Vb z^BACPXB4>7&=lxgh0Z<%A{pn;ZV2PVpvnQUF%0t|72m|yN_nY@RVzYQzpJI(BBV!n z`4ewHi~U-_kqs?!WTxsy*JwCd?jc83e8~MM@Fe3*@h8-D`(gQn+_F8R`TjaFaBC+Y zCt_BGj7&=SJ7l)KpeFtjZiOW`yiI+sgd+MOSa^CJI?G>!d>$G2a{fS#dlYd;0&(9! z+ycn$4F=-2BJLF8jv(I)h-*e%%#YQ4dopl;_v?Yk7ZLfjkjPwQ+>P6>K1aqD#F?-d z6p*=bxC4m0gnSkVP98)YPCljB2o(}Vh-*Nc3BDC^d5C-YTWY>9BaR%Yrne!Eu`tGV zoX{u;W8&cld^}dfl!kz1^Y=CEs9w#r?n8|N=lbx`-&7if2cNq+lg{Y==lEc{|K$lq zC%Djp>30Dcae@0{`2YX!$h#j>Fm-56$W8D6*BT6d=U!0$ljV$pB=drX4gW>MKG4Ee z!Vfhp6TT5Re-gj#crH8%;Y;*EJbW(vJoqK>%Yb%I;7QJneqD$3li!2%nVMf8P9nh4Rhi>6Yu1H)2}}1{UY9%A^rnA zcYLm4y?Blb!wCPWhK1pI4Y)iHPaFL6@JHc$;6DnyU%+z^J|Y|gC_aFW#epY%zz?U* z8@`0~n02NA6cghTU`?LAgZCLTb8;iXqS@c@o;o2pC2d+f8^pUdamwT*jUTF-TBeH* z*JmdBSrXp$)58<#$9^6EX z>U8C9JSHXMJ%t&XVy^`zy&eqCG)w`Mun0rGE0Ad$p2y)W6alw9l1UVHJ~S+bNtEXy z_)_@C;J*T20lxvh27W92PWV0WR8B3ra`itjejY-<7Ti$~MhvYB89$Xkh(`lRaTEx$ z5bu-e+#2rRkh>WZ7a3tt0s{YT!$SCDn8eNsq=mI_a%=Fu5bxVx0kR>E+(&rt|F(vG z{g8(JoogAiD%b>gEF+IFg4RVu6eED#Ogyh)9N4t#@9$#DE`a|5ejkNb;O{47LSo#T zPIDXOT6Pu(AXHA3MRb#7`)>qr=9a6Tma&^hF%o$N^*}V~Vm#yAjCDk`p zR@&?9*Ed#IZ=WGOTr1hP1@cqAKp|Ug^-)aCkT2EL*4QOmP5oB83;8o@YF(T0wb~lV zCRN1N+FVuAR%e5wwy^eXYx$FHJM$l|_ve92-dV|_?w+j_J^&6_Fq ztmkc`1+3?WyCTTaOsTk_^!~ZC*>WK{_|)mkoz>fmYo*fMWt;07tLyDjNp)jA`;xuh zQR=c)ZnC?ivRN!EOGqw0dCL7WOu4SgS-G>#vvM~)FsI^ySr3@)^D5`fma?+4qQoLc zkb$-#wC$%IyhcbHLs?iGL)&zEBgI#T##i4HUrq5fYP|4n=9A02fKGHuA#JX$vP)I9 zfL={)gXFBKa#q?J(1_I~QEbU-t5jLrSY4&WRoEq$y}{+QZ}~51u-aj(u9j*Lnp1_v1+*L2}q?s;ZGNiWN1wT=tpD zklv2TMw#4#Sp&~1Oj-e2g1zyfShR(|gyQ}PuUCuQ#tr6RkzdHmlF%6PKTlQb`8O;& z7)+?@A-IX_gl{`fO6x)fsxM_^ZQWX$kJ~Vxk}?r_ znxaMdlRTb>i7ij^wM~Z*NPgQ46=F2MLVNtK+h#_%GU=;9ze8H`U}v!?3PL_5FXa!E zLBV}UL!NqoJjzha&(mv&3xFp~1<%tVm`HZ$6ofDTC2Ca~RzBp=K`Pi%tF2Ww(yp+%wLY19!b z(1=6SfF_uLrp9(6Nf-Rxj==lvUuf97=QJ$-YW!8|gnxx=`d1YCPm~$|-=+oNu7ojD Itvl*J0gxz37XSbN delta 35609 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 O@h`8=4gK*|`u9KM4bQ*; delta 29993 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 QtT2?v9Ug9Y [!NOTE] > A normal upgrade keeps your identity, settings, contacts and phone pairing. @@ -16,8 +18,8 @@ a search from scratch. Three boards now keep that data: - **SenseCAP T1000-E and MeshTracker X1**: the receiver uses its low-power backup sleep. On a T1000-E, a fix after a minute asleep took about 9 seconds instead of about 77. -- **SenseCAP Solar**: the receiver stays in standby between fixes instead of being switched off. In - marginal reception it got a fix on 5 of 6 wakes, against 2 of 6 before. +- **SenseCAP Solar**: the receiver stays in standby between fixes instead of being switched off, + which makes a fix in marginal reception more likely. Keeping that data uses a little power all the time, so it is only done when the GPS interval is one hour or less. Longer intervals (a repeater's default is 48 hours) switch the receiver off completely, @@ -72,22 +74,39 @@ traffic, while Bluetooth kept advertising. Only a reset recovered it. The cause is in the USB serial driver of the Zephyr version 1.17.6 moved to: with its first 64-byte buffer already full, it reported free space but accepted no data, and the firmware retried forever. -On a Wio Tracker L1 debug build, where the boot log fills that buffer, it happened on every such boot. -It was not reproduced on a release build, but the boot banner left only 3 bytes of margin there. - -The driver is patched. On the Wio Tracker L1, 6 of 6 debug boots and 3 of 3 release boots now respond, -and the companion protocol test gives the same replies as 1.17.6. +The driver is patched. The same patch covers a second way into the same loop: a host computer going to sleep with the port -open. That one is fixed from reading the code and has not been reproduced on hardware. +open. + +## USB companion: a hang when the port closed during a contact sync + +A companion sending its contact list could get stuck when the last connected app went away before +the list was complete: the app or browser tab was closed, or the tool exited. The node stayed in the +loop that sends the list, with nobody to send it to, and handled nothing else until an app connected +again; that app then received the rest of the list unasked. Present since 1.17.6. + +The loop ran for as long as no app was busy. With no app left none is busy and nothing can be sent, +so it never ended. It now also stops when no app is connected. + +## USB companion: a host that goes away without closing the port + +A USB session ended only when the host closed the port or the cable lost power. A host that went +away any other way (a USB hub resetting, a computer going to sleep with the app open) left the node +treating the app as still connected. With one app that did no harm. With a phone on Bluetooth at the +same time, the node could stop answering the phone once the data waiting for the USB side filled its +buffer. + +Now a reset of the USB bus ends the session, and a session on a sleeping computer does not count as a +connected app until the computer wakes. An app that keeps the port open but stops reading can still +hold up a second app. ## Repeaters: the external flash chip is powered down Some boards carry a second flash chip, which companions keep contacts and channels on. Repeaters, room servers and observers store nothing on it. Since 1.17.6 those roles no longer set the chip up at -all, and that left its control pins unconnected: on a Wio Tracker L1 repeater the chip-select line -read low, so the chip sat selected for as long as the node ran instead of idling. 1.17.4 and earlier -set the chip up at boot on every role. +all, and that left its control pins unconnected, so the chip could sit selected for as long as the +node ran instead of idling. 1.17.4 and earlier set the chip up at boot on every role. These roles now set the chip up once at start-up and put it into its deep power-down mode, with chip-select held high. A factory `erase` wakes the chip, erases it and puts it back. Companions are @@ -96,14 +115,32 @@ unchanged: they use the chip, so it stays in standby. Boards with this chip: T-Echo, T-Impulse Plus, MeshTracker X1, SenseCAP Solar, ThinkNode M1, ThinkNode M6, Wio Tracker L1, Wio Tracker L1 Pro 1W and XIAO nRF52840. -Checked on a Wio Tracker L1 repeater after a normal boot, after a first-boot format and after -`erase`: the chip ignores an ID request, and answers again once it is woken. The current this saves -has not been measured, and the change has not been run on the other boards. - This was found while looking into a report of a XIAO nRF52840 repeater drawing about 4 mA more on 1.17.6 than on 1.17.4 (thanks to **Kimotu**). Whether it accounts for that difference is not confirmed yet. +## Formatter: the external flash chip is erased on every board that has one + +The nRF52 formatters (`SoftDevice_v6_formatter` and `SoftDevice_v7_formatter`, as `.uf2` and as the +`.zip` the Mesh America configurator uses for its erase step) wipe a node completely: the internal storage, the +Bluetooth pairings and the external flash chip. On some boards the formatter did not reach that chip, +left it as it was and still reported success. A companion flashed afterwards then found the old +contacts and channels on it. + +- **MeshTracker X1 and T-Impulse Plus**: the formatter did not know how the chip is wired on these + boards. It does now. +- **Boards that switch the chip's power supply** (ThinkNode M1, T-Echo, T-Impulse Plus, + MeshTracker X1): the formatter now switches the supply on itself and waits for the chip to start. +- **A chip in deep power-down**: a repeater, room server or observer on this release puts the chip to + sleep (see above), and a sleeping chip answers nothing. The formatter now wakes it first. + +The erase is also checked: the formatter reads the chip back afterwards and reports a failure if it +is not blank. The last lines of its serial log say whether the external chip was erased, or that no +such chip answered, which is normal on a board without one. + +Use the formatters from this release; the file names have not changed. As before, pick the one that +matches the SoftDevice version of your bootloader. + ## Also in this release - **A repeater with adverts off is no longer discoverable**, as in upstream MeshCore: with both @@ -130,9 +167,8 @@ confirmed yet. companion port and the update page) was predictable, and is now derived from a secret as intended. - **Heltec V3 and Wireless Tracker companions** (WiFi and Bluetooth together, no PSRAM) had under 1 KB of memory left; they now have about 2.5 KB. The Bluetooth controller's stack on ESP32-S3 and - ESP32-C3 companions was 4 KB and is now 2 KB: its measured use is 1.1 KB with WiFi connected. - Nothing changes in use. Measured on a XIAO ESP32-S3; not run on the two Heltec boards themselves or - on an ESP32-C3. + ESP32-C3 companions was 4 KB and is now 2 KB; it uses about 1.1 KB with WiFi connected. Nothing + changes in use. - **Documentation**: the README was reorganised, build and flashing instructions moved to `docs/BUILDING.md`, and the example board and porting guide were rewritten for the current board layout. diff --git a/zephcore/CMakeLists.txt b/zephcore/CMakeLists.txt index b84313ba..013bdbfc 100644 --- a/zephcore/CMakeLists.txt +++ b/zephcore/CMakeLists.txt @@ -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.17.6-zephcore") +set(ZEPHCORE_FIRMWARE_VERSION "1.17.7-zephcore") # ========== Helpers ========== # Append FILE to the overlay list once, keeping its FIRST position. Position is diff --git a/zephcore/src/main_companion.cpp b/zephcore/src/main_companion.cpp index ae9f6153..ffd49801 100644 --- a/zephcore/src/main_companion.cpp +++ b/zephcore/src/main_companion.cpp @@ -339,14 +339,19 @@ static void process_companion_rx(void) /* The contact dump, as far as every connected transport has room * (MESH_EVENT_CONTACT_ITER): BLE and TCP below their 2/3 high-water mark and * not congested, USB with room for a whole frame. A transport's TX idle - * brings us back. */ + * brings us back. + * + * Only while a client is connected: with none, nothing is busy and no write + * succeeds, so the dump would not advance and this loop would not end. The + * last client can leave while we are in here (its callbacks preempt this + * thread), before MESH_EVENT_LINK has cancelled the dump. */ static void run_contact_iteration(void) { #ifdef ZEPHCORE_LORA if (!companion_mesh_ptr) { return; } - while (!companion_serial.isWriteBusy()) { + while (companion_serial.isConnected() && !companion_serial.isWriteBusy()) { if (!companion_mesh_ptr->continueContactIteration()) { break; /* iteration complete */ } diff --git a/zephcore/tools/formatter/src/main.c b/zephcore/tools/formatter/src/main.c index 990bd4cb..1b7d2b0a 100644 --- a/zephcore/tools/formatter/src/main.c +++ b/zephcore/tools/formatter/src/main.c @@ -31,6 +31,7 @@ #include #include #include +#include #include "qspi_probe.h" #if defined(CONFIG_SOC_SERIES_NRF52) @@ -122,7 +123,13 @@ int main(void) #endif /* ── QSPI external flash (auto-detect via pin probing) ── */ - qspi_probe_and_erase(); /* Probes all known boards; skips if no flash found */ + /* Probes all known boards. Finding no flash is normal (most boards have + * none); a flash that answered and then failed to erase is an error. */ + int qspi_rc = qspi_probe_and_erase(); + + if (qspi_rc && qspi_rc != -ENODEV) { + errors++; + } led_off(); @@ -130,8 +137,11 @@ int main(void) printk("\n"); if (errors) { printk("Completed with %d error(s).\n", errors); + } else if (qspi_rc == 0) { + printk("All partitions erased successfully (internal flash + QSPI).\n"); } else { - printk("All partitions erased successfully.\n"); + printk("Internal flash erased successfully. No external QSPI flash was\n"); + printk("erased: none answered (normal on a board without one).\n"); } printk("Rebooting into UF2 DFU mode...\n"); diff --git a/zephcore/tools/formatter/src/qspi_probe.c b/zephcore/tools/formatter/src/qspi_probe.c index 8afc1406..c332848a 100644 --- a/zephcore/tools/formatter/src/qspi_probe.c +++ b/zephcore/tools/formatter/src/qspi_probe.c @@ -12,6 +12,8 @@ */ #include +#include +#include #include "qspi_probe.h" #if defined(CONFIG_SOC_NRF52840) @@ -25,6 +27,8 @@ #define CMD_WRITE_EN 0x06 /* Write Enable (required before erase) */ #define CMD_CHIP_ERASE 0xC7 /* Full chip erase */ #define CMD_READ_STATUS 0x05 /* Read Status Register 1 */ +#define CMD_RELEASE_DPD 0xAB /* Release from Deep Power-Down */ +#define CMD_READ 0x03 /* Read data, 24-bit address */ #define STATUS_WIP 0x01 /* Write-In-Progress bit in status register */ /* ── CINSTRCONF length values (opcode + N-1 data bytes) ───── */ @@ -32,6 +36,21 @@ #define CINSTR_1B 1 /* opcode only */ #define CINSTR_2B 2 /* opcode + 1 byte response */ #define CINSTR_4B 4 /* opcode + 3 bytes response (JEDEC ID) */ +#define CINSTR_8B 8 /* opcode + 3 address bytes + 4 data bytes (READ) */ + +/* A flash behind a switched rail needs time after the rail comes up before it + * answers: one settle, then a few more tries (MeshTracker X1 answered 00 00 00 + * right after its rail was switched on). */ +#define PWR_SETTLE_MS 100 +#define PWR_RETRY_MS 200 +#define PWR_RETRIES 2 + +/* Which SoftDevice this image is built for, from where the application starts + * (0x26000 under v6, 0x27000 under v7). */ +#define SD_ANY 0 +#define SD_V6 6 +#define SD_V7 7 +#define BUILD_SD ((DT_REG_ADDR(DT_CHOSEN(zephyr_code_partition)) == 0x27000) ? SD_V7 : SD_V6) /* ── Pin encoding: matches nRF52840 PSEL register format ──── */ @@ -49,20 +68,27 @@ struct qspi_pin_config { const char *name; uint8_t sck, csn, io0, io1, io2, io3; uint8_t pwr_pin; /* GPIO to drive HIGH before probe, QPIN_NONE if none */ + uint8_t sd; /* SD_V6/SD_V7: probe only in that build; SD_ANY: both */ }; /* * Pin table — add new boards here. * Sourced from ZephCore DTS + Arduino MeshCore variants. * + * PWR is the GPIO that switches the flash's supply rail. The firmware raises it + * from a regulator node; here nothing else does, and the rail has been off for + * the bootloader's whole DFU, so a missing PWR pin reads as "no flash". + * * Config SCK CSN IO0 IO1 IO2 IO3 PWR Flash chip Boards * ───────────────────────── ──────── ──────── ──────── ──────── ──────── ──────── ──────── ──────────────── ──────────────────────────────────────── - * Wio/XIAO/Ikoka/SenseCap P0.21 P0.25 P0.20 P0.24 P0.22 P0.23 — P25Q16H 2MB Wio Tracker L1, XIAO nRF52, Ikoka Stick/Nano/Handheld, SenseCap Solar - * ThinkNode M1 / TEcho P1.14 P1.15 P1.12 P1.13 P0.07 P0.05 — MX25R1635F 2MB ThinkNode M1, LilyGo TEcho + * Wio/XIAO/Ikoka/SenseCap P0.21 P0.25 P0.20 P0.24 P0.22 P0.23 — P25Q16H 2MB Wio Tracker L1 (+ Pro 1W), XIAO nRF52, Ikoka Stick/Nano/Handheld, SenseCap Solar + * ThinkNode M1 / TEcho P1.14 P1.15 P1.12 P1.13 P0.07 P0.05 P0.12 MX25R1635F 2MB ThinkNode M1, LilyGo TEcho * ThinkNode M6 P1.03 P0.23 P1.01 P1.02 P1.04 P1.05 P0.21 MX25R1635F 2MB ThinkNode M6 * RAK4631 / GAT562 P0.03 P0.26 P0.30 P0.29 P0.28 P0.02 — IS25LP080D 1MB RAK4631, RAK3401, GAT562 variants * LilyGo TEcho Lite P0.04 P0.12 P0.06 P0.08 P1.09 P0.26 — ZD25WQ32C 4MB LilyGo TEcho Lite * Nano G2 Ultra P0.08 P1.07 P0.06 P0.26 P1.04 P1.02 — W25Q16JV 2MB Nano G2 Ultra + * LilyGo T-Impulse Plus P0.04 P0.12 P0.06 P1.09 P0.08 P0.26 P0.14 ZD25WQ32C 4MB LilyGo T-Impulse Plus (SD v6; IO1/IO2 are the TEcho Lite's swapped) + * MeshTracker X1 P0.19 P0.20 P0.21 P0.22 P0.23 P1.00 P0.15 GD25Q64E 8MB MeshTracker X1 (SD v7) */ static const struct qspi_pin_config known_boards[] = { { @@ -77,7 +103,7 @@ static const struct qspi_pin_config known_boards[] = { .sck = QPIN(1, 14), .csn = QPIN(1, 15), .io0 = QPIN(1, 12), .io1 = QPIN(1, 13), .io2 = QPIN(0, 7), .io3 = QPIN(0, 5), - .pwr_pin = QPIN_NONE, + .pwr_pin = QPIN(0, 12), }, { .name = "ThinkNode M6", @@ -107,6 +133,22 @@ static const struct qspi_pin_config known_boards[] = { .io2 = QPIN(1, 4), .io3 = QPIN(1, 2), .pwr_pin = QPIN_NONE, }, + { + .name = "LilyGo T-Impulse Plus", + .sck = QPIN(0, 4), .csn = QPIN(0, 12), + .io0 = QPIN(0, 6), .io1 = QPIN(1, 9), + .io2 = QPIN(0, 8), .io3 = QPIN(0, 26), + .pwr_pin = QPIN(0, 14), + .sd = SD_V6, + }, + { + .name = "MeshTracker X1", + .sck = QPIN(0, 19), .csn = QPIN(0, 20), + .io0 = QPIN(0, 21), .io1 = QPIN(0, 22), + .io2 = QPIN(0, 23), .io3 = QPIN(1, 0), + .pwr_pin = QPIN(0, 15), + .sd = SD_V7, + }, }; /* ── GPIO helpers (raw register access) ───────────────────── */ @@ -220,50 +262,103 @@ static bool qspi_cinstr(uint8_t opcode, uint8_t len, uint32_t *dat0) return true; } +/** + * Read the 4 bytes at flash address 0 (READ 0x03, single-line). + * The address goes out from CINSTRDAT0 bytes 0-2; the data comes back in the + * byte slots after it: CINSTRDAT0 byte 3 and CINSTRDAT1 bytes 0-2. + */ +static bool qspi_read_first4(uint32_t *out) +{ + NRF_QSPI->CINSTRDAT0 = 0; + NRF_QSPI->CINSTRDAT1 = 0; + NRF_QSPI->EVENTS_READY = 0; + NRF_QSPI->CINSTRCONF = ((uint32_t)CMD_READ) | + ((uint32_t)CINSTR_8B << 8) | + (1UL << 12) | (1UL << 13); + + if (!qspi_wait_ready(500)) { + return false; + } + + *out = (NRF_QSPI->CINSTRDAT0 >> 24) | (NRF_QSPI->CINSTRDAT1 << 8); + return true; +} + /* ── Per-config probe and erase ───────────────────────────── */ +/* @return 0 erased, -ENODEV nothing answered on these pins, -EIO a flash + * answered but the erase failed. + */ static int probe_one(const struct qspi_pin_config *cfg) { - uint32_t dat0; + uint32_t dat0, first4; uint8_t mfr, type, cap; + int rc = -ENODEV; printk(" Probing %s pins...", cfg->name); /* Power-enable GPIO if needed */ if (cfg->pwr_pin != QPIN_NONE) { gpio_set_output_high(cfg->pwr_pin); - k_msleep(2); + k_msleep(PWR_SETTLE_MS); } - /* Configure and activate QSPI peripheral */ + /* Activate with the pins disconnected, connect them afterwards (what + * Zephyr's nrf_qspi_nor exit_dpd() does). ACTIVATE polls the flash's + * status register until WIP reads 0; a part in deep power-down, or no + * part at all, leaves IO1 floating and READY may never come. + */ NRF_QSPI->ENABLE = QSPI_ENABLE_ENABLE_Disabled; qspi_disconnect_pins(); - qspi_set_pins(cfg); if (!qspi_activate()) { printk(" activate timeout\n"); goto fail; } + qspi_set_pins(cfg); - /* READ JEDEC ID (0x9F → 3 bytes: manufacturer, type, capacity) */ - if (!qspi_cinstr(CMD_READ_ID, CINSTR_4B, &dat0)) { - printk(" read ID timeout\n"); - goto fail_deactivate; - } + for (int attempt = 0; ; attempt++) { + /* A repeater, room server or observer leaves the flash in deep + * power-down, and the reset into this formatter does not wake + * it: it then ignores everything but this command. + */ + if (!qspi_cinstr(CMD_RELEASE_DPD, CINSTR_1B, NULL)) { + printk(" wake timeout\n"); + goto fail_deactivate; + } + k_msleep(1); - mfr = (dat0 >> 0) & 0xFF; - type = (dat0 >> 8) & 0xFF; - cap = (dat0 >> 16) & 0xFF; + /* READ JEDEC ID (0x9F → 3 bytes: manufacturer, type, capacity) */ + if (!qspi_cinstr(CMD_READ_ID, CINSTR_4B, &dat0)) { + printk(" read ID timeout\n"); + goto fail_deactivate; + } - /* Validate — reject bus-float (0xFF) and bus-ground (0x00) */ - if (mfr == 0xFF || mfr == 0x00) { - printk(" no flash (ID=%02X %02X %02X)\n", mfr, type, cap); - goto fail_deactivate; + mfr = (dat0 >> 0) & 0xFF; + type = (dat0 >> 8) & 0xFF; + cap = (dat0 >> 16) & 0xFF; + + /* Validate — reject bus-float (0xFF) and bus-ground (0x00) */ + if (mfr != 0xFF && mfr != 0x00) { + break; + } + if (cfg->pwr_pin == QPIN_NONE || attempt >= PWR_RETRIES) { + printk(" no flash (ID=%02X %02X %02X)\n", mfr, type, cap); + goto fail_deactivate; + } + k_msleep(PWR_RETRY_MS); } printk(" found! JEDEC=%02X %02X %02X (%u KB)\n", mfr, type, cap, (unsigned)((1UL << cap) / 1024)); + /* From here on a failure is an error, not "no flash". */ + rc = -EIO; + + if (qspi_read_first4(&first4)) { + printk(" First bytes before: %08X\n", (unsigned)first4); + } + /* ── Erase ── */ printk(" Erasing QSPI flash..."); @@ -294,6 +389,14 @@ static int probe_one(const struct qspi_pin_config *cfg) } if (!(dat0 & STATUS_WIP)) { + /* A chip erase the part refused (write-protected) + * also ends with WIP clear: check it took. + */ + if (!qspi_read_first4(&first4) || first4 != 0xFFFFFFFF) { + printk(" FAILED (first bytes after: %08X)\n", + (unsigned)first4); + goto fail_deactivate; + } printk(" OK (%d.%ds)\n", elapsed_ms / 1000, (elapsed_ms % 1000) / 100); @@ -313,24 +416,30 @@ static int probe_one(const struct qspi_pin_config *cfg) if (cfg->pwr_pin != QPIN_NONE) { gpio_release(cfg->pwr_pin); } - return -1; + return rc; } /* ── Public API ───────────────────────────────────────────── */ int qspi_probe_and_erase(void) { - printk(" QSPI: probing %d known pin configurations...\n", - (int)ARRAY_SIZE(known_boards)); + printk(" QSPI: probing known pin configurations...\n"); for (int i = 0; i < (int)ARRAY_SIZE(known_boards); i++) { - if (probe_one(&known_boards[i]) == 0) { - return 0; + int rc; + + if (known_boards[i].sd != SD_ANY && known_boards[i].sd != BUILD_SD) { + continue; + } + + rc = probe_one(&known_boards[i]); + if (rc != -ENODEV) { + return rc; } } printk(" QSPI: no external flash found — skipped\n"); - return -1; + return -ENODEV; } #else /* !CONFIG_SOC_NRF52840 */ @@ -338,7 +447,7 @@ int qspi_probe_and_erase(void) int qspi_probe_and_erase(void) { printk(" QSPI: not supported on this platform\n"); - return -1; + return -ENODEV; } #endif /* CONFIG_SOC_NRF52840 */ diff --git a/zephcore/tools/formatter/src/qspi_probe.h b/zephcore/tools/formatter/src/qspi_probe.h index e98a41d1..4dd1e105 100644 --- a/zephcore/tools/formatter/src/qspi_probe.h +++ b/zephcore/tools/formatter/src/qspi_probe.h @@ -13,7 +13,8 @@ * Probe all known QSPI pin configurations for connected flash. * If found, erase the entire chip. * - * @return 0 if a chip was found and erased, -1 if no chip found or not supported + * @return 0 if a chip was found and erased, -ENODEV if no chip answered (or the + * platform has no QSPI), -EIO if a chip answered but the erase failed */ int qspi_probe_and_erase(void); From 9d6b21a2649022998849460589cd49665ba324a9 Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Tue, 6 Oct 2026 21:43:35 +0200 Subject: [PATCH 12/16] comment diet vol.2 --- docs/ARCHITECTURE.md | 28 ++- zephcore/adapters/ble/ZephyrBLE.cpp | 75 ++----- zephcore/adapters/ble/ble_dfu.cpp | 32 +-- zephcore/adapters/board/ZephyrBoard.cpp | 82 +------ zephcore/adapters/board/zephyr_poweroff.c | 15 +- zephcore/adapters/clock/ZephyrRTCDiscover.c | 25 +-- zephcore/adapters/clock/ZephyrRTCDiscover.h | 46 +--- .../adapters/datastore/ZephyrDataStore.cpp | 53 +---- zephcore/adapters/datastore/ZephyrFsFormat.c | 35 +-- zephcore/adapters/datastore/ZephyrFsFormat.h | 13 +- zephcore/adapters/gps/ZephyrGPSManager.cpp | 107 ++------- zephcore/adapters/gps/ZephyrGPSManager.h | 24 +- zephcore/adapters/gps/gps_internal.h | 12 +- zephcore/adapters/gps/gps_module_cfg.cpp | 207 +++--------------- zephcore/adapters/gps/gps_power.cpp | 206 +++-------------- zephcore/adapters/radio/LoRaRadio.cpp | 14 +- zephcore/adapters/radio/radio_common.h | 79 ++----- zephcore/adapters/rng/ZephyrRNG.cpp | 143 ++---------- zephcore/adapters/rng/ZephyrRNG.h | 47 +--- .../adapters/sensors/ZephyrEnvSensors.cpp | 17 +- zephcore/adapters/sensors/t1000e_analog.c | 28 +-- zephcore/adapters/usb/ZephyrCompanionUSB.cpp | 37 +--- zephcore/adapters/usb/ZephyrUSBCDC.cpp | 33 +-- zephcore/app/CompanionMesh.cpp | 92 ++------ zephcore/app/CompanionMesh.h | 13 +- zephcore/app/ObserverMesh.cpp | 13 +- zephcore/app/RepeaterDataStore.cpp | 17 +- zephcore/app/RepeaterMesh.cpp | 68 +----- zephcore/app/RoomServerMesh.cpp | 27 +-- zephcore/app/VContact.cpp | 9 +- zephcore/app/main_observer.cpp | 28 +-- zephcore/helpers/CommonCLI.cpp | 158 +++---------- zephcore/helpers/NodePrefs.h | 146 +++--------- zephcore/helpers/battery_curve.c | 10 +- zephcore/helpers/boot_prefs.h | 15 +- zephcore/helpers/input/stc8h_keypad.c | 12 +- zephcore/helpers/led_gate.h | 36 +-- zephcore/helpers/ui-button/ui_pages.c | 31 +-- zephcore/helpers/ui-button/ui_task.c | 75 ++----- zephcore/helpers/ui-joystick/joystick_defs.h | 23 +- zephcore/helpers/ui/display.c | 39 +--- zephcore/helpers/ui/display.h | 57 +---- zephcore/helpers/ui/doom_game.c | 34 +-- zephcore/helpers/ui/haptic.c | 12 +- zephcore/helpers/ui/ui_common.c | 29 +-- zephcore/helpers/ui/ui_task.h | 16 +- zephcore/src/ContentionTracker.cpp | 11 +- zephcore/src/main_companion.cpp | 17 +- zephcore/src/server_main_common.cpp | 97 ++------ zephcore/tools/crypto_bench/src/main.cpp | 24 +- .../lr1110_updater/src/lr11xx_hal_updater.c | 84 ++----- zephcore/tools/lr1110_updater/src/main.c | 144 +++--------- zephcore/tools/rng_selftest/src/main.cpp | 10 +- 53 files changed, 515 insertions(+), 2190 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index c6aa673e..a0ea9b06 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -598,7 +598,8 @@ mesh::Mesh Handles the binary BLE protocol with ~50 command opcodes. Key features: - **Offline queue**: circular buffer with peek/confirm pattern (survives BLE drops); `CONFIG_ZEPHCORE_OFFLINE_QUEUE_SIZE`, default 256 frames (lowered on RAM-bound boards) - **ACK tracking**: 8-slot table, computes expected ACK = SHA256(secret + hash)[0:4] -- **Contact iteration**: Streaming protocol with `lastmod` filtering for incremental sync +- **Contact iteration**: Streaming protocol with `lastmod` filtering for incremental sync. The dump advances only + while a client is connected and every connected transport has room; it is cancelled when the last client leaves - **Lazy write batching**: Dirty contacts/channels flush after 5-second delay - **Protocol versioning**: V2/V3 frame format negotiation with phone app - **Ed25519 signing**: 3-phase flow (start→data→finish) for signing up to 8KB @@ -763,7 +764,7 @@ Full command reference with constraints and remote-admin restrictions: `CLI_comm - TX congestion control: queue (12 frames) + overflow buffer + retry + timeout watchdog - Fast/slow advertising switching with post-disconnect flap prevention - DLE (Data Length Extension) to 251 bytes -- Interface coexistence: BLE vs USB, one active at a time +- One of the companion transports served at the same time (BLE, USB/UART, TCP; [7.6](#76-wifi--mqtt--tcp-transports)) - Debug: build with `debug.conf` plus `-DCONFIG_ZEPHCORE_BLE_LOG_LEVEL_DBG=y` for adapter-level DBG logging ### 7.2 DataStore (`adapters/datastore/`) @@ -771,7 +772,9 @@ Full command reference with constraints and remote-admin restrictions: `CLI_comm - **Internal**: LittleFS on flash (`/lfs`), 256-byte cache for reduced flash I/O - **External**: Optional LittleFS on QSPI (`/ext`) with auto-migration. The flash is `zephyr,deferred-init` and `/ext` is not automounted (`boards/common/qspi-ext.dtsi`): `zephcore_fs_mount_ext()` brings it up on first use, because a - boot-time probe could beat a cold power rail (first boot after a UF2 update) and silently drop to internal flash + boot-time probe could beat a cold power rail (first boot after a UF2 update) and silently drop to internal flash. + Roles that keep nothing on `/ext` (repeater, room server, observer) put the flash into deep power-down at start-up + (`zephcore_fs_ext_power_down()`), with chip-select held high; a factory `erase` resumes it and puts it back - **BLE bonds**: NVS (`storage_partition`, 0xD0000 on nRF52) via Zephyr settings backend (≥1.16.2) - **Prefs**: `prefs.json` through upstream's `ConfigSerializer`, with upstream's key names for shared fields and ZephCore's own under `zc` (see §13); the older binary file is read once to migrate and kept - **Contacts**: 152-byte records, stored on external flash if available @@ -829,9 +832,20 @@ Repeaters and room servers default to `CONFIG_ZEPHCORE_REPEATER_GPS_INTERVAL_SEC ### 7.4 USB (`adapters/usb/`) -- **CompanionUSB**: V3-framed CDC (little-endian 16-bit length prefix + payload) -- **RepeaterUSB**: Minimal CDC with 1200-baud DFU touch detection -- Both share message queues with BLE adapter (transport-agnostic mesh layer) +- **`ZephyrUSBCDC`** (every role): the USB device and its CDC ACM class, the 1200-baud touch into the bootloader, DTR + tracking, and `zephcore_usbd_detach()` before a reset (a soft reset leaves the ESP32-S3's PHY attached). A host that + went away is reported as a DTR drop in three cases: DTR low, VBUS removed, and a bus reset. The reset case exists + because the CDC class keeps the old line state across a reset, so nothing else says that the port the host had open + is gone (a hub resetting upstream leaves VBUS up). Bus suspend, resume and reset also reach a second callback. +- **`ZephyrCompanionUSB`** (companion): the wired companion transport, over the CDC port or a plain UART + (`zephcore,companion-uart`). Framing: `<` (to the node) or `>` (from it), a little-endian 16-bit length, the payload; + a first byte that is printable and not `<` starts the text CLI instead. It has its own receive queue and an + interrupt-driven 2 KB TX ring whose "drained" callback paces the contact dump. +- **Session rule**: a session starts on the first inbound frame or CLI line and ends on a DTR drop as defined above. + While the bus is suspended (the computer asleep with the port open) the session is kept but is not a connected + client: nothing is queued for it, and it does not hold up the clean-up that runs when the last client leaves. Each + change reaches the main loop as a connect or a disconnect. +- Servers (repeater, room server, observer) use the CDC port as their serial console. ### 7.5 Board (`adapters/board/`) @@ -845,7 +859,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. 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), 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. 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 diff --git a/zephcore/adapters/ble/ZephyrBLE.cpp b/zephcore/adapters/ble/ZephyrBLE.cpp index 5324cba9..49fc0c9c 100644 --- a/zephcore/adapters/ble/ZephyrBLE.cpp +++ b/zephcore/adapters/ble/ZephyrBLE.cpp @@ -117,17 +117,9 @@ static bool fast_adv_active; * Cleared by recycled() itself (both run on the cooperative system work queue). */ static bool adv_stop_for_interval_change; -/* Ground truth for "controller is currently broadcasting adv PDUs": - * set TRUE : bt_le_adv_start() returned success - * set FALSE : bt_le_adv_stop() called explicitly (set_enabled(false), - * adv_slow_work interval change, update_name restart) - * set FALSE : a phone connected — Zephyr stops adv internally to consume - * the BT_MAX_CONN=1 slot (no slot left to advertise from). - * Re-set TRUE later when recycled() → start_adv() runs. - * Exposed via zephcore_ble_is_advertising() for the companion advertising - * watchdog (main_companion.cpp housekeeping) that catches transient - * bt_le_adv_start failures. Arduino nrf52 has an equivalent 10s watchdog - * (SerialBLEInterface.cpp:343). */ +/* True while the controller is advertising: set when bt_le_adv_start() + * succeeds, cleared on an explicit stop and when a central connects. Read by + * the companion's advertising watchdog. */ static bool adv_running; /* Administrative BLE state */ @@ -159,13 +151,8 @@ static void kick_tx_drain(void); /* ========== GATT Service ========== */ /* - * NUS service — secured with AUTHEN permissions. - * Matches Arduino's SECMODE_ENC_WITH_MITM on bleuart. - * - * When the phone tries to subscribe (CCC write) or send data (RX write), - * Zephyr returns ATT_ERR_AUTHENTICATION. The phone's BLE stack then - * initiates pairing (PIN dialog). After pairing succeeds, - * security_changed() fires at L3+ and the phone retries the operation. + * NUS service; every attribute needs an authenticated link. An unpaired + * phone gets ATT_ERR_AUTHENTICATION, pairs, and retries. */ BT_GATT_SERVICE_DEFINE(secure_nus_svc, BT_GATT_PRIMARY_SERVICE(BT_UUID_NUS_SERVICE), @@ -191,23 +178,9 @@ K_WORK_DELAYABLE_DEFINE(tx_drain_work, tx_drain_work_fn); K_WORK_DELAYABLE_DEFINE(adv_slow_work, adv_slow_work_fn); /* ========== Unpaired-connection timeout ========== - * - * A connection that never reaches L2 holds the node's only peripheral slot. - * With CONFIG_BT_MAX_CONN=1 Zephyr stops advertising while that slot is taken, - * and the companion's advertising watchdog (main_companion.cpp) deliberately - * skips any state where a connection exists — so a client that connects and - * never pairs makes the node invisible to everyone else until it is power - * cycled. A BLE scanner app left connected does it by accident; iOS does it - * routinely. Nothing else times the connection out: pairing here is reactive - * by design (Apple §55 — we never send a Security Request, we wait for the - * phone to hit ATT insufficient-authentication and start pairing itself), so - * "connected but idle forever" is a state the node otherwise accepts happily. - * - * Dropping it costs a legitimate client nothing: every characteristic on both - * services is *_AUTHEN, so an unsecured connection cannot read, write or - * subscribe to anything. The window has to cover discovery plus the phone's - * own pairing dialog — 15 s matches upstream MeshCore PR #3263, which measured - * a real unpaired connection being dropped at ~13 s. */ + * A connection that never reaches security L2 holds the only peripheral slot + * and stops advertising, so it is dropped after this long. An unsecured + * connection can do nothing here, so the drop costs a real client nothing. */ #define BLE_SECURITY_TIMEOUT_MS 15000 static void sec_timeout_conn_cb(struct bt_conn *conn, void *user_data) @@ -278,15 +251,9 @@ static void build_device_name_and_adv(const char *name_from_prefs) /* Prepend "MeshCore-" prefix so apps that filter on it can find us */ snprintf(device_name, sizeof(device_name), "MeshCore-%s", name_from_prefs); - /* Sanitize the BLE-advertised name to printable ASCII, compacting - * in place (write index w never outpaces read index r): - * - ':' / ';' -> '-' (Apple Accessory Design Guidelines) - * - non-ASCII bytes -> dropped (e.g. emoji in the node name). - * iOS's BLE scanner blanks the WHOLE advertised name if it - * contains any non-ASCII byte, showing the device nameless. - * This only sanitizes the BLE/GAP copy; the mesh node name (emoji - * and all) is untouched and still shown by the companion app. - */ + /* Sanitize the advertised name to printable ASCII, in place: ':' and ';' + * become '-', non-ASCII bytes are dropped (iOS blanks a name containing + * any). The mesh node name is untouched. */ size_t w = 0; for (size_t r = 0; device_name[r]; r++) { unsigned char c = (unsigned char)device_name[r]; @@ -357,15 +324,8 @@ static void connected(struct bt_conn *conn, uint8_t err) * with a fallback in security_changed() if PHY update never fires. */ dle_requested = false; - /* Do NOT proactively request security here. - * - * Apple Accessory Design Guidelines §55 (Pairing): the accessory should - * not request pairing until an ATT request is rejected with "Insufficient - * Authentication." Pairing is triggered reactively when the phone tries - * to access our AUTHEN-secured GATT attributes (CCC write / RX write). - * - * For bonded reconnects, Zephyr auto-encrypts with stored keys when - * CONFIG_BT_SMP and CONFIG_BT_BONDABLE are enabled. */ + /* Do NOT request security here (ADR 0004): pairing starts when the phone + * hits an AUTHEN attribute. Bonded reconnects are encrypted by the stack. */ /* Notify main of BLE connection */ if (ble_cbs && ble_cbs->link.on_connected) { @@ -636,13 +596,8 @@ static void tx_drain_work_fn(struct k_work *work) int err; /* - * BLE TX path - Event-driven (like Arduino's HVN_TX_COMPLETE) - * Uses bt_gatt_notify_cb() callback to chain TX without polling. - * Re-entrancy guard prevents concurrent notify calls. - * - * IMPORTANT: Must wait for ble_tx_ready before sending. This is set in - * security_changed() after encryption is established. Sending before the - * connection is fully secured causes "No ATT channel for MTU" errors. + * Event-driven TX: the notify-complete callback chains the next frame. + * Nothing is sent before ble_tx_ready (set once the link is encrypted). */ if (!current_conn || !nus_notif_enabled || !ble_tx_ready) { LOG_DBG("tx_drain[BLE]: not ready (conn=%p notif=%d ready=%d)", diff --git a/zephcore/adapters/ble/ble_dfu.cpp b/zephcore/adapters/ble/ble_dfu.cpp index ddc2c41c..47f7c225 100644 --- a/zephcore/adapters/ble/ble_dfu.cpp +++ b/zephcore/adapters/ble/ble_dfu.cpp @@ -22,34 +22,12 @@ LOG_MODULE_DECLARE(zephcore_ble, CONFIG_ZEPHCORE_BLE_LOG_LEVEL); #include "ble_internal.h" /* ========== Legacy Nordic/Adafruit buttonless DFU service ========== + * As Adafruit BLEDfu: a paired phone writes 0x01 to the control point and the + * node resets into the bootloader's BLE OTA mode (GPREGRET 0xA8). All three + * characteristics are present because iOS requires them; Packet is a no-op. * - * Mirrors Adafruit BLEDfu (Arduino MeshCore >=1.15.0) so the same DFU tools - * interoperate: a paired phone writes 0x01 to the control point and the device - * resets into the bootloader's BLE OTA mode. We use the *unbonded* OTA reset - * (GPREGRET 0xA8, same as `start ota`): the bootloader comes up as a fresh DFU - * target and the tool re-scans for it (the legacy buttonless flow). Adafruit's - * 0xB1 bonded-resume path needs SoftDevice peer-data enrollment we can't - * replicate on Zephyr, so the phone makes a fresh connection to the bootloader. - * - * The service exposes the FULL legacy DFU shape Adafruit ships (not just the - * control point): iOS's LegacyDFUService treats the DFU Packet (1532) char as - * a *required* characteristic — discovery fails (DFU "instantly fails") without - * it — and reads the DFU Revision (1534 = 0x0001 "app mode") to classify the - * device as an application that supports the buttonless jump (missing → the app - * can't identify the device → shows it nameless). Packet is a no-op here: the - * real image upload happens in the bootloader, not the running app. All three - * chars sit behind AUTHEN, matching the Arduino companion's service-wide MITM - * floor (`bledfu.setPermission(SECMODE_ENC_WITH_MITM, ...)`); a bonded phone's - * DFU app reuses the OS-level bond, an unpaired stranger is rejected. - * - * NOTE on the service symbol name: Zephyr registers static GATT services in - * the order ld's SORT_BY_NAME emits them — i.e. alphabetically by the symbol - * passed to BT_GATT_SERVICE_DEFINE. This service MUST sort *after* - * `secure_nus_svc` so the NUS attribute handles stay fixed; otherwise every - * NUS handle shifts and bonded phones with cached handles get ATT 0x03 - * (Write Not Permitted) on the NUS RX write. Hence the `secure_nus_svc_dfu` - * name (a string sorts before its own extensions, so NUS keeps the low range). - */ + * The service symbol must sort after secure_nus_svc (static services are + * registered in symbol order), or the NUS handles shift under bonded phones. */ static struct bt_uuid_128 dfu_svc_uuid = BT_UUID_INIT_128( BT_UUID_128_ENCODE(0x00001530, 0x1212, 0xefde, 0x1523, 0x785feabcd123)); static struct bt_uuid_128 dfu_ctrl_uuid = BT_UUID_INIT_128( diff --git a/zephcore/adapters/board/ZephyrBoard.cpp b/zephcore/adapters/board/ZephyrBoard.cpp index 851398f3..7310826f 100644 --- a/zephcore/adapters/board/ZephyrBoard.cpp +++ b/zephcore/adapters/board/ZephyrBoard.cpp @@ -37,59 +37,17 @@ #endif /* ESP32-S3: reboot into the ROM download mode rather than back into the app. - * - * The USB CDC companion transport (boards/common/esp32s3_usb.conf) hands the - * shared D+/D- pads to USB OTG and disables USB-Serial-JTAG. USJ is exactly - * what esptool drives to put the chip into download mode, so once our CDC-ACM - * device owns the port esptool's auto-reset is inert: DTR/RTS land on a Zephyr - * CDC endpoint that is not wired to EN/BOOT, and the only way back into the - * bootloader is the physical BOOT button. - * - * FORCE_DOWNLOAD_BOOT lives in the RTC domain, which a software system reset - * does not clear (only a power-on reset does), so setting it and rebooting - * brings the ROM up in download mode. That gives both `start dfu` and the - * Arduino-style 1200-baud touch a way to hand the port back to esptool / - * esptool-js / the web configurator. - * - * Setting it is not enough on its own, though. WHICH USB controller the ROM - * appears on is a separate mux, and it lives in the same RTC domain: Zephyr's - * DWC2 quirk layer claims the internal PHY for USB OTG at init - * (usb_wrap_ll_phy_enable_external(hw, false) -> RTC_CNTL_USB_CONF_REG - * SW_HW_USB_PHY_SEL=1, SW_USB_PHY_SEL=1), and those bits survive the reboot - * exactly like FORCE_DOWNLOAD_BOOT does. Left alone the ROM therefore comes up - * on USB OTG (303a:0009), not USB-Serial-JTAG (303a:1001). That state is still - * flashable -- esptool reports "USB mode: USB-OTG" and even loads the stub -- - * but it breaks everything that expects USJ: esptool's auto-reset does nothing - * over ROM OTG CDC so it needs --before no-reset, and esptool-js special-cases - * only PID 0x1001, so browser flashers fall into a classic DTR/RTS reset path - * that cannot work. Clearing SW_HW_USB_PHY_SEL hands the mux back to - * hardware/eFuse control, whose default routes the internal PHY to USJ -- i.e. - * it reproduces what a power-on reset would have left behind. - * - * Same register and sequence Arduino-ESP32 uses in usb_persist_restart() - * (RESTART_BOOTLOADER). OPTION1 carries no other bits on this SoC, but set the - * bit rather than writing the word so it stays correct if that changes. Scoped - * to the S3 deliberately: it is the only part we ship whose USB port can be - * taken away from the ROM this way (C3/C6 use a different LP_AON register, and - * classic ESP32 has no native USB — it always flashes through its UART bridge). - */ + * FORCE_DOWNLOAD_BOOT and the USB PHY mux both live in the RTC domain and + * survive a software reset. */ #if defined(CONFIG_SOC_SERIES_ESP32S3) #include #include #define ESP32_FORCE_DOWNLOAD_BOOT 1 #endif -/* Detach the USB device stack before a reset so the host sees a real unplug — - * see zephcore_usbd_detach(). Matters most on the ESP32-S3 USB companion, - * whose OTG PHY survives a soft reset with D+ still pulled up. - * - * The condition below must be EXACTLY the one CMakeLists.txt uses to compile - * adapters/usb/ZephyrUSBCDC.cpp, or this call site compiles against an - * implementation that is never linked (findings #22 — that failure mode has - * already cost this repo four separate undefined-reference bugs). The - * repeater/observer/room-server branches use the inner half alone; the - * companion branch wraps it in (LOG || COMPANION_USB || COMPANION_SERIAL), and - * ZEPHCORE_COMPANION is the compile definition that identifies that branch. */ +/* Detach the USB device stack before a reset so the host sees a real unplug. + * The condition must be exactly the one CMakeLists.txt uses to compile + * adapters/usb/ZephyrUSBCDC.cpp, or the call is never linked. */ #if !defined(CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT) && \ (defined(CONFIG_USB_CDC_ACM) || defined(CONFIG_USBD_CDC_ACM_CLASS)) #if !defined(ZEPHCORE_COMPANION) || defined(CONFIG_LOG) || \ @@ -146,13 +104,8 @@ static const struct device *vbat_enable_dev = NULL; #endif /* - * Battery voltage multiplier - prefer devicetree, fallback to Kconfig - * Formula: Battery_mV = (raw * VBAT_MV_MULTIPLIER) / 4096 - * - * To define in devicetree, add to board's DTS/overlay: - * zephyr,user { - * vbat-mv-multiplier = <7200>; - * }; + * Battery_mV = raw * VBAT_MV_MULTIPLIER / 4096. Devicetree + * (zephyr,user vbat-mv-multiplier) wins over the Kconfig fallback. */ #define ZEPHYR_USER_NODE DT_PATH(zephyr_user) #if DT_NODE_HAS_PROP(ZEPHYR_USER_NODE, vbat_mv_multiplier) @@ -521,26 +474,15 @@ void ZephyrBoard::rebootToBootloader() usb_serial_jtag_ll_phy_enable_pad(false); k_msleep(100); #ifdef ESP32_FORCE_DOWNLOAD_BOOT - /* ...but put the pad back before resetting into the ROM. The pad-enable - * bit survives a software reset, and unlike the app the ROM download mode - * never re-enables it, so leaving it off keeps USB-Serial-JTAG off the bus: - * the host sees the port die and only a power cycle brings it back. That - * was `start dfu` on every S3 build whose console is USJ (repeater, - * observer, room server, debug). The 100 ms low above still gives the - * host its disconnect; the ROM then enumerates as 303a:1001. */ + /* Put the pad back before resetting into the ROM: the bit survives the + * reset and the ROM never re-enables it. */ usb_serial_jtag_ll_phy_enable_pad(true); #endif #endif #ifdef ESP32_FORCE_DOWNLOAD_BOOT - /* Hand the internal USB PHY back to USB-Serial-JTAG, so the ROM download - * mode we just armed appears as 303a:1001 rather than ROM USB OTG. See - * the header comment on ESP32_FORCE_DOWNLOAD_BOOT for why this is needed - * and why it has to happen HERE -- after zephcore_usbd_detach(), which - * has to drop the D+ pull-up while OTG still owns the PHY, or the host - * never sees a clean disconnect. - * - * No-op on builds that never took the PHY (repeater/observer/debug keep - * USJ, so both bits are already 0). */ + /* Hand the internal USB PHY back to USB-Serial-JTAG, so the ROM appears as + * 303a:1001. After zephcore_usbd_detach(), which needs OTG to still own the + * PHY. */ REG_CLR_BIT(RTC_CNTL_USB_CONF_REG, RTC_CNTL_SW_HW_USB_PHY_SEL); REG_CLR_BIT(RTC_CNTL_USB_CONF_REG, RTC_CNTL_SW_USB_PHY_SEL); #endif diff --git a/zephcore/adapters/board/zephyr_poweroff.c b/zephcore/adapters/board/zephyr_poweroff.c index 4b588766..840d249a 100644 --- a/zephcore/adapters/board/zephyr_poweroff.c +++ b/zephcore/adapters/board/zephyr_poweroff.c @@ -178,18 +178,9 @@ void zephcore_power_off(void) } #endif /* CONFIG_SOC_FAMILY_NORDIC_NRF && sw0 */ - /* 5. Release the board power latch — must be dead last. - * - * Only present on soft-power boards (see zephcore,poweroff-gpios). The - * rail is cut here rather than in step 2 because the pins are ordered - * loads-first/latch-last, and because step 4 must have observed the - * button release first: on these boards the button is also the power-on - * input, so dropping the latch while it is still held would let the I/O - * controller re-latch the rail immediately. - * - * On battery this does not return — the supply is gone mid-loop, which - * is the intended outcome. On USB the rail may be held up externally, in - * which case we fall through to System OFF. */ + /* 5. Release the board power latch, dead last: loads are already off and + * step 4 has seen the button released (it is also the power-on input). On + * battery this does not return; on USB we fall through to System OFF. */ #if DT_HAS_COMPAT_STATUS_OKAY(zephcore_poweroff_gpios) { #define _PWROFF_NODE DT_COMPAT_GET_ANY_STATUS_OKAY(zephcore_poweroff_gpios) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index 7f0b6b56..b701a4b5 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -197,14 +197,9 @@ static bool rtc_all_ff(const uint8_t blk[7]) return true; } -/* Decide whether the device at d is the RTC it declares. A first read that - * fails means nothing is there, and one that is all 0xFF is an erased EEPROM, - * though a real RTC can power up that way, so it is skipped for now. - * Otherwise the device is ruled out only when two reads each rule it out: a - * failed second read does not, an all-0xFF second read does, and an - * unreadable power-loss flag cannot vouch for the fields. RTC_FOUND leaves - * the block to decode in blk; RTC_FOUND_GARBLED is this RTC with no clean - * read to take a time from. */ +/* Decide whether the device at d is the RTC it declares: ruled out only when + * two reads each rule it out. RTC_FOUND leaves the block to decode in blk; + * RTC_FOUND_GARBLED is this RTC with no clean read to take a time from. */ static enum rtc_verdict rtc_identify(const struct rtc_desc *d, uint8_t blk[7]) { uint8_t again[7]; @@ -386,16 +381,10 @@ static uint8_t rv3028_cfg_backup_ram(const struct rtc_desc *d, uint8_t ram) return ram; } -/* Store the descriptor's rv3028-eeprom-config in the chip's EEPROM. The - * EEPROM is only touched with automatic refresh held off (4.6.7) and backup - * switchover disabled in RAM (3.15.6: BSM 00 for any EEPROM read or write). - * Each byte is compared against the EEPROM itself (4.6.6) and written only if - * it differs (4.6.5). A closing Refresh (4.6.4) reloads RAM from the EEPROM, - * switching back to the stored BSM, and the config is read back on every run. - * A triplet outside 35h-37h, masking an unimplemented bit, or repeating a - * register is ignored, so a devicetree mistake cannot write on every boot. - * Reports whether the config was confirmed, and if so whether EERD, which - * also stops the daily refresh, was cleared afterwards. */ +/* Store the descriptor's rv3028-eeprom-config in the chip's EEPROM, writing + * only bytes that differ, with refresh and backup switchover held off as the + * data sheet requires. Reports whether the config was confirmed, and if so + * whether EERD was cleared afterwards. */ enum rv3028_store { RV3028_STORED, RV3028_EERD_SET, RV3028_NOT_STORED }; static enum rv3028_store rv3028_store_config(const struct rtc_desc *d) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.h b/zephcore/adapters/clock/ZephyrRTCDiscover.h index c2d22aeb..42943c3a 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.h +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -27,50 +27,20 @@ extern "C" { * Probe the declared RTC chips in order, stopping at the first that holds a * sane time (year >= 2025 and its power-loss flag clear): store its Unix * epoch in *epoch_out and return true. The first chip found (valid time or - * not) is remembered as the write-back target. A failed first read means - * nothing is there, and a first read of all 0xFF (an erased EEPROM) is - * skipped. Otherwise a device is passed over only if two reads each show it - * is not that RTC: a bit set that the data sheet shows as 0 (the - * descriptor's zero-mask), seconds, minutes, date or month out of range - * while the power-loss flag does not read as set (an unreadable flag counts - * as not set), or, on the second read, all 0xFF. A failed second read never - * rules a device out. A time is taken only from a clean read whose fields, - * hours and year are each in range (12-hour mode is not decoded). Returns - * false if none present or no trustworthy time is held. + * not) is remembered as the write-back target. Returns false if none is + * present or none holds a trustworthy time. * - * At a descriptor with rv3028-eeprom-config, the time is read once more - * with the chip's backup switch flag (BSF) cleared before and checked - * after, and is not taken if the read failed or BSF was set. A chip adopted - * on a read that identified it (not on a failed second read), with BSF - * clear, has that config stored at once: after the power-on refresh (up to - * ~66 ms), backup switchover is turned off while the EEPROM is read, each - * byte that differs is written, and a refresh from EEPROM restores it. - * Measured on a RAK4631: about 14 ms when nothing is written, 52 ms for two - * bytes, then up to 2 ms for DSM to react (4.2.2). If the store fails, the - * config is set in RAM, which lasts until the chip's next refresh from - * EEPROM (daily while EERD is clear), and only once EEbusy reads 0; - * otherwise switchover stays as the store left it, off once the store has - * disabled it, until a retry or that refresh. A failed store, or one - * skipped at boot, is retried on the system work queue every 10 minutes, at - * most 3 times, each retry identifying the chip again first. + * A descriptor with rv3028-eeprom-config also has that config stored in the + * chip's EEPROM, retried on the system work queue if it fails. */ bool zephcore_rtc_restore(uint32_t *epoch_out); /* * Persist an authoritative epoch to the discovered RTC chip and clear its - * power-loss flag. No-op if no RTC was discovered. Safe to call often, but - * intended only for real syncs (GPS/app/CLI), not per-packet clock nudges. - * If restore adopted nothing but skipped a candidate whose first read was all - * 0xFF, the first save probes again, once per boot, in the caller's context. - * A device ruled out on its second read is not probed again for that reason. - * - * At a descriptor with rv3028-eeprom-config the time is written with BSF - * cleared before and checked after, year 00h first and the real year last, - * so a write cut by a power loss reads at boot as "time not yet set". A - * write not confirmed is repeated every 5 s on the system work queue, with - * the time run on, until one is confirmed, a newer save replaces it, or 12 - * repeats (13 attempts in all) have failed. Call only from the system work - * queue: the repeat shares state with this call. + * 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. */ void zephcore_rtc_save(uint32_t epoch); diff --git a/zephcore/adapters/datastore/ZephyrDataStore.cpp b/zephcore/adapters/datastore/ZephyrDataStore.cpp index 04b350c9..38b985ab 100644 --- a/zephcore/adapters/datastore/ZephyrDataStore.cpp +++ b/zephcore/adapters/datastore/ZephyrDataStore.cpp @@ -301,19 +301,10 @@ bool ZephyrDataStore::hasOldSettingsFile() const void ZephyrDataStore::adoptVolume() { - /* /lfs/_zc_init is written after the first clean boot of ZephCore, so - * this runs once per volume. - * - * - No prefs, or prefs whose radio preset is implausible (a file in - * some other layout): format everything, bonds included. - * - Our prefs plus /lfs/settings: an upgrade from ZephCore <= 1.16.1 on - * nRF52, whose file-based bond store left old app code where the NVS - * bond partition now is. Bytes there can pass NVS sector validation - * and hang settings_load(), so BLE never advertises: erase the NVS - * only; identity, prefs and contacts stay, the phone re-pairs. - * (On nRF54L and MG24 /lfs/settings is the live bond store, but a - * volume that has booted this firmware once has the marker.) - * - Our prefs, no /lfs/settings: bonds already live in NVS, keep all. */ + /* Runs once per volume (/lfs/_zc_init marks a clean first boot): no prefs or + * implausible prefs = format everything; our prefs plus /lfs/settings (an + * upgrade from <= 1.16.1 on nRF52) = erase the bond NVS only; our prefs and no + * /lfs/settings = keep all. See docs/ARCHITECTURE.md 7.2. */ if (hasInitMarker()) { return; } @@ -499,23 +490,8 @@ void ZephyrDataStore::saveContacts(DataStoreHost *host) { const char *path = contactsFile(); - /* Atomic replace ONLY where there is external flash — deliberately, and - * not to be "fixed" later. - * - * zephcore_fs_atomic_write() needs room for a second full copy before the - * rename. contacts3 is by far the largest store here (152 B per record, - * ~47 KB at 313 contacts) and internal /lfs on these boards is 128 KB - * total, shared with identity, prefs and channels2. Two copies would sit - * at ~94 KB of 128 KB before LittleFS metadata, so the atomic path could - * fail with ENOSPC exactly when it is most needed — a worse failure than - * the one it prevents. - * - * channels2, identity and prefs are atomic everywhere because they are - * small enough for the second copy to be free. Only contacts is gated. - * - * The non-atomic branch below is therefore the constrained-board path, - * and it writes in place and truncates rather than unlinking first — see - * the note there. */ + /* Atomic replace only where there is external flash: internal /lfs (128 KB) + * cannot hold a second copy of the contacts file. Deliberate. */ bool use_atomic = _has_ext_fs; const char *save_mode = use_atomic ? "atomic" : "direct"; @@ -555,21 +531,8 @@ void ZephyrDataStore::saveContacts(DataStoreHost *host) }; write_ok = zephcore_fs_atomic_write(path, atomic_contacts_writer, &ctx, "saveContacts"); } else { - /* Overwrite in place, then truncate — never unlink first. - * - * This branch runs on boards with no external flash, i.e. the ones - * that cannot afford the atomic temp-file dance. It used to - * fs_unlink() the contacts file before recreating it, which left a - * window spanning the whole ~47 KB write where contacts3 did not - * exist at all: a power cut there lost every contact rather than - * corrupting some. The unlink was only ever a way to truncate. - * - * Truncating afterwards is the same guarantee without the window — - * the file is always present, and at worst briefly longer than its - * new contents (stale records past the end, which the truncate then - * removes). FS_O_TRUNC is NOT usable here: Zephyr's LittleFS - * backend maps only CREATE/READ/WRITE/APPEND and drops TRUNC - * silently, so asking for it would leave the stale tail in place. */ + /* Overwrite in place, then truncate; never unlink first (a power cut would + * lose every contact). FS_O_TRUNC is dropped by Zephyr's LittleFS backend. */ fs_file_t_init(&file); int rc = fs_open(&file, path, FS_O_CREATE | FS_O_WRITE); if (rc < 0) { diff --git a/zephcore/adapters/datastore/ZephyrFsFormat.c b/zephcore/adapters/datastore/ZephyrFsFormat.c index b65232e4..17220991 100644 --- a/zephcore/adapters/datastore/ZephyrFsFormat.c +++ b/zephcore/adapters/datastore/ZephyrFsFormat.c @@ -39,19 +39,9 @@ static void flatten(uint8_t id, const char *tag) } #if PARTITION_EXISTS(qspi_storage_partition) -/* qspi-ext.dtsi makes the flash under /ext zephyr,deferred-init, so nothing - * probes it at boot. The flash sits behind a GPIO-switched rail on several - * boards, and the boot-time JEDEC read went out before a cold rail had come - * up: the driver failed, /ext stayed unmounted and contacts silently fell - * back to internal flash. A warm reboot keeps the rail charged; the first - * boot after a UF2 update (seconds in the bootloader, rail off) did not. - * Bringing the part up here, on first use, is long after the rail. Each role - * reaches this on its own path, so it is idempotent: -EALREADY means an - * earlier call already ran the init, whatever the result was. - * - * A role that does not use /ext parks the part in deep power-down - * (zephcore_fs_ext_power_down()); a format still has to reach it, so a - * suspended flash is resumed here. */ +/* Bring up the deferred-init flash under /ext on first use (a boot-time probe + * can beat a cold rail). Idempotent: -EALREADY means an earlier call ran the + * init. A flash parked in deep power-down is resumed. */ static void ext_flash_init(void) { const struct device *dev = PARTITION_DEVICE(qspi_storage_partition); @@ -143,14 +133,8 @@ bool zephcore_fs_format_all(bool *out_ext_mounted) *out_ext_mounted = false; } - /* Properly unmount from Zephyr's VFS before erasing flash. Clearing a - * local "mounted" flag is not enough — Zephyr would still hold /lfs - * mounted, so flash_area_flatten destroys the on-flash superblock while - * LittleFS considers itself active. Every subsequent file op then hits - * the erased blocks and logs "Corrupted dir pair at {0x0, 0x1}". - * FS_FSTAB_DECLARE_ENTRY exposes the non-static mount struct generated - * from the DTS fstab; fs_mount() on a blank partition auto-formats - * (littlefs_fs.c: lfs_mount fail -> lfs_format -> lfs_mount). */ + /* Unmount through the VFS before erasing: flattening under a mounted LittleFS + * corrupts it. fs_mount() on the blank partition then formats it. */ FS_FSTAB_DECLARE_ENTRY(DT_NODELABEL(lfs)); fs_unmount(&FS_FSTAB_ENTRY(DT_NODELABEL(lfs))); @@ -186,13 +170,8 @@ bool zephcore_fs_format_all(bool *out_ext_mounted) } #if DT_NODE_EXISTS(DT_NODELABEL(qspi_lfs)) - /* Remount external QSPI too. We unmounted it above and flattened its - * partition, so it must be re-mounted here — otherwise a runtime format - * (factory reset, or the first-boot "no prefs" auto-format) leaves /ext - * unmounted for the rest of the session. begin() then reads - * ext_lfs_mounted=false and the store falls back to internal /lfs, so - * contacts/channels save to /lfs and get needlessly migrated back to - * /ext on the next boot ("Migrating contacts to external storage"). */ + /* Remount /ext too, or a runtime format leaves it down for the session and + * the store falls back to /lfs. */ { bool ext_mounted = zephcore_fs_mount_ext(); diff --git a/zephcore/adapters/datastore/ZephyrFsFormat.h b/zephcore/adapters/datastore/ZephyrFsFormat.h index 7cccc040..5b27b064 100644 --- a/zephcore/adapters/datastore/ZephyrFsFormat.h +++ b/zephcore/adapters/datastore/ZephyrFsFormat.h @@ -18,15 +18,10 @@ extern "C" { /** * @brief Erase every ZephCore storage region and remount the filesystems. * - * Unmounts /lfs (and /ext when the board has QSPI), flattens lfs_partition, - * storage_partition (BLE bonds NVS) and qspi_storage_partition where each - * exists, then remounts. A blank LittleFS partition is auto-formatted by - * fs_mount(), so the volume comes back empty rather than corrupt. - * - * This is the only path that erases the LittleFS *volume*. Deleting files - * cannot recover a volume another firmware has written into — on nRF52840 the - * Adafruit core's InternalFileSystem lives at 0xED000, inside our 0xD4000 - * lfs_partition, and its format() scribbles our top 7 blocks. + * Unmounts /lfs (and /ext), flattens lfs_partition, storage_partition (BLE + * bonds) and qspi_storage_partition where each exists, then remounts; a blank + * partition is formatted by fs_mount(). The only path that erases the + * LittleFS volume itself. * * @param out_ext_mounted optional; receives whether /ext came back mounted. * Set to false on boards with no QSPI. diff --git a/zephcore/adapters/gps/ZephyrGPSManager.cpp b/zephcore/adapters/gps/ZephyrGPSManager.cpp index 4e6781b3..0e298c24 100644 --- a/zephcore/adapters/gps/ZephyrGPSManager.cpp +++ b/zephcore/adapters/gps/ZephyrGPSManager.cpp @@ -229,13 +229,8 @@ static void gnss_data_cb(const struct device *dev, const struct gnss_data *data) ARG_UNUSED(dev); if (!gps_enabled || gps_current_state == GPS_STATE_STANDBY) { - /* GPS disabled or in standby — ignore NMEA data. - * The GNSS driver fires callbacks as long as the UART has data, - * even after we de-assert GPS_EN (module drains its buffer). - * On boards without GPS power control (e.g. RAK3401 where 3V3_S - * rail is shared with LoRa FEM), the GPS module stays powered in - * standby and keeps streaming NMEA — suppress those callbacks to - * avoid log spam and wasted CPU for the entire standby period. */ + /* Disabled or in standby: ignore NMEA. The driver keeps calling while the + * UART has data, and a module without power control keeps streaming. */ return; } @@ -250,20 +245,8 @@ static void gnss_data_cb(const struct device *dev, const struct gnss_data *data) k_mutex_lock(&gps_mutex, K_FOREVER); if (data->info.fix_status >= GNSS_FIX_STATUS_GNSS_FIX) { - /* Reject "Null Island" (0,0) fixes. Zephyr's NMEA parser splits - * data across callbacks: gnss_nmea0183_parse_gga fills altitude + - * fix_status but NOT lat/lon; gnss_nmea0183_parse_rmc fills - * lat/lon. A merged publish fires when both GGA and RMC share a - * UTC. If the chip emits GGA quality=1 while RMC is still 'V' - * (or reports null-island coords), parse_rmc's early-exit on 'V' - * leaves lat/lon at their previous value (zero at first boot, or - * stale) while altitude advances — the caller sees valid - * fix_status + altitude-only motion + (0,0) coords. Observed on - * AT6558R (RAK WisMesh Tag) during early acquisition; Air530Z - * (ThinkNode M1) doesn't desync GGA/RMC this way. (0,0) is never - * a real fix — skip so we don't poison current_pos (and with it - * telemetry and the node position) or promote - * consecutive_good_fixes. */ + /* Reject (0,0): the parser can publish a valid fix status with lat/lon not + * yet filled (GGA ahead of RMC). It is never a real fix. */ if (data->nav_data.latitude == 0 && data->nav_data.longitude == 0) { LOG_DBG("GPS: Ignoring (0,0) fix — GGA/RMC desync " "(fix=%d sats=%d alt_mm=%d)", @@ -391,13 +374,8 @@ GNSS_DATA_CALLBACK_DEFINE(NULL, gnss_data_cb); #ifdef CONFIG_ZEPHCORE_GPS_SAT_DIAG /* ========== Per-constellation satellite tally (diagnostic) ========== - * The Zephyr GSV parser fills gnss_satellite.system from the NMEA talker ID - * ($GPGSV/$GLGSV/$GAGSV/$GBGSV), so this is direct evidence of which - * constellations the module is actually tracking — the only way to confirm - * that the boot-time PMTK353 / UBX-CFG-GNSS configuration was accepted. - * A module still in its GPS-only default yields sats_gps only. - * - * Counts only tracked satellites (is_tracked), not merely visible ones. */ + * Tracked satellites per constellation, from the GSV talker ID: shows which + * constellations the module was actually configured for. */ static uint8_t sat_count[5]; /* gps, glonass, galileo, beidou, other */ static int64_t sat_seen_ms[5]; /* uptime when each bucket was last reported */ @@ -485,18 +463,9 @@ static void gps_hold_sleep_lock(bool hold) } } -/* Acquire-window timeout (ms) for the current phase. - * - Repeater: fixed 5-min time-sync window. - * - First acquisition after enable (cold start, no fix yet): a generous but - * bounded window so almanac download has time, without pinning the module - * on forever when there's no sky. Spent once (first_acquire_used set on the - * first standby), after which the node uses the normal duty cycle regardless - * of whether a fix was obtained. - * - A wake from a standby that was the full power-off: the same long window, - * every time. The module lost its state, so this is a cold start again, and - * in poor reception that does not fit the warm window. Decided from the - * interval like the power-off itself, never from the board. - * - All other windows: the normal (warm) acquire timeout. */ +/* Acquire-window timeout (ms) for the current phase: repeaters a fixed + * 5 minutes; the long first-fix window for the first acquisition after enable + * and after every full power-off; the warm timeout otherwise. */ static uint32_t gps_acquire_window_ms(void) { if (gps_repeater_mode) { @@ -538,14 +507,9 @@ static void gps_go_to_standby(void) * RTC survive the cut and re-acquisition is a warm/hot start, not cold. * Other non-GPIO boards: software sleep via UART commands (PMTK + UBX). */ gps_reapply_cancel(); - /* The module keeps its state (backup sleep, standby pin, VRTC) only for - * a short interval: that draws all the time, a cold start only for a - * few minutes, so past gps_standby_max_sec the full power-off is the - * cheaper one. Not a board property: every board takes the same - * decision, and gps_power_control() turns it into whatever the board's - * pins can do. Where they give one off state only (a bare supply - * switch, a bare standby pin, the UART sleep commands) both sides of - * the limit are that state. */ + /* Keep the module's state only for a short interval; past + * gps_standby_max_sec use the full power-off. The same decision on every + * board: gps_power_control() maps it onto the pins. */ standby_powered_off = wake_interval / 1000U > gps_standby_max_sec; gps_module_power(false, !standby_powered_off); @@ -563,15 +527,9 @@ static void gps_go_to_standby(void) k_work_schedule(&gps_wake_work, K_MSEC(wake_interval)); } -/* Wake GPS and start acquiring. - * GPIO boards: hardware power-on. - * Non-GPIO boards: UART wake byte (wakes L76K from standby, ZOE-M8Q from backup). - * Does NOT call gps_module_configure(). Calling modem_chat_run_script() here - * would deadlock: the chip needs ~300ms to boot after GPIO power restore, - * but modem_chat blocks the calling thread waiting for the system work - * queue which may be processing stale UART data. CASIC settings are NOT - * persisted (no PCAS00, see the air530z driver), so they are re-sent blind - * a second after power-on (CONFIG_ZEPHCORE_GPS_REAPPLY). */ +/* Wake GPS and start acquiring (power-on, or a UART wake byte). Must not + * run a modem_chat script here: it would deadlock. CASIC settings are re-sent + * a second later (CONFIG_ZEPHCORE_GPS_REAPPLY). */ static void gps_start_acquiring(void) { LOG_INF("GPS: Waking for %s", gps_repeater_mode ? "time sync" : "position fix"); @@ -664,18 +622,9 @@ static int gnss_init(void) } if (!device_is_ready(gnss_dev)) { - /* Root cause: GPS transmits NMEA immediately at power-up before - * modem_chat opens its DMA pipe. UARTE accumulates overrun/framing - * errors, causing modem_pipe_open() to fail and device_init to return - * an error. - * - * Strategy: use UARTE ERRORSRC as a real signal. Wait until errors - * appear (GPS is transmitting), clear them, then call device_init. - * This avoids arbitrary delays — we act when the hardware tells us - * conditions are ready, not after a fixed sleep. - * - * IMPORTANT: Do NOT use uart_poll_in() — it corrupts nRF52840 UARTE - * DMA state and breaks modem_pipe async receive. */ + /* The module sends NMEA before the modem pipe opens, and the UARTE errors + * that causes make device_init fail. Wait for those errors, clear them, then + * init. Do not use uart_poll_in() here. */ LOG_INF("GNSS device not ready — waiting for GPS activity on UART"); gps_power_control(true); @@ -748,13 +697,9 @@ static int gnss_init(void) } #ifdef CONFIG_PM_DEVICE - /* Some upstream GNSS drivers (gnss-nmea-generic) start suspended under - * CONFIG_PM_DEVICE and never open their modem pipe until resumed — no - * NMEA would ever flow (the old "PM broke GPS" trap). Resume once - * here: main thread at boot, the one safe context for the modem_chat - * scripts a resume may run. PM-less drivers (luatos,air530z) return - * -ENOSYS. Retried like device_init above — opening the pipe while - * the module is mid-sentence can fail transiently. */ + /* Some GNSS drivers start suspended under CONFIG_PM_DEVICE: resume once, + * here on the main thread. PM-less drivers return -ENOSYS. Retried like + * device_init above. */ int pm_ret = pm_device_action_run(gnss_dev, PM_DEVICE_ACTION_RESUME); for (int attempt = 1; pm_ret != 0 && pm_ret != -EALREADY && pm_ret != -ENOSYS && attempt < 3; attempt++) { @@ -1073,14 +1018,8 @@ void gps_request_fresh_fix(void) k_work_cancel_delayable(&gps_wake_work); gps_start_acquiring(); } else if (gps_current_state == GPS_STATE_ACQUIRING) { - /* Already acquiring — reschedule the timeout so the caller's - * fresh-fix request gets a full window from now. Otherwise, a - * telemetry request that arrives 25s into a 30s acquire window - * only has 5s left, which in marginal signal usually means the - * chip goes to standby before producing a fix the requester - * could use. Each duty phase has a bounded window - * (gps_acquire_window_ms); in always-on there's no timeout to extend - * (GPS is continuously acquiring and current_pos is always fresh). */ + /* Already acquiring: restart the timeout so the request gets a full window. + * Always-on has no timeout to extend. */ if (gps_duty_cycling()) { uint32_t timeout_ms = gps_acquire_window_ms(); LOG_INF("GPS: Fresh fix requested, extending acquire timeout to %u s", diff --git a/zephcore/adapters/gps/ZephyrGPSManager.h b/zephcore/adapters/gps/ZephyrGPSManager.h index 15cd4519..76d3d895 100644 --- a/zephcore/adapters/gps/ZephyrGPSManager.h +++ b/zephcore/adapters/gps/ZephyrGPSManager.h @@ -39,14 +39,8 @@ struct gps_state_info { }; /* ===== GPS configuration diagnostics (runtime toggle, RAM only) ===== - * - * Module configuration (PMTK/UBX or the GNSS API) is sent blind at boot and - * runs exactly once. With diag on, the next GPS enable re-runs it and records - * what actually happened, so an operator can power-cycle the GPS ("gps off" - * then "gps on") and read the outcome back over the CLI on a release build — - * no debug logging, no reflash. - * - * Not persisted: a diagnostic, not a setting. Clears on reboot. */ + * With diag on, the next GPS enable re-runs the module configuration and + * records the outcome for `get gps diag`. Not persisted. */ void gps_set_diag(bool on); /* Render the last configuration attempt as a single CLI line. Always @@ -70,17 +64,9 @@ void gps_set_enable_callback(gps_enable_callback_t cb); typedef void (*gps_fix_callback_t)(double lat, double lon, int64_t utc_time); void gps_set_fix_callback(gps_fix_callback_t cb); -/* GPS event callback - called when the GPS state machine needs the main thread - * to process a state transition (wake from standby, timeout, etc.). - * - * IMPORTANT: GNSS drivers use modem_chat_run_script() which blocks on a - * semaphore signaled from the system work queue. GPS timer work items also - * run on the system work queue → calling blocking GNSS APIs from a timer - * work handler deadlocks the system work queue. - * - * Solution: timer work handlers signal this callback, which posts an event - * to the main mesh thread. The main thread then calls gps_process_event() - * which safely executes the blocking GNSS configuration. */ +/* GPS event callback: the state machine needs the main thread. Timer work + * handlers must not call blocking GNSS APIs (they would deadlock the system + * work queue), so they signal here and main calls gps_process_event(). */ typedef void (*gps_event_callback_t)(void); void gps_set_event_callback(gps_event_callback_t cb); diff --git a/zephcore/adapters/gps/gps_internal.h b/zephcore/adapters/gps/gps_internal.h index 5d6aa1ce..847945c0 100644 --- a/zephcore/adapters/gps/gps_internal.h +++ b/zephcore/adapters/gps/gps_internal.h @@ -29,16 +29,8 @@ #endif /* ========== GPS Feature Detection ========== - * Every HAS_GPS_* predicate is defined HERE, before first use. They are pure - * devicetree tests with no side effects, kept apart from the variables they - * gate so that ordering can never drift again. - * - * Why this block exists: HAS_GPS_UART used to be defined ~500 lines below its - * first `#if`, and an undefined identifier in `#if` is silently 0 — so the - * entire PMTK/UBX module-configuration path compiled to nothing on every - * board, and GPS ran at module defaults (GPS-only constellations, no AOP). - * If you add another HAS_GPS_* macro, define it in this block. - */ + * Every HAS_GPS_* predicate is defined here, before first use: an undefined + * identifier in #if is silently 0. Add new ones to this block. */ /* GNSS module hangs off a UART we can write to (any compatible). */ #if HAS_GNSS && DT_NODE_HAS_STATUS(DT_NODELABEL(gnss), okay) && \ diff --git a/zephcore/adapters/gps/gps_module_cfg.cpp b/zephcore/adapters/gps/gps_module_cfg.cpp index 07a63644..e1e6e44d 100644 --- a/zephcore/adapters/gps/gps_module_cfg.cpp +++ b/zephcore/adapters/gps/gps_module_cfg.cpp @@ -21,17 +21,9 @@ static bool gps_diag_on = false; #if HAS_GNSS -/* Multi-constellation configuration — runs ONCE at boot. - * modem_chat_run_script() blocks on a semaphore signaled from the system - * work queue. Calling it after a GPIO power cycle can deadlock because: - * 1. The L76K needs ~300ms to boot after power restore - * 2. Meanwhile the modem_chat may be processing stale UART data - * 3. The script completion callback competes with NMEA processing - * - * Safe to call at boot because the driver init already ran and the chip - * is powered and outputting NMEA. PCAS settings are NOT persisted (nothing - * sends PCAS00), so what a power cut loses is re-sent blind after every - * power-on instead (gps_module_reapply_step, CONFIG_ZEPHCORE_GPS_REAPPLY). */ +/* Multi-constellation configuration, run once at boot: the one moment a + * modem_chat script is safe (the chip is up and streaming). Settings a power + * cut loses are re-sent blind after each power-on (gps_module_reapply_step). */ static bool gnss_configured = false; /* ========== Configuration Diagnostics ========== @@ -57,14 +49,9 @@ static struct { } gps_cfg_diag; -/* Module identification string, captured by the GNSS driver from the reply to - * its version query (CASIC parts answer in-band as a $GPTXT sentence). - * Weak so that boards whose driver has no version query still link — an - * absent symbol and an empty string mean the same thing to the report. - * - * Its real value is not the version text: on a transport where every command - * is written blind, a captured reply is the only positive proof that the - * MCU's TX line reaches the module at all. */ +/* Module identification string from the driver's version query; weak, so + * boards whose driver has none still link. A captured reply is also the only + * proof that our TX line reaches the module. */ extern "C" int zephcore_gnss_version_get(char *buf, size_t len) __attribute__((weak)); /* Count of NMEA sentences the GNSS driver has parsed. This is the one signal @@ -79,16 +66,8 @@ extern "C" uint32_t zephcore_gnss_rx_count(void) __attribute__((weak)); static bool gps_cfg_counting = false; /* ========== Vendor-Specific Configuration Commands ========== - * - * The RAK WisBlock GPS slot accepts multiple modules (L76K, ZOE-M8Q, etc.) - * and we use gnss-nmea-generic which is a passive NMEA listener — it has no - * GNSS API for configuration. - * - * Strategy: send BOTH Quectel PMTK and u-blox UBX configuration commands. - * Each module ignores the protocol it doesn't understand. - * - * This runs once at boot. Both modules persist config to internal flash, - * so these are effectively no-ops on subsequent boots. */ + * The generic-NMEA boards' GPS slot can hold any module, so the PMTK, PCAS and + * UBX configuration are all sent; each module ignores the others' protocol. */ #if HAS_GPS_UART @@ -96,15 +75,8 @@ static bool gps_cfg_counting = false; * regardless of compatible string. */ const struct device *const gps_uart_dev = DEVICE_DT_GET(DT_BUS(DT_NODELABEL(gnss))); -/* Send raw bytes to the GPS UART using blocking poll_out. - * Safe to call even though modem_chat/modem_ubx owns the UART pipe: - * uart_poll_out writes one byte at a time through the TX register, - * and GNSS modules are receive-only (no TX contention). - * - * Gated on HAS_GPS_UART alone — writing to the module is safe on every - * UART-attached GNSS. The narrower power-control gate below applies to the - * software *sleep* commands, which are only a fallback for boards that - * cannot cut GPS power. */ +/* Send raw bytes to the GPS UART with blocking poll_out. Safe beside the + * driver's pipe: GNSS modules do not contend for our TX line. */ void gps_uart_send(const uint8_t *data, size_t len) { if (!device_is_ready(gps_uart_dev)) { @@ -122,13 +94,7 @@ void gps_uart_send(const uint8_t *data, size_t len) } /* --- MediaTek-family (PMTK) configuration --- - * - * PMTK is MediaTek's protocol. It applies to genuine MTK parts (L76B and - * relatives). It does NOT apply to the Quectel L76K/L76KB or Air530Z, which - * are CASIC silicon and speak PCAS — their protocol specification contains no - * PMTK command at all, so these sentences are inert there. Those modules are - * driven by the air530z driver via the GNSS API instead and never reach this - * path. Kept because the RAK WisBlock GPS slot can hold an MTK part. */ + * For genuine MTK parts (L76B). Inert on CASIC parts (L76K, Air530Z). */ /* PMTK353: Enable GPS + GLONASS + Galileo + BeiDou (no QZSS). * Default is GPS-only. Multi-constellation dramatically improves TTFF @@ -148,15 +114,8 @@ static const char pmtk_easy[] = "$PMTK869,1,1*35\r\n"; static const char pmtk_aic[] = "$PMTK286,1*23\r\n"; /* --- CASIC (PCAS) configuration --- - * - * The Quectel L76K/L76KB (RAK12501) and Air530Z are CASIC silicon: they speak - * neither PMTK nor UBX, so without these sentences such a module in a - * WisBlock slot receives no configuration at all and sits on its factory - * defaults. Boards that always carry one use the air530z driver and the GNSS - * API instead; these are for the generic-NMEA boards whose GPS slot can hold - * any module. - * - * Inert on the other families, same as PMTK and UBX are here. */ + * For an L76K/L76KB or Air530Z in a generic-NMEA slot; boards that always + * carry one use the air530z driver instead. Inert on the other families. */ /* PCAS03: NMEA sentence selection. Field order is * GGA,GLL,GSA,GSV,RMC,VTG,ZDA,ANT,... — keep GGA + RMC (position, time) and @@ -210,23 +169,8 @@ static const char pcas_version_query[] = "$PCAS06,0*1B\r\n"; static const char pubx_version_query[] = "$PUBX,04*37\r\n"; /* --- u-blox NMEA output trim ($PUBX,40) --- - * - * THE LINK BUDGET IS THE CONSTRAINT, and it is easy to blow past it. At 9600 - * baud only ~960 bytes/s fit. With multi-GNSS enabled a u-blox emits, per - * second: GGA + RMC + GLL + VTG, one GSA per constellation, and a GSV burst - * that grows with satellite count — roughly 1030 bytes/s at ~26 SVs. The - * stream then cannot fit in the second it is generated in, sentences are - * truncated or dropped, and the symptom is not "slow GPS" but a receiver - * that appears to have stopped: no parseable GGA, so no fix, so no position. - * - * Enabling constellations without trimming output is therefore actively - * harmful on a 9600-baud link. We already do exactly this trim for CASIC - * parts via $PCAS03; u-blox had no equivalent, which is the asymmetry this - * fixes. GLL, GSA and VTG are dropped outright — nothing in the driver parses - * them — and GSV is kept only when the satellite tally needs it. - * - * Dropping GLL/GSA/VTG takes ~1030 -> ~650 bytes/s; dropping GSV as well - * takes it to ~160. */ + * At 9600 baud the multi-GNSS sentence set does not fit in a second, so GLL, + * GSA and VTG are switched off, and GSV unless the satellite tally needs it. */ static const char pubx_off_gll[] = "$PUBX,40,GLL,0,0,0,0,0,0*5C\r\n"; static const char pubx_off_gsa[] = "$PUBX,40,GSA,0,0,0,0,0,0*4E\r\n"; static const char pubx_off_vtg[] = "$PUBX,40,VTG,0,0,0,0,0,0*5E\r\n"; @@ -236,60 +180,18 @@ static const char pubx_off_gsv[] = "$PUBX,40,GSV,0,0,0,0,0,0*59\r\n"; /* --- u-blox ZOE-M8Q (UBX binary) configuration --- */ -/* UBX-CFG-PRT: force UART1 to 9600 8N1 with BOTH UBX and NMEA enabled in and - * out. Sent first, before anything that depends on the module talking to us. - * - * This exists because the module's port configuration is persistent and not - * necessarily ours. RAK's own RAK12500 example — and any host using the - * SparkFun u-blox library — calls setUART1Output(COM_TYPE_UBX) followed by - * saveConfiguration(), which stores "UBX only, NMEA off" in the module's - * flash. A module that has ever been driven that way stays silent on an - * NMEA-only host forever after, through power cycles and reflashes, and - * presents as a completely dead receiver: no GGA, no fix, no reply to any - * query. Re-asserting the port configuration costs one frame and removes a - * failure mode that is otherwise almost impossible to diagnose from the host. - * - * Limitation: if the module was also saved at a different baud rate, it will - * not parse this frame either. Recovering from that needs a baud scan, which - * the devicetree's fixed current-speed does not currently allow. */ +/* UBX-CFG-PRT: force UART1 to 9600 8N1 with UBX and NMEA in and out, sent + * first. A module saved as "UBX only" by another host is otherwise silent. */ static const uint8_t ubx_cfg_prt_uart1[] = { 0xB5, 0x62, 0x06, 0x00, 0x14, 0x00, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x08, 0x00, 0x00, 0x80, 0x25, 0x00, 0x00, 0x03, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8E, 0x95 }; -/* UBX-CFG-GNSS: Enable GPS + Galileo + GLONASS (+ QZSS) on u-blox M8. - * - * Config block layout is gnssId, resTrkCh, maxTrkCh, reserved0, flags[4] — - * EIGHT bytes, and the payload length must be exactly 4 + 8*numConfigBlocks - * or the receiver rejects the whole message. The previous version of this - * frame omitted reserved0, giving 7-byte blocks and a 39-byte payload where - * numConfigBlocks=5 demanded 44, so no u-blox module ever accepted it. - * - * Only THREE major GNSS (GPS/Galileo/GLONASS/BeiDou) can run concurrently on - * M8, so BeiDou is explicitly disabled rather than left alone: if the module - * came up with BeiDou on, enabling three others would make four and the - * message would be refused. - * - * QZSS is enabled even though it is Japan-regional and costs ~3 channels — - * u-blox require GPS and QZSS to be both enabled or both disabled (they - * share L1 C/A), and a mismatch is grounds for rejection. - * - * SBAS stays off: it needs 30-60 s to download corrections, useless for our - * quick-fix-then-sleep pattern (companions 30 s, repeaters 5 min). - * - * numTrkChHw = 0 and numTrkChUse = 0xFF (read-only / "use max available"). - * - * resTrkCh MUST be 0 on the disabled blocks. Reserving tracking channels for - * a system whose enable bit is clear is self-contradictory, and the receiver - * validates CFG-GNSS atomically — one bad block rejects all six. An earlier - * revision left SBAS at 1 and BeiDou at 8 and the whole frame was refused - * (observed on hardware: RAK12500/ZOE-M8Q stayed GPS-only, sys=G8/R0/E0/B0). - * - * NOTE — this is an M8 frame. u-blox 7 parts (MAX-7Q on the RAK1910) have no - * Galileo or BeiDou and use a different sigCfgMask, so they will refuse it - * and stay GPS-only. Sending an M7 frame as well is NOT safe blind: its - * sigCfgMask of 0 would be a signal-disabling value on M8. */ +/* UBX-CFG-GNSS for u-blox M8: GPS + Galileo + GLONASS + QZSS on, BeiDou and + * SBAS off. Blocks are 8 bytes each (payload 4 + 8*n); resTrkCh must be 0 on + * the disabled blocks, and GPS and QZSS go together. The receiver rejects + * the whole message for one bad block. u-blox 7 parts refuse it. */ static const uint8_t ubx_cfg_gnss[] = { 0xB5, 0x62, 0x06, 0x3E, 0x34, 0x00, 0x00, 0x00, 0xFF, 0x06, 0x00, 0x08, 0x10, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x00, 0x03, 0x00, 0x00, 0x00, @@ -298,33 +200,16 @@ static const uint8_t ubx_cfg_gnss[] = { 0x01, 0x01, 0x06, 0x08, 0x0E, 0x00, 0x01, 0x00, 0x01, 0x01, 0xEE, 0x64 }; -/* UBX-CFG-NMEA: switch the NMEA output to version 4.10. - * - * Without this, Galileo and BeiDou satellites cannot be reported at all: the - * $GAGSV and $GBGSV talker IDs only exist from NMEA 4.10, and M8 firmware - * defaults lower. So a fully successful CFG-GNSS would still show zero - * Galileo in "get gps diag" — a reporting limit masquerading as a config - * failure. Mirrors Meshtastic's "enable NMEA 4.10" step (src/gps/ubx.h). - * - * gsvTalkerId = 0 (use the GNSS-specific talker per constellation) is what - * makes the per-constellation tally work — do not set it to 1, which forces - * every GSV onto the main talker and would collapse the tally into GPS. */ +/* UBX-CFG-NMEA: NMEA 4.10, without which Galileo and BeiDou cannot be + * reported. gsvTalkerId must stay 0 for the per-constellation tally. */ static const uint8_t ubx_cfg_nmea_410[] = { 0xB5, 0x62, 0x06, 0x17, 0x14, 0x00, 0x00, 0x41, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x75, 0x57 }; -/* UBX-CFG-NAV5: Set 5° minimum satellite elevation. - * Ignore satellites below 5° elevation — they have more atmospheric - * noise and multipath, degrading fix quality. The default 0° lets in - * everything including horizon-level junk. - * Dynamic model left at factory default (Portable) — works for fixed - * repeaters, walking companions, and vehicles alike. - * apply mask 0x0002 = minEl(bit1) only - * - * The trailing checksum was CK_B=0x37 (should be 0xE7) — a one-byte typo - * that made every receiver drop this frame silently, with no NAK. */ +/* UBX-CFG-NAV5: 5 degree minimum satellite elevation (apply mask 0x0002); + * the dynamic model stays at the factory default. */ static const uint8_t ubx_cfg_nav5_minelev[] = { 0xB5, 0x62, 0x06, 0x24, 0x24, 0x00, 0x02, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, @@ -450,13 +335,8 @@ static void gps_configure_via_uart(void) LOG_INF("GPS: PCAS config sent (sentences, constellations, nav mode, version query)"); /* --- u-blox ZOE-M8Q (UBX) --- - * NMEA 4.10 goes first: it governs whether the constellations enabled - * by the next frame can be *reported* at all. CFG-GNSS restarts the - * navigation engine, so it gets the long settle before the frames that - * follow it — at 50 ms they were being issued into a restarting - * receiver. CFG-CFG stays last so it only persists whatever was - * actually accepted; note that makes a rejected configuration sticky - * too, which is why a frame bug here survives power cycles. */ + * Order matters: NMEA 4.10 first, CFG-GNSS with a long settle (it restarts + * the navigation engine), CFG-CFG last so only accepted settings are saved. */ /* Make sure NMEA output is even switched on before anything else — a * module saved as UBX-only by a previous host is otherwise mute. */ gps_send_ubx(ubx_cfg_prt_uart1, sizeof(ubx_cfg_prt_uart1), GPS_CFG_RESTART_MS); @@ -487,14 +367,9 @@ static void gps_configure_via_uart(void) } #endif /* HAS_GPS_UART */ -/* Re-run module configuration on a GPS enable, but only when diagnostics are - * armed — this is a deliberate, operator-triggered action, not a normal path. - * - * Only the raw-UART path is re-runnable. gps_module_configure()'s API path goes - * through modem_chat_run_script(), which is safe at boot only: after a GPIO - * power restore the chip needs ~300 ms and calling it here deadlocks the main - * thread. So on API-driver boards this records nothing new and "get gps diag" - * keeps reporting the boot-time result, which is the honest answer. */ +/* Re-run module configuration on a GPS enable, only when diagnostics are + * armed. Only the raw-UART path can be re-run; on API-driver boards + * `get gps diag` keeps the boot-time result. */ void gps_diag_maybe_reconfigure(void) { if (!gps_diag_on || gnss_configured || gnss_dev == NULL) { @@ -568,25 +443,9 @@ void gps_module_configure(void) } #if HAS_GPS_UART && defined(CONFIG_ZEPHCORE_GPS_REAPPLY) -/* The boot-time settings a power cut loses (see the Kconfig help). One - * sentence per step, written with gps_uart_send() directly and no settle - * sleep: the caller waits each entry's gap with a work item, so the main - * thread only spends the UART time. - * - * Kept boot-only on purpose: - * - UBX: already saved to BBR + flash by CFG-CFG at boot, and repeating it - * would rewrite the module's flash on every wake; - * - PCAS02 (1 Hz is the default) and PCAS06 (a query, no state). - * - * Both constellation commands restart their chip's navigation engine, and a - * sentence written into a restarting engine is lost, so each is followed by - * the boot sequence's restart settle: PMTK353 first, PCAS04 last. Each only - * restarts its own family's chip (the other ignores it). On a MediaTek part - * that merely slept this costs one engine restart per wake, accepted - * (architect, 2026-09-27): one that lost power is otherwise left searching - * GPS-only, which is slower than a restart with every constellation. The - * PMTK sentences go only where the module is unknown (the generic-NMEA - * path); an API-driver board's module is known CASIC. */ +/* The boot-time settings a power cut loses, one sentence per step; the caller + * waits each entry's gap with a work item. UBX, PCAS02 and PCAS06 stay + * boot-only. Each constellation command is followed by its restart settle. */ struct reapply_cmd { const char *sentence; uint16_t gap_ms; /* wait before the next one */ diff --git a/zephcore/adapters/gps/gps_power.cpp b/zephcore/adapters/gps/gps_power.cpp index ac70bc27..e3750b9e 100644 --- a/zephcore/adapters/gps/gps_power.cpp +++ b/zephcore/adapters/gps/gps_power.cpp @@ -15,44 +15,14 @@ LOG_MODULE_DECLARE(zephcore_gps, CONFIG_ZEPHCORE_GPS_LOG_LEVEL); /* ========== GPS Power Strategy ========== - * Module power is GPIO/regulator controlled — GNSS driver PM is not used - * for the module itself: - * - Wio Tracker L1 (L76K): P1.09 is the module's WAKEUP pin, not a supply - * switch. Per the L76K hardware design: WAKEUP is a digital input, active - * low with an internal pull-up, that "enters or exits Standby mode". In - * Standby the RF is powered off but the internal core and I/O power domain - * stay active, so VCC is never removed and ephemeris/almanac/RTC survive — - * every wake is a warm start, not a cold one. (Backup mode, the deeper - * state, requires cutting VCC while V_BCKP holds the RTC domain; this - * board has no VCC switch, so Standby is the floor available to us.) - * - T1000-E, MeshTracker X1 (AG3335): RTC backup sleep ($PAIR650,0), then - * GPS_EN de-asserted with VRTC kept; a GPS_RTC_INT pulse wakes it - * - All boards: gps-enable alias → GPIO power control - * - * CONFIG_PM_DEVICE is on globally, but nothing suspends automatically — - * system-managed suspend is compiled only under CONFIG_PM (off everywhere). - * This manager makes exactly two kinds of PM calls, both main-thread only: - * - a one-time RESUME of the GNSS device at boot (gnss-nmea-generic inits - * suspended under CONFIG_PM_DEVICE and never opens its pipe otherwise); - * - suspend/resume of the GNSS UARTE around standby/off (an armed UARTE RX - * holds HFCLK ≈0.5-1 mA on nRF52840 even with the module powered off). - * The old "PM broke GPS" deadlock was modem_chat_run_script() being reached - * from the system workqueue via driver PM hooks — the air530z driver is - * PM-less now and every PM call here stays on the main thread. */ + * The module is switched by GPIO or regulator, not by GNSS driver PM. The two + * PM calls made here, both on the main thread: a one-time RESUME of the GNSS + * device at boot, and suspend/resume of the GNSS UARTE around standby/off. */ /* ========== GPS Power GPIO Control ========== - * These are unconditional (not gated by HAS_GNSS) because - * gps_power_off_for_shutdown() must be available for System OFF - * even on boards without a GNSS driver. - * - * IMPORTANT: Do NOT touch GPIO during init! The GNSS driver needs the GPS - * to be powered and outputting NMEA for the modem pipe to work. - * We only configure GPIO lazily on first power-off request. - * - * Board-specific pins (defined in board overlays as gps-enable alias): - * - T1000-E: P1.11 (GPS_EN), P0.8 (GPS_VRTC_EN), P1.15 (GPS_RESET), P1.12 (GPS_SLEEP_INT) - * - Wio Tracker L1: P1.09 (GPS power, shared with luatos,air530z on-off-gpios) - */ + * Not gated by HAS_GNSS: gps_power_off_for_shutdown() must exist for System + * OFF on every board. No GPIO is touched during init (the GNSS driver needs + * the module powered); the lines are configured on the first power-off. */ #if HAS_GPS_POWER_CONTROL static const struct gpio_dt_spec gps_enable_gpio = GPIO_DT_SPEC_GET(DT_ALIAS(gps_enable), gpios); #endif @@ -144,13 +114,9 @@ static const struct gpio_dt_spec gps_resetb_gpio = GPIO_DT_SPEC_GET(DT_ALIAS(gps #define HAS_GPS_RESETB 0 #endif -/* Module standby pin (L76K WAKEUP: asserted = run, de-asserted = Standby, RF - * off with the core powered) on a board that ALSO has a supply switch - * (gps-enable): standby uses this pin and keeps the supply, so a wake is a - * hot start; a full off (`gps off`, a standby past prefs.gps_standby_max, - * boot with GPS disabled, System OFF) cuts the supply, since Standby still - * draws. A board whose only control is the standby pin (Wio Tracker L1) - * declares it as gps-enable instead. */ +/* Module standby pin (asserted = run) on a board that also has a supply + * switch: standby uses the pin, a full off cuts the supply. A board whose only + * control is the standby pin declares it as gps-enable instead. */ #if HAS_GPS_WAKEUP static const struct gpio_dt_spec gps_wakeup_gpio = GPIO_DT_SPEC_GET(DT_ALIAS(gps_wakeup), gpios); #endif @@ -192,16 +158,9 @@ static void gps_enter_backup_sleep(void) #endif /* The off levels, shared by gps_power_control(false) and System OFF: reset - * and resetb asserted (the module held in reset while its supply is gated, - * so it starts cleanly on the next power-on), the enable and rtcint - * de-asserted, VRTC de-asserted unless keep_vrtc (warm standby). - * - * The sleep line is the one difference: runtime standby/off leaves it - * asserted, System OFF drops it -- as upstream, whose T1000-E sleep_gps() and - * stop_gps() drive GPS_SLEEP_INT HIGH and only powerOff() drives it LOW. - * - * gpio_pin_configure_dt() on every line: a boot with the GPS off never ran - * the power-on path, so the pins may not be outputs yet. */ + * asserted, enable and rtcint de-asserted, VRTC de-asserted unless keep_vrtc. + * The sleep line stays asserted at runtime and is dropped for System OFF. + * Every line is configured here: a boot with the GPS off never ran power-on. */ static void gps_drive_off_levels(bool keep_vrtc, bool system_off) { #if HAS_GPS_WAKEUP @@ -249,15 +208,11 @@ static void gps_drive_off_levels(bool keep_vrtc, bool system_off) #endif } -/* GPS power control with warm standby support. +/* GPS power control. * @param on true = power on, false = power off - * @param keep_vrtc When powering off: true = the state-keeping off this board - * has (AG3335 backup sleep, the gps-wakeup standby pin, VRTC - * left on: ephemeris/almanac/RTC survive for a fast - * re-acquisition), false = full power-off (cold start on - * next wake). The caller decides from the standby length - * alone (prefs.gps_standby_max), never from the board; a - * board with one off state lands in it either way. */ + * @param keep_vrtc when powering off: true = the state-keeping off this board + * has, false = full power-off. A board with one off state + * lands in it either way. */ void gps_power_control(bool on, bool keep_vrtc) { #if HAS_GPS_POWER_REGULATOR @@ -275,15 +230,7 @@ void gps_power_control(bool on, bool keep_vrtc) } #endif #if HAS_GPS_POWER_CONTROL - /* Direct GPIO power control — works on all boards. - * We toggle the GPS power pin ourselves rather than using driver PM - * (driver PM can hang on modem_pipe_close / modem_chat_run_script). - * The GNSS driver's modem pipe stays open. - * - * T1000-E / X1 (HAS_GPS_BACKUP_SLEEP): standby up to - * prefs.gps_standby_max uses the AG3335 RTC backup sleep, as Arduino - * stop_gps(); a longer standby and `gps off` cut VRTC too. - * Simple boards (Wio etc.): Full power off/on via GPS_EN. */ + /* We switch the module ourselves; the GNSS driver's modem pipe stays open. */ if (on) { #if HAS_GPS_WAKEUP if (gpio_is_ready_dt(&gps_wakeup_gpio)) { @@ -291,22 +238,9 @@ void gps_power_control(bool on, bool keep_vrtc) } #endif #if HAS_T1000_GPS_CONTROL - /* T1000-E power-on sequence (from Arduino target.cpp start_gps()) - * Must follow this exact order with delays. Levels are ASSERTED / - * DE-ASSERTED, not physical: Arduino states them as HIGH/LOW because - * its pins are all active-high, while this block is also reached by - * boards whose lines are active-low (see the gate below). - * 1. GPS_EN asserted, delay 10ms - * 2. GPS_VRTC_EN asserted, delay 10ms (critical - RTC power) - * 3. GPS_RESET asserted, delay 10ms, then released - * 4. GPS_SLEEP_INT asserted - * - * Despite the name this is not a T1000-E-only path: - * HAS_T1000_GPS_CONTROL is (HAS_GPS_VRTC || HAS_GPS_RESET || - * HAS_GPS_SLEEP), so a bare gps-reset alias is enough to route a - * board here. heltec_wifi_lora32_v4, _v43 and thinknode_m9 all arrive - * this way, and all three declare gps-enable active-low. - */ + /* Power-on sequence, in this order with delays (Arduino start_gps()): + * enable, VRTC, reset pulse, sleep line. Levels are asserted/de-asserted, not + * physical; a bare gps-reset alias is enough to route a board here. */ if (gpio_is_ready_dt(&gps_enable_gpio)) { gpio_pin_configure_dt(&gps_enable_gpio, GPIO_OUTPUT_ACTIVE); } @@ -431,66 +365,18 @@ void gps_power_off_for_shutdown(void) #if HAS_GNSS /* ========== Software Sleep/Wake (no GPIO required) ========== - * - * On boards without dedicated GPS power control (e.g. RAK3401 where the - * 3V3_S rail is shared with the LoRa FEM), we send vendor-specific UART - * commands to put the GPS module into low-power mode. - * - * Strategy: send BOTH MediaTek and u-blox sleep commands — the module that - * isn't present simply ignores the bytes it doesn't understand. - * - * - MediaTek (e.g. L76B): $PMTK161,0*28\r\n → standby (~1mA), wake on UART - * - u-blox ZOE-M8Q (RAK12500): UBX-RXM-PMREQ → backup (~7µA), wake on UART - * - * Neither reaches a CASIC part (L76K/L76KB/Air530Z): those ignore PMTK, UBX - * and PCAS12 sleep commands alike — verified on hardware, which is why the - * boards carrying them duty-cycle with a power GPIO instead. Note also that - * the RAK1910 is a u-blox MAX-7Q, not an L76K, despite older comments here. - * - * Wake: any byte on UART wakes both modules from their low-power modes. - * After wake, the module resumes outputting NMEA autonomously. - */ + * Boards without GPS power control: both the MediaTek and the u-blox sleep + * command are sent, and the absent module ignores the other's bytes. Any byte + * on the UART wakes either. CASIC parts ignore both. */ /* HAS_GPS_UART, gps_uart_dev and gps_uart_send are defined near the top of * this file (see "GPS Feature Detection") — they are needed by the boot-time * module configuration, which runs long before this section. */ /* ========== GNSS UART Suspend/Resume (device PM) ========== - * nRF UARTE only. An armed UARTE RX holds HFCLK (~0.5-1 mA on nRF52840) - * even when the GPS module is powered off or silent, so standby/off - * suspends the UART device and every wake resumes it first. - * - * Verified symmetric in uart_nrfx_uarte.c under the still-open modem pipe: - * suspend saves the RX-interrupt state, STOPRXes, disables the peripheral - * and applies the sleep pinctrl; resume restores all of it. Other UART - * drivers (legacy nordic,nrf-uart on RAK4631, ESP32) are deliberately not - * gated in — their suspend/resume round-trip is unverified and the HFCLK - * cost is UARTE-specific. - * - * Every GPS UART node must carry a sleep pinctrl state (all boards do): - * without one, suspend fails *after* disabling RX while the PM state stays - * ACTIVE, so the next resume no-ops with -EALREADY — a dead GPS. - * - * Safe on boards WITHOUT GPS power control too, since patch 0010. - * uarte_pm_suspend() used to busy-wait for RXTO with no timeout after - * triggering STOPRX. In interrupt-driven mode RX runs on a 1-byte buffer - * with no ENDRX_STARTRX short, so the receiver stops after every byte until - * the ISR re-arms it — and suspend disables the ENDRX interrupt *before* - * STOPRX, removing the re-arm. Land in that window with bytes still arriving - * and STOPRX hits an already-stopped receiver, no RXTO is generated, and the - * caller spun forever on the main thread (observed: RAK3401 1W repeater on - * 1.16.6, CLI answering only "-> busy"). That is why this gate once required - * power control: boards with it cut the module first, so the line is quiet. - * Boards without it send gps_software_sleep()'s PMTK/UBX commands, which the - * module may ignore (CASIC parts; a MAX-7Q is protocol 14/15, older than the - * UBX-RXM-PMREQ we send) and keep streaming NMEA straight into the suspend. - * - * Patch 0010 bounds that wait (4 ms, then the unconditional - * nrf_uarte_disable() force-stops the receiver) and clears the RX events, so - * the race can no longer hang the caller, and resume re-arms RX from a clean - * state. So every nRF UARTE GPS board releases its UART in standby/off now, - * including an optional GPS port with nothing fitted (xiao_nrf54l15 uart21). - * Bench-tested by the streaming-into-suspend case: devdocs/lld/13. */ + * nRF UARTE only: an armed UARTE RX holds HFCLK even with the module off, so + * standby/off suspends the UART and every wake resumes it first. Every GPS + * UART node needs a sleep pinctrl state. Relies on patch 0010. */ #if HAS_GPS_UART && defined(CONFIG_PM_DEVICE) && \ DT_NODE_HAS_COMPAT(DT_BUS(DT_NODELABEL(gnss)), nordic_nrf_uarte) #define HAS_GPS_UART_PM 1 @@ -509,18 +395,8 @@ void gps_uart_set_power(bool on) return; } if (!on) { - /* Let the GNSS line go quiet before suspending. Every caller - * cuts module power (GPS_EN low / reset asserted / regulator - * off) immediately before this, but a byte can still be in - * flight. Settle so it finishes and the driver's RX ISR re-arms, - * leaving the receiver armed-and-idle when the suspend's STOPRX - * fires — that state yields RXTO, whereas a just-stopped, - * un-rearmed receiver can produce none and (pre-0010) hung the - * main thread. ~5 ms comfortably covers one character time at - * GNSS baud plus ISR latency; standby happens at most every few - * minutes, so the cost is negligible. Backstop: patch 0010 - * bounds the driver's RXTO wait so a missed RXTO can never hang - * us even if a byte still lands in the race window. */ + /* Let a byte in flight finish and the RX ISR re-arm before the suspend's + * STOPRX. */ k_msleep(5); } int ret = pm_device_action_run(gps_uart_dev, @@ -546,29 +422,9 @@ void gps_uart_set_power(bool on) { ARG_UNUSED(on); } * Inert on CASIC parts (L76K and relatives) — they have no such command. */ static const uint8_t pmtk_standby[] = "$PMTK161,0*28\r\n"; -/* u-blox ZOE-M8Q: UBX-RXM-PMREQ → enter backup mode - * UBX frame: B5 62 | 02 41 | 10 00 | payload(16) | CK_A CK_B - * Payload (protocol 23+, 16 bytes): - * version=0, reserved[3]=0, - * duration=0x00000000 (infinite), - * flags=0x00000006 (backup + force), - * wakeupSources=0x00000028 (uartrx bit3 | extint0 bit5) - * - * THE WAKE SOURCE BIT IS LOAD-BEARING. wakeupSources bit 3 is uartrx; bit 5 - * is extint0. This frame previously sent 0x20 — extint0 only — with a comment - * claiming that was "UART RX (bit 5)". It is not. EXTINT is not routed to the - * WisBlock connector on the RAK12500 (RAK's datasheet: only UART/I2C, 1PPS, - * RESET, VDD and GND are connected), so the module was told to sleep forever - * with a wake source that can never be asserted. duration=0 means infinite, - * so it never came back: no NMEA at any baud, and no I2C either, because - * backup mode powers down the DDC interface too. Only a physical power cycle - * recovered it — a reboot does not, since the 3V3_S rail stays up. - * Confirmed on hardware: reseat -> module answers -> one fix -> first standby - * -> gone again. - * - * 0x28 sets both, so a board that does wire EXTINT keeps that path as well. - * Module stops all output and draws ~20µA (ZOE-M8Q at 3V, datasheet Table 13; - * the 15µA hardware-backup figure needs VCC removed entirely). */ +/* u-blox ZOE-M8Q: UBX-RXM-PMREQ, enter backup mode until a wake source fires. + * wakeupSources = 0x28: uartrx (bit 3) | extint0 (bit 5). uartrx must be set: + * EXTINT is not wired on the RAK12500, and without it the module never wakes. */ static const uint8_t ubx_pmreq_backup[] = { 0xB5, 0x62, /* UBX sync chars */ 0x02, 0x41, /* Class: RXM, ID: PMREQ */ diff --git a/zephcore/adapters/radio/LoRaRadio.cpp b/zephcore/adapters/radio/LoRaRadio.cpp index 55d413f9..46653f91 100644 --- a/zephcore/adapters/radio/LoRaRadio.cpp +++ b/zephcore/adapters/radio/LoRaRadio.cpp @@ -433,17 +433,9 @@ void LoRaRadio::configure(bool tx) return; } - /* Fast path: if only the TX/RX direction changed, skip the full - * hwConfigure → lora_config() call. The driver already has a valid - * config for the target direction (RadioSetRxConfig / RadioSetTxConfig - * with TxTimeout=4000) from a previous cycle — Radio.Rx(0) / Radio.Send() - * will use those register values directly. This avoids the - * modem_acquire → modem_release → Radio.Sleep() round-trip that wastes - * ~5 ms on every TX↔RX transition. - * - * Not used for loramac-node: Radio.SetTxConfig() and Radio.SetRxConfig() - * configure completely disjoint internal state (including TxTimeout). - * Skipping either on a direction change leaves that state uninitialized. */ + /* Fast path: only the TX/RX direction changed, so skip lora_config(); the + * driver keeps a valid config for each direction. Not for loramac-node, + * whose TX and RX configs are disjoint state. */ if (!_ops.loramac_node && _config_cached && onlyDirectionDiffers(cfg, _last_cfg)) { LOG_DBG("%s: direction-only change, skip hwConfigure", who); _last_cfg = cfg; diff --git a/zephcore/adapters/radio/radio_common.h b/zephcore/adapters/radio/radio_common.h index 64cbe2d8..59f0a603 100644 --- a/zephcore/adapters/radio/radio_common.h +++ b/zephcore/adapters/radio/radio_common.h @@ -14,41 +14,15 @@ #include "radio_tuning.h" /* --- RSSI read timing (SX1261/2 DS rev 2.2 Table 13-82) --- - * - * The median-of-N above only rejects outliers if the N reads are actually - * independent. The chip updates RSSI once per "averaging window"; reads - * issued faster than that return the same underlying sample repeatedly, and - * the median degenerates into one read with extra SPI traffic. - * - * The published table is indexed by GFSK channel-filter bandwidth. Across - * all 19 rows the product window_us * BW_kHz lands in 921..938, so - * 936 / BW_kHz reproduces every published value to within a microsecond. - * The separate "RSSI delay" column (BUSY falling edge -> first valid sample) - * is 12x to 15x the window across the same rows. - * - * CAVEAT: Semtech documents this for GFSK only and publishes no LoRa - * equivalent. The RSSI path is the same analog/AGC chain, so these are used - * as the best available proxy — not as specified LoRa figures. `get cad` - * reports the measured burst spread so the assumption stays falsifiable on - * real hardware. - */ + * Reads faster than the chip's averaging window return the same sample. + * window_us = 936 / BW_kHz reproduces the table; it is specified for GFSK and + * used here for LoRa as the best available figure. */ #define RSSI_WINDOW_BW_PRODUCT 936U /* window_us * BW_kHz, DS Table 13-82 */ #define RSSI_SETTLE_WINDOWS 16U /* delay/window is 12..15; round up */ -/* Burst-stat rescale point. The counters behind `get cad`'s sp field are - * halved (all three together, so the mean and the zero-spread share are - * preserved exactly) once the burst count reaches this. - * - * Two reasons, and the display one is the hard constraint: the remote CLI - * reply is capped at CLI_REMOTE_REPLY_SIZE (161 B) and has to fit the header - * plus three level lines, so no field may grow without bound. At one burst - * per 15 s a free-running counter passes 500 000 in three months and would - * eat the level rows. 8192 keeps it to four digits forever. - * - * The second reason is that halving turns the totals into an exponential - * forgetting window, so the numbers describe recent conditions instead of - * being anchored to whatever the channel was doing at boot. Same idea as - * CAD_STATS_DECAY_MS below, which halves the CAD counters on a timer. */ +/* Burst-stat rescale point: the counters behind `get cad`'s sp field are + * halved together at this count, which keeps the reply inside the remote CLI + * limit and makes the totals describe recent conditions. */ #define RSSI_BURST_STATS_CAP 8192U static inline uint32_t rssi_avg_window_us(uint16_t bw_khz) @@ -71,16 +45,8 @@ static inline uint32_t rssi_settle_delay_us(uint16_t bw_khz) * the every-16th-sample unguarded bypass are counted in samples, so they scale * with this value — see the Kconfig help before changing it. */ #define NOISE_FLOOR_INTERVAL_MS CONFIG_ZEPHCORE_NOISE_FLOOR_INTERVAL_MS -/* A due sample that lands while the radio is mid-packet, transmitting, or in - * its duty-cycle sleep window is retried on this deadline rather than waiting a - * full interval. - * - * 5000 is not a tuned guess: before the deadline conversion a blocked sample - * simply waited for the next 5 s housekeeping tick, so this reproduces the old - * retry grid exactly. It matters under RX duty cycle, where the chip is in its - * sleep window a large fraction of the time and blocked attempts are the norm - * rather than the exception — a shorter retry there can push the wake rate - * ABOVE the fixed tick this conversion replaced, inverting the whole point. */ +/* A due sample that lands mid-packet, during TX or in the duty-cycle sleep + * window is retried after this long (the old housekeeping grid). */ #define NOISE_FLOOR_RETRY_MS 5000 /* Blocked attempts allowed before standing down to the next full interval. */ #define NOISE_FLOOR_MAX_RETRIES 2 @@ -144,14 +110,8 @@ static inline enum lora_signal_bandwidth bw_khz_to_enum(uint16_t bw_khz) case 125: return BW_125_KHZ; case 250: return BW_250_KHZ; case 500: return BW_500_KHZ; - /* Wide bandwidths, accepted under both spellings: the round name people - * type and the chip's true value they may read off a datasheet. Both - * select the same modem setting. - * - * Only the LR2021 implements these; every other driver here falls back - * to 125 kHz for an unmapped enum, which would be a silent mismatch. - * That is why the CLI only accepts a bandwidth above 500 on an LR2021 - * build — see the bw range check in CommonCLI.cpp. */ + /* Wide bandwidths, under both spellings (the round name and the chip's true + * value). LR2021 only; the CLI rejects them elsewhere. */ case 200: case 203: return BW_200_KHZ; case 400: case 406: return BW_400_KHZ; case 800: case 812: return BW_800_KHZ; @@ -160,22 +120,9 @@ static inline enum lora_signal_bandwidth bw_khz_to_enum(uint16_t bw_khz) } } -/* Lowest physically-possible noise floor for a given bandwidth, in dBm. - * - * This is raw thermal noise — kTB at 290 K, with NO noise-figure term: - * -174 dBm/Hz + 10*log10(BW_Hz) - * A passive receiver cannot read below it, so it is the one place a sanity - * clamp belongs: anything under this line is a bad RSSI read, not a quiet - * site. Adding a receiver noise figure here would clamp ABOVE what the - * hardware can legitimately report and manufacture a floor — which is exactly - * what the old fixed -120 rail did to BW 62.5 kHz, pinning it on every EMA - * update because -120 happens to be that preset's kTB+NF. - * - * Bandwidth is the only term that moves. SF changes the SNR the demodulator - * can decode at, not the noise power in the channel, so it must NOT appear. - * - * For reference, a typical SX126x (NF ~6 dB) reads about 6 dB above these: - * BW 62.5 kHz measures ~-120 dBm on a quiet site against a -126 kTB limit. */ +/* Lowest physically possible noise floor for a bandwidth, in dBm: thermal + * noise kTB at 290 K, -174 dBm/Hz + 10*log10(BW_Hz), with no noise figure and + * no SF term. A reading below it is a bad RSSI read. */ static inline int16_t noise_floor_min_dbm(uint16_t bw_khz) { switch (bw_khz) { diff --git a/zephcore/adapters/rng/ZephyrRNG.cpp b/zephcore/adapters/rng/ZephyrRNG.cpp index 20492e4d..a44b733f 100644 --- a/zephcore/adapters/rng/ZephyrRNG.cpp +++ b/zephcore/adapters/rng/ZephyrRNG.cpp @@ -41,21 +41,9 @@ void ZephyrRNG::random(uint8_t *dest, size_t sz) Utils::cryptoPanicReboot("CSPRNG unavailable after retries"); } -/* ===== Timing-entropy health check ======================================= - * - * Online health check (NIST SP 800-90B style) for the two-clock beat source - * below: repetition count + a distinct-value check tracked across all samples - * in the window with scalar state — no per-sample buffer needed. Detects - * stuck-source catastrophic failure (e.g. a frozen slow clock). Does not - * statistically prove entropy quality — that's what the selftest - * output-diversity run is for. - * - * (The former CPU-jitter fallback — Stephan Müller style k_cycle_get_32() - * delta sampling — was removed: it only ever carried entropy where the cycle - * counter was already cross-domain from the CPU, and every such board is - * exactly a board the beat covers. Where the beat is unavailable the counter - * is same-domain, the loop is deterministic, and jitter yields ~0 bits — - * measured dead on ESP32 hardware.) */ +/* ===== Timing-entropy health check ===== + * Repetition count plus a distinct-value check over the window, with scalar + * state. Detects a stuck source; it does not prove entropy quality. */ /* Health statistics for one beat window. Timing statistics only — never * pool contents or derived key material. Reporting these is standard practice @@ -90,45 +78,11 @@ struct beat_stats { bool ok; }; -/* ===== Universal two-clock beat entropy ================================== - * - * One physical entropy source for every board: count CPU cycles elapsed across - * a fixed interval of an INDEPENDENT low-frequency oscillator. The two clocks - * come from different sources, so the count fluctuates with the slow - * oscillator's phase noise — real physical entropy, not the deterministic - * same-domain loop that CPU-jitter degenerates to where the cycle counter and - * CPU share a clock (measured dead on ESP32: thousands of identical deltas). - * - * FAST counter = timing_counter_get() — portable CPU cycle counter (DWT on - * Cortex-M, CCOUNT on Xtensa; both at CPU frequency). Needs - * CONFIG_TIMING_FUNCTIONS and a one-time timing_init()/timing_start(). - * - * SLOW clock = an oscillator in a DIFFERENT domain from the CPU, selected by a - * principled rule so the choice is coherent across boards: - * - ESP32: the RTC-slow oscillator via esp_rtc_get_time_us() (internal - * ~136 kHz RC, independent of the XTAL->PLL CPU path). - * - Any board whose Zephyr system timer runs < 1 MHz: that timer IS a - * low-frequency oscillator cross-domain from the CPU (e.g. nRF's - * 32.768 kHz RTC off LFXO/LFRC), so k_cycle_get_32() is a valid slow - * clock. REQUIRES the LF clock to be LFXO/LFRC, not synthesised from - * HFCLK — true for every BLE-capable nRF config; the health check below - * catches it if a board ever violates that. - * - Otherwise (system timer at CPU frequency, e.g. bare SysTick): no - * independent slow clock is identified and the timing stages are - * SKIPPED — a same-domain counter measures a deterministic loop - * (~0 bits, measured), so sampling it would only pretend to add - * entropy. Such boards (STM32WL SysTick, nRF54L 1 MHz GRTC) rely on - * their true TRNG via the CSPRNG stages, which is what the removed - * CPU-jitter fallback effectively did anyway. - * - * Window = 500 us, from an on-hardware ESP32 sweep (memory/findings.md): - * 120/250/500/1000 us gave 1.85/3.04/3.81/5.14 bits/sample; 500 us is the knee. - * Full 32-bit delta is mixed. On ESP32 this is a SECONDARY source (the - * bootloader_random-seeded HWRNG is primary); on nRF it is the strong - * non-HWRNG leg. - * - * CAVEAT (memory/findings.md): the health stats show the beat VARIES, not that - * it is random — the selftest output-diversity run is what validates it. */ +/* ===== Universal two-clock beat entropy ===== + * CPU cycles counted across a fixed interval of an independent slow + * oscillator: ESP32's RTC-slow clock, or a system timer below 1 MHz (nRF's + * 32.768 kHz RTC). Where no independent slow clock exists the timing stages + * are skipped and the board relies on its TRNG. Window 500 us. */ #if defined(CONFIG_SOC_FAMILY_ESPRESSIF_ESP32) #define BEAT_SLOW_HZ 1000000ULL static inline uint64_t beat_slow_ticks(void) { return esp_rtc_get_time_us(); } @@ -236,15 +190,9 @@ static int distinct_bytes(const uint8_t *buf, size_t len) return n; } -/* One line per beat window. `distinct` is capped at 8 by the sampler, so 8/8 - * means "at least 8" — the pass threshold is 5. `maxrep` is the longest run of - * identical deltas; >=32 fails. The span/distinct/maxrep line IS the beat's - * health — per-sample entropy is characterised offline by the selftest window - * sweep, not estimated here (an in-path MCV estimate would need a 16k-slot - * histogram). Deliberately no statistics on the conditioned output: - * AES-256-CTR makes any input look uniform, so output statistics would read - * perfect even for a near-zero-entropy seed. Entropy is a property of the - * source. */ +/* One line per beat window: `distinct` is capped at 8 (pass at 5), `maxrep` + * is the longest run of identical deltas (fail at 32). No statistics on the + * conditioned output: AES-CTR makes any input look uniform. */ #ifdef HAVE_TWO_CLOCK_BEAT static void report_beat(const char *label, const struct beat_stats *st) { @@ -255,31 +203,10 @@ static void report_beat(const char *label, const struct beat_stats *st) } #endif /* HAVE_TWO_CLOCK_BEAT */ -/* ===== Entropy extraction via AES-256-CTR ================================ - * - * Per crypto consultant (MeshCore upstream PR#2280 author): the - * conditioning step is most correctly an XOF or stream cipher, not a - * truncated hash. For our 32-byte Ed25519-seed output the difference - * is design hygiene rather than security, but the cost is the same - * order of magnitude (~one SHA-512 vs SHA-256 + two AES-ECB blocks). - * - * Construction (NIST SP 800-108 KDF-in-Counter-Mode style): - * 1. Extract: SHA-256(pool) → 32-byte AES-256 key. - * 2. Expand: AES-256-ECB(counter_i) for counter_i = 0, 1, 2 ... - * output = concatenation of ciphertext blocks. - * Plaintext-XOR (true CTR mode) is omitted because plaintext would be - * all-zero — we want just the keystream. - * - * Uses PSA crypto API (already enabled via PSA_WANT_KEY_TYPE_AES + - * PSA_WANT_ALG_ECB_NO_PADDING in zephcore_common.conf). - * - * Every failure path fills `err` with the failing step and the raw - * psa_status_t. Failure here is a panic-reboot loop the user sees as a - * dead board (GH #88 on ttgo_tbeam), and the status code is the only - * thing that says WHICH step failed — a bare "extraction failed" costs a - * round trip to the reporter for a build that prints it. Sized for the - * caller's stack buffer; truncation is harmless. - */ +/* ===== Entropy extraction via AES-256-CTR ===== + * Extract: SHA-256(pool) = AES-256 key. Expand: AES-256-ECB over a counter, + * the ciphertext blocks concatenated. Every failure path fills `err` with the + * failing step and the psa_status_t. */ static int extract_via_aes_ctr(const uint8_t *pool, size_t pool_len, uint8_t *out, size_t out_len, char *err, size_t err_len) @@ -372,28 +299,9 @@ void ZephyrRNG::mixIdentitySeed(uint8_t *out, size_t out_len, uint8_t pool[512]; memset(pool, 0, sizeof(pool)); - /* ESP32 only: give the HWRNG a real entropy source for the duration of - * this function. - * - * WDEV_RANDOM is a PRNG that receives hardware entropy only "provided - * Wi-Fi or BT are enabled" (Zephyr drivers/entropy/entropy_esp32.c), and - * sys_csrand_get() maps straight to it. Every caller of this function - * runs before RF is up, and repeater / room-server builds never enable - * RF at all — so stages 1 and 5 below contributed NOTHING on ESP32, - * leaving CPU jitter as the only real source. That was observed failing - * its health check on ThinkNode M9 hardware while deriving a permanent - * identity key. - * - * bootloader_random_enable() puts the SAR ADC into continuous sampling - * and mixes its noise into the HWRNG; Espressif's header explicitly - * sanctions calling it from app code when RF is not up. It must be - * disabled again before anything else touches the ADC or RF — done at - * the end of the collection phase, before AES extraction, so the ADC is - * held for as short a window as possible. - * - * WARNING for future callers: this is unsafe if RF or the ADC is already - * in use. Do not call mixIdentitySeed() after bt_enable() or alongside a - * battery read on ESP32. */ + /* ESP32 only: bootloader_random_enable() feeds the HWRNG for the collection + * phase, because without RF it gets no hardware entropy. Unsafe once RF or + * the ADC is in use: do not call mixIdentitySeed() after bt_enable(). */ #if defined(CONFIG_SOC_FAMILY_ESPRESSIF_ESP32) bootloader_random_enable(); RNG_RPT("[RNG] === identity seed health ===\n"); @@ -435,18 +343,9 @@ void ZephyrRNG::mixIdentitySeed(uint8_t *out, size_t out_len, RNG_RPT("[RNG] stage3 extra : %d bytes\n", (int)n); } - /* Stage 4: hardware-timing entropy, 200ms — the two-clock beat. - * - * Skipped where no independent slow clock exists (see the beat header - * comment): a same-domain counter would sample a deterministic loop and - * only pretend to add entropy. Those boards rely on their true TRNG via - * stages 1 and 5. - * - * Also skipped on POSIX arch (native_sim / Linux): the simulated clock - * only advances when Zephyr threads yield, so k_uptime_get() is frozen - * while this loop spins → infinite loop. On Linux we have /dev/urandom - * (via sys_csrand_get in stages 1 and 5) which is a far stronger source - * than this sampling anyway. */ + /* Stage 4: hardware-timing entropy, 200 ms of the two-clock beat. Skipped + * where no independent slow clock exists, and on POSIX (the simulated clock + * does not advance while this loop spins). */ #if defined(HAVE_TWO_CLOCK_BEAT) && !defined(CONFIG_ARCH_POSIX) struct beat_stats js = {}; bool health_ok = sample_two_clock_beat(pool, sizeof(pool), 112, 200, &js); diff --git a/zephcore/adapters/rng/ZephyrRNG.h b/zephcore/adapters/rng/ZephyrRNG.h index 840368eb..fb08021e 100644 --- a/zephcore/adapters/rng/ZephyrRNG.h +++ b/zephcore/adapters/rng/ZephyrRNG.h @@ -15,27 +15,10 @@ class ZephyrRNG : public RNG { public: void random(uint8_t *dest, size_t sz) override; - /* Layered entropy mixer for one-time first-boot identity-key - * generation. Combines: - * 1. sys_csrand_get (early) — CSPRNG; on ESP32 only real - * because bootloader_random seeds - * the HWRNG (else pre-RF = 0 bits) - * 2. HWINFO unique device ID — per-device uniqueness (public) - * 3. Optional caller-supplied data — e.g. external noise samples - * 4. Hardware-timing entropy — two-clock beat (ESP32 RTC-slow, - * nRF/MG24 32 kHz RTC); skipped on - * boards with no independent slow - * clock (they rely on their TRNG) - * 5. sys_csrand_get (late) — second HWRNG draw - * 6. Hardware-timing entropy #2 — independent window - * Conditioned via AES-256-CTR (SHA-256(pool) → key, ECB on counter). - * On ESP32 the primary source is the bootloader_random-seeded HWRNG - * (stages 1+5); the beat (4+6) is a physical second source. On nRF the - * CSPRNG and the beat are both independently strong. Health-checked; - * reboots on degenerate output. Blocks ~450ms — first-boot identity only. - * - * Output is suitable as an Ed25519 seed regardless of platform - * TRNG state at boot. */ + /* Layered entropy mixer for the one-time identity key: CSPRNG draws early and + * late, the device ID, optional caller data and two windows of the two-clock + * beat, conditioned via AES-256-CTR. Health-checked; reboots on degenerate + * output. Blocks ~450 ms. */ static void mixIdentitySeed(uint8_t *out, size_t out_len, const uint8_t *extra = nullptr, size_t extra_len = 0); @@ -51,25 +34,13 @@ class ZephyrRNG : public RNG { static void setTestKillHWRNG(bool kill); #endif - /* Silence mixIdentitySeed's per-stage entropy health report. - * - * Default OFF — the node prints it once, at first-boot identity - * generation, and it is the only record of what the entropy sources - * actually did for a key that is then permanent. Do not silence it there. - * - * Exists for tools/rng_selftest, which calls mixIdentitySeed thousands of - * times: without this the ~12 lines per seed bury the summary. */ + /* Silence mixIdentitySeed's per-stage health report. Default off; for + * tools/rng_selftest only. */ static void setSeedHealthQuiet(bool quiet); - /* End-to-end first-boot identity generation. Mixes a fresh seed, - * derives the Ed25519 keypair, and retries (up to 100×) if the - * MeshCore protocol-reserved 0x00/0xFF public-key prefix happens - * to land. Panics-and-reboots on cap exhaustion (essentially - * impossible with a working CSPRNG: P(100 reserved in a row) ≈ 10⁻²¹¹). - * Wipes the intermediate seed before return. - * - * Use this from main()'s `loadIdentity` fall-through path instead - * of open-coding the mix+derive+retry+zeroize sequence. */ + /* First-boot identity generation: mixes a seed, derives the Ed25519 keypair, + * retries (up to 100 times) on a reserved 0x00/0xFF public-key prefix, and + * wipes the seed. */ static void generateFirstBootIdentity(LocalIdentity &out_identity); }; diff --git a/zephcore/adapters/sensors/ZephyrEnvSensors.cpp b/zephcore/adapters/sensors/ZephyrEnvSensors.cpp index a6d19264..3c973809 100644 --- a/zephcore/adapters/sensors/ZephyrEnvSensors.cpp +++ b/zephcore/adapters/sensors/ZephyrEnvSensors.cpp @@ -53,20 +53,9 @@ struct sensor_entry { static struct sensor_entry s_sensors[MAX_SENSORS]; static uint8_t s_count; -/* Is this sensor usable — bringing it up first if its node deferred init? - * - * A part behind a switched rail cannot be probed at POST_KERNEL. Regulators - * come up at priority 75 and sensors at 90, typically microseconds later, and a - * regulator-boot-on rail never applies its startup-delay-us (regulator_common_init - * takes the refcount-only branch, so regulator_delay() never runs). Such a node - * is marked zephyr,deferred-init and initialised from here instead, where the - * rail has had the whole boot to settle. See the i2c0 comment in the - * MeshTracker X1 DTS for the failure this prevents. - * - * Safe to call for every candidate on every board: do_device_init() marks a - * device initialized even when its init function failed, so device_init() - * answers -EALREADY for anything that already ran at POST_KERNEL and this - * reduces to a plain device_is_ready() check. */ +/* Is this sensor usable? A node marked zephyr,deferred-init (a part behind a + * switched rail) is initialised here first. For any other device + * device_init() answers -EALREADY and this is a device_is_ready() check. */ static bool sensor_ready(const struct device *dev) { if (dev == NULL) { diff --git a/zephcore/adapters/sensors/t1000e_analog.c b/zephcore/adapters/sensors/t1000e_analog.c index 98f39c88..7eebd4ad 100644 --- a/zephcore/adapters/sensors/t1000e_analog.c +++ b/zephcore/adapters/sensors/t1000e_analog.c @@ -26,18 +26,9 @@ LOG_MODULE_REGISTER(t1000e_analog, CONFIG_SENSOR_LOG_LEVEL); * 10 ms settle, so a few extra conversions are free noise rejection. */ #define T1000E_ADC_SAMPLES 4 -/* ================================================================ - * NTC thermistor - * - * Resistance in ohms at each degree from -30 C (index 0) to +105 C - * (index 135) — a 10k-at-25C part, effective beta about 3250. Seeed's - * firmware carries a parallel array of temperatures, which is just the - * index minus 30, so only the resistances are stored here. - * - * (Seeed's source also defines a beta of 4250 next to this table. It is - * dead code there — nothing reads it — and it does not describe this - * curve, so do not "simplify" the table into a beta formula with it.) - * ================================================================ */ +/* NTC thermistor: resistance in ohms at each degree from -30 C (index 0) to + * +105 C (index 135), a 10k-at-25C part. Do not replace the table by a beta + * formula. */ #define NTC_TABLE_LEN 136 #define NTC_TABLE_T_MIN (-30) @@ -59,17 +50,8 @@ static const uint32_t ntc_res[NTC_TABLE_LEN] = { 974, 949, 925, 902, 880, 858, }; -/* ================================================================ - * Photocell - * - * Seeed maps the divider voltage onto 0-100 with a dead band at each end. - * LIGHT_SPAN_MV happens to equal LIGHT_MAX_MV - LIGHT_MIN_MV today, but it is - * kept as its own constant rather than derived from them: the stock firmware - * treats the divisor as an independent number (subtract the 80 mV floor, - * divide by 2400, clamp at the top rather than reach it). Deriving it would - * let a future tweak to either endpoint silently move the scale factor away - * from what the stock firmware uses, and the readings would stop matching. - * ================================================================ */ +/* Photocell: the divider voltage mapped onto 0-100 with a dead band at each + * end, as Seeed's firmware. LIGHT_SPAN_MV is its own constant on purpose. */ #define LIGHT_MIN_MV 80 #define LIGHT_MAX_MV 2480 diff --git a/zephcore/adapters/usb/ZephyrCompanionUSB.cpp b/zephcore/adapters/usb/ZephyrCompanionUSB.cpp index 9b8eaf0d..adb58878 100644 --- a/zephcore/adapters/usb/ZephyrCompanionUSB.cpp +++ b/zephcore/adapters/usb/ZephyrCompanionUSB.cpp @@ -47,16 +47,9 @@ enum usb_rx_state { #define USB_TEXT_LINE_MAX 128 -/* ---- Backend selection -------------------------------------------------- - * The companion byte transport is normally a native-USB CDC-ACM UART, but the - * same frame parser / TX ring / CLI runs unchanged over a plain UART too — for - * boards whose USB-C is a USB-UART bridge (e.g. Heltec V3 / CP2102) or that - * have no USB device controller at all. A board selects the backend with the - * `zephcore,companion-uart` chosen node; absent that we fall back to the sole - * cdc-acm-uart, so existing native-USB and nRF builds resolve identically. - * - * COMPANION_HAS_DTR is true only for the CDC backend — a plain UART has no DTR - * line, so a session there, once started, lasts until reboot. */ +/* ---- Backend selection ---- + * The chosen `zephcore,companion-uart` node, else the sole cdc-acm-uart. Only + * the CDC backend has DTR; a plain-UART session lasts until reboot. */ #if DT_HAS_CHOSEN(zephcore_companion_uart) # define COMPANION_UART_DEV DEVICE_DT_GET(DT_CHOSEN(zephcore_companion_uart)) # define COMPANION_HAS_DTR DT_NODE_HAS_COMPAT(DT_CHOSEN(zephcore_companion_uart), zephyr_cdc_acm_uart) @@ -104,10 +97,8 @@ K_MSGQ_DEFINE(usb_recv_queue, sizeof(struct frame), USB_RECV_QUEUE_DEPTH, 4); * bus reset). */ static bool usb_session_active; -/* The session can carry frames: it is open and the bus is not suspended (host - * asleep with the port open). Main hears each edge as a connect or a - * disconnect, so a suspended session is not a client: nothing is queued for - * it, and it does not hold up the last-client cleanup. */ +/* The session is open and the bus is not suspended. Main hears each edge as + * a connect or a disconnect. */ static bool usb_link_up; /* CLI text line callback — fired when a complete line arrives in text mode. */ @@ -244,13 +235,8 @@ static void usb_rx_work_fn(struct k_work *work) uint8_t byte; - /* Timeout partial input — if we've been mid-frame or mid-text-line too - * long without completing, reset the parser and resync. usb_frame_start_time - * is refreshed on every text byte (below), so for USB_RX_TEXT this acts as an - * inactivity watchdog: it recovers a stray printable byte back to IDLE (so a - * later binary frame parses) without truncating a line that is actively being - * typed. Note this only runs when bytes arrive — it is not a timer and never - * wakes a sleeping node. */ + /* A frame or text line left unfinished too long: resync. Checked only when + * bytes arrive, never by a timer. */ if (usb_rx_st != USB_RX_IDLE && (k_uptime_get_32() - usb_frame_start_time) > USB_FRAME_TIMEOUT_MS) { LOG_WRN("usb_rx: partial input timeout (state=%d, expected=%u), resync", @@ -531,13 +517,8 @@ void zephcore_usb_companion_init(const struct companion_link_cbs *link) { s_link = link; - /* COMPANION_UART_DEV resolves to the chosen `zephcore,companion-uart` node, - * or the sole cdc-acm-uart for back-compat (see the backend block above). - * The cdc_acm_uart DT node may be present without the class driver compiled - * (shared esp32s3_usb_otg.dtsi exposes the node unconditionally; the class - * is only enabled with esp32s3_usb.conf) — the COMPANION_HAS_BACKEND gate - * folds that case in, so DEVICE_DT_GET never references an undefined ordinal - * on, e.g., a debug ESP32-S3 companion built without esp32s3_usb.conf. */ + /* COMPANION_HAS_BACKEND is false when the cdc_acm_uart node exists without + * the class driver (a debug ESP32-S3 companion). */ #if COMPANION_HAS_BACKEND usb_dev = COMPANION_UART_DEV; if (device_is_ready(usb_dev)) { diff --git a/zephcore/adapters/usb/ZephyrUSBCDC.cpp b/zephcore/adapters/usb/ZephyrUSBCDC.cpp index ef3a3494..81805b72 100644 --- a/zephcore/adapters/usb/ZephyrUSBCDC.cpp +++ b/zephcore/adapters/usb/ZephyrUSBCDC.cpp @@ -148,21 +148,14 @@ static void usbd_msg_callback(struct usbd_context *const ctx, s_dtr_cb(dtr_now); } } else if (msg->type == USBD_MSG_VBUS_REMOVED) { - /* Physical unplug — VBUS lost. On a device-side cable yank the - * host often never sends a clean DTR=0 line-state change, so the - * CONTROL_LINE_STATE release above won't fire and the companion - * would stay stuck on the USB interface (rejecting every BLE - * connection until reboot). Treat VBUS loss as a DTR drop so the - * interface is handed back to BLE. */ + /* Unplugged: the host often sends no DTR=0, so VBUS loss counts as + * a DTR drop. */ LOG_INF("CDC ACM: VBUS removed (device unplug)"); host_gone(); } else if (msg->type == USBD_MSG_RESET) { - /* Bus reset. At enumeration no port is open and this does nothing. - * Later it means the host is enumerating us again (a hub upstream - * was reset, or the host lost the device): the port it had open is - * gone, and no DTR=0 says so, since the class keeps the line state - * of the old session. VBUS can stay up throughout, so treat it as - * a DTR drop too. A reset also ends a suspend, with no RESUME. */ + /* The host is enumerating us again, so the port it had open is + * gone, and no DTR=0 says so: a DTR drop too. A reset also ends a + * suspend, with no RESUME. See docs/ARCHITECTURE.md 7.4. */ LOG_INF("CDC ACM: bus reset"); host_gone(); if (s_bus_cb) { @@ -310,19 +303,9 @@ extern "C" void zephcore_usbd_detach(void) return; } - /* usbd_disable() -> udc_disable() drops the D+ pull-up, which is the only - * thing that makes the host see an unplug. A soft reset does not: on the - * ESP32-S3, esp_restart_noos() resets WiFi/BT, timers, SPI, UART, DMA and - * crypto but nothing USB, so the PHY pad stays enabled across the reset - * and the host keeps talking to an endpoint the firmware has abandoned. - * (Same root cause as GH #43, which hit the USB-Serial-JTAG controller; - * this covers the USB OTG one that the CDC companion transport uses.) - * - * Safe from the usbd message callback: CONFIG_USBD_MSG_DEFERRED_MODE is - * on by default, so that callback runs on the system workqueue rather - * than in the device stack context, and usbd_disable() is not re-entered - * from the thread it stops. - */ + /* Drop the D+ pull-up so the host sees an unplug; a soft reset alone does + * not. Safe from the usbd message callback, which runs on the system work + * queue (CONFIG_USBD_MSG_DEFERRED_MODE). */ (void)usbd_disable(&zephcore_usbd); s_initialized = false; s_dtr_active = false; diff --git a/zephcore/app/CompanionMesh.cpp b/zephcore/app/CompanionMesh.cpp index fea95bd7..24169de5 100644 --- a/zephcore/app/CompanionMesh.cpp +++ b/zephcore/app/CompanionMesh.cpp @@ -490,27 +490,10 @@ void CompanionMesh::confirmOfflineMessage() _offline_queue_count--; } -/* How long CMD_APP_START may suppress v-contact notice prompts. - * - * This deadline is also the worst-case freeze a mid-session CMD_APP_START can - * impose, because the app is under no obligation to follow one with the - * message sync that clears the latch — so it must be short enough that nobody - * notices, not merely short enough to recover eventually. - * - * It only has to cover the handoff between CMD_APP_START and the - * CMD_GET_CONTACTS that normally follows it, which is where a prompt could - * make the app fetch a message from a contact it has not been told about yet. - * The dump itself is gated directly by _contact_iter_active below, for however - * long it takes, and once the dump is done a prompt is harmless. Measured on - * hardware, a full 350-contact initial sync runs APP_START to - * PACKET_NO_MORE_MSGS in ~2.9 s, so the handoff is a small fraction of that. - * - * History, because the sizing is the whole bug: 60 s produced the ~54 s - * "v-contact is frozen" reports. 10 s only looked fixed — hardware logs showed - * the user's own settings-screen-to-message time landing at 10.5-11.7 s, so - * the latch was expiring inline microseconds before the prompt and the feature - * was winning a race by half a second. Do not raise this to "be safe"; raising - * it re-creates the bug, and the thing it guards does not need the time. */ +/* How long CMD_APP_START may suppress v-contact notice prompts: just the + * handoff to the CMD_GET_CONTACTS that follows (the dump has its own gate). + * It is also the worst freeze a mid-session APP_START can cause, so it stays + * short; do not raise it. */ #define VCONTACT_HOLD_MAX_MS 3000 bool CompanionMesh::vcontactMsgWaitHeld() @@ -543,16 +526,8 @@ void CompanionMesh::pushMsgWaiting() sendPush(PUSH_CODE_MSG_WAITING); } -/* Re-prompt cadence. - * - * Observed on hardware: the app ignores the first MSG_WAITING after a settings - * write (the GPS toggle) but honours a later identical one, so the reply is - * recovered rather than lost — it just arrives one cadence late. At the - * original 15 s that was a ~20 s wait with the 5 s housekeeping granularity on - * top, which reads as broken even though nothing is. The prompt is a 1-byte - * idempotent frame, so the cost of asking again sooner is nil, and the 5 s tick - * puts the real floor here anyway: 4 s means the first re-prompt lands on the - * first tick at least 4 s after the original, i.e. 4-5 s. */ +/* Re-prompt cadence: the app can ignore a first MSG_WAITING, and asking + * again is free. With the 5 s tick the first re-prompt lands at 4-5 s. */ #define MSGWAIT_REPROMPT_MS 4000 void CompanionMesh::msgWaitingWatchdog() @@ -736,16 +711,9 @@ void CompanionMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8 LOG_INF("onDiscoveredContact: '%s' is_new=%d path_len=%d num_contacts=%d", contact.name, is_new, path_len, getNumContacts()); - /* is_new is upstream's "NOT in contacts[]": auto-add declined it (type - * filter, hop limit, or a full table), so nothing that is stored changed - * and there is nothing to write -- upstream schedules its lazy write only - * for !is_new. A contact the base class has just added arrives with - * is_new false. - * - * Of those, an addition or a changed name, type or position is - * substantive and flushes on the short deadline; a known contact - * re-advertising (only the timestamps moved) is liveness and waits the - * long one, rather than costing a full-file rewrite per advert. */ + /* is_new is upstream's "not in contacts[]": nothing stored changed, so + * nothing to write. Otherwise an addition or a changed name, type or position + * flushes on the short deadline, a bare re-advert on the long one. */ if (!is_new) { bool substantive = !_advert_prev.known || _advert_prev.type != contact.type || @@ -760,13 +728,7 @@ void CompanionMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8 // Update advert path table if (path && mesh::Packet::isValidPathLen(path_len)) { /* Update this node's existing slot if it has one, else take the least - * recently heard (an empty slot has recv_timestamp 0 and wins first). - * - * This used to round-robin into a write cursor with no lookup, so a - * node heard N times occupied N of the slots, evicting other nodes, - * and findAdvertPath() -- which returns the first array match, not the - * newest -- could hand the app a stale path for a node whose route had - * since changed. */ + * recently heard (an empty slot has recv_timestamp 0 and wins first). */ AdvertPath *ap = &_advert_paths[0]; uint32_t oldest = 0xFFFFFFFF; for (int i = 0; i < ADVERT_PATH_TABLE_SIZE; i++) { @@ -1844,14 +1806,9 @@ bool CompanionMesh::handleCmdAppStart(const uint8_t *data, size_t len) return true; } LOG_INF("CMD_APP_START: session reset, %d queued", _offline_queue_count); - // Reset per-session state for a fresh app session. BLE/USB also run - // this cleanup on disconnect, but the serial transport has no - // disconnect event, so APP_START is its authoritative session-reset - // point: drop a stale contact iteration (a leftover PACKET_CONTACT_END - // would confuse the new session's sync state machine), a half-finished - // message sync, and free an abandoned Ed25519 sign buffer (else an 8KB - // leak if the previous session dropped mid-CMD_SIGN). Idempotent — all - // no-ops when the state is already clean. + // Reset per-session state: APP_START is the session reset for a transport + // with no disconnect event. Drops a stale contact iteration, a half-finished + // message sync and an abandoned sign buffer. Idempotent. _contact_iter_active = false; cancelSyncPending(); cleanupSignState(); @@ -1875,17 +1832,8 @@ bool CompanionMesh::handleCmdAppStart(const uint8_t *data, size_t len) * deferred v-contact and flush buffered notices for this session. */ vcontactClockSynced(); - /* Re-stamp the v-contact once per app session so it stays "fresh". - * _vcontact_lastmod feeds both lastmod and last_advert_timestamp, and - * it used to move only on boot / rename / identity import: the app - * showed an ever-growing "last seen" age, and — worse — the contact - * sync gate is `_vcontact_lastmod > _contact_iter_since`, so after the - * first sync the v-contact was never streamed again and app-side state - * (flags, name) could never be corrected. Bumping here, before the - * CMD_GET_CONTACTS that follows APP_START, means every session's sync - * carries a current timestamp; deferring it to the end of sync would - * always land one session late. Silent on purpose — no NEW_ADVERT push - * mid-handshake; the sync itself delivers it. */ + /* Re-stamp the v-contact once per app session, before the CMD_GET_CONTACTS + * that follows, so every sync carries it with a current timestamp. No push. */ if (vcontactReady() && vcontactClockValid()) { _vcontact_lastmod = (uint32_t)getRTCClock()->getCurrentTime(); } @@ -2699,14 +2647,8 @@ bool CompanionMesh::handleCmdImportPrivateKey(const uint8_t *data, size_t len) bool CompanionMesh::handleCmdSendRawData(const uint8_t *data, size_t len) { - /* Raw data packet: [cmd][path_len][path...][payload...] - * - * path_len is the encoded 2-bit (hash_size - 1) + 6-bit hop count, not - * a byte count, so it has to be decoded rather than added to an offset. - * Treating it as a length rejected every path with a hash size above 1: - * hash_size 2 sets bit 6 (path_len 0x42 for two hops read as 66 bytes, - * failing the length check) and hash_size 3 sets bit 7, which used to - * read back as a negative int8_t. */ + /* Raw data packet: [cmd][path_len][path...][payload...]. path_len is the + * encoded 2-bit (hash_size - 1) + 6-bit hop count, not a byte count. */ if (len >= 6) { /* min: cmd + path_len + 4 byte payload */ size_t i = 1; uint8_t path_len = data[i++]; diff --git a/zephcore/app/CompanionMesh.h b/zephcore/app/CompanionMesh.h index 20ac65be..61f09cf3 100644 --- a/zephcore/app/CompanionMesh.h +++ b/zephcore/app/CompanionMesh.h @@ -338,16 +338,9 @@ class CompanionMesh : public BaseChatMesh, public DataStoreHost { int64_t _dirty_channels_expiry; static constexpr int64_t LAZY_WRITE_DELAY_MS = 5000; /* as upstream */ - /* Deadline for a liveness-only change (a known contact re-advertised with - * nothing but a newer timestamp): only last_advert_timestamp and lastmod - * moved. saveContacts() rewrites the whole file (~40 KB, ten 4 KB blocks - * on a T1000-E's 128 KB /lfs); on a busy mesh something is always - * re-advertising, so this deadline IS the rewrite rate. 5 s (upstream) - * was a rewrite per advert, 10 min was 144 a day, and one hour is 24, - * below the adv_blobs write per advert. Every clean reboot and power-off - * flushes first (flushPendingWrites()), so only a crash or a pulled - * battery loses the last hour of "last heard" times. A substantive - * change still pulls the deadline in. */ + /* Deadline for a liveness-only change (a known contact re-advertised): each + * flush rewrites the whole contacts file, so this is the rewrite rate on a + * busy mesh. Clean reboots and power-offs flush first. */ static constexpr int64_t LAZY_WRITE_LIVENESS_MS = 3600000; /* 1 hour */ void markContactsDirty(bool substantive = true); diff --git a/zephcore/app/ObserverMesh.cpp b/zephcore/app/ObserverMesh.cpp index 84c6c7e1..7b4bd2fe 100644 --- a/zephcore/app/ObserverMesh.cpp +++ b/zephcore/app/ObserverMesh.cpp @@ -61,16 +61,9 @@ void ObserverMesh::begin(RepeaterDataStore *store, struct ObserverCreds *creds) * saves these defaults as they are. */ _store->loadPrefs(_prefs); - /* Load or generate node identity. - * - * Use ZephyrRNG::generateFirstBootIdentity — the SAME hardened path the - * companion and repeater use (bootloader_random-seeded HWRNG + two-clock - * beat, conditioned via AES-256-CTR) — NOT LocalIdentity(_rng). The old - * form drew straight from ZephyrRNG::random() / sys_csrand_get, which on - * an ESP32 observer is unseeded (BLE never comes up to seed WDEV_RANDOM), - * so it derived a permanent key from a weak PRNG. generateFirstBootIdentity - * also owns the reserved-prefix retry (100 attempts + panic backstop), - * replacing the weaker 10-try loop that silently kept a reserved prefix. */ + /* Load or generate the node identity, through + * ZephyrRNG::generateFirstBootIdentity like the other roles: on an ESP32 + * observer the plain RNG is unseeded (BLE never comes up). */ if (!_store->loadIdentity(_self_id)) { LOG_INF("No identity found — generating new keypair"); mesh::ZephyrRNG::generateFirstBootIdentity(_self_id); diff --git a/zephcore/app/RepeaterDataStore.cpp b/zephcore/app/RepeaterDataStore.cpp index b4244999..f24a56c6 100644 --- a/zephcore/app/RepeaterDataStore.cpp +++ b/zephcore/app/RepeaterDataStore.cpp @@ -49,20 +49,9 @@ const char* RepeaterDataStore::getBasePath() const { return BASE_PATH; } bool RepeaterDataStore::hasRoleData() const { char path[64]; - /* Only THIS role's files count. A companion volume does not: the roles - * are deliberately not interchangeable, and a companion's contacts and - * blob cache would eat into the same 128 KB the repeater needs, so a - * repeater booting onto a companion volume formats it. The reverse - * already happens — ZephyrDataStore::hasPrefs() tests /lfs/new_prefs, - * which a repeater volume never has. - * - * Repeater, room server and observer DO share this store and base path; - * they use the same prefs layout, so switching among them keeps the - * node's identity, which is what an operator wants. - * - * Self-limiting: loadPrefs() persists defaults on boot 1 and main_*.cpp - * saves a generated identity on the same boot, so after one successful - * boot at least one of these exists and the check never fires again. */ + /* Only this role's files count: a repeater booting onto a companion volume + * formats it. Repeater, room server and observer share the store, so + * switching among them keeps the identity. */ static const char* const ours[] = { "prefs.json", "prefs", "_main.id" }; for (size_t i = 0; i < ARRAY_SIZE(ours); i++) { snprintf(path, sizeof(path), "%s/%s", BASE_PATH, ours[i]); diff --git a/zephcore/app/RepeaterMesh.cpp b/zephcore/app/RepeaterMesh.cpp index 59c58306..68d54163 100644 --- a/zephcore/app/RepeaterMesh.cpp +++ b/zephcore/app/RepeaterMesh.cpp @@ -181,16 +181,9 @@ uint8_t RepeaterMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t if (client == nullptr) { uint8_t perms; - /* Constant-time comparison: zero-pad BOTH the received password and - * the stored passwords into cleared buffers (copying only up to the - * NUL) before comparing full-width. Don't trust the stored buffer to - * be zero-padded: a password set over a longer previous value via the - * CLI leaves trailing garbage past the NUL (and such garbage may - * already be persisted in flash on upgraded devices). Comparing the - * raw stored buffer full-width would then fail to match a correct - * password. Compare both unconditionally so timing is identical for - * any wrong password regardless of which (admin/guest) it most - * resembles. */ + /* Constant-time comparison: zero-pad both the received and the stored + * passwords into cleared buffers (up to the NUL) and compare full-width. + * The stored buffer can carry garbage past the NUL. */ uint8_t received[sizeof(_prefs.password)] = {0}; uint8_t admin_pw[sizeof(_prefs.password)] = {0}; uint8_t guest_pw[sizeof(_prefs.guest_password)] = {0}; @@ -206,27 +199,9 @@ uint8_t RepeaterMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t guest_pw, sizeof(received)); - /* An empty stored guest password means OPEN GUEST ACCESS on a repeater, - * deliberately, matching Arduino MeshCore: its NodePrefs default is - * guest_password[0] = 0 (src/helpers/CommonCLI.h) and - * examples/simple_repeater/MyMesh.cpp compares with a plain strcmp, so a - * blank submitted password logs in as guest. Only a blank submission - * matches -- a wrong non-blank password still fails, because both - * buffers are zero-padded and compared full-width. - * - * This is deliberately NOT what RoomServerMesh.cpp does, and the two - * must not be "made consistent". There an empty guest password disables - * guest access, because on a room server that password is what gates - * posting and an accidentally open room is a real hole (the 2026-07-30 - * finding: every ZephCore room server shipped open). Arduino closes the - * same hole from the other side, by defaulting the room server's - * guest_password to ROOM_PASSWORD rather than leaving it empty. - * - * The blast radius on a repeater is small by construction: - * PERM_ACL_GUEST cannot run CLI commands -- the PAYLOAD_TYPE_TXT_MSG - * path in onPeerDataRecv() requires isAdmin() -- and cannot read the - * access list (REQ_TYPE_GET_ACCESS_LIST likewise). A guest gets login - * plus status/telemetry. Set a guest password to close it. */ + /* An empty stored guest password means open guest access on a repeater, as + * Arduino MeshCore. Deliberately not what RoomServerMesh does, where it + * disables guest access. A repeater guest gets login plus status/telemetry. */ if (admin_match) { perms = PERM_ACL_ADMIN; @@ -272,21 +247,9 @@ uint8_t RepeaterMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t memcpy(client->shared_secret, secret, PUB_KEY_SIZE); if (client->isAdmin() && (client->permissions != prev_perms || secret_changed)) { - /* Flush admin sessions now instead of leaving them on the lazy - * timer. The entry carries the shared secret this session's - * traffic is encrypted with; if power is lost before the timer - * fires, the client keeps a secret the repeater no longer knows - * and every later request decrypts to nothing, which presents as - * a wrong password or an unreachable node. A repeater is the - * device most likely to lose power unattended, so the few - * milliseconds are worth it. save() writes the whole ACL, so any - * other pending changes go out with it. - * - * Only when the stored entry changed, though. The secret is the - * ECDH of the two fixed keys, so a known admin logging in again - * carries the same one: rewriting an identical ACL on every login - * (a monitoring tool polling every few minutes) was pure wear. - * Upstream schedules a lazy write on every non-guest login. */ + /* Flush admin sessions now, not on the lazy timer: the entry carries the + * session's shared secret, and losing it to a power cut locks the client out. + * Only when the stored entry changed. */ if (_store) { acl.save(_store->getFS()); dirty_contacts_expiry = 0; @@ -1553,16 +1516,9 @@ void RepeaterMesh::handleRegionLoadLine(uint32_t sender_timestamp, char* command while (*np == ' ') np++; int indent = np - command; - /* An unindented, name-like line is a typed command, not a region row: - * real rows are indent >= 1 (load_stack[0] is the wildcard), and the - * one unindented line a client legitimately sends is the exported - * wildcard header "*", whose '*' is not a name char. Without this, - * `region load` is only escapable by a blank line -- which the USB - * reader discards (main_repeater.cpp) and a dead remote-admin client - * never sends, stranding the CLI until a reboot. Abort without - * committing temp_map and run the command. Must come BEFORE the - * name-terminator write below, which would truncate `set foo 1` to - * `set`. */ + /* An unindented, name-like line is a typed command, not a region row: abort + * the load without committing and run the command. Must come before the + * name-terminator write below. */ if (indent == 0 && RegionMap::is_name_char((uint8_t)*np)) { region_load_active = false; handleCommand(sender_timestamp, command, reply); diff --git a/zephcore/app/RoomServerMesh.cpp b/zephcore/app/RoomServerMesh.cpp index 30298bf8..394a7d3c 100644 --- a/zephcore/app/RoomServerMesh.cpp +++ b/zephcore/app/RoomServerMesh.cpp @@ -391,17 +391,9 @@ void RoomServerMesh::onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, } if (client == nullptr) { - /* Constant-time compare against both stored passwords. Admin grants - * ADMIN; the guest/room password grants READ_WRITE (so guests may - * post); allow_read_only downgrades any other login to GUEST. - * - * Zero-pad BOTH operands into cleared buffers first: the CLI's - * StrHelper::strncpy null-terminates but does NOT clear the rest of - * the 16-byte buffer, so a password set over a longer previous value - * leaves trailing garbage. Comparing the raw stored buffer full-width - * against the (zero-padded) received bytes would then fail to match a - * correct password. Copy only up to the NUL so the compare reflects - * the actual string while staying constant-time over the full width. */ + /* Constant-time compare against both stored passwords, zero-padded into + * cleared buffers first. Admin grants ADMIN; the guest/room password grants + * READ_WRITE; allow_read_only downgrades any other login to GUEST. */ uint8_t received[sizeof(_prefs.password)] = {0}; uint8_t admin_pw[sizeof(_prefs.password)] = {0}; uint8_t guest_pw[sizeof(_prefs.guest_password)] = {0}; @@ -1148,16 +1140,9 @@ void RoomServerMesh::handleRegionLoadLine(uint32_t sender_timestamp, char* comma while (*np == ' ') np++; int indent = np - command; - /* An unindented, name-like line is a typed command, not a region row: - * real rows are indent >= 1 (load_stack[0] is the wildcard), and the - * one unindented line a client legitimately sends is the exported - * wildcard header "*", whose '*' is not a name char. Without this, - * `region load` is only escapable by a blank line -- which the USB - * reader discards (main_repeater.cpp) and a dead remote-admin client - * never sends, stranding the CLI until a reboot. Abort without - * committing temp_map and run the command. Must come BEFORE the - * name-terminator write below, which would truncate `set foo 1` to - * `set`. */ + /* An unindented, name-like line is a typed command, not a region row: abort + * the load without committing and run the command. Must come before the + * name-terminator write below. */ if (indent == 0 && RegionMap::is_name_char((uint8_t)*np)) { region_load_active = false; handleCommand(sender_timestamp, command, reply); diff --git a/zephcore/app/VContact.cpp b/zephcore/app/VContact.cpp index 295a0412..aa73c791 100644 --- a/zephcore/app/VContact.cpp +++ b/zephcore/app/VContact.cpp @@ -170,13 +170,8 @@ void CompanionMesh::vcontactFlushConfirm() void CompanionMesh::deriveVContactKey() { /* The Ed25519 public point of a keypair seeded from - * SHA256("zc-vcontact" || self prv_key || counter). - * - A real point: strict clients decompress peer keys and reject the ~50% - * of random 32-byte strings that are not valid points. - * - Seeded from the private key, so nobody else can link the v-contact to - * this node or derive its private half, which is dropped here unused. - * - The counter skips pub_key[0] of 0x00/0xFF, which MeshCore reserves - * (P = 2/256 per try, so still deterministic). */ + * SHA256("zc-vcontact" || self prv_key || counter): a real point, unlinkable + * to this node; the counter skips a reserved 0x00/0xFF first byte. */ static const char vc_salt[] = "zc-vcontact"; uint8_t material[PRV_KEY_SIZE + 1]; uint8_t seed[SEED_SIZE]; diff --git a/zephcore/app/main_observer.cpp b/zephcore/app/main_observer.cpp index 23ac57ad..f83e6d9d 100644 --- a/zephcore/app/main_observer.cpp +++ b/zephcore/app/main_observer.cpp @@ -326,17 +326,8 @@ int main(void) } #endif - /* First boot on a volume that is not this role's - a fresh chip, a - * companion, or a node that was running Arduino MeshCore, whose nRF52 - * filesystems overlap our lfs_partition - * (devdocs/HANDOVER_lfs_arduino_overlap.md). Erase everything so we - * start from a known state: Zephyr's automount only - * auto-formats the LittleFS volume when it fails to mount, and never - * touches storage_partition (BLE bonds NVS) or QSPI. - * - * Self-limiting, so it needs no "done" marker: the identity is generated - * and saved a few lines below, and loadPrefs() persists defaults on the - * same boot, so the next boot sees this role's data and skips this. */ + /* First boot on a volume that is not this role's: erase everything (LittleFS, + * bond NVS, QSPI). Self-limiting: this boot writes the role's data. */ if (!data_store.hasRoleData()) { LOG_WRN("Volume holds no data for this role - formatting before first boot"); if (!zephcore_fs_format_all(nullptr)) { @@ -360,19 +351,8 @@ int main(void) /* LoRa RX callback — observer never needs TX done */ lora_radio.setRxCallback(lora_rx_callback, nullptr); - /* Bind the radio to the mesh's NodePrefs BEFORE begin(). - * - * The radio reads freq/bw/sf/cr through this pointer, both during - * begin() → Dispatcher::begin() → Radio::begin() and on every later - * reconfigure(). Bound to a placeholder that was never loaded from - * flash, the radio stayed on the compiled-in defaults forever: `set freq/sf/bw/cr` wrote flash and - * updated the CLI/MQTT readback but never reached the hardware, so the - * setting looked accepted and then "reverted" on reboot. - * - * Binding before begin() is safe and required: begin() calls loadPrefs() - * first and Dispatcher::begin() last, so the object is populated by the - * time the radio reads through it. Mirrors main_repeater.cpp and - * main_room_server.cpp. */ + /* Bind the radio to the mesh's NodePrefs before begin(): the radio reads + * freq/bw/sf/cr through this pointer at begin() and on every reconfigure. */ lora_radio.setPrefs(observer_mesh.getNodePrefs()); /* Initialize and start mesh (loads prefs + identity from flash) */ diff --git a/zephcore/helpers/CommonCLI.cpp b/zephcore/helpers/CommonCLI.cpp index c25bf7c9..47db72f3 100644 --- a/zephcore/helpers/CommonCLI.cpp +++ b/zephcore/helpers/CommonCLI.cpp @@ -46,15 +46,9 @@ static uint32_t _atoi(const char* sp) { } /* ---- "default" keyword + strict numeric parsing for the `set` path ---- - * - * Bare atoi()/atof() fold every non-numeric string to 0, the word "default" - * included. On the knobs where 0 is itself legal and means "off" — - * probe.interval, cad.busycap, flood.max, rxduty — that silently switched the - * feature off and still answered OK. `set probe.interval default` disabling - * probing is the report that prompted this. - * - * Every `set` that has a default now takes the literal `default`, and rejects - * trailing garbage rather than turning it into a zero. */ + * atoi()/atof() fold any non-number to 0, which is "off" on several knobs. + * Every `set` with a default takes the literal `default` and rejects + * trailing garbage. */ /* The defaults are read out of initNodePrefs() itself rather than restated as * constants here, so `set default` cannot drift from what a factory-fresh @@ -73,14 +67,8 @@ static const NodePrefs* cliDefaults() { /* ---- leds.radio / leds.hb mode names ----------------------------------- */ /* - * Which LEDs this board actually has, so the CLI can tell the user when a - * setting it just accepted will not do anything here. The pref is still - * stored either way: the same prefs file follows a node onto a board that does - * have the LED, and silently dropping the value would be worse than storing a - * setting that is dormant. - * - * These mirror the aliases the drivers use — lora-tx-led in ZephyrBoard.cpp, - * led0 with an led1 fallback in helpers/ui/ui_common.c. + * Which LEDs this board has, so the CLI can say when a setting it stored + * does nothing here. Mirrors the aliases the drivers use. */ #define CLI_HAS_RADIO_LED DT_NODE_EXISTS(DT_ALIAS(lora_tx_led)) #define CLI_HAS_HB_LED (DT_NODE_HAS_PROP(DT_ALIAS(led0), gpios) || \ @@ -248,30 +236,12 @@ void CommonCLI::savePrefs() { _callbacks->savePrefs(); } -/* Drop everything the adaptive-CAD controller has learned. - * - * Called both by `set cad.reset` and by every command that moves the radio to a - * different preset, because such a change invalidates all three pieces at once: - * detPeak's base table is per-SF and per-bandwidth, so the ladder the offset - * addresses is re-based under it (SF7->SF12 is 5 counts on SX126x and 16 on the - * LR11xx), and the per-level statistics that justified the offset describe a - * preset that is no longer on air. Re-converging from the new preset's own - * base costs an hour or two; carrying a stale offset across can leave the node - * either transmitting over live receptions or unable to transmit at all. - * - * The operating offset lives in two places — _prefs->cad_offset, and the radio's - * CadController, which applyCadPrefs() reloads through setCadParams(). +/* Drop everything the adaptive-CAD controller has learned: on `set + * cad.reset` and on every change of preset. * - * preset_pending = the caller has already written the NEW freq/bw/sf/cr to - * _prefs but the radio is still running the OLD preset (frozen by - * freezeRadioParams until the reboot the caller is about to ask for). - * applyCadPrefs() stamps cad_base from the RUNNING radio, i.e. the preset being - * left, so that stamp has to be cleared again afterwards: a stale anchor in - * flash is precisely what makes setCadParams() re-anchor a freshly reset offset - * by (old_base - new_base) on the next boot, which is the bug this reset - * exists to avoid. 0 means "no anchor recorded", which that re-anchor - * deliberately treats as a no-op, and the next boot re-stamps it. - */ + * preset_pending = the new preset is in _prefs but the radio still runs the + * old one until the reboot. applyCadPrefs() then stamps cad_base from the + * preset being left, so the stamp is cleared again (0 = no anchor). */ void CommonCLI::resetCadState(bool preset_pending) { _prefs->cad_offset = cliDefaults()->cad_offset; _prefs->cad_base = 0; @@ -352,21 +322,9 @@ void CommonCLI::scheduleReboot(uint8_t type) k_work_schedule(&_reboot_work, K_SECONDS(2)); } -/* CLI commands are case-sensitive, matching upstream Arduino MeshCore. - * - * A case-insensitive normalizer lived here from 2026-07-12 until 2026-07-19. - * It lowercased the first two whitespace-delimited tokens before matching, on - * the assumption that a value never appears before the third token. That is - * false for "password ", whose value IS token 1 -- so any admin - * password containing uppercase was silently stored folded to lowercase and - * could never be used to log in again. ("set guest.password " was - * unaffected: three tokens.) - * - * Do not reintroduce input folding here. Any scheme that rewrites the buffer - * before dispatch has to guess where keywords end and arguments begin, and - * that guess is what broke. If case-insensitivity is wanted again, do it at - * the comparison sites so argument bytes are never touched. - */ +/* CLI commands are case-sensitive, matching upstream Arduino MeshCore. Do + * not fold the input buffer: a value can be the second token (`password + * `). */ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, char* reply) { if (handleRadioCmd(sender_timestamp, command, reply)) { // is a radio CLI command? return; @@ -377,18 +335,9 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch } if (strcmp(command, "start dfu") == 0) { - /* Reboot into the chip's own firmware-update mode. nRF52: the Adafruit - * UF2 bootloader. ESP32-S3: the ROM download mode, which is the only - * way to get the USB port back from the CDC companion transport for - * esptool (see ZephyrBoard::rebootToBootloader). - * - * Everything else has no bootloader this command can reach, so it must - * NOT reboot. rebootToBootloader() would just reset back into the app, - * dropping the node off the mesh for nothing while replying that it had - * gone somewhere it cannot go. C3/C6 and classic ESP32 do not need it - * anyway -- they have no DWC2 controller, so USB-Serial-JTAG never - * loses the port and esptool resets them itself; nRF54L15/MG24/STM32WL - * have no USB device peripheral at all and are flashed over SWD. */ + /* Reboot into the chip's own firmware-update mode: the UF2 bootloader on + * nRF52, the ROM download mode on ESP32-S3. Other platforms have no + * bootloader this command can reach, so they must not reboot. */ #if defined(CONFIG_SOC_SERIES_ESP32S3) strcpy(reply, "OK - rebooting to ESP32 download mode"); scheduleReboot(REBOOT_DFU); @@ -798,14 +747,9 @@ bool CommonCLI::handleRadioCmd(uint32_t sender_timestamp, const char* command, c /* Plain number, matching upstream Arduino MeshCore's "> %d". */ snprintf(reply, CLI_REPLY_SIZE, "> %d", (int)_prefs->tx_power_dbm); } else if (memcmp(config, "freqerr", 7) == 0) { - /* MUST stay above "freq" — that is a 4-char prefix match and - * would swallow this one. - * - * Carrier frequency error measured on received packets, LR2021 - * only. Diagnostic: nothing acts on it. The mean approximates - * THIS node's reference error only once averaged over many - * different peers (theirs cancel, ours does not), which is why - * the spread and packet count are shown alongside it. */ + /* MUST stay above "freq", a 4-char prefix match that would swallow this + * one. Carrier frequency error of received packets, LR2021 only; + * diagnostic. */ int n = snprintf(reply, CLI_REPLY_SIZE, "> "); if (_callbacks->formatFreqErrorStatus(reply + n, CLI_REPLY_SIZE - n) == 0) { @@ -821,13 +765,8 @@ bool CommonCLI::handleRadioCmd(uint32_t sender_timestamp, const char* command, c if (memcmp(command, "set ", 4) == 0) { const char* config = &command[4]; if (memcmp(config, "dutycycle ", 10) == 0) { - /* Floor is 10%, not Arduino's 1%: this writes airtime_factor as - * (100/dc)-1, and sanitizeNodePrefs() clamps that field to 0..9. - * A 1% duty cycle stores af=99, survives until the next load, and - * is then silently rewritten to 9 (=10%) — the node would run at - * one duty cycle and boot into another. 10% is the lowest value - * that round-trips, and it matches the 0..9 bound `set af` now - * enforces directly. */ + /* Floor is 10%, not Arduino's 1%: airtime_factor is clamped to 0..9, so + * a lower duty cycle would not survive a reload. */ float dc; if (!cliFloat(&config[10], 100.0f / (cliDefaults()->airtime_factor + 1.0f), &dc)) { strcpy(reply, "ERROR: dutycycle must be 10-100, or default"); @@ -875,13 +814,7 @@ bool CommonCLI::handleRadioCmd(uint32_t sender_timestamp, const char* command, c } } } else if (memcmp(config, "agc.reset.interval ", 19) == 0) { - /* Periodic AGC recalibration was removed: it reset the noise floor - * to its unseeded sentinel on every fire, forcing a fresh seed and - * a full EMA warmup, and it was already forced off under RX duty - * cycle. RX duty cycle is the supported way to cut RX current. - * The prefs BYTE is retained (read/written, never acted on) — the - * on-disk layout is byte-exact and shifting it would corrupt every - * existing node's prefs. */ + /* Removed. The prefs byte is kept for the legacy on-disk layout. */ strcpy(reply, "Removed - Automatic AGC reset is on"); } else if (memcmp(config, "multi.acks ", 11) == 0) { long val; @@ -1068,15 +1001,8 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, const char* command, cha zephcore_buzzer_mode_name(mode)); #endif #ifdef CONFIG_ZEPHCORE_ROLE_ROOM_SERVER - /* Room server only. RoomServerMesh.cpp is the sole consumer of - * allow_read_only; RepeaterMesh.cpp never reads it, so on a repeater - * this advertised a setting that silently did nothing. Note this is a - * deliberate divergence from Arduino MeshCore, whose shared CommonCLI - * exposes the knob on every role for the same reason ours used to. - * - * The pref itself stays unconditional -- it is byte 114 of the on-flash - * prefs layout, identical to Arduino's, so dropping it would shift every - * field after it and invalidate existing prefs files. */ + /* Room server only: nothing else reads allow_read_only. The pref itself + * stays for the legacy on-disk layout. */ } else if (memcmp(config, "allow.read.only", 15) == 0) { snprintf(reply, CLI_REPLY_SIZE, "> %s", _prefs->allow_read_only ? "on" : "off"); #endif @@ -1274,18 +1200,8 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, const char* command, cha (unsigned)_prefs->cad_busycap); } } else if (strcmp(config, "cad") == 0) { - /* Arduino exposes this as a boolean knob; ZephCore always does CAD, - * so the answer is a constant "on". Apps parse the word. - * - * EXACT match, not the 3-byte prefix this used to be. Every branch - * in this chain is a memcmp prefix test, so "cad" matched every - * `get cad.*` that had no branch of its own and answered "on" for - * all of them -- a percentage, a signed offset and an action all - * came back as a boolean. cad.stats escaped only by being ordered - * above it, which is a fragile way to stay correct. Anchoring this - * one fixes the whole family at once: anything under cad. without a - * getter now falls through to the honest "??:" reply instead of - * being answered wrongly. */ + /* ZephCore always does CAD, so the answer is a constant; apps parse the + * word. Exact match: a prefix test would answer every `get cad.*`. */ strcpy(reply, "> on"); } else if (memcmp(config, "extra.sf", 8) == 0) { /* No "> " prefix, and the empty case is a sentence -- both match @@ -1444,15 +1360,8 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, const char* command, cha strcpy(reply, "OK"); } } else if (memcmp(config, "cad.reset", 9) == 0) { - /* Clearing the probe statistics alone left the node re-converging - * FROM wherever the staircase had already walked detPeak, with no - * evidence left to justify sitting there — the opposite of a reset, - * and useless for the one job this command has (recovering after a - * base-table change). - * - * preset_pending = false: the radio is running the preset this - * reset is for, so the base applyCadPrefs() stamps is the right - * one to keep. */ + /* Reset the offset and the base anchor as well, not only the statistics. + * preset_pending = false: the radio runs the preset this reset is for. */ resetCadState(false); snprintf(reply, CLI_REPLY_SIZE, "OK - CAD probe stats cleared, detPeak offset reset to %d", @@ -1773,15 +1682,8 @@ 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 ` all stay UTC: they round-trip - // with each other, apps parse them, and a timezone that reached the - // clock would look like a backward jump to every timestamp consumer - // on the node (see NodePrefs::tz_offset). - // - // Whole hours, matching upstream MeshCore's command of the same - // name, so an app that speaks to both trees behaves identically. - // Half-hour zones (+5:30, -3:30) cannot be expressed. + // Whole-hour offset from UTC for the on-device clock display only; the RTC, + // `clock` and `time ` stay UTC. Whole hours as upstream. long val; if (!cliNum(&config[10], cliDefaults()->tz_offset, &val)) { snprintf(reply, CLI_REPLY_SIZE, "Error: expected %d..%d or default", diff --git a/zephcore/helpers/NodePrefs.h b/zephcore/helpers/NodePrefs.h index 2667e363..c5e2b289 100644 --- a/zephcore/helpers/NodePrefs.h +++ b/zephcore/helpers/NodePrefs.h @@ -17,27 +17,11 @@ * and this header is C++; see the note there. */ #include "led_gate.h" -/* ── Accepted radio parameter ranges ────────────────────────────────── - * - * One definition, used by everything that accepts or validates a radio - * preset: the USB CLI, the BLE companion protocol, the observer CLI, and the - * load-time sanity checks in both datastores. - * - * It lives here because these ranges were previously copy-pasted into six - * places and drifted. That drift was not theoretical: when 2.4 GHz support - * landed, the setters were widened and the *loaders* were not, so a node - * accepted `set freq 2450`, saved it, and then silently reverted to factory - * defaults on the next boot because the load-time guard still capped at - * 960 MHz. A setter must never accept what the loader will throw away. - * - * FREQ spans both LR2021 RF paths — the sub-GHz one (150-960 MHz) and the - * high band (1.9-2.5 GHz). Boards whose radio cannot reach the high band are - * limited by their own driver, not by this range. - * - * BW_MAX is the one value that is per-build. Only the LR2021 implements the - * wide 203/406/812/1000 kHz set; every other driver here maps an unknown - * bandwidth to 125 kHz instead of refusing it, so accepting 812 on those - * boards would put a node on a channel width it never reported. */ +/* ── 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 + * accept what the loader throws away. FREQ spans both LR2021 RF paths; + * BW_MAX is per build (only the LR2021 has the wide bandwidths). */ #define ZC_RADIO_FREQ_MIN_MHZ 150.0f #define ZC_RADIO_FREQ_MAX_MHZ 2500.0f #define ZC_RADIO_BW_MIN_KHZ 7.0f @@ -62,31 +46,15 @@ #define LOOP_DETECT_MODERATE 2 #define LOOP_DETECT_STRICT 3 -/* Adaptive-CAD operating detPeak offset range (levels from the family base). - * Wide on purpose: a dense hilltop can need a much higher detPeak than a quiet - * valley node. The per-family absolute clamp inside the driver (SX126x 15-40, - * LR11xx/LR20xx 48-90) is a firmware guardrail, NOT a chip limit — cadDetPeak - * is a full uint8_t (0-255). It just keeps the staircase from wandering into - * "CAD never fires" (too high) or "CAD always busy" (too low) territory. This - * offset range limits how far the staircase / manual offset may roam; MUST - * match CAD_LEVEL_MIN/MAX in adapters/radio/radio_common.h (they index the - * per-level stats array). */ +/* Adaptive-CAD detPeak offset range (levels from the family base): how far + * the staircase or a manual offset may roam. Must match CAD_LEVEL_MIN/MAX in + * adapters/radio/radio_common.h. */ #define CAD_OFFSET_MIN (-8) #define CAD_OFFSET_MAX 12 -/* leds_disabled, as stored in the repeater/room-server/observer prefs layout. - * - * It occupies the byte that used to hold agc_reset_interval (offset 120), - * retired when periodic AGC recalibration was removed. That byte is NOT - * reusable as a plain 0/1 boolean: the old command stored seconds/4, so a node - * upgrading from a build that had it configured has an arbitrary small integer - * sitting there, and a bare non-zero test would silently kill its LEDs. Hence - * a magic encoding — anything that is not one of these two values is a legacy - * AGC interval and decodes to the default (LEDs on). The first savePrefs() - * claims the byte for good. - * - * The companion layout is unaffected: it has always stored leds_disabled as a - * plain 0/1 at its own offset 93. */ +/* leds_disabled in the server prefs layout sits in the byte that held + * agc_reset_interval (offset 120), so it is magic-encoded: any other value is + * a legacy AGC interval and decodes to LEDs on. */ #define LEDS_PREF_ON 0xA0 #define LEDS_PREF_OFF 0xA1 @@ -152,14 +120,8 @@ struct NodePrefs { uint8_t rx_boost; // 1 = boosted RX gain (+3dB), 0 = power save uint8_t fem_rxgain; // 1 = external FEM LNA active during RX, 0 = off (power save) uint8_t rx_duty_cycle; // 1 = RX duty cycle, 0 = continuous RX - /* RESERVED — formerly apc_enabled / apc_margin (Adaptive Power Control, - * removed in 1.16.6). These two bytes are still read and written at their - * original offsets in both prefs layouts (companion new_prefs 94/95, server - * prefs 292/293; helpers/PrefsCodec.cpp) because every field after them - * is positional: dropping them would shift the rest of the layout and make - * every already-deployed node misparse its saved prefs on upgrade. - * Do not reuse for a new setting — an upgraded node still has the old APC - * values sitting in these bytes. */ + /* RESERVED: formerly apc_enabled / apc_margin. Still read and written at + * their offsets in both legacy layouts; do not reuse. */ uint8_t _reserved_apc_enabled; uint8_t _reserved_apc_margin; uint8_t meshtimesync; // 1 = mesh time-sync clock correction on (default off) @@ -180,49 +142,20 @@ struct NodePrefs { * driver rejects anything else. Ignored on non-LR2021 radios. */ uint8_t extra_sf[EXTRA_SF_MAX]; - /* Physical mounting orientation. Common to both roles: a repeater board - * with an OLED (RAK4631, Heltec) can be mounted upside down just as a - * companion can, and the joystick boards ship a repeater artifact too. - * - * display_rotate rotates the panel 180 degrees in hardware and is only - * honoured on SSD1306/SH1106 (see MC_DISPLAY_ROTATE_SUPPORTED); other - * panels report it unsupported rather than silently ignoring it. - * input_rotate swaps the joystick/D-pad axes to match, and is kept - * separate because the two are not always wanted together — a screen can - * be remounted without moving the stick. */ + /* Physical mounting orientation, both roles. display_rotate turns the panel + * 180 degrees where the panel supports it; input_rotate swaps the + * joystick/D-pad axes and is separate on purpose. */ 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: a negative offset applied to the clock - * itself reads as a backward jump to every timestamp consumer at once - * (advert timestamps, the repeater ACL's monotonic sender_timestamp gate, - * discovery_mod_timestamp, MeshTimeSync), and a backward clock is a silent - * mesh-wide mute. The CLI's `clock` / `time` commands stay UTC for the - * same reason -- they round-trip with each other and apps parse them. - * - * Whole hours only, matching upstream's `set tz.offset` so an app that - * speaks to both trees behaves the same. Half-hour zones (India +5:30, - * Newfoundland -3:30) therefore cannot be expressed. - * - * Common to both roles: a repeater with an OLED shows a clock too. On a - * headless repeater the field is simply inert. */ + /* 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; /* The family base detPeak that cad_offset was learned against (0 = never - * stored, i.e. a node upgrading from a build without this field). - * - * cad_offset is a SIGNED OFFSET from a per-SF/per-bandwidth base table - * that lives in the driver, so changing that table silently re-points a - * stored offset at a different absolute detPeak. The probe statistics - * behind the offset are RAM-only and die at the reboot a firmware upgrade - * involves, but the offset itself is persisted and survives — so after a - * table change a converged node quietly starts operating somewhere it - * never measured. Recording the base turns that into something the - * firmware can correct at boot (see LoRaRadio::setCadParams), instead - * of a "run set cad.reset after upgrading" line in the release notes that - * most users will not read. */ + * stored). Lets the firmware re-anchor the offset at boot when the driver's + * base table changes (LoRaRadio::setCadParams). */ uint8_t cad_base; /* ---- Companion-only fields ---- */ @@ -264,19 +197,10 @@ struct NodePrefs { uint8_t wifi_enabled; // 1 = join wifi_ssid when set (upstream default) }; -/* Range guards for prefs that came off flash. - * - * The atomic replace in every savePrefs() plus littlefs's own CRCs make a torn - * write impossible, so this is not about power loss — it is about a blob that - * is structurally intact and semantically wrong. Several of these fields make - * a node look bricked when they are: auto_shutdown_mv powers it off seconds - * after boot, ble_pin locks pairing out, and the char fields are stored as - * fixed-size blocks with no terminator in the file format, so an unterminated - * one runs every later %s off the end of the struct. - * - * Called by both prefs decoders (PrefsCodec.cpp). Fields whose whole range is - * legal (autoadd_config bitmask, discovery_mod_timestamp, the v_contact_* - * sentinels) are deliberately left alone. */ +/* Range guards for prefs that came off flash: a blob can be intact and + * semantically wrong, and several fields make a node look bricked then. + * Called by both prefs decoders. Fields whose whole range is legal are left + * alone. */ template static inline T clampPref(T v, T lo, T hi) { return v < lo ? lo : (v > hi ? hi : v); } @@ -483,14 +407,8 @@ static inline void initNodePrefs(NodePrefs* prefs) { prefs->flood_max_unscoped = 64; // un-scoped flood hop limit (defaults to flood_max) prefs->flood_max_advert = 8; // ADVERT flood hop limit (upstream default) /* 2-byte path hashes (mode + 1 = hash size) for everything this node - * originates. ZephCore's default, not upstream's -- Arduino MeshCore - * ships 0 (1-byte) for every role, which collides far more often on a - * dense mesh. It lived only in the RepeaterMesh and RoomServerMesh - * constructors until now, so a fresh companion quietly originated 1-byte - * floods and reported mode 0 to the app, and `set path.hash.mode default` - * wrote 0 even on a repeater that had booted at 1. Deployed nodes keep - * whatever they stored: path_hash_mode sits at a fixed prefs offset that - * always loads, so this only changes a factory-fresh node. */ + * originates. ZephCore's default for every role; upstream ships 0. Only + * affects a factory-fresh node. */ prefs->path_hash_mode = 1; // 2-byte path hashes prefs->interference_threshold = 0; prefs->leds_disabled = 0; // LEDs on @@ -521,14 +439,8 @@ static inline void initNodePrefs(NodePrefs* prefs) { prefs->v_contact_enabled = 1; // Default ON — v-contact loopback admin chat (companion) prefs->v_battery_alert_mv = 0xFFFF; // Sentinel: derive from board auto-shutdown threshold prefs->wifi_enabled = 1; // as upstream: WiFi still stays off until an SSID is set - /* Companion-only feature, and main_companion.cpp used to assign this by - * hand right after calling us — so no node ever ran without it. It lives - * here now because a default listed only at one call site is invisible to - * every other caller of initNodePrefs(), which is the exact drift that - * zeroed probe_interval and cad_auto in the past. Matches what - * sanitizeNodePrefs() already uses. - * 0 means disabled and is a legal stored value, so sanitize passes it - * through untouched — this default only applies to a fresh prefs struct. */ + /* Companion-only default, set here so every caller of initNodePrefs() sees + * it. 0 means disabled and is a legal stored value. */ #ifdef CONFIG_ZEPHCORE_AUTO_SHUTDOWN_MILLIVOLTS prefs->auto_shutdown_mv = CONFIG_ZEPHCORE_AUTO_SHUTDOWN_MILLIVOLTS; prefs->auto_shutdown_set = 1; diff --git a/zephcore/helpers/battery_curve.c b/zephcore/helpers/battery_curve.c index 02cb24a0..67a5d531 100644 --- a/zephcore/helpers/battery_curve.c +++ b/zephcore/helpers/battery_curve.c @@ -5,13 +5,9 @@ #include "battery_curve.h" /* - * Generic single-cell LiPo OCV curve — 21 points at 5% steps. - * Derived from measured LiPo discharge data; 11-point base expanded to - * 21 points by linear interpolation for better knee resolution. - * - * Index 0 = 100% (4190 mV), index 20 = 0% (3100 mV). - * 100% is 4190 mV, not 4200 mV — reflects observed charge ceiling across - * multiple boards after the charger transitions to maintenance mode. + * Generic single-cell LiPo OCV curve, 21 points at 5% steps. + * Index 0 = 100% (4190 mV, the observed charge ceiling), index 20 = 0% + * (3100 mV). */ static const uint16_t ocv_generic[21] = { 4190, 4120, 4050, 4020, 3990, /* 100 .. 80% */ diff --git a/zephcore/helpers/boot_prefs.h b/zephcore/helpers/boot_prefs.h index 00bdd385..cb90e19f 100644 --- a/zephcore/helpers/boot_prefs.h +++ b/zephcore/helpers/boot_prefs.h @@ -16,18 +16,9 @@ #include "ZephyrGPSManager.h" /* Push the prefs that drive hardware directly (LED gate, GPS duty) into their - * managers. Ordering is load-bearing: - * - after loadPrefs(): before it the struct still holds the initNodePrefs() - * defaults (leds on, gps_interval=300, the companion value). On - * repeater/room server loadPrefs() is also where the 48 h GPS default is - * set and persisted. Calling this too early is the bug class where CLI - * readback is correct and the hardware disagrees. - * - after ui_init(), so the heartbeat cycle exists to be stopped. - * - after the GPS fix/event callbacks are registered, because - * gps_enable() starts acquisition immediately. - * gps_time_sync: put GPS in repeater (time-sync-only) mode and apply - * prefs.gps_enabled, as upstream's applyGpsPrefs(). Only roles that own the - * GPS pass true. */ + * managers. Call after loadPrefs(), after ui_init() and after the GPS + * callbacks are registered. gps_time_sync: repeater (time-sync-only) GPS mode + * plus prefs.gps_enabled; only roles that own the GPS pass true. */ static inline void apply_boot_prefs(const NodePrefs *p, bool gps_time_sync) { bool leds_off = p->leds_disabled != 0; diff --git a/zephcore/helpers/input/stc8h_keypad.c b/zephcore/helpers/input/stc8h_keypad.c index 92a64280..413f16b4 100644 --- a/zephcore/helpers/input/stc8h_keypad.c +++ b/zephcore/helpers/input/stc8h_keypad.c @@ -44,15 +44,9 @@ #include LOG_MODULE_REGISTER(stc8h_keypad, CONFIG_ZEPHCORE_BOARD_LOG_LEVEL); -/* Register map — see dts/bindings/input/zephcore,stc8h-keypad.yaml. - * - * The key register is 0x01, NOT the 0x05 "matrix key" register that the - * reference header documents: the recovered implementation bodies read the - * key from 0x01, and the 0x05 define is dead code nothing consumes. Reading - * 0x05 returns constant 0xFF — that was this driver's original bug (keys - * fully dead, phantom 0xFF at init). The overlap with the battery registers - * (0x01..0x04 little-endian) is odd but is what the shipped firmware does; - * it is also why key reads are IRQ-gated below. */ +/* Register map: see dts/bindings/input/zephcore,stc8h-keypad.yaml. The key + * register is 0x01, not the 0x05 the reference header names (0x05 reads + * 0xFF). It overlaps the battery registers, hence the IRQ-gated key reads. */ #define STC8H_REG_BATTERY 0x01 /* 0x01..0x04, little-endian millivolts */ #define STC8H_REG_KEY 0x01 #define STC8H_REG_STATE 0x06 diff --git a/zephcore/helpers/led_gate.h b/zephcore/helpers/led_gate.h index 020ce72a..726dfacf 100644 --- a/zephcore/helpers/led_gate.h +++ b/zephcore/helpers/led_gate.h @@ -40,18 +40,9 @@ void zephcore_leds_set_disabled(bool disabled); void zephcore_leds_ui_sync(bool disabled); /* - * Mode values for the two per-LED settings. They live here rather than in - * NodePrefs.h, where the rest of the prefs enums are, because both consumers of - * the heartbeat modes are C (helpers/ui/ui_common.c) and NodePrefs.h is C++. - * NodePrefs.h includes this header so the persisted fields and these values - * still have exactly one definition between them. - * - * leds.radio — what the LoRa activity LED (the `lora-tx-led` DT alias) reacts - * to. TX is deliberately value 0: it is what every node did before the setting - * existed, so an absent byte in an old prefs file, a short read, and a - * zero-filled byte all decode to the historical behaviour. Same reasoning - * applies to LEDS_HB_ALL. Boards without the alias have no activity LED at all - * and ignore this setting whatever it says. + * Mode values for the two per-LED settings (here, not in NodePrefs.h, because + * the consumers are C). Value 0 is the historical behaviour in both, so an + * absent or zero byte in an old prefs file decodes to it. */ #define LEDS_RADIO_TX 0 /* lit for the duration of each transmit (default) */ #define LEDS_RADIO_RX 1 /* short pulse on each packet received */ @@ -73,12 +64,8 @@ void zephcore_leds_ui_sync(bool disabled); #define LEDS_HB_MAX LEDS_HB_OFF /* - * Per-LED modes, below the master gate: "set leds off" still wins over both. - * - * Kept here rather than read from NodePrefs directly because the consumers are - * on different threads from the CLI that writes them: the heartbeat runs on the - * system work queue and the activity LED on the radio's TX path, so both need a - * lock-free snapshot rather than a pointer into a struct the main thread edits. + * Per-LED modes, below the master gate ("set leds off" wins). Lock-free + * snapshots: the consumers run on other threads than the CLI that writes. */ uint8_t zephcore_leds_radio_mode(void); void zephcore_leds_set_radio_mode(uint8_t mode); @@ -87,16 +74,9 @@ uint8_t zephcore_leds_hb_mode(void); void zephcore_leds_set_hb_mode(uint8_t mode); /* - * Shared-pin arbitration. On 8 of the supported boards the `lora-tx-led` alias - * IS `led0`, so the heartbeat cycle and the radio activity LED drive the same - * physical GPIO from two different modules. Without this the two clip each - * other: the heartbeat's off-work clears the pin in the middle of a transmit, - * and onAfterTransmit() truncates a heartbeat pulse. - * - * ZephyrBoard raises the hold for the transmit window and for each RX pulse; - * ui_common.c's heartbeat handlers skip lighting and clearing the pin while it - * is held, so radio activity wins and the liveness tick yields. Boards whose - * pins differ compile the check out entirely (see ZEPHCORE_LED_PIN_SHARED). + * Shared-pin arbitration: where `lora-tx-led` is `led0`, the radio holds the + * pin for a transmit or RX pulse and the heartbeat skips it meanwhile. + * Compiled out where the pins differ (ZEPHCORE_LED_PIN_SHARED). */ bool zephcore_led_radio_holds_pin(void); void zephcore_led_radio_hold_pin(bool held); diff --git a/zephcore/helpers/ui-button/ui_pages.c b/zephcore/helpers/ui-button/ui_pages.c index 058dcb67..ba4a4879 100644 --- a/zephcore/helpers/ui-button/ui_pages.c +++ b/zephcore/helpers/ui-button/ui_pages.c @@ -35,18 +35,11 @@ LOG_MODULE_REGISTER(ui_pages, CONFIG_ZEPHCORE_BOARD_LOG_LEVEL); * the joystick UI's splash. See display.h for declaration. */ /* ========== Layout ========== */ -/* All layout is derived from the actual display and font dimensions - * queried at runtime, so the UI adapts to any resolution/font size. - * - * Naming convention: UPPER_CASE macros call display getters (cheap — - * they just return a cached static). This keeps page renderers - * readable while being resolution-independent. - * - * Layout (top to bottom): - * Row 0..FONT_H-1: Top bar (node name left, battery right) - * DOTS_Y..SEP_Y: Page indicator dots + separator line - * CONTENT_Y..end: Page-specific content - */ +/* Layout is derived from the display and font dimensions at run time. The + * UPPER_CASE macros call cached display getters. + * Row 0..FONT_H-1: top bar (node name left, battery right) + * DOTS_Y..SEP_Y: page indicator dots + separator line + * CONTENT_Y..end: page-specific content */ /* Full-scale end of the radio page's TX power bar. The board's configured * ceiling when it has one (same symbol CommonCLI clamps "set tx" against), * else the common SX126x/LR11xx maximum. */ @@ -601,17 +594,9 @@ static void render_tiny_title(void) } /* - * Per-page renderer split (UI profile pattern) - * -------------------------------------------- - * Each page's capability-divergent bodies live in dedicated _mono / _color - * functions. render_() is a thin dispatcher that picks one: - * - Mono / tiny / e-ink panels use render__mono(). - * - RGB565 color panels use render__color(), which is compiled ONLY - * when a `tft` node exists in devicetree (MC_DISPLAY_COLOR_PANEL). On a - * mono board the color body — and every color-only helper it references — - * drops out entirely, so no color RAM/flash is spent. - * The bodies are moved verbatim from the old combined function, so on-screen - * output is byte-identical to before. + * Per-page renderer split: render_() dispatches to render__mono() + * or, on an RGB565 panel, render__color(), which is compiled only when + * a `tft` node exists (MC_DISPLAY_COLOR_PANEL). */ static void render_messages_mono(void) { diff --git a/zephcore/helpers/ui-button/ui_task.c b/zephcore/helpers/ui-button/ui_task.c index 5047f30d..61c1533e 100644 --- a/zephcore/helpers/ui-button/ui_task.c +++ b/zephcore/helpers/ui-button/ui_task.c @@ -71,20 +71,12 @@ extern void ui_led_flash_msg(void); #include LOG_MODULE_REGISTER(ui_task, CONFIG_ZEPHCORE_BOARD_LOG_LEVEL); -/* Action feedback melodies. - * b=200, d=16 → each chirp ~75ms, rest ~75ms = clear separation. - * - * The chirp count identifies the action, not the tap count — the two are only - * equal on boards using the stock tap-codes order, and LED already differs - * there (2 taps, 5 chirps). Do not "fix" a count to match a gesture. - * - * advert sent (zero-hop or flood): chirp-chirp - * buzzer / notification mode: chirp×3 + high(ON) or low(OFF) - * GPS toggle: chirp×4 + high(ON) or low(OFF) - * LED heartbeat toggle: chirp×5 + high(ON) or low(OFF) - * - * ON tail: high E7 (~2637Hz) = "enabled" - * OFF tail: low G5 (~784Hz) = "disabled" */ +/* Action feedback melodies (b=200, d=16: ~75 ms chirps). The chirp count + * identifies the action, not the tap count. + * advert sent: chirp x2 + * buzzer / notification mode: chirp x3 + high (on) or low (off) + * GPS toggle: chirp x4 + high or low + * LED heartbeat toggle: chirp x5 + high or low */ #define MELODY_BEEP_2 "b2:d=16,o=7,b=200:c,p,c" #define MELODY_BUZZER_ON "bon:d=16,o=7,b=200:c,p,c,p,c,p,p,8e" @@ -542,28 +534,10 @@ static void action_deep_sleep(void) /* ========== Input Event Handler ========== */ #ifdef CONFIG_ZEPHCORE_UI_DISPLAY -/* Set when a button press woke the display; consumed by the resolved action - * code so the wake-up press does not also run its normal action. - * - * The display wakes on the raw press event (INPUT_KEY_0 from gpio-keys), but - * the action it maps to is emitted later by the longpress / multi-tap filters - * (up to tap-delay-ms or long-delay-ms afterwards). By then the display - * reports as on, so the resolved code used to fall through and run normally — - * a single tap woke the screen and immediately paged forward. - * - * The flag is matched against real action codes rather than "the next event" - * because the multi-tap filter passes an unhandled raw KEY_A through per tap - * before it resolves; swallowing that would leave a double tap still acting. - * - * No expiry is needed, but the flag must only be armed when the wake press was - * a raw code that still has an action code coming. ZEPHCORE_UI_DESIGN_JOYSTICK - * depends on ZEPHCORE_ROLE_COMPANION, so the repeater build of a joystick board - * (wio_tracker_l1, gat562_30s — both shipped as repeater artifacts) falls back - * to ZEPHCORE_UI_DESIGN_BUTTON and *does* reach this callback, with its - * joystick GPIOs wired straight to INPUT_KEY_UP/DOWN/LEFT/RIGHT/ENTER and no - * filter in between. There the wake press is already an action code and is - * consumed at the wake, so the flag stays clear and the next press acts. - */ +/* Set when a button press woke the display; the action code that press + * resolves to (emitted later by the longpress / multi-tap filters) is then + * swallowed. Armed only when the wake press was a raw code with an action + * code still to come. */ static bool display_woken_pending; static bool is_ui_action_code(uint16_t code) @@ -650,16 +624,8 @@ static void ui_input_cb(struct input_event *evt, void *user_data) } #ifdef CONFIG_ZEPHCORE_UI_DISPLAY - /* If display is off, wake it and consume the event. The action this - * press resolves to arrives later from the longpress / multi-tap filter; - * display_woken_pending makes the switch below swallow it. - * - * Only arm that flag when this press really does resolve into a *later* - * action code, i.e. when the code we just consumed is a raw one. Boards - * that wire an action code straight to the GPIO (joystick lines on - * wio_tracker_l1 / gat562_30s in repeater builds) already consumed their - * action here — arming the flag would make the next, genuinely separate - * press get swallowed instead. */ + /* Display off: wake it and consume the event. Arm display_woken_pending only + * if the consumed code is a raw one, whose action code arrives later. */ if (!mc_display_is_on()) { mc_display_on(); display_woken_pending = !is_ui_action_code(code); @@ -697,20 +663,9 @@ static void ui_input_cb(struct input_event *evt, void *user_data) } #endif - /* Map input key codes to UI actions. - * - * Two-code boards (RAK4631 / WisMesh Pocket / Heltec V3–V4.3 / T-Echo / - * ThinkNode M1 / T-Impulse): 1 tap KEY_1, 2 taps KEY_LEFT; long press - * KEY_ENTER (page enter). Five-code boards emit KEY_B/D/C/E as well, and - * KEY_POWER or KEY_F long → deep sleep. The gesture behind each code is - * the board's business (see the tap-codes note at the top of this file). - * KEY_RIGHT/LEFT/ENTER/UP/DOWN: joystick (Wio Tracker) - * - * NOTE: KEY_A (raw short-press from longpress filter) is NOT handled - * here. It feeds into the multi-tap filter which emits the tap codes. - * Since INPUT_CALLBACK_DEFINE(NULL) sees events from all devices, - * the raw KEY_A events fall through to default: break. - */ + /* Map input key codes to UI actions. Which gesture produces which code is + * the board's business (tap-codes note at the top of this file). Raw KEY_A + * is not handled here: it feeds the multi-tap filter. */ switch (code) { /* ===== Multi-tap outputs ===== */ case INPUT_KEY_1: diff --git a/zephcore/helpers/ui-joystick/joystick_defs.h b/zephcore/helpers/ui-joystick/joystick_defs.h index 1275f286..f11497d0 100644 --- a/zephcore/helpers/ui-joystick/joystick_defs.h +++ b/zephcore/helpers/ui-joystick/joystick_defs.h @@ -9,27 +9,12 @@ #pragma once /* ===== Key codes ===== - * - * These are the UI's INTERNAL codes, carried as a single char through - * JoystickUITask::enqueueKey(). They are deliberately NOT the Zephyr - * INPUT_KEY_* codes — joystick_ui_input_cb() translates between the two — and - * they are never persisted or transmitted, so they can be renumbered freely. - * - * Key-space contract (keep this true): + * The UI's internal codes, one char through JoystickUITask::enqueueKey(); + * not the Zephyr INPUT_KEY_* codes, never persisted or transmitted. * 0x01-0x1F control keys (navigation, enter, cancel) - * 0x20-0x7E RESERVED for printable characters typed on a keyboard + * 0x20-0x7E reserved for printable characters typed on a keyboard * 0xF1-0xFF long-press and global action keys - * - * The printable range is reserved so a keyboard board (ThinkNode M9's STC8H - * matrix MCU at I2C 0x6C) can enqueue characters directly without colliding - * with a control code. The multi-tap action keys below used to sit at - * 0x42-0x45 — i.e. on 'B'..'E' — which would have made typing "BCDE" fire a - * flood advert, toggle GPS, mute the buzzer and kill the LED. - * - * Note KEY_ENTER and KEY_CANCEL are already ASCII CR and ESC, so a keyboard - * that reports plain ASCII (the convention for this class of I2C matrix MCU) - * produces correct enter/cancel with no translation at all. - */ + * KEY_ENTER and KEY_CANCEL are ASCII CR and ESC. */ #define KEY_ENTER 0x0D /* center/OK button click */ #define KEY_LEFT 0x01 /* joystick left */ #define KEY_RIGHT 0x02 /* joystick right */ diff --git a/zephcore/helpers/ui/display.c b/zephcore/helpers/ui/display.c index 8dc5466f..84eefe65 100644 --- a/zephcore/helpers/ui/display.c +++ b/zephcore/helpers/ui/display.c @@ -384,13 +384,8 @@ static void auto_off_handler(struct k_work *work) } /* ========== Early blanking ========== - * OLED controllers (SSD1306, SH1106) turn the display ON during driver init, - * showing stale VRAM from before reset. Our mc_display_init() runs much later - * (after BLE, LoRa, etc.), so there's a visible garbage flash. - * - * Fix: SYS_INIT hook runs right after the driver, sending "Display OFF" before - * main() starts. This is harmless for non-OLED displays (blanking is a no-op - * or already blanked). */ + * OLED controllers switch the panel on during driver init and show stale + * VRAM. Send Display OFF right after the driver, before main(). */ static int display_early_blank(void) { const struct device *dev = NULL; @@ -478,13 +473,8 @@ int mc_display_init(void) return ret; } - /* Font selection. - * Default: smallest height for best text density — our custom 6x8 - * Latin-1 font typically wins on OLEDs. - * LARGE_FONT: smallest font whose height is >= 16 — picks Zephyr's - * built-in 10x16 (cfb_fonts.c) for larger e-paper panels where 6x8 - * is too small to read. Falls back to smallest-overall if no tall - * font is compiled in. */ + /* Font selection: the smallest height by default; with LARGE_FONT the + * smallest font at least 16 high, falling back to the smallest overall. */ const bool want_large = IS_ENABLED(CONFIG_ZEPHCORE_DISPLAY_LARGE_FONT); int num_fonts = cfb_get_numof_fonts(disp_dev); @@ -856,24 +846,9 @@ void mc_display_finalize(void) const int full_interval = CONFIG_ZEPHCORE_DISPLAY_EPD_FULL_REFRESH_INTERVAL; if (is_epd && full_interval > 0 && ++epd_partial_count >= (uint32_t)full_interval) { - /* Periodic full refresh to clear accumulated ghosting. - * - * Replicate the clean post-boot sequence: full-refresh the panel - * to WHITE, then redraw the current page as a partial. blanking_on - * selects the FULL profile; ssd16xx_fill_ram_white clears BOTH RAM - * banks to white; blanking_off runs the full-refresh waveform, - * leaving the panel (and both RAM banks) white. The trailing - * cfb_framebuffer_finalize then redraws the current frame — still - * held in the CFB buffer — as a partial against the all-white - * reference: every content pixel is actively driven and there is - * nothing stale to erase. - * - * Going through white is required. A partial after a full refresh - * of *content* leaves unchanged regions (e.g. the top bar) on the - * neutral waveform → they fade; and forcing the old-frame reference - * white while the panel still shows content fails to erase pixels - * that should clear → letters overlap. The brief white flash is - * the intended clean between frames. */ + /* Periodic full refresh to clear ghosting: full-refresh the panel to white, + * then redraw the current frame as a partial. Going through white is + * required. */ display_blanking_on(disp_dev); #if ZEPHCORE_DISPLAY_HAS_SSD16XX ssd16xx_fill_ram_white(disp_dev); diff --git a/zephcore/helpers/ui/display.h b/zephcore/helpers/ui/display.h index e7c724f6..9e730e89 100644 --- a/zephcore/helpers/ui/display.h +++ b/zephcore/helpers/ui/display.h @@ -85,22 +85,9 @@ bool mc_display_is_on(void); */ bool mc_display_is_epd(void); -/* Color overlay support is compiled only when the devicetree points at a raw - * RGB565 TFT (the runtime probe still verifies pixel format and readiness). - * Boards without one get constant-false / mono-fallback inlines so every - * color code path — including the ~3.8 KB overlay op queue in display.c — - * is dropped at compile time. - * - * Two ways to name the panel, in priority order: - * - * 1. chosen { zephcore,color-tft = <&some_panel>; } - * 2. the `tft` nodelabel on the panel node - * - * (1) exists for boards whose panel node lives in an upstream Zephyr DTS: - * an overlay cannot add a nodelabel to an existing node, but it can always - * set a chosen property. (2) is kept because every in-tree board that had - * color before this indirection names its panel `tft:` — they need no edit. - */ +/* Color overlay support is compiled only when the devicetree names a raw + * RGB565 TFT, by the `zephcore,color-tft` chosen node (for a panel in an + * upstream DTS) or the `tft` nodelabel. Other boards get mono inlines. */ #if DT_HAS_CHOSEN(zephcore_color_tft) #define MC_DISPLAY_COLOR_NODE DT_CHOSEN(zephcore_color_tft) #elif DT_NODE_EXISTS(DT_NODELABEL(tft)) @@ -128,40 +115,10 @@ bool mc_display_is_epd(void); #endif /* - * 180-degree rotation support, decided at compile time from the panel. - * - * Deliberately an allow-list of the two families where the rotation is a - * hardware remap the driver actually implements: display_ssd1306.c flips - * SEGMENT_MAP + COM_OUTPUT_SCAN (two bytes on the wire, framebuffer - * untouched, no per-frame cost), and that driver backs both solomon,ssd1306 - * and sinowealth,sh1106. - * - * Other panel families are excluded on purpose rather than probed: - * - st7735r/st7789v (our mono-tft boards) return -ENOTSUP upstream for - * anything but NORMAL, so a probe would just fail; - * - ssd16xx e-paper *accepts* ROTATED_180 but implements it by flipping - * the RAM entry mode only, which reverses byte order without reversing - * bit order inside each byte — the 8 pixels a byte spans stay in their - * original order. It would report success and render wrong, which is - * worse than reporting unsupported. - * - * The geometry test on top of the family check matters as much as the family - * check itself. Both remaps reverse the controller's *entire* addressable - * range, not the part a given panel happens to use, so the flip only lands - * back on the glass when the visible window is centred in that range: - * - * - Vertically, that means the panel uses the full multiplex height from - * page 0. Boards that window a small panel into a larger controller do - * not: lilygo_timpulse_plus is a 64x32 glass on a 128x64 SSD1306 driven - * at page-offset 4 with multiplex-ratio 63, so its content sits on COM - * 32..63. Reversing the COM scan moves it to COM 31..0 — off the bonded - * region entirely, i.e. a blank screen. `page-offset == 0` and - * `height == multiplex-ratio + 1` is exactly the "uses the whole - * controller" condition, and it excludes that board. - * - Horizontally the surviving boards are already centred: the SSD1306 - * ones are a full 128 columns at segment-offset 0, and the SH1106 ones - * are 128 columns at segment-offset 2 in 132 columns of RAM — the - * standard 2-either-side layout these modules ship with. + * 180-degree rotation support, decided at compile time: an SSD1306/SH1106 + * panel that uses the controller's whole height from page 0 (page-offset 0, + * height == multiplex-ratio + 1). Other families and windowed panels are + * excluded on purpose. */ #if DT_NODE_HAS_COMPAT(MC_DISPLAY_NODE, solomon_ssd1306) || \ DT_NODE_HAS_COMPAT(MC_DISPLAY_NODE, sinowealth_sh1106) diff --git a/zephcore/helpers/ui/doom_game.c b/zephcore/helpers/ui/doom_game.c index 3f01742a..04a175d1 100644 --- a/zephcore/helpers/ui/doom_game.c +++ b/zephcore/helpers/ui/doom_game.c @@ -84,12 +84,8 @@ LOG_MODULE_REGISTER(doom_game, CONFIG_ZEPHCORE_BOARD_LOG_LEVEL); #define BOSS_CAST_PERIOD_RAGE 45 /* 2.25 s, phase 2 */ #define BOSS_CAST_MIN_DIST (FP_ONE * 5 / 2) /* nearer than this it swings */ /* - * Frames the boss spends backing off after a melee swing. Without this it - * closes once and camps in the player's face forever: the cast cycle is held - * below BOSS_CAST_MIN_DIST, so the tell never fires again and the fight - * collapses into a melee grind with nothing to read. Disengaging restores the - * loop — close, swing, back out, cast, close again — and at phase-1 speed 50 - * frames buys back about 1.5 tiles, just past casting range. + * Frames the boss backs off after a melee swing, so the cast cycle can + * start again instead of a melee grind. */ #define BOSS_RETREAT 50 @@ -394,16 +390,9 @@ static uint32_t isqrt64(uint64_t v) return (uint32_t)r; } -/* Turn by sin_r (signed) and put the view back on the unit circle. - * - * The rotation used to be cos = 1 - ROT/8, sin = ROT: a matrix with scale - * ~0.993, plus truncation in every fp_mul, so each turn tick shrank dir and - * plane. Half a view vector after ~1.3 full turns, 1/16 after ~5: walls read - * as ever farther away (the level "stretches for kilometres", steps get - * short), and around ten turns 1/ray_dir overflowed int32 in the raycaster - * and a zero wall height divided by zero - a usage fault that rebooted the - * node. Now: the small-angle cosine, then dir renormalised to exactly unit - * length and plane rebuilt perpendicular to it at its 2/3 field of view. */ +/* Turn by sin_r (signed) and put the view back on the unit circle: the + * small-angle cosine, then dir renormalised to unit length and plane rebuilt + * perpendicular to it. Without the renormalisation the vectors shrink. */ static void rotate_view(struct doom_player *p, fixed_t sin_r) { fixed_t cos_r = FP_ONE - fp_mul(sin_r, sin_r) / 2; @@ -682,16 +671,9 @@ static inline void clear_px(int x, int y) } /* - * Render one 16×16 sprite at world position (wx, wy), scale_pct percent of a - * wall's height with its feet on the floor. Shared by the portal, the enemies - * and the fireballs. - * - * Sprites are solid with a one-pixel black halo, not distance-dithered like - * the walls. On a 1-bit panel a dithered sprite ORed over the dithered floor - * band at the horizon has nothing to contrast against: the L2 boss, which - * starts 8-12 tiles away, was a few dots at best and a grey smudge in the - * floor's own pattern at 4 tiles. The halo pass clears first, then the fill - * sets; both honour the zbuffer, so a nearer wall is never cut into. + * Render one 16x16 sprite at world position (wx, wy), scale_pct percent of a + * wall's height with its feet on the floor. Solid with a one-pixel black + * halo; both passes honour the zbuffer. */ static void render_one_sprite(const uint16_t *sprite, fixed_t wx, fixed_t wy, diff --git a/zephcore/helpers/ui/haptic.c b/zephcore/helpers/ui/haptic.c index fd473891..f9c5cc77 100644 --- a/zephcore/helpers/ui/haptic.c +++ b/zephcore/helpers/ui/haptic.c @@ -21,15 +21,9 @@ LOG_MODULE_REGISTER(zephcore_haptic, CONFIG_ZEPHCORE_SENSORS_LOG_LEVEL); #define HAPTIC_NODE DT_COMPAT_GET_ANY_STATUS_OKAY(ti_drv2605) #if DT_PROP(HAPTIC_NODE, zephyr_deferred_init) -/* Wake a deferred-init DRV2605 and run its init here, where the part's supply - * rail has been up since boot instead of microseconds. - * - * The enable pin is asserted *before* device_init() on purpose. Upstream's - * drv2605_init() spends its DRV2605_POWER_UP_DELAY_US before it configures the - * GPIOs, so EN rises and the very next thing it does is read the status - * register over I2C — no settling time at all between the two. Raising EN - * ourselves and waiting turns that race into a plain sequence; the driver's own - * gpio_pin_configure_dt() to OUTPUT_ACTIVE afterwards is then a no-op. */ +/* Wake a deferred-init DRV2605 and run its init here. The enable pin is + * asserted, with a wait, before device_init(): the driver reads the status + * register right after raising EN. */ static void haptic_deferred_init(const struct device *dev) { static const struct gpio_dt_spec en = diff --git a/zephcore/helpers/ui/ui_common.c b/zephcore/helpers/ui/ui_common.c index c97dbd2c..ca7865d8 100644 --- a/zephcore/helpers/ui/ui_common.c +++ b/zephcore/helpers/ui/ui_common.c @@ -29,13 +29,8 @@ LOG_MODULE_REGISTER(ui_led, CONFIG_ZEPHCORE_BOARD_LOG_LEVEL); /* ========== Input axis flip ========== - * - * Shared by both UI variants so an upside-down mount only has to be - * configured once. Written from the mesh/CLI thread, read from the input - * callback. A plain bool needs no atomic here: it is a single aligned byte, - * and the only race — a keypress landing in the same instant the setting is - * toggled — costs that one keypress its direction, which is what toggling an - * axis swap does anyway. */ + * Shared by both UI variants. Written from the mesh/CLI thread, read from the + * input callback; a plain bool is enough. */ static bool input_flipped; @@ -77,13 +72,8 @@ void ui_play_startup_chime(void) /* ========== LED Heartbeat ========== */ /* - * Uses led0 (or led1 fallback) as a heartbeat indicator. - * Pulse width extends to LED_ON_MSG_MS when there are unread messages, - * driven by ui_led_get_msg_count() which the button variant overrides. - * - * led1 is also claimed as a message indicator in non-repeater companion builds - * when both led0 and led1 are present. The heartbeat cycle turns led1 on - * only when msg count > 0, giving a visual unread-message reminder. + * Heartbeat on led0 (or led1). The pulse widens while there are unread + * messages; with both LEDs present, companions light led1 for unread. */ #if DT_NODE_HAS_PROP(DT_ALIAS(led0), gpios) @@ -125,14 +115,9 @@ static struct k_work_delayable s_led_off_work; __attribute__((weak)) uint16_t ui_led_get_msg_count(void) { return 0; } /* - * Does the heartbeat LED light on this pass? "unread" is not a separate blink - * — it is this same cycle widening its pulse — so the modes are expressed as - * two questions over one cycle: may it light at all right now, and how wide. - * - * The cycle keeps running in every mode including LEDS_HB_OFF. That is on - * purpose: on companions with two LEDs the unread indicator is lit from inside - * this work chain, so stopping the chain would take unread indication down with - * the heartbeat. An idle pass costs one work item every 4 s. + * Does the heartbeat LED light on this pass? The cycle keeps running in + * every mode, LEDS_HB_OFF included, because the unread indicator is lit from + * inside it. */ static bool hb_should_light(uint16_t msg_count) { diff --git a/zephcore/helpers/ui/ui_task.h b/zephcore/helpers/ui/ui_task.h index dbd6fc7e..f84a3151 100644 --- a/zephcore/helpers/ui/ui_task.h +++ b/zephcore/helpers/ui/ui_task.h @@ -257,19 +257,9 @@ void ui_notify_channel_msg(const char *channel_name, const char *text, void ui_notify_packet_sent(void); /* ===== Input axis flip ===== - * - * A case that mounts the board upside down (e.g. the Meshnology N37E kit) - * rotates the joystick along with the screen, so "up" on the stick walks the - * menu down. This flips the two axes back. - * - * Kept separate from the display rotation on purpose: the two are not always - * wanted together — a panel can be remounted alone, and boards whose display - * cannot rotate can still benefit from the axis swap. - * - * The state lives in ui_common.c so both UI variants (button and joystick) - * share one source of truth; each variant's input callback runs its raw - * event code through zephcore_input_map_code() before decoding it. - */ + * For an upside-down mount: flips the two joystick/D-pad axes back. Separate + * from the display rotation on purpose. State in ui_common.c; each variant's + * input callback maps its raw code through zephcore_input_map_code(). */ /** * Enable or disable the joystick/D-pad axis swap. diff --git a/zephcore/src/ContentionTracker.cpp b/zephcore/src/ContentionTracker.cpp index a3aaeeb9..ac516197 100644 --- a/zephcore/src/ContentionTracker.cpp +++ b/zephcore/src/ContentionTracker.cpp @@ -152,15 +152,8 @@ void ContentionTracker::tick(uint32_t now_ms) if (_last_retransmit_ms != 0 && now_ms - _last_retransmit_ms > STALE_MS && _ema_x256 > 0) { if (_last_decay_ms == 0) { - /* Arm one full period in the PAST, so the loop below - * applies a step on this very call. The old code decayed - * immediately on the first tick that observed staleness; - * arming at now_ms instead would make the first check - * (0 < DECAY_PERIOD_MS) break without decaying, deferring - * onset by a whole period on every stale transition — and - * would let the reset branch below starve decay entirely - * for traffic that goes stale and un-stale repeatedly. - * Unsigned wraparound makes this exact even near zero. */ + /* Arm one full period in the past, so the loop below applies a step on + * this call. Unsigned wraparound keeps it exact near zero. */ _last_decay_ms = now_ms - DECAY_PERIOD_MS; } for (int n = 0; n < MAX_DECAY_CATCHUP; n++) { diff --git a/zephcore/src/main_companion.cpp b/zephcore/src/main_companion.cpp index ffd49801..d86b4a74 100644 --- a/zephcore/src/main_companion.cpp +++ b/zephcore/src/main_companion.cpp @@ -200,12 +200,9 @@ extern "C" void zephcore_persist_before_off(void) #endif /* ========== Companion transports ========== - * * As upstream's companion_radio/main.cpp: every transport this build has is a - * BaseSerialInterface in one MultiSerialInterface, and CompanionMesh sees only - * that (through CompanionSerial, which keeps a lossless reply the transports - * could not take). Every connected transport gets every frame; a command from - * any of them is served. */ + * BaseSerialInterface in one MultiSerialInterface. Every connected one gets + * every frame; a command from any of them is served. */ static MultiSerialInterface interface_manager; static CompanionSerial companion_serial(interface_manager); #if IS_ENABLED(CONFIG_BT) @@ -337,14 +334,8 @@ static void process_companion_rx(void) } /* The contact dump, as far as every connected transport has room - * (MESH_EVENT_CONTACT_ITER): BLE and TCP below their 2/3 high-water mark and - * not congested, USB with room for a whole frame. A transport's TX idle - * brings us back. - * - * Only while a client is connected: with none, nothing is busy and no write - * succeeds, so the dump would not advance and this loop would not end. The - * last client can leave while we are in here (its callbacks preempt this - * thread), before MESH_EVENT_LINK has cancelled the dump. */ + * (MESH_EVENT_CONTACT_ITER); a transport's TX idle brings us back. Only while a + * client is connected: with none no write succeeds and the loop would not end. */ static void run_contact_iteration(void) { #ifdef ZEPHCORE_LORA diff --git a/zephcore/src/server_main_common.cpp b/zephcore/src/server_main_common.cpp index c8de5bd1..1ea9351e 100644 --- a/zephcore/src/server_main_common.cpp +++ b/zephcore/src/server_main_common.cpp @@ -36,13 +36,8 @@ extern "C" void bt_ctlr_assert_handle(char *file, uint32_t line) #endif /* USB CDC ACM init + 1200-baud DFU + DTR callbacks (shared with companion). - * - * Gate on the CDC-ACM class driver, NOT on DT_HAS_COMPAT_STATUS_OKAY alone: a - * board overlay may expose a cdc_acm_uart DT node unconditionally (the shared - * esp32s3_usb_otg.dtsi does), so the node can be present while the class driver - * — and therefore zephcore_usbd_* and the device object — is not compiled - * (e.g. an ESP32-S3 server built without esp32s3_usb.conf). This mirrors the - * CMake condition that compiles ZephyrUSBCDC.cpp. */ + * Gated on the class driver, as the CMake condition that compiles + * ZephyrUSBCDC.cpp: the DT node can exist without it. */ #define ZEPHCORE_USB_STACK \ (IS_ENABLED(CONFIG_USB_CDC_ACM) || IS_ENABLED(CONFIG_USBD_CDC_ACM_CLASS)) @@ -67,16 +62,8 @@ extern "C" void bt_ctlr_assert_handle(char *file, uint32_t line) #include "display.h" #endif -/* Headless servers link the weak no-op ui_* stubs (ui_headless_stubs.c), so - * the periodic UI refresh in the maintenance pass is pure work for nothing on - * them. Guard the hot path on a real ui_* implementation being linked; the - * one-shot calls at init and in the CLI reply path stay unguarded, matching - * the rest of the file. - * - * This MUST mirror the CMake condition that selects the stubs, not the display - * devicetree node: ZEPHCORE_UI_DESIGN_BUTTON is enabled by BUTTONS *or* - * DISPLAY *or* BUZZER, so a board with buttons/buzzer and no panel still links - * the real UI and still needs these updates. */ +/* A real ui_* implementation is linked (not the headless stubs). Mirrors the + * CMake condition that selects the stubs, not the display devicetree node. */ #define ZEPHCORE_HAS_UI (IS_ENABLED(CONFIG_ZEPHCORE_UI_DESIGN_BUTTON) || \ IS_ENABLED(CONFIG_ZEPHCORE_UI_DESIGN_JOYSTICK)) @@ -103,17 +90,9 @@ static const struct gpio_dt_spec led1 = GPIO_DT_SPEC_GET(LED1_NODE, gpios); #define MESH_EVENT_WAKE BIT(MESH_EVENT_ROLE_BASE + 1) /* Off-main state set; run loop() promptly */ #define MESH_EVENT_ALL (MESH_EVENT_LORA_RX | MESH_EVENT_LORA_TX_DONE | MESH_EVENT_CLI_RX | MESH_EVENT_MAINTENANCE | MESH_EVENT_GPS_ACTION | MESH_EVENT_TX_DRAIN | MESH_EVENT_INIT_ADVERT | MESH_EVENT_WAKE) -/* Maintenance is deadline-driven, not periodic: after every pass the loop asks - * the mesh when its soonest pending deadline is (msUntilNextMaintenance) and - * arms a single one-shot wake for exactly that moment. An idle node with - * nothing scheduled therefore sleeps until its next real deadline instead of - * waking on a fixed cadence. - * - * MAINTENANCE_BACKSTOP_MS bounds that: it caps how long we will go without a - * pass even when everything reports idle, so a deadline that is missed or - * mis-reported degrades to the old behaviour instead of wedging. - * MAINTENANCE_MIN_MS floors it, so an item that is due-but-blocked (radio - * mid-packet, duty-cycle sleep window) re-arms shortly rather than spinning. */ +/* Maintenance is deadline-driven: after every pass a one-shot is armed for the + * mesh's soonest deadline. MAINTENANCE_BACKSTOP_MS caps the wait, + * MAINTENANCE_MIN_MS floors it. */ #define MAINTENANCE_BACKSTOP_MS CONFIG_ZEPHCORE_MAINTENANCE_BACKSTOP_MS #define MAINTENANCE_MIN_MS 50 @@ -256,16 +235,9 @@ static bool cli_rx_bytes(void) const uint8_t prev = cli_prev_byte; cli_prev_byte = byte; - /* Process command on \r OR \n. Accepting both is what makes a piped - * `echo "cmd" > /dev/ttyACM0` (LF only) work. The cost is that a CRLF - * terminal sends two terminator bytes per Enter, so collapse the pair - * here: skip a terminator that directly follows a *different* one. - * "\r\r" and "\n\n" (two genuine blank lines) still dispatch twice. - * Blank lines ARE dispatched -- `region load` commits on one, and - * handleCommand() no-ops a blank line otherwise. The previous - * de-duplicator keyed on an empty line buffer instead: it ate every - * blank line (stranding `region load` until a reboot) and still let - * the CRLF tail fall through and emit a stray newline of its own. */ + /* Process the command on \r or \n, collapsing a CRLF pair: skip a terminator + * that directly follows a different one. Blank lines are dispatched + * (`region load` commits on one). */ if (byte == '\r' || byte == '\n') { if ((prev == '\r' || prev == '\n') && byte != prev) { continue; /* tail of a CRLF/LFCR pair */ @@ -342,17 +314,9 @@ static void maintenance_timer_fn(struct k_timer *timer) k_event_post(&mesh_events, MESH_EVENT_MAINTENANCE); } -/* Ask the mesh for its soonest pending deadline and arm the one-shot for it. - * Called after every loop pass, whatever woke us: any event may have created - * or cleared a deadline (a queued advert, a CLI tempradio command, a CAD probe - * that just ran), so the schedule is recomputed from scratch each time rather - * than tracked incrementally. - * - * The backstop below is an unconditional CEILING on the wait, not a fallback - * used only when everything reports idle. That means it must stay well above - * the shortest legitimate recurring deadline (the noise-floor sampler, and the - * CAD probe) or it becomes the effective period and the deadline scheduling - * buys nothing — see ZEPHCORE_MAINTENANCE_BACKSTOP_MS. */ +/* Ask the mesh for its soonest pending deadline and arm the one-shot for it, + * after every loop pass. The backstop is a ceiling on the wait, so it must + * stay well above the shortest recurring deadline. */ static void arm_maintenance_wake(void) { uint32_t delay = MAINTENANCE_BACKSTOP_MS; @@ -576,25 +540,9 @@ static void server_event_loop(void) #endif #ifdef ZEPHCORE_LORA - /* Radio maintenance runs OPPORTUNISTICALLY, on every pass, whatever - * woke us — not only when its own timer fired. - * - * Every item inside is deadline-gated internally, so this is nearly - * free when nothing is due. The point is what it does to a busy - * node: a hilltop repeater is already waking constantly for packets, - * so maintenance rides along on those wakes, its deadlines advance, - * and arm_maintenance_wake() below keeps pushing the timer out — the - * maintenance timer then almost never fires and costs no wakes at - * all. Running it only on its own event (as this did originally) - * inverted that: the busiest nodes, which can least afford it, paid - * the full timer cadence on top of their packet wakes, and every - * blocked sample burned a retry against a channel that is busy for - * sustained reasons (real traffic in isReceiving(), the RSSI - * prefilter) rather than the transient duty-cycle sleep window the - * retries were sized for. - * - * Ordering matters: this sits AFTER packet processing so an inbound - * frame is handled before we spend SPI time on an RSSI sweep. */ + /* Radio maintenance runs on every pass, whatever woke us: each item is + * deadline-gated, and on a busy node it rides along on packet wakes. After + * packet processing, so an inbound frame is handled first. */ if (server_role) { server_role->mesh->maintenanceLoop(); } @@ -673,17 +621,8 @@ int server_main(const ServerRole &role) } #endif - /* First boot on a volume that is not this role's - a fresh chip, a - * companion, or a node that was running Arduino MeshCore, whose nRF52 - * filesystems overlap our lfs_partition - * (devdocs/HANDOVER_lfs_arduino_overlap.md). Erase everything so we - * start from a known state: Zephyr's automount only - * auto-formats the LittleFS volume when it fails to mount, and never - * touches storage_partition (BLE bonds NVS) or QSPI. - * - * Self-limiting, so it needs no "done" marker: the identity is generated - * and saved a few lines below, and loadPrefs() persists defaults on the - * same boot, so the next boot sees this role's data and skips this. */ + /* First boot on a volume that is not this role's: erase everything (LittleFS, + * bond NVS, QSPI). Self-limiting: this boot writes the role's data. */ if (!data_store.hasRoleData()) { LOG_WRN("Volume holds no data for this role - formatting before first boot"); if (!zephcore_fs_format_all(nullptr)) { diff --git a/zephcore/tools/crypto_bench/src/main.cpp b/zephcore/tools/crypto_bench/src/main.cpp index aa5fcfc6..af64879b 100644 --- a/zephcore/tools/crypto_bench/src/main.cpp +++ b/zephcore/tools/crypto_bench/src/main.cpp @@ -82,16 +82,9 @@ static inline void sink(const uint8_t *p, size_t n) /* ---------------------------------------------------------------- timing */ -/* Zephyr's portable cycle counter (DWT on Cortex-M, CCOUNT on Xtensa) — the - * same mechanism ZephyrRNG's two-clock beat uses. See - * memory/zephyrrng-entropy.md for the platform traps. - * - * MIN, not mean, is the headline figure: every perturbation (interrupt, - * cache miss, flash wait state) can only ADD time, so the minimum over N runs - * is the closest estimate of the true cost. Mean is printed alongside because - * a large min/mean gap is itself information — it says the operation is being - * interfered with, which matters on a node where it shares a CPU with the - * radio ISR. */ +/* Zephyr's portable cycle counter (DWT on Cortex-M, CCOUNT on Xtensa). MIN + * is the headline figure (perturbations only add time); the mean is printed + * beside it because a large gap shows interference. */ struct BenchResult { const char *name; uint64_t min_ns; @@ -388,15 +381,8 @@ int main(void) /* ---- composites: real call paths -------------------------------- */ print_header("Per-packet totals — the real RX/TX call paths"); - /* Dedup as implemented today. SimpleMeshTables::wasSeen() and - * markSeen() each call packet->calculatePacketHash() independently, - * and since the 2026-07-02 hasSeen split every caller invokes markSeen - * right after wasSeen returns false — so a NEW packet hashes twice. - * The linear memcmp scan those functions also do is not crypto and is - * not timed here; it is a 160-entry 8-byte compare, nowhere near the - * hash. Packet.cpp is compiled from the main tree, so the buffer - * assembly (the memcpy into a stack buffer) is included, as it is on a - * node. */ + /* Dedup as implemented: wasSeen() and markSeen() each hash the packet, so a + * new packet hashes twice. The table scan is not crypto and is not timed. */ /* Not `static`: a function-local static with a non-trivial constructor * emits __cxa_guard_acquire/release, which Zephyr's minimal C++ runtime * does not provide. ~260 bytes on the stack instead. */ diff --git a/zephcore/tools/lr1110_updater/src/lr11xx_hal_updater.c b/zephcore/tools/lr1110_updater/src/lr11xx_hal_updater.c index af465151..2c7eabbd 100644 --- a/zephcore/tools/lr1110_updater/src/lr11xx_hal_updater.c +++ b/zephcore/tools/lr1110_updater/src/lr11xx_hal_updater.c @@ -26,16 +26,9 @@ LOG_MODULE_REGISTER(lr1110_hal, LOG_LEVEL_INF); /* SPI bus device */ static const struct device *spi_dev = DEVICE_DT_GET(DT_BUS(LR1110_NODE)); -/* Flashing SPI clock cap. - * - * In bootloader mode the chip runs off its internal RC oscillator (the XOSC - * needs either a crystal or a powered TCXO), so it has far less timing - * margin than during normal operation. Flashing pushes ~1000 back-to-back - * 256-byte writes, and the images are encrypted+signed: a SINGLE corrupted - * byte anywhere makes the whole image fail its integrity check at boot, - * with every write still reporting OK (the bootloader never reads back). - * Cap the operational 8-16 MHz down to a conservative rate — the entire - * 239 KB image still takes ~1 s of SPI time at 2 MHz. */ +/* Flashing SPI clock cap. In bootloader mode the chip runs on its RC + * oscillator with less timing margin, and one corrupted byte fails the whole + * signed image with every write reporting OK. */ #define UPDATER_SPI_MAX_HZ 2000000 /* SPI config — manual CS (we toggle NSS via GPIO) */ @@ -102,22 +95,9 @@ static int wait_on_busy(uint32_t timeout_ms) } /* - * Wait for a command the chip has just been given to actually COMPLETE. - * - * The chip does not raise BUSY the instant NSS deasserts — it needs a few - * microseconds. Polling only for "BUSY is low" therefore has a race: on a - * fast host (ESP32-S3 at 240 MHz drives GPIO in nanoseconds) the poll can - * observe the *stale* pre-command LOW and conclude the command is already - * finished. The next transaction then starts clocking while the chip is - * still writing flash, and because WriteFlashEncrypted is fire-and-forget - * — no read-back, no status check — the resulting corruption is silent. - * One bad chunk anywhere invalidates the whole signed image, so the odds - * of a clean flash fall off a cliff as the image grows: a 19 KB loader is - * 77 transactions, a 239 KB firmware is 959. - * - * So: first watch for the rising edge (bounded — a command that finishes - * faster than we can look is fine and simply never appears busy), then - * wait for the fall. + * Wait for a command the chip has just been given to complete: first watch + * for BUSY rising (bounded: a fast command may never show busy), then wait + * for it to fall. Polling for low alone can read the stale pre-command level. */ /* ── Per-command instrumentation ─────────────────────────── * @@ -230,15 +210,9 @@ int lr1110_updater_reset_to_bootloader(void) { printk("Resetting LR1110 into bootloader (BUSY held LOW)...\n"); - /* Semtech lr1110_updater_tool pattern: - * 1. Drive BUSY LOW as output during reset - * 2. Pulse RESET - * 3. Wait 500ms - * 4. Release BUSY back to input - * 5. Wait 100ms + BUSY low - * - * When BUSY is held LOW by the host during reset, the LR1110 - * enters bootloader mode instead of executing flash firmware. */ + /* Semtech lr1110_updater_tool pattern: hold BUSY low as an output across + * the RESET pulse, which makes the LR1110 enter its bootloader; wait 500 ms, + * release BUSY, wait 100 ms and for BUSY low. */ /* Drive BUSY to physical LOW (pin_busy has GPIO_ACTIVE_HIGH, * so we use raw GPIO to be explicit about physical level) */ @@ -266,25 +240,10 @@ int lr1110_updater_reset_to_bootloader(void) return ret; } -/* ── SD card presence probe ─────────────────────────────────── - * - * On boards where the SD slot shares the radio's SPI bus, an inserted card - * breaks LR1110 flashing: every write still reports OK and the programmed - * image fails its integrity check at boot, which is indistinguishable from a - * dozen other faults and cost days to track down. There is no card-detect pin - * wired on the M9, so presence is established over the bus. - * - * Standard SPI-mode detection: >=74 dummy clocks with CS high to bring the - * card up in SPI mode, then CMD0 (GO_IDLE_STATE). A present card answers R1 - * with the MSB clear (0x01 = idle). An empty slot leaves MISO pulled high, so - * every byte reads 0xFF and nothing else. False "absent" is possible if a card - * ignores CMD0 — no worse than not probing; false "present" essentially cannot - * happen, since 0xFF is all an empty slot can produce. - * - * Runs at 400 kHz (SD init is specified at 100-400 kHz; the radio path stays - * at its own clock) and is safe on the shared bus: the LR1110's NSS is parked - * inactive by hal_init, and the TFT is held in reset and is write-only. - */ +/* ── SD card presence probe ── + * An inserted card on the radio's SPI bus silently corrupts LR1110 flashing, + * and the M9 has no card-detect pin. SPI-mode detection at 400 kHz: 74+ dummy + * clocks with CS high, then CMD0; a card answers R1, an empty slot reads 0xFF. */ #if defined(CONFIG_BOARD_THINKNODE_M9) /* No DT node exists for the slot — the base board DTS only parks its CS with * a gpio-hog. GPIO48 = gpio1 pin 16. */ @@ -385,21 +344,8 @@ lr11xx_hal_status_t lr11xx_hal_write(const void *context, const uint8_t *command return LR11XX_HAL_STATUS_ERROR; } - /* Send the command and its payload as ONE contiguous buffer. - * - * Passing them as two spi_bufs makes Zephyr's ESP32 SPI driver walk - * the set buffer-by-buffer (spi_context_max_continuous_chunk() never - * spans a buffer boundary), so a 6-byte command and a 256-byte payload - * become separate hardware transactions — and each is further split at - * SOC_SPI_MAXIMUM_BUFFER_SIZE (64 bytes on the S3, no DMA). - * - * That matters here because WriteFlashEncrypted is the ONLY command - * this tool issues with a payload: every command known to work - * (GetVersion, GetStatus, EraseFlash, the EUI reads) is a single - * sub-64-byte frame. Keeping the frame contiguous — together with a - * flash chunk size chosen so command+payload stays under 64 bytes — - * makes the write path look exactly like the paths already proven - * good on this hardware. */ + /* Send the command and its payload as one contiguous buffer: as two + * spi_bufs the ESP32 SPI driver makes them separate hardware transactions. */ static uint8_t txbuf[LR11XX_HAL_MAX_FRAME]; if ((size_t)command_length + (size_t)data_length > sizeof(txbuf)) { diff --git a/zephcore/tools/lr1110_updater/src/main.c b/zephcore/tools/lr1110_updater/src/main.c index 09c2c070..7d3e4968 100644 --- a/zephcore/tools/lr1110_updater/src/main.c +++ b/zephcore/tools/lr1110_updater/src/main.c @@ -39,15 +39,8 @@ #include "lr1110_bootloader.h" #include "lr1110_bl_updater.h" /* - * Which transceiver image to flash. 0x0402 is the current release (and the - * only one Semtech's own tool will attempt on bootloader 0x1001). - * - * 0x0401 is selectable to TEST whether the downgrade block is enforced by - * the CHIP or merely by the host tool: the compatibility table that pairs - * 0x0401 with bootloader 0x6500 lives in Semtech's reference *application* - * (lr11xx_update_utils.c), not in the silicon. Since 0x0402 is a CVE fix, - * anti-rollback in the new bootloader is plausible — but unverified, and a - * chip that runs 0x0401 is fully usable by ZephCore (T1000-E ships it). + * Which transceiver image to flash. 0x0402 is the current release; 0x0401 is + * selectable to test whether the chip itself blocks a downgrade. * * west build ... -- -DUPDATER_TARGET_FW=0x0401 */ @@ -57,19 +50,8 @@ /* * Perform the 0x6500 -> 0x1001 chip-bootloader update when firmware 0x0402 is - * the target (0x0402 will not run on the old bootloader). - * - * Verified end-to-end on a T1000-E 2026-07-21: Stage A then Stage B, chip - * afterwards reports TYPE=0x01 FW=0x0402 and runs from flash. An earlier - * belief that this update bricks radios came from a sample where every - * observed 0x1001 chip was on one board (ThinkNode M9) — that board has a - * separate, still-open problem, so a board fault was being read as a - * bootloader fault. - * - * It IS one-way: Semtech ships loaders in the forward direction only, and the - * new bootloader lives inside the encrypted loader payload, so there is - * nothing to flash back. Set this to 0 to flash firmware only and leave the - * chip bootloader untouched. + * the target (0x0402 does not run on the old bootloader). One-way: there is + * no loader back. Set to 0 to flash firmware only. */ #ifndef UPDATER_ALLOW_BOOTLOADER_UPDATE #define UPDATER_ALLOW_BOOTLOADER_UPDATE 1 @@ -124,32 +106,16 @@ static void led_off(void) {} static void led_toggle(void) {} #endif -/* Idle gap inserted after every flash page program. - * - * Diagnostic for a marginal supply. A page program pulls current for ~3.6 ms - * and we issue 959 of them back to back, so a rail that cannot sustain that - * burst will sag — and a sagging rail corrupts what gets programmed while the - * write still reports OK (the bootloader never reads back). Spacing the pages - * lets the supply recover between them. 10 ms stretches a full image write - * from ~6.6 s to ~16 s, which costs nothing. - * west build ... -- -DUPDATER_CHUNK_DELAY_MS=10 - */ +/* Idle gap after every flash page program: a diagnostic for a marginal + * supply. + * west build ... -- -DUPDATER_CHUNK_DELAY_MS=10 */ #ifndef UPDATER_CHUNK_DELAY_MS #define UPDATER_CHUNK_DELAY_MS 0 #endif -/* Refuse to flash with an SD card in the slot. - * - * On the M9 the slot shares SPI2 with the radio, and a card present during - * flashing produces a silently corrupt image: every write reports OK, the - * chip spends real time programming, and the result fails its integrity check - * at boot with no error anywhere to point at. Field-confirmed 2026-07-21 — - * removing the card was what finally made that board flash. - * - * Gated rather than warned because the asymmetry is stark: the cost of the - * gate is ejecting a card, the cost of missing it is days of debugging. - * west build ... -- -DUPDATER_IGNORE_SDCARD=1 - */ +/* Refuse to flash with an SD card in the slot (M9: the slot shares SPI2 with + * the radio and a card corrupts the image silently). + * west build ... -- -DUPDATER_IGNORE_SDCARD=1 */ #ifndef UPDATER_IGNORE_SDCARD #define UPDATER_IGNORE_SDCARD 0 #endif @@ -235,27 +201,11 @@ static uint8_t tcxo_code_from_mv(uint16_t mv) return LR1110_TCXO_CTRL_1_6V; } -/* Power the TCXO and switch the chip onto it before touching flash. - * - * On a crystal board the chip can start its 32 MHz XOSC by itself. On a - * TCXO board the oscillator is powered from DIO3, which stays OFF until - * SetTcxoMode is issued — so the chip would otherwise run the ENTIRE - * update on its internal RC oscillator, which is both slower and far less - * accurate. Flash program/erase pulse timing and the charge-pump sequencing - * derive from that clock, so a marginal clock can produce writes that - * report OK but do not survive the image integrity check at boot. - * - * Not in Semtech's update documentation (their reference hardware does not - * need it), but field-reported to make LR1110 flashing succeed on boards - * where it otherwise fails. Best-effort: a chip that rejects the command - * simply stays on RC, exactly as before. */ +/* Power the TCXO and switch the chip onto it before touching flash, so a + * TCXO board does not run the update on its RC oscillator. Best-effort. */ /* - * Off by default. Neither Semtech's SWTL001 nor RadioLib touches the TCXO - * while flashing, and SetTcxoMode (0x0117) / Calibrate (0x010F) are *system* - * opcodes that the bootloader command set does not include — so issuing them - * here is unverified behaviour on a bootloader we need to keep in a - * well-defined state. The field report that TCXO helps was not reproducible - * on this board (the failure predates and survives it). Set to 1 to re-test. + * Off by default: SetTcxoMode and Calibrate are not in the bootloader's + * command set. Set to 1 to re-test. */ #define UPDATER_ENABLE_TCXO 0 @@ -409,15 +359,9 @@ static void erase_and_flash_image(void *ctx, const char *what, const int64_t write_start = k_uptime_get(); - /* MUST be 64 words (256 bytes = one flash page). A 14-word write is - * rejected outright with PERR at chunk 0 — the chip validates the byte - * count and only accepts whole pages (a short final chunk is fine). - * - * NOTE: that PERR result does NOT clear the SPI path, though it was - * once read that way. A 14-word write is a 62-byte frame — one hardware - * transaction on the ESP32-S3 — so it says nothing about whether a - * 262-byte frame survives being split across five. Matches Semtech's - * LR11XX_FLASH_DATA_MAX_LENGTH_UINT32 = 64 regardless. */ + /* Must be 64 words (256 bytes = one flash page): the chip accepts whole + * pages only (a short final chunk is fine). Semtech's + * LR11XX_FLASH_DATA_MAX_LENGTH_UINT32. */ const uint32_t chunk_size = 64; uint32_t num_chunks = (total + chunk_size - 1) / chunk_size; uint32_t progress_step = num_chunks / 10; /* Print every 10% */ @@ -508,28 +452,9 @@ static void loader_rewrite_and_verify(void *ctx) lr1110_bl_updater_report_t report = { 0 }; lr1110_status_t rc; - /* No configure_tcxo() here, deliberately. The 0x8100 rewrite below is a - * flash write and the chip is back on its RC oscillator after the reboot - * into the loader, so this looks like a hole in the TCXO workaround that - * erase_and_flash_image() applies — it is not: - * - * - SWTL001 configures no TCXO at ANY point of the update (its - * lr11xx_update_firmware() is just erase + write; the driver's - * set_tcxo_mode is never called by the update application). Our - * configure_tcxo() is a ZephCore-only field workaround with no - * counterpart in the reference flow. - * - The loader image's documented command set is only 0x8100/01/02 plus - * GetVersion/GetStatus. It exposes no system commands, so SetTcxoMode - * (0x0117) is undocumented against a RUNNING loader — sending it is a - * guess, not a fix. - * - The failure modes are not symmetric. A bad transceiver flash leaves - * the chip falling back to the bootloader, which is retryable. A bad - * bootloader rewrite may not be recoverable at all. That asymmetry is - * what decides it: do not fire an undocumented opcode at the chip in - * the seconds before the one irreversible operation in this tool. - * - * If M9 bring-up ever shows the rewrite failing on a TCXO board, revisit - * with the GetStatus output below as evidence — do not add it blind. */ + /* No configure_tcxo() here, deliberately: SetTcxoMode is undocumented + * against a running loader, and this is the one irreversible operation in + * the tool. */ /* Ask the loader to rewrite the bootloader — BUSY is held for the * duration; the HAL grants opcode 0x8100 the long timeout. */ @@ -761,16 +686,9 @@ int main(void) fatal_error("Unknown chip type — cannot proceed"); } - /* Enter the bootloader the way RadioLib does: the SOFTWARE reboot - * command with stay_in_bootloader=true. - * - * We previously used the BUSY-held hardware reset (what SWTL001 - * does). Both are documented, but RadioLib is the reference that - * demonstrably flashes this chip family from an ESP32 host, and the - * entry method is the last structural difference left between its - * flow and ours — opcodes, offsets, byte order and 64-word pages are - * all now verified identical. The BUSY-held reset stays as the - * fallback for a chip that will not answer commands at all. */ + /* Enter the bootloader as RadioLib does: the software reboot command with + * stay_in_bootloader=true. The BUSY-held hardware reset stays as the + * fallback for a chip that does not answer. */ printk("[4/8] Entering bootloader (software reboot, stay=true)...\n"); lr1110_bootloader_reboot(ctx, true); k_msleep(500); @@ -863,19 +781,9 @@ int main(void) erase_and_flash_image(ctx, "transceiver firmware", lr11xx_firmware_image, LR11XX_FIRMWARE_IMAGE_SIZE); - /* Boot the freshly written firmware with the bootloader's own reboot - * command (0x8005, stay=false) — and then DO NOT TOUCH NRESET. - * - * The LR1110 samples BUSY as NRESET is released and enters the - * bootloader whenever it reads LOW. Nothing holds that line high while - * the chip is in reset, so a hardware reset here lands back in the - * bootloader every time — which is precisely what the old "re-reset for - * a clean state" did, making this step report "firmware not running" - * no matter how good the flash was. Stage A reboots the very same way - * *without* a trailing hardware reset, and its image always came up - * running: that asymmetry is what exposed this. - * - * Firmware boot takes ~273 ms (datasheet); 500 ms is comfortable. */ + /* Boot the new firmware with the bootloader's reboot command (0x8005, + * stay=false) and do not touch NRESET afterwards: the chip samples BUSY as + * NRESET is released and would re-enter the bootloader. Boot takes ~273 ms. */ printk(" Rebooting LR1110 into new firmware...\n"); lr1110_bootloader_reboot(ctx, false); k_msleep(500); diff --git a/zephcore/tools/rng_selftest/src/main.cpp b/zephcore/tools/rng_selftest/src/main.cpp index a3b1c1c5..c851ab84 100644 --- a/zephcore/tools/rng_selftest/src/main.cpp +++ b/zephcore/tools/rng_selftest/src/main.cpp @@ -38,14 +38,8 @@ #include #if defined(CONFIG_SOC_FAMILY_ESPRESSIF_ESP32) -/* RTC-slow-clock beat diagnostic. ESP32's k_cycle_get_32() is CCOUNT, same - * clock domain as the CPU, so CPU-jitter sampling measures a deterministic - * loop (proven on hardware: maxrep in the thousands, ~0 bits). The RTC slow - * clock is a SEPARATE oscillator (internal ~136 kHz RC, or a 32 kHz crystal) - * that drifts independently of the main-crystal->PLL->240 MHz path — so the - * number of CPU cycles that elapse across a fixed RTC interval fluctuates, and - * that fluctuation is real physical entropy. This is what nRF gets for free - * (its system timer IS the 32 kHz RTC). This diagnostic MEASURES that beat; it +/* RTC-slow-clock beat diagnostic (ESP32): measures the CPU cycles elapsed + * across a fixed interval of the RTC slow clock, a separate oscillator. It * does not touch identity generation. */ #include #define HAVE_RTC_BEAT 1 From 81d98abf2ee3c94f06624f709f54e8d574a37641 Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Tue, 6 Oct 2026 21:54:36 +0200 Subject: [PATCH 13/16] HLD update --- docs/ARCHITECTURE.md | 11 ++++++++--- docs/DESIGN.md | 4 ++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index a0ea9b06..43430a80 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -34,7 +34,7 @@ ZephCore is a LoRa mesh networking firmware running on Zephyr RTOS. It supports - **Room Server**: Headless store-and-forward shared message room (BBS). Reuses the repeater's ACL/region/CLI; pushes new posts to logged-in clients (per-client sync cursor + ACK). - **Observer** (ESP32): Listen-only node that publishes received LoRa packets to MQTT over WiFi. -Supported hardware: nRF52840, nRF54L15, ESP32 (classic PICO-D4 and C3/C6/S3), EFR32MG24, and STM32WL (LoRa-E5). Radios: SX126x family (SX1261/62/68, LLCC68, STM32WL sub-GHz), LR1110, SX127x (SX1272/76/78, loramac-node backend), and LR2021 (validated on the MeshTracker X1). A native Linux port runs the full stack on SBCs (Femtofox, Raspberry Pi) via Zephyr `native_sim` — see `LINUX_NATIVE.md`. +Supported hardware: nRF52840, nRF54L15, nRF54LM20A, ESP32 (classic PICO-D4 and C3/C6/S3), EFR32MG24, and STM32WL (LoRa-E5). Radios: SX126x family (SX1261/62/68, LLCC68, STM32WL sub-GHz), LR1110, SX127x (SX1272/76/78, loramac-node backend), and LR2021 (validated on the MeshTracker X1). A native Linux port runs the full stack on SBCs (Femtofox, Raspberry Pi) via Zephyr `native_sim` — see `LINUX_NATIVE.md`. ### Upstream Relationship @@ -124,7 +124,7 @@ zephcore/ ├── boards/ # Board definitions │ ├── common/ # Shared configs, DTS includes, partition layouts │ ├── nrf52840/ # RAK4631, T1000-E, ThinkNode M1/M3/M6, T-Echo, T114, ... -│ ├── nrf54l/ # XIAO nRF54L15 +│ ├── nrf54l/ # XIAO nRF54L15, Seeed LR2021 EVK (nRF54L15 and nRF54LM20A), ME25LS02 │ ├── esp32/ # XIAO C3/C6/S3, Heltec V3/V4.x, Station G2, T-Beam, ... │ ├── mg24/ # XIAO MG24 │ ├── stm32wl/ # Seeed LoRa-E5 mini @@ -722,6 +722,7 @@ Autonomous operation features: - **Permission levels**: GUEST(0), READ_ONLY(1), READ_WRITE(2), ADMIN(3) - **Region filtering**: `RegionMap` with transport key matching per flood packet - **Rate limiting**: 4 requests per 120s (discovery), 4 per 180s (anonymous), 4 failed logins per 180s +- **Node discovery**: answers discovery requests only while forwarding is on. With `advert.interval` and `flood.advert.interval` both `0` the node is hidden and does not answer, as upstream (`isHiddenNode()`); the request still counts against the discovery rate limit - **Neighbor tracking**: RSSI/SNR/name/timestamp table (`CONFIG_ZEPHCORE_MAX_NEIGHBOURS`, default 50 slots) - **Temporary radio params**: `tempradio` command applies freq/bw/sf/cr via `LoRaRadio::setRadioOverride()` (does not mutate `_prefs`); auto-revert timer calls `clearRadioOverride()` to fall back to saved prefs - **WiFi+MQTT uplink** (ESP32, `CONFIG_ZEPHCORE_REPEATER_UPLINK`): `RepeaterUplink.cpp` reports packets observer-style while still repeating; configured via `set uplink.*` CLI @@ -859,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. 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), 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 @@ -998,6 +999,7 @@ prj.conf (base: console; production defaults — LOG=n, ASSERT=n) - **nRF52840**: Zephyr open-source BLE controller, UF2 bootloader, partial flash erase for BLE coexistence - **nRF54L15**: Same BLE controller as nRF52, CMSIS-DAP via SAMD11 bridge, no native USB +- **nRF54LM20A** (the LR2021 EVK with a XIAO nRF54LM20A, its own board directory): built and flashed like the nRF54L15 (`--no-sysbuild`, CMSIS-DAP); source-only, no published firmware - **ESP32-C3/C6/S3**: Espressif proprietary BLE blob, 32KB heap, asserts disabled (blob IRQ false positives); simple-boot by default, MCUboot only with `--sysbuild` (WiFi OTA) - **ESP32 classic (PICO-D4)**: much smaller DRAM — contact/queue caps shrunk in `board.conf`; console/CLI on `uart0` (no native USB); DIO flash mode required (QIO bootloops) - **EFR32MG24**: SiLabs proprietary BLE blob, 32KB heap, SEMAILBOX enabled for hardware TRNG/crypto entropy, ADC disabled (no battery divider), CMSIS-DAP via onboard SAMD11 @@ -1019,6 +1021,8 @@ Applied automatically at CMake configure time; a failed patch aborts the configu | 0008-flash-sim-per-node-file | LOW | Flash simulator defaults to per-node settings file (native Linux) | | 0009-display-ssd16xx-fill-ram-white | LOW | E-paper full-refresh-to-white anti-ghosting helper | | 0010-uarte-pm-suspend-bounded-rxto-wait | MEDIUM | Bounds the nRF UARTE STOPRX/RXTO spin on PM suspend; unbounded upstream, wedges the mesh thread | +| 0017-wifi-esp32-rx-never-block | MEDIUM | ESP32 Wi-Fi RX callback drops a frame instead of waiting 100 ms for a net buffer; the wait livelocked RX under load | +| 0019-usb-cdc-acm-poll-out-flush | MEDIUM | USB CDC ACM: a full TX buffer is handed to the stack from `poll_out()` and `fifo_fill()`, and only the current buffer counts as free space while the bus is suspended. Without it console bursts are cut at 64 bytes and a companion can hang on its first reply after boot | **One patch per file.** No upstream file is touched by more than one patch, so apply order is irrelevant and no patch can be anchored inside another's added @@ -1068,6 +1072,7 @@ Build strings: `docs/supported_boards.md`. Flash methods: `docs/BUILDING.md`. Ad | ProMicro SX1262 | nRF52840 | SX1262 (E22-900M30S) | Yes | - | Button, LED, battery ADC | | muzi works R1 Neo | nRF52840 | SX1262 | Yes | - | Buzzer, button, RX8130CE RTC, latched-rail power-off | | XIAO nRF54L15 | nRF54L15 | SX1262 | - | - | Contacts capped at 450 | +| Seeed LR2021 LoRa Plus EVK | nRF54L15 or nRF54LM20A | **LR2021** | - | - | Two board directories, one per XIAO; the nRF54LM20A one is source-only | | XIAO ESP32-C3 | ESP32-C3 | SX1262 | - | - | Contacts capped at 300 | | XIAO ESP32-C6 | ESP32-C6 | SX1262 | - | - | - | | LilyGo TLoRa C6 | ESP32-C6 | SX1262 | - | - | - | diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 9484b077..f7b60170 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -23,7 +23,7 @@ multiple MCU families. bounded recovery (§8.7). 3. **Upstream portability** — most new mesh features arrive from Arduino MeshCore, so code that mirrors upstream stays structurally identical to it (§9). -4. **Hardware breadth** — one codebase across nRF52840, nRF54L15, the ESP32 family, EFR32MG24, STM32WL and native +4. **Hardware breadth** — one codebase across nRF52840, nRF54L15/LM20A, the ESP32 family, EFR32MG24, STM32WL and native Linux, with four radio families. 5. **Resource discipline** — static allocation for the packet path; companion builds are RAM-bound. @@ -104,7 +104,7 @@ flowchart LR | Dimension | Values | Selected by | |---|---|---| -| Platform | nRF52840, nRF54L15, ESP32 classic, ESP32-S3/C3/C6, EFR32MG24, STM32WL, native Linux | board directory | +| Platform | nRF52840, nRF54L15, nRF54LM20A, ESP32 classic, ESP32-S3/C3/C6, EFR32MG24, STM32WL, native Linux | board directory | | Radio | SX126x family (default), LR1110, LR2021, SX127x | devicetree / board conf | | Role | companion (default), repeater, room server, observer | user `EXTRA_CONF_FILE` | | UI | none, button UI, joystick/keypad UI (companion) | board Kconfig select | From 3bfdaa87256246b0bec1f3978cb701dee3842f53 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 6 Oct 2026 14:15:56 -0700 Subject: [PATCH 14/16] rtc: restore two comment corrections the merge trim lost Local review: the trimmed rv3028_write_time() comment said a write cut before the year byte leaves the mark, which a cut on the A0h byte itself does not (ed8802f had this right), and the trimmed header called the whole A0h-FEh range the mark a cut write leaves (b9fb124 had narrowed it). The time block comment again names the two-digit year as ZephCore's encoding. Co-Authored-By: Claude Opus 5.5 --- zephcore/adapters/clock/ZephyrRTCDiscover.c | 4 ++-- zephcore/adapters/clock/ZephyrRTCDiscover.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index b8144647..d56c9d85 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -225,7 +225,7 @@ static enum rtc_verdict rtc_identify(const struct rtc_desc *d, uint8_t blk[7]) } /* The 7-byte time block for an epoch, in d's register order. False outside - * 2000-2099, which a two-digit BCD year with no century bit cannot hold. */ + * 2000-2099: ZephCore writes the year as two BCD digits and no century bit. */ static bool rtc_time_block(const struct rtc_desc *d, uint32_t epoch, uint8_t blk[7]) { int y; @@ -284,7 +284,7 @@ static void rtc_clear_power_flag(const struct rtc_desc *d) #define RV3028_YEAR_UNSET 0xA0 /* not BCD; a year of A0h-FEh reads "not set" */ /* Mark the year A0h, then write all seven registers in one access (4.5): - * a write cut before the year byte leaves the mark. */ + * a write cut between the mark and the year byte leaves the mark. */ static bool rv3028_write_time(const struct rtc_desc *d, const uint8_t blk[7]) { return i2c_reg_write_byte(d->bus, d->addr, d->time_reg + 6, RV3028_YEAR_UNSET) == 0 && diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.h b/zephcore/adapters/clock/ZephyrRTCDiscover.h index 323df3e0..76ea11f3 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.h +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -32,7 +32,7 @@ extern "C" { * * A descriptor with rv3028-eeprom-config also has that config stored in the * chip's EEPROM, retried on the system work queue if it fails. There, a year - * of A0h-FEh, the mark a cut time write leaves, gives no time. + * of A0h-FEh gives no time: the A0h mark, or a New Year's count on from it. */ bool zephcore_rtc_restore(uint32_t *epoch_out); From 9550a815b5c62cb48abfeb726060cd06db480f3b Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 6 Oct 2026 14:21:47 -0700 Subject: [PATCH 15/16] rtc: state the A0h-FEh rule without a reason, and the 2000-2099 write range Local review: "the A0h mark, or a New Year's count on from it" is false for the xAh-xFh values in the range, which a count from A0h never reaches; the code and header now state the rule alone. The Kconfig help and ARCHITECTURE.md said every sync writes the RTC, which a time outside 2000-2099 no longer does. Co-Authored-By: Claude Opus 5.5 --- docs/ARCHITECTURE.md | 2 +- zephcore/Kconfig | 4 ++-- zephcore/adapters/clock/ZephyrRTCDiscover.c | 2 +- zephcore/adapters/clock/ZephyrRTCDiscover.h | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 43430a80..e07f786b 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -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 and the time is within 2000-2099 (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. **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 diff --git a/zephcore/Kconfig b/zephcore/Kconfig index 90d8d3ad..485b86ec 100644 --- a/zephcore/Kconfig +++ b/zephcore/Kconfig @@ -911,8 +911,8 @@ 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 diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index d56c9d85..ca10b363 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -644,7 +644,7 @@ static bool rtc_probe(uint32_t *epoch_out) uint8_t db = blk[d->date_index] & 0x3F, ob = blk[5] & 0x1F, yb = blk[6]; #if RTC_RV3028_CFG - /* The mark, or a New Year's count on from it; FFh is a cut read. */ + /* A0h-FEh holds the A0h mark; FFh is a cut read. */ if (d->cfg != NULL && yb >= RV3028_YEAR_UNSET && yb != 0xFF) { LOG_INF("%s present, time not yet set (year %02Xh)", d->name, yb); continue; diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.h b/zephcore/adapters/clock/ZephyrRTCDiscover.h index 76ea11f3..ea29fe2a 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.h +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.h @@ -32,7 +32,7 @@ extern "C" { * * A descriptor with rv3028-eeprom-config also has that config stored in the * chip's EEPROM, retried on the system work queue if it fails. There, a year - * of A0h-FEh gives no time: the A0h mark, or a New Year's count on from it. + * of A0h-FEh gives no time. */ bool zephcore_rtc_restore(uint32_t *epoch_out); From 04bf64dda5cd9cd966a2b34fa04614f7c4b41a00 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 6 Oct 2026 14:38:20 -0700 Subject: [PATCH 16/16] rtc: say a refused RV3028 save marks the year, drop the FFh reason Local review: ARCHITECTURE.md said an out-of-range time is not written, but at an RV3028 with rv3028-eeprom-config its year is marked instead. A year of FFh reaching the A0h-FEh check has passed the BSF-checked read, so it is a stored value, not a cut read; the comment no longer gives a reason. A Kconfig help line the last edit left long is wrapped. Co-Authored-By: Claude Opus 5.5 --- docs/ARCHITECTURE.md | 2 +- zephcore/Kconfig | 3 ++- zephcore/adapters/clock/ZephyrRTCDiscover.c | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index e07f786b..34fc91f5 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -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 and the time is within 2000-2099 (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, and at an RV3028 with `rv3028-eeprom-config` its year is marked unset instead), 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 diff --git a/zephcore/Kconfig b/zephcore/Kconfig index 485b86ec..0cd751c4 100644 --- a/zephcore/Kconfig +++ b/zephcore/Kconfig @@ -912,7 +912,8 @@ config ZEPHCORE_RTC_AUTODISCOVER 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 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 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 diff --git a/zephcore/adapters/clock/ZephyrRTCDiscover.c b/zephcore/adapters/clock/ZephyrRTCDiscover.c index ca10b363..fdbe54ce 100644 --- a/zephcore/adapters/clock/ZephyrRTCDiscover.c +++ b/zephcore/adapters/clock/ZephyrRTCDiscover.c @@ -644,7 +644,7 @@ static bool rtc_probe(uint32_t *epoch_out) uint8_t db = blk[d->date_index] & 0x3F, ob = blk[5] & 0x1F, yb = blk[6]; #if RTC_RV3028_CFG - /* A0h-FEh holds the A0h mark; FFh is a cut read. */ + /* A0h-FEh holds the A0h mark. */ if (d->cfg != NULL && yb >= RV3028_YEAR_UNSET && yb != 0xFF) { LOG_INF("%s present, time not yet set (year %02Xh)", d->name, yb); continue;