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.
This commit is contained in:
249
.pi/BUILD.md
Normal file
249
.pi/BUILD.md
Normal file
@@ -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`.
|
||||
48
.pi/Dockerfile.gplan
Normal file
48
.pi/Dockerfile.gplan
Normal file
@@ -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
|
||||
30
.pi/build-gplan.sh
Executable file
30
.pi/build-gplan.sh
Executable file
@@ -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"
|
||||
6
Makefile
6
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
|
||||
|
||||
Reference in New Issue
Block a user