Commit Graph

1204 Commits

Author SHA1 Message Date
3b622d3d17 ExternalAxis: enforce soft limits in execute_to_mm and enqueue_target_mm
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
2026-05-03 11:50:49 +02:00
aa747dcc85 Hide X cursor on kiosk (touchscreen)
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.
2026-05-03 11:47:05 +02:00
56c3406f25 ExternalAxis: option (b) homing - user A=0 at home, deterministic on re-home
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).
2026-05-03 10:46:47 +02:00
7cdab010b3 axis-vars: guard motor lookup for synthetic motor 4
_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.
2026-05-03 10:26:36 +02:00
53f26b0be8 feat: integrate W axis as virtual A axis through gplan
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.
2026-05-02 17:17:20 +02:00
3614a2bcd4 AuxPreprocessor: canonical M100-M103 for ATC pneumatics
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.
2026-05-01 18:49:44 +02:00
06f0e6517e AuxAxis: wire DROPTOOL/GRABTOOL/RELEASE/CLAMP as gcode hooks
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.
2026-05-01 16:35:45 +02:00
1a6f926181 Hooks: dispatch HOOK messages directly, bypassing state.messages
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.
2026-05-01 16:32:02 +02:00
b0712a5bf0 run_macro: use fetch instead of api.get for /api/file
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.
2026-05-01 16:29:25 +02:00
1c69c0a157 Macros: fix wrong-file race, suppress HOOK message popups, preprocess at load
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.
2026-05-01 16:27:14 +02:00
muehe
748f092795 control: properly wait for gantry homing before W
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.
2026-05-01 16:18:23 +02:00
muehe
cfc14643d2 control: home W last in 'Home All'
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.
2026-05-01 16:16:51 +02:00
muehe
b0f38619ba control: keep jog grid visible during jog/home/probe/mdi
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.
2026-05-01 16:08:39 +02:00
549b69c234 motor-view: stop clobbering user edits with controller state
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.
2026-05-01 16:08:31 +02:00
muehe
5376d23f8b control: shrink Home All button in DRO header
Smaller than the per-axis home buttons so it reads as a header-level
shortcut rather than competing visually with each row's action.
2026-05-01 15:57:31 +02:00
muehe
ecf3191fcc control: restore Home All button in DRO header
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.
2026-05-01 15:54:52 +02:00
muehe
3baa67360c control: align XY/Z origin tile color with other axis tiles
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.
2026-05-01 14:50:22 +02:00
muehe
b7bd7a1c9c AuxAxis: faster sane defaults for homing and stepping
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.
2026-05-01 14:47:11 +02:00
muehe
6fe2e79bff settings: unify W axis save into master Save button
- 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.
2026-05-01 14:28:15 +02:00
muehe
19e6cc6c93 ui: unify jog button sizing across tablet and kiosk
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.
2026-05-01 14:24:33 +02:00
muehe
50839718e2 kiosk: chromium 72 mime + flex-gap fixes
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.
2026-05-01 11:16:28 +02:00
68a92bb297 AuxAxis: pre-load home_zero via HOMECFG, drop post-home WPOS
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.
2026-05-01 11:08:51 +02:00
muehe
41d720c1d0 kiosk: pi-friendly compact mode + chromium 72 fallbacks
- Detect kiosk mode (localhost / ?kiosk=1) and add html.kiosk-mode
- Suppress 3D path-viewer in kiosk mode (Pi 3B too slow)
- Compact 1366x768 layout: 56px header, smaller jog grid, 4-col macros
  2-col status, 540px jog column
- Flex-gap fallbacks for Chromium 72 (header tabs, sys-btn, state-badge,
  ktab, sp-row, etc.) using "> * + *" margin-* rules
- Path-viewer: opaque WebGL canvas, ResizeObserver-gated render loop,
  no first-frame size flash
- Path-viewer renderer cleared properly on component teardown
- W axis row: W- | W+ | Probe XYZ | Probe Z (was W-|HomeW|W+|Probe)
- Running panel only for actual program execution (not jogging)
- Settings sectioned (Display+Units / Probing / G-code+Motion)
- Routed component now keep-alive across tab swaps
- FA4 -> FA6 webfonts
2026-05-01 11:05:39 +02:00
3d73e6c59d install.sh: mask sysstat, replace dphys-swapfile with fstab entry
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).
2026-05-01 10:31:40 +02:00
860ca30aba install.sh: ship cold-boot optimisations with firmware updates
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).
2026-05-01 10:15:35 +02:00
8e3b7a29e5 Cold-boot: 4 optimisations cutting bbctrl listen by ~8s on the Pi
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.
2026-05-01 10:07:23 +02:00
420caf52be Trace: anchor events to kernel boot, mark first GET /
- 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.
2026-05-01 09:56:21 +02:00
561d2fd7ea Restart timing: bbctrl.Trace, /api/diag/timing, UI marks
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.
2026-05-01 09:48:10 +02:00
73c6a4f160 Macros: suppress placeholder color stripes (#dedede, #fff, ...)
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.
2026-05-01 08:18:28 +02:00
5926316a25 Fix: real-hardware bring-up issues found at 1920x1080 on the Pi
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/.
2026-04-30 22:24:55 +02:00
ea23f94b87 Settings rail: add W Axis entry; deploy scripts (local/hardware/prod)
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).
2026-04-30 21:45:17 +02:00
b8c4f53bb1 Merge branch 'hooks' into master
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
2026-04-30 21:33:48 +02:00
32f3aca368 UX redesign V09: replace shell, split Program/Console/Settings
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.
2026-04-30 21:27:00 +02:00
081209decf Plan: resolve open questions (hard cut, macros slice, override drawer, defer pin)
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.
2026-04-30 20:43:29 +02:00
ef4658aaf6 Plan: V09 full UX redesign mock + implementation plan
- 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.
2026-04-30 20:00:03 +02:00
ef78f20eaa Docs: README + W axis docs cover macOS build/flash and new UI
- 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.
2026-04-30 19:54:30 +02:00
36829020a5 Settings: add W axis (auxcnc) panel
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.
2026-04-30 19:10:24 +02:00
2413fc49ab UI: collapse W axis to home-only (drop set-zero / W-origin)
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.
2026-04-30 19:07:17 +02:00
Claude
7d5949f5fc UI: add W jog row (W- / W Origin / W+ / Home W) under the XYZ jog grid
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.
2026-04-30 17:37:49 +02:00
Claude
23f22105a8 UI: align W axis marker/home buttons with the X/Y/Z columns
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.
2026-04-30 17:23:55 +02:00
Claude
4f74e75d44 UI: render W (aux) axis row in the main DRO
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.
2026-04-30 17:10:00 +02:00
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
54a15f9d12 Rewrite BUILD.md: clean up, add quick start, remove dead weight
- Quick start section at the top (3 commands)
- Removed inline Bullseye build recipe (moved to 'why not' appendix)
- Added build time estimates
- Cleaner table formatting
- gplan.so contents documented (cbang + camotics)
2026-04-30 16:39:57 +02:00
704bc8d35c gplan.so: build from source using Raspbian Stretch Docker
Use balenalib/raspberry-pi-debian:stretch with legacy.raspbian.org repos.
Exact match: GCC 6.3, Python 3.5, GLIBC 2.24 — identical to the Pi.
First build ~25min (QEMU), subsequent builds ~1sec (cached image).

Replaces the broken Bullseye approach that had GLIBC/GLIBCXX mismatches.
2026-04-30 16:33:20 +02:00
4d2d5fd88c Update BUILD.md: gplan.so can't be built from source on Bullseye
Document GLIBC/GLIBCXX version constraints and Python 3.5 compat notes.
Recommend using official release gplan.so instead.
2026-04-30 15:57:31 +02:00
eab204b7be Fix Python 3.5 compat: capture_output and text= not available
Use stdout=PIPE/stderr=PIPE and manual .decode() instead.
Use official 1.6.6 gplan.so (built with Stretch-era GCC, no GLIBC_2.29 dep).
2026-04-30 15:56:42 +02:00
e3c059eb9b Add cached gplan.so build: 30min first time, 3sec after
- Dockerfile.gplan: pre-built armv7 image with cbang + camotics objects
- build-gplan.sh: relinks against Python 3.5m in ~3sec
- Pi Python 3.5 headers cached in .pi/pi-python35.tar.gz (gitignored)
2026-04-30 14:43:05 +02:00
7306464440 Document gplan.so build-from-source procedure
Build in armv7 QEMU Docker, compile with Python 3.9 SCons,
relink final .so against Python 3.5m from the Pi.
2026-04-30 13:52:58 +02:00
1625b768d8 Add build/flash/backup documentation for Pi firmware 2026-04-30 12:09:12 +02:00
5be7515a92 Fix gplan.so: use armv7 binary from official 1.6.6 release
The gplan.so (CAMotics G-code planner) must be a 32-bit ARM binary
matching the Pi's Python 3.5. Source it from the official release
package rather than cross-compiling (SCons ignores CC/CXX overrides).

Also revert install.sh gplan.so preservation logic — simpler to just
ship the correct binary in the package.
2026-04-30 11:36:09 +02:00