Files
onefinity-firmware/docs/AUX_W_AXIS.md
Claude c7cf9483b3 Add W axis integration via auxcnc ESP32 over /dev/ttyUSB0
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.
2026-04-30 16:51:24 +02:00

6.9 KiB

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

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