Skip to content
ilia-aePublic

About

πŸ§©πŸ’§πŸ“‘ ZigNode β€” a modular ESP32 Zigbee/Wi-Fi node. Safety-first firmware you extend with modules (flagship: auto-watering pump). Home Assistant / Zigbee2MQTT Β· boards ESP32-H2 & C6 Β· OTA + rollback

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Latest commit

Β 

History

93 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
ZigNode β€” modular ESP32 Zigbee/Wi-Fi node

CI ESP Component Registry firmware ESP-IDF license SemVer PRs welcome

ESP32-C6 ESP32-H2 RISC-V C CMake

Zigbee 3.0 802.15.4 Wi-Fi 6 coexistence OTA mDNS SNTP

BLE 5.3 Thread Matter

Home Assistant Zigbee2MQTT host tests pre-commit signed commits OpenTimestamps Contributor Covenant

A modular ESP32 Zigbee/Wi-Fi node β€” safety-first firmware you extend with modules.
Its flagship module drives a DC pump (MOSFET) or a mains pump (opto-relay) and auto-waters from a programmable rule engine β€” controllable from Home Assistant / Zigbee2MQTT. The pump is just one module; the core runs on multiple boards, peripherals compiled in per board.


✨ Highlights

🧩 Modular core πŸ’§ Safety-first pump πŸ“‘ Native Zigbee 3.0
Module registry + event bus β€” add a peripheral in one file, no core edit. Parked OFF before radios, hard max-runtime watchdog, dry-run gate, min-off cooldown. Router for Home Assistant & Zigbee2MQTT: pump / auto-water / soil endpoints + 0xFC00 config.
πŸ›œ Wi-Fi web panel πŸ–₯️ Expansion peripherals πŸ”„ OTA + rollback
C6 only: live panel at zignode.local β€” telemetry + a local update window. OLED Β· buzzer Β· PCF8563 RTC Β· wear-safe microSD event log (RAM-buffered). Dual-slot Zigbee / HTTP OTA, health-gated with bootloader rollback.

Built with ESP-IDF v5.4.4 (β‰₯5.4.1 required for the ESP32-H2 v1.2/ECO5 silicon β€” see docs/BUILD.md) and the classic esp-zigbee-lib 1.6.x (esp_zb_*) API.

Supported boards

Board Chip Extras Build
M5Stack NanoH2 ESP32-H2 Zigbee-only, moisture ADC, IR tools/build_board.sh m5nanoh2
Seeed XIAO ESP32-C6 ESP32-C6 + Wi-Fi + web panel + mDNS tools/build_board.sh xiao_c6
XIAO C6 + Expansion Base ESP32-C6 + OLED, buzzer, RTC, microSD log tools/build_board.sh xiao_c6_exp

Board selection is a Kconfig choice (decoupled from the chip), and each board takes an optional :release/:beta/:debug channel. See docs/BOARDS.md for the capability matrix + pin maps and docs/MODULES.md for how a peripheral module plugs in.

What else you need

The board alone is not a system β€” ZigNode is a Zigbee device, so it needs a network to join and something to talk to.

Required? Notes
A Zigbee 3.0 coordinator Yes Any standard stick β€” SONOFF ZBDongle-E/P, UZG-01, ConBee II/III, TI CC2652. Developed and tested against a UZG-01 (zstack).
Zigbee2MQTT + an MQTT broker Yes (this path) Z2M is the supported integration. It needs a broker β€” Mosquitto is the usual choice (the HA "Mosquitto broker" add-on works). Install the external converter from docs/zigbee2mqtt/.
or Home Assistant ZHA alternative ZHA needs no MQTT broker, and standard entities (pump on/off, soil humidity) work out of the box β€” but the 0xFC00 config cluster needs a ZHA quirk that this project does not ship, so tuning/rules would be unavailable. Z2M is recommended.
Home Assistant Optional Not required to run, but it's where the automations and the ready-made watering blueprint live β†’ docs/home-assistant/.
Pump + driver electronics Yes A DC pump with a logic-level N-MOSFET, or a mains pump with an opto-isolated relay board, plus a PSU, a flyback diode and the pull-up/pull-down resistors. Read Safety before wiring.
Soil moisture sensor Optional An analog capacitive probe on the free Grove pin (H2), or separate Zigbee soil sensors bound directly to the node (cluster 0x0408).
Wi-Fi network Optional, C6 only Only for the web panel / SNTP / Wi-Fi OTA. Zigbee works without it.
ESP-IDF v5.4.1+ To build Only if you build from source. β‰₯5.4.1 is mandatory on ESP32-H2 v1.2/ECO5 silicon β€” see docs/TROUBLESHOOTING.md.

