Files
onefinity-firmware/.pi/BUILD.md
Henrik Muehe 7306464440 Document gplan.so build-from-source procedure
Build in armv7 QEMU Docker, compile with Python 3.9 SCons,
relink final .so against Python 3.5m from the Pi.
2026-04-30 13:52:58 +02:00

9.5 KiB

Onefinity CNC Firmware — Build, Flash & Backup

Architecture Overview

The Onefinity controller is a Raspberry Pi 2/3 (armv7l, Raspbian Stretch, Python 3.5) connected to an ATxmega192a3u AVR microcontroller over serial. The Pi runs a Tornado web server (bbctrl) that serves the UI and plans G-code motion. The AVR executes realtime step/direction pulses.

Browser ←WebSocket→ Pi (Tornado/Python) → GCode Planner → Serial → AVR → Stepper drivers

The firmware package (bbctrl-X.Y.Z.tar.bz2) contains:

  • Python backend (src/py/bbctrl/) — Tornado web server, planner, state machine
  • Web frontend (build/http/) — Pug/Stylus/Svelte compiled to static HTML/JS/CSS
  • AVR firmware (src/avr/bbctrl-avr-firmware.hex) — realtime motion controller
  • gplan.so (src/py/camotics/gplan.so) — CAMotics G-code planner (native ARM .so)
  • Install scripts (scripts/install.sh, etc.)

Prerequisites

  • Docker (for the devcontainer build environment)
  • The devcontainer image: docker build -t onefin-dev -f .devcontainer/Dockerfile .devcontainer/
  • SSH access to the Pi: ssh bbmc@10.1.10.55 (password: onefinity)

Building

Full build (frontend + AVR + package)

docker run --rm -v "$(pwd):/workspace" -w /workspace onefin-dev \
  bash -c 'make all && python3 ./setup.py sdist'

Produces: dist/bbctrl-1.6.7.tar.bz2 (~3MB)

What make all builds

Component Command Output
Web frontend npm install, pug/stylus/svelte compile build/http/
AVR firmware make -C src/avr (avr-g++) src/avr/bbctrl-avr-firmware.hex
Bootloader make -C src/boot src/boot/bbctrl-avr-boot.hex
Power MCU make -C src/pwr src/pwr/bbctrl-pwr-firmware.hex
Jig firmware make -C src/jig src/jig/bbctrl-jig-firmware.hex

gplan.so — the critical gotcha

gplan.so is the CAMotics G-code planner compiled as a Python C extension. It must be a 32-bit ARM binary linked against Python 3.5 to run on the Pi.

Do NOT build gplan.so in the devcontainer. The devcontainer runs arm64/Debian Bullseye with Python 3.9. The resulting .so will be the wrong architecture and wrong Python ABI. Cross-compiling with CXX=arm-linux-gnueabihf-g++ also fails because SCons ignores CC/CXX env vars.

The correct file must be:

ELF 32-bit LSB shared object, ARM, EABI5  (linked against libpython3.5m.so.1.0)

If you see ELF 64-bit LSB shared object, ARM aarch64 or libpython3.9 — wrong.

Option A: Build from source (recommended)

Uses Docker's armv7 QEMU emulation on Apple Silicon. Requires a one-time copy of Python 3.5 headers from the Pi (~1.7MB):

# One-time: grab Python 3.5 headers + lib from Pi
ssh bbmc@10.1.10.55 'tar czf - /usr/include/python3.5m \
  /usr/lib/arm-linux-gnueabihf/libpython3.5m.so*' > /tmp/pi-python35.tar.gz

Then build (takes ~30min under QEMU):

docker run --rm --platform linux/arm/v7 \
  -v "$(pwd):/workspace" -w /workspace \
  -v /tmp/pi-python35.tar.gz:/tmp/pi-python35.tar.gz \
  debian:bullseye bash -c '
    set -e
    tar xzf /tmp/pi-python35.tar.gz -C /
    apt-get update -qq && apt-get install -y -qq build-essential scons git \
      ca-certificates python3-dev libssl-dev libexpat1-dev libbz2-dev \
      liblz4-dev zlib1g-dev perl file
    ln -sf /usr/lib/arm-linux-gnueabihf/libpython3.5m.so.1.0 \
           /usr/lib/arm-linux-gnueabihf/libpython3.5m.so

    # Clone cbang + camotics at pinned commits
    mkdir -p /tmp/cbang && cd /tmp/cbang && git init -q
    git remote add origin https://github.com/CauldronDevelopmentLLC/cbang
    git fetch --depth 1 -q origin 18f1e963107ef26abe750c023355a5c40dd07853
    git reset --hard FETCH_HEAD -q

    mkdir -p /tmp/camotics && cd /tmp/camotics && git init -q
    git remote add origin https://github.com/CauldronDevelopmentLLC/camotics
    git fetch --depth 1 -q origin ec876c80d20fc19837133087cef0c447df5a939d
    git reset --hard FETCH_HEAD -q

    # Build cbang
    cd /tmp/cbang && scons -j2 disable_local="re2 libevent"
    export CBANG_HOME="/tmp/cbang"

    # Patch camotics (clamp div-by-zero in planner)
    P="/tmp/camotics/src/gcode/plan"
    mkdir -p /tmp/camotics/build && touch /tmp/camotics/build/version.txt
    for F in LineCommand.cpp LinePlanner.cpp; do
      for V in maxVel maxJerk maxAccel; do
        perl -i -0pe "s/(fabs\\((config\\.$V\\[axis\\]) \\/ unit\\[axis\\]\\));/std::min(\\2, \\1);/gm" $P/$F
      done
    done

    # Compile with Python 3.9 (SCons needs python3-dev to find headers)
    cd /tmp/camotics && scons -j2 gplan.so with_gui=0 with_tpl=0

    # Relink against Python 3.5m (the Pi target)
    g++ -o /workspace/src/py/camotics/gplan.so \
      -Wl,--as-needed -Wl,-s -Wl,-x -Wl,--gc-sections -pthread -shared \
      build/gplan.os -L/tmp/cbang/lib \
      build/libCAMoticsPy.a build/libCAMotics.a build/libDXF.a \
      build/libSTL.a build/libGCode.a \
      -lstdc++ -lutil -lm -ldl -lz -lcbang -lcbang-boost \
      -lssl -lcrypto -llz4 -lexpat -lbz2 -lcrypt -lpthread \
      -lpython3.5m build/dxflib/libdxflib.a
  '

