ilia-ae/zignode

2.0.0

Latest
uploaded 5 hours ago
ZigNode — a modular ESP32 Zigbee/Wi-Fi node. One ESP-IDF component with a board-selectable module registry; its flagship module is a safety-gated watering pump (max-runtime watchdog, min-off cooldown, dry-run gate, 3-way relay/MOSFET drive) with soil-moisture auto-watering, plus status LED, button gestures, a wear-safe persistent event log, optional OLED / buzzer / RTC / microSD, a Wi-Fi web panel, and Zigbee Router endpoints with an OTA client. Boards: M5Stack NanoH2 (ESP32-H2) and Seeed Studio XIAO ESP32-C6 (bare or on the Expansion Base).

Readme

<!-- SPDX-License-Identifier: MIT -->
<!-- Copyright (c) 2026 Ilia Arestov <code@ilia.ae> -->

# zignode

A modular ESP32 Zigbee/Wi-Fi node for ESP-IDF — the whole device in one component. Its flagship
module is a safety-first watering pump.

Safety-first pump driver, soil-moisture auto-watering, status LED, button gestures, a
wear-safe persistent event log, and a Zigbee Router with a Home Assistant / Zigbee2MQTT
friendly endpoint model plus an OTA client.

**Verified on real hardware** on both supported boards: it joins Zigbee2MQTT, is
controllable from Home Assistant, and its settings survive reboots and power-cycles.

## Supported boards

| Target | Board | On-board extras |
|--------|-------|-----------------|
| `esp32h2` | **M5Stack NanoH2** | WS2812 status pixel, blue activity LED, IR NEC LED, soil-moisture ADC |
| `esp32c6` | **Seeed Studio XIAO ESP32-C6** | single user LED; pump on D9/D10; RF switch + antenna initialized automatically |

Other targets do not build, by design — the component has no pin map for them and would
otherwise drive whatever GPIOs happen to exist. This is enforced three ways: `targets:` in
`idf_component.yml` (registry-side), a compile-time `#error` in the component, and in
practice the Zigbee libs ship no 802.15.4 port for non-radio targets. (Verified: an
`esp32s3` build fails.)

> **ESP-IDF ≥ 5.4.1 is required** (or ≥ 5.3.3 / ≥ 5.5). The M5NanoH2 ships with ESP32-H2
> chip revision **v1.2 (ECO5)**, which older 5.4.x does not support: it boots only via the
> `CONFIG_ESP32H2_REV100_DEVELOPMENT` *Preview* flag, which relaxes the boot-revision gate
> **without** the v1.2 RF/PHY bring-up — leaving the 802.15.4 receiver deaf, so the device
> transmits beacon requests but hears nothing and never joins. Do not set that flag; use a
> supported IDF instead. See Espressif advisory AR2025-002.

## Install

```bash
idf.py add-dependency "ilia-ae/zignode^2.0.0"
```

## Use

```c
#include "zignode.h"

void app_main(void)
{
    zignode_config_t cfg = ZIGNODE_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(zignode_run(&cfg));
}
```

That is the entire application — see `examples/zignode_minimal`.

`zignode_run()` is `zignode_init()` + `zignode_start()`. Call the two separately
if you need a hook between "all local subsystems up, pump provably parked OFF" and "radio
started" — that is where, for example, Wi-Fi/802.15.4 coexistence setup belongs on the C6.

## Project requirements

The component cannot own a partition table or project-wide sdkconfig, so the application
must provide both.

**`partitions.csv`** (copy from `examples/zignode_minimal/partitions.csv`) needs
dual OTA slots, the Zigbee stack's `zb_storage` + `zb_fct`, and — for the event log — an
`evtlog` partition:

```
nvs,        data, nvs,  ,        0x6000,
otadata,    data, ota,  ,        0x2000,
phy_init,   data, phy,  ,        0x1000,
ota_0,      app,  ota_0,,        0x180000,
ota_1,      app,  ota_1,,        0x180000,
zb_storage, data, fat,  ,        0x4000,
zb_fct,     data, fat,  ,        0x400,
evtlog,     data, nvs,  ,        0x8000,
```

Without `evtlog` the event log degrades to RAM-only and says so at boot; everything else
keeps working.

**`sdkconfig.defaults`** minimum:

