Flash-ready Armbian images — Bitfocus Buttons USB Relay and Companion Satellite, switchable at runtime
Automated GitHub Actions build pipeline that produces ready-to-flash .img.gz images
for ARM single-board computers (Rock Pi S, Orange Pi Zero, etc.) with
Bitfocus Buttons USB Relay and
Companion Satellite pre-installed.
Switch between modes from the browser — no re-flash needed.
Write the image, plug in your Stream Deck, power on — done.
»
Download a Release »
Report Bug
·
Request Feature
·
🖨️ 3D Case for Rock Pi S
·
📸 Screenshots
This project mirrors the architecture of companion-satellite-armbian but targets Bitfocus Buttons USB Relay (headless) instead of Companion Satellite — making it work on ARM single-board computers that aren't Raspberry Pis.
The build pipeline is fully automated via GitHub Actions:
- The Armbian build framework compiles a minimal Ubuntu Noble (24.04) base image for the target board.
- The Bitfocus Buttons USB Relay
.tar.gzpackage is pulled from this repo'sbuttons-deb-mirrorrelease (maintained manually — no Bitfocus account or secrets needed in CI). - HashiCorp Packer chroots into the image, installs the Buttons
.deb, then builds and installs Companion Satellite from source via the official install script. Both services are installed; Buttons is the default active mode. - On first boot,
dpx-set-hostname.servicereads the board's Ethernet MAC address from sysfs and permanently sets the hostname todpx-buttnode-XXXX(last 4 hex chars, e.g.dpx-buttnode-C833). - The image is zeroed, gzip-compressed, and published as a GitHub Release.
Note on build time: Companion Satellite is built from source inside the chroot (Node.js + Yarn build). This adds ~30–60 minutes to the total build time.
A daily scheduled workflow checks whether the mirror release has a version that hasn't been built yet, and automatically triggers a full matrix build if so.
author(s): // www.dubpixel.tv - i@dubpixel.tv
- Armbian Build Framework — base Linux image for ARM SBCs
- HashiCorp Packer + arm-image plugin — chroot image customization
- GitHub Actions — CI/CD build, scheduling, and release publishing
- Bitfocus Buttons USB Relay (headless) — USB relay mode
- Bitfocus Companion Satellite — Companion satellite mode
Pick your path:
- → I just want to flash a board — download, flash, done
- → Bitfocus released a new version — one command to update the pipeline
- → I need a board not in the auto-release list — manual dispatch for any of 150+ boards
The following boards are built automatically on every new Buttons release and published to Releases:
| Board | Armbian ID |
|---|---|
| Rock Pi S — buy | rockpi-s |
| Orange Pi Zero 3 — buy | orangepizero3 |
| Rock Pi 4B | rockpi-4b |
| Rock Pi 4B+ | rockpi-4bplus |
| Rock Pi S0 | rock-s0 |
| Accessory | Board | Why |
|---|---|---|
| Rock Pi S PoE HAT | Rock Pi S | Dedicated HAT — cleanest single-cable install |
| ecoPI S housing | Rock Pi S | Enclosure designed for Rock Pi S |
| Waveshare PoE Splitter USB-C 2.5A | Orange Pi Zero 3 | PoE → USB-C 5V/2.5A, metal case, gigabit |
| Waveshare PoE Splitter USB-C 5A | Orange Pi Zero 3 | Same but 25W — more headroom for heavier loads |
All 150+ Armbian-supported boards are available for one-off manual builds — see Path C.
What you need: A microSD card (8 GB min), your Rock Pi board, a Stream Deck.
Go to Releases and download the .img.gz for your board:
rockpi-s-dpx-buttnode-0.1.0-beta.4.img.gz
Easiest — Balena Etcher (Mac / Windows / Linux):
- Open Etcher → Flash from file → pick the
.img.gz - Select your SD card
- Click Flash — Etcher handles the
.gzdecompression automatically
Command line (macOS):
# Find your SD card — look for the right size disk
diskutil list
diskutil unmountDisk /dev/diskN
gunzip -c rockpi-s-dpx-buttnode-0.1.0-beta.4.img.gz \
| sudo dd of=/dev/rdiskN bs=4m status=progress
diskutil eject /dev/diskNCommand line (Linux):
lsblk # find your SD card device
gunzip -c rockpi-s-dpx-buttnode-0.1.0-beta.4.img.gz \
| sudo dd of=/dev/sdX bs=4M status=progress conv=fsync
⚠️ Triple-check your device path (/dev/diskNor/dev/sdX). Wrong device = wiped disk.
- Insert SD card into the Rock Pi
- Plug in your Stream Deck via USB
- Plug in ethernet
- Power on — wait ~30 seconds
That's it. Open Bitfocus Buttons on your computer — the relay appears automatically under discovered devices. No configuration needed.
Hostname: Each device gets a unique hostname derived from its MAC address:
dpx-buttnode-XXXX.localwhereXXXXis the last 4 hex characters of the MAC (e.g.dpx-buttnode-C833.local). This is stable — the same board always gets the same name.
Web UI: A device config panel runs on port 8080:
http://dpx-buttnode-XXXX.local:8080— change hostname, switch DHCP/static IP, manage devices, discover other buttnodes, and switch between Buttons and Satellite mode.
SSH: enabled —
ssh root@dpx-buttnode-XXXX.local— default password1234(Armbian forces a change on first login).
When Bitfocus releases a new version, this is the entire process:
Go to user.bitfocus.io/download, log in, and download:
bitfocus-buttons-usb-relay-headless_X.Y.Z_arm64.tar.gz
Requires GitHub CLI:
brew install ghthengh auth login
./scripts/upload-mirror.sh ~/Downloads/bitfocus-buttons-usb-relay-headless_X.Y.Z_arm64.tar.gzThis uploads the file to the buttons-deb-mirror release in this repo. Done.
The daily scheduled check at 06:00 UTC will detect the new version and automatically build all boards and publish a release.
To trigger it right now instead of waiting:
gh workflow run release-action.yaml --repo dubpixel/dpx_buttnodeWatch it: Actions → Release — dpx-buttnode Images → latest run
Any of the 150+ Armbian-supported boards can be built on demand. The artifact is available for 7 days under the Actions run (not published as a public release).
Via GitHub web UI:
- Go to Actions → Build Armbian + dpx-buttnode Image
- Click Run workflow
- Pick your board from the dropdown
- Click Run workflow
- Wait ~45-90 min, then download the
.img.gzfrom the run's Artifacts section
Via terminal:
gh workflow run armbian-builder.yaml \
--repo dubpixel/dpx_buttnode \
-f armbian-board=orangepizero3Replace orangepizero3 with any board ID from the Armbian hardware list.
- Fork this repo on GitHub
- Seed the mirror with the current package:
./scripts/upload-mirror.sh ~/Downloads/bitfocus-buttons-usb-relay-headless_0.1.0-beta.4_arm64.tar.gz - Trigger a first build:
- Actions → Release — dpx-buttnode Images → Run workflow → Force: true
- Done — updates are fully automated from here
No GitHub Secrets needed. The pipeline uses only the built-in
GITHUB_TOKEN.
Every device runs a lightweight web UI on port 8080:
http://dpx-buttnode-XXXX.local:8080
| Tab | What it does |
|---|---|
| Status | Hostname, IP, MAC, network mode, current mode + active service status, mDNS health, USB devices |
| Hostname | Change the device hostname — applies immediately and persists across reboots |
| Network | Switch between DHCP and static IP. Survives reboots. |
| Devices | USB device list, Stream Deck USB power cycle, Buttons service restart |
| Nodes | Discover all other dpx-buttnode-* units on the LAN with links to each web UI |
| Mode | Switch between Buttons and Satellite. Configure Companion server IP + port for Satellite mode. |
Note: The Network tab writes directly to
/etc/systemd/network/and restartssystemd-networkd. After an IP change, navigate to the new address — the hostname (dpx-buttnode-XXXX.local) resolves correctly via mDNS within a few seconds.
SSH is enabled by default. As soon as the board is on the network:
ssh root@dpx-buttnode-XXXX.local
# where XXXX is the last 4 hex chars of the board's MAC address
# Default password: 1234 (Armbian forces a change on first login)If mDNS isn't resolving, find the IP from your router and use that directly.
cat /etc/dpx-mode # prints: buttons or satelliteYou can also switch from SSH — same as what the UI does:
# Switch to Satellite mode
systemctl stop bitfocus-buttons-usb-relay
systemctl disable bitfocus-buttons-usb-relay
systemctl enable satellite
systemctl start satellite
echo satellite > /etc/dpx-mode
# Switch back to Buttons mode
systemctl stop satellite
systemctl disable satellite
systemctl enable bitfocus-buttons-usb-relay
systemctl start bitfocus-buttons-usb-relay
echo buttons > /etc/dpx-modeIn Satellite mode the device connects outbound to a running Bitfocus Companion instance and exposes the attached Stream Deck(s) as remote surfaces.
Requirements:
- Bitfocus Companion v3.4.0 or newer running somewhere on the network
- Companion must have Satellite enabled: Settings → Surfaces → Enable Satellite
- TCP port 16622 reachable from the dpx-buttnode to the Companion machine
Configure from the web UI:
- Open
http://dpx-buttnode-XXXX.local:8080 - Go to the Mode tab
- Enter your Companion server IP and port (default
16622) - Click Switch to Satellite — the device switches immediately and persists on reboot
Configure from SSH:
# Write persistent config
cat > /etc/dpx-satellite.conf <<EOF
HOST=192.168.1.10
PORT=16622
EOF
# Stage it for satellite's boot import
cat > /boot/satellite-config <<EOF
COMPANION_IP=192.168.1.10
COMPANION_PORT=16622
EOF
# Or push it live to the running service via REST API
curl -X POST http://localhost:9999/api/config \
-H 'Content-Type: application/json' \
-d '{"host":"192.168.1.10","port":16622}'Check satellite status:
systemctl status satellite
journalctl -u satellite -f
# REST API shows current config + connection status
curl http://localhost:9999/api/config# Is it running?
systemctl status bitfocus-buttons-usb-relay
# Watch live logs
journalctl -u bitfocus-buttons-usb-relay -f
# Restart it
sudo systemctl restart bitfocus-buttons-usb-relayBy default the relay announces itself via mDNS and Buttons discovers it automatically — no config needed for most setups.
If your network blocks mDNS (some managed switches do), point the relay directly at your Buttons server:
sudo nano /etc/default/bitfocus-buttons-usb-relayAdd this line:
EXTRA_ARGS="-buttonsAddress 192.168.1.10:3000"
Then restart:
sudo systemctl restart bitfocus-buttons-usb-relayThe device announces itself as dpx-buttnode-XXXX.local on port 3040 (Buttons relay) and port 8080 (web UI).
# Confirm it's on the network
ping dpx-buttnode-XXXX.local
# Discover all buttnodes on the LAN
avahi-browse -t _dpx-buttnode._tcp # Linux
dns-sd -B _dpx-buttnode._tcp local # macOS
# Or just open the Nodes tab in the web UI- what did we learn?
- x
- what do we like/hate?
- y
- what would/could we do differently?
- z
- Core Armbian + Packer two-stage build pipeline
- Self-hosted package mirror via GitHub Releases (no Bitfocus secrets in CI)
- Matrix builds for Orange Pi Zero family
- Daily automated version check + GitHub Release publishing
-
upload-mirror.shhelper for one-command package updates - Dynamic MAC-derived hostname (
dpx-buttnode-XXXX) on first boot -
dpx-buttnode-ui— device config web UI on port 8080 (hostname, network, devices, node discovery) - Companion Satellite A/B mode — both services baked in, switch without re-flash
- Mode tab in web UI — configure Companion server IP/port, switch modes from browser
- Additional board support (Banana Pi M2 Zero, NanoPi R4S, Orange Pi 5)
- SHA256 checksums attached to each release
- WiFi pre-configuration support in image (via Armbian
wpa_supplicantoverlay) - OTA image update — download latest release from GitHub and flash in-place (kexec into RAM,
ddto SD, reboot)
See the open issues for a full list of proposed features (and known issues).
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated.
If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". Don't forget to give the project a star! Thanks again!
- Fork the Project
- Create your Feature Branch (
git checkout -b feature/AmazingFeature) - Commit your Changes (
git commit -m 'Add some AmazingFeature') - Push to the Branch (
git push origin feature/AmazingFeature) - Open a Pull Request
Distributed under the [LICENSE_TYPE] License. See LICENSE.txt for more information.
Joshua Fleitell - i@dubpixel.tv
Project Link: https://github.com/dubpixel/dpx_buttnode
- elliotmatson/companion-satellite-armbian — architecture and workflow pattern this project is based on
- Bitfocus — creators of Buttons and Companion
- Armbian — Linux for ARM SBCs
- Bitfocus Buttons USB Relay — Official Docs — source of truth for installation, service management, and configuration
- Bitfocus Companion Satellite — GitHub — official install script and documentation
- dpx_raxda_rockpis — 3D printable POE enclosure for the Rock Pi S hardware this project runs on
This project uses a two-stage pipeline to produce flash-ready images for ARM SBCs. Here's the general recipe so you can adapt it for any headless software you want to bake into an Armbian image.
GitHub Actions runner (x86)
└─ 1. Build Armbian base image for target board
└─ 2. Packer chroots into image via QEMU
└─ copies your software in
└─ installs it
└─ configures it (hostname, services, etc.)
└─ 3. Compress and publish the image
No cross-compilation. No physical board needed. Runs entirely on standard x86 CI runners.
- Your software packaged as a
.deb, or an install script that runs inside a Debian/Ubuntu chroot - A GitHub repo with Actions enabled
- HashiCorp Packer — free, open source
Create your-software.pkr.hcl:
packer {
required_plugins {
arm-image = {
version = "0.2.7"
source = "github.com/solo-io/arm-image"
}
}
}
variable "url" { type = string } # path to Armbian .img
variable "deb_path" { type = string } # path to your .deb
source "arm-image" "armbian" {
iso_checksum = "none"
iso_url = var.url
target_image_size = 5000000000 # 5 GB — adjust as needed
output_filename = "output/image.img"
qemu_binary = "qemu-aarch64-static"
image_mounts = ["/"]
# Required for DNS to work inside the chroot
additional_chroot_mounts = [["bind", "/run/systemd", "/run/systemd"]]
}
build {
sources = ["source.arm-image.armbian"]
# Copy your software into the image
provisioner "file" {
source = var.deb_path
destination = "/tmp/your-software.deb"
}
# Copy your install script
provisioner "file" {
source = "scripts/install.sh"
destination = "/tmp/install.sh"
}
# System config (hostname, first-login, SSH)
provisioner "shell" {
inline = [
"rm -f /root/.not_logged_in_yet",
"echo your-device-name > /etc/hostname",
"systemctl disable ssh || true",
]
}
# Install your software (runs as root)
provisioner "shell" {
execute_command = "chmod +x {{ .Path }}; {{ .Vars }} su root -c {{ .Path }}"
inline_shebang = "/bin/bash -e"
inline = ["chmod +x /tmp/install.sh", "/tmp/install.sh"]
}
}scripts/install.sh runs inside the chroot as root. Treat it like a normal Debian post-install script:
#!/usr/bin/env bash
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
# Install dependencies
apt-get update -q
apt-get install -y --no-install-recommends avahi-daemon libusb-1.0-0
# Install your package
dpkg -i /tmp/your-software.deb || apt-get install -f -y
# Enable services
systemctl enable your-service
systemctl enable avahi-daemon
# Cleanup
apt-get clean- name: Install QEMU (required for ARM chroot on x86 runners)
run: sudo apt-get install -y qemu-user-static
- name: Build Armbian base image
run: |
git clone --depth=1 https://github.com/armbian/build build
sudo ./build/compile.sh build \
BOARD=your-board-id \
BRANCH=current \
RELEASE=noble \
BUILD_MINIMAL=yes \
KERNEL_CONFIGURE=no \
COMPRESS_OUTPUTIMAGE=no
sudo mv build/output/images/*.img build/output/images/armbian.img
- name: Install Packer
run: |
wget -qO - https://apt.releases.hashicorp.com/gpg \
| sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \
https://apt.releases.hashicorp.com $(lsb_release -cs) main" \
| sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt-get update -q && sudo apt-get install -y packer
- name: Run Packer
run: |
sudo packer init your-software.pkr.hcl
sudo packer build \
-var "url=build/output/images/armbian.img" \
-var "deb_path=path/to/your-software.deb" \
your-software.pkr.hcl
- name: Compress image
run: |
sudo apt-get install -y zerofree
IMG="output/image.img"
LOOP=$(sudo losetup -fP --show "$IMG")
sudo e2fsck -fy "${LOOP}p1" || true
sudo zerofree "${LOOP}p1"
sudo losetup -d "$LOOP"
gzip -n "$IMG"| Thing | Why it matters |
|---|---|
qemu-user-static must be installed before Packer runs |
Packer uses it to emulate ARM64 instructions inside the chroot on your x86 runner |
additional_chroot_mounts = [["bind", "/run/systemd", "/run/systemd"]] |
Without this, DNS resolution inside the chroot fails and apt-get can't reach package servers |
All packer commands need sudo |
The arm-image plugin creates loop devices and bind mounts — root required |
sudo mv the Armbian output |
The Armbian build framework runs as root inside Docker, so output files are owned by root |
zerofree before gzip |
Zeros unused filesystem blocks so the image compresses 3-5x smaller |
Set target_image_size generously |
Packer will fail if the image fills up during install. 5 GB is safe for most software |
Any board in Armbian's supported hardware list works. Look up the board's BOARD= ID from the Armbian docs or the supported boards list.