diff --git a/.devcontainer/install_tools.sh b/.devcontainer/install_tools.sh old mode 100644 new mode 100755 diff --git a/.gitignore b/.gitignore index aaee4ba..01992fa 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,17 @@ __pycache__ .idea/deployment.xml backup/*.img.gz backup/*.partial + +# Demo mode artifacts +bbctrl.log* +hooks.json +/*/bbctrl.log* +src/py/camotics/__init__.py +src/py/camotics/gplan.so +src/avr/emu/bbemu +src/avr/emu/build/ + +.pi/pi-python35.tar.gz +src/py/camotics/gplan.so.built +tmp/ +backup/ 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/README.md b/README.md index 8988acf..90e6554 100644 --- a/README.md +++ b/README.md @@ -1 +1,122 @@ -#OneFinity CNC Controller Firmware +# OneFinity CNC Controller Firmware (W-axis fork) + +This is the OneFinity / Buildbotics bbctrl firmware with a virtual W +axis driven by an auxcnc ESP32 over USB serial. See +[docs/AUX_W_AXIS.md](docs/AUX_W_AXIS.md) for the design and config. + +## Layout + +``` +src/avr/ AVR firmware (motion controller, AtxMega) +src/boot/ AVR bootloader +src/bbserial/ Linux kernel module for the bbserial driver +src/py/bbctrl/ Python control daemon (Tornado + websockets) +src/js/ Vue.js UI (legacy) +src/svelte-components/ Newer Svelte UI for dialogs and settings +src/pug/ Pug templates compiled into build/http/index.html +src/resources/ Static assets and config templates +scripts/ Install / update / RPi build helpers +docs/ Architecture, dev setup, W-axis docs +``` + +## Build & flash (quick path, macOS or Linux) + +The full build (`make`) requires `avr-gcc`, but the controller and UI +only depend on the Python + web parts. If you're shipping a UI/Python +change you don't need the AVR toolchain. + +### Prerequisites + +- Node.js (any recent LTS) with npm +- Python 3 with setuptools +- `npm install` once at the project root (this is wired into the + `node_modules` Make target, but on a fresh checkout it's clearer to + do it explicitly) + +```bash +npm install +(cd src/svelte-components && npm install) +``` + +#### macOS gotcha: esbuild platform pin + +The Pi build leaves `node_modules/esbuild` pinned to +`linux-arm64`, which won't run on Darwin. If `npm run build` inside +`src/svelte-components` complains about esbuild, reinstall it for the +host: + +```bash +cd src/svelte-components +rm -rf node_modules/esbuild +npm install esbuild@0.14.49 --no-save +``` + +(Use the version that matches `package-lock.json`.) + +### Build the web UI + Python sdist + +```bash +# Build the Svelte components +(cd src/svelte-components && npm run build) + +# Render pug templates and copy assets into build/http +make all # AVR step will fail without avr-gcc; safe to ignore + # if you didn't change anything under src/avr or src/boot + +# Package +./setup.py sdist +ls dist/bbctrl-*.tar.bz2 +``` + +`make pkg` is the canonical target but it tries to build AVR first. On +hosts without avr-gcc, run the steps above directly. + +If `bbctrl-*.tar.bz2` is missing `src/bbserial/bbserial.ko`, copy the +prebuilt `.ko` from a previous official release into `src/bbserial/` +before running `setup.py sdist` (the install script on the controller +just installs the existing module if a newer one isn't shipped). + +### Flash to a controller + +```bash +curl -X PUT -H "Content-Type: multipart/form-data" \ + -F "firmware=@dist/bbctrl-1.6.7.tar.bz2" \ + -F "password=onefinity" \ + http://onefinity.local/api/firmware/update +``` + +…or use the Make target: + +```bash +make update HOST=onefinity.local PASSWORD=onefinity +``` + +The controller stops bbctrl, untars the package, runs +`scripts/install.sh`, and brings the service back up. Total downtime +is ~30-45s. Watch progress at `http:///` (you'll get 404s while +bbctrl restarts, then the new UI). + +### Verify the flash + +```bash +curl -s http://onefinity.local/ | grep -c "OneFinity" +curl -s http://onefinity.local/api/aux/status # if W axis is enabled +``` + +## Build & flash (full path, Debian/Linux) + +For AVR + GPlan rebuilds, see [docs/development.md](docs/development.md). +That path uses qemu + chroot to cross-compile gplan for ARM and needs +the `gcc-avr` / `avr-libc` toolchain. + +## W axis (auxcnc) + +This fork adds a virtual W axis. See +[docs/AUX_W_AXIS.md](docs/AUX_W_AXIS.md) for: + +- G-code surface (`G28 W0`, `G1 W25`, etc.) +- The G-code preprocessor and hook architecture +- aux.json keys +- REST API (`/api/aux/*`) +- UI surface (jog row in Control, settings panel in Settings) +- Edge cases (ESP reboot mid-job, limit closed at home start, …) diff --git a/docs/AUX_W_AXIS.md b/docs/AUX_W_AXIS.md new file mode 100644 index 0000000..2948cf2 --- /dev/null +++ b/docs/AUX_W_AXIS.md @@ -0,0 +1,172 @@ +# W axis (auxcnc) integration + +This adds a virtual `W` axis to the bbctrl controller, driven by the +auxcnc ESP32 over USB serial (`/dev/ttyUSB0`). The ESP owns step-pulse +generation, real-time limit-switch monitoring, and the homing dance. +The Pi owns units (mm), soft limits, sequencing inside G-code jobs, and +a small REST API for jogging / homing from the UI. + +## How it works + +The bbctrl planner (gplan) only understands `xyzabc`, so adding a true +7th axis would require rebuilding gplan + the AVR firmware. We avoid +that by treating W as a synchronous out-of-band axis: W moves run +*between* G-code blocks, not blended with XYZ. + +Pipeline: + +1. User uploads a G-code file containing `W` words. +2. `FileHandler` runs `AuxPreprocessor` on the upload, rewriting W + tokens in place into `(MSG,HOOK:aux:)` etc. The original line + minus the W word continues to drive XYZ. +3. The planner sees only XYZ + message comments. When it reaches a + message line, the message goes through `state.add_message` which + `Hooks._on_state_change` watches for the `HOOK:` prefix. +4. `Hooks._fire('custom', ...)` finds the registered internal handler + for the event name (`aux`, `aux_rel`, `aux_home`, `aux_setzero`). +5. The handler runs in a hook thread, gating `Mach.unpause` until done. + While the handler is busy the machine is in HOLDING - no XYZ motion + can resume until W finishes. +6. The handler talks to the ESP over `/dev/ttyUSB0` via `AuxAxis`, + blocking on a deterministic reply token (`[step] done`, `[home] + done`, etc). + +MDI commands containing `W` words are rewritten the same way at the +`Mach.mdi()` boundary so manual jog and macros work too. + +## G-code surface + +```gcode +G21 G90 +G28 W0 ; home W axis +G1 W25 F300 ; move W to 25 mm absolute +G1 X100 W12.5 ; mixed: W moves first, then XYZ (configurable) +G91 +G1 W-2.5 ; relative W move +G90 +G92 W0 ; set current W as zero (G92-style) +``` + +Rules: + +- `G28` / `G28.2` with W only -> homing hook; the bare `G28` is NOT + emitted to gplan (that would mean home-all). +- `G28.2 X0 Y0 W0` -> emit hook, then keep `G28.2 X0 Y0` for XY homing. +- A line with both W and XYZ axis words is split into two sequential + blocks. Default order: W first, then XYZ. Toggle via the + `w_first` constructor arg. +- Lines inside parens or after `;` are passed through verbatim. + +## Configuration + +Per-controller config lives at `/aux.json` (created on first +save via the API). Keys: + +| Key | Default | Notes | +|------------------------|----------------|------------------------------------| +| `enabled` | `false` | Master switch | +| `port` | `/dev/ttyUSB0` | Serial device | +| `baud` | `115200` | | +| `steps_per_mm` | `80.0` | Logical steps per mm | +| `dir_sign` | `1` | +1 or -1: maps logical+ to motor+ | +| `min_w`, `max_w` | `0`, `100` | Soft limits in mm | +| `home_dir` | `'-'` | Direction toward limit switch | +| `home_position_mm` | `0.0` | mm value assigned at home | +| `home_fast_sps` | `4000` | Fast seek rate | +| `home_slow_sps` | `400` | Slow re-seek rate | +| `home_backoff_steps` | `200` | Backoff after touching limit | +| `home_maxtravel_steps` | `200000` | Hard cap on phase 1 seek | +| `step_max_sps` | `4000` | Cruise rate for STEPS | +| `step_accel_sps2` | `16000` | Trapezoidal ramp accel | +| `step_start_sps` | `200` | Ramp floor | +| `limit_low` | `true` | Switch active low (closed = LOW) | + +Most of these are pushed to the ESP via `HOMECFG` on connect and +persisted there in NVS. + +## REST API + +| Verb | Path | Body | Effect | +|------|----------------------------|-----------------------|------------------------| +| GET | `/api/aux/config` | - | Current config | +| PUT | `/api/aux/config/save` | `{key: val, ...}` | Save and re-push | +| GET | `/api/aux/status` | - | `{enabled, present, homed, pos_mm}` | +| PUT | `/api/aux/home` | - | Run home cycle (blocks)| +| PUT | `/api/aux/abort` | - | Cancel running motion | +| PUT | `/api/aux/jog` | `{mm: 1.5}` or `{steps: 200}` | Relative move | +| PUT | `/api/aux/move` | `{mm: 12.5}` | Absolute move (mm) | +| PUT | `/api/aux/set-zero` | `{mm: 0}` | Set current pos to mm | + +Steps-mode jog ignores soft limits (use it to inch the axis to the +limit switch when the axis isn't homed yet). + +## UI + +**Control view** + +- A jog row appears under the XYZ jog grid when `aux_enabled` is true, + with three buttons: `W-`, `W+`, and a wide `Home W`. There is + intentionally no separate "set zero" or "W origin" button - homing + lands the axis at `home_position_mm` (0 by default), so home and + zero are the same point. +- The DRO table shows a W axis row with position, status (OFFLINE / + UNHOMED / HOMED), and a single Home button in the actions column + (the cog and map-marker columns are placeholders for layout). + +**Settings view** + +A "W Axis (auxcnc)" section exposes every aux.json field except +`enabled` (which stays read-only - flipping the W axis on/off requires +editingaux.json on the controller, so a fresh install can't surprise +the user with hardware that isn't there). Saving PUTs the merged +config to `/api/aux/config/save`, which writes aux.json and pushes +`HOMECFG` to the ESP. A status line shows whether the axis is +disabled / offline / connected-unhomed / homed at ` mm`. + +## State surface + +These are pushed via `state.set` and visible in the websocket stream: + +- `aux_enabled` - bool, axis is configured + enabled +- `aux_present` - bool, ESP responding on serial +- `aux_homed` - bool, has been homed since last ESP reset +- `aux_pos` - float, current W in mm (4 decimals) + +## Edge cases + +- **ESP reboots mid-job**: `[boot] auxcnc v=N` banner -> `aux_homed` + cleared, message added: "W axis controller restarted - re-home + before use". Subsequent W moves still run; if you want a hard fail + instead, that's a one-line change in `_require_present`. +- **Limit switch closed at boot of HOME**: `[home] failed + reason=already_at_limit` -> hook raises -> Mach surfaces error. +- **Pause mid-W-move**: the hook is blocking, so feed-hold takes + effect *after* the W move completes. For an immediate stop hit + estop; the Hooks listener will call `aux.abort()` which sends + `ABORT\n` to the ESP and the step-pulse loop exits. +- **Connection loss**: if `/dev/ttyUSB0` can't be opened at startup, + `aux_present=False` and any G-code with W will fail-fast at the + hook handler with "Aux axis not connected". +- **No home enforcement**: per design, manual jogs and W moves are + allowed even without a successful home. Soft limits still apply + unless you use the raw step jog endpoint. + +## Files added/changed + +- `src/py/bbctrl/AuxAxis.py` (new): serial worker + RPC layer +- `src/py/bbctrl/AuxPreprocessor.py` (new): G-code rewriter +- `src/py/bbctrl/Hooks.py`: register_internal(), fix the messages + listener so `(MSG,HOOK:...)` actually fires +- `src/py/bbctrl/Ctrl.py`: instantiate AuxAxis, register hooks +- `src/py/bbctrl/Mach.py`: rewrite MDI commands containing W +- `src/py/bbctrl/FileHandler.py`: rewrite uploads in place +- `src/py/bbctrl/Web.py`: REST endpoints +- `src/py/bbctrl/__init__.py`: export AuxAxis +- `src/pug/templates/control-view.pug`: W jog row + DRO row +- `src/js/control-view.js`: aux_home / aux_jog / aux_jog_incr handlers +- `src/js/axis-vars.js`: `_compute_aux_axis` for W state +- `src/svelte-components/src/components/WAxisSettings.svelte`: settings panel +- `src/svelte-components/src/components/SettingsView.svelte`: hosts WAxisSettings +- `auxcnc/src/main.cpp`: new commands HOME, HOMECFG, WPOS, HOMED?, + LIMIT?, ABORT-able STEPS with limit-aware abort, trapezoidal ramps, + NVS-persisted config, `[boot]` banner, deterministic reply tokens diff --git a/docs/mocks/v09_full_ux.html b/docs/mocks/v09_full_ux.html new file mode 100644 index 0000000..2b5841d --- /dev/null +++ b/docs/mocks/v09_full_ux.html @@ -0,0 +1,900 @@ + + + + + +Onefinity · V09 · Full UX + + + + + + +
+ +
+
+
+ ONEFINITY · V09 · Full UX preview +
+ Click the inner tabs to navigate +
+ + + 100% +
+ +
+
+
+ + +
+
+
+ +
ONEFINITY
+
+
+ + + + +
+ + READY + +
+ +
+ + +
+
+ +
+
+
Jog · step 10mm
+
+ +
+
+
+ + + + + + + + + + + + + + + + +
+
+ + +
+
+
+
Axis
Position
Absolute
Offset
State
Toolpath
Actions
+
+
+
X
+
0.000mm
+
0.000
+
0.000
+
Unhomed
+
OK
+
+ + + +
+
+
+
Y
+
0.000mm
+
0.000
+
0.000
+
Unhomed
+
OK
+
+ + + +
+
+
+
Z
+
0.000mm
+
0.000
+
0.000
+
Unhomed
+
Over
+
+ + + +
+
+
+
W
+
0.000mm
+
0.000
+
+
Unhomed
+
OK
+
+ + +
+
+
+ +
+
State
READY
No alerts
+
Velocity / Feed
0 · 0
m/min · mm/min
+
Spindle
0 (0)
RPM (commanded / actual)
+
Job
0 / 1,785
Line · 19:07 remaining
+
+
+
+ + +
+ + + + + + + + +
+
+ + +
+
+ +
+
+ + + + + + +
+ +
+ + + Default folder + thin-rough.nc + By Upload Date +
+ +
+
+
+
+ + + + + + + + + Stock: 250 × 25 × 16 mm + + + + + + START + END + + + + + + X + Z + Y + + +
+
+ + + + + + + +
+
+ thin-rough.nc · 1,785 lines · 12.4 KB + est. 19:07 +
+
+
+
+ +
+
+ + +
+
+ +
+ + + +
+ + +
+
+ G> + G0 X100 Y50 F2000 + +
+
+ + + + + + + + + + + + + + + + +
+
+
19:42:11G21✓ ok
+
19:42:14G90✓ ok
+
19:43:02G0 Y12.800✓ ok
+
19:43:08G0 Z19.040✓ ok
+
19:43:30G1 Z-20 F800✗ blocked: Z over travel
+
19:44:01G0 Z5✓ ok
+
+
+ + +
+
+
+
+
+
Z toolpath exceeds soft-limit
+
2 min ago · sticky
+
+
Loaded program reaches Z = -16.500. Configured soft-limit is Z = -15.000. Adjust the Z origin or set a deeper soft-limit before running.
+
+
+ + +
+
+ +
+
+
+
+
Camera offline
+
12 min ago
+
+
Camera at 10.1.10.55:8554 did not respond on last poll. Live preview disabled.
+
+
+ + +
+
+ +
+
+
+
+
File uploaded · thin-rough.nc
+
21 min ago
+
+
1,785 lines · 12.4 KB · checksum verified.
+
+
+ +
+
+ +
+
+
+
+
WiFi: not connected
+
1 h ago
+
+
Falling back to wired ethernet. SSID workshop-2g last seen 53 min ago.
+
+
+ + +
+
+
+ + +
+
+
Spindle Load
+
0 %
+
idle
+
+
+
+
Spindle Temp
+
24 °C
+
nominal
+
+
+
+
Driver Voltage
+
48.1 V
+
ok
+
+
+
Coolant
+
OFF
+
standby
+
+ +
+
Limit X
+
CLEAR
+
ok
+
+
+
Limit Y
+
CLEAR
+
ok
+
+
+
Limit Z
+
BLOCKED
+
over-travel
+
+
+
Probe
+
OPEN
+
not contacted
+
+ +
+
E-Stop
+
RELEASED
+
safe
+
+
+
Door
+
CLOSED
+
ok
+
+
+
Air Pressure
+
6.2 bar
+
ok
+
+
+
+
Vacuum
+
−0.81 bar
+
hold
+
+
+
+ +
+
+ + +
+
+
+
Display & Units
+
Motion
+
Spindle
+
Safety / Soft-limits
+
Network
+
Camera
+
Macros
+
About
+
+
+
+
Display & Units
+
+
+
Display Units
+
Position, feed and dimensions throughout the UI.
+
+
+
+
+
+
+
Decimal places
+
Position readout precision.
+
+
+
0–4
+
+
+
+
Pulse-dot animation
+
Animate status badges (ready, idle, alarm).
+
+
+
+
+
+
+
Theme
+
Pick a tile finish.
+
+
V09 · Flat soft slate
+
+
+
+ +
+
Network
+
+
+
IP Address
+
Wired ethernet, DHCP.
+
+
10.1.10.55
+
+
+
+
+
WiFi
+
Wireless network connection.
+
+
Not connected
+
+
+
+
+
Hostname
+
Used in mDNS / Bonjour discovery.
+
+
+
+
+
+
+
+
+ +
+
+ +
+
+
+ +
+ + + + diff --git a/plans/2026-04-30_ux_redesign.md b/plans/2026-04-30_ux_redesign.md new file mode 100644 index 0000000..744b587 --- /dev/null +++ b/plans/2026-04-30_ux_redesign.md @@ -0,0 +1,169 @@ +# UX Redesign — Implementation Plan + +Reference mock: `docs/mocks/v09_full_ux.html` +Target hardware: 10.8" portable monitor, 1920×1080, capacitive touch, Chrome fullscreen. + +## 1. Goals + +The redesign keeps every existing feature but reorganizes the page into a single-screen control surface for finger-touch use: + +- A slim 96 px header replaces the 140 px nav-header. Only logo + ONEFINITY wordmark + tab bar + system pill + READY badge + octagonal STOP. +- 4 top-level sections accessed via underline-ribbon tabs in the header: + 1. **Control** — jog pad, DRO table, status strip, macro row. + 2. **Program** — Auto run controls, file actions, G-code listing, 3D viewer. + 3. **Console** — MDI, Messages, Indicators (sub-tabs). + 4. **Settings** — paged settings (replaces the Pure left rail). +- Touch targets ≥ 64 px (jog tiles 72 px, axis action icons 72 px, macro buttons 84 px). +- All action chip-soup (WiFi/Camera/Rotary/IP/Version) collapses into one "All systems · view" pill that opens a popover. Burger menu removed (Settings tab supersedes it). +- V09 jog/macro palette: flat soft slate (#3f4b63), no drop shadow; yellow (#fde047) accent for active states (step seg, tab underline, macro number badge). +- Spindle override / feed override sliders live in a bottom-edge drawer triggered by tapping the Spindle KPI tile (no permanent screen real estate). +- Hard cut: no `config.ui.layout` flag; the new shell replaces the old in a single release. + +## 2. Scope of code change + +The build is Pug + Stylus + Browserify Vue (Vue 1.x). `index.pug` defines the chrome; `src/pug/templates/*.pug` defines each view; `src/js/*.js` mirrors them as Vue components routed by `currentView` from the URL hash. + +Files we will touch: + +- `src/pug/index.pug` — replace `#layout / #menu / #main / .nav-header` with the new header + tab bar + body. Drop the burger and the side-menu include. +- `src/pug/templates/control-view.pug` — restructure into the new Control panel (jog grid + DRO table + status strip + macro row). MDI/Messages/Indicators move out. +- New `src/pug/templates/program-view.pug` — Auto sub-panel content (action bar, file bar, gcode-viewer, path-viewer). +- New `src/pug/templates/console-view.pug` — MDI / Messages / Indicators sub-tabs hosting existing `console.pug` and `indicators.pug` partials. +- `src/js/app.js` — extend `parse_hash` so `#program`, `#console`, `#settings` resolve; expose tab state for the header to highlight. +- `src/js/control-view.js` — keep jog/DRO logic, drop the Auto/MDI/Messages/Indicators internal `tab` state and template hooks. +- New `src/js/program-view.js`, `src/js/console-view.js` — extracted Vue components. +- `src/stylus/style.styl` — add `.app-shell`, `.head`, `.tabs-host`, `.ktab`, panel styles, V09 jog tokens. Keep legacy classes alive until templates fully migrated. +- `src/static/css/side-menu.css` — stop including in `index.pug`. +- Settings: keep `settings-view.pug`, `admin-general-view.pug`, `admin-network-view.pug`, `motor-view.pug`, `tool-view.pug`, `io-view.pug`, etc., and surface them through a left-rail navigator inside the Settings panel rather than the sidebar. +- Settings → Macros owns the full macro list (1…N). Control's macro row is a slice of the first 8; reordering happens in Settings. + +## 3. Routing model + +We keep the existing URL hash routing because everything in `src/js/app.js#parse_hash` and the deep-linked menu items (`#motor:0`, `#admin-network`, etc.) depend on it. + +| URL hash | Top tab | Notes | +|-------------------------|------------|-------------------------------------------------------| +| `#control` | Control | Default | +| `#program` / `#program:auto` | Program | Auto sub-view (only sub-view for now) | +| `#console` / `#console:mdi` | Console | MDI default, also `:messages` and `:indicators` | +| `#settings` | Settings | Settings home (Display & Units) | +| `#admin-general`, `#admin-network`, `#motor:N`, `#tool`, `#io`, `#help`, `#cheat-sheet` | Settings | Existing routes remain, surfaced in the Settings left rail | + +The header tab bar maps URL prefix → active tab. A tiny helper `topTabFromHash(hash)` lives in `app.js` and is reused by the header template. + +## 4. Step-by-step + +### Phase 1 — Mock parity (1–2 days) +1. Add `docs/mocks/v09_full_ux.html` (done) so anyone can preview the target. +2. Move the V09 palette into Stylus tokens at the top of `style.styl`: + ```styl + $jog-bg = #3f4b63 + $jog-hover = #4a5777 + $jog-dir = #5b6885 + $jog-ghost = #8c97ad + $accent = #fde047 + $accent-ink = #0f172a + ``` +3. Build the header in `index.pug`: + ```pug + .app-shell + header.head + .brand-blk + .brand-logo + .brand-name ONEFINITY + nav.tabs-host(role="tablist") + a.ktab(:class="{active: topTab === 'control'}", href="#control") + .fa.fa-gamepad + | Control + a.ktab(:class="{active: topTab === 'program'}", href="#program") … + a.ktab(:class="{active: topTab === 'console'}", href="#console") … + a.ktab(:class="{active: topTab === 'settings'}", href="#settings") … + button.sys-btn(@click="toggle_sys_popover") … + span.state-badge(:class="state_class") + estop(@click="estop") + ``` +4. Style the header tabs as **underline ribbon** (V02): transparent fills, slate-gray text, dark text + 5 px yellow underline on active. CSS already proven in the mock. +5. Move the rotary toggle and pi-temp warning into the system pill popover. + +### Phase 2 — Control panel (2 days) +1. Rewrite the outer markup of `control-view.pug` to a CSS grid: + ``` + .control-grid → 720px jog-card | 1fr right-col(dro-card + status-strip) + ``` + Drop the ``-based outer layout (axes table stays — it's a real data table). +2. Replace the legacy ` + {#if saveMessage} + {saveMessage} + {/if} + + +
+ Changes are written to aux.json. Homing rates and the + limit polarity are pushed to the ESP immediately; any + running motion is unaffected. Re-home the W axis after + changing direction, sign, or step settings. +
+ + {/if} + + +