Skip to content

A broken sensor wire reads as clear track, not occupied (CS fix under test; IR open) #9

Closed

Description

@bazauto

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:

  1. 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.
  2. 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 PCF8591 ADC driver in src/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.
  3. 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

  1. added a commit that references this issue on Aug 23, 2026
    2fefb4d
  2. bazauto commented on Aug 24, 2026

    @bazauto
    OwnerAuthor

    Reopening: this was closed against a premise that turns out to be false

    The closed-circuit option was rejected here on this basis:

    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.

    And the issue closed noting that the blocking question was electrical:

    the blocking question is electrical, not software: what the sensor's output stage
    tolerates. Worth measuring before designing anything.

    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 B
    referenced 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.

    Input A Clear Occupied
    Unpowered (as wired today) open circuit (floats) pulled to 0 V
    Powered driven to 5 V pulled to 0 V

    With A unpowered there is no internal rail, so B is the bare output transistor — which is
    the "electronic switch, not a relay contact" this issue described, and it is accurate for the
    layout as wired. With A powered 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 clear stops being an absence and
    becomes an assertion
    :

    State Line at the node
    Clear ~5 V
    Occupied 0 V
    Wire broken / detector dead floating

    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:

    LM-iD B ──┬── 74LVC14A input ──► MCP23017 input
              └── 100k ── 0V
    
    • Clear → ~5 V in → output low
    • Occupied → 0 V in → output high
    • Broken wire → pull-down wins → 0 V in → output high → reads occupied

    74LVC14A because its inputs are 5 V tolerant at VCC = 3.3 V, so it takes the detector
    output 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 A is what turns clear into evidence. With A unpowered, clear and
    broken-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

    • Power Input A on each detector — the manufacturer's intended wiring, off the DCC bus or a
      regulated 12 V supply.
    • Three 74LVC14A packages, sixteen 100 kOhm resistors, three decouplers.
    • ACTIVE_LOW flips to False, because the inverter moves the polarity. One config line,
      already covered by test_sensors_are_active_low which would need its expectation updated.
    • A bench pass to confirm each channel follows, and a deliberate wire-pull test per channel —
      which is the check this whole issue is about and is currently untestable.

    Not doing it yet

    Deliberately. A stays unpowered while the layout is being brought up; there is plenty else
    to 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.

    ⚠️ One thing that follows immediately, tracked in #12: powering A puts 5 V onto a 3.3 V
    MCP23017 input
    (absolute maximum VDD + 0.6 V = 3.9 V). The level shifting is not optional
    if A is powered, and every manufacturer datasheet tells you to wire A to the DCC bus.

  3. bazauto commented on Aug 24, 2026

    @bazauto
    OwnerAuthor

    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:

    Broken wire reports Consequence
    block_detection clear A train is admitted to occupied track
    ir_position not yet reached A berthing run never gets its stop trigger — overrun, not an early stop

    The 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.

  4. bazauto commented on Sep 23, 2026

    @bazauto
    OwnerAuthor

    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:

    State Line at the node Reads
    Occupied 3.3 V, driven occupied
    Clear 0 V, driven clear
    Signal wire broken pulled up occupied

    This 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 SENSORS entry has its own active_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 a cs--- 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:

    • Signal wire pulled → reads occupied.
    • +3.3 V supply wire pulled → reads 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 reads clear. If it does, the fix is hardware: a separate supply feed, or an output stage that fails high.
    • GND wire pulled → reads occupied. Expected, but unmeasured.
    • The installed channel moved to 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.

  5. changed the title [-]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)[/+] on Sep 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions