Skip to content

fix(dmx-driver): accept a signed inputs word so bit 31 stops freezing the lights - #1587

Merged
groupsky merged 1 commit into
masterfrom
fix/1586-dmx-driver-signed-inputs
Sep 9, 2026
Merged

fix(dmx-driver): accept a signed inputs word so bit 31 stops freezing the lights#1587
groupsky merged 1 commit into
masterfrom
fix/1586-dmx-driver-signed-inputs

Conversation

@groupsky

@groupsky groupsky commented Sep 9, 2026

Copy link
Copy Markdown
Owner

Fixes #1586.

The bug

message-handler.js rejected any reading whose inputs field was negative. But
inputs from the mbsl32di driver is a signed 32-bit word — built as
data[1] << 16 | data[0], so it is negative exactly when input bit 31 is closed.
Bit 31 is a door contact in production, so whenever that one door was in that
state the driver dropped every dry-switch reading and all three bath door
indicators froze at their last frame.

On routy the universe sat at [0, 0, 128] from 2026-09-09T05:22:06Z: 305
readings dropped, 145 frames written, last frame 05:21:31Z. Sample dropped
reading: {"inputs":-39828385, ...}(-39828385) >>> 0 is
0b11111101101000000100010001011111, a perfectly valid word with bit 31 set.

Introduced by c92cae9 (#1543). That hardening was right to reject Infinity and
fractional values; the < 0 clause overreached — its rationale assumed -1 and
did not account for bit 31 being a used input on a 32-input module.

The fix

Accept the full 32-bit range, signed or unsigned, and normalise with >>> 0
before the bit tests — the same thing docker/mqtt-influx/converters/mbsl32di.js
already does.

  • Number.isSafeInteger still closes Infinity (reachable from JSON as 1e999)
    and fractional values.
  • The range check now closes only values genuinely outside 32 bits, which the
    bitwise operators would otherwise wrap into a different reading.
  • A negative inputs is now a normal reading. -1 is all 32 inputs closed.
  • The drop log said "unsigned integer inputs field", which no longer describes
    the guard; it now names the 32-bit range.

dmx-driver was the only service carrying the < 0 clause; the other sites
touched by c92cae9 are unaffected.

Tests

Written first, watched fail, then fixed — the four new negative/range cases
failed against the old guard for exactly the expected reasons.

  • the production reading -39828385 drives the channels instead of being dropped
  • -2147481056 (every mapped bit closed and bit 31) lights all three channels
  • -1 is all inputs closed, not a malformed reading
  • 4294967296 and -2147483649 are still dropped, universe untouched
  • the drop log names the range it enforced
  • Infinity/1e999 and fractional values still dropped (unchanged)

The obsolete drops a negative inputs field rather than lighting every channel
test encoded the bug and is replaced by the out-of-range cases.

$ npm ci --ignore-scripts && npm test
Test Suites: 2 passed, 2 total
Tests:       26 passed, 26 total

Done when

  • message-handler.js accepts a reading with bit 31 set and drives channels 1-3 from its bits
  • Infinity, 1e999, fractional and out-of-32-bit-range values are still dropped, universe untouched
  • Regression test covering a negative inputs (-39828385) asserting the expected channel levels
  • npm test green in docker/dmx-driver/
  • docker/dmx-driver/CLAUDE.md corrected
  • Deployed to routy; docker logs homy-dmx-driver-1 shows frames written with no "Ignoring payload" lines for well-formed readings
  • Locking a bath visibly changes the light on its door

https://claude.ai/code/session_01DAyhJDqko1EAvddsatHRDo

…ng the lights

`inputs` from the mbsl32di driver is a signed 32-bit word — it is built as
`data[1] << 16 | data[0]`, so it is negative exactly when input bit 31 is
closed. Bit 31 is a door contact in production, so the `payload.inputs < 0`
clause added in c92cae9 dropped *every* dry-switch reading for as long as that
one door was in that state, freezing all three bath door indicators at their
last frame. On routy the universe sat at [0, 0, 128] from 2026-09-09T05:22:06Z
with 305 readings dropped against 145 frames written.

Accept the full 32-bit range, signed or unsigned, and normalise with `>>> 0`
before the bit tests, as `mqtt-influx/converters/mbsl32di.js` already does. The
`Number.isSafeInteger` guard is what closes `Infinity` (reachable from JSON as
`1e999`) and fractional values; the range check now closes only values that are
genuinely outside 32 bits, which the bitwise operators would otherwise wrap into
a different reading.

The drop log said "unsigned integer inputs field", which no longer describes the
guard; it now names the 32-bit range. `CLAUDE.md` documented the `>= 0` check as
intended behaviour and is corrected.

Fixes #1586

Claude-Session: https://claude.ai/code/session_01DAyhJDqko1EAvddsatHRDo
@groupsky
groupsky merged commit 1961568 into master Sep 9, 2026
18 checks passed
@groupsky
groupsky deleted the fix/1586-dmx-driver-signed-inputs branch September 9, 2026 06:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

dmx-driver drops every dry-switch reading while input bit 31 is set — bath door lights frozen on routy

1 participant