diff --git a/CLAUDE.md b/CLAUDE.md index f6ddb12..9568372 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -154,11 +154,11 @@ directory on the path for the same reason. - **Any `mpremote` command stops the running node**, dropping it to the raw REPL until the next reset. - `AT+MQTTPUB` honours backslash escaping, so JSON needs no `AT+MQTTPUBRAW`. -- **Both boards read active low** — occupied/triggered is 0 — but they are different devices - and only agree by coincidence. `ACTIVE_LOW = True` is one global flag covering both - (`main.py` passes `config.ACTIVE_LOW` for every sensor); the day a batch disagrees, it has - to become per-sensor. An earlier "active high" note here was #8's, superseded by #11; see - `docs/block-detector-wiring.md`. +- **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 + "active high" note here was #8's, superseded by #11; see `docs/block-detector-wiring.md`. ### Traps @@ -187,9 +187,10 @@ directory on the path for the same reason. `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. Accepted knowingly — but #9 rejected - the closed-circuit fix on a premise since shown false, so it is reopened - (`docs/block-detector-wiring.md`). + cannot catch it: the node is alive and republishing. The fix for board 1 is 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. - **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 diff --git a/docs/block-detector-wiring.md b/docs/block-detector-wiring.md index bb2ce8c..b9b5080 100644 --- a/docs/block-detector-wiring.md +++ b/docs/block-detector-wiring.md @@ -47,7 +47,7 @@ transistor — a switch to ground and nothing else. With `A` powered, the same p 5 V from an internal pull-up. The vendor's *"the LM-iD will act without outside power, but powering external devices needs a power source added"* is describing exactly this. -**Occupied reads 0 in both modes**, which is why `config.py`'s `ACTIVE_LOW = True` is correct +**Occupied reads 0 in both modes**, which is why `config.py`'s `LM_ID_ACTIVE_LOW = True` is correct regardless of how `A` is wired, and why the node has never mis-reported occupancy over this. ## Why this repo held two contradictory conclusions @@ -142,7 +142,7 @@ LM-iD B ──┬── 74LVC14A input ──► MCP23017 input That is the closed-circuit principle #9 was reaching for, achieved with one 14-pin package per six channels and no change to the detectors. It requires `A` to be powered, and it -requires `ACTIVE_LOW` to be flipped to `False`, because the inverter has moved the polarity. +requires those sensors' `active_low` to be `False`, because the inverter has moved the polarity. **The important half is not the circuit, it is what it means:** powering `A` is what turns `clear` from an *absence of signal* into an *assertion*. With `A` unpowered, clear and @@ -155,6 +155,12 @@ Current position is unchanged — `A` stays unpowered while the layout is being the broken-wire limitation stands as #9 describes it. This document records that the door #9 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 IR sensors are not these Board 2 carries [Waveshare Infrared Reflective Sensor](https://www.waveshare.com/wiki/Infrared_Reflective_Sensor) @@ -172,10 +178,9 @@ which means, unlike an unpowered LM-iD, **its high level is asserted at the sens than being an absence. There is no 5 V hazard and no mode to get wrong. The module also brings out `AOUT` and a sensitivity potentiometer; neither is used. -Both boards happen to read **active low**, so the single global `ACTIVE_LOW = True` is correct -for both. That is a coincidence of two unrelated devices, not a property of the design — -`main.py` passes `config.ACTIVE_LOW` for every sensor, so the first batch that disagrees makes -it a per-sensor field. +Both boards happen to read **active low**. That is a coincidence of two unrelated devices, not +a property of the design, which is why polarity is a per-sensor field in `config.py` with one +constant per device (`docs/pin-allocation.md`). ### The broken-wire exposure is still there, and it fails the other way diff --git a/docs/pin-allocation.md b/docs/pin-allocation.md index 270e964..e35e765 100644 --- a/docs/pin-allocation.md +++ b/docs/pin-allocation.md @@ -107,15 +107,29 @@ If in doubt, do not count — measure. `mpremote run` a scan of all 32 pins with triggered and see which one moves. That took under a minute and settled it definitively both times counting got it wrong. -## Sensor polarity: active LOW +## Sensor polarity: per sensor -Both boards 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 `config.py` has -`ACTIVE_LOW = True`. +Polarity is a property of the device on the far end of the wire, not of the board or the +node, so every `SENSORS` entry in `config.py` carries its own `active_low`, set from a +per-device constant. There is no default: startup refuses an entry without one, or with +anything other than `True` or `False`. -**The two boards are different devices that agree by coincidence, not by design.** `main.py` -passes one global `config.ACTIVE_LOW` for every sensor, so the first batch that disagrees -makes it a per-sensor field rather than a config edit. +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. + +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. + +| Constant | Device | Occupied reads | +|---|---|---| +| `LM_ID_ACTIVE_LOW = True` | Legacy Models LM-iD.1 | 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 | |---|---|---| diff --git a/src/apps/io-node/config.py b/src/apps/io-node/config.py index a38e063..fcd7d6c 100644 --- a/src/apps/io-node/config.py +++ b/src/apps/io-node/config.py @@ -40,17 +40,26 @@ EXPANDER_IR = 0x21 EXPANDER_POINTS = 0x22 -# The sensors are switches to ground: closed (conducting) when the block is occupied, +# 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. # -# **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. Accepted knowingly for now; -# see #9 for the closed-circuit alternatives. -ACTIVE_LOW = True +# **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 +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). +BAZAUTO_BD_ACTIVE_LOW = False DEBOUNCE_MS = 200 @@ -58,25 +67,42 @@ # and empty because Engine / Goods Transfer has current sensing and no IR beam. SENSORS = ( # --- board 1, current sensing (block_detection) --- - {"sensor_id": "cs---fiddle-yard-1", "expander": EXPANDER_CS, "pin": 0}, - {"sensor_id": "cs---fiddle-yard-2", "expander": EXPANDER_CS, "pin": 1}, - {"sensor_id": "cs---siding-1", "expander": EXPANDER_CS, "pin": 2}, - {"sensor_id": "cs---siding-2", "expander": EXPANDER_CS, "pin": 3}, - {"sensor_id": "cs---siding-3", "expander": EXPANDER_CS, "pin": 4}, - {"sensor_id": "cs---engine-goods-transfer", "expander": EXPANDER_CS, "pin": 5}, - {"sensor_id": "cs---engine-shed-1", "expander": EXPANDER_CS, "pin": 6}, - {"sensor_id": "cs---engine-shed-2", "expander": EXPANDER_CS, "pin": 7}, - {"sensor_id": "cs---goods-shed", "expander": EXPANDER_CS, "pin": 8}, + {"sensor_id": "cs---fiddle-yard-1", "expander": EXPANDER_CS, "pin": 0, + "active_low": LM_ID_ACTIVE_LOW}, + {"sensor_id": "cs---fiddle-yard-2", "expander": EXPANDER_CS, "pin": 1, + "active_low": LM_ID_ACTIVE_LOW}, + {"sensor_id": "cs---siding-1", "expander": EXPANDER_CS, "pin": 2, + "active_low": LM_ID_ACTIVE_LOW}, + {"sensor_id": "cs---siding-2", "expander": EXPANDER_CS, "pin": 3, + "active_low": LM_ID_ACTIVE_LOW}, + {"sensor_id": "cs---siding-3", "expander": EXPANDER_CS, "pin": 4, + "active_low": LM_ID_ACTIVE_LOW}, + {"sensor_id": "cs---engine-goods-transfer", "expander": EXPANDER_CS, "pin": 5, + "active_low": LM_ID_ACTIVE_LOW}, + {"sensor_id": "cs---engine-shed-1", "expander": EXPANDER_CS, "pin": 6, + "active_low": LM_ID_ACTIVE_LOW}, + {"sensor_id": "cs---engine-shed-2", "expander": EXPANDER_CS, "pin": 7, + "active_low": LM_ID_ACTIVE_LOW}, + {"sensor_id": "cs---goods-shed", "expander": EXPANDER_CS, "pin": 8, + "active_low": LM_ID_ACTIVE_LOW}, # --- board 2, IR (ir_position) --- - {"sensor_id": "ir---fiddle-yard-1", "expander": EXPANDER_IR, "pin": 0}, - {"sensor_id": "ir---fiddle-yard-2", "expander": EXPANDER_IR, "pin": 1}, - {"sensor_id": "ir---siding-1", "expander": EXPANDER_IR, "pin": 2}, - {"sensor_id": "ir---siding-2", "expander": EXPANDER_IR, "pin": 3}, - {"sensor_id": "ir---siding-3", "expander": EXPANDER_IR, "pin": 4}, + {"sensor_id": "ir---fiddle-yard-1", "expander": EXPANDER_IR, "pin": 0, + "active_low": WAVESHARE_IR_ACTIVE_LOW}, + {"sensor_id": "ir---fiddle-yard-2", "expander": EXPANDER_IR, "pin": 1, + "active_low": WAVESHARE_IR_ACTIVE_LOW}, + {"sensor_id": "ir---siding-1", "expander": EXPANDER_IR, "pin": 2, + "active_low": WAVESHARE_IR_ACTIVE_LOW}, + {"sensor_id": "ir---siding-2", "expander": EXPANDER_IR, "pin": 3, + "active_low": WAVESHARE_IR_ACTIVE_LOW}, + {"sensor_id": "ir---siding-3", "expander": EXPANDER_IR, "pin": 4, + "active_low": WAVESHARE_IR_ACTIVE_LOW}, # pin 5 reserved — no IR beam on Engine / Goods Transfer - {"sensor_id": "ir---engine-shed-1", "expander": EXPANDER_IR, "pin": 6}, - {"sensor_id": "ir---engine-shed-2", "expander": EXPANDER_IR, "pin": 7}, - {"sensor_id": "ir---goods-shed", "expander": EXPANDER_IR, "pin": 8}, + {"sensor_id": "ir---engine-shed-1", "expander": EXPANDER_IR, "pin": 6, + "active_low": WAVESHARE_IR_ACTIVE_LOW}, + {"sensor_id": "ir---engine-shed-2", "expander": EXPANDER_IR, "pin": 7, + "active_low": WAVESHARE_IR_ACTIVE_LOW}, + {"sensor_id": "ir---goods-shed", "expander": EXPANDER_IR, "pin": 8, + "active_low": WAVESHARE_IR_ACTIVE_LOW}, ) # Physically wired, and therefore the only sensors published. Both on Goods Shed, which @@ -104,7 +130,7 @@ # # Each input is pulled to 0 V against the expander's pull-up when its side is made, so # a made input reads 0. Its own flag -# rather than `ACTIVE_LOW` above: this is a third device that happens to agree with the +# rather than the sensor polarities above: this is a third device that happens to agree with the # other two, and treating that coincidence as a rule is a mistake this repo has already # made once. POINTS_ACTIVE_LOW = True diff --git a/src/apps/io-node/main.py b/src/apps/io-node/main.py index aa65147..363351a 100644 --- a/src/apps/io-node/main.py +++ b/src/apps/io-node/main.py @@ -51,7 +51,11 @@ def get_expander(expanders, i2c, available, address, what): def build_sensors(i2c, available, expanders, wiring): - """Configure one input pin per installed sensor.""" + """Configure one input pin per installed sensor, returned with its polarity. + + The pull-up stays on for every sensor, whatever its polarity. For an active-high + device it is what makes a broken wire read `occupied` rather than float (#9). + """ sensors = [] for entry in wiring: @@ -60,7 +64,7 @@ def build_sensors(i2c, available, expanders, wiring): pin = expander.create_pin( entry["pin"], mode="input", debounce_ms=config.DEBOUNCE_MS) pin.setup() - sensors.append((entry["sensor_id"], pin)) + sensors.append((entry["sensor_id"], pin, entry["active_low"])) return sensors @@ -171,7 +175,7 @@ def bring_up(): on_publish_failure=lambda topic_id, exc: print( "io-node: publish failed for %s (%s)" % (topic_id, exc)), ) - for sensor_id, _pin in sensors: + for sensor_id, _pin, _active_low in sensors: layout.register_sensor(sensor_id) for point_id, _normal, _reverse in points: layout.register_point(point_id) @@ -185,8 +189,8 @@ def bring_up(): # Publish what is on the track right now. Without this the first reading waits for # an edge, and a block occupied since before boot would never be reported at all. now = ticks_ms() - for sensor_id, pin in sensors: - layout.publish_sensor(sensor_id, is_occupied(pin.state, config.ACTIVE_LOW), now) + for sensor_id, pin, active_low in sensors: + layout.publish_sensor(sensor_id, is_occupied(pin.state, active_low), now) for point_id, normal_pin, reverse_pin in points: position, cross_wired = read_point(normal_pin, reverse_pin) if cross_wired: @@ -201,10 +205,10 @@ def run(sensors, points, layout, client): while True: now = ticks_ms() - for sensor_id, pin in sensors: + for sensor_id, pin, active_low in sensors: if pin.update_state(): layout.publish_sensor( - sensor_id, is_occupied(pin.state, config.ACTIVE_LOW), now) + sensor_id, is_occupied(pin.state, active_low), now) for point_id, normal_pin, reverse_pin in points: # Both, every pass, and never short-circuited: a throw moves one contact diff --git a/src/lib/point_wiring.py b/src/lib/point_wiring.py index d0ccba4..6e443f6 100644 --- a/src/lib/point_wiring.py +++ b/src/lib/point_wiring.py @@ -117,7 +117,7 @@ def is_closed(level, active_low): Each input is pulled to 0 V when its side is made, whether by a contact, an opto or a Hall switch, so a made input reads 0 against the internal pull-up. Its own flag rather - than the sensors' `ACTIVE_LOW`: this is a third device that happens to agree with the + than the sensors' per-device polarity: this is a third device that happens to agree with the other two, and the repo has already been bitten once by treating a coincidence as a rule. """ diff --git a/src/lib/sensor_wiring.py b/src/lib/sensor_wiring.py index ccc3ab0..4f79ec3 100644 --- a/src/lib/sensor_wiring.py +++ b/src/lib/sensor_wiring.py @@ -26,12 +26,21 @@ def select_installed(sensors, installed): sensor_id = entry["sensor_id"] expander = entry["expander"] pin = entry["pin"] + active_low = entry["active_low"] except (KeyError, TypeError): raise ValueError( - "every SENSORS entry needs sensor_id, expander and pin — got %r" % (entry,)) + "every SENSORS entry needs sensor_id, expander, pin and active_low — got %r" + % (entry,)) validate_sensor_id(sensor_id) + # No default, and nothing truthy standing in for a bool. Polarity is a property of + # the device on the far end of the wire, and a guessed one publishes `clear` for + # every occupied block that sensor watches. + if active_low is not True and active_low is not False: + raise ValueError( + "%s: active_low must be True or False — got %r" % (sensor_id, active_low)) + if sensor_id in by_id: raise ValueError("sensor id %r appears twice in SENSORS" % sensor_id) by_id[sensor_id] = entry @@ -69,9 +78,9 @@ def is_occupied(level, active_low): One line, and it has its own name because getting it backwards publishes `clear` for an occupied block — which for a block_detection sensor is enough on its own to - let the orchestrator clear the block. Westgate Hollow's block detectors are active - **low** — occupied reads 0 (measured, see docs/pin-allocation.md). The polarity stays - configurable because the next batch of hardware may not be, and because fitting the - inverting stage in docs/block-detector-wiring.md would flip this one. + let the orchestrator clear the block. `active_low` comes from the sensor's own + SENSORS entry, because it is a property of the device on the far end of the wire: + the LM-iD.1 detectors and the IR modules are active low, the bazauto/block-detection + board is active high (see docs/pin-allocation.md). """ return (level == 0) if active_low else (level == 1) diff --git a/tests/test_io_node_config.py b/tests/test_io_node_config.py index 4a4a718..ccab5a9 100644 --- a/tests/test_io_node_config.py +++ b/tests/test_io_node_config.py @@ -54,14 +54,31 @@ 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_sensors_are_active_low(): - """Switches to ground: closed when occupied, so occupied reads 0. +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. """ - assert config.ACTIVE_LOW is True + 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["ir---goods-shed"]["active_low"] is config.WAVESHARE_IR_ACTIVE_LOW is True + + +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 + + +def test_every_ir_sensor_uses_the_ir_polarity(): + """The bazauto/block-detection board replaces current sensing only. An IR entry + given a detector's polarity would read every beam upside down.""" + for entry in config.SENSORS: + if entry["expander"] == config.EXPANDER_IR: + assert entry["active_low"] is config.WAVESHARE_IR_ACTIVE_LOW, entry["sensor_id"] def test_no_outputs_are_configured(): diff --git a/tests/test_sensor_wiring.py b/tests/test_sensor_wiring.py index b4127a2..83dd592 100644 --- a/tests/test_sensor_wiring.py +++ b/tests/test_sensor_wiring.py @@ -9,9 +9,9 @@ from sensor_wiring import is_occupied, select_installed TABLE = ( - {"sensor_id": "cs---goods-shed", "expander": 0x20, "pin": 8}, - {"sensor_id": "ir---goods-shed", "expander": 0x21, "pin": 8}, - {"sensor_id": "cs---siding-1", "expander": 0x20, "pin": 2}, + {"sensor_id": "cs---goods-shed", "expander": 0x20, "pin": 8, "active_low": True}, + {"sensor_id": "ir---goods-shed", "expander": 0x21, "pin": 8, "active_low": True}, + {"sensor_id": "cs---siding-1", "expander": 0x20, "pin": 2, "active_low": False}, ) @@ -45,14 +45,16 @@ def test_an_installed_id_missing_from_the_table_is_refused(): def test_two_sensors_on_one_pin_are_refused(): """At least one of them would be reporting another block's state, and nothing on the broker would show it.""" - clashing = TABLE + ({"sensor_id": "cs---siding-2", "expander": 0x20, "pin": 8},) + clashing = TABLE + ({"sensor_id": "cs---siding-2", "expander": 0x20, "pin": 8, + "active_low": True},) with pytest.raises(ValueError, match="both on expander"): select_installed(clashing, ()) def test_a_duplicate_sensor_id_is_refused(): - duped = TABLE + ({"sensor_id": "cs---goods-shed", "expander": 0x21, "pin": 3},) + duped = TABLE + ({"sensor_id": "cs---goods-shed", "expander": 0x21, "pin": 3, + "active_low": True},) with pytest.raises(ValueError, match="twice in SENSORS"): select_installed(duped, ()) @@ -64,14 +66,14 @@ def test_a_duplicate_in_installed_is_refused(): def test_a_pin_outside_the_expander_range_is_refused(): - bad = ({"sensor_id": "cs---x", "expander": 0x20, "pin": 16},) + bad = ({"sensor_id": "cs---x", "expander": 0x20, "pin": 16, "active_low": True},) with pytest.raises(ValueError, match="out of range"): select_installed(bad, ()) def test_a_placeholder_id_in_the_table_is_refused(): - bad = ({"sensor_id": "", "expander": 0x20, "pin": 0},) + bad = ({"sensor_id": "", "expander": 0x20, "pin": 0, "active_low": True},) with pytest.raises(ValueError): select_installed(bad, ()) @@ -84,12 +86,39 @@ def test_a_malformed_entry_is_refused(): # --- polarity --------------------------------------------------------------- +def test_an_entry_without_a_polarity_is_refused(): + """No default. A guessed polarity publishes `clear` for every occupied block.""" + bad = ({"sensor_id": "cs---x", "expander": 0x20, "pin": 0},) + + with pytest.raises(ValueError, match="active_low"): + select_installed(bad, ()) + + +@pytest.mark.parametrize("value", [1, 0, "False", None]) +def test_a_polarity_that_is_not_a_bool_is_refused(value): + """`"False"` is truthy, so a string would silently mean active low.""" + bad = ({"sensor_id": "cs---x", "expander": 0x20, "pin": 0, "active_low": value},) + + with pytest.raises(ValueError, match="must be True or False"): + select_installed(bad, ()) + + +def test_each_sensor_keeps_its_own_polarity(): + """The point of making it per-sensor: an active-low LM-iD and an active-high + bazauto/block-detection board on the same node, each read the right way round.""" + selected = select_installed(TABLE, ("cs---goods-shed", "cs---siding-1")) + + assert [is_occupied(0, e["active_low"]) for e in selected] == [True, False] + assert [is_occupied(1, e["active_low"]) for e in selected] == [False, True] + + def test_active_high_reads_1_as_occupied(): - """Westgate Hollow, measured on the bench.""" + """The bazauto/block-detection board: push-pull, 3.3 V when occupied.""" assert is_occupied(1, active_low=False) is True assert is_occupied(0, active_low=False) is False def test_active_low_inverts(): + """The LM-iD.1 and the Waveshare IR modules, measured on the bench.""" assert is_occupied(0, active_low=True) is True assert is_occupied(1, active_low=True) is False