Log 6 closed with a promise: the one control in the whole UI that transmits anything at all — the NMEA2K screen's GNSS source node, and how a heartbeat exception gets built so it can never quietly become a general-purpose one. This is that log.
Why anything on this screen would transmit at all
Everything else in Scottina listens. The CAN screen sniffs raw arbitration
IDs, the NMEA2K screen decodes known PGNs against installed tables, and
both enforce RX-only in code, not by promise. But an NMEA2000 bus doesn't
let a silent node sit on it indefinitely — a device that never answers an
ISO address claim or an ISO request looks broken to everything else on the
bus, and there's a real bench case for wanting Scottina to be a GPS
source: proving a freshly converted PGN table against real bus traffic
without dragging an actual chartplotter to the workbench. n2k/node.py
exists for exactly that case, and nowhere else. It claims a source address,
defends it, answers ISO requests, and broadcasts five GNSS PGNs —
126992, 129025, 129026, 129029, 126993 — sourced from the same gpsd
snapshot the GPS tile already reads. Nothing about the rest of the CAN
stack changes; one module gets permission to speak.
A state machine that can say "no"
GnssSourceNode isn't a flag, it's five states: OFF, CLAIMING,
ACTIVE, CANNOT_CLAIM, STOPPED_FIX. The NMEA2K screen's one TX
affordance — a single button, deliberately living on the bus screen and
not the GPS tile, because sourcing PGNs is a bus action — reads its label
straight off that state: Source GNSS → bus when off, GNSS: claiming…
mid-handshake, GNSS ■ stop once active, GNSS: no address if the claim
loses out to another device already holding that source address, GNSS: fix lost ■ if the bus keeps the node parked after the fix that justified
it disappears. Every one of those is a real, distinct outcome shown in
plain words — there's no state where the button just goes quiet and leaves
the bench guessing whether Scottina is talking or not.
The node doesn't get to guess it has a fix
_gnss_eligible() gates the button on two facts, not one: /dev/gps0
has to exist, and gps.snapshot.read_position() has to return a current
fix — the same staleness rule GPS.md already applies everywhere else a
tile reads position. No fix, no button; the toast says so plainly, "No GPS
fix — a node never sources stale data." That check doesn't stop mattering
once the node is running: losing the fix mid-session drops the state to
STOPPED_FIX rather than letting the node keep broadcasting a last-known
position as if it were current. A GNSS source that quietly goes stale is
worse than one that visibly stops.
Telling your own echo from the boat's GPS
Once the node is active, its own broadcasts land right back in the NMEA2K screen's decode view like any other bus traffic — which is useful, since watching your own PGNs arrive is how you confirm the TX actually worked. But that echo has to be told apart from a real GPS already on the bus, so every row sourced from the node's own claimed address gets tagged with a ▸ marker, and the screen's GPS-vs-bus comparison explicitly excludes self-traffic. Verifying your own transmission and trusting a second, independent GNSS source are two different questions, and the UI keeps them from blurring into one.
The part that keeps this from creeping
None of the above matters if the exception leaks. So it's checked the
same way the LAN Scan screen's blocked nmap flags are checked: in code, at
build time, not by anyone remembering a rule. tests/test_txscan.py
AST-scans the entire tree against a positive allow-list of exactly one
TX-permitted module — n2k/node.py. Any send-shaped call on a socket
turning up anywhere else fails the build outright. That's one half of the
gate; tests/test_busmon.py and tests/test_n2k.py are the independent
reject pass, proving the CAN and NMEA2K screens and their bus models stay
RX-only on their own terms, without relying on the allow-list to catch
what they might do wrong. A heartbeat exception granted to one named
module, checked from both directions, is not the same thing as a
diagnostics tool that happens to transmit today and might transmit more
tomorrow.
Next log: the other half of that same discipline, working in the opposite direction — the LAN Scan screen, where six specific nmap flags are the ones that must never be reachable, and how a reject-list earns the same build-time trust as the GNSS allow-list did here.
Scottsky
Discussions
Become a Hackaday.io Member
Create an account to leave a comment. Already have an account? Log In.