```ini
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
CONFIG_ZB_ENABLED=y
CONFIG_ZB_ZCZR=y            # mains-powered Router (or CONFIG_ZB_ZED=y for a sleepy device)
CONFIG_ZB_RADIO_NATIVE=y
CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y
```

The component `#error`s at build time if the Zigbee role or `CONFIG_ZB_ENABLED` is missing,
rather than failing mysteriously later.

## Safety model

This drives a water pump, so the failure mode that matters is "stuck ON floods the room".

- **One choke-point.** A single function drives the GPIOs; nothing else touches them.
- **Boot-safe OFF.** The pump is parked OFF as the very first action of
  `zignode_init()` — before NVS, the radio, or anything else. Pair it with an external
  pull-up on the relay pin and pull-down on the MOSFET gate so the pre-firmware Hi-Z window
  is safe too.
- **Max-runtime watchdog.** Every ON arms a one-shot timer (default 300 s, hard ceiling
  3600 s) that force-stops the pump regardless of connectivity. If the timer cannot be
  armed, the pump is driven OFF instead of running unwatched.
- **Min-off cooldown** between automated runs protects the motor and relay contacts.
  Deliberate manual control (button / Home Assistant) bypasses it, so a human is never
  locked out.
- **Dry-run gate.** A faulted moisture sensor blocks starts and forces OFF.
- **Task watchdog** enabled: a firmware hang resets the MCU, which returns the pads to
  Hi-Z and lets the external resistors re-park the pump.

## Zigbee model

Router (ZCZR) by default. Three endpoints, one plain custom cluster.

| EP | Role | Clusters |
|----|------|----------|
| 10 | Pump | Basic, Identify, On/Off, custom `0xFC00` config, OTA Upgrade (client) |
| 11 | Auto-water on/off | Identify, On/Off |
| 13 | Soil moisture | Basic, Identify, Relative Humidity (soil % reported as %RH) |

`0xFC00` is a **plain** custom cluster (no manufacturer code) carrying max-runtime,
min-off, dry/wet thresholds, auto interval/duration, ADC calibration, a read-only status
bitmask, the pump drive mode, an IR-NEC write, and a cursor to read the event log.

The device also **self-binds** its reportable clusters to the coordinator on every boot, so
attribute reporting survives a power-cycle without a manual "Reconfigure".

A ready Zigbee2MQTT external converter and a Home Assistant blueprint live in the
[repository `docs/`](https://github.com/ilia-ae/zignode/tree/main/docs).

## Configuration

`idf.py menuconfig` → **Component config → ZigNode**:

| Option | Default | Notes |
|---|---|---|
| `ZIGNODE_ENABLE_MOISTURE` | y on H2, n elsewhere | on-board ADC soil sensor |
| `ZIGNODE_ENABLE_SCHEDULER` | y | interval auto-watering |
| `ZIGNODE_ENABLE_ZB_CONFIG_CLUSTER` | y | expose `0xFC00` |
| `ZIGNODE_ENABLE_OTA` | y | Zigbee OTA client |
| `ZIGNODE_ENABLE_IR` | y on H2, n elsewhere | **never enable on the XIAO C6** — its GPIO3 is the RF-switch power line |
| `ZIGNODE_ENABLE_EVENTLOG` | y | needs the `evtlog` partition |
| `ZIGNODE_ZB_PRIMARY_CHANNEL` | 0 | pin the join channel (0 = scan all) |
| `ZIGNODE_ZB_MIN_JOIN_LQI` | 0 | 0 = accept any parent; the stack's default of 32 can make an H2 refuse a coordinator that is right next to it |

## License

MIT — see `LICENSE`. This component also contains third-party material from Espressif
(Apache-2.0 and CC0-1.0); see `NOTICE` and `THIRD_PARTY_NOTICES.md`, and the SPDX headers
in the individual sources.

Links

Maintainer

  • Ilia Arestov <code@ilia.ae>
To add this component to your project, run:

idf.py add-dependency "ilia-ae/zignode^2.0.0"

download archive

Stats

  • Archive size
    Archive size ~ 171.53 KB
  • Downloaded in total
    Downloaded in total 0 times
  • Downloaded this version
    This version: 0 times

Badge

ilia-ae/zignode version: 2.0.0
|