diff --git a/README.md b/README.md new file mode 100644 index 0000000..23c91c7 --- /dev/null +++ b/README.md @@ -0,0 +1,210 @@ +# RPi Zigbee Stack + +Docker Compose stack running on a Raspberry Pi 3B+ (DietPi 64-bit) with a ThirdReality Zigbee 3.0 USB dongle. + +## Hardware + +| Component | Detail | +|---|---| +| Board | Raspberry Pi 3B+ | +| OS | DietPi 64-bit | +| Zigbee Dongle | ThirdReality Zigbee 3.0 USB (USB-C, use with USB-A adapter) | +| Dongle Chip | **BL702** (Bouffalo Lab) — *not* BL706 as the vendor README states | +| USB Bridge | CH340 (QinHeng, VID `1a86`, PID `7523`) | +| Flash | 2MB SPI (jedec `c84015`) | +| Dongle Port | **Powered USB hub** required — the CH340 drops off unpowered hubs | + +Use `lsusb -t` on the RPi to verify the dongle is on a powered hub, not daisy-chained behind multiple unpowered hubs. + +## Services + +| Service | Image | Port | Purpose | +|---|---|---|---| +| Mosquitto | `eclipse-mosquitto:latest` | 1883 | MQTT message broker | +| Zigbee2MQTT | `ghcr.io/pine64/zigbee2mqtt:latest-dev` | 8080 | Zigbee coordinator (BLZ fork) | +| Node-RED | `nodered/node-red:latest` | 1880 | Flow-based automation + dashboard | + +All containers communicate via Docker's default bridge network using service names (e.g., `mosquitto:1883`). + +## Zigbee Dongle Setup + +### Flashing the Firmware + +The dongle ships with factory firmware that **must be replaced** with the BLZ coordinator firmware. This requires an **x86 machine** — the Bouffalo Lab DevCube tool does not run on ARM. + +1. Clone the firmware repo: + ``` + git clone https://github.com/ThirdReality/ThirdReality-Zigbee-3.0-USB-dongle + ``` + +2. Enter bootloader mode on the dongle: + - Press and **hold** the pin hole button on the dongle body + - While holding, plug the dongle into your x86 machine's USB port + - Hold for 2 more seconds, then release + +3. Flash the BL702 firmware (v1.00.12): + ``` + ThirdReality-Zigbee-3.0-USB-dongle/Flashing-Tool/BouffaloLabDevCube-v1.9.0/bflb_iot_tool-ubuntu \ + --chipname BL702 \ + --interface uart \ + --port /dev/ttyUSB0 \ + --baudrate 2000000 \ + --firmware Image/Coordinator/blz/v1.00.12/R3_706_dongle_v1.00.12/R3_bl702_dongle.bin \ + --pt Image/Coordinator/blz/v1.00.12/R3_706_dongle_v1.00.12/partition_cfg_1M.toml + ``` + +4. Power cycle: unplug and replug the dongle. The **red LED** should illuminate, confirming the firmware is running. + +### Firmware Version Compatibility + +Do **not** use these firmware versions: +- v1.00.01 (`R3_706_dongle.bin`) — built for BL706, wrong chip → no boot +- v1.00.07 (`whole_flash_data.bin`) — flashes but firmware doesn't boot on BL702 + +| Firmware | Chip | Partition | Works? | +|---|---|---|---| +| v1.00.12 `R3_bl702_dongle.bin` | BL702 | `partition_cfg_1M.toml` | ✅ | +| v1.00.12 + v1.00.01 `partition_cfg_2M.toml` | BL702 | (mismatched) | ❌ | +| v1.00.01 `R3_706_dongle.bin` | BL706 | `partition_cfg_2M.toml` | ❌ (wrong chip) | +| v1.00.07 `whole_flash_data.bin` | (any) | (none) | ❌ (no boot) | + +### Recovery from Bad Flash + +If the dongle gets into a bad state: +1. Unplug +2. Press-and-hold bootloader button, plug in, hold 2s, release +3. The ROM bootloader is always accessible via the button +4. Re-flash with the correct firmware + +### Verifying the Dongle Works + +On the RPi, stop zigbee2mqtt and test with pyserial: + +```python +import serial, time, struct + +def calc_crc16(data): + crc16 = 0xFFFF + for byte in data: + crc16 = ((crc16 >> 8) | (crc16 << 8)) & 0xFFFF + crc16 ^= byte + crc16 ^= (crc16 & 0xFF) >> 4 + crc16 ^= ((crc16 << 8) << 4) & 0xFFFF + crc16 ^= (((crc16 & 0xFF) << 5) | ((crc16 & 0xFF) >> 3) << 8) & 0xFFFF + return bytes([(crc16 >> 8) & 0xFF, crc16 & 0xFF]) + +def escape(data): + o = bytearray() + for b in data: + if b in (0x42,0x4C,0x07): o.extend([0x07,b^0x10]) + else: o.append(b) + return bytes(o) + +def build(fc, seq, fid, payload=b''): + frm = bytes([fc,seq]) + fid.to_bytes(2,'little') + payload + return b'\x42' + escape(frm + calc_crc16(frm)) + b'\x4C' + +ser = serial.Serial('/dev/ttyUSB0', 2000000, timeout=2) +time.sleep(0.3) +ser.write(build(0, 0, 0x0003)) # BLZ RESET +time.sleep(0.5) +resp = ser.read(200) +# Expect: RESET_ACK (frameId 0x0004) with reset_reason byte +print('Dongle OK' if resp else 'Dongle not responding') +ser.close() +``` + +A working dongle responds to the BLZ RESET command (frame ID `0x0003`) with a RESET_ACK (frame ID `0x0004`). + +## Configuration Files + +### Mosquitto (`mosquitto.conf`) + +Bare-bones config for local MQTT. No auth, no TLS — only suitable for a trusted LAN. + +``` +listener 1883 +allow_anonymous true +persistence true +persistence_location /mosquitto/data +log_dest file /mosquitto/log/mosquitto.log +``` + +### Zigbee2MQTT (`zigbee2mqtt.yaml`) + +Critical settings for the BLZ dongle: + +```yaml +serial: + port: /dev/ttyUSB0 + adapter: blz # must be "blz", NOT "zstack" or "ember" + baudrate: 2000000 # must be exactly 2M (not 115200!) + rtscts: false # CH340 doesn't support hardware flow control +``` + +The BLZ protocol requires **exactly 2,000,000 baud**. Any other value causes `frameId:21 after 1000ms` timeout errors — the dongle and host talk at different speeds. + +### Node-RED (`nodered-package.json`) + +Additional nodes installed: +- `node-red-dashboard` — provides the UI dashboard at `/ui` + +## BLZ Protocol Summary + +The Bouffalo Lab Zigbee (BLZ/BZSP) protocol v2.0 is used for host↔dongle communication: + +- **Baudrate:** 2,000,000 +- **Frame format:** `START(0x42) | data | CRC16-CCITT | STOP(0x4C)` +- **Byte stuffing:** Bytes `0x42`, `0x4C`, `0x07` escaped with `0x07` prefix + `XOR 0x10` +- **Key frame IDs:** RESET=`0x0003`, RESET_ACK=`0x0004`, ADD_ENDPOINT=`0x0015`, ACK=`0x0001` + +The `zigpy-blz` Python library (from `bouffalolab/zigpy-blz`) can be used for direct protocol interaction. + +## Troubleshooting + +| Symptom | Cause | Fix | +|---|---|---| +| Dongle USB disconnects/reconnects repeatedly | Unpowered USB hub or weak port | Use a **powered** USB hub | +| Red LED not lit | Wrong firmware or bad flash | Re-flash with correct firmware | +| `zh:blz:uart: frameId:21 after 1000ms` | Baudrate mismatch | Set `baudrate: 2000000` in zigbee2mqtt config | +| `Chip type is not correct` (flashing) | Wrong `--chipname` | Use `BL702` for this dongle | +| Bootloader won't respond | Firmware corruption | Use bootloader button + reflash | +| `BFLB LOAD HELP BIN FAIL` (flashing) | Flash in bad state | Power cycle, re-enter bootloader mode | +| YAML validation error on restart | Malformed config | Use inline arrays `[1,2,3]` not multi-line in advanced section | +| Node-RED "circular config dependency" | Broker node conflict | Deploy as full flow with `Node-RED-Deployment-Type: full` | + +## Daily Management + +```bash +# Clone this repo +git clone https://git.notsosm.art/david/rpi-zigbee-stack.git +cd rpi-zigbee-stack + +# Stack control +just up # start all services +just restart # restart all +just logs # view all logs +just zb-status # zigbee2mqtt status + +# Zigbee device pairing +just zb-permit # allow joining for 60s +just zb-deny # disable joining + +# Config management +just backup # pull current configs from RPi +just sync # push local configs to RPi +just redeploy # sync + restart + +# Web interfaces +just nr-editor # Node-RED editor (1880) +just nr-dashboard # Node-RED dashboard (1880/ui) +``` + +Zigbee2MQTT frontend: `http://192.168.9.147:8080` + +## Related Repos + +- [nr-flow-validator](https://git.notsosm.art/david/nr-flow-validator) — Declarative Node-RED flow generation + conftest/Rego validation +- [ThirdReality Zigbee Dongle](https://github.com/ThirdReality/ThirdReality-Zigbee-3.0-USB-dongle) — Firmware images and flashing tools +- [zigpy-blz](https://github.com/bouffalolab/zigpy-blz) — Python BLZ protocol library +- [Pine64 Zigbee2MQTT](https://github.com/pine64/zigbee2mqtt) — BLZ fork of Zigbee2MQTT