# 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