Files

211 lines
7.6 KiB
Markdown

# 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