Files
brewpi/docs/hendi_lockout_findings.md
jensandClaude Sonnet 5 bd6249af6b docs: record 10s idle-timeout (remote->suspend) findings for v1.16
Confirmed on hardware: 15s idle while remote-enabled with the heater
on and powered auto-suspends (R? and S? both drop to 0). P? does not
read literal 0 though - it lands on 4095, the raw register's max value
past the calibrated curve's low end, not the digit 0. Any check
expecting P?=="0" to confirm zero power would be wrong on this
firmware; S? is the correct check.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GpePKZiEZWbGo9HrfuML6U
2026-07-02 18:51:18 +02:00

5.3 KiB

Hendi remote-control lockout findings

Reproduced and verified on real hardware using scripts/hendi_ctrl_app.py's --trigger-error-state, --reset and --test-heater flags, across four firmware versions (getSoftwareVersion() / I?+V? via components/actor/hendiCtrl.py).

Trigger

Calling remoteEnable(True) (R1) and then disconnecting/exiting without a matching remoteEnable(False) (R0) - i.e. an ungraceful disconnect while remote control is enabled - puts the device into a locked-out state.

Recovery attempts - v1.12 vs v1.14 vs v1.15 vs v1.16

v1.14 flashed via --update-firmware HendiCtrl_0114.srec; v1.15 via HendiCtrl_0115.srec; v1.16 via HendiCtrl_0116.srec. getSoftwareVersion() confirmed each version after flashing.

Recovery method Mechanism v1.12 (original) v1.14 (firmware fix) v1.15 (firmware fix) v1.16 (firmware fix)
Plain reconnect, no --reset Fresh serial connection only, no DTR/RTS toggle Fails Fails - unchanged, a bare reconnect alone still does not clear the lockout Intermittent - recovered on 1 of 3 isolated trigger/reconnect cycles, failed the other 2; not a reliable recovery path Recovers - reproducible across 4 of 4 independent trigger/reconnect/verify cycles
--reset, isolated process Soft reset via DTR/RTS toggle, no power loss Fails (one fluke recovery when combined with --test-heater in the same process/connection - not reproducible in isolation) Recovers - reproducible across two independent trigger/reset/verify cycles Recovers - reproducible across all trigger/reset/verify cycles run Not retested (superseded by plain reconnect working)
Physical power cycle (device knob) Full power-off/on of the unit Recovers Not retested (superseded by --reset working) Not retested (superseded by --reset working) Not retested (superseded by plain reconnect working)

10s idle-timeout (remote -> suspend)

Tested on v1.16: with remote enabled (R1), the heater switched on (S1) and a non-zero power applied (P3151, ~1000W), going idle for 15s with no further commands sent causes the device to auto-suspend:

Register Before idle (R1/S1/P3151) After 15s idle
R? (remote enabled) 1 0 - timer drops remote back to suspend
S? (switch) 1 0 - heater switched off
P? (power digits) 3151 (~1000W) 4095 - see note below

P? does not read the literal digit 0 after the timeout. The power/digit mapping is inverted (components/actor/hendiCtrl.py's poly_w2d/poly_d2w: higher digit = lower watts, calibrated down to pwr_digits_min = 4092 for the lowest calibrated point, 500W). 4095 is the raw 12-bit register's maximum value (2^12 - 1), one past the lowest-calibrated end of that curve (toWatts(4095) extrapolates to ~518W) - i.e. the hardware's actual "off" register value, not the number 0. Any check that expects P? to equal "0" to confirm the heater has zero power would be wrong on this firmware; the correct check is S? (or P? >= pwr_digits_min).

Device remained healthy afterward - no lockout was triggered, since remote was already back to 0 (via the timer) before the test script's connection closed.

Symptom while locked out

  • I? / V? (software identifier/version queries) still respond normally.
  • R1 (remoteEnable(True)) is the only rejected command; it blocks any command path that needs remote control enabled (e.g. --test-heater). R0 (remoteEnable(False)) still succeeds (OK:0) even while locked out.
  • With hendi_ctrl_app.py --debug (raw protocol trace added to components/actor/hendiCtrl.py's cmd()), the actual wire exchange for a rejected R1 on v1.15 is:
    req='R1'  echo=b'R1\r\n'  answer=b'\rERR:Invalid state\r\n'
    
    So this isn't a timeout or garbled response - the firmware actively replies with ERR:Invalid state. HendiCtrl.cmd() previously only checked for "OK" in the response and raised a generic HendiException("Communication error") for anything else, discarding this actual error text; it now includes the raw echo/answer in the exception message.

Conclusion

On firmware v1.12, only a physical power cycle clears the lockout; neither a serial soft-reset nor a fresh serial reconnect does reliably.

Firmware v1.14 fixes this partially: a soft reset (--reset, i.e. toggling the DTR/RTS line) now reliably clears the lockout, confirmed across two independent trigger/reset/verify cycles - no physical power cycle needed. However, a plain reconnect without --reset still does not clear it, so the fix specifically ties recovery to the DTR/RTS reset line rather than to any new connection.

Firmware v1.15 keeps --reset working reliably, same as v1.14. It also introduces an intermittent plain-reconnect recovery (1 of 3 isolated trials) that v1.14 didn't show at all - but since it fails the majority of the time, it should be treated as a remaining bug/race condition rather than a fix, and --reset is still the recommended recovery path.

Firmware v1.16 appears to fully fix the lockout: a plain reconnect with no --reset at all recovered on 4 of 4 independent trigger/reconnect/verify cycles - the intermittent v1.15 behavior looks to have become fully reliable. --reset was not retested since it's no longer needed as a workaround.