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.
When ext.home() raised, Mach.home logged the error but never reset
the cycle from 'homing' back to 'idle'. The AVR had not moved (the
homing was external), so its state stayed at READY without a
transition - meaning _update's normal 'state changed to READY ->
exit cycle' path never fired and the UI was permanently locked out
of every action that requires is_idle (jog buttons, the home button
itself, run, etc).
Wrap the external-homing block in try/except and force the cycle
back to idle on any failure. The success path is unchanged - it
still relies on the AVR's queued set_axis + G92 acks to bring the
state back to READY.
Replaces the legacy 'fixed-rate STEPS' path used in Planner.__encode
with a new ExternalAxis.enqueue_line() that hands the ESP the full
7-segment S-curve parameters of every gplan line block (max_accel,
max_jerk, entry_vel, exit_vel, times[7]).
The ESP's new LINE command (auxcnc commit 8acc6f7) integrates the
same SCurve math the AVR uses, so the W axis now physically moves
in lockstep with whatever the planner thinks A is doing. Result:
DRO stays in sync with the actual stepper, no more multi-second lag
between commanded and observed A position.
enqueue_target_mm is kept as a no-frills STEPS path for jog/move UI
endpoints that don't have planner timing context.
AuxAxis._do_line builds the LINE command with mm/min/min^2/min^3
units (matching gplan's internal unit system) and waits for
[line] done|aborted from the ESP. Limit aborts still flag _homed=False.
The ESP's homed flag survives bbctrl restarts (since the ESP itself
stays powered). Host state, on the other hand, gets reset to zero on
boot - State.reset zeros ap and offset_a. Trusting the ESP's homed
flag in that situation made gplan think A was homed at machine-coord 0
while physically the axis was at 134, which then rejected any move
to the bottom (G1 A-134) as 'less than minimum soft limit 0'.
Send UNHOME (new auxcnc verb that just clears g_homed without
moving) on every host connect. The user has to re-home explicitly,
which goes through the proper Mach.home path that sets up the
offset and gplan position consistently.
Falls back to HOMED? if the firmware doesn't know UNHOME, so older
auxcnc builds keep their previous behaviour.
State.reset extended to also clear motor 4's homed flags
(<motor>homed and <motor>h) so the synthetic external-axis motor
gets reset alongside the real AVR motors.
When the auxcnc axis is integrated as a virtual machine axis via
ExternalAxis (synthetic motor 4 enabled), Mach.home(None) already
homes the external axis as it iterates xyzabc. The legacy 'home W
last' path in home_all() then fired PUT /api/aux/home a second time,
causing the A axis to home twice on Home All.
Skip the trailing aux/home when state['4me'] is set; keep the
fallback for setups where aux is enabled but not integrated as a
virtual axis.
Soft limits in machine coords (min_w/max_w from aux.json) were only
checked by gplan. UI jog/move endpoints went through ExternalAxis
directly without any check, so the W+ button at home would happily
push past max_w into a physical crash.
Add _check_soft_limit(target_abs_mm) called by both motion paths:
the synchronous execute_to_mm (UI) and the non-blocking
enqueue_target_mm (planner). Boundaries inclusive within a 1e-4
epsilon for floating-point round-trip stability. Skipped when the
axis isn't homed, matching the standard bbctrl convention that
soft limits are gated by homing state. Skipped when max <= min
(disabled).
Tested locally:
- pre-home: 200mm allowed (jog-out-of-trouble path)
- post-home: 0 and 134 (boundaries) accepted
- post-home: 135 and -1 rejected with clear error
- 134.00005 accepted (within epsilon), 134.001 rejected
- enqueue path also rejects, propagating up through Planner.next()
- max==min config skips check
Pass -nocursor to startx so the mouse pointer never appears on the\nOnefinity touchscreen. Patched in all three boot paths: rc.local.fast\n(active), legacy rc.local, and the setup_rpi.sh bootstrap.
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).
_compute_axis indexed config.motors[motor_id] directly without
checking the array length. For motor_id=4 (the synthetic external-
axis motor used by ExternalAxis) there is no entry in config.motors,
so motor was undefined and motor["homing-mode"] threw - which
made the entire 'a' computed prop return undefined and the A row
never rendered.
Default to {} when the index is out of bounds.
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.
Map four user-defined M-codes to the existing ATC hooks:
M100 DROPTOOL -> (MSG,HOOK:droptool:)
M101 GRABTOOL -> (MSG,HOOK:grabtool:)
M102 RELEASE -> (MSG,HOOK:release:)
M103 CLAMP -> (MSG,HOOK🗜️)
M100-M103 are in LinuxCNC/Buildbotics user-defined range so the
planner won't error on the raw codes if the preprocessor is bypassed.
Stripped from the residual line and replaced with the hook line.
Order is left-to-right; multiple ATC codes per line and ATC+W on
the same line both work (M100 W10 -> drop then move to W=10).
The file scanner (file_uses_aux, formerly file_uses_w) now wakes
up for either W tokens or ATC M-codes; backwards-compat alias kept.
MDI rewrite (Mach._rewrite_w_mdi) updated likewise.
Tested locally with mixed ATC/W gcode in tmp/20260501_atc_mcodes.
Adds atc_droptool/atc_grabtool/atc_release/atc_clamp wrappers in
AuxAxis (each just an RPC waiting on the matching terminal reply
line from the firmware), and registers them as internal hook
handlers in Ctrl. Macros and gcode programs can now invoke the
tool changer with:
(MSG,HOOK:droptool:)
(MSG,HOOK:grabtool:)
(MSG,HOOK:release:)
(MSG,HOOK🗜️)
block_unpause + auto_resume mirrors the W-axis hooks: the program
pauses while the ESP runs the pneumatic sequence and resumes when
done. Soft timeouts match the worst-case ESP sequence durations.
The previous fix routed (MSG,HOOK:...) lines through state.messages
and then immediately ack'd them to suppress the user-visible popup.
But state changes are debounced 0.25s before listeners fire, so the
HOOK message was already ack'd (removed from the list) by the time
Hooks._on_state_change saw the update - and the hook never ran.
Add Hooks.dispatch_hook_message() as a direct entry point and call
it from Planner._add_message. HOOK lines are dispatched synchronously
from the planner thread; the user message list is left untouched, so
no popup leaks and no debounce race.
api.get assumes JSON responses, but /api/file/<name> returns raw
gcode text. The await threw on response.json() and start_pause()
never fired. Use fetch directly and await response.text() to make
sure FileHandler.get's select_file side effect has been processed
before mach.start() runs.
Three fixes for macro/W-axis interaction:
1. run_macro raced the file selection. The frontend mutated
state.selected client-side and immediately fired api.put('start').
Selection on the server is a side effect of GET /api/file/<name>
(FileHandler.get calls state.select_file). The GET request was
often still in flight when start ran, so mach.start() executed
whichever file was selected last - pressing W Down would re-run
W Up. Now run_macro awaits the file fetch before starting.
2. (MSG,HOOK:aux:N) lines, used as the IPC channel between the
W-axis preprocessor and the Hooks system, were leaking to the
user as message popups (because the planner forwards every
(MSG,...) comment to state.messages). Filter HOOK: messages in
Planner._add_message: still pushed through state.messages so
Hooks._on_state_change can dispatch them, but immediately
acked so the UI doesn't render them.
3. AuxPreprocessor only ran at upload time (FileHandler.put_ok and
Mach.mdi). Files written via scp, restored from a config backup,
or hand-edited still contained raw W tokens that the planner
couldn't parse. Run preprocess_file in Planner.load() too. It's
idempotent (no-op when no W tokens remain) so re-loading a
already-rewritten file is free.
api.put('home') returns immediately when queued, not when homing
finishes, so the previous polling loop saw cycle=='idle' (homing
hadn't started yet) and fired W right away. Now we first wait up
to 5s for the cycle to *leave* idle, then up to 2min for it to
return, before kicking off the auxcnc W home.
Previously XYZ and W homing dispatched in parallel. Wait for the
main AVR homing cycle to return to idle before kicking off the W
auxcnc home so they never run simultaneously.
is_program_executing was checking only state.xx (RUNNING/HOLDING/
STOPPING) which is also true during jogging, homing, probing and
MDI cycles. The Now Running panel therefore took over the Control
view whenever the user jogged. Add a state.cycle check so only the
'running' cycle (a loaded program executing) triggers the swap.
The legacy Vue 1 motor settings page had nine `current_xxx` computed
props mirroring controller state vars (`<idx>vm`, `<idx>am`,
`<idx>jm`, `<idx>sa`, `<idx>tr`, `<idx>mi`, `<idx>tm`,
`<idx>tn`, `<idx>an`) paired with watchers that copied the state
value back into `config.motors[index]`, plus an `attached()` hook
running the same sync on mount.
The controller streams those vars continuously over the websocket.
Whenever a user typed into a field, the next state tick reverted it
to the controller's pre-edit value, so the form felt racy and edits
disappeared before Save. The same path also nuked unsaved edits when
navigating to another settings page and back.
The watcher logic was added in 749d63e to handle the case where
toggling rotary mode (PUT /api/rotary) rewrites motor 1+2 in
config.json on the server. Move that fix to the right place: refetch
config after the rotary PUT in app.js. The form now edits config
directly, Save PUTs it, and incoming controller state never overwrites
the user's in-progress edits.
Also drop the unused `syncStateToConfig` method.
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.
Old defaults (4000 fast / 400 slow / 200 backoff / 16000 accel /
200 start) were never aggressive in practice because the user-
saved config drifted to even lower values (600/80/110). Re-tune
the DEFAULTS dict to values that are sensible at 25 steps/mm:
home_fast_sps 2500 ~100 mm/s seek
home_slow_sps 250 ~10 mm/s re-engage
home_backoff_steps 400 ~16 mm clear hysteresis
step_max_sps 4000 ~160 mm/s normal-move cap
step_accel_sps2 12000
These only affect machines without an existing aux.json. The Pi
at 10.1.10.55 was patched manually.
- Drop the in-form 'Save W Axis Settings' button. The Svelte
WAxisSettings component now listens for a global onefin:save-all
event and PUTs aux/config/save when fired.
- Vue root's save() dispatches that event after saving config.json,
so a single click of the master Save button persists both the
controller config and aux.json atomically.
- Editing any W axis field triggers onefin:dirty, which the Vue
root catches to set modified=true so the master Save lights up
with the unsaved-changes indicator.
Big jog labels were only set inside the kiosk-mode override block,
which made the 1920x1080 tablet preview look small and inconsistent
with the Pi kiosk. Move the larger sizes to the base .jbtn rule
(font 1.6rem, ico 2.4rem, lbl 1.5rem) and drop the kiosk-mode
.jbtn override so both viewports use the same single source of
truth.
Pi's onboard chromium is 72 (Jan 2019). Two issues:
1. Python 3.5's mimetypes doesn't know woff/woff2/ttf, so Tornado
serves them as application/octet-stream which chromium 72 refuses
to use as web fonts -> all FA6 icons render as empty boxes. Add
scripts/deploy/patch_font_mime.py that monkey-patches bbctrl
Web.py's StaticFileHandler with correct content types. Run
automatically by deploy-hardware.sh (idempotent).
2. flex-gap landed in chromium 84. Add '> * + *' margin fallbacks
for the flex containers that show up on Program tab (action-bar,
action-btn, file-bar, file-btn) and tighten the kiosk-mode
settings rail so all 14 items fit in 768px height.
home() previously matched the wrong [home] line (firmware-side bug,
fixed in auxcnc) and even when it would have matched, it tried to
shift the step counter by writing 'WPOS <n>' after homing. The ESP's
WPOS handler clears HOMED, so a bbctrl restart would forget the home
state.
Push the desired step counter via HOMECFG zero= (firmware writes it
into the counter at the end of a successful HOME, leaving HOMED set).
home() now only reads the terminal [home] line; no post-home counter
fixup.
Round-3 cold-boot trims:
- mask sysstat.service (sadc CPU/IO logger; nothing reads it).
- mask dphys-swapfile.service and add /var/swap to /etc/fstab so swap
is brought up by systemd at local-fs.target instead of by an LSB
wrapper that re-checks the swap file size on every boot.
Both are reversible: `systemctl unmask <unit>` and remove the fstab
line. Before doing the dphys swap, install.sh verifies /var/swap
exists; on a fresh image where the file hasn't been created yet,
nothing is changed and dphys-swapfile keeps running normally.
Userspace boot 11.5s -> 10.7-11.4s on clean runs; bbctrl listen
unchanged at boot+10.4s (the saving moves to chromium/multi-user).
Persist the cold-boot wins (was: only manually deployed via
tmp/20260501_restart_timing/deploy-fast.sh, would silently revert on
the next prod firmware update).
- Install bbserial-rebind.service alongside bbctrl.service and enable
it. Eliminates the rc.local bbserial reload mid-boot.
- Prefer scripts/rc.local.fast over scripts/rc.local when present.
Legacy rc.local left as a fallback for old firmware tarballs.
- Mask plymouth-read-write, plymouth-quit-wait, and raspi-config.
Together these were ~6s of userspace startup that bought nothing
on a deployed Onefinity Pi.
Cumulative: bbctrl listening at boot+10.6s (was 20.6s), userspace
boot 11.5s (was ~13s), bbctrl.service @2.9s in critical-chain (was
@6.5s after the first optimisation pass).
Measured on onefinity.local (Pi 3, Raspbian Stretch, bbctrl 1.6.7).
Before -> after:
bbctrl listen boot+20.6s -> boot+12.4s (-8.2s)
host -> /api/config/load 28.2s -> 22.5s (-5.7s)
The 4 changes (each independently revertable):
1. scripts/bbserial-rebind.service: do the bbserial unbind + reload
in a dedicated unit ordered Before=bbctrl.service, instead of in
rc.local AFTER bbctrl is already listening on the serial port.
Eliminates a full bbctrl restart mid-boot.
2. scripts/bbctrl.service: drop "After=network.target". bbctrl talks
to the AVR on a local serial port and to the LCD on I2C; it does
not need DHCP / network-online to come up. Also adds explicit
ordering after the new bbserial-rebind unit.
3. scripts/rc.local.fast: trimmed rc.local that no longer touches
bbserial and backgrounds 'startx' so chromium launches in
parallel with bbctrl rather than after rc.local finishes.
4. src/py/bbctrl/Planner.py: lazy-import camotics.gplan. Costs ~130ms
on cold cache, deferred from import-time to ctrl.mach init.
5. (bonus) src/py/bbctrl/Log.py: tolerate FileNotFoundError in
_rotate(). The improved boot path exposed a pre-existing log
rotator bug that crashed bbctrl on first start when bbctrl.log.16
was missing.
- Trace reads /proc/stat btime and /proc/uptime at import so every
event in /api/diag/timing can be expressed as 'seconds since
power-on' (uptime_at_anchor + ev.t).
- Web.StaticFileHandler.prepare emits 'web.first_root_get' the first
time chromium hits / or /index.html, so we can see when the kiosk
browser actually started loading the UI on cold boot.
Add a lightweight, self-contained phase tracer for measuring end-to-end
bbctrl restart and Pi boot time. Disabled by setting BBCTRL_TRACE=0.
- src/py/bbctrl/Trace.py: monotonic-anchored event log + sd_notify helper.
- bbctrl/__init__.py: marks for imports, args parsed, ioloop, web init,
listen, and an sd_notify READY=1 once HTTP is bound.
- bbctrl/Ctrl.py: spans around each subsystem (avr, i2c, lcd, mach,
preplanner, jog, pwr, hooks, aux, mach.connect).
- bbctrl/Comm.py: avr.firmware_rebooted mark.
- bbctrl/Web.py: TimingHandler (GET /api/diag/timing) and
UITimingHandler (PUT /api/diag/timing/ui), plus a ws.first_open mark.
- src/js/restart-timing.js + app.js: UI-side performance.now() marks
(script.load, ws.open, ws.first_msg, ui.first_state, window.load),
posted once to the controller.
- scripts/bbctrl.service: stdout/stderr -> journal so TRACE lines are
visible via journalctl -u bbctrl. (Was StandardOutput=null.)
Revert: git revert this commit. To disable at runtime without
reverting, set BBCTRL_TRACE=0 in the bbctrl service environment.
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).
Brings in:
- W axis (auxcnc) integration via ESP32 over /dev/ttyUSB0, including
the W axis settings panel, DRO row, jog row aligned with X/Y/Z, and
collapsed home-only W controls.
- README + W axis docs covering macOS build/flash and the new UI.
- Build & flash docs for the Pi firmware (BUILD.md), including the
cached gplan.so build via Docker (~30 min first time, 3 sec after).
- Hooks v2: external triggers during G-code execution that block
unpause until the hook completes.
- V09 full UX redesign mock + implementation plan + mock variations.
- V09 implementation: new app shell with underline-ribbon tabs,
Program / Console / Settings shells, V09 jog/macro palette, slim
status pill replacing the old chip soup, and an octagonal STOP that
wraps the existing <estop> SVG.
- Vue.config.async = false to fix sticky :class bindings under hash
navigation.
# Conflicts:
# .gitignore
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.
Replaces the 'Open questions' section with 'Resolved decisions' and
propagates the four decisions into the relevant phases:
- Hard cut: no config.ui.layout flag. Phase 6 now includes the
removal of .nav-header, side-menu.css and the #tab1..#tab4 block
with a git grep verification step.
- Macros: Control row binds to config.macros.slice(0, 8); Settings
-> Macros owns the master list and reordering.
- Pin to Control: deferred, status strip stays at State / V&F /
Spindle / Job for this iteration.
- Feed/spindle override: bottom drawer triggered by the Spindle
KPI tile, reusing override_feed / override_speed.
Goals (s.1) and Phase 6 testing checklist updated to match.
- docs/mocks/v09_full_ux.html — high-fidelity 1920x1080 mock
showing the proposed Control / Program / Console / Settings tab
layout with the V09 flat slate jog/macro palette and an underline
ribbon header tab style.
- plans/2026-04-30_ux_redesign.md — phased implementation plan to
port index.pug + control-view.pug to the new shell while keeping
hash routing and existing settings/admin views intact.
- README.md (was a one-liner): describe the layout, the macOS quick
path including the esbuild platform-pin gotcha, and how to flash
with curl or 'make update'.
- docs/AUX_W_AXIS.md: document the new Control jog row layout, the
Settings 'W Axis (auxcnc)' section, and list the additional UI
files touched by this fork.
Expose the aux.json fields under a new 'W Axis (auxcnc)' section in
Settings: serial port/baud, mechanics (steps/mm, dir sign, soft limits,
max feed), homing (direction, position, fast/slow seek, backoff, max
travel, limit polarity) and the step profile (max/start rate, accel).
The 'enabled' flag stays read-only in the UI; flipping the W axis
on/off is still done via aux.json so a fresh install can't surprise the
user with hardware that isn't there. Live status (offline / unhomed /
homed at <pos> mm) is shown above the form.
Saving PUTs the merged config to /api/aux/config/save, which writes
aux.json and pushes the homing/step config to the ESP.
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.
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.