One image, three Bitfocus Companion modes — flash any ARM SBC or real Raspberry Pi 4/5, switch between Satellite, full Companion, or Buttons USB Relay from the browser
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.) via Armbian, and for real
Raspberry Pi 4/5 via the official Raspberry Pi OS — that boot straight into
Bitfocus Companion Satellite —
or, on the Full image variant, run full Bitfocus Companion itself.
Bitfocus Buttons USB Relay is also supported as a third mode.
Switch between modes from the browser — no re-flash needed.
An opt-in Companion Dashboard kiosk display can run alongside any mode on units with a
screen attached.
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 extends the architecture of companion-satellite-armbian to run on ARM single-board computers that aren't Raspberry Pis — turning any supported board into a Bitfocus Companion Satellite node (or a full Bitfocus Companion instance, on the Full image variant) out of the box. Bitfocus Buttons USB Relay is also supported as a third mode, for boards dedicated to driving a physical USB relay instead. An opt-in Companion Dashboard kiosk display can run alongside any of the three modes on units with a screen attached.
Real Raspberry Pi 4/5 hardware runs on a separate, parallel pipeline using the official Raspberry Pi
OS — not Armbian. Armbian's own rpi4b/rpi5b board entries exist but are secondary support for
boards it isn't actually built around (Rockchip/Allwinner/Amlogic); real Pi hardware gets the Pi
Foundation's own first-class OS instead, which matters especially for GPU/HDMI/VideoCore (Dashboard's
display output).
The build pipeline is fully automated via GitHub Actions, with two parallel base-OS sources feeding the same customization stage:
- Armbian boards: the Armbian build framework compiles a minimal Ubuntu Noble (24.04) base image for the target board. Raspberry Pi 4/5: the official Raspberry Pi OS Lite (64-bit) image is downloaded directly from Raspberry Pi's own release manifest (the same one Raspberry Pi Imager uses) — one universal image covers both Pi 4 and Pi 5.
- HashiCorp Packer chroots into the image (identical customization stage regardless of which OS produced it), installs Companion Satellite from source via the official install script,
installs Bitfocus Buttons USB Relay from this repo's
buttons-deb-mirrorrelease (maintained manually — no Bitfocus account or secrets needed in CI), and — on the Full variant — installs full Bitfocus Companion itself, plus the opt-in Companion Dashboard kiosk. All modes are installed; Companion Satellite is the default active mode, switchable to Buttons or Companion at runtime with no re-flash. - On first boot,
dpx-set-hostname.servicereads the board's Ethernet MAC address from sysfs and permanently sets the hostname todpx-buttonode-XXXX(last 4 hex chars, e.g.dpx-buttonode-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; the Full variant adds more on top for the full Companion install.
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 non-Pi ARM SBCs
- Raspberry Pi OS — base image for real Raspberry Pi 4/5 hardware
- 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
- Companion Dashboard — opt-in kiosk display 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 | Base OS | ID |
|---|---|---|
| Rock Pi S — buy | Armbian | rockpi-s |
| Orange Pi Zero 3 — buy | Armbian | orangepizero3 |
| Rock Pi 4B | Armbian | rockpi-4b |
| Rock Pi 4B+ | Armbian | rockpi-4bplus |
| Rock Pi S0 | Armbian | rock-s0 |
| Raspberry Pi 4 — buy | Official Raspberry Pi OS (not Armbian) | one universal image, no board matrix |
| Raspberry Pi 5 — buy | Official Raspberry Pi OS (not Armbian) | same image as Pi 4 |
| 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.
Each release ships two variants — pick one:
lite— Buttons USB Relay + Companion Satellite (smaller image, the default)full— everything inlite, plus full Bitfocus Companion itself installed
rockpi-s-dpx-buttonode-<version>-lite.img.gz
rockpi-s-dpx-buttonode-<version>-full.img.gz
rpi4-5-dpx-buttonode-<version>-lite.img.gz # real Raspberry Pi 4/5
rpi4-5-dpx-buttonode-<version>-full.img.gz # real Raspberry Pi 4/5
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-buttonode-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-buttonode-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-buttonode-XXXX.localwhereXXXXis the last 4 hex characters of the MAC (e.g.dpx-buttonode-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-buttonode-XXXX.local:8080— change hostname, switch DHCP/static IP, manage devices, discover other buttonodes, and switch between Buttons, Satellite, and Companion mode.
SSH: disabled by default, no hardcoded credential — see SSH into the device below.
Before any mode claims the device, the attached Stream Deck itself becomes a stage-then-GO config screen — no web UI or SSH needed for initial setup:
- MODE cycles the pending selection through Buttons → Satellite → Companion (color-coded, skips Companion if not installed)
- NET toggles staged DHCP ↔ static
- SUBNET cycles
/24 /22 /16 /8 - Holding an octet key spins its value (locked unless NET is staged to static)
- D (if Dashboard is installed) is a passive status indicator — red when Dashboard is actually running, gray when installed but off
- SSH (press-to-toggle) reveals the first-boot random root password — see SSH into the device below
- GO commits the staged mode + network together as one operation
Nothing is applied until GO is pressed — cycling MODE/NET/SUBNET only changes what's staged.
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_buttonodeWatch it: Actions → Release — dpx-buttonode 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-buttonode 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_buttonode \
-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-buttonode 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-buttonode-XXXX.local:8080
A persistent status bar (version, active mode, live RAM — color-coded) shows across every tab, not just Status.
| Tab | What it does |
|---|---|
| Status | Hostname, IP, MAC, network mode, current mode + active service status, mDNS health, USB devices, uptime, RAM |
| 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-buttonode-* units on the LAN with links to each web UI |
| Mode | Switch between Buttons, Satellite, and Companion. Configure Companion server IP + port for Satellite mode. Companion Dashboard toggle (opt-in kiosk display, if installed) with a Toggle Fullscreen button — sends F11 into the running kiosk, no keyboard needed. |
| SSH | Enable/disable SSH, change the root password. Every action requires the current root password — see SSH into the device below. |
| Updates | Check and apply updates for the web UI, Buttons, Satellite, and Companion in place |
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-buttonode-XXXX.local) resolves correctly via mDNS within a few seconds.
SSH ships disabled, and there's no hardcoded default credential — enable it from the web UI's SSH tab. On first boot, a random per-device root password is generated automatically. It's deliberately never shown on the web UI itself (that page has no login of its own) — the only place it's ever revealed is the Stream Deck: press the SSH key (in the action row, next to GO) to show it; press it again to hide it. Seeing it requires physically being at the device.
- On the Stream Deck, press the SSH key to reveal the generated password (press again to hide it).
- Enter it in the web UI's Enable SSH form (or use it to
sshin directly — SSH being off just means thesshservice isn't running yet; enabling it from the UI starts it). - Set your own password via the same tab — this deletes the generated one, and the SSH key goes blank.
No Stream Deck attached at first boot? The generated password is not recoverable remotely by design —
there is currently no other way to retrieve it. Connect a deck temporarily (it only needs to be attached
long enough to press the SSH key once) to get in, or reset the root password locally instead
(sudo passwd root via a serial console / direct login, if your board supports one).
ssh root@dpx-buttonode-XXXX.local
# where XXXX is the last 4 hex chars of the board's MAC addressIf mDNS isn't resolving, find the IP from your router and use that directly.
⚠️ Why this exists. Every action on the SSH tab requires the current root password — that page has no login of its own (plain HTTP, reachable by anyone on the LAN), so without this check, "enable SSH from the web UI" would let anyone on the network turn SSH on with a password of their own choosing, no guessing required. Requiring the current password keeps the bar at "you already have root," the same as SSH access always implied, just reachable through the browser too. This is aimed at keeping honest people honest on a home/studio LAN — it is not hardened against a determined attacker with sustained LAN access. SeeAGENTS.mdgotcha 10a for implementation details.
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-buttonode to the Companion machine
Configure from the web UI:
- Open
http://dpx-buttonode-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/configAn opt-in kiosk display that runs alongside whatever mode (Buttons/Satellite/Companion) is active, on units with an HDMI display attached — tomhillmeyer/companion-dashboard, installed but disabled by default.
Enable it:
- Open the web UI's Mode tab
- Find the Companion Dashboard section, click Turn On
- Dashboard boots into its own first-run screen on the attached display — point it at your Companion instance from there (Dashboard manages its own connection settings, independent of this project)
- Toggle Fullscreen sends F11 into the running kiosk from the web UI — no keyboard needed at the physical display
Low-RAM boards: a non-blocking warning shows on boards reporting under 1GB RAM — Dashboard's X11/ Electron stack needs real headroom to run reliably alongside a mode service. It's still your call whether to enable it; the warning just sets expectations.
# 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-buttonode-XXXX.local on port 3040 (Buttons relay) and port 8080 (web UI).
# Confirm it's on the network
ping dpx-buttonode-XXXX.local
# Discover all buttonodes on the LAN
avahi-browse -t _dpx-buttonode._tcp # Linux
dns-sd -B _dpx-buttonode._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
- Real Raspberry Pi OS build pipeline for Pi 4/5 (parallel to Armbian, not a substitute)
- 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-buttonode-XXXX) on first boot -
dpx-buttonode-ui— device config web UI on port 8080 (hostname, network, devices, node discovery, SSH, updates) - Buttons / Satellite / Companion three-way mode — all baked in, switch without re-flash
- Mode tab in web UI — configure Companion server IP/port, switch modes from browser
- Stream Deck HID boot splash — stage-then-GO config screen, no web UI/SSH needed for initial setup
- SSH security overhaul — disabled by default, no hardcoded credential, physical-access-only reveal
- On-device auto-update system (web UI, Buttons, Satellite, Companion)
- Companion Dashboard — opt-in kiosk display, F11 fullscreen toggle, deck status key
- Set a default Companion Dashboard connection from the web UI (#8)
- On-screen keyboard for the Dashboard kiosk (#9)
- 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_buttonode
- 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",
# Disable BOTH units -- ssh.socket alone keeps systemd listening on
# :22 and lazily starting ssh.service on demand (socket activation),
# so disabling only ssh.service does not actually disable SSH.
"systemctl disable --now ssh.socket || true",
"systemctl disable --now ssh.service || 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.