timesync cosmetics and uptime back to UI

This commit is contained in:
liquidraver
2026-07-04 23:12:41 +02:00
parent 000f6ed494
commit f58f7de8f5
4 changed files with 76 additions and 26 deletions
+1 -1
View File
@@ -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 |
+48 -20
View File
@@ -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;
+6 -1
View File
@@ -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,
+21 -4
View File
@@ -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)