Why the relink step: SCons compiles .o files that are Python-version-agnostic (they only use #include <Python.h> which is ABI-compatible between 3.5-3.9 for the subset camotics uses). The only version-specific part is the final -lpython3.X link. So we let SCons build with 3.9 (since it ignores overrides) then relink the same objects against 3.5m.

Option B: From official release

curl -L https://github.com/OneFinityCNC/onefinity-firmware/releases/download/v1.6.6/onefinity-1.6.6.tar.bz2 \
  | tar xjf - --include='*/gplan.so' --strip-components=3 -C src/py/camotics/

Option C: From a working Pi

scp bbmc@10.1.10.55:/usr/local/lib/python3.5/dist-packages/bbctrl-*.egg/camotics/gplan.so src/py/camotics/

bbserial.ko — kernel module

The bbserial.ko kernel module requires cross-compiling against the Pi's exact kernel headers (4.9.59-v7+). The make pkg target tries to build it but it's rarely needed — the Pi already has a matching .ko installed. The install.sh script skips it gracefully if the file is missing (cp: cannot stat 'src/bbserial/bbserial.ko': No such file or directory).

AVR emulator (for local demo mode)

docker run --rm -v "$(pwd):/workspace" -w /workspace/src/avr/emu onefin-dev make

Produces src/avr/emu/bbemu — a native binary that emulates the AVR for demo mode.

Flashing

curl -X PUT \
  -H "Content-Type: multipart/form-data" \
  -F "firmware=@dist/bbctrl-1.6.7.tar.bz2" \
  -F "password=onefinity" \
  http://10.1.10.55/api/firmware/update

Or: make update HOST=10.1.10.55 PASSWORD=onefinity

Via SSH (if web UI is down / crash-looping)

scp dist/bbctrl-1.6.7.tar.bz2 bbmc@10.1.10.55:/tmp/

ssh bbmc@10.1.10.55 'echo onefinity | sudo -S bash -c "
  systemctl stop bbctrl
  mkdir -p /var/lib/bbctrl/firmware
  cp /tmp/bbctrl-1.6.7.tar.bz2 /var/lib/bbctrl/firmware/update.tar.bz2
  /usr/local/bin/update-bbctrl
"'

What happens during flash

  1. update-bbctrl stops bbctrl, extracts tarball to /tmp/update/
  2. install.sh runs:
    • Flashes AVR via scripts/avr109-flash.py (serial bootloader protocol)
    • setup.py install --force — installs Python package + frontend + gplan.so
    • Restarts bbctrl systemd service
    • May reboot if boot config or kernel module changed

Recovery if flash breaks the Pi

If bbctrl is crash-looping after a bad flash:

  1. SSH still works: ssh bbmc@10.1.10.55
  2. Check the error: sudo python3 /usr/local/bin/bbctrl 2>&1 | head -20
  3. Common fix: replace gplan.so with correct ARM binary (see above)
  4. Nuclear option: restore from SD card backup (see below)

Running locally (demo mode)

Run the full stack in Docker with the AVR emulator:

# Build everything first (make all + bbemu + gplan.so for arm64 devcontainer)
# Then:
docker run --rm -d --name onefin-demo \
  -v "$(pwd):/workspace" -w /workspace -p 8765:80 \
  onefin-dev bash -c '
    pip3 install -q tornado sockjs-tornado pyserial watchdog
    cp src/avr/emu/bbemu /usr/local/bin/
    pip3 install -q -e .
    exec bbctrl --demo --port 80 --addr 0.0.0.0 --disable-camera
  '

Note: demo mode needs its own gplan.so matching the container's arch (arm64 + Python 3.9). Build it with the gplan build procedure in the Makefile, or use the one already in src/py/camotics/ if it matches.

Open http://localhost:8765 — full UI with emulated AVR.

SD Card Backup & Restore

# Backup (streams raw dd from Pi, compresses locally with gzip, ~50 min)
./backup/onefinity-backup.sh backup

# Verify
./backup/onefinity-backup.sh verify backup/onefinity-20260430.img.gz

# Restore to local SD card
./backup/onefinity-backup.sh restore backup/onefinity-20260430.img.gz /dev/diskN

See backup/onefinity-backup.sh for details. Environment variables: ONEFINITY_HOST (default 10.1.10.55), ONEFINITY_USER (bbmc), ONEFINITY_PASS (onefinity).

Pi Details

Host 10.1.10.55
SSH user bbmc
sudo password onefinity
OS Raspbian Stretch (Debian 9)
Kernel 4.9.59-v7+
Python 3.5.3
Arch armv7l (32-bit ARM)
SD card 30GB
Service systemctl {start,stop,restart,status} bbctrl
Log /var/log/bbctrl.log or journalctl -u bbctrl
Config /var/lib/bbctrl/config.json
Uploads /var/lib/bbctrl/upload/
Web root /usr/local/lib/python3.5/dist-packages/bbctrl-*.egg/bbctrl/http/
AVR serial /dev/ttyAMA0 at 230400 baud