Repository navigation
A broken sensor wire reads as clear track, not occupied (CS fix under test; IR open) #9
Description
The sensors are switches to ground: closed when the block is occupied, open otherwise, with the MCP23017's internal pull-up holding the line high when open. So a broken wire is indistinguishable from clear track.
| Condition | Pin | Node publishes | Orchestrator |
|---|---|---|---|
| Occupied | 0 | occupied |
keeps trains out |
| Clear | 1 | clear |
routes trains in |
| Wire breaks / sensor dies | 1 | clear |
routes trains in |
The pull-up gives a defined level on an open circuit, which is good for noise immunity. But that level is the permissive state, so the failure lands on "this track is empty, come ahead" rather than on the restrictive answer.
Nothing downstream catches it
The 30 s re-assert does not help. The node is alive and republishing clear every 25 s, so the orchestrator has every reason to trust it — SENSOR_FRESHNESS_TIMEOUT_MS catches a dead node, not a dead wire. A block_detection sensor publishing clear is enough on its own to let the orchestrator clear the block (domain/occupancy.ts, clause 2).
Why it is open rather than fixed
This is the closed-circuit principle that vital signalling has used since relays: arrange things so the safe state is the one requiring current to flow, and any loss of continuity drops into the restrictive state. Getting there needs one of:
- A normally-closed output — circuit closed when the block is clear, opening when occupied. Broken wire then reads occupied. Not available on the current sensors (checked 2026-08-23); they are electronic switches, not relay contacts.
- End-of-line resistor read as analogue. A resistor at the sensor end gives three distinguishable values — shorted (occupied), nominal (clear), open (fault). This repo already has an unused
PCF8591ADC driver insrc/lib/, and it is exactly what alarm systems do to supervise a loop. Needs the sensor specs checked first: passing the pull-up through an electronic switch may not be within its ratings, and damaging the sensors to gain supervision would be a poor trade. - Accept it, knowingly.
Option 3 is the current decision, taken deliberately on 2026-08-23 rather than by omission. config.py records it at ACTIVE_LOW.
If this is revisited
Option 2 is the realistic path, and the blocking question is electrical, not software: what the sensor's output stage tolerates. Worth measuring before designing anything.
It is also arguably an orchestrator question — "may a clear be trusted from a device that cannot prove its sensing path is intact" is the same family as bazauto/layout-orchestration#28, which already deals with trusting a reading from a device whose liveness is uncertain. A per-sensor "supervised / unsupervised" attribute there would let the backend treat an unsupervised clear with less confidence than a supervised one.
Scope
Affects every block-detection sensor on the layout, not just the two installed. Worth settling before scaling past the current pair.
Activity
Debounce MCP23017 inputs properly, and correct the sensor polarity (#11)
bazauto commentedon Aug 24, 2026
Reopening: this was closed against a premise that turns out to be false
The closed-circuit option was rejected here on this basis:
And the issue closed noting that the blocking question was electrical:
That measurement has now been made. Full write-up in
docs/block-detector-wiring.md(#12); the part that matters here follows.
Output B has two modes, and Input A selects them
The detectors are Legacy Models LM-iD.1. Measured 2026-08-24 with a meter on output
Breferenced to Pico 0 V, disconnected from the expander — a pin read cannot answer this,
because a floating MCP23017 input and a driven-high one both read 1.
With
Aunpowered there is no internal rail, soBis the bare output transistor — which isthe "electronic switch, not a relay contact" this issue described, and it is accurate for the
layout as wired. With
Apowered the same pin idles at a driven 5 V.Which makes the three states separable after all
The point is not that a relay appeared. It is that
clearstops being an absence andbecomes an assertion:
A 100 kOhm pull-down at the node end resolves the float into the restrictive answer, and
an inverting stage puts it the right way round:
74LVC14A because its inputs are 5 V tolerant at
VCC= 3.3 V, so it takes the detectoroutput directly with no level shifter in the path, and its Schmitt hysteresis is worth having
on layout-length runs beside a DCC bus. Six channels per 14-pin package, so three packages
cover all sixteen inputs.
This is option 1 from the original list, reached by a different route. It needs no change to
the detectors and no ADC, so option 2 (end-of-line resistor on the PCF8591) is now the more
expensive path, not the realistic one.
The general statement
Powering
Ais what turnsclearinto evidence. WithAunpowered, clear andbroken-wire are electrically the same state — no pull-up value, no inversion, no firmware
change can separate them, because the information is not on the wire.
That is the same principle as the orchestrator's sensor-trust rules one layer down: a
retained reading is not evidence, an empty payload asserts nothing, and neither does an open
circuit. Worth stating plainly because it means the choice here was never "how do we detect a
broken wire in software" — it was always "do we make the detector assert its clear".
Cost of doing it
Aon each detector — the manufacturer's intended wiring, off the DCC bus or aregulated 12 V supply.
ACTIVE_LOWflips toFalse, because the inverter moves the polarity. One config line,already covered by
test_sensors_are_active_lowwhich would need its expectation updated.which is the check this whole issue is about and is currently untestable.
Not doing it yet
Deliberately.
Astays unpowered while the layout is being brought up; there is plenty elseto validate and this costs money and bench time. The limitation as originally described
stands and is unchanged — a broken wire still reads as clear track today.
What has changed is that it is no longer accepted as unavoidable. Reopening so it is
tracked as a decision with a known price rather than a closed door.
Aputs 5 V onto a 3.3 VMCP23017 input (absolute maximum
VDD + 0.6 V= 3.9 V). The level shifting is not optionalif
Ais powered, and every manufacturer datasheet tells you to wireAto the DCC bus.Document the LM-iD output stage, and correct the polarity comments
bazauto commentedon Aug 24, 2026
Scoping the comment above: it was board 1 only
The two-mode output stage and the 5 V hazard are the LM-iD.1 current-sensing detectors.
Board 2 is a different device — Waveshare IR reflective modules, LM393, run at 3.3 V — and
none of that applies to them. Details in
docs/block-detector-wiring.md(#13).This issue still covers both. A severed wire floats to the permissive reading either way.
What differs is what it costs:
block_detectionclearir_positionThe IR row is the one to keep in view as these get used as position stops for load/unload
alignment. A dead beam does not stop the train short; it fails to stop it.
Supervising board 2 is the cheaper half of this issue. Because an LM393's output is
open-collector with an on-board pull-up to the module's own 3.3 V VCC, its high level is
already an assertion rather than an absence — the thing board 1 only gets by powering Input
A. So the fix is one resistor per channel: a pull-down at the node end instead of the
expander's internal pull-up separates connected and not triggered from broken. No level
shifter, no inverting stage, no change to the sensors.
If this issue is ever picked up in halves, board 2 is the half with the better ratio.
Nothing fitted, nothing planned yet — recording it so the two are not conflated next time.
bazauto commentedon Sep 23, 2026
Update 2026-09: the current-sensing half has a fix in hand
The plan for board 1 has changed since the comments above. Instead of powering the LM-iD.1's Input A and adding a 74LVC14A inverter, the LM-iD.1 is being replaced by our own detector,
bazauto/block-detection. It's designed and built, and rev 1.0 is under bench test now.Why it fixes the named fault
The new board has a push-pull, active-high 3.3 V output: occupied drives 3.3 V, clear drives 0 V. The MCP23017 pull-ups stay enabled, so a severed signal wire floats high, and that now reads occupied:
occupiedclearoccupiedThis is the closed-circuit principle from the original issue: clear is now something the detector asserts, not an absence of signal. It gets there more simply than the Input A route: no 5 V on the wire, no level shifting, no extra stage.
Firmware: ready (#23)
Polarity is now per sensor. Every
SENSORSentry has its ownactive_low, set from a per-device constant (LM_ID_ACTIVE_LOW,WAVESHARE_IR_ACTIVE_LOW,BAZAUTO_BD_ACTIVE_LOW = False). Startup refuses an entry without one. Nothing on the layout has changed: every sensor is still on its current device. Moving acs---sensor to the new board is a one-line config change.Still open before this half can close
This is a design argument, not a measurement. The current-sensing half closes on a recorded bench result, per channel:
occupied.occupied. This is the unproven one. The board is powered through its output header, so this wire runs in the same cable as the signal. With the supply gone, the unpowered push-pull output is a diode path from the expander's pull-up into the board's rail, and that could pull the line low, which readsclear. If it does, the fix is hardware: a separate supply feed, or an output stage that fails high.occupied. Expected, but unmeasured.BAZAUTO_BD_ACTIVE_LOW, deployed, and seen following a loco.The IR half is not touched by this
Board 2's Waveshare IR modules still fail toward overrun: a broken wire reads beam not reached. The cheap fix recorded above still stands: a pull-down at the node end with the expander's pull-up disabled for those inputs. This issue stays open for that half.
Out of scope, recorded so it isn't mistaken for covered
Current sensing can't see a stalled train that draws no current. So a broken CT lead, a dead track feeder, a threshold set too high, or a comparator stuck low all still read
clear, and a train sitting on a dead feeder is invisible. The orchestrator-side "supervised / unsupervised" idea in the original issue is still the only answer to that class of fault.[-]A broken sensor wire reads as clear track, not occupied[/-][+]A broken sensor wire reads as clear track, not occupied (CS fix under test; IR open)[/+]Make sensor polarity per-sensor, ready for the active-high detector (#9…
Switch board 1 to the active-high bazauto detector (#9) (#27)