Skip to content

dubpixel/dpx_buttnode

Repository files navigation

Contributors Forks Stargazers Issues License LinkedIn Latest Release

Logo

dpx-buttnode

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

dpx-buttnode hardware

Table of Contents

  1. About The Project
  2. Getting Started
  3. Usage
  4. dpx-buttnode-ui — Device Config UI
  5. Reflection
  6. Roadmap
  7. Contributing
  8. License
  9. Contact
  10. Acknowledgments

About The Project

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:

  1. The Armbian build framework compiles a minimal Ubuntu Noble (24.04) base image for the target board.
  2. The Bitfocus Buttons USB Relay .tar.gz package is pulled from this repo's buttons-deb-mirror release (maintained manually — no Bitfocus account or secrets needed in CI).
  3. 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.
  4. On first boot, dpx-set-hostname.service reads the board's Ethernet MAC address from sysfs and permanently sets the hostname to dpx-buttnode-XXXX (last 4 hex chars, e.g. dpx-buttnode-C833).
  5. 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

(back to top)

Built With

(back to top)

Getting Started

Pick your path:


Supported Boards

The following boards are built automatically on every new Buttons release and published to Releases:

Board Armbian ID
Rock Pi Sbuy rockpi-s
Orange Pi Zero 3buy orangepizero3
Rock Pi 4B rockpi-4b
Rock Pi 4B+ rockpi-4bplus
Rock Pi S0 rock-s0

Recommended accessories

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.


✅ Path A — Flash a pre-built image

What you need: A microSD card (8 GB min), your Rock Pi board, a Stream Deck.

1. Download the image

Go to Releases and download the .img.gz for your board:

rockpi-s-dpx-buttnode-0.1.0-beta.4.img.gz

2. Flash to SD card

Easiest — Balena Etcher (Mac / Windows / Linux):

  1. Open Etcher → Flash from file → pick the .img.gz
  2. Select your SD card
  3. Click Flash — Etcher handles the .gz decompression 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/diskN

Command 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/diskN or /dev/sdX). Wrong device = wiped disk.

3. Boot and connect

  1. Insert SD card into the Rock Pi
  2. Plug in your Stream Deck via USB
  3. Plug in ethernet
  4. 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.local where XXXX is 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 password 1234 (Armbian forces a change on first login).


🔄 Path B — Update to a new Buttons version

When Bitfocus releases a new version, this is the entire process:

1. Download the new package from Bitfocus

Go to user.bitfocus.io/download, log in, and download:

bitfocus-buttons-usb-relay-headless_X.Y.Z_arm64.tar.gz

2. Upload it to the mirror (one command)

Requires GitHub CLI: brew install gh then gh auth login

./scripts/upload-mirror.sh ~/Downloads/bitfocus-buttons-usb-relay-headless_X.Y.Z_arm64.tar.gz

This uploads the file to the buttons-deb-mirror release in this repo. Done.

3. Let CI do the rest (or trigger immediately)

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_buttnode

Watch it: Actions → Release — dpx-buttnode Images → latest run


🛠 Path C — Build any other board manually

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:

  1. Go to Actions → Build Armbian + dpx-buttnode Image
  2. Click Run workflow
  3. Pick your board from the dropdown
  4. Click Run workflow
  5. Wait ~45-90 min, then download the .img.gz from the run's Artifacts section

Via terminal:

gh workflow run armbian-builder.yaml \
  --repo dubpixel/dpx_buttnode \
  -f armbian-board=orangepizero3

Replace orangepizero3 with any board ID from the Armbian hardware list.


⚙️ Fork and run your own pipeline

  1. Fork this repo on GitHub
  2. 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
  3. Trigger a first build:
    • Actions → Release — dpx-buttnode Images → Run workflow → Force: true
  4. Done — updates are fully automated from here

No GitHub Secrets needed. The pipeline uses only the built-in GITHUB_TOKEN.

(back to top)

Usage

dpx-buttnode-ui — Device Config Web Interface

Every device runs a lightweight web UI on port 8080:

http://dpx-buttnode-XXXX.local:8080
Status tab Hostname tab
Network tab Devices tab
Nodes tab Mode tab
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 restarts systemd-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 into the device

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.


Check which mode is active

cat /etc/dpx-mode   # prints: buttons  or  satellite

Switch modes from the terminal

You 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-mode

Companion Satellite mode

In 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:

  1. Open http://dpx-buttnode-XXXX.local:8080
  2. Go to the Mode tab
  3. Enter your Companion server IP and port (default 16622)
  4. 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

Check the Buttons relay is running

# 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-relay

Connect to a specific Buttons server (client mode)

By 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-relay

Add this line:

EXTRA_ARGS="-buttonsAddress 192.168.1.10:3000"

Then restart:

sudo systemctl restart bitfocus-buttons-usb-relay

Network discovery

The 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

Reflection

  • 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.sh helper 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_supplicant overlay)
  • OTA image update — download latest release from GitHub and flash in-place (kexec into RAM, dd to SD, reboot)

See the open issues for a full list of proposed features (and known issues).

Contributing

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!

  1. Fork the Project
  2. Create your Feature Branch (git checkout -b feature/AmazingFeature)
  3. Commit your Changes (git commit -m 'Add some AmazingFeature')
  4. Push to the Branch (git push origin feature/AmazingFeature)
  5. Open a Pull Request

Top contributors:

contrib.rocks image

License

Distributed under the [LICENSE_TYPE] License. See LICENSE.txt for more information.

Contact

Joshua Fleitell - i@dubpixel.tv

Project Link: https://github.com/dubpixel/dpx_buttnode

Acknowledgments


🔧 How to replicate this pattern for any software

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.


The pattern

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.


What you need

  • 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

Step 1 — Write your Packer HCL file

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"]
  }
}

Step 2 — Write your install script

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

Step 3 — Wire it into GitHub Actions

- 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"

Key things to know

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

Supported boards

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.

(back to top)

About

Flash-ready Armbian images with Bitfocus Buttons USB Relay pre-installed for ARM SBCs

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages