Repository navigation
fix(nodes): one "on the bus" rule — pages count as online, quiet unplugged, short replies ignored - #151
Merged
Conversation
…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
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
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.canHide/canForgettreated any fresh pages as live, separately from the availability, so the panel could read OFFLINE beside the "Online — can't be removed" footer.offlineSignalscomputes it withavailabilityOfover the same signals; the footer reads the model's.OfflineSignals.pduTelemetryandoffline-actions.ts pduTelemetryLiveare gone.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.nodesCardwhile down) and About's registry rows (registryOnBus:—while down; a hidden row still readshidden). They all go quiet while unplugged and come back when the link does.telemetry: 12 V …ortelemetry: board status.X2 variants deleted (the co-located answers from the rewrite):
cockpit-snapshot/index.ts: thelive ? strays : []gate is nowstrayNew(..., 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, andnewDevices' ownif (!live)is folded intoisReportableNew.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.sendingandavailabilityboth readnode-telem.ts pduPagesLive, whichoffline-actions.ts pduTelemetryLivereads 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, withnewestPduPageAtnext 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):NodeModelInputgainsnow(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 emitAllNodesat the pinned commita78dd90and on OpenSkyhawkmain:emitSelf(), then one row per alive node, then_NODE_STATUS_END <count>;countis decimal and starts at 1, so it includes the node-0 row:1means bridge only;present = 1;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_STATUSline 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.
NodeRostercompares the END count with the distinct NODE_IDs the reply brought.unknown, notoffline), and the next whole reply decides.roster reply incomplete: 2 of 3 rows; absences not reconciled), with no event row.No firmware change.
Changed assertions (each on purpose)
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)onBusNow(live)unplugged: … CAN nodes still read "yes" (R5 today)→… no CAN row says it is on the bus (R5, #132)#1 … yeswhile unplugged—(hidden stayshidden), back toyeson reconnectopened while unplugged: the same#1 … yes#1 … —registry-rows.testlists each registry row … before and after the model(built with link down)yes/offline—/—offline-actions.testallowed only offline with no fresh PDU pages→allowed only offlinegate({ availability: 'offline' | 'unknown', pduTelemetry: true })refused{ availability }; that loop is removedavailabilityOf), so a separate "pages win" signal would contradict it. The footer and main agreement is still pinned bynode-hub.test"one rule for the footer and main", unchanged.offline-actions.testPDU pages count only while fresh, and only for that NODE_IDpduTelemetryLivenode-telem.test"pduPagesLive: the newest evidence wins"phase0-probesM3: a v2 bridge — END completes the roster, added boards read offline, Forget allowed→M3 / #132 (fixed): a reply whose rows were dropped completes nothing2with its only (malformed, 22-hex) row dropped completed the roster: boardoffline, Forget allowedunknown, Forget refusedEvery other assertion is unchanged. Some call sites changed without changing what they assert:
isReportableNew,strayNew,newCountandnodesSummarygained the explicitliveargument (truein the old cases),boardActionslost itspduTelemetryargument, andbuildNodeModelinputs gainednow. 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:
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)node-model.test(a pages-only registry PDU is online, reportable and an Adopt row withkind: pduand telemetry evidence; every new board in the model is online)cockpit-snapshot/on-the-bus.test.ts(CTA, strays incl. band, Overview notice, Connected nodes, About), through the snapshot builder and throughCockpitSnapshotHostunplug → replug with no new model; plus T9 / T32 aboveonBusNowignoreliveand remove the two!liveguardsnode-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 logsoffline)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). Alsonode-model.test(absent before vs after the newest page) andnode-telem.test(pduPagesLive/newestPduPageAt)absentAtinpduPagesLiveNodeHub.offlineSignalsdrop the pagesChecks
npm test: 1394 passed (baseline 1372, +22)npm run lint: 0 / 0npm run lint:deps: cleannpm run typecheck(3 projects): cleanThe renderer only displays: no renderer source changed.
Part of #132