Radios & protocols

The ESP32-C6 is a multi-protocol radio, so it's worth being explicit about what the silicon can do versus what this firmware actually implements:

Protocol ESP32-H2
M5Stack NanoH2
ESP32-C6
XIAO / XIAO + Base
Status in ZigNode
Zigbee 3.0 βœ… Router (ZCZR) βœ… End Device (non-sleepy) Shipped β€” classic esp_zb_* API on esp-zigbee-lib 1.6.8: BDB steering on a pinned channel, min-join-LQI gate, multi-endpoint model + the 0xFC00 config cluster.
IEEE 802.15.4 βœ… native radio βœ… native radio Shipped, owned by the Zigbee stack. Called directly only for RSSI/LQI link telemetry β€” no raw 802.15.4 networking.
Zigbee OTA 0x0019 βœ… client on EP10 βœ… client on EP10 Shipped β€” dual-slot images + bootloader rollback behind a health gate.
Wi-Fi 6 β€” 802.11ax (HE) β€” no Wi-Fi silicon βœ… STA Shipped on C6, and measured β€” the panel reports esp_wifi_sta_get_negotiated_phymode(), and on a Wi-Fi 6 AP it reads HE20: the link really is running 802.11ax, not merely talking to an AP that could. Falls back to b/g/n otherwise. Station mode comes up after Zigbee joins and coexists with 802.15.4: web panel, mDNS, SNTP, ICMP link monitor, HTTP firmware upload. Not done: explicit iTWT power-save tuning. No SoftAP, no ESP-NOW, no TLS.
Bluetooth LE 5.3 βšͺ hardware-ready βšͺ hardware-ready Not implemented β€” the controller is compiled out of both shipped builds.
Thread 1.3 βšͺ hardware-ready βšͺ hardware-ready Not implemented β€” the single 802.15.4 PHY is exclusively owned by Zigbee, so Thread is mutually exclusive with it.
Matter βšͺ over Thread βšͺ over Wi-Fi or Thread Roadmap only β€” docs/MATTER.md describes a separate esp-matter binary; nothing Matter is in the default build.

βœ… shipped in this firmware Β· βšͺ hardware-ready β€” the silicon supports it, ZigNode does not implement it Β· β€” not present in the silicon

Hardware β€” reference board (M5Stack NanoH2)

Pin maps for all three boards β€” including the XIAO C6 and the Expansion Base peripherals β€” are in docs/BOARDS.md. The table below is the original H2 reference wiring.

Board: M5Stack NanoH2 Dev Kit Β· SoC ESP32-H2FH4S (RISC-V, 96 MHz, 4 MB flash, 320 KB SRAM). Radios: BLE 5 + IEEE 802.15.4 (Zigbee 3.0 / Thread / Matter-over-Thread). No Wi-Fi. Programming and console are over the SoC's native USB-Serial-JTAG (no UART bridge). Pin map verified against schematic SCH_M5NanoH2_v0.0.3_2025_08_20.

Silkscreen GPIO Role in this firmware
G1 (Grove white) GPIO1 Transistor/MOSFET gate β€” active-HIGH (HIGH = pump ON). Moisture ADC (ADC1_CH0) when in relay mode.
G2 (Grove yellow) GPIO2 Relay IN β€” active-LOW (LOW = pump ON). Moisture ADC (ADC1_CH1) when in transistor mode.
5V / GND (Grove) β€” Sensor / relay-module power + ground.
G3 GPIO3 On-board IR LED (PIN_IR) β€” NEC transmit, drivable from HA via ir_nec.
G4 GPIO4 On-board blue LED (active-HIGH) β€” per-command activity blink.
G9 GPIO9 BOOT / user button (active-LOW, external pull-up).
G10 GPIO10 WS2812 power-enable β€” drive HIGH to power the RGB pixel.
G11 GPIO11 WS2812 data (single RGB pixel).

G2 = GPIO2 (MTMS) is a strapping pin, but it is only sampled at reset; using it as a runtime relay output is fine. The external resistors below keep it safe through the power-on-reset window.

Features

πŸ’§ Pump & safety

  • Three drive modes, switchable at runtime β€” transistor (DC via a logic-level N-MOSFET, active-HIGH), relay (AC/mains via an opto-isolated relay, active-LOW), or both (the default β€” a pump wired to either path just works). A single-driver mode frees the other pin for the on-board moisture sensor.
  • A safety chain that does not depend on the network. The pump is parked OFF before the radios start; every ON goes through one choke-point that arms a hard max-runtime watchdog (default 300 s, ceiling 3600 s), enforces a min-off cooldown, and applies a fail-closed dry-run gate (stale/faulty moisture β‡’ "unsafe, do not run"). A firmware hang trips the Task Watchdog and the pads return to a safe state.
  • Manual dose β€” a one-shot "run N seconds" from the panel or Home Assistant that bypasses the automation cooldown but never the safety limits.

πŸ—“οΈ Watering rule engine

  • 8 independent rules, each with a trigger β€” time of day (with day-of-week mask and a catch-up window), every N minutes, moisture below X, or on demand β€” and an action: a single pulse, or a soak loop (pulse β†’ wait β†’ re-check, capped by max pulses).
  • Budgets and interlocks: per-rule minimum gap, a global daily seconds budget, a max runs-per-day, and a moisture-staleness limit. The engine is fail-CLOSED β€” if it can't prove watering is safe and due, it doesn't water.
  • 14 human-readable veto reasons ("soil is not dry enough", "moisture reading is stale", "daily budget spent", …) so an idle pump is never a mystery.
  • Rules are editable both over Zigbee (cursor + commit attributes) and in the web panel.

πŸ“‘ Zigbee 3.0

  • Native Zigbee for Home Assistant and Zigbee2MQTT β€” endpoints for Pump (EP10), Auto-water (EP11) and Soil moisture (EP13), plus the 0xFC00 config cluster surfaced as HA numbers and a pump_mode enum.
  • Direct sensor binding β€” bind an external Zigbee soil sensor (cluster 0x0408) straight to the node, so auto-watering keeps working even if the coordinator is down.
  • Link telemetry β€” parent address, LQI/RSSI, parent poll interval, round-trip time to the coordinator, and "last seen by coordinator", all readable live.

πŸ›œ Wi-Fi & the web panel Β (ESP32-C6)

The C6 is a Wi-Fi 6 (802.11ax) part and the station uses it β€” associating in 11ax with a Wi-Fi 6 AP, falling back to b/g/n otherwise, and reporting the negotiated PHY in the panel. Wi-Fi comes up after Zigbee joins, and the two share one radio (see the coexistence readout below).

