Skip to content
This repository was archived by the owner on Aug 9, 2026. It is now read-only.
/otbrstackPublic archive

Repository files navigation

otbr-commissioner-stacks

Flash a Raspberry Pi 4B with Ubuntu Server 26.04 LTS, pre-configured as an OpenThread Border Router (OTBR). An ESP32-C6 acts as the Thread Radio Co-Processor (RCP), connected via USB.

The same .env file drives four deployment paths:

CommandWhere it runsHow
otbr flashRaspberry Pi 4B (SD card)Ubuntu Server + cloud-init + snap
otbr snapAny Ubuntu host (bare metal)snap (live install)
otbr dockerAny Ubuntu host (bare metal)Docker CE + nginx
otbr vm x64Incus VM or container (testing)snap (no hardware needed)

What you'll need

Hardware (for the Pi path)

ItemNotes
Raspberry Pi 4BPi 3B also works
ESP32-C6 development boardThe Thread radio. Connected to the Pi via USB at first boot
microSD card8 GB minimum, 16 GB+ recommended
Ethernet cableRecommended for first boot. Wi-Fi is supported as a fallback

Software on your Linux host (x86-64)

  • Standard tools: curl, xzcat, dd, lsblk, python3, snap, rsync
  • QEMU user-mode emulation (for the arm64 chroot step): qemu-user-binfmt

Install any missing packages with:

sudo apt-get install qemu-user-binfmt rsync

Getting started

1. Clone the repo and activate the command

git clone https://github.com/farscapian/otbr-commissioner-stacks.git
cd otbr-commissioner-stacks
source ./otbr.sh

The source command loads the otbr shell function into your current terminal. To make it permanent, add this line to your ~/.bashrc:

source /path/to/otbr-commissioner-stacks/otbr.sh

2. Create your env file

Env files live in ~/.otbr/env/. Copy an example and fill in your values:

# Name it after your machine (hostname-specific, takes priority):
cp .env.example ~/.otbr/env/$(hostname).env
nano ~/.otbr/env/$(hostname).env
# Or use the generic fallback name:
cp .env.example ~/.otbr/env/.env

The two required values are:

  • THREAD_DATASET_TLV — your Thread network's Active Operational Dataset (a hex string). If you don't have one yet, generate a new one:

    # On any machine with the OTBR snap or ot-ctl installed:
    snap run openthread-border-router.ot-ctl dataset init new
    snap run openthread-border-router.ot-ctl dataset commit active
    snap run openthread-border-router.ot-ctl dataset active -x
    # Copy the hex output into THREAD_DATASET_TLV in your .env

    Or if you already have an existing Thread commissioner, export it:

    ot-ctl dataset active -x
  • SSH_PUBKEY — your SSH public key (the contents of ~/.ssh/id_ed25519.pub or similar). This is injected into the Pi image so you can SSH in without a password.

Everything else in .env has sensible defaults. Wi-Fi credentials are optional — if you're using Ethernet, leave WIFI_SSID and WIFI_PASSWORD blank.

3. Add an SSH config entry for your Pi

The flash script checks that your ~/.ssh/config has an entry for the Pi's hostname (default: otbr-raspi4) so you can reach it by name after boot. If one doesn't exist, the script will offer to create it. You can also add it manually:

# ~/.ssh/config
Host otbr-raspi4
User ubuntu
# HostName 192.168.x.y # optional if you use mDNS / .local hostname

Flashing the Raspberry Pi

