# 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:<mm>)` 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 `<ctrl_path>/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 `<pos> 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