A self-hosted panel at http://zignode.local/ (mDNS), open as a self-closing maintenance window by default or permanently on, with optional HTTP Basic auth β€” 12 REST endpoints across 7 sections:

  • Live pump card β€” animated running state with an elapsed/remaining countdown, plus ON / OFF / timed-dose controls.
  • Watering engine β€” automation toggle, engine state, the current veto reason, soil moisture with reading age, seconds used today, lifetime pumping time and estimated Wh.
  • Rule editor β€” all 8 rules, every field, day-of-week pickers, and "run this rule now".
  • Diagnostics β€” CPU load, chip temperature, heap (free / minimum-ever / largest block), uptime, decoded reset reason, and the event log.
  • Radio coexistence readout β€” Wi-Fi and Zigbee share one 2.4 GHz radio; the panel shows the effective time-share (ED poll interval) and flags sharing OK vs contended.
  • Network β€” SSID, BSSID, channel/PHY, RSSI, clickable IP / gateway / mDNS links, link uptime, connect/drop counters, and a gateway ping with min/avg/max and loss %.
  • Antenna switch β€” flip between the on-board ceramic antenna and the external u.FL connector from the browser.

πŸ”„ Firmware updates β€” two independent paths

  • Zigbee OTA β€” dual-slot (ota_0/ota_1) updates from the coordinator.
  • Wi-Fi OTA β€” drop a .bin into the panel; it is streamed straight into the inactive slot with the pump locked for the duration of the flash, then the boot partition is switched.
  • Integrity-checked β€” the panel hashes the .bin in the browser and declares it via X-SHA-256; the device hashes every byte it accepts and aborts before the image can become bootable on any mismatch. GET /api/status reports the running image's ELF SHA-256, so you can confirm exactly which build is executing β€” a version string can't tell two "2.0.0"s apart.
  • Health-gated rollback β€” a new image must prove itself or the bootloader rolls back to the previous one, so a bad update can't leave a pump controller bricked.

⏱️ Three time sources

ZCL Time from the Zigbee coordinator, SNTP over Wi-Fi, and a battery-backed PCF8563 RTC that seeds the clock at cold boot. Time-of-day rules simply refuse to run without a clock. The panel shows the active source, UTC offset, last sync and last correction.

πŸ–₯️ Expansion peripherals Β (XIAO C6 + Expansion Base)

SSD1306 OLED status screen Β· passive buzzer Β· PCF8563 RTC Β· microSD event log that is RAM-buffered, batched and rotated so it never wears out internal flash (off by default β€” its SPI pins overlap the pump pads; relocate them via Kconfig to enable).

πŸ“‹ Diagnostics & local control

  • Persistent event log β€” a wear-safe ring (boot/reset cause, pump on/off, auto-water cycles, mode changes, IR sends, factory reset, faults, OTA) in a dedicated evtlog flash partition. It is dumped to the serial console at every boot, readable over Zigbee (0xFC00 cursor attrs 0x000B–0x000E), and rendered in the panel.
  • Button gestures β€” short = toggle pump, double = toggle automation, long β‰₯3 s = factory reset / network leave β€” plus a status LED (WS2812 on H2, single LED on C6).
  • IR NEC transmitter (H2) β€” write (address << 16) | command to the 0x000A attribute to blast a frame straight from a Home Assistant automation.

🧩 Modular by design

Every feature above is a module in one explicit, ordered registry with an event bus. Adding a peripheral is a file + a descriptor + one gated line β€” never an edit to the core. Board selection is a Kconfig choice, decoupled from the chip target. See docs/MODULES.md.

The 2-pin moisture constraint

The Grove port only exposes G1 and G2 as user signals. In the two single-driver modes the moisture sensor uses whichever Grove pin the active mode does not drive: relay mode pumps on G2 and reads moisture on G1 (ADC1_CH0); transistor mode pumps on G1 and reads moisture on G2 (ADC1_CH1). The default both mode drives both pins, so no pin is free and the moisture sensor is auto-disabled β€” pick a single-driver mode if you want on-board moisture sensing. Do not hot-swap pump_mode at runtime with a sensor connected to a pin the new mode will drive; the firmware re-points the ADC and disables the sensor rather than collide with the pump pin.

Quick start

