Skip to content

fix(nodes): one "on the bus" rule — pages count as online, quiet unplugged, short replies ignored - #151

Merged
mottihoresh merged 2 commits into
fix/chunk-3from
fix/c3-on-the-bus
Oct 17, 2026
Merged

mottihoresh merged 2 commits into
fix/chunk-3from
fix/c3-on-the-bus

Conversation

@mottihoresh

@mottihoresh mottihoresh commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

The last PR of fix-track chunk 3. Two behaviours change on purpose (decided 2026-10-16); nothing else does.

1. X2 / R5 / S10 — one "on the bus" rule

Before After
Fresh PDU telemetry Made a board visible but not online. A registry PDU seen by its pages alone (no roster row, or a complete roster that left it out) was a "new but offline" board: not counted, not news, never adoptable (S10). Fresh PDU pages are evidence in the one availability rule (node-model.ts nodeAvailability → { availability, evidence: 'roster' | 'telemetry' | 'none' }). Order: roster present → online (roster); fresh PDU pages newer than the roster's last "absent" reading → online (telemetry); roster absent / complete roster without it → offline (roster); else unknown (none). availabilityOf (node-model.ts) is the one place that weighs them.
Hide / Forget canHide / canForget treated any fresh pages as live, separately from the availability, so the panel could read OFFLINE beside the "Online — can't be removed" footer. The gate reads only the one availability. NodeHub's offlineSignals computes it with availabilityOf over the same signals; the footer reads the model's. OfflineSignals.pduTelemetry and offline-actions.ts pduTelemetryLive are gone.
Every "on the bus" surface While the Gateway was unplugged, the Cockpit's "New on the bus · N / Adopt…", Settings › About's On the bus column ("yes" / "offline") and Connected nodes all read the stale roster as live. One onBusNow(entry, live) (node-model.ts) = link up and online. isReportableNew(e, live) is built on it. Every surface reads it: the Cockpit CTA (newCount), the strays (strayNew), the Overview's new-devices notice (newDevices), Connected nodes (nodesSummary: COCKPIT_IDLE.nodesCard while down) and About's registry rows (registryOnBus: — while down; a hidden row still reads hidden). They all go quiet while unplugged and come back when the link does.
Adopt evidence An online board with no status row read "not reporting right now". A PDU online by its pages reads telemetry: 12 V … or telemetry: board status.

X2 variants deleted (the co-located answers from the rewrite):

  • cockpit-snapshot/index.ts: the live ? strays : [] gate is now strayNew(..., live). The X2 rows are gone from both variant lists in the header.
  • attention.ts: "New on the bus counts whatever the last model said" is gone. newCount(model, live) replaces it.
  • summaries.ts: "Connected nodes reads the last model whatever the link" is gone. nodesSummary(..., live) replaces it, and newDevices' own if (!live) is folded into isReportableNew.
  • registry-rows.ts: "a CAN row reads the last model whatever the link" is gone.
  • node-model.ts: the two "visible vs online" copies of the telemetry rule are gone. sending and availability both read node-telem.ts pduPagesLive, which offline-actions.ts pduTelemetryLive reads too.

Newest evidence wins (follow-up commit, from review)

The first commit let fresh pages win whatever the roster said. A PDU that lost power then read online until its last page went stale (10 s) instead of when the roster's heartbeat timeout reported it absent (3 s), so its outage formed about 7 s late.

Now node-telem.ts pduPagesLive(t, now, absentAt), still the one freshness helper, with newestPduPageAt next to it, counts pages only while the newest one arrived after the roster's last "absent" reading for that board (seen.offlineAt, while its row is absent):

  • The roster says absent after the last page: the PDU is offline at once and folds into its outage on the roster's timing.
  • Pages arrive after that "absent": the PDU is online again.
  • No absent reading at all: pages count. That covers no row, or a complete roster that left it out, which is the S10 case.
  • A tie goes to the roster.

NodeModelInput gains now (when the telemetry snapshot was taken) so page ages can be compared with the roster's times.

2. A lost line inside a reply

  • END count semantics (confirmed in the firmware). PanelBridge.cpp emitAllNodes at the pinned commit a78dd90 and on OpenSkyhawk main:

    • it sends emitSelf(), then one row per alive node, then _NODE_STATUS_END <count>;
    • count is decimal and starts at 1, so it includes the node-0 row: 1 means bridge only;
    • every row in a reply has present = 1;
    • the bridge writes the burst synchronously, so no delta can interleave with it.

    One caveat: the A-4 worktree named in the brief is on feat/pdu-telemetry-216, which still has the pre-#344 v2 version (no node-0 row, count from 0). The client is pinned to the v3 one.

  • Before. One _NODE_STATUS line lost inside a reply made that board offline for one poll. That is a false offline row, an extra save and possibly a false PDU outage, all against the #290 "no false offline" gate.

  • After. NodeRoster compares the END count with the distinct NODE_IDs the reply brought.

    • A reply starts at its node-0 row. From there its rows are the reply: not the deltas before it, and not what an earlier reply with a lost END brought.
    • On a mismatch, absences are not reconciled. The roster does not count as complete (so remembered boards stay unknown, not offline), and the next whole reply decides.
    • NodeHub logs one debug line (roster reply incomplete: 2 of 3 rows; absences not reconciled), with no event row.
    • An END with no count completes as before: there is nothing to check it against, and no v3 bridge sends one.
  • No firmware change.

