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.
337 lines
15 KiB
Svelte
337 lines
15 KiB
Svelte
<script lang="ts">
|
|
import { onMount } from "svelte";
|
|
import Button, { Label } from "@smui/button";
|
|
import * as api from "$lib/api";
|
|
|
|
// Mirrors the DEFAULTS in src/py/bbctrl/AuxAxis.py. The "enabled"
|
|
// flag is read-only here; toggling the auxiliary A axis on/off
|
|
// is done via aux.json on disk, so adding/removing the hardware
|
|
// doesn't have a surprise UI that bricks bring-up. Legacy aux.json
|
|
// files using min_w/max_w are migrated up to min_mm/max_mm by
|
|
// AuxAxis._migrate_legacy_fields on load.
|
|
type AuxConfig = {
|
|
enabled: boolean;
|
|
port: string;
|
|
baud: number;
|
|
steps_per_mm: number;
|
|
dir_sign: number;
|
|
axis_letter: string;
|
|
min_mm: number;
|
|
max_mm: number;
|
|
max_feed_mm_min: number;
|
|
max_velocity_m_per_min: number;
|
|
max_accel_km_per_min2: number;
|
|
max_jerk_km_per_min3: number;
|
|
home_dir: string;
|
|
home_position_mm: number;
|
|
home_fast_sps: number;
|
|
home_slow_sps: number;
|
|
home_backoff_steps: number;
|
|
home_maxtravel_steps: number;
|
|
step_max_sps: number;
|
|
step_accel_sps2: number;
|
|
step_start_sps: number;
|
|
limit_low: boolean;
|
|
couple_z_enabled: boolean;
|
|
couple_z_clearance_mm: number;
|
|
z_home_mm: number;
|
|
};
|
|
|
|
let cfg: AuxConfig | null = null;
|
|
let status: { enabled: boolean; present: boolean; homed: boolean; pos_mm: number } | null = null;
|
|
let busy = false;
|
|
|
|
// Listen for the global "save-all" event the Vue root dispatches
|
|
// when the user clicks the master Save button. We persist our
|
|
// current cfg the same way the in-form button used to. This way
|
|
// the user only ever needs one Save button.
|
|
function onGlobalSave() {
|
|
save().catch(e => console.error("aux save failed:", e));
|
|
}
|
|
|
|
onMount(async () => {
|
|
await refresh();
|
|
window.addEventListener("onefin:save-all", onGlobalSave);
|
|
return () => window.removeEventListener("onefin:save-all", onGlobalSave);
|
|
});
|
|
|
|
async function refresh() {
|
|
try {
|
|
cfg = await api.GET("aux/config");
|
|
status = await api.GET("aux/status");
|
|
} catch (e) {
|
|
console.error("Failed to load aux config/status:", e);
|
|
}
|
|
}
|
|
|
|
async function save() {
|
|
if (!cfg) return;
|
|
busy = true;
|
|
try {
|
|
await api.PUT("aux/config/save", cfg);
|
|
await refresh();
|
|
} catch (e) {
|
|
console.error("Failed to save aux config:", e);
|
|
throw e;
|
|
} finally {
|
|
busy = false;
|
|
}
|
|
}
|
|
|
|
// Mark the root config as modified whenever an auxiliary axis
|
|
// field is edited, so the master Save button highlights and
|
|
// the user knows there are unsaved changes.
|
|
function markDirty() {
|
|
try {
|
|
const root = (window as any).$root || (window as any).Vue?.root;
|
|
if (root && "modified" in root) root.modified = true;
|
|
} catch (_e) {}
|
|
// Also dispatch a generic event the Vue root listens for.
|
|
window.dispatchEvent(new CustomEvent("onefin:dirty"));
|
|
}
|
|
</script>
|
|
|
|
<div class="a-axis-settings">
|
|
{#if !cfg}
|
|
<p class="tip">Loading A axis configuration...</p>
|
|
{:else}
|
|
<div class="status">
|
|
{#if status}
|
|
<span>
|
|
Status:
|
|
{#if !status.enabled}
|
|
disabled
|
|
{:else if !status.present}
|
|
offline
|
|
{:else if status.homed}
|
|
homed at {status.pos_mm.toFixed(3)} mm
|
|
{:else}
|
|
connected, unhomed
|
|
{/if}
|
|
</span>
|
|
{/if}
|
|
</div>
|
|
|
|
<div class="pure-form pure-form-aligned" on:input={markDirty} on:change={markDirty}>
|
|
<fieldset>
|
|
<div class="pure-control-group" title="Enable the auxiliary axis (auxcnc-driven A). Edit aux.json to toggle.">
|
|
<label for="enabled">enabled</label>
|
|
<input id="enabled" type="checkbox" checked={cfg.enabled} disabled />
|
|
<label for="" class="units">(edit aux.json)</label>
|
|
</div>
|
|
|
|
<div class="pure-control-group" title="Serial port for the auxcnc ESP32.">
|
|
<label for="port">serial port</label>
|
|
<input id="port" type="text" bind:value={cfg.port} />
|
|
</div>
|
|
|
|
<div class="pure-control-group" title="Serial baud rate.">
|
|
<label for="baud">baud</label>
|
|
<input id="baud" type="number" bind:value={cfg.baud} min={1200} step={1} />
|
|
</div>
|
|
</fieldset>
|
|
|
|
<h3>Mechanics</h3>
|
|
<fieldset>
|
|
<div class="pure-control-group" title="Logical steps per mm of axis travel.">
|
|
<label for="steps_per_mm">steps per mm</label>
|
|
<input id="steps_per_mm" type="number" bind:value={cfg.steps_per_mm} step="any" />
|
|
<label for="" class="units">steps/mm</label>
|
|
</div>
|
|
|
|
<div class="pure-control-group" title="Direction sign: +1 or -1. Flip if A+ moves the wrong way.">
|
|
<label for="dir_sign">direction sign</label>
|
|
<select id="dir_sign" bind:value={cfg.dir_sign}>
|
|
<option value={1}>+1</option>
|
|
<option value={-1}>-1</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="pure-control-group" title="gcode axis letter exposed to the planner. Default 'a' (the standard 4th axis).">
|
|
<label for="axis_letter">axis letter</label>
|
|
<select id="axis_letter" bind:value={cfg.axis_letter}>
|
|
<option value="a">A</option>
|
|
<option value="b">B</option>
|
|
<option value="c">C</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="pure-control-group" title="Soft-limit minimum in mm.">
|
|
<label for="min_mm">soft min</label>
|
|
<input id="min_mm" type="number" bind:value={cfg.min_mm} step="any" />
|
|
<label for="" class="units">mm</label>
|
|
</div>
|
|
|
|
<div class="pure-control-group" title="Soft-limit maximum in mm.">
|
|
<label for="max_mm">soft max</label>
|
|
<input id="max_mm" type="number" bind:value={cfg.max_mm} step="any" />
|
|
<label for="" class="units">mm</label>
|
|
</div>
|
|
</fieldset>
|
|
|
|
<h3>Z-A Coupling</h3>
|
|
<p class="tip">
|
|
The auxiliary tool hangs below the Z spindle. Beyond a small
|
|
Z descent the two collide unless A drops with Z. The rule
|
|
in machine coordinates is
|
|
<code>A − Z ≤ (A_home − Z_home) + clearance</code>.
|
|
When enabled, the planner refuses moves that would violate
|
|
it and the gcode preprocessor injects pre-position A moves
|
|
into uploaded files.
|
|
</p>
|
|
<fieldset>
|
|
<div class="pure-control-group" title="Master switch for the Z-A interlock. When off, no checks are performed.">
|
|
<label for="couple_z_enabled">enable coupling</label>
|
|
<input id="couple_z_enabled" type="checkbox" bind:checked={cfg.couple_z_enabled} />
|
|
</div>
|
|
|
|
<div class="pure-control-group" title="How far Z may descend below its home position before A must move with it.">
|
|
<label for="couple_z_clearance_mm">Z clearance</label>
|
|
<input id="couple_z_clearance_mm" type="number" bind:value={cfg.couple_z_clearance_mm} step="any" />
|
|
<label for="" class="units">mm</label>
|
|
</div>
|
|
|
|
<div class="pure-control-group" title="Z's machine position when homed. Almost always 0.">
|
|
<label for="z_home_mm">Z home position</label>
|
|
<input id="z_home_mm" type="number" bind:value={cfg.z_home_mm} step="any" />
|
|
<label for="" class="units">mm</label>
|
|
</div>
|
|
</fieldset>
|
|
|
|
<h3>Planner Limits</h3>
|
|
<fieldset>
|
|
<div class="pure-control-group" title="Maximum velocity used by gplan trajectory planning.">
|
|
<label for="max_velocity_m_per_min">max velocity</label>
|
|
<input id="max_velocity_m_per_min" type="number" bind:value={cfg.max_velocity_m_per_min} step="any" />
|
|
<label for="" class="units">m/min</label>
|
|
</div>
|
|
|
|
<div class="pure-control-group" title="Maximum acceleration used by gplan trajectory planning.">
|
|
<label for="max_accel_km_per_min2">max acceleration</label>
|
|
<input id="max_accel_km_per_min2" type="number" bind:value={cfg.max_accel_km_per_min2} step="any" />
|
|
<label for="" class="units">km/min²</label>
|
|
</div>
|
|
|
|
<div class="pure-control-group" title="Maximum jerk used by gplan trajectory planning.">
|
|
<label for="max_jerk_km_per_min3">max jerk</label>
|
|
<input id="max_jerk_km_per_min3" type="number" bind:value={cfg.max_jerk_km_per_min3} step="any" />
|
|
<label for="" class="units">km/min³</label>
|
|
</div>
|
|
|
|
<div class="pure-control-group" title="Informational max feed; rate caps live on the ESP via step_max_sps.">
|
|
<label for="max_feed_mm_min">max feed</label>
|
|
<input id="max_feed_mm_min" type="number" bind:value={cfg.max_feed_mm_min} step="any" />
|
|
<label for="" class="units">mm/min</label>
|
|
</div>
|
|
</fieldset>
|
|
|
|
<h3>Homing</h3>
|
|
<fieldset>
|
|
<div class="pure-control-group" title="Direction the axis moves when looking for the home limit switch.">
|
|
<label for="home_dir">home direction</label>
|
|
<select id="home_dir" bind:value={cfg.home_dir}>
|
|
<option value="-">- (toward A-)</option>
|
|
<option value="+">+ (toward A+)</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="pure-control-group" title="Axis position assigned when homing completes.">
|
|
<label for="home_position_mm">home position</label>
|
|
<input id="home_position_mm" type="number" bind:value={cfg.home_position_mm} step="any" />
|
|
<label for="" class="units">mm</label>
|
|
</div>
|
|
|
|
<div class="pure-control-group" title="Fast seek rate during homing search.">
|
|
<label for="home_fast_sps">fast seek</label>
|
|
<input id="home_fast_sps" type="number" bind:value={cfg.home_fast_sps} step={1} min={1} />
|
|
<label for="" class="units">steps/s</label>
|
|
</div>
|
|
|
|
<div class="pure-control-group" title="Slow seek rate during homing latch.">
|
|
<label for="home_slow_sps">slow seek</label>
|
|
<input id="home_slow_sps" type="number" bind:value={cfg.home_slow_sps} step={1} min={1} />
|
|
<label for="" class="units">steps/s</label>
|
|
</div>
|
|
|
|
<div class="pure-control-group" title="Backoff after the limit triggers, before the slow seek.">
|
|
<label for="home_backoff_steps">backoff</label>
|
|
<input id="home_backoff_steps" type="number" bind:value={cfg.home_backoff_steps} step={1} min={0} />
|
|
<label for="" class="units">steps</label>
|
|
</div>
|
|
|
|
<div class="pure-control-group" title="Maximum travel before homing aborts as a runaway.">
|
|
<label for="home_maxtravel_steps">max travel</label>
|
|
<input id="home_maxtravel_steps" type="number" bind:value={cfg.home_maxtravel_steps} step={1} min={1} />
|
|
<label for="" class="units">steps</label>
|
|
</div>
|
|
|
|
<div class="pure-control-group" title="Limit switch active-low? Off = active-high.">
|
|
<label for="limit_low">limit active low</label>
|
|
<input id="limit_low" type="checkbox" bind:checked={cfg.limit_low} />
|
|
</div>
|
|
</fieldset>
|
|
|
|
<h3>Step Profile</h3>
|
|
<fieldset>
|
|
<div class="pure-control-group" title="Maximum step rate during normal moves.">
|
|
<label for="step_max_sps">max rate</label>
|
|
<input id="step_max_sps" type="number" bind:value={cfg.step_max_sps} step={1} min={1} />
|
|
<label for="" class="units">steps/s</label>
|
|
</div>
|
|
|
|
<div class="pure-control-group" title="Acceleration in steps per second squared.">
|
|
<label for="step_accel_sps2">acceleration</label>
|
|
<input id="step_accel_sps2" type="number" bind:value={cfg.step_accel_sps2} step={1} min={1} />
|
|
<label for="" class="units">steps/s²</label>
|
|
</div>
|
|
|
|
<div class="pure-control-group" title="Initial step rate at the start of a move.">
|
|
<label for="step_start_sps">start rate</label>
|
|
<input id="step_start_sps" type="number" bind:value={cfg.step_start_sps} step={1} min={1} />
|
|
<label for="" class="units">steps/s</label>
|
|
</div>
|
|
</fieldset>
|
|
|
|
<div class="tip">
|
|
Changes are written to aux.json when you click the
|
|
master <strong>Save</strong> button at the bottom of the
|
|
settings rail. Homing rates and the limit polarity are
|
|
pushed to the ESP immediately; any running motion is
|
|
unaffected. Re-home the auxiliary axis after changing direction,
|
|
sign, or step settings.
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<style lang="scss">
|
|
.a-axis-settings {
|
|
.status {
|
|
margin-bottom: 1em;
|
|
font-size: 90%;
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.actions {
|
|
margin-left: 210px;
|
|
margin-top: 1em;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1em;
|
|
}
|
|
|
|
.save-msg {
|
|
font-style: italic;
|
|
}
|
|
|
|
.tip {
|
|
margin-left: 210px;
|
|
margin-top: 1em;
|
|
margin-bottom: 15px;
|
|
font-style: italic;
|
|
font-size: 90%;
|
|
line-height: 1.5;
|
|
}
|
|
}
|
|
</style>
|