ESP-IDF v5.4.1+ (pinned: v5.4.4). Build a board profile β€” it picks the chip target for you and builds into its own isolated directory:

tools/build_board.sh m5nanoh2              # or: xiao_c6 Β· xiao_c6_exp
tools/build_board.sh xiao_c6_exp:release   # optional :release Β· :beta Β· :debug channel
tools/build_board.sh all                   # every board, sequentially
idf.py -B build.xiao_c6_exp -p <PORT> flash monitor

Don't run idf.py set-target in the working tree β€” it regenerates sdkconfig and wipes your Wi-Fi credentials. build_board.sh exists precisely to avoid that; see docs/BOARDS.md.

The managed Zigbee/LED components (esp-zigbee-lib ~1.6.0, esp-zboss-lib ~1.6.0, led_strip ^2.5.0) are pulled automatically at build time. If the board doesn't enumerate for flashing, hold the button (GPIO9) while plugging in USB to enter download mode.

Full build/flash/OTA details: docs/BUILD.md.

Pairing / Home Assistant

Put a Zigbee coordinator into join mode, then trigger commissioning on the device (fresh flash auto-joins; otherwise long-press the button to factory-reset and re-join). The three endpoints surface as switches and a humidity sensor; the 0xFC00 config attributes (max_runtime_s, thresholds, calibration, pump_mode, ir_nec, …) surface as numbers plus a pump_mode enum.

  • Zigbee2MQTT: drop in the external converter at docs/zigbee2mqtt/zignode.js.
  • Home Assistant: entity map and the auto-water blueprint live under docs/home-assistant/.
  • Endpoint / cluster / attribute reference and the pairing walkthrough: docs/ZIGBEE.md.

Safety

Read this before wiring a real pump.

  • Mains through the relay path only. Switch AC/mains loads exclusively with an opto-isolated relay board on G2 (drive IN from 3V3, power the coil from 5V). Never switch mains with a bare MOSFET. DC pumps (5/12/24 V) go on the transistor path.
  • Boot-safe OFF is enforced, but external resistors are mandatory. Add a 10 kΞ© pull-up from G2 β†’ 3V3 (relay reads de-energized during the reset Hi-Z window) and, for the transistor path, a 100 Ξ© gate series resistor + 10 kΞ© gate pull-down (G1 β†’ GND) so the FET is OFF at boot. Without these, the pump can twitch on during reset.
  • Flyback diode across any DC pump/motor (cathode to +V). Use a true logic-level N-MOSFET (AO3400 / SI2302 / IRLML0030).
  • The firmware defends you in software too: pump parked OFF before radios init, a single pump_set() choke-point, a hard max-runtime cutoff, min-off cooldown, a dry-run gate, and a Task Watchdog that resets the MCU (returning the pads to Hi-Z) on a hang. None of this replaces the resistors and the flyback diode.

See docs/DESIGN.md Β§1–2 for the full wiring and control-invariant spec.

Repo layout

zignode/
β”œβ”€β”€ CMakeLists.txt              project cmake (project zignode)
β”œβ”€β”€ partitions.csv             dual-OTA (ota_0/ota_1) + zb_storage/zb_fct + evtlog layout
β”œβ”€β”€ sdkconfig.defaults         Zigbee ZCZR defaults, 4 MB flash, OTA rollback ON
β”œβ”€β”€ main/
β”‚   └── app_main.c             six lines: zignode_run(&cfg) β€” all logic is in the component
β”œβ”€β”€ components/zignode/     THE reusable component (esp32h2 + esp32c6)
β”‚   β”œβ”€β”€ include/zignode.h   the entire public API surface
β”‚   β”œβ”€β”€ Kconfig                CONFIG_ZIGNODE_* feature gates, target-aware defaults
β”‚   β”œβ”€β”€ idf_component.yml      registry metadata + pinned deps (esp-zigbee-lib, led_strip)
β”‚   β”œβ”€β”€ examples/              minimal app that consumes the component
β”‚   └── src/
β”‚       β”œβ”€β”€ zignode.c       boot-safe init order, wires the modules together
β”‚       β”œβ”€β”€ board.c            per-board early init (C6: RF switch + antenna β€” load-bearing)
β”‚       β”œβ”€β”€ pump_control.c     THE choke-point: mode, max-runtime, min-off, dry-run, OTA lock
β”‚       β”œβ”€β”€ watering_program.c rules/profiles/limits model, versioned CRC'd NVS blob
β”‚       β”œβ”€β”€ watering_engine.c  evaluates the program and drives the pump β€” fail-CLOSED
β”‚       β”œβ”€β”€ moisture.c         ADC oneshot on the free Grove pin, calibration, %
β”‚       β”œβ”€β”€ status_led.c       WS2812 (H2) or single active-low LED (C6)
β”‚       β”œβ”€β”€ button.c           short=toggle pump Β· double=automation Β· long=factory reset
β”‚       β”œβ”€β”€ nvs_store.c        persist pump_settings_t; factory reset
β”‚       β”œβ”€β”€ event_log.c        wear-safe persistent event ring (evtlog partition)
β”‚       β”œβ”€β”€ zigbee_device.c    endpoints, 0xFC00 (incl. rule editor), reporting, OTA client
β”‚       β”œβ”€β”€ ota_guard.c        health gate: confirms a new image, or lets it roll back
β”‚       β”œβ”€β”€ ir_blaster.c       NEC transmit over RMT (H2 only)
β”‚       └── private_include/   everything that is not the public API
β”œβ”€β”€ tools/make_ota.py          wraps a build into a ZCL OTA image for a LOCAL index
└── docs/
    β”œβ”€β”€ DESIGN.md              single source of truth (hardware + firmware contract)
    β”œβ”€β”€ ZIGBEE.md              endpoints, clusters, every 0xFC00 attribute
    β”œβ”€β”€ OTA.md                 private updates: rollback, health gate, pump interlock
    β”œβ”€β”€ BUILD.md               build / flash
    β”œβ”€β”€ WIRING.md              pump wiring, sensor, external passives
    β”œβ”€β”€ TROUBLESHOOTING.md     what actually blocked the join (ECO5 + IDF < 5.4.1)
    β”œβ”€β”€ REUSE.md               reuse map: what we copy vs. clean-room reimplement
    β”œβ”€β”€ zigbee2mqtt/           Z2M external converter + OTA index template
    └── home-assistant/        on-device program package + HA-driven blueprint

Reuse & licensing

This firmware is built on permissively licensed Espressif examples (esp-zigbee-sdk and esp-idf peripheral examples β€” mostly CC0-1.0 / Unlicense, ADC and managed components Apache-2.0), version-matched to the pinned esp-zigbee-lib 1.6.x. Board pin facts are ported from M5Stack's MIT-licensed docs. The domain architecture (multi-endpoint model, safety watchdog, moisture control loop) is clean-room reimplemented: GPL-3.0 and unlicensed community projects were consulted as design references only, never copied β€” copying them would force this firmware to their license.

Every file copied verbatim keeps its original upstream SPDX-License-Identifier header. Apache-2.0 origins are listed in THIRD_PARTY_NOTICES.md. The full copy-vs-write decision table is in docs/REUSE.md.

License

MIT Β© 2026 Ilia Arestov. See LICENSE. Third-party components pulled at build time (Espressif ESP-IDF, esp-zigbee-sdk / esp-zboss-lib) remain under their own licenses; see COPYRIGHT and THIRD_PARTY_NOTICES.md.

About

πŸ§©πŸ’§πŸ“‘ ZigNode β€” a modular ESP32 Zigbee/Wi-Fi node. Safety-first firmware you extend with modules (flagship: auto-watering pump). Home Assistant / Zigbee2MQTT Β· boards ESP32-H2 & C6 Β· OTA + rollback

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages