Rather than rebuild gplan + the AVR firmware to add a true 7th axis,
we treat W as a synchronous out-of-band axis that moves between G-code
blocks. The pipeline:
upload -> AuxPreprocessor rewrites W tokens into (MSG,HOOK:aux:N)
comments -> planner sees only XYZ + messages -> Hooks fires the
registered internal handler -> AuxAxis sends STEPS/HOME over serial
to the ESP and blocks the planner until done.
New files:
src/py/bbctrl/AuxAxis.py serial worker + RPC layer
src/py/bbctrl/AuxPreprocessor.py G-code rewriter
docs/AUX_W_AXIS.md design + ops notes
Changed:
Hooks.py register_internal(); fix the (MSG,HOOK:...) listener
to read the 'messages' state list (was broken before)
Ctrl.py instantiate AuxAxis, register aux/aux_rel/aux_home/
aux_setzero hooks
FileHandler.py rewrite uploads in place when they use W
Mach.py rewrite W tokens in MDI input the same way
Web.py REST endpoints under /api/aux/*
The ESP firmware in ../auxcnc was extended in lockstep: HOME, HOMECFG
(NVS-persisted), WPOS, HOMED?, LIMIT?, abortable STEPS with
limit-aware abort, trapezoidal ramps, deterministic [topic] reply
tokens, [boot] banner.
Real-time decisions (limit switch, step pulses) live on the ESP. The
host owns mm units, soft limits, and aux_homed bookkeeping. ESP
reboot mid-job clears aux_homed and surfaces a message; per design
manual jogs are still allowed without homing.
145 lines
6.9 KiB
Markdown
145 lines
6.9 KiB
Markdown
# 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:<mm>)` 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 `<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).
|
|
|
|
## State surface (UI)
|
|
|
|
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
|
|
- `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
|