From f7409b9601ac232661a1d5672db14293dadc12c8 Mon Sep 17 00:00:00 2001 From: Paul Barrett Date: Fri, 25 Sep 2026 19:53:51 +0100 Subject: [PATCH] Switch board 1 to the active-high bazauto detector (#9) The LM-iD.1 on Goods Shed is removed. Every cs--- entry now uses BAZAUTO_BD_ACTIVE_LOW, so a broken signal wire reads occupied. Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 21 ++++++++-------- README.md | 2 +- docs/block-detector-wiring.md | 6 +++-- docs/pin-allocation.md | 25 ++++++++++--------- src/apps/io-node/config.py | 47 +++++++++++++++++++---------------- tests/test_io_node_config.py | 22 +++++++++------- 6 files changed, 67 insertions(+), 56 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b44b4b0..2128e92 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,7 +20,7 @@ Reasoning belongs in `docs/`. |---|---| | `../layout-orchestration/docs/mqtt-contract.md` | **Binding.** Topics, payloads, QoS, retention, the 30 s re-assert. This repo does not own it and must never amend it. | | `docs/pin-allocation.md` | Which sensor is on which expander pin, and why allocation is not installation | -| `docs/block-detector-wiring.md` | The LM-iD output stage: its two modes, why Input A is unpowered, the 3.3 V hazard if it is not | +| `docs/block-detector-wiring.md` | The retired LM-iD output stage: its two modes, the 5 V hazard if Input A is powered. Read before refitting one | | `docs/point-position-feedback.md` | Board 3 (`0x22`): why two inputs per point, the `0x22` allocation, and why command and feedback being different devices is the hazard. **Firmware built; no `S2` wiring landed** | | `docs/broker-auth.md` | Broker identities, the ACL, where the node's password lives, and why a denied publish is silent | | `docs/startup-and-status-led.md` | The power-on race with the modem, the retry-then-reset policy, and **what the LED flash codes mean** | @@ -85,7 +85,7 @@ honestly**, because the backend needs the live message to keep trusting the sens | PN7150 NFC | I2C1, SDA=GP2 SCL=GP3, addr `0x28` | NCI + IRQ. IRQ means "a message is ready", **not** "a tag is present". | | TCA9548A mux | I2C1, addr `0x70` | Fans I2C1 out to up to 8 PN7150 readers | | ESP-AT modem | UART1, TX=GP8 RX=GP9, 9600 baud | Wired ethernet; reports `+ETH_GOT_IP` when ready | -| LM-iD.1 detectors | board 1 (`0x20`) inputs | Current sensing. Open-drain, **5 V if Input A is ever powered** — `docs/block-detector-wiring.md` | +| `bazauto/block-detection` | board 1 (`0x20`) inputs | Current sensing. Push-pull 3.3 V, **active high**: a broken signal wire reads occupied. Replaced the LM-iD.1 | | Waveshare IR reflective | board 2 (`0x21`) inputs | LM393, VCC 3.0–5.3 V, run at 3.3 V. Open-collector + on-board pull-up, so its high is asserted at the sensor | | Cobalt iP Digital points | board 3 (`0x22`), **planned** | Commanded over the **accessory DCC bus**, not MQTT, with no feedback or query of its own. Both of its changeovers are taken (`S2` powers the frog), so the feedback source is undecided. `docs/point-position-feedback.md` | @@ -158,8 +158,8 @@ directory on the path for the same reason. - `AT+MQTTPUB` honours backslash escaping, so JSON needs no `AT+MQTTPUBRAW`. - **Polarity is per sensor**, because it belongs to the device, not the board. Every `SENSORS` entry names its device's constant (`LM_ID_ACTIVE_LOW`, `WAVESHARE_IR_ACTIVE_LOW`, - `BAZAUTO_BD_ACTIVE_LOW`), and startup refuses an entry without a real bool. Both fitted - devices read active low; the `bazauto/block-detection` board is active high. An earlier + `BAZAUTO_BD_ACTIVE_LOW`), and startup refuses an entry without a real bool. Board 1 is all + `bazauto/block-detection`, active high (2026-09-25); board 2's IR is active low. An earlier "active high" note here was #8's, superseded by #11; see `docs/block-detector-wiring.md`. ### Traps @@ -194,18 +194,17 @@ directory on the path for the same reason. - **A point's `unknown` is real and must not be debounced away.** A break-before-make changeover passes through both-open on every throw, so the honest sequence is `normal` → `unknown` → `reverse`. The backend expects it; its confirmation timeout is 8 s. -- **A broken sensor wire reads as `clear`, not `occupied`** (#9). Both sensor types switch to - ground, so an open circuit floats up to the pull-up — the permissive state. The re-assert - cannot catch it: the node is alive and republishing. The fix for board 1 is the +- **A broken IR wire reads as `clear`, not `occupied`** (#9). The IR switches to ground, so + an open circuit floats up to the pull-up — the permissive state. The re-assert cannot + catch it: the node is alive and republishing. Board 1 is fixed by the `bazauto/block-detection` board, active high, so a broken signal wire reads occupied; - swap a `cs---` entry to `BAZAUTO_BD_ACTIVE_LOW` only after its wire-pull bench check. - Board 2 is still unfixed. Status in #9. + the supply- and ground-wire pulls are still to be recorded in #9. Board 2 is still + unfixed. - **A dead IR beam fails toward overrun, not toward stopping.** Same broken wire, different consequence: a `block_detection` sensor wrongly says empty track, an `ir_position` beam wrongly says *not yet reached*, so a berthing run never gets its stop trigger. Cheaper to supervise than the detectors — they already run at 3.3 V — but nothing is fitted (#9). -- **The block detectors' Input A is deliberately unpowered, and that is load-bearing.** - Powering it — which every manufacturer datasheet tells you to do — makes the output idle at +- **If an LM-iD.1 is ever refitted, its Input A must stay unpowered.** Powering it — which every manufacturer datasheet tells you to do — makes the output idle at **5 V** into 3.3 V expander inputs. Do not wire it without reading `docs/block-detector-wiring.md`. - **You cannot determine this sensor's polarity by reading an expander pin.** A floating diff --git a/README.md b/README.md index 2873cc3..be6df80 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ exist yet; see [#4](https://github.com/bazauto/layout-feedback/issues/4). | PN7150 NFC | I2C1, SDA=GP2 SCL=GP3, `0x28` | NCI + IRQ. The IRQ means "a message is ready", not "a tag is present" | | TCA9548A mux | I2C1, `0x70` | Fans I2C1 out to up to 8 readers | | ESP-AT modem | UART1, TX=GP8 RX=GP9, 9600 | Wired ethernet; reports `+ETH_GOT_IP` when ready | -| Legacy Models LM-iD.1 | board 1 inputs | The current-sensing detectors, active low. Output stage and its two modes: `docs/block-detector-wiring.md` | +| `bazauto/block-detection` rev 1.0 | board 1 inputs | The current-sensing detectors, push-pull, active high, so a broken signal wire reads occupied. Replaced the LM-iD.1 (`docs/block-detector-wiring.md`) | | Waveshare IR reflective | board 2 inputs | LM393, run at 3.3 V, active low | | Cobalt iP Digital points | board 3 (`0x22`), planned | Commanded over DCC, never MQTT, and can report nothing itself. Neither changeover is free (`S2` powers the frog), so the feedback source is undecided. See `docs/point-position-feedback.md` | diff --git a/docs/block-detector-wiring.md b/docs/block-detector-wiring.md index b9b5080..347a0cd 100644 --- a/docs/block-detector-wiring.md +++ b/docs/block-detector-wiring.md @@ -158,8 +158,10 @@ believed was shut is open, and what it costs to walk through it. **Superseded as the plan for board 1 (2026-09).** Rather than powering `A` and adding an inverter, the LM-iD.1 is being replaced by the `bazauto/block-detection` board: a CT detector with a push-pull, **active-high** 3.3 V output, so a broken signal wire reads occupied against -the expander's pull-up, with no level shifting. It is under bench test and not yet fitted to -any sensor. The firmware side is ready: polarity is per sensor (`docs/pin-allocation.md`). +the expander's pull-up, with no level shifting. **Done 2026-09-25:** the LM-iD.1 on Goods Shed has been +removed and every board 1 entry is on `BAZAUTO_BD_ACTIVE_LOW` (`docs/pin-allocation.md`). The +rest of this document describes hardware no longer fitted, kept because refitting an LM-iD +without reading it reintroduces the 5 V hazard. ## The IR sensors are not these diff --git a/docs/pin-allocation.md b/docs/pin-allocation.md index e35e765..b86d0ec 100644 --- a/docs/pin-allocation.md +++ b/docs/pin-allocation.md @@ -114,32 +114,33 @@ node, so every `SENSORS` entry in `config.py` carries its own `active_low`, set per-device constant. There is no default: startup refuses an entry without one, or with anything other than `True` or `False`. -Both fitted devices pull to ground when asserting — occupied on board 1, triggered on board -2 — with the internal pull-up holding the line high otherwise. **Occupied reads 0**, so both -constants are `True`. They are different devices that agree by coincidence, not by design. +**Board 1 is active high.** The `bazauto/block-detection` board replaced the LM-iD.1 on +2026-09-25, and every `cs---` entry uses `BAZAUTO_BD_ACTIVE_LOW = False`: it drives its output +push-pull, 3.3 V when occupied. Against the pull-up, a broken signal wire from it reads +**occupied**, which is the point of it (#9). The pull-up has to stay on for those inputs: +without it, the broken wire would float instead. Only installed entries are published, so the +unconnected detectors are silent until each is added to `INSTALLED`. -The `bazauto/block-detection` board, built to replace the LM-iD.1 on board 1, is the first -that disagrees. It drives its output push-pull, 3.3 V when occupied, so it is -`BAZAUTO_BD_ACTIVE_LOW = False`. Against the pull-up, a broken signal wire from it reads -**occupied**, which is the point of it (#9). Moving a `cs---` entry onto it is a one-line -change, to make only after that channel's wire-pull check on the bench. The pull-up has to -stay on for those inputs: without it, the broken wire would float instead. +**Board 2 is active low.** The Waveshare IR pulls to ground when triggered, with the internal +pull-up holding the line high otherwise, so a broken wire there still reads `clear`. + +`LM_ID_ACTIVE_LOW` is kept for the record but is no longer used by any entry. | Constant | Device | Occupied reads | |---|---|---| -| `LM_ID_ACTIVE_LOW = True` | Legacy Models LM-iD.1 | 0 | +| `LM_ID_ACTIVE_LOW = True` | Legacy Models LM-iD.1 (no longer fitted) | 0 | | `WAVESHARE_IR_ACTIVE_LOW = True` | Waveshare IR reflective | 0 | | `BAZAUTO_BD_ACTIVE_LOW = False` | `bazauto/block-detection` rev 1.0 | 1 | | Board | Device | Output | |---|---|---| -| 1 (`0x20`) | Legacy Models LM-iD.1 | Open-drain. **Idles at 5 V if Input A is ever powered** — which is what every manufacturer datasheet tells you to do | +| 1 (`0x20`) | `bazauto/block-detection` rev 1.0 | Push-pull 3.3 V, active high. Replaced the LM-iD.1, which is open-drain and **idles at 5 V if Input A is ever powered** | | 2 (`0x21`) | Waveshare IR reflective | LM393 open-collector with an on-board pull-up to its own 3.3 V VCC. Safe on an expander input directly | **`docs/block-detector-wiring.md` is the whole picture**, and the place to read before changing anything about how either is wired. -**A broken wire therefore reads as `clear`, not `occupied`** — the failure lands on the +**On board 2, a broken wire therefore reads as `clear`, not `occupied`** — the failure lands on the permissive state, and the 30 s re-assert cannot catch it because the node is alive and happily republishing. Accepted knowingly; see #9 for why, and for the closed-circuit alternatives — one of which `docs/block-detector-wiring.md` shows is available after all. diff --git a/src/apps/io-node/config.py b/src/apps/io-node/config.py index d14d6e6..b9ceeec 100644 --- a/src/apps/io-node/config.py +++ b/src/apps/io-node/config.py @@ -46,24 +46,27 @@ # Output polarity, per device. Polarity belongs to the device on the far end of the wire, # not to the board or the node, so every SENSORS entry names the device it is. # -# The two fitted devices are switches to ground: closed (conducting) when asserting, -# open otherwise, with the MCP23017's internal pull-up holding the line high when open. -# So occupied reads 0. Confirmed on the bench 2026-08-23 by scanning all 32 pins with a -# loco in Goods Shed and again with it removed — pin 8 on both boards was the only one -# that moved. +# The Waveshare IR modules on board 2 are switches to ground: closed (conducting) when +# triggered, open otherwise, with the MCP23017's internal pull-up holding the line high +# when open. So triggered reads 0. Confirmed on the bench 2026-08-23 by scanning all 32 +# pins with a loco in Goods Shed and again with it removed. # -# **For both, a broken wire reads as `clear`, not `occupied`** (#9). The pull-up gives a -# defined level on an open circuit, but that level is the permissive one, so a severed -# wire asserts empty track and the re-assert keeps confirming it. -LM_ID_ACTIVE_LOW = True # Legacy Models LM-iD.1, board 1: pulls to 0 V occupied +# **For the IR, a broken wire reads as `clear`, not `occupied`** (#9). The pull-up gives a +# defined level on an open circuit, but that level is the permissive one. WAVESHARE_IR_ACTIVE_LOW = True # Waveshare IR (LM393), board 2: pulls to 0 V triggered # The bazauto/block-detection board drives its output push-pull: 3.3 V occupied, 0 V # clear. Against the pull-up a broken signal wire therefore reads **occupied** — the fix -# for #9 on board 1. Not yet fitted to any sensor. Swap a board 1 entry to it only once -# that channel has passed its wire-pull check on the bench (see #9). +# for #9 on board 1. Every board 1 entry uses it as of 2026-09-25, when the LM-iD.1 on +# Goods Shed was removed. The pull-up must stay on for these inputs, or a broken wire +# floats instead of reading occupied. BAZAUTO_BD_ACTIVE_LOW = False +# Legacy Models LM-iD.1: pulls to 0 V occupied. **No longer fitted** — board 1 is all +# bazauto/block-detection now. Kept so that refitting one is a visible, one-line change +# rather than a guess; see docs/block-detector-wiring.md before doing so. +LM_ID_ACTIVE_LOW = True + DEBOUNCE_MS = 200 # The full allocation. Pin N is the same block on both boards; board 2 pin 5 is reserved @@ -71,23 +74,23 @@ SENSORS = ( # --- board 1, current sensing (block_detection) --- {"sensor_id": "cs---fiddle-yard-1", "expander": EXPANDER_CS, "pin": 0, - "active_low": LM_ID_ACTIVE_LOW}, + "active_low": BAZAUTO_BD_ACTIVE_LOW}, {"sensor_id": "cs---fiddle-yard-2", "expander": EXPANDER_CS, "pin": 1, - "active_low": LM_ID_ACTIVE_LOW}, + "active_low": BAZAUTO_BD_ACTIVE_LOW}, {"sensor_id": "cs---siding-1", "expander": EXPANDER_CS, "pin": 2, - "active_low": LM_ID_ACTIVE_LOW}, + "active_low": BAZAUTO_BD_ACTIVE_LOW}, {"sensor_id": "cs---siding-2", "expander": EXPANDER_CS, "pin": 3, - "active_low": LM_ID_ACTIVE_LOW}, + "active_low": BAZAUTO_BD_ACTIVE_LOW}, {"sensor_id": "cs---siding-3", "expander": EXPANDER_CS, "pin": 4, - "active_low": LM_ID_ACTIVE_LOW}, + "active_low": BAZAUTO_BD_ACTIVE_LOW}, {"sensor_id": "cs---engine-goods-transfer", "expander": EXPANDER_CS, "pin": 5, - "active_low": LM_ID_ACTIVE_LOW}, + "active_low": BAZAUTO_BD_ACTIVE_LOW}, {"sensor_id": "cs---engine-shed-1", "expander": EXPANDER_CS, "pin": 6, - "active_low": LM_ID_ACTIVE_LOW}, + "active_low": BAZAUTO_BD_ACTIVE_LOW}, {"sensor_id": "cs---engine-shed-2", "expander": EXPANDER_CS, "pin": 7, - "active_low": LM_ID_ACTIVE_LOW}, + "active_low": BAZAUTO_BD_ACTIVE_LOW}, {"sensor_id": "cs---goods-shed", "expander": EXPANDER_CS, "pin": 8, - "active_low": LM_ID_ACTIVE_LOW}, + "active_low": BAZAUTO_BD_ACTIVE_LOW}, # --- board 2, IR (ir_position) --- {"sensor_id": "ir---fiddle-yard-1", "expander": EXPANDER_IR, "pin": 0, "active_low": WAVESHARE_IR_ACTIVE_LOW}, @@ -110,7 +113,9 @@ # Physically wired, and therefore the only sensors published. Both on Goods Shed, which # is the one block with both a detector and a beam — so the bring-up exercises the -# occupancy derivation rather than one sensor in isolation. +# occupancy derivation rather than one sensor in isolation. Every board 1 entry above is +# already on the bazauto/block-detection polarity, so bringing another detector online is +# only a matter of adding its id here. INSTALLED = ( "cs---goods-shed", "ir---goods-shed", diff --git a/tests/test_io_node_config.py b/tests/test_io_node_config.py index ccab5a9..80f795b 100644 --- a/tests/test_io_node_config.py +++ b/tests/test_io_node_config.py @@ -54,20 +54,24 @@ def test_pin_n_is_the_same_block_on_both_boards(): assert cs[pin] == block, "pin %d names different blocks on the two boards" % pin -def test_the_installed_sensors_are_active_low(): - """Both Goods Shed devices are switches to ground: occupied reads 0. - - Confirmed by scanning all 32 pins with a loco in the block and again with it - removed. Note the consequence recorded in #9: a broken wire floats to the pull-up - and therefore reads `clear`, which is the permissive state, not the safe one. - Swapping `cs---goods-shed` to the bazauto/block-detection board changes this. - """ +def test_the_installed_sensors_have_their_devices_polarity(): + """Goods Shed's detector is the bazauto/block-detection board, occupied reads 1; its + beam is a Waveshare IR switch to ground, triggered reads 0.""" by_id = {e["sensor_id"]: e for e in select_installed(config.SENSORS, config.INSTALLED)} - assert by_id["cs---goods-shed"]["active_low"] is config.LM_ID_ACTIVE_LOW is True + assert by_id["cs---goods-shed"]["active_low"] is config.BAZAUTO_BD_ACTIVE_LOW is False assert by_id["ir---goods-shed"]["active_low"] is config.WAVESHARE_IR_ACTIVE_LOW is True +def test_every_current_sensor_uses_the_bazauto_detector_polarity(): + """Board 1 is all bazauto/block-detection. A `cs---` entry left on the LM-iD.1's + polarity would read upside down the moment it is added to INSTALLED: an empty block + published as occupied, and — worse — an occupied one as clear.""" + for entry in config.SENSORS: + if entry["expander"] == config.EXPANDER_CS: + assert entry["active_low"] is config.BAZAUTO_BD_ACTIVE_LOW, entry["sensor_id"] + + def test_the_bazauto_detector_is_active_high(): """Push-pull, 3.3 V when occupied, so a broken wire into the pull-up reads occupied.""" assert config.BAZAUTO_BD_ACTIVE_LOW is False