power: drive Tasmota plugs and HTTP relays; v500: continuous handshake - #144
Conversation
Two new DEFIB_POWER_TYPE backends, so lab benches built from cheap smart plugs no longer need someone at a second shell to flip the power: - tasmota: the /cm?cmnd= HTTP API (DEFIB_TASMOTA_HOST, optional _RELAY, _USER, _PASSWORD). On/off replies are checked against the requested state, and an auth WARNING reply is an error rather than a silent no-op. power_cycle runs on the plug as "Backlog Power OFF; Delay N; Power ON", so the off window does not depend on network latency and a dropped connection mid-cycle still leaves the camera powered; it then polls until the relay reports ON again. - http: any relay with an "on" GET URL and an "off" GET URL (DEFIB_HTTP_POWER_ON_URL / _OFF_URL, optional _TIMEOUT) — e.g. a local bridge in front of a cloud-only Sonoff switch. agent upload accepted --power-cycle but ignored it on the HiSiliconStandard path; only CV6xx used the controller. It now powers off, starts the handshake while the camera is still dark, then powers on. The order matters for relays whose power_on returns seconds after the relay actually closed: the blaster is already on the wire when the bootrom window opens, and nothing a running OS prints can be mistaken for bootrom markers. A handshake that does not complete within 15 s gets one fresh cycle. Verified on hardware: - hi3516ev300 behind a cloud-backed Sonoff Micro USB relay, through a local HTTP bridge: `DEFIB_POWER_TYPE=http ... defib agent upload -c hi3516ev300 --power-cycle` caught the bootrom on the first cycle; DDR step, SPL and agent uploaded, agent READY, `agent info` answered. - GK7205V500 behind a Tasmota 14.3 mains plug: the Backlog cycle reboots the board (vendor U-Boot -> Linux boot log captured read-only), and `defib burn -c gk7205v500 --power-cycle` completed the full upload once the V500 handshake fix in the next commit was in place.
The V500 handshake wrote one 14-byte frame (~1.2 ms at 115200) and then blocked up to 100 ms waiting for a 14-byte reply that had to start exactly at the read boundary. The line sat idle ~99% of the time, and the bootrom only listens for a few tens of ms after reset before it falls through to flash boot. On a board with healthy NAND that was a lottery, lost most of the time — it is why catching a GK7205V510 on a known-good flash "worked once, then failed three times in a row". Now each iteration writes an 8-frame burst (~10 ms), drains whatever has arrived without blocking, and searches the accumulated stream for the BD 00 reply wherever it lands — after boot noise or split across reads. Once it is found, the replies to the rest of the burst are allowed to settle and are flushed, so they cannot be read as ACKs in the HEAD stage. Verified on a GK7205V500 with GD5F1GM7 NAND and working vendor firmware, power-cycled by a Tasmota plug: `defib burn -c gk7205v500 -f u-boot-gk7205v500-nand.bin --power-cycle` caught the bootrom on the first cycle and uploaded HEAD, AUX and the 292 KB boot image (34 s). Before the change the same command never caught it in a 120 s run.
PR Summary by QodoAdd HTTP power controllers and improve bootrom handshake capture
AI Description
Diagram
High-Level Assessment
Files changed (11)
|
Code Review by Qodo
1.
|
SocketTransport.read() goes straight to the socket, so _buf only ever held bytes pushed back by unread(), and bytes_waiting() reported 0 while data sat in the kernel. Anything that polls before reading was blind on socket://, tcp:// and rack:// links — Transport.drain_input(), the CV6xx handshake, and the V500 handshake from the previous commit, which would blast forever without ever seeing the bootrom's reply. bytes_waiting() now pulls whatever the non-blocking socket already has into _buf. The V500 handshake also drains its TX queue once the reply arrives, before the settle delay and input flush: bursts still queued on the host would otherwise keep reaching the bootrom, and their replies could be read as ACK/NAK bytes in the HEAD stage. (A timed read every pass instead of the bytes_waiting() gate was tried; it reprograms the serial timeout with a tcsetattr per pass and was dropped.) Both found in review (Qodo). A socketpair-backed test fails on the old bytes_waiting() and passes now. Re-verified on the GK7205V510: three consecutive `defib burn --power-cycle` catches, 3/3.
Review (Qodo) of the new power backends:
- tasmota: power_cycle sent "Backlog Power OFF; Delay N; Power ON" and
never checked it happened; Tasmota answers any Backlog with {}. Polling
to verify made it worse: on Tasmota 14.3 a command sent during the
Delay cuts the delay short (relay back ON at ~2 s instead of ~4.5 s),
and the mains-powered GK7205V510 then never booted at all — measured by
timing poll replies against the board's first UART byte. The plug now
uses the base off / wait / on, each step confirmed by its reply.
- agent upload --power-cycle passed "" as the port, which RouterOS
cannot resolve. It now takes --poe-port and falls back to comment
discovery from the serial device name, like burn.
- Vectis can only pulse, and rejects power_off/power_on. Controllers now
declare supports_independent_power; for pulse-only ones the handshake
starts before the pulse on proactive protocols and after it on the
reactive HiSilicon-standard one (whose 0x20 markers a still-running
OS could fake), matching RecoverySession. A live RFC 2217 transport is
attached so Vectis pulses over the UART connection defib holds.
- http: configured URLs were logged and echoed in errors verbatim. Logs
and errors now show scheme://host/path with user info and query
redacted (also inside echoed response bodies), and user:pass@ in a URL
is sent as Basic auth — urllib would otherwise try to resolve a host
literally named "user:pass@host".
Re-verified on hardware: GK7205V510 behind the Tasmota plug, `defib burn
--power-cycle` 3/3; hi3516ev300 behind the HTTP-bridged Sonoff, `agent
upload --power-cycle` to READY.
Phase 0 of the NAND ECC study for OpenIPC/firmware#2285 / #2519: get the flash agent onto the V500 family, which had no path at all (no SPL stage, no profile JSON by design). **How** - `agent upload -c gk7205v5x0` builds a V500 boot image around the agent. The key area, params and aux (DDR-init) code come from a donor OpenIPC u-boot-xmedia image, auto-downloaded `u-boot-<chip>-nor.bin` or `-f`. The agent goes in the boot-code slot, with the length fields patched (`wrap_v500_payload`). - After a UART download the bootrom runs the boot code **in place at `0x41007000`** (load address + 8 KiB key + 20 KiB aux). It does not use the header's entry field (`0x40707000`). A 144-byte probe in the slot printed `pc=0x41007000 lr=0xad8`. The new `gk7205v500` agent stanza therefore links there, and the wrap checks the link address against the donor's code offset. - `nand_identify()` now uses a table: MX35LF, **W25N01GV**, and **GD5F1GM7** (3.3 V / 1.8 V). An unknown NAND fell into the NOR path, which hung forever in `flash_global_unlock()` polling a NOR WIP bit that a NAND never clears. UART breadcrumbs placed through startup → `main` → `flash_init` pinned it down. - `--power-cycle` / `--poe-port` go through the shared helper from #144 (proactive ordering). **Verified on hardware** - GK7205V510 (chip ID `0x72050510`, GD5F1GM7 NAND, working vendor firmware), Tasmota-cycled: `defib agent upload -c gk7205v510 --power-cycle` → READY. `agent info` reports JEDEC `00c891`, 128 MiB, 128 KiB blocks. `agent read -a 0x10000000 -s 0x100` dumps the FMC registers (`FMC_CFG 0x1821`, version `0x100`). - hi3516ev300 + W25N01GV: previously misreported as 16 MiB NOR, now 128 MiB NAND. - The stock gk7205v510 donor RAM-boots this board; the gk7205v500 one does not, so `-c` must name the actual chip. **Checks:** pytest 952 passed, ruff, mypy, `make -C agent test`, `all-socs` plus clean `gk7205v500` / `hi3516ev300` builds, JS tests.
Prerequisite for the low-level NAND ECC study of OpenIPC/firmware#2285 and #2519, which needs unattended power cycling on two bench DUTs.
power: drive Tasmota plugs and plain HTTP relays
DEFIB_POWER_TYPE=tasmota(/cm?cmnd=API; off / wait / on, each switch confirmed from the plug's reply) andDEFIB_POWER_TYPE=http(on/off GET URLs, e.g. a local bridge for a cloud-only Sonoff; secrets in the URLs are redacted from logs and errors,user:pass@is sent as Basic auth).agent upload --power-cyclewas ignored on the HiSiliconStandard path. It now powers off, starts the handshake while the camera is dark, powers on, and re-cycles once if the bootrom stays silent for 15 s. Takes--poe-port/ comment discovery for RouterOS; pulse-only controllers (Vectis) are sequenced like RecoverySession.protocol/v500: keep the handshake on the wire continuously
transport/socket:
bytes_waiting()sees unread data_buf, so polling callers (drain_input, the CV6xx and V500 handshakes) were blind onsocket://,tcp://andrack://.Review (Qodo) — all six findings addressed
SocketTransport.bytes_waiting()(root cause, also affected CV6xx/drain_input), with a socketpair test that fails on the old code.--poe-port+ comment discovery.supports_independent_powercapability, pulse ordered by protocol style, RFC 2217 transport attached.Backlog ... Delayis harmful on Tasmota 14.3 — commands sent during the Delay cut it short (~4.5 s → ~2 s off) and the mains-powered camera then never booted, soBacklogwas dropped altogether.flush_output()before settle +flush_input().Verification
pytest944 passed, ruff, mypy,make -C agent test, JS tests.agent upload --power-cycle→ READY.burn --power-cyclecaught the bootrom 3/3 consecutive runs, full 292 KB upload each. Before the V500 fix it never caught it in a 120 s run.