Insert the microSD card into your Linux host and identify its device path (lsblk or dmesg | tail after inserting). It will look like /dev/sdb or /dev/mmcblk0never/dev/sda (that's usually your main drive).

# Standard flash — asks for confirmation before writing
otbr flash /dev/sdX
# Force a full reflash even if Ubuntu is already on the card
otbr flash -f /dev/sdX
# Skip the confirmation prompt (useful for scripting)
otbr flash -y /dev/sdX
# Use a different env file
otbr flash --env-file=pangolin.env /dev/sdX
# Set a custom hostname for this device
otbr flash --hostname=otbr-kitchen /dev/sdX

Smart re-flash: If Ubuntu Server is already on the card (detected by the system-boot partition label), otbr flash skips the image download and dd step entirely and only rewrites the cloud-init config. This is much faster when you just changed a config value.

What happens during the flash

  1. Downloads ubuntu-26.04-preinstalled-server-arm64+raspi.img.xz from Canonical (cached in cache/ubuntu/server/ — only downloaded once).
  2. Verifies the SHA-256 of the downloaded image.
  3. Expands the root partition to fill the SD card.
  4. Runs an arm64 chroot to pre-install packages (git, cmake, python3, etc.) and pre-load the ESP-IDF toolchain so the Pi doesn't have to download them at boot.
  5. Caches arm64 snaps (openthread-border-router, chip-tool) and copies them to the SD card so first boot can install offline.
  6. Writes a cloud-init config into the system-boot partition that sets up networking, installs and configures the OTBR snap, and seeds the Thread dataset.

First boot

Insert the SD card into the Pi, connect the ESP32-C6 via USB, and power on.

First boot takes 5–15 minutes depending on internet speed (the Pi may still need to pull some packages). You can watch progress over SSH:

# Tail all logs in real time (journald + firstboot log)
otbr logs -f otbr-raspi4
# Or SSH in and check the firstboot log directly
ssh otbr-raspi4
sudo tail -f /var/log/otbr-firstboot.log

What happens on first boot

  1. Networking — eth0 comes up via DHCP (preferred). If Wi-Fi credentials were set, wlan0 is configured as a fallback.
  2. RCP firmwareotbr-rcp-update.sh waits for the ESP32-C6 to enumerate, then fetches the latest ESP-IDF release, builds the ot_rcp firmware, and flashes it to the ESP32-C6 if the firmware has changed. Identical firmware is detected by SHA-256 and skipped.
  3. Snap installopenthread-border-router is installed from the pre-loaded copy on the SD card (no store download needed). chip-tool is also installed for Matter commissioning.
  4. OTBR configuration — snap interfaces are connected, the radio URL is set, the backbone interface is configured, and the service is started.
  5. Thread dataset — the TLV from your .env is committed and the Thread interface is brought up.
  6. Firewall — UFW is enabled. SSH is allowed from the CIDRs in SSH_MGMT_CIDRS (or from anywhere if that's empty).

After first boot, otbr-rcp-update.service runs on every subsequent boot to keep the RCP firmware up to date. A weekly timer triggers a reboot to check for new firmware.

Checking OTBR status

ssh otbr-raspi4
# Check Thread state (should be leader, router, or child)
snap run openthread-border-router.ot-ctl state
# Check active dataset
snap run openthread-border-router.ot-ctl dataset active -x
# Check snap service status
snap services openthread-border-router

Remote management

# Tail logs from any otbr-managed device
otbr logs -f otbr-raspi4
# View last-boot logs (static)
otbr logs otbr-raspi4
# Reboot the device
otbr restart otbr-raspi4
# Graceful shutdown
otbr shutdown otbr-raspi4

Testing without hardware (Incus VM)

otbr vm x64 provisions an Incus VM with the same OTBR first-boot sequence — no Raspberry Pi or ESP32-C6 needed. A simulated RCP (ot-rcp) is built from OpenThread source and used instead of the physical ESP32-C6.

Prerequisites: Incus installed, current user in the incus group.

otbr vm x64 # VM (default), instance name: otbr-test-x64
otbr vm x64 --container # system container (faster), name: otbr-test-ct
otbr vm arm64 # arm64 VM (QEMU-emulated, slower)

The first run clones and builds the OpenThread simulator (~5 min). Subsequent runs reuse the cached binary at cache/ot-rcp-sim/ot-rcp.

To tear down an instance:

incus delete otbr-test-x64 --force

Running the test suite

sudo ./tests/test_otbr_vm.sh # full test, x64 VM
sudo ./tests/test_otbr_vm.sh --no-peer-test # skip neighbor exchange (T6)

Six tests verify: Ubuntu 26.04 running, OTBR snap installed, all services active, Thread state is leader/router/child, dataset TLV committed, and neighbor table has ≥1 entry (T6, requires ot-cli binary).


Bare-metal snap (otbr snap)

Installs and configures the openthread-border-router snap on any Ubuntu host with a USB Thread radio attached. Runs as your normal user — sudo is invoked internally only where needed.

Supported radios (auto-detected):

  1. ESP32-C6 (USB vendor 303a) on /dev/ttyACM0
  2. Sonoff Dongle-E / CP210x (Silicon Labs 10c4:ea60) on /dev/ttyUSB0
otbr snap

The script detects the radio, verifies its Spinel firmware (flashing the ESP32-C6 if needed), installs and configures the snap, and commits the Thread dataset. Optional .env variables:

VariableDefaultPurpose
INFRA_IFauto (default route)Backbone network interface
THREAD_IFwpan0Thread virtual interface name

Bare-metal Docker (otbr docker)

Installs Docker CE, pulls openthread/otbr, writes a stable udev symlink for the USB dongle, and sets up nginx as a reverse proxy. Run as root.

otbr docker

Exposes: REST API on :8080, web UI on :8088.

Required .env variables (find values with udevadm info /dev/ttyACM0):

VariablePurpose
DONGLE_VENDORUSB vendor ID
DONGLE_PRODUCTUSB product ID
DONGLE_SERIALUSB serial string (unique per device)

Optional:

VariableDefaultPurpose
DONGLE_SYMLINKttyTHREADSymlink name under /dev/
BAUD_RATE460800Serial baud rate

Environment variable reference

All variables are read from your .env file. See .env.example for the full annotated list. Key variables:

VariableUsed byPurpose
THREAD_DATASET_TLVallThread Active Operational Dataset (hex). Required.
SSH_PUBKEYflash, vmSSH public key injected into the image
OTBR_SNAP_CHANNELflash, snap, vmSnap channel (default: latest/edge)
CHIP_TOOL_SNAP_CHANNELflashchip-tool snap channel (default: latest/stable)
WIFI_SSID / WIFI_PASSWORDflashWi-Fi credentials (optional; eth0 is preferred)
OTBR_HOSTNAMEflashDevice hostname (default: otbr-raspi4)
SSH_MGMT_CIDRSflashSpace-separated CIDRs for SSH access via UFW
HTTP_PROXYallOptional HTTP proxy (e.g. http://squid.local:3128)
INFRA_IFsnapBackbone interface (default: auto-detected)
DONGLE_VENDOR/PRODUCT/SERIALdockerUSB dongle identification

Repository layout

otbr.sh # Shell function — source this to get the otbr command
flash-piotbr.sh # Raspberry Pi SD card flasher (called by otbr flash)
provision_incus.sh # Incus VM/container provisioner (called by otbr vm)
otbr-snap-setup.sh # Bare-metal snap provisioner (called by otbr snap)
otbr-docker-setup.sh # Bare-metal Docker provisioner (called by otbr docker)
scripts/
flash_rcp.sh # ESP32-C6 RCP firmware builder and flasher
verify_rcp.py # Spinel probe (checks if RCP firmware responds correctly)
tests/
test_otbr_vm.sh # Integration test suite for Incus provisioning
.env.example # Annotated template — copy to ~/.otbr/env/ and edit
pangolin.env # Example env for a specific host (copy to ~/.otbr/env/ and adapt)
~/.otbr/ # Runtime data (created by setup.sh / on first run)
env/ # Env files: .env or <hostname>.env
cache/ # Downloaded images, snaps, firmware
logs/ # Per-device and per-session log files
artifacts/ # Generated cloud-init payloads, pyspinel venv

About

A collection of bash scripts and supporting files that help you deploy an OpenThread Border Router. Create a new Thread Network, or Join an exiting one. Several deployment methods available, including Raspberry Pi, known as PIOTBR.

Resources

Stars

1 star

Watchers

0 watching

Forks

Contributors

Languages