From 24215a8b36fe9ce07d55b37aa90b5b67fe1f055f Mon Sep 17 00:00:00 2001 From: Henrik Muehe Date: Sun, 3 May 2026 14:03:20 +0200 Subject: [PATCH] build: document Pi firmware build/flash + gplan.so cross-build via Stretch Docker - .pi/BUILD.md: end-to-end macOS dev workflow, deploy paths, dphys-swapfile vs fstab, troubleshooting. - .pi/Dockerfile.gplan + build-gplan.sh: rebuild gplan.so from source on Raspbian Stretch (Bullseye is too new for the toolchain). - Makefile: ensure trailing newline between concatenated pug templates so Pug doesn't glue file boundaries together. --- .pi/BUILD.md | 249 +++++++++++++++++++++++++++++++++++++++++++ .pi/Dockerfile.gplan | 48 +++++++++ .pi/build-gplan.sh | 30 ++++++ Makefile | 6 +- 4 files changed, 332 insertions(+), 1 deletion(-) create mode 100644 .pi/BUILD.md create mode 100644 .pi/Dockerfile.gplan create mode 100755 .pi/build-gplan.sh diff --git a/.pi/BUILD.md b/.pi/BUILD.md new file mode 100644 index 0000000..63e2624 --- /dev/null +++ b/.pi/BUILD.md @@ -0,0 +1,249 @@ +# Onefinity CNC Firmware — Build, Flash & Backup + +## Quick Start + +```bash +# 1. Build gplan.so (first time ~25min, then ~1sec) +.pi/build-gplan.sh + +# 2. Build firmware package (frontend + AVR + Python, ~1min) +docker run --rm -v "$(pwd):/workspace" -w /workspace onefin-dev \ + bash -c 'make all && python3 ./setup.py sdist' + +# 3. Flash to controller +curl -X PUT -F "firmware=@dist/bbctrl-1.6.7.tar.bz2" \ + -F "password=onefinity" http://10.1.10.55/api/firmware/update +``` + +## Architecture Overview + +The controller is a **Raspberry Pi 2/3** (armv7l, Raspbian Stretch, Python 3.5) +connected to an **ATxmega192a3u** AVR over serial. The Pi runs a Tornado web +server that serves the UI, parses G-code, and plans 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: + +| Component | Source | Description | +|---|---|---| +| Python backend | `src/py/bbctrl/` | Tornado web server, state machine, planner bridge | +| Web frontend | `build/http/` | Pug + Stylus + Svelte → 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 C++ extension) | +| Install scripts | `scripts/install.sh` | AVR flash, Python install, service restart | + +## Prerequisites + +- **Docker** with QEMU binfmt support (default on Docker Desktop for Mac) +- **devcontainer image**: `docker build -t onefin-dev -f .devcontainer/Dockerfile .devcontainer/` +- **SSH access**: `ssh bbmc@10.1.10.55` (password: `onefinity`) + +## Building + +### Step 1: gplan.so + +`gplan.so` is the CAMotics G-code planner — a C++ Python extension that must +be a **32-bit ARM binary linked against Python 3.5**. It cannot be built in the +devcontainer (wrong arch + wrong Python + wrong glibc). + +**Build from source** (recommended): + +```bash +.pi/build-gplan.sh +``` + +This uses a Raspbian Stretch Docker image (`balenalib/raspberry-pi-debian:stretch`) +with the Pi's exact toolchain: GCC 6.3, Python 3.5, GLIBC 2.24. The image is +built once (~25min under QEMU), then cached — subsequent runs take ~1sec. + +The image pre-compiles two C++ dependencies: +- [cbang](https://github.com/CauldronDevelopmentLLC/cbang) @ `18f1e96` — C++ utility library +- [camotics](https://github.com/CauldronDevelopmentLLC/camotics) @ `ec876c8` — G-code planner with S-curve motion planning + +To force a full rebuild: `docker rmi onefin-gplan && .pi/build-gplan.sh` + +**Alternatives** (if Docker build fails): + +```bash +# From official release +curl -sL https://github.com/OneFinityCNC/onefinity-firmware/releases/download/v1.6.6/onefinity-1.6.6.tar.bz2 \ + | tar xjf - --include='*/gplan.so' -O > src/py/camotics/gplan.so + +# From the running Pi +scp bbmc@10.1.10.55:/usr/local/lib/python3.5/dist-packages/bbctrl-*.egg/camotics/gplan.so src/py/camotics/ +``` + +**Verify** — must show `ELF 32-bit ... ARM ... libpython3.5m`: + +```bash +file src/py/camotics/gplan.so +readelf -d src/py/camotics/gplan.so | grep python +``` + +### Step 2: Firmware package + +```bash +docker run --rm -v "$(pwd):/workspace" -w /workspace onefin-dev \ + bash -c 'make all && python3 ./setup.py sdist' +``` + +This builds inside the devcontainer (arm64 Bullseye — fine for frontend/AVR/Python): + +| Component | Tool | Time | +|---|---|---| +| Node modules | `npm install` | ~30sec | +| Svelte components | `vite build` | ~5sec | +| Pug/Stylus → HTML | `pug-cli`, `stylus` | ~2sec | +| AVR firmware | `avr-g++` (ATxmega192a3u) | ~10sec | +| Boot/Power/Jig MCUs | `avr-gcc` | ~5sec | +| Python sdist | `setup.py sdist` | ~2sec | + +Produces: `dist/bbctrl-X.Y.Z.tar.bz2` (~3-4MB) + +### bbserial.ko (kernel module — usually skip) + +Cross-compiles against the Pi's kernel headers (4.9.59-v7+). The Pi already has +a working `bbserial.ko` installed. `install.sh` skips it gracefully if missing. + +## Flashing + +### Via web API (machine running) + +```bash +curl -X PUT -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` + +### Via SSH (web UI down or crash-looping) + +```bash +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 from bad flash + +SSH still works even when bbctrl is crash-looping: +1. Check the error: `sudo python3 /usr/local/bin/bbctrl 2>&1 | head -20` +2. Common cause: wrong gplan.so architecture → replace with correct one (see above) +3. Nuclear option: restore SD card from backup + +## Running Locally (demo mode) + +Full stack in Docker with AVR emulator — no Pi needed: + +```bash +# Build bbemu (AVR emulator, native in devcontainer) +docker run --rm -v "$(pwd):/workspace" -w /workspace/src/avr/emu onefin-dev make + +# Run demo (needs arm64 gplan.so for the container, not armhf) +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 + ' +``` + +Open http://localhost:8765 — full UI with emulated controller. + +Note: demo mode needs a **container-arch** gplan.so (arm64 + Python 3.9), not the +Pi one. The devcontainer build from the Makefile's `gplan` target produces this, +or it can be built following the procedure in `scripts/gplan-build.sh`. + +## SD Card Backup & Restore + +```bash +# Backup (~50 min, streams raw dd from Pi, compresses locally) +./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 + +# Restore back to Pi over SSH +./backup/onefinity-backup.sh restore backup/onefinity-20260430.img.gz +``` + +Environment: `ONEFINITY_HOST` (default 10.1.10.55), `ONEFINITY_USER` (bbmc), +`ONEFINITY_PASS` (onefinity). + +## Python 3.5 Compatibility + +The Pi runs Python 3.5.3. Avoid features added in later versions: + +| Avoid | Use instead | +|---|---| +| `f"hello {name}"` | `"hello %s" % name` or `"hello {}".format(name)` | +| `subprocess.run(capture_output=True)` | `stdout=subprocess.PIPE, stderr=subprocess.PIPE` | +| `subprocess.run(text=True)` | `.stdout.decode('utf-8')` | +| `dataclasses` | plain classes with `__init__` | +| `:=` walrus operator | separate assignment | +| `asyncio.run()` | `loop.run_until_complete()` | +| `dict[str, int]` | `Dict[str, int]` from `typing` | + +## Pi Details + +| | | +|---|---| +| Host | `10.1.10.55` | +| SSH | `bbmc` / `onefinity` | +| OS | Raspbian Stretch (Debian 9) | +| Kernel | 4.9.59-v7+ | +| Python | 3.5.3 | +| GCC | 6.3.0 | +| GLIBC | 2.24 (max symbol: GLIBC_2.24) | +| GLIBCXX | 3.4.22 | +| Arch | armv7l (32-bit ARM, EABI5) | +| SD card | 30GB (~2.8GB used) | +| 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 | + +## Why Not Build gplan.so on Bullseye? + +Documented for reference — we tried two approaches that don't work: + +**1. devcontainer (arm64 Bullseye):** Wrong ELF class (64-bit vs 32-bit) and wrong +Python (3.9 vs 3.5). Cross-compiling with `CXX=arm-linux-gnueabihf-g++` fails +because SCons ignores CC/CXX environment variables. + +**2. Bullseye armhf container:** Correct architecture, but GCC 10 / glibc 2.31 +produce objects requiring GLIBC_2.29+ and GLIBCXX_3.4.26+ symbols. The Pi's +Stretch only has GLIBC_2.24 / GLIBCXX_3.4.22. Even `-static-libstdc++ +-static-libgcc` doesn't help — glibc symbols leak through the object files. +Relinking against Python 3.5m works but the GLIBC mismatch remains. + +**3. Plain `debian:stretch` armhf:** The archived repos have broken package +metadata — `apt-get install build-essential` fails with unresolvable version +conflicts. + +**Solution:** `balenalib/raspberry-pi-debian:stretch` with `legacy.raspbian.org` +repos. See `.pi/Dockerfile.gplan`. diff --git a/.pi/Dockerfile.gplan b/.pi/Dockerfile.gplan new file mode 100644 index 0000000..97ee8f5 --- /dev/null +++ b/.pi/Dockerfile.gplan @@ -0,0 +1,48 @@ +# Raspbian Stretch armhf build environment for gplan.so +# Matches the Pi exactly: GCC 6.3, Python 3.5, GLIBC 2.24 +# +# Build image: docker build -t onefin-gplan -f .pi/Dockerfile.gplan .pi/ +# Build gplan: .pi/build-gplan.sh +FROM balenalib/raspberry-pi-debian:stretch + +# Fix repos to use archived Raspbian mirrors +RUN echo "deb http://legacy.raspbian.org/raspbian/ stretch main contrib non-free rpi" \ + > /etc/apt/sources.list && \ + rm -f /etc/apt/sources.list.d/*.list + +RUN apt-get -o Acquire::Check-Valid-Until=false \ + -o Acquire::AllowInsecureRepositories=true update && \ + apt-get -o Acquire::Check-Valid-Until=false --allow-unauthenticated \ + install -y --no-install-recommends \ + build-essential python3-dev scons git ca-certificates \ + libssl-dev libexpat1-dev libbz2-dev liblz4-dev zlib1g-dev perl file && \ + rm -rf /var/lib/apt/lists/* + +# Clone and build cbang +RUN mkdir -p /opt/cbang && cd /opt/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 && \ + scons -j2 disable_local="re2 libevent" && \ + rm -rf .git build/dep + +# Clone, patch, and build camotics/gplan +RUN mkdir -p /opt/camotics && cd /opt/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 && \ + mkdir -p build && touch build/version.txt && \ + P="src/gcode/plan" && \ + 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 && \ + rm -rf .git + +ENV CBANG_HOME=/opt/cbang + +# Pre-compile everything including gplan.so +RUN cd /opt/camotics && scons -j2 gplan.so with_gui=0 with_tpl=0 + +WORKDIR /opt/camotics diff --git a/.pi/build-gplan.sh b/.pi/build-gplan.sh new file mode 100755 index 0000000..fa15a99 --- /dev/null +++ b/.pi/build-gplan.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Build gplan.so for the Onefinity Pi (armv7l, Python 3.5, GCC 6.3) +# +# Uses a Raspbian Stretch Docker image that exactly matches the Pi's +# toolchain. No cross-compile, no relink hacks, no GLIBC mismatches. +# +# First run: ~30min (builds Docker image with cbang + camotics) +# After that: ~1sec (copies pre-built gplan.so from image) + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" +IMAGE="onefin-gplan" +OUTPUT="$PROJECT_DIR/src/py/camotics/gplan.so" + +# Build image if needed (one-time) +if ! docker image inspect "$IMAGE" >/dev/null 2>&1; then + echo "Building $IMAGE Docker image (one-time, ~30min under QEMU)..." + docker build -t "$IMAGE" -f "$SCRIPT_DIR/Dockerfile.gplan" "$SCRIPT_DIR" +fi + +# Copy gplan.so out of the image +echo "Extracting gplan.so..." +docker run --rm -v "$PROJECT_DIR:/workspace" "$IMAGE" \ + bash -c 'cp /opt/camotics/gplan.so /workspace/src/py/camotics/gplan.so && \ + file /workspace/src/py/camotics/gplan.so && \ + readelf -d /workspace/src/py/camotics/gplan.so | grep -E "NEEDED|python"' + +echo "✓ Built: $OUTPUT" diff --git a/Makefile b/Makefile index 406ad63..bf9e472 100644 --- a/Makefile +++ b/Makefile @@ -68,7 +68,11 @@ update: pkg build/templates.pug: $(TEMPLS) mkdir -p build - cat $(TEMPLS) >$@ + # Use awk to ensure each template is followed by a newline so the + # next file's first line never gets glued onto the previous file's + # last line (some templates ship without a trailing newline, which + # would produce subtle Pug parse failures). + awk 'FNR==1 && NR>1 {print ""} {print} END{print ""}' $(TEMPLS) >$@ node_modules: package.json npm install && touch node_modules