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
94 lines
5.3 KiB
Markdown
94 lines
5.3 KiB
Markdown
# 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.
|