Changed assertions (each on purpose)

Test Was Now Why
T9 stays from the last model while the link is down (R5 today) → goes quiet while the link is down, and returns when it is back (R5, #132) CTA "New on the bus · 1" and Adopt… enabled with the link down CTA and Adopt… absent while down, back on reconnect Contract R5 / F12 (X2 final): onBusNow(live)
T32 unplugged: … CAN nodes still read "yes" (R5 today) → … no CAN row says it is on the bus (R5, #132) #1 … yes while unplugged Every CAN row — (hidden stays hidden), back to yes on reconnect Contract X7 / R5: on the bus means now
T32 opened while unplugged: the same #1 … yes #1 … — Same
registry-rows.test lists each registry row … before and after the model (built with link down) rows 1/2 yes / offline — / — The X2 variant's own unit pin; S10 / R5
offline-actions.test allowed only offline with no fresh PDU pages → allowed only offline gate({ availability: 'offline' | 'unknown', pduTelemetry: true }) refused The gate reads only { availability }; that loop is removed Newest-evidence rule: pages are weighed inside the availability (availabilityOf), so a separate "pages win" signal would contradict it. The footer and main agreement is still pinned by node-hub.test "one rule for the footer and main", unchanged.
offline-actions.test PDU pages count only while fresh, and only for that NODE_ID pduTelemetryLive Removed with the helper Replaced by node-telem.test "pduPagesLive: the newest evidence wins"
main phase0-probes M3: a v2 bridge — END completes the roster, added boards read offline, Forget allowed → M3 / #132 (fixed): a reply whose rows were dropped completes nothing END 2 with its only (malformed, 22-hex) row dropped completed the roster: board offline, Forget allowed Incomplete reply: board unknown, Forget refused The new count check. That row was never a real v2 row (v2 is 26 hex, already handled by M3)

Every other assertion is unchanged. Some call sites changed without changing what they assert: isReportableNew, strayNew, newCount and nodesSummary gained the explicit live argument (true in the old cases), boardActions lost its pduTelemetry argument, and buildNodeModel inputs gained now. The soak's seeded PRNG now also draws for line drops, so its random sequence shifted, but its assertions are the same.

New tests, each shown able to fail

Each was run against a temporarily broken source, then restored:

Test Broken source Result
Telemetry-only PDU counts online: node-model.test "the one availability rule" (rule, PDU in your cockpit roster-or-not, stale pages not); node-hub.test (online, and Hide / Forget refused) Drop the telemetry branch 5 fail
No unadoptable new-but-offline board: node-model.test (a pages-only registry PDU is online, reportable and an Adopt row with kind: pdu and telemetry evidence; every new board in the model is online) Drop the telemetry branch Covered by the 5 above
Every on-the-bus surface goes quiet with the link down and returns when it is up: cockpit-snapshot/on-the-bus.test.ts (CTA, strays incl. band, Overview notice, Connected nodes, About), through the snapshot builder and through CockpitSnapshotHost unplug → replug with no new model; plus T9 / T32 above Make onBusNow ignore live and remove the two !live guards 12 fail
Incomplete reply never marks a board offline; a whole reply still does: node-status-wire.test "a reply missing a line reconciles nothing" (lost row, lost node-0 row, lost END, a short first reply leaves the roster incomplete, deltas before node 0 not counted, END without a count); node-hub.test (one debug line, no event row, a later whole reply still logs offline) Disable the count check 5 fail (incl. the soak)
Same, the node-0 framing Disable the node-0 reset "deltas before node 0" fails
Newest evidence wins: node-hub.test "a PDU's pages against the roster". A PDU that loses power is offline at 13.2 s (absent at 13 s, last page at 10 s, still fresh) and folds into its outage (at: 13000), shown as one Attention item, with the footer and main both allowing Hide. Pages after the absent bring it back (outage gone; footer, Forget and Hide all refuse). Also node-model.test (absent before vs after the newest page) and node-telem.test (pduPagesLive / newestPduPageAt) Ignore absentAt in pduPagesLive 3 fail
Same: every surface and the gate agree Make NodeHub.offlineSignals drop the pages 4 fail (footer-vs-main matrix, M1/C3 probe, both new hub cases)
#150's 30-minute soak: one in five replies now loses a single random line (a board row, node 0 or END). It asserts no false offline, no extra saves, no node event rows and no "not responding" flash, and that over 50 lines were dropped Disable the count check Fails: a false offline

Checks

  • npm test: 1394 passed (baseline 1372, +22)
  • npm run lint: 0 / 0
  • npm run lint:deps: clean
  • npm run typecheck (3 projects): clean

The renderer only displays: no renderer source changed.

Part of #132

…ugged, short replies ignored

X2 / R5 / S10: fresh PDU pages are evidence in the one availability rule (roster / telemetry /
none), so a PDU seen by its pages alone is online and can be adopted; Hide and Forget read the
same answer. Every "on the bus" surface reads onBusNow(live): the Cockpit's New on the bus and
its strays, the Overview's new-devices notice, Connection's Connected nodes and About's On the
bus all go quiet while the link is down and return with it. The co-located X2 variants are gone.

A reply whose _NODE_STATUS_END count (PanelBridge's rows, node 0 included) does not match the
NODE_IDs that arrived reconciles nothing: boards it left out keep their state, the roster does
not count as complete, the next whole reply decides, and one debug line is logged.

Part of #132
…st absent reading

Fresh PDU pages were online whatever the roster said, so a PDU that lost power read online
until its last page went stale (10 s) and its outage formed ~7 s late. Now the newest evidence
wins (node-telem.ts pduPagesLive): pages count while the newest one arrived after the roster's
last "absent" reading (seen.offlineAt). A PDU listed absent after its last page is offline at
once and folds into its outage on the roster's timing; pages that arrive after it bring it back.

node-model.ts availabilityOf is the one place that weighs them. The model, NodeHub's Hide and
Forget refusal and the Cockpit footer all read that availability, so the gate no longer takes a
separate pduTelemetry signal (offline-actions.ts pduTelemetryLive is gone).

Part of #132
@mottihoresh
mottihoresh merged commit eae910a into fix/chunk-3 Oct 17, 2026
3 checks passed
@mottihoresh
mottihoresh deleted the fix/c3-on-the-bus branch October 17, 2026 17:18
mottihoresh added a commit that referenced this pull request Oct 17, 2026
…ng, one on-the-bus rule, reload keeps state (#152)

Fix-track chunk 3 (#132): link and roster lifecycle, HID, reload.
Behaviour changes on purpose; final answers decided 2026-10-16 (see
#135), plus two items added by the user on 2026-10-17.

| PR | Change |
|---|---|
| #150 | Roster polling: a new request never discards a reply in flight
(no false offline); re-ask every 1 s inside the 5 s deadline, so
"Gateway not responding" needs five lost replies in a row (a real outage
still alerts in the same time); callbacks from a stopped serial bridge
ignored (no orphan holding the port); Retry saves last seen; a v2
bridge's END no longer marks everything offline; the Connected-nodes
Refresh button removed (user 2026-10-17) |
| #149 | Calibrate badge follows the link rule; one NACK name table from
the firmware sync; a window reload keeps recording, replay, serial
monitor, aircraft, gauges, peak W and link-down time |
| #151 | One "on the bus" rule: fresh PDU telemetry counts as online
(newest evidence wins against the roster), every on-the-bus surface goes
quiet while the Gateway is unplugged; a reply whose END count doesn't
match its rows never marks a board offline |

N7 (HID controls follow a reflash) dropped: the hat/button list is the
client's synced copy, and a mismatched reflash isn't a realistic case.

Tests: 1339 → 1394.

Rig test 2026-10-17 (Gateway + one panel group, debug log on):
- Soak, 25 min: 303 roster replies, every one complete
(`_NODE_STATUS_END 2`), no interval over 6 s, Center_Armament present in
all 305 rows, TEC/REC 0 and no flags throughout, no re-asks, no
incomplete replies, no offline or not-responding rows
- Unplug: Gateway "no device", every CAN board "—" on the bus; replug:
back within seconds, last seen saved
- Disconnect/Connect ×5: five clean reopens, no "port busy"
- Reload while recording: still recording
- Calibrate badge with the link down; no Refresh button

BEGIN_COMMIT_OVERRIDE
fix(nodes): steady roster polling — no lost replies, no false
not-responding, no orphan bridge (#150)
fix(hid): calibrate badge follows the link, one NACK name table, reload
keeps the app's state (#149)
fix(nodes): one "on the bus" rule — telemetry counts as online, quiet
while unplugged, incomplete replies ignored (#151)
END_COMMIT_OVERRIDE

Closes #132
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant