# A axis (auxcnc) integration > **Note:** This document describes the original out-of-band W-axis > architecture (gcode preprocessor rewriting W tokens into HOOK > messages dispatched between blocks). The current implementation > integrates the auxcnc-driven stepper as a *virtual A axis* through > gplan via a synthetic motor (`bbctrl/ExternalAxis.py`), so A is > blended with XYZ in the same S-curve plan and the gcode surface > below applies as plain `A` words. > > The HOOK pipeline still exists for ATC pneumatics (M100..M103), > see `bbctrl/AuxPreprocessor.py`. This adds a virtual `A` 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: A moves run *between* G-code blocks, not blended with XYZ. Pipeline: 1. User uploads a G-code file containing `A` words. 2. `FileHandler` runs `AuxPreprocessor` on the upload, rewriting W tokens in place into `(MSG,HOOK:aux:)` etc. The original line minus the A 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 A 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 `A` 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 A0 ; home A axis G1 A25 F300 ; move A to 25 mm absolute G1 X100 W12.5 ; mixed: A moves first, then XYZ (configurable) G91 G1 A-2.5 ; relative A move G90 G92 A0 ; set current A 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: `A-`, `A+`, 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 A 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 A 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: "A axis controller restarted - re-home before use". Subsequent A 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 A 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 A 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