Releases: tostmann/esp-coordinator
Release list
v1.5.73 — Coordinator IEEE byte-order fix (issue #6)
v1.5.73 — Coordinator IEEE byte-order fix (issue #6)
What was wrong
GET_LOCAL_IEEE_ADDR (the NCP command a host uses to read the coordinator's own
64-bit IEEE address) returned the address in big-endian byte order. On the
ZBOSS NCP link IEEE addresses are little-endian, so the host stored the
coordinator's IEEE with all 8 bytes reversed.
Consequence: the host (Zigbee2MQTT / zigbee-herdsman, and any ZBOSS-NCP host)
recorded a reversed, phantom coordinator IEEE and used it as the destination
when it bound devices to the coordinator. Any device that then has to resolve the
coordinator by IEEE — a bound switch (e.g. IKEA E2201/RODRET), or a sensor bound
to report to the coordinator — sends Network Address Requests for an address no
device on the mesh owns, so it never resolves and retries endlessly; its bound
commands/reports are never delivered. Devices addressed by short address 0x0000
(most routine traffic) were unaffected, which is why the symptom looked narrow.
What the fix does
GET_LOCAL_IEEE_ADDR now returns the operational long address in correct
little-endian (wire) order — the same order every other IEEE field already used.
Fixed at the source, so a corrected coordinator IEEE is handed out from now on.
Migration — required for existing installs
The firmware fix prevents the wrong address from being handed out again, but two
copies of the old reversed address live off-chip and must be refreshed:
- Flash v1.5.73.
- Restart your host (Zigbee2MQTT). It re-reads the coordinator IEEE on start,
sees it changed, logsCoordinator address changed, updating to '0x…', and
corrects its own database automatically. No manual database editing needed.
(Verified against zigbee-herdsman end-to-end.) - Re-pair or re-bind the affected devices. A device keeps the old reversed
coordinator address in its own binding table until it is re-paired or its
bindings are removed and recreated. For each device stuck in a request loop or
not delivering bound reports (bound switches, sensors bound to the
coordinator): re-pair it, or remove+recreate its bindings / run Reconfigure.
What is NOT affected
The coordinator's actual on-air identity, PAN ID, network keys and device
associations were always correct — only the value reported to the host was
reversed. No network re-formation is required; the mesh is intact.
ZHA / zigpy-zboss users
zigpy-zboss decodes the IEEE per spec (little-endian, t.EUI64), so the fix is
correct for it as well. Its host-side behavior on a coordinator-IEEE change has
not been bench-verified here; expect the same "re-bind the affected devices" step
and watch the startup log after flashing.
Also in this release
- APSDE request-slot watchdog: reclaims request-table slots left in the executing
state (e.g. after an unconfirmed delivery) so the 16-slot table cannot saturate
and cause network-wide poll timeouts.
v1.5.70 — NWK_LEAVE_IND spec-compliant + ZDO indication trims (#6)
v1.5.70 — ZBOSS-NCP indication layout fixes (issue #6)
Fixes the coordinator's ZDO/NWK indication payloads so they match the ZBOSS NCP Serial Protocol byte layout expected by both host stacks (Zigbee2MQTT adapter:zboss / zigbee-herdsman and zigpy-zboss). Reported in #6 (IKEA E2201 / RODRET).
NWK_LEAVE_IND(0x040b) — previously sent as the raw ZBOSS signal struct, which carries a leading 16-bit short address the protocol does not define here. The host decodedshort_addr ++ ieee[0:6]as the IEEE address and the 7th IEEE byte as the rejoin flag — the "Device leave is from unknown or already deleted device" symptom in #6. Now emitted spec-exact as[ieee(8) | rejoin(1)]. Effect of the old bug: on-air device leaves were never processed (the device stayed in the host database, no leave event).ZDO_DEV_ANNCE_IND(0x020c) andZDO_DEV_UPDATE_IND(0x0215) — trimmed to the spec-exact 11-byte payloads (drop the struct alignment pad / the non-spectc_action+parent_shorttrailing bytes). Both hosts already tolerated the extra bytes, so this is hygiene, not a behaviour change.ZDO_DEV_AUTHORIZED_IND(0x0214) left unchanged (already spec-exact).
Verified end-to-end on test hardware: a real end-device join yields correct DEV_UPDATE/DEV_ANNCE fields, and a device leave yields NWK_LEAVE_IND = [ieee|rejoin] with the true IEEE; the coordinator does not self-wipe across the leave. Firmware rebuilt for all three targets (ESP32-C6 / C5 / H2).
Note: the separate "IKEA E2201 switches stuck in a Network Address Request loop / actions lost" part of #6 is a distinct, sleepy-end-device address-resolution issue still under investigation — not addressed by this release.
v1.5.0 — ESP32-H2 support
ESP32-H2 support
v1.5.0 adds the ESP32-H2 as a third build target alongside the ESP32-C6 (reference platform) and the ESP32-C5 — the same firmware, from one codebase.
Validated end-to-end on real ESP32-H2 silicon: boot + NCP over native USB-Serial/JTAG, 802.15.4 network formation, NVRAM resume across a reboot, and on-air pairing of a real Zigbee device (join → authorize → interview). Independently confirmed beforehand on two further boards and host stacks (Zigbee2MQTT and ZHA), see #5.
Flashing
The web flasher auto-detects the connected chip and serves the matching image: https://install.busware.de/zboss/
Notes for ESP32-H2
- The 1900K application partition requires a 4 MB flash board (e.g. ESP32-H2-MINI-1).
- The H2 has no Wi-Fi, so the Wi-Fi-coexistence build does not apply — it is a pure USB/UART coordinator.
The ESP32-C6 remains the reference platform.
v1.4.63 — coordinator-wipe hardening
v1.4.63 — coordinator-wipe hardening
Critical fix. A routine "remove device" in Zigbee2MQTT could make the coordinator wipe its own network. If you have ever lost your Zigbee network unexpectedly, this is very likely why — please update.
What was wrong
When you remove a device, the host sends a ZDO Mgmt_Leave to the coordinator. The firmware routed that through a raw low-level ZBOSS call which, for an unknown/unresolvable target, made the coordinator perform a network-leave on itself — wiping its live network and NVRAM with no reboot (~2 out of 3 leaves on the bench; independent of the target address or flags). Removing an offline device (which Zigbee2MQTT force-removes) was enough to trigger it.
Fixes in this release
- Leave self-wipe (the root cause). Device-leave now goes through the official esp-zigbee-lib API
esp_zb_zdo_device_leave_req(), which does not self-wipe; a self-addressed leave (dst=0x0000) is refused outright. Verified end-to-end: a real device removed via Zigbee2MQTT is removed correctly and the coordinator keeps its network. - Restore buffer-before-commit gate.
RESTORE_NETWORKnow buffers the full 40 KB image in RAM and structurally validates it before writing anything to flash, so a corrupt/truncated restore is side-effect-free — your existing network and credentials are preserved, no reboot. - Poison-backup guard.
GET_NETWORK_BACKUPrefuses to snapshot a coordinator that is not currently on a network, so a host cannot store an un-restorable backup of an already-blank stack.
Flashing
Web flasher: https://install.busware.de/zboss/ (ESP32-C6 and ESP32-C5). Or esptool write_flash 0x0 with the attached factory image for your chip. Flashing the factory image does not erase your Zigbee network — it lives in a separate flash partition.
Bench-validated end-to-end on ESP32-C6 over USB.
v1.4.0 — ESP32-C5 support (multi-target)
v1.4.0 — ESP32-C5 support (multi-target)
The firmware now builds and runs on the ESP32-C5 in addition to the ESP32-C6, from a single codebase. The web flasher auto-detects your chip and serves the matching image.
What's new
- ESP32-C5 support. Full ZBOSS NCP coordinator on C5 silicon — USB-Serial/JTAG transport, 802.15.4 network formation and NVRAM resume across reboot, validated end-to-end with
zigpy-zboss. Memory headroom on the C5 (384 KB SRAM): the coordinator links with ~189 KB of internal RAM free and uses only 34 % of the app partition. - Per-target NCP-UART pin defaults — C5: TX=GPIO23, RX=GPIO24 (the C6 defaults GPIO22/23 land on the C5's SPI-flash pins). Override via
menuconfig→NCP_UART_*. - Separate per-chip factory images: the C5's second-stage bootloader sits at flash
0x2000, the C6's at0x0. Each image is still written from0x0. - Build a target with
idf.py set-target esp32c6|esp32c5(the default config is no longer pinned to one chip; CMake fails loudly if no target is set).
Status / caveats
- The C6 remains the reference platform. C5 support is validated on one board (rev v1.0) so far.
- The C5 NCP-UART pin defaults are datasheet-safe on standard Espressif C5 boards but have not been hardware-tested; third-party board pinouts may need an override. The default USB path needs none of this.
Flash
Web installer: https://install.busware.de/zboss/ (auto-detects C6/C5)
CLI:
esptool.py --chip esp32c5 write_flash 0x0 binaries/factory-c5.bin # ESP32-C5
esptool.py --chip esp32c6 write_flash 0x0 binaries/factory.bin # ESP32-C6
v1.3.57 — GET_NWK_KEYS for zigpy-zboss 2.0.2 / ZHA compatibility
ZHA / zigpy-zboss 2.0.2 compatibility
zigpy-zboss 2.0.2 changed how the network key is read at startup and now hard-aborts the connection when GET_NWK_KEYS returns no usable key — where 2.0.1 silently tolerated the unimplemented command. As a result, ZHA (and zigpy-zboss 2.0.2 generally) could no longer resume an already-formed network against this firmware.
This release implements GET_NWK_KEYS (0x001e), returning a fixed, recognizable placeholder key so the resume path succeeds. The NCP holds the real key and performs all cryptography itself; the host-side key is bookkeeping only — a ZHA network backup is not restorable against this firmware regardless (the frame-counter restore command is not implemented; use GET_NETWORK_BACKUP / RESTORE_NETWORK for a full NVRAM snapshot). Zigbee2MQTT / herdsman never queries GET_NWK_KEYS and is unaffected.
Validated end-to-end: zigpy-zboss 2.0.2 resume + blank auto-form over USB and over a TCP-bridged link, full Home Assistant / ZHA coordinator init, and a live device pairing.
v1.3.56 — host-controlled permit join, truthful PAN getter, boot-guard safe mode
Three independent hardenings, each root-caused and validated on real hardware.
Host-controlled permit join (BOOT-1)
The coordinator no longer opens its network for joining without being told to. Previously, BDB network steering ran at every boot and after each successful formation — steering on a formed network broadcasts Mgmt_Permit_Joining(180 s), so the network could be open for joining for 3 minutes after a plain reboot with no host attached. The boot path now runs BDB initialization only (NVRAM resume, or idle when factory-new), and permit join is exclusively driven by the host (NWK_PERMIT_JOINING / ZDO_PERMIT_JOINING_REQ, i.e. zigbee2mqtt's permit_join).
Validated on-air: a continuously steering factory-new probe device could not join for 220 s after a coordinator reboot, then joined 4 seconds after a host permit-join command.
Note: any device already ON the network may still broadcast a 180 s permit-join window after its own steering (e.g. when it rejoins) — that is spec-compliant Zigbee behaviour and unchanged.
Truthful PAN getter (PAN-1)
SET_PAN_ID on a formed network is now rejected (OPERATION_FAILED) instead of silently rewriting only the cache that the getters read. Hosts that write their desired settings after forming (zigpy-zboss does) previously saw their requested PAN echoed back by every getter while the device actually ran — and persisted — a different one; after the next reboot the PAN appeared to have changed, which can trip a host's "network changed, reinitialize" logic. Setting the PAN before formation (zigbee-herdsman's flow) keeps working and is honoured at formation time.
Boot-guard safe mode (WEDGE-1)
New resilience layer for a failure class where a power event can leave the stick with a permanently silent application (suspected hang early in stack init). The firmware now counts boots that die before the Zigbee stack comes up (a 30 s deadline converts silent hangs into countable reboots); after 3 consecutive failures it boots into a sticky safe mode: the serial link comes up without the Zigbee stack, GET_MODULE_VERSION and NCP_RESET are served, and everything else answers GENERIC_BLOCKED — the stick stays reachable and diagnosable instead of appearing dead, and NCP_RESET with the factory-reset option doubles as a remote recovery path. Safe mode exits on a host-commanded reset, a true power cycle, or a 10-minute automatic retry.
Validation
- On-air permit-gate probe (new
test/permit_gate_check.py): closed for 220 s post-boot, join 4 s after host permit - Formation PAN now equals post-reboot PAN across repeated reboots; zigpy-zboss 2.0.0 tolerates the rejected post-formation write
- Safe-mode chain exercised end-to-end with an artificial pre-stack hang build: deadline-reboot accumulation, sticky safe mode across port reopens, blocked getters, supervised exit
- Regression on the final build: zigpy-zboss 2.0.0 full startup (network resume) and the v1.3.50 duplicate-drop suite, all pass
Flash via the web flasher at install.busware.de/zboss or esptool write_flash 0x0 binaries/factory.bin. Network settings (NVRAM) are preserved across an app update; a backup via the web flasher before any flash is recommended regardless.
v1.3.50 — link-layer duplicate drop
Link-layer duplicate drop (DUP-1)
Fixes handling of retransmitted host data frames in the ZBOSS NCP serial protocol layer.
The bug: the header's retransmit bit was treated as NACK on every frame type. A host that re-sent a data frame after an ACK timeout (stable packet_seq + retransmit flag — exactly what zigpy-zboss ≥ 2.0.0 and zigbee-herdsman do) got its retransmission silently discarded: no re-ACK, no execution. A host retry could therefore never succeed — and an unflagged duplicate would have been executed twice. Latent on USB/TCP (frame loss is rare there), real on lossy UART links.
The fix: the firmware now remembers the sequence number of the last accepted host data frame. A flagged retransmission with a matching sequence is re-ACKed but not executed again (lost-ACK case); a flagged frame with an unseen sequence is accepted as a first delivery (lost-frame case).
Validation (on real hardware, ESP32-C6):
- New on-wire probe
test/dup_drop_check.py: 4/4 PASS — duplicate re-ACKed and not re-executed, fresh flagged frame dispatched, link continues normally - zigpy-zboss 2.0.0 full-startup runs against this build: restart on a formed network and blank-NCP
auto_form, both SUCCESS - New
test/routing_gate_check.py: 4/4 PASS — isolated validation of the v1.3.45 RX routing gate (interface routing flips only on a frame signature; port-prober garbage, e.g. ModemManager AT probes, is discarded; signature split across reads handled)
Who should update: anyone using the UART transport (TTL header / ser2net setups) or driving the coordinator with zigpy-zboss ≥ 2.0.0 (ZHA path). USB users with Zigbee2MQTT are unaffected in practice but get the hardening for free.
Flash via the web flasher at install.busware.de/zboss or esptool write_flash 0x0 binaries/factory.bin. Network settings (NVRAM) are preserved across an app update; the web flasher's backup feature is recommended before any flash regardless.
v1.3.49 — ZBOSS thread-safety fix
v1.3.49 — ZBOSS thread-safety fix (recommended upgrade)
Bugfix release. No new features, no protocol changes — existing setups upgrade by reflashing, network data is preserved.
The fix
The NCP command dispatch called ZBOSS APIs from the application task without holding the ZBOSS port-layer lock (zb_esp_osif_lock_acquire — the same rule esp-zigbee-lib enforces via esp_zb_lock_acquire(): the ZBOSS core is not thread-safe). The two tasks could race inside the stack's interrupt-bracketing primitive, corrupting the FreeRTOS critical-section nesting count and crash-rebooting the coordinator with:
assert failed: vPortExitCritical port.c:618 (port_uxCriticalNesting[0] > 0)
This design flaw is inherited from the original codebase and affects all previous releases. It is timing-dependent: it reproduced reliably under device-interview bursts on the WiFi-coexistence development branch, and is rarer (but equally present) on plain USB/UART builds — if your coordinator ever rebooted spontaneously during device pairing or interviews, this was a likely cause.
The dispatch is now serialised against the stack task with the port-layer lock. Validated with repeated device-interview stress runs (previously ~80% crash rate on the reproducer, now 0 failures) plus an 8-hour continuous-load soak.
Also in this release
- Boot log line
BOOT: reset reason: N(visible on debug builds; aids crash diagnosis on boards whose eFuses suppress the ROM reset banner) - Event-queue hardening: dropped-event diagnostics + bounded retry on the host-RX path
- Release binary built with the documented UART default pins (TX=GPIO22, RX=GPIO23)
Flashing
Web flasher: https://install.busware.de/zboss/ — or esptool --chip esp32c6 write_flash 0x0 factory.bin. Reflashing without erase preserves the formed network and paired devices.
v1.3.45 — UART Host Transport, XIAO ESP32-C6 Support, NCP_RESET Fixes
New: UART host transport — run the coordinator without USB
The NCP frame stream is now additionally available on a hardware UART, in parallel to USB-Serial/JTAG (#2, discussion #1):
- Default pins: coordinator TX = GPIO22, RX = GPIO23 (= D4/D5 on the Seeed XIAO ESP32-C6), 115200 8N1, no flow control — configurable via
menuconfig→ Zigbee Network Co-processor. - Hook the coordinator straight onto an OpenWRT router's TTL header, a Raspberry Pi GPIO header UART, or a USB-UART adapter / ser2net box. See the README wiring guide.
- Both interfaces stay live: responses follow whichever interface last delivered a valid NCP frame. The routing flip is signature-gated, so port probers (e.g. ModemManager's AT probes) can neither steal the link nor corrupt the stream.
- Bonus: on a UART the host's port survives the coordinator's resets — the USB-CDC reset quirks disappear entirely on this path.
- Validated end-to-end with zigbee2mqtt (
adapter: zboss) and zigpy-zboss: network formation, device pairing, full 40 KB NVRAM backup.
New: Seeed XIAO ESP32-C6 external antenna option
CONFIG_NCP_XIAO_EXT_ANTENNA routes RF to the XIAO's external U.FL connector (GPIO3/GPIO14 RF-switch control per Seeed's reference). Default off; the default build now leaves the switch pins in a defined passive INPUT state — internal antenna on the XIAO, no-op on every other board.
Critical fixes
- NCP_RESET over USB no longer freezes the chip until power-cycle. The legacy USB "phy detach" register writes hard-freeze the ESP32-C6 on current ESP-IDF builds. They are removed; NVRAM/factory erase now runs at the start of the next boot, before the radio is active.
- NCP_RESET protocol semantics fixed: no more pre-reboot "OK" response. Hosts treated that response as "reset complete" and continued immediately — everything sent before the actual reboot died mid-flight. This was the root cause of the long-standing "Backup failed … after 10000ms" failures after factory-reset / restore / formation. The boot-ready frame after the reboot is now the response, so reset → re-form and restore → resume flows complete deterministically.
- Full-spec
NWK_FORMATIONresponse (NWKAddr + PANID + page + channel) — fixes formation against hosts parsing the complete layout, notablyzigpy-zboss≥ 2.x / Home Assistant ZHA (hardware-validated against kardia-as/zigpy-zboss#73). - Link-layer sequence race closed (duplicate
packet_seqfrom concurrent ACK/data paths → sporadic "Unexpected packet sequence" on the host).
Improvements
GET_NETWORK_BACKUPserves all chunks from a point-in-time RAM snapshot — consistent 40 KB images, single bulk flash read.- README: UART wiring guide, XIAO pin mapping, refreshed ZHA status.
Flashing: Web flasher (updated to v1.3.45) or esptool.py --chip esp32c6 write_flash 0x0 esp-coordinator-esp32c6-factory.bin.