diff --git a/zephcore/Repeater_CLI_commands.md b/zephcore/Repeater_CLI_commands.md index fe17251..8e2ecec 100644 --- a/zephcore/Repeater_CLI_commands.md +++ b/zephcore/Repeater_CLI_commands.md @@ -206,7 +206,7 @@ All `set uplink.*` changes are saved immediately and only applied after reboot. | `get radio.rxgain` | RX gain boost: `0` or `1` | | `get rxduty` | RX duty cycle mode: `0` or `1` | | `get gps duty` | Now-effective GPS duty interval in seconds (`always on (0)` when continuous) | -| `get meshtimesync` | Mesh time-sync state + live dry-run: on/off, eligible voter count, votes for/against, consensus skew and radius, would-be verdict (`ok`/`in-band`/`step±N`/`abstain (reason)`/`hold (reason)`; a recent clock set — manual or GPS — shows as `hold (suppressed)`, and a backward step a forward-only role would refuse is annotated `(skipped: forward-only)`), step counters, suppression countdown, and a per-sender evidence table (`prefix hops count skew E`, `E` = tenure-eligible). Sensing runs even while off, so this works as a dry-run before enabling. Over remote admin the reply is truncated to the packet size (summary always fits); the full table needs the USB CLI. | +| `get meshtimesync` | Mesh time-sync state + live dry-run: on/off, eligible voter count, votes for/against, consensus skew and radius, would-be verdict (`ok`/`in-band`/`step±N`/`abstain (reason)`/`hold (reason)`; a recent clock set — manual or GPS — shows as `hold (suppressed)`, and a backward step a forward-only role would refuse is annotated `(skipped: forward-only)`), step counters, suppression countdown, and a per-sender evidence table (`prefix hops count skew E`, `E` = counted toward the verdict above). Entries that count print first, so a size-capped reply never hides the ones that explain the summary; if the table doesn't fully fit, a trailing `+N more` shows how many were left out. Sensing runs even while off, so this works as a dry-run before enabling. Over remote admin the reply is truncated to the packet size (summary always fits); the full table needs the USB CLI. | | `get dc.restarts` | Duty-cycle preamble false-positive re-arm counter (RxTimeout re-arms + parked-RX watchdog recoveries). High values mean the preamble detector is tripping on noise/interference without real packets arriving — inflates RX-on time and drains battery; packets are never lost to it. Reset by `clear stats`. | | `get adc.multiplier` | Battery voltage ADC calibration multiplier | | `get bootloader.ver` | Bootloader version string | diff --git a/zephcore/helpers/MeshTimeSync.cpp b/zephcore/helpers/MeshTimeSync.cpp index a835d1e..64554cd 100644 --- a/zephcore/helpers/MeshTimeSync.cpp +++ b/zephcore/helpers/MeshTimeSync.cpp @@ -55,10 +55,10 @@ bool MeshTimeSync::slotTenured(const Slot &s, uint32_t uptime_secs) const s.count >= TENURE_MIN_ADVERTS; } -bool MeshTimeSync::slotEligible(const Slot &s, uint32_t uptime_secs) const +bool MeshTimeSync::slotEligible(const Slot &s, uint32_t uptime_secs, bool bootstrap) const { - return s.used && slotTenured(s, uptime_secs) && - (uptime_secs - s.arrival_uptime) <= MAX_SAMPLE_AGE_SECS; + return s.used && (uptime_secs - s.arrival_uptime) <= MAX_SAMPLE_AGE_SECS && + (bootstrap || slotTenured(s, uptime_secs)); } int64_t MeshTimeSync::slotSkew(const Slot &s, uint32_t local_time, uint32_t uptime_secs) const @@ -159,9 +159,7 @@ MeshTimeSync::Consensus MeshTimeSync::computeConsensus(uint32_t local_time, int n = 0, m = 0; for (int i = 0; i < MESHTIMESYNC_TABLE_SIZE; i++) { const Slot &s = _slots[i]; - if (!s.used) continue; - if ((uptime_secs - s.arrival_uptime) > MAX_SAMPLE_AGE_SECS) continue; - if (!bootstrap && !slotTenured(s, uptime_secs)) continue; + if (!slotEligible(s, uptime_secs, bootstrap)) continue; int64_t skew = slotSkew(s, local_time, uptime_secs); int32_t r = RADIUS_BASE_SECS + RADIUS_PER_HOP_SECS * s.hops; val[m] = skew - r; typ[m] = 1; m++; @@ -475,22 +473,52 @@ int MeshTimeSync::formatStatus(char *out, size_t cap, uint32_t local_time, (unsigned long)_evals, (unsigned long)_abstains); if (pos < 0 || (size_t)pos >= cap) goto full; - /* Evidence table: one entry per used slot, as many as fit. */ - for (int i = 0; i < MESHTIMESYNC_TABLE_SIZE; i++) { - const Slot &s = _slots[i]; - if (!s.used) continue; - int w = snprintf(out + pos, cap - pos, "\r\n %02x%02x h%u n%u %+lds%s", - s.prefix[0], s.prefix[1], (unsigned)s.hops, - (unsigned)(s.count > 99 ? 99 : s.count), - clampl(slotSkew(s, local_time, uptime_secs)), - slotEligible(s, uptime_secs) ? " E" : ""); - if (w < 0 || (size_t)(pos + w) >= cap) { - out[pos] = 0; /* drop the partial entry */ - return pos; + { + /* Evidence table: entries that actually count toward this verdict + * print first, then the rest — on a size-capped reply (161 B over + * remote admin), the buffer must not run out on still-building-tenure + * entries while hiding the ones that explain the verdict. + * (Braced so these locals go out of scope before the `full` label + * below — a goto may not jump over a variable's initialization.) */ + int total_used = 0; + for (int i = 0; i < MESHTIMESYNC_TABLE_SIZE; i++) { + if (_slots[i].used) total_used++; } - pos += w; + + int printed = 0; + for (int pass = 0; pass < 2 && printed < total_used; pass++) { + for (int i = 0; i < MESHTIMESYNC_TABLE_SIZE; i++) { + const Slot &s = _slots[i]; + if (!s.used) continue; + bool participates = slotEligible(s, uptime_secs, v.bootstrap); + if (participates != (pass == 0)) continue; + + int w = snprintf(out + pos, cap - pos, "\r\n %02x%02x h%u n%u %+lds%s", + s.prefix[0], s.prefix[1], (unsigned)s.hops, + (unsigned)(s.count > 99 ? 99 : s.count), + clampl(slotSkew(s, local_time, uptime_secs)), + participates ? " E" : ""); + if (w < 0 || (size_t)(pos + w) >= cap) { + out[pos] = 0; /* drop the partial entry */ + goto truncated; + } + pos += w; + printed++; + } + } + return pos; + + truncated: + if (printed < total_used) { + char tail[24]; + int tw = snprintf(tail, sizeof(tail), "\r\n +%d more", total_used - printed); + if (tw > 0 && (size_t)tw < cap - (size_t)pos) { + memcpy(out + pos, tail, (size_t)tw + 1); + pos += tw; + } + } + return pos; } - return pos; full: out[cap - 1] = 0; diff --git a/zephcore/helpers/MeshTimeSync.h b/zephcore/helpers/MeshTimeSync.h index d689d17..29df9a7 100644 --- a/zephcore/helpers/MeshTimeSync.h +++ b/zephcore/helpers/MeshTimeSync.h @@ -160,7 +160,12 @@ private: Slot *findSlot(const uint8_t *prefix); bool slotTenured(const Slot &s, uint32_t uptime_secs) const; - bool slotEligible(const Slot &s, uint32_t uptime_secs) const; + /* Whether this slot counts toward the current evaluation — bootstrap + * relaxes tenure, so this takes the mode explicitly rather than + * hardcoding normal-mode rules. Single source of truth shared by + * computeConsensus (who votes) and formatStatus (who gets the "E" + * marker) — they must never diverge on what "counted" means. */ + bool slotEligible(const Slot &s, uint32_t uptime_secs, bool bootstrap) const; int64_t slotSkew(const Slot &s, uint32_t local_time, uint32_t uptime_secs) const; Consensus computeConsensus(uint32_t local_time, uint32_t uptime_secs, diff --git a/zephcore/helpers/ui-button/ui_pages.c b/zephcore/helpers/ui-button/ui_pages.c index e2af79d..8c60d4a 100644 --- a/zephcore/helpers/ui-button/ui_pages.c +++ b/zephcore/helpers/ui-button/ui_pages.c @@ -527,6 +527,12 @@ static void render_messages(void) draw_centered_color(y, buf, state.offgrid_enabled ? UI_COLOR_ACTIVE : UI_COLOR_DISABLED); + y += LINE_H; + + uint32_t up_s = (uint32_t)(k_uptime_get() / 1000); + snprintf(buf, sizeof(buf), "Up: %ud %uh %um", + up_s / 86400, (up_s % 86400) / 3600, (up_s % 3600) / 60); + draw_centered_color(y, buf, UI_COLOR_LABEL); return; } @@ -559,21 +565,32 @@ static void render_messages(void) draw_centered_color(y, buf, state.offgrid_enabled ? UI_COLOR_ACTIVE : UI_COLOR_DISABLED); + y += LINE_H; + + uint32_t up_s = (uint32_t)(k_uptime_get() / 1000); + snprintf(buf, sizeof(buf), "Up: %ud %uh %um", + up_s / 86400, (up_s % 86400) / 3600, (up_s % 3600) / 60); + draw_centered_color(y, buf, UI_COLOR_LABEL); return; } snprintf(buf, sizeof(buf), "MSG: %u", state.msg_count); - draw_centered(centered_row(0, 3), buf); + draw_centered(centered_row(0, 4), buf); if (state.ble_connected) { - draw_centered(centered_row(1, 3), "< Connected >"); + draw_centered(centered_row(1, 4), "< Connected >"); } else { - draw_centered(centered_row(1, 3), "Waiting for app..."); + draw_centered(centered_row(1, 4), "Waiting for app..."); } snprintf(buf, sizeof(buf), "Offgrid: %s", state.offgrid_enabled ? "ON" : "OFF"); - draw_centered(centered_row(2, 3), buf); + draw_centered(centered_row(2, 4), buf); + + uint32_t up_s = (uint32_t)(k_uptime_get() / 1000); + snprintf(buf, sizeof(buf), "Up: %ud %uh %um", + up_s / 86400, (up_s % 86400) / 3600, (up_s % 3600) / 60); + draw_centered(centered_row(3, 4), buf); } static void render_recent(void)