Repository navigation
Read point position from board 3, and answer the backend's queries - #21
Merged
Merged
Conversation
Board 3 is fitted and answers at 0x22, so the node can now read the Cobalt iP
motors' S2 changeover contacts and publish `point/{pointId}/reading`.
`layout-orchestration#167` — the blocker the design note called out — closed on
2026-08-24. It settled as the first of its two candidate shapes: republish on a
timer, the same thing this repo already does for sensors, rather than answer a
periodic query. So points re-assert on the same 25 s tick as the sensors, through
the same `LayoutMQTT.tick()`. One rule, one loop.
Two inputs per point, never one. A single input infers `reverse` from the absence
of `normal`, and an absence is equally a broken wire, a lost supply, or a point
sitting mid-throw. Both-open is a real reading and is published: a break-before-make
changeover passes through it on every throw, so the honest sequence is
`normal` -> `unknown` -> `reverse`. Both-closed is impossible on an SPDT, so it
reads `unknown` and is reported as cross-wiring rather than guessed at.
Subscribing is new for this repo — the node only published before. A query is
noted, never answered from the callback: `poll()` dispatches the handler, so
publishing from there would re-enter the modem mid-command. `tick()` answers on
the next pass, which is safe to be late because a query does not arm the
confirmation deadline. A failed subscribe refuses to start, because unlike a
failed publish it is not survivable — the node would look healthy and never answer.
`POINTS_INSTALLED` is empty: no S2 contacts are landed yet, and an unwired pair
reads both-open, which would publish a confident `unknown` for a point nothing is
watching.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Board 3 is built and answers at
0x22, so the node can now read the Cobalt iP motors'S2changeover contacts and publishpoint/{pointId}/reading.Closes #15.
The blocker cleared first
The design note said this should not be built until
layout-orchestration#167settled,because the two candidate answers were different firmware. #167 closed on 2026-08-24,
and it settled as the first of them — republish on a timer, which is the shape this repo
already runs for sensors, rather than answer a periodic query. The contract now carries
the same 30 s re-assert on
point/*/readingthat sensors have, withPOINT_FRESHNESS_TIMEOUT_MSat 90 s.So points re-assert on the same 25 s tick as sensors, through the same
LayoutMQTT.tick().One rule, one loop — the re-assert is the easiest thing in this repo to forget and the most
expensive to get wrong, and it should not be possible to implement it for one topic and
miss it for the other.
Two inputs per point, never one
A single input would infer
reversefrom the absence ofnormal, and an absence is equallya broken wire, a lost supply, or a point sitting mid-throw.
normalreversenormalreverseunknownunknownBoth-open is a real reading and is published, not suppressed: a break-before-make
changeover passes through it on every throw, so the honest sequence is
normal→unknown→reverse. The backend expects it and its confirmation timeout is8000 ms, so a real throw lands well inside.
POINT_DEBOUNCE_MSis its own knob for exactlythis reason — tuning the sensors' debounce must not be able to swallow that transient.
Both-closed cannot happen on an SPDT, so it reads
unknown— the safe answer, not a guessbetween the two — and prints a cross-wiring warning rather than being averaged away.
Subscribing, which is new here
The node only published before. Two decisions worth knowing:
A query is noted, never answered from the callback.
poll()dispatches the handler, sopublishing from inside it would re-enter the modem in the middle of whatever command
poll()was called from.note_query()sets a flag andtick()answers on the next pass,a few tens of milliseconds later. Safe to be late, because a
querydeliberately does notarm the backend's confirmation deadline the way a
commanddoes.A failed subscribe refuses to start. Unlike a failed publish it is not survivable: the
node would come up healthy, re-assert happily, and never answer a query — leaving every
queried point at
unknown, every edge through it untraversable, and nothing anywherereporting a fault. It raises, the supervisor retries, and the LED flashes code 5.
A query for a point this node does not report is ignored rather than answered, and so is one
arriving before the first read. Replying about a point we cannot see is exactly the
fabricated reading the installed allow-list exists to prevent.
Nothing is published yet
POINTS_INSTALLEDis empty. The expander answers on the bus but noS2contacts are landed,and an unwired pair reads both-open — which would publish a confident
unknownfor a pointnothing is watching. Same allocation is not installation rule as the sensors.
This is also why landing it is safe: the backend only queries points configured
required,and all six are
none, so nothing changes on the layout until the first point is commissioned.Bring them up one at a time. Every point fault kind Safe-Stops the whole layout, including
a fault on a point no route holds. The commissioning check that catches the mapping hazard is
to throw each point individually and confirm the expected pair — and only that pair — moves.
Throwing them together proves nothing, and neither does testing one while the others sit in
matching positions.
Which throw is
normalis unverifiable authored data — nothing in the wiring reveals it, sothe
normal/reversecolumns are the allocation's intent and commissioning confirms orcorrects them.
Verification
Host suite:
49 of those are new: the decode table including both failure rows, the allocation checks, the
payload against the backend's
.strict()schema, retention, the re-assert, and the query path./mqtt-checkrun against the diff —point/*/readingat QoS 1 and retain 0, payloadcarrying exactly
pointId/position/source,sourceneverdriver, subscribe returnvalue checked, no logic in the message callback.
On the bench, after deploying:
What is not verified, and cannot be until the contacts are landed: that a real throw
produces
normal→unknown→reverseon the wire, and that each point's pair is the onethe allocation says it is. Both are commissioning checks on hardware, not host tests.
Docs
docs/pin-allocation.mdgains board 3 and records that no points are installed.docs/point-position-feedback.mdloses its Not built marker, its stale "blockingquestion" section is rewritten as settled, and it gains a map of where each piece landed.
CLAUDE.mdpicks uppoint_wiring.py, the board 3 status, and three new traps — thecommissioning check, the unverifiable
normal, and not debouncing the transient away.