Auxiliary axis is the auxcnc-driven stepper exposed to gplan as A,
not W. Half the stack already used A (gcode, DRO row, soft limits,
homing); the other half (settings tab, macros, internal field
names) still said W which was confusing.
Renames:
- aux.json fields: min_w/max_w -> min_mm/max_mm
- svelte component: WAxisSettings -> AAxisSettings
- settings tab slug: #w-axis -> #a-axis
- js view module: w-axis-view.js -> a-axis-view.js
- pug template: w-axis-view.pug -> a-axis-view.pug
- macros: w_down.nc/w_up.nc -> a_down.nc/a_up.nc
'W Down'/'W Up' -> 'A Down'/'A Up'
- css class & ids: .w-axis-settings -> .a-axis-settings,
min_w/max_w form ids match field names
- internal js identifiers and comments
Migration:
- AuxAxis._migrate_legacy_fields() promotes min_w/max_w in aux.json
on every load and persists the upgraded form, so existing
installs come out clean on first restart.
- Config._upgrade() renames macro file_name and display name in
every config.json load, so a stale in-memory copy can't
reintroduce the W names. Ships with a save right after.
The auxcnc ESP wire protocol verbs (WPOS/HOMED) are unchanged - they
are wire-format identifiers, not user-facing labels.
The auxcnc-driven stepper has been integrated into gplan as a
virtual A axis since the option-b migration. User-facing labels
that still said 'W axis' are now confusing because the user types
G-code with A, sees 'A' in the DRO, but config tabs and tooltips
still talked about W.
Cleaned up:
- Settings tab label W Axis -> A Axis (route slug stays #w-axis
for back-compat with bookmarks)
- WAxisSettings.svelte tooltips and tip text say 'auxiliary' or
just describe the field generically
- control-view.pug DRO row tooltips, comments, labels
- control-view.js console.error messages and route-comments
- axis-vars.js _compute_aux_axis tooltips
- AuxAxis.py boot-banner message
Internal identifiers (filenames WAxisSettings.svelte, route slug
#w-axis, aux.json fields min_w/max_w, internal comments referring
to the historical W-as-aux design) are kept where they are tied to
on-disk state or wire formats - those rename moves are not
worthwhile and would force users to migrate their aux.json files.
Previously the homing loop iterated zxyabc and processed each axis
in turn, but the AVR axes (Z/X/Y) just queue G-code to the planner
and return immediately - the gantry keeps moving in the background.
The external A homing was then driven synchronously on the same
loop iteration, which meant the W stepper started its limit-seek
*while the gantry was still actively homing Z/X/Y*. Visually
confusing and unsafe.
Split into two phases:
1. The HTTP handler thread queues every AVR axis (no change) and
collects external axes into a deferred list.
2. A background thread polls cycle until it returns to 'idle'
(signalling the AVR finished its queued homing). It then runs
each external axis home in order, blocking on the ESP serial
RPC. Post-home bookkeeping (set_axis to AVR, planner G92, cycle
reset) is scheduled back onto the IOLoop via add_callback so
gplan and the AVR command queue are only touched from one
thread.
A guard prevents overlapping threads if Home is clicked again while
the previous deferred run is still waiting.
Three changes that together implement option (b) home semantics:
1. Mach.home for the external axis: replace G28.3 with explicit
AVR position sync (Cmd.set_axis) + planner abs sync
(position_change) + G92 a0 (set user-coord origin to current
physical position, computing offset = home_position_mm).
G28.3 was wrong: it preserves the current user-coord position
and adjusts the offset to bridge to the new abs. After a move
away from home and a re-home, the offset accumulates
(134 -> 268 -> ...). G92 a0 with a freshly-synced abs always
produces offset = home_position_mm regardless of prior state.
2. Planner.__encode: stop stripping the external axis target from
the AVR line. The AVR has no motor mapped to A so it steps no
motor, but exec_move_to_target updates ex.position[A] which
gets reported back as ap. Leaving A in the AVR target keeps
state.ap consistent with gplan's idea of A; stripping it left
ex.position[A] stale and clobbered ExternalAxis's state.ap on
the next status report.
Side benefit: removes the special-case empty-string return for
pure external moves; every line block follows the same path now.
3. ExternalAxis.enqueue_target_mm: stop writing to state.<axis>p
from the planner hot path. The AVR's status reports drive it
instead, which avoids DRO jitter (jump to target then snap back
to intermediate values as the trapezoid runs). _pos_mm internal
mirror is still updated for delta computation.
Re-verified with the integration smoke test in tmp/20260503_option_b/:
home/move-down/move-up/re-home/home-from-bottom all produce the
expected DRO position values (0 at home, -134 at bottom).
Big-bang refactor of the W-axis integration. The auxcnc ESP stepper
is now exposed to the bbctrl planner (camotics gplan) as a virtual
A axis with no AVR motor mapping. gplan parses gcode for A natively,
applies soft limits, units, accel ramping and S-curve trajectories.
Line blocks with A motion are intercepted in Planner.__encode and
forked to the ESP via ExternalAxis on a worker thread; the residual
XYZ motion goes to the AVR as before.
This replaces the previous (MSG,HOOK:aux:N) side-channel: gcode
authors now write G1 A50 F1000 (or G28 A0 to home) and the planner
handles it the same way it handles X/Y/Z.
## Architecture
The AVR has 4 motor channels (0-3, all assigned to X/Y/Y/Z on
Onefinity). Looking at the AVR source, an axis with no motor
mapping is fully accepted: line blocks with that axis target update
ex.position[axis] in exec.c, but no motor steps because
motor_get_axis(motor)==axis returns -1. The AVR reports 'p' for
all 6 axes regardless. So we expose A to State as a synthetic
motor (index 4, host-only), populated from aux.json with full
kinematic config (vm/am/jm/tn/tm). State.find_motor and the
snapshot projection now walk 0..4. gplan sees A as a real axis.
## New module: ExternalAxis
- Registers synthetic motor 4 with vm/am/jm/tn/tm so
State.find_motor('a') returns 4 and gplan picks up
soft limits + kinematics.
- Worker thread drains a target queue so ESP RPCs (which can
take seconds) never block the bbctrl ioloop.
- execute_to_mm: synchronous, used by HTTP endpoints.
- enqueue_target_mm: non-blocking, used by Planner.__encode.
- home(): runs ESP cycle, syncs <axis>p and <axis>_homed.
- abort(): drains queue.
## Planner
- __encode splits external-axis target out of line blocks.
- Pure A move -> emits id-sync only (planner advances cleanly).
- Mixed XYZ + A -> AVR runs XYZ trapezoid concurrent with the
ESP move (v1 accepts the slight desync; users wanting strict
sequencing put A on its own gcode line).
- _<axis>_homed for the synthetic motor mirrors into State only.
- Planner.reset drains the worker queue and forces resync.
## Mach
- Mach.home(axis='a') routes through ext.home() instead of the
standard G28.2/G38.6 latch sequence (which doesn't apply to an
ESP-driven axis), then issues G28.3 a<home> to sync gplan.
- Mach.unhome strips the AVR path for A.
- Mach.stop / E-stop drain the external-axis worker queue.
- Mach.jog strips A so the AVR doesn't see it (continuous-rate
jogging not supported on ESP yet; use /api/aux/jog instead).
## State
- find_motor walks 0..4 (synthetic motor 4 lives in vars).
- snapshot projection includes motor 4 so 4tn -> a_tn etc.
- get_axis_vector picks up motor-4 values without changes.
## AuxAxis
- Adds set_state_observer hook so ExternalAxis sees homed-flag
changes after homing/boot-banner.
- DEFAULTS now include axis_letter, max_velocity_m_per_min,
max_accel_km_per_min2, max_jerk_km_per_min3 in user-facing
motor-config units (m/min, km/min^2, km/min^3) matching the
onefinity per-motor convention.
## AuxPreprocessor
- Drops W-token rewriting entirely. M100..M103 ATC mapping kept.
- W tokens in legacy gcode now warn (once per file) instead of
being rewritten. Migration: replace W with A.
## Hooks
- aux/aux_rel/aux_setzero hooks retired. aux_home kept as a
legacy alias routing to ext.home() for older preprocessed
gcode. ATC hooks (droptool/grabtool/release/clamp) unchanged.
- E-stop now drains the external-axis worker queue.
## Web.py
- /api/aux/{home,jog,move} now route through ExternalAxis when
available so DRO and gplan position stay in sync.
## UI (axis-vars.js + control-view.pug)
- _get_motor_id and _check_is_enabled fall back to motor index 4
so the standard A column in the DRO renders state for the
ESP-driven axis (with full offset / set-position / per-axis
home support).
- Legacy W row is gated on !a.enabled - shown only for installs
that haven't migrated.
- WAxisSettings.svelte exposes the new max_velocity_m_per_min /
max_accel_km_per_min2 / max_jerk_km_per_min3 fields and an
axis_letter selector for picking A/B/C.
## Open follow-ups (validate on hardware)
- Q1: gplan soft-limit enforcement for A with min/max set.
Easy smoke test: max_w=50, MDI G1 A100, expect rejection.
- Q2: AVR behaviour with a target dict containing A values for
a motorless axis. Read of exec.c suggests it's safe; needs a
smoke test (no motor faults, no unexpected step counts).
- Q3: pause/resume mid-A-move semantics. ESP doesn't honour
bbctrl pauses; ext.abort drains the queue but a move-in-flight
runs to completion. Acceptable for v1; v2 could add a synced
pause.
Legacy Onefinity exposed a master Home All in the DRO header. Our
V09 redesign only kept it inside the no-W fallback row, so machines
with the W axis enabled (which is most users now) had no master
home button. Add it back to the DRO header's Actions column.
home_all() fires /api/home for X/Y/Z/A and /api/aux/home for W in
parallel \u2014 the AVR and the auxcnc ESP run independent homing cycles
so the user sees one click homing everything.
Drop the .ghost class from the XY Origin and Z Origin tiles so they
use the same flat slate color as the X-/X+/Z+ neighbors. The lighter
ghost tone made them look like a different category of action when
they are just origin shortcuts.
The bbctrl controller seeds new macros with placeholder colors like
'#dedede' and '#ffffff'. Treating those as 'configured' lit up the
asymmetric 6 px left stripe on every default macro, which looked
lopsided.
Add control-view.has_macro_color() that filters out a fixed set of
default placeholders plus anything within the near-white band
(R+G+B > 690). The .has-color class and the inline border-left-color
style are gated on that helper, so unconfigured macros render as
clean symmetric slate tiles.
Tested live on http://10.1.10.55/#control with the existing macro
config (#dedede): button now renders without the gray stripe.
After testing the V09 redesign live on the Pi at onefinity.local
(1920x1080, Chrome fullscreen) several real bugs surfaced. This
commit fixes all of them.
Layout fits at 1920x1080
- Cap .app-shell at 100vh height with overflow:hidden so child
flex containers actually constrain to one screen.
- Make .control-page / .program-page / .console-page use
flex 1 1 auto + min-height 0 + overflow hidden so the page total
no longer grows to ~36 000 px when the gcode-viewer is mounted.
- Override clusterize.css default max-height: 200px on the
.clusterize-scroll element with max-height: none + flex 1 1 0 +
height 100% so the gcode listing fills the available column.
E-Stop in the header
- The legacy estop.pug SVG had width=130 height=130 but no
viewBox, so CSS-only sizing did nothing and the SVG content
spilled ~26 px off the right edge of the screen and ~70 px
below the header. Add viewBox="0 0 130 130" plus
preserveAspectRatio so CSS sizing actually shrinks the inner
geometry. Drop the octagonal clip-path (the SVG already
carries its own yellow safety ring + EMERGENCY/STOP text).
3D toolpath preview (path-viewer)
- The legacy .path-viewer.small CSS clamped the canvas to
340 x 150 floated into the corner. In the new program-body
grid we want it to fill the 600 px right column. Override
with width 100%, height auto, float none, !important.
- Make orbit.js wheel/touchstart/touchmove listeners
{passive: false} so OrbitControls.preventDefault() actually
works and the page no longer scrolls while panning the 3D
view on a touch screen.
Vue 1 template + reactivity bugs exposed by the live data
- Replace v-else-if (Vue 1 has no v-else-if) in
control-view.pug with three sibling v-if templates that
mutually exclude on w.enabled and state['2an'] == 3.
- axis-vars._get_motor_id: guard motor.axis.toLowerCase()
against undefined motors (initial config is [{}, {}, ...]).
- axis-vars._check_is_enabled: prefer config.motors[i].axis
when present, fall back to state[N + 'an'] only for
recognised axes (x/y/z/a) so undefined == undefined
doesn't mistakenly enable b/c rows.
- program-mixin: tolerate state.files / state.gcode_list
being undefined right after connect.
App-shell race conditions
- Skip the early parse_hash() in app.js ready() when the
initial hash is in the settings family. Those Svelte
components read settings.units / settings.probing-prompts /
motion.* etc. and crash on first paint with the empty
placeholder config. Stay on loading-view until update()
completes and routes us in itself.
Misc
- src/static/js/ui.js: null-guard the legacy burger menu code
(#menuLink no longer exists). Was throwing 'Cannot set
properties of null (setting onclick)'.
- src/static/css/Audiowide.css: switch the gstatic font URL
from http:// to https:// so it isn't blocked as mixed
content under the home.muehe.org HTTPS proxy.
- Macro buttons: drop the default 6 px yellow border-left.
The stripe now only appears via .has-color when
state.macros[i].color is actually configured. Removes the
asymmetric/lopsided look from the screenshot.
Tested live on http://10.1.10.55/ and via the HTTPS proxy at
https://onefinity.home.muehe.org/.
UX
- The V09 redesign already exposed the W axis in the Control jog grid
(row 4 when w.enabled) and as a row in the DRO table. The Settings
shell now also surfaces a dedicated 'W Axis' rail entry that smooth-
scrolls to the W Axis (auxcnc) section of the main settings page.
The rail item is marked active only while the user is on Display &
Units AND the W Axis link was the most recent click.
- The W Axis section in src/svelte-components/src/components/Settings
View.svelte gets an id="w-axis" anchor so the scroll lands cleanly.
Tested live against onefinity.local. Aux status reports
{enabled: true, present: true, pos_mm: 43.96, homed: false}; the W
axis row appears in the DRO with the right purple styling, and the
jog row 4 shows W- / Home W / W+ / Probe.
Deploy scripts
- deploy.sh dispatches to scripts/deploy/{local,hardware,prod}.sh
with shorthand wrappers (deploy-local.sh / deploy-hardware.sh /
deploy-prod.sh).
- local: builds the UI bundle and serves build/http/ via
python3 -m http.server 8770 in a tmux session 'onefin-local'.
Useful for visual iteration on macOS — chrome only, no controller.
- hardware: rsyncs the freshly built build/http/ tree onto the Pi at
onefinity.local and restarts bbctrl. Stages to /tmp on the Pi and
uses sudo to install into the running egg's bbctrl/http directory,
so iteration time is ~5 seconds.
- prod: requires a clean working tree, then runs 'make pkg' followed
by 'make update HOST=onefinity.local PASSWORD=onefinity'.
Defaults can be overridden with environment variables (HOST, PASSWORD,
REMOTE_USER for the hardware path).
Implements the V09 mock end-to-end (per plans/2026-04-30_ux_redesign.md):
Top shell
- index.pug rebuilt around .app-shell with a slim 96px header.
- Underline-ribbon tab bar (Control / Program / Console / Settings)
replaces the old side menu and the inline #tab1..#tab4 system.
- Single 'All systems' pill collapses the legacy WiFi/Camera/Rotary/
IP/Version chip-soup into one popover (sys-popover) anchored to the
header; rotary toggle, camera feed and shutdown live there.
- Octagonal 88x88px STOP button wraps the existing <estop> SVG; STATE
pill with pulse-dot honors prefers-reduced-motion.
Routing
- app.js parse_hash maps every existing hash:
#control -> Control
#program / #program:auto -> Program
#console / #console:mdi|messages|indicators -> Console
#settings, #admin-general,
#admin-network, #motor:N, #tool, #io, #macros, #help,
#cheat-sheet -> Settings (rail picks inner)
- All deep links are preserved.
Control panel (control-view.pug + .js)
- 720px jog grid + 4-axis DRO + 4 KPI cards + 8-macro row.
- Jog tiles use V09 flat slate (#3f4b63) with diagonal helpers and
a ghost row for XY/Z origin shortcuts.
- Per-axis Settings/Set-zero/Home buttons grow to 72x72px.
- Status strip cards: State / Velocity-Feed / Spindle / Job. Tapping
the Spindle card opens the new override-drawer with feed + spindle
range inputs (resolved decision in plans/...).
- Macro row binds to state.macros.slice(0, 8); >8 lives in Settings.
- Drops the old <table> control-buttons, .info, .override and .tabs
blocks entirely.
Program panel (program-view.pug + .js)
- Extracts the Auto bar, file selectors, gcode-viewer and path-viewer
out of control-view.
- Action buttons (RUN/STOP/UPLOAD-FOLDER/UPLOAD-FILE/DOWNLOAD-FILE/
DELETE) at 84px with explicit color affordances.
- Reuses control-view's existing methods via the new program-mixin.
Console panel (console-view.pug + .js)
- Three sub-tabs: MDI / Messages / Indicators. Sub-tab persists in the
URL fragment (#console:messages etc.).
- MDI: terminal-style prompt + SEND, plus an 8-wide on-screen keypad
(G0/G1/G2/G3/G28/G92/M3/M5 + axis letters + CLEAR/SEND).
- Messages: pulls from .messages_log (mirrored from
state.messages); badge in the header tab counts unread.
- Indicators: mounts the existing <indicators> component.
Settings shell (settings-shell.pug + .js)
- New left rail navigator listing Display, Network, General/Firmware,
Spindle&Tool, IO, Motors 0..3, Macros, Cheat Sheet, Help.
- Inner area mounts the existing settings family templates via an
explicit v-if cascade (avoiding a Vue 1 :is reactivity quirk).
- Shutdown / Save buttons relocated from the dropped side menu.
JS plumbing
- main.js: Vue.config.async = false to keep dependent watchers in
sync when reactive data is mutated outside Vue's normal event loop
(e.g. from a hashchange listener).
- program-mixin.js extracted so control-view.js no longer carries the
file/macro/gcode methods that are now Program-only.
- control-view.js trimmed to jog/DRO/probe/home logic.
- console-view.js / settings-shell-view.js use a hashchange listener
+ local data props because Vue 1 cannot reliably observe
.sub_tab from a child component.
Stylus rewrite
- Removes the old .header (140px), .nav-header, .brand subtree, #menu,
#main, .control-view block, .info, .override, .toolbar, .macros-div,
.macros-button, the .tabs > input radio-tab system and the .control-
view #control media-query overrides. None of these are referenced
any more.
- Adds V09 tokens (jog/macro palette + accent + line/card colors) at
the top, the new shell rules, .ktab / .sys-btn / .state-badge /
.estop chrome, the .control-page grid, status strip + override
drawer, .program-page action / file bars and program body,
.console-page MDI keypad / messages / indicators panes, and the
.settings-shell rail.
- Adds a 1820px breakpoint that stacks the right column under the jog
on smaller portable monitors.
Hard cut: no config.ui.layout flag, the old shell is removed in this
single commit. side-menu.css is no longer included from index.pug.
Tested locally with agent-browser (1920x1080) on every top tab and
every settings sub-route; routing, active tab highlighting and inner
view selection all work without a controller connection.
The W axis homing already drives toward the configured limit (home_dir
in aux.json, default '-') and lands at home_position_mm = 0, so
'home' and 'zero' are the same point. Remove the now-redundant 'W
Origin' (move-to-zero) and 'Set W to 0' map-marker buttons; keep just
W-, W+, and a single Home W button. Also drop the unused
aux_move_zero / aux_set_zero JS handlers.
Mirrors the 4-column rotary A row that appears when 2an==3, so the same
fine/small/medium/large increment selector that drives XYZ jogging now
also drives W jogging. New control-view methods:
- aux_jog_incr(sign) - PUTs aux/jog with the current jog_incr amount
converted to mm (handles imperial display units)
- aux_move_zero() - PUTs aux/move {mm:0}, the absolute counterpart to
aux_set_zero (which redefines the current pos as zero without moving)
Row is hidden when w.enabled is false, so users without the auxcnc
controller see no change.
The xyzabc rows have three actions (set-position cog, zero marker, home),
W only has two. Without a placeholder the W buttons render in the left two
slots of the actions cell, leaving the home button unaligned with the home
column above. Added a hidden disabled cog button so the marker and home
buttons sit under the same columns as the rest.
Adds the auxcnc W axis to the front-page Position table:
- axis-vars.js exposes a 'w' computed property fed by state.aux_pos /
aux_enabled / aux_homed / aux_present (set by AuxAxis on the host).
No motor mapping, no soft-limit warnings - the aux controller does
its own bounds.
- control-view.pug adds a W row after the xyzabc loop. The Set/Zero
button calls /api/aux/set-zero {mm:0} and the Home button calls
/api/aux/home, which hit the new endpoints exposed by Web.py.
- control-view.js: aux_home(), aux_set_zero(), and aux_jog() helpers.
When aux_enabled is false (no aux.json or aux.json has enabled=false)
the row stays hidden, matching the existing axis-row behavior.