Files
onefinity-firmware/docs/AUX_A_AXIS.md
Henrik Muehe 130c39fad9 ATC: split tool-change M-codes into composable atoms
Match auxcnc firmware v3, which dropped the monolithic DROPTOOL /
GRABTOOL ESP tasks in favour of three atoms: RELEASE, CLAMP, EJECT.
This lets host macros interleave Z moves between ejector pulses
(the old DROPTOOL ran open->oscillate->clamp in a single ESP task,
so you couldn't lift Z mid-eject).

  AuxAxis: replace atc_droptool() / atc_grabtool() with atc_eject(
    pulse_ms=, dwell_ms=). atc_release() / atc_clamp() are unchanged.

  Ctrl: register internal hooks for release / clamp / eject only.
    The eject hook parses 'pulse=' and 'dwell=' kwargs out of the
    HOOK:eject:<data> payload so macros can emit
    (MSG,HOOK:eject:pulse=400 dwell=300) for tuned wiggles.

  AuxPreprocessor: M100 now maps to eject (was droptool); M101 is
    unmapped (was grabtool, now a pure host-side macro); M102/M103
    are unchanged. Header comment updated.

  docs/AUX_A_AXIS.md: mention the new atom set.

The drop.nc and grab.nc gcode macros on the controller are
correspondingly rewritten on-device as compositions:
  drop = M102 + 4xM100 + G53 G0 Z0 + M103
  grab = M102 + G4 P2 + M103
2026-05-03 18:15:55 +02:00

8.8 KiB

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 pneumatic atoms (M100 EJECT, M102 RELEASE, M103 CLAMP) - see bbctrl/AuxPreprocessor.py. Macros compose drop/grab tool sequences from those atoms.

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

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