Two bugs surfaced when macros got prose like:
(Composed from atoms: M102 = RELEASE (V1 on), M103 = CLAMP)
1. _ATC_M_RE.finditer was being run against the raw line, so the
M102/M103 *inside* the comment fired spurious release/clamp
hooks at file load.
2. The simple _PAREN_COMMENT_RE = re.compile(r'\(\[^)]*\)') is a
greedy non-nested match, so a header with a nested paren
(e.g. 'M102 = RELEASE (V1 on)') only stripped the inner
paren, leaving the trailing 'M103 = CLAMP)' visible to the
matcher.
Fix:
- Add _strip_comments() that walks the line tracking paren depth
and drops the trailing semicolon comment. Handles nested parens
correctly.
- Run _ATC_M_RE.finditer against the comment-stripped 'code'
instead of the raw line, so prose mentions are inert.
- Drop the original line's comments from the rewritten output;
keeping them around led to the M-codes being matched twice
(once stripped, once still in the trailing comment).
- Use _strip_comments in file_uses_aux too.
The grab.nc and drop.nc macros on the controller already had the
prose headers; they now preprocess correctly to clean
release / G4 / clamp and release / N x eject / Z0 / clamp
sequences.
Match auxcnc firmware v3, which dropped the monolithic DROPTOOL /
GRABTOOL ESP tasks in favour of three atoms: RELEASE, CLAMP, EJECT.
This lets host macros interleave Z moves between ejector pulses
(the old DROPTOOL ran open->oscillate->clamp in a single ESP task,
so you couldn't lift Z mid-eject).
AuxAxis: replace atc_droptool() / atc_grabtool() with atc_eject(
pulse_ms=, dwell_ms=). atc_release() / atc_clamp() are unchanged.
Ctrl: register internal hooks for release / clamp / eject only.
The eject hook parses 'pulse=' and 'dwell=' kwargs out of the
HOOK:eject:<data> payload so macros can emit
(MSG,HOOK:eject:pulse=400 dwell=300) for tuned wiggles.
AuxPreprocessor: M100 now maps to eject (was droptool); M101 is
unmapped (was grabtool, now a pure host-side macro); M102/M103
are unchanged. Header comment updated.
docs/AUX_A_AXIS.md: mention the new atom set.
The drop.nc and grab.nc gcode macros on the controller are
correspondingly rewritten on-device as compositions:
drop = M102 + 4xM100 + G53 G0 Z0 + M103
grab = M102 + G4 P2 + M103
Pendant hold-to-jog now picks the more restrictive of the soft
limit and the Z-A coupling bound when computing target_steps for
the ESP. The coupling rule (a - z <= K) caps how high A may go
for the current Z; only the +A direction (toward larger machine
A) is constrained, -A jogs are unaffected.
ExternalAxis already exposes couple_K and _z_machine_now; we
project a_max_mm = z_now + K into step space via the same
_mm_to_steps the rest of AuxAxis uses.
The combined helper _a_combined_target_steps picks whichever of
the two targets is reached first when moving in . The
log line includes target_src so journalctl shows whether a stop
was triggered by softlimit or coupling.
Refusal-on-press logic was extended to use the combined target so
we won't even start a jog when sitting on a coupling-blocked
position.
Limitation: the target is computed once at JOG start. If Z drops
during the jog the bound moves with it; this version doesn't
re-evaluate. Z motion during a manual A jog is rare in practice
(both hands are on the pendant), but a periodic re-check is on
the follow-up list.
Pendant hold-to-jog could drive A past min_mm / max_mm because the
JOG path bypassed the planner-driven soft-limit checks. Wire the
host to compute a step-counter target for whichever soft-limit
boundary lies in the requested direction and pass it through the
new JOG target= parameter.
AuxAxis.jog_start now accepts target_steps; when given it emits
'JOG ... target=<n>'. The ESP picks the decel start point so the
motor ramps to a smooth stop AT the boundary, with no overshoot.
Jog._a_soft_limit_target_steps:
- Returns None when the axis is not homed -- pre-home setup jogs
are still allowed (matches the rest of the manual-jog API).
- Otherwise projects min_mm/max_mm into step space (honoring
dir_sign) and returns the boundary on the requested side of
the current position.
Jog._a_start additionally refuses to send the JOG when the
position is already at-or-past the boundary in the requested
direction, so we don't depend on the ESP's wrong-side reject path
for the common 'press button while sitting on the limit' case.
Verified end-to-end on hardware (bare ESP, no gantry):
JOG dir=+ maxrate=400 target=300 stops at pos=299
JOG dir=+ target=-50 (wrong side) rejected immediately.
When a plan fails (e.g. AuxPreprocessor Z-A coupling rejection at
planner-load time, or any other gplan error in the plan.py
subprocess), the Plan future was never resolved. PathHandler then
1-second-times-out forever returning {progress:0}, and the JS poll
loop in load_toolpath kept the 'Processing New File' dialog up
indefinitely.
- Preplanner.Plan now records .error and always resolves the future.
- PathHandler returns {progress:1, error:...} when the plan failed.
- load_toolpath closes the dialog and alerts the operator on error,
and breaks out of the poll loop on api errors instead of looping.
- FileHandler upload-time AuxPreprocessor coupling errors now post a
visible state message instead of being silently swallowed.
Adds visibility into the gamepad event path so future regressions
can be diagnosed without the gantry attached. AJOG EV logs every
incoming KEY event and any ABS event matching the trigger codes;
AJOG STATE logs every transition; the would-be JOG / JOGSTOP is
also logged.
BBCTRL_AJOG_DRYRUN=1 in the bbctrl env disables actuation while
keeping the logging, so the host-side state machine can be tested
without driving the ESP.
Default is live actuation (dry-run off). Used this to prove the
host side was correct on hardware where the firmware bug was
hiding -- pendant taps produced perfect press/release pairs at
~200 ms while the ESP was the one ignoring JOGSTOP.
Previous attempts (small STEPS chunks per 250 ms tick, then a
single-big-STEPS plus ABORT-on-release) both gave a jerky ride: the
chunked path produced staccato accel/decel ladders, and the
ABORT-on-release path raced with task creation, frequently letting
STEPS run to its full target.
The auxcnc ESP gained a continuous-rate JOG / JOGSTOP pair (see
auxcnc commit 'Add JOG / JOGSTOP for smooth hold-to-jog'). On
press we issue JOG dir=+/- maxrate=... accel=... and the ESP ramps
up and cruises until JOGSTOP, which triggers a controlled decel.
AuxAxis additions:
- jog_start(direction, max_rate_sps=, accel_sps2=, ignore_limits=)
sends JOG and waits only for the immediate '[jog] started' ack.
- jog_stop() sends JOGSTOP fire-and-forget (no RPC lock so it can
interrupt anything in flight, mirroring abort()).
- _on_line picks up async '[jog] done|aborted ...' lines and
resyncs _pos_steps so subsequent moves compute the correct
delta.
Jog.py:
- On Xbox 360 pad RB (BTN_TR) -> A+ press, RT (ABS_RZ) -> A-
press; release -> JOGSTOP. Speed buttons (X/A/B/Y) scale max
rate by 1/128, 1/32, 1/4, 1.0x.
- safe=0 only when A is unhomed; otherwise the ESP enforces
limit-toward-home_dir abort.
- On release, schedules a 200 ms-deferred ext_axis._pos_mm
refresh so any subsequent gplan-driven A motion sees the new
position.
Verified end-to-end on hardware: smooth ramp-cruise-ramp on
press/release, no overshoot on quick taps, soft limits respected
when homed.
The default USB pendant config exposed A only via the right stick
X axis (ABS_RX). Most Onefinity-shipped pendants only have one
usable stick, so A was effectively unreachable.
Map BTN_TR (0x137, upper-right back) to A+ and BTN_TR2 (0x139,
lower-right back) to A- while held. Speed scaling matches the
sticks (1/128, 1/32, 1/4, 1x via X/A/B/Y).
R1 was the vertical-axis-lock toggle; horizontal lock on L1 is
preserved, vertical lock is dropped to free R1.
The active rewriter for jogs/MDI didn't help anyway because the
continuous-jog buttons send rate-based /api/jog commands to the AVR
and bypass the planner+MDI path entirely. Rather than build out
continuous-jog coupling on the ESP firmware or fake it with browser
ticks, simplify back to:
* Runtime check (Planner.__encode + ExternalAxis motion entry
points) refuses any move that would worsen the Z-A gap. Already
improvement-aware so X/Y jogs and Z-up/A-down recoveries pass.
* File preprocessor (AuxPreprocessor) injects pre-position A
moves into uploaded gcode so well-formed programs run without
operator intervention.
Operator workflow: jog freely down to the safe band; if you need to
go deeper, lower A first (aux jog mm) or use a step-jog MDI like
'G91 G0 Z-10 A-10' that includes the A delta. Programs do the right
thing on their own.
Tells the auxcnc ESP how far (in steps) to back off if HOME is
invoked while the limit switch is already tripped. The ESP now
hard-fails instead of zeroing blindly when the switch stays active
after the preclear move. Default 10 mm; set home_preclear_mm=0 to
disable the preclear and revert to immediate failure.
Match the file-preprocessor behaviour for live operator input. When a
Z-down jog or MDI line would push (A-Z) above the safe band, append
the matching A delta to the same line so the planner runs Z and A
together. Same direction-aware refusal: only error when the operator
explicitly asks A to move *up* (delta > 0) past the bound, or when
the required A would violate A's soft minimum.
Implementation:
* ExternalAxis.coordinate_mdi rewrites a multi-line MDI burst,
tracking G90/G91 modal across lines (jogs always emit
M70/G91/G0/M72; standard MDI defaults to G90). Z and A targets
are computed in machine coords using offset_z and offset_a so
the work-coord A token we emit is consistent with the operator's
frame.
* The 'A0' the jog UI emits for axes that aren't moving is treated
as 'no A intent' (G91 delta of zero) and freely overridden.
* Hooked into Mach.mdi after the existing ATC rewrite. On
ExternalAxisError the burst is dropped with a user message; the
planner check downstream still fires as defense in depth.
* Planner.__encode also catches ExternalAxisError now (vs
bricking on uncaught) - logs to the operator messages list and
halts the cycle cleanly so subsequent jogs work.
* check_coupling itself is now improvement-aware: only refuses
moves that worsen an existing violation. Pure XY jogs and
Z-up/A-down recovery moves pass even when (A-Z) is currently
above the bound.
Tested locally with synthetic MDI: small Z jog within band, Z jog
across the boundary (auto-injects A delta), G90 MDI G0 Z-50
(appends A106), explicit A-lift while Z deep (refuses), pure XY
jog (unchanged), G91 A-down (unchanged), G90 G0 A0 with
offset_a=134 (refuses as lift to home).
The auxiliary A axis carries a tool that hangs below the Z spindle.
Beyond a small Z descent the two physically collide unless A drops
with Z. Enforce in machine coords:
A_machine - Z_machine <= K
K = (A_home_mm - z_home_mm) + couple_z_clearance_mm
With our setup K = (134 - 0) + 22 = 156. At rest A=134 Z=0, A-Z=134
which is fine. Z can descend 22mm before the rule starts forcing A
down with it.
Two complementary layers:
(1) AuxPreprocessor injection (auto-fix uploaded files)
Tracks modal Z, A and distance mode (G90/G91) while scanning the
file. When a line would put A above Z by more than the clearance
we emit a 'G0 A<safe>' BEFORE the line so A is already at the
safe position when Z descends. Endpoint check is sufficient
because Z moves monotonically along a single line.
Errors are raised (not silently auto-fixed) when:
- the line lifts A above the safe band while Z stays put
(would require auto-injecting a Z-up which could swing
through a fixture)
- the line endpoint targets an A above the safe band
G91 disables injection with a one-shot warning; the runtime
check still applies.
(2) Runtime check (ExternalAxis.check_coupling)
Single source of truth for live motion. Hooked into:
* Planner.__encode for every line block (covers MDI and
running programs - gplan emits machine-coord targets)
* ExternalAxis.execute_to_mm/enqueue_target_mm/enqueue_line
for direct A motion (covers UI jog/move and planner-A
dispatch)
Raises ExternalAxisError on violation; gplan and the API both
surface the message. Skipped when coupling is disabled or the
axis isn't homed (mirrors the soft-limit gate).
Continuous Z jog from the AVR is not gated - it's an active
operator action without a pre-known endpoint. Operator-driven
over-travel during continuous jog will be caught by the next
MDI/file-load attempt.
Configuration in aux.json:
couple_z_enabled bool default true (per agreed setup)
couple_z_clearance_mm float default 22.0
z_home_mm float default 0.0
Surfaced in the new Z-A Coupling section of the A Axis settings
page with a description of the rule. Existing aux.json files get
the new keys via the merged-defaults path on read.
Tested locally with synthetic gcode covering Z descent, combined
moves, A lift while Z deep, G92 reset, G91 mode, and combined
Z+A target violations.
- Move docs/AUX_W_AXIS.md to docs/AUX_A_AXIS.md and rebadge W -> A
throughout, with a header note pointing at ExternalAxis as the
current implementation.
- README: A-axis fork heading, link to AUX_A_AXIS.md, /api/aux/status
in verify-flash, small comment in scripts/deploy/local.sh.
The auxiliary stepper used to be exposed as a W axis. After the
gplan integration it is exposed as A. Migrate persisted macro
config on every load:
w_down.nc -> a_down.nc
w_up.nc -> a_up.nc
'W Down' -> 'A Down'
'W Up' -> 'A Up'
Idempotent so a stale in-memory copy can never reintroduce the old
names.
Front-end side of the gplan-integrated A axis (B3).
- a-axis-view.{js,pug}: dedicated settings page that mounts the
AAxisSettings Svelte component and lives at #a-axis in the V09
settings rail.
- AAxisSettings.svelte: aux.json-backed form (axis letter, port,
homing direction, soft limits, ATC pin map, etc.) with master
Save integration via 'onefin:save-all'.
- main.ts + SettingsView.svelte: register AAxisSettings in the
Svelte component map; SettingsView no longer embeds the W axis
fieldset.
- settings-shell-view: 'A Axis' rail entry; route to a-axis-view.
- app.js: extend settings family to include 'a-axis'; broadcast
onefin:save-all from the master Save button.
- control-view: Home All button waits for the gantry cycle to
finish before firing Home A on a non-virtual setup; A jog
buttons; aux_jog/aux_home/aux_jog_incr methods.
- control-view.pug: A row in the DRO (with set-position + zero +
home actions), A- / A+ tiles in the jog grid (gated on
w.enabled || a.enabled), legacy W row kept for installs that
haven't migrated to the gplan integration.
- style.styl: dro-axis.axis-w color.
ATC pneumatics in g-code (drop tool / grab tool / release clamp /
engage clamp) are expressed as M100..M103. AuxPreprocessor rewrites
those into (MSG,HOOK:droptool:) etc on file upload + on planner
load + on MDI input, so the Hooks layer (B1) can dispatch them via
registered ATC handlers in Ctrl.
- AuxPreprocessor.py: regex-based file rewriter, idempotent.
- FileHandler: invoke preprocessor on every upload.
- Planner.init: also re-preprocess on load (catches files written
before this version).
- Mach.mdi: same rewrite for ad-hoc MDI input so M101 typed at the
console produces a HOOK message.
- Ctrl: register the four ATC hooks (droptool/grabtool/release/clamp)
with block_unpause + auto_resume so programs using them pause at
the right point and resume cleanly. aux_home retained as a legacy
alias for older preprocessed files.
ExternalAxis exposes the auxcnc-driven ESP stepper as motor 4 (a
synthetic, host-only motor that gplan sees but the AVR doesn't). The
result is a virtual A axis that is fully integrated with the planner:
G1 A25 F1500 schedules a coordinated S-curve and the ESP runs the
exact same 7-segment trajectory the AVR would have run if A were a
real motor.
- ExternalAxis.py: synthetic-motor state, S-curve LINE block forward
to the ESP, soft-limit enforcement, option-(b) homing (user A=0
at the home limit).
- State: walk motors 0..4 in find_motor; clear both homed and h on
reset; expose synthetic motor vars.
- axis-vars.js: motor-4 guard so the JS computed axis bindings don't
throw when motor 4 has no entry in config.motors; resolve motor_id
for the synthetic axis by scanning state['4an'].
- Ctrl: instantiate ExternalAxis after AuxAxis, share the axis_letter
setting, wire AuxAxis state observer.
- Web: route /api/aux/{home,jog,move} through ExternalAxis when it
is enabled so the DRO and synthetic-motor flags stay in sync.
bbctrl.AuxAxis manages a stepper driven by an auxcnc-style ESP32
over /dev/ttyUSB0 (or whichever serial port). Persistent config in
aux.json; UI talks to it via /api/aux/* endpoints.
- AuxAxis: serial framing, position tracking, soft-limit enforcement,
homing state machine, ATC pneumatic control (M100..M103 wrappers).
- Ctrl: instantiate self.aux alongside the other subsystems and
close it during shutdown.
- Web: handlers for /api/aux/{config,status,home,abort,jog,move,set-zero}.
Adds bbctrl.Hooks: a small dispatch layer for HOOK:<event>:<data>
messages embedded in g-code as (MSG,HOOK:droptool:) etc. Hooks can
block the unpause until the registered callback completes and
auto-resume after.
- bbctrl.Hooks: registry, fire, dispatch_hook_message, persistent
config in hooks.json, REST surface (/api/hooks, /api/hooks/save,
/api/hooks/status, /api/hooks/fire/<event>).
- Ctrl: instantiate self.hooks alongside the other subsystems.
- Planner._add_message: when a (MSG,...) line is HOOK:<event>:<data>,
route it through ctrl.hooks instead of state.messages so it never
surfaces as a UI popup and dispatch is immediate (state.messages
has a 250ms debounce).
- Web: handlers for the /api/hooks routes.
- Drop FA4 font files and font-awesome.min.css.
- Ship FA6 webfonts (solid, regular, brands) and fa6.min.css.
- io-indicator: use FA6 names (fa-circle-plus / -minus / -exclamation).
- static/js/ui.js: no-op the legacy side-menu click handler when menu
links are not present (V09 chrome removes them) so the Settings tab
no longer logs 'cannot set properties of null'.
- scripts/rc.local.fast: minimal rc.local that defers the heavy bits.
- scripts/bbserial-rebind.service: oneshot unit that unbinds ttyAMA0
from pl011 and (re)loads bbserial before bbctrl.service.
- scripts/bbctrl.service: declare the After/Wants on bbserial-rebind
so we can rely on it rather than racing rc.local.
- scripts/install.sh: ship the cold-boot bits with firmware updates
(mask sysstat, replace dphys-swapfile with an fstab swap entry).
- scripts/rc.local + setup_rpi.sh + setup.py: wire updated paths.
bbctrl.Trace records monotonic-anchored events from process start.
Ctrl, Comm, the Web layer and __init__ are instrumented so a single
GET /api/diag/timing returns a full timeline of import, controller
init, AVR connection, first websocket, and first GET /. The
restart-timing.js client posts performance.now() marks back so the
browser side can be aligned in the same view.
Used to drive the cold-boot optimisations that reduce listen latency
on the Pi by ~8s.
Importing camotics.gplan pulls in a C++ extension (libstdc++,
boost::python, etc.) which adds several seconds to bbctrl startup
on the Pi. Defer it to Planner.init() — bbctrl can serve the UI
and accept connections without ever touching the planner, and the
penalty is paid only the first time motion is queued.
Tornado removed @web.asynchronous in 6.x; bbctrl on the Pi runs an
older but compatible async-aware build. Switching to coroutine syntax
keeps the streaming endpoint working across Tornado 5/6.
Recursive _rotate() may have already moved or unlinked the source path
by the time we try to rename it (also tolerates concurrent logrotate
runs from /etc/cron.reboot). Catch FileNotFoundError instead of
crashing bbctrl on startup.
backup/onefinity-backup.sh: dd-based whole-card backup/restore with
shrink/expand support so a Pi image can be moved between SD cards
of different sizes.
- .pi/BUILD.md: end-to-end macOS dev workflow, deploy paths, dphys-swapfile vs fstab, troubleshooting.
- .pi/Dockerfile.gplan + build-gplan.sh: rebuild gplan.so from source on Raspbian Stretch (Bullseye is too new for the toolchain).
- Makefile: ensure trailing newline between concatenated pug templates so Pug doesn't glue file boundaries together.