Make feed override control the WASM trajectory
This commit is contained in:
@@ -59,7 +59,7 @@ const PANEL_GATE_ACTIONS = Object.freeze({
|
||||
const WASM_GATE_COMMANDS = Object.freeze({
|
||||
ESTOP: 1, RESET: 2, POWER: 3, HOME: 4, JOG: 5, MDI: 6, RUN: 7,
|
||||
PAUSE: 8, RESUME: 9, ABORT: 10, RESTART: 11, KINEMATICS: 12,
|
||||
SET_MODE: 13, PROGRAM_LOAD: 14, STEP: 15,
|
||||
SET_MODE: 13, PROGRAM_LOAD: 14, STEP: 15, OVERRIDE: 16,
|
||||
});
|
||||
|
||||
const WASM_GATE_REASONS = Object.freeze({
|
||||
@@ -228,7 +228,7 @@ export class SimSession {
|
||||
axes: { x: position.x, y: position.y, z: position.z },
|
||||
joints: kinematicsJoints ?? (home.status === "verified" && Array.isArray(home.state.joints) ? [...home.state.joints] : [position.x, position.y, position.z, position.a ?? 0, position.c ?? 0]),
|
||||
velocity: wasmMotion.status === "verified" ? wasmMotion.state.velocity : 0,
|
||||
feed_override: 1,
|
||||
feed_override: this.wasmMotion.controls?.feed_override ?? 1,
|
||||
spindle_override: 1,
|
||||
homed: Boolean(home.state?.homed),
|
||||
homed_axes: Array.from({ length: 5 }, (_, index) => Boolean(home.state?.homed && index < (home.state?.joint_count ?? 0))),
|
||||
@@ -319,6 +319,7 @@ export class SimSession {
|
||||
jogStart: this.xyzacCommandGate({ kind: "JOG_START", payload: {} }),
|
||||
jogStop: this.xyzacCommandGate({ kind: "JOG_STOP", payload: {} }),
|
||||
home: this.xyzacCommandGate({ kind: "HOME", payload: {} }),
|
||||
override: this.xyzacCommandGate({ kind: "SET_OVERRIDE", payload: { feed: 1 } }),
|
||||
estop: this.xyzacCommandGate({ kind: "ESTOP", payload: { active: true } }),
|
||||
recover: this.xyzacCommandGate({ kind: "IO_RECOVER", payload: {} }),
|
||||
power: this.xyzacCommandGate({ kind: "POWER", payload: { active: ioState.estop ? false : !Boolean(ioState.ready) } }),
|
||||
@@ -439,7 +440,8 @@ export class SimSession {
|
||||
const ready = Boolean(io.ready) && !estop;
|
||||
const loaded = program.status !== 0;
|
||||
const action = PANEL_GATE_ACTIONS[input.kind] ?? null;
|
||||
const command = input.kind === "JOG_STOP" || input.kind === "JOG_START" || input.kind === "SET_OVERRIDE" ? WASM_GATE_COMMANDS.JOG
|
||||
const command = input.kind === "JOG_STOP" || input.kind === "JOG_START" ? WASM_GATE_COMMANDS.JOG
|
||||
: input.kind === "SET_OVERRIDE" ? WASM_GATE_COMMANDS.OVERRIDE
|
||||
: input.kind === "PROGRAM_LOAD" ? WASM_GATE_COMMANDS.PROGRAM_LOAD
|
||||
: input.kind === "PROGRAM_RESTART" ? WASM_GATE_COMMANDS.RESTART
|
||||
: input.kind === "PROGRAM_RUN" ? WASM_GATE_COMMANDS.RUN
|
||||
@@ -1813,7 +1815,13 @@ export class SimSession {
|
||||
if (input.kind === "SET_OVERRIDE") {
|
||||
const feed = Number(input.payload?.feed ?? input.payload?.feed_override);
|
||||
if (!Number.isFinite(feed) || feed < 0 || feed > 2) return finish(this.error(input, "motion_override", "motion_override_range"), "failed");
|
||||
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, feed_override: feed, state: { ...this.motion.export(), feed_override: feed }, motion: this.motionProjection(), motion_authority: "wasm", operation_id: input.operation_id ?? null }), "verified");
|
||||
try {
|
||||
this.wasmMotion.setFeedOverride(feed);
|
||||
} catch (error) {
|
||||
return finish(this.error(input, "motion_override", String(error?.message ?? error)), "failed");
|
||||
}
|
||||
const motion = this.wasmMotionProjection();
|
||||
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, feed_override: feed, overrides: { feed, spindle: [this.wasmMotionProjection().spindle_override] }, state: motion, motion, wasm_motion: this.wasmMotion.exportState(), motion_authority: "wasm", operation_id: input.operation_id ?? null }), "verified");
|
||||
}
|
||||
if (input.kind === "FOLLOWING_ERROR") return finish(this.error(input, "motion_following_error", "wasm_following_error_unavailable"), "failed");
|
||||
const current = this.wasmMotion.read();
|
||||
|
||||
65
web/worker/test_xyzac_feed_override.mjs
Normal file
65
web/worker/test_xyzac_feed_override.mjs
Normal file
@@ -0,0 +1,65 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import createModule from "../../build-wasm/linuxcnc-sim-core.js";
|
||||
import { loadSimulationCase, toWorkerConfiguration } from "../workspace/sim-case-loader.mjs";
|
||||
import { SimSession, CONTEXT_ID } from "./sim-session.mjs";
|
||||
|
||||
const CASE = "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini";
|
||||
const fetchImpl = async (url) => {
|
||||
try {
|
||||
const body = await readFile(new URL(`../../${String(url).replace(/^\//, "")}`, import.meta.url));
|
||||
return { ok: true, text: async () => body.toString("utf8") };
|
||||
} catch {
|
||||
return { ok: false, text: async () => "" };
|
||||
}
|
||||
};
|
||||
|
||||
const bundle = await loadSimulationCase({ path: CASE, fetchImpl });
|
||||
assert.equal(bundle.status, "ready");
|
||||
const core = await createModule({ wasmBinary: await readFile(new URL("../../build-wasm/linuxcnc-sim-core.wasm", import.meta.url)) });
|
||||
const session = new SimSession(core, { sessionId: "xyzac-feed-override" });
|
||||
const base = { schema_version: 1, context_id: CONTEXT_ID, session_id: session.sessionId };
|
||||
let seq = 0;
|
||||
const send = (kind, payload = {}) => session.accept({ ...base, kind, seq: ++seq, operation_id: `override-${seq}`, payload });
|
||||
|
||||
send("HELLO");
|
||||
send("SESSION_ATTACH");
|
||||
assert.equal(send("LOAD_WORKSPACE", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) }).payload.status, "verified");
|
||||
assert.equal(send("PROGRAM_LOAD", { path: "feed-override.ngc", source: "G21 G90\nG1 X10 F100\nM2\n" }).payload.status, "verified");
|
||||
assert.equal(send("POWER", { active: true }).payload.status, "verified");
|
||||
assert.equal(send("HOME", { all: true }).payload.status, "verified");
|
||||
assert.equal(send("SET_OVERRIDE", { feed: 0.5 }).payload.status, "verified");
|
||||
const half = send("PROGRAM_RUN");
|
||||
assert.equal(half.payload.status, "verified");
|
||||
const halfStep = send("SIM_STEP", { cycles: 3 });
|
||||
assert.equal(halfStep.payload.status, "verified");
|
||||
assert.equal(session.wasmMotion.controls.feed_override, 0.5);
|
||||
assert.equal(halfStep.payload.panel_snapshot.overrides.feed, 0.5);
|
||||
assert.ok(halfStep.payload.wasm_motion.state.velocity > 0.82 && halfStep.payload.wasm_motion.state.velocity < 0.85, JSON.stringify(halfStep.payload.wasm_motion.state));
|
||||
|
||||
const fullOverride = send("SET_OVERRIDE", { feed: 2 });
|
||||
assert.equal(fullOverride.payload.status, "verified");
|
||||
assert.equal(fullOverride.payload.panel_snapshot.overrides.feed, 2);
|
||||
const fullStep = send("SIM_STEP", { cycles: 1 });
|
||||
assert.equal(session.wasmMotion.controls.feed_override, 2);
|
||||
assert.ok(fullStep.payload.wasm_motion.state.velocity > 3.32 && fullStep.payload.wasm_motion.state.velocity < 3.35, JSON.stringify(fullStep.payload.wasm_motion.state));
|
||||
|
||||
const invalid = send("SET_OVERRIDE", { feed: 2.01 });
|
||||
assert.equal(invalid.kind, "ERROR");
|
||||
assert.equal(invalid.payload.code, "motion_override");
|
||||
|
||||
const checkpoint = send("CHECKPOINT");
|
||||
assert.equal(checkpoint.payload.status, "verified");
|
||||
const recovered = send("RECOVER", { checkpoint: checkpoint.payload.checkpoint });
|
||||
assert.ok(["verified", "recovered"].includes(recovered.payload.status));
|
||||
assert.equal(session.wasmMotion.controls.feed_override, 2);
|
||||
|
||||
console.log(JSON.stringify({
|
||||
schema: "xyzac-feed-override.v1",
|
||||
status: "verified",
|
||||
authority: "linuxcnc-wasm-motion-queue",
|
||||
half: { requested: 50, base_feed_mm_min: 100, actual: halfStep.payload.wasm_motion.state.velocity, panel: halfStep.payload.panel_snapshot.overrides.feed },
|
||||
full: { requested: 200, base_feed_mm_min: 100, actual: fullStep.payload.wasm_motion.state.velocity, panel: fullStep.payload.panel_snapshot.overrides.feed },
|
||||
invalid: invalid.payload.message_key,
|
||||
recovery_feed: session.wasmMotion.controls.feed_override,
|
||||
}));
|
||||
@@ -6,7 +6,7 @@ const SEGMENT_INT_OFFSET = SEGMENT_DOUBLES * 2;
|
||||
const QUEUE_HEADER_BYTES = 32;
|
||||
const STATE_POSITION_BYTES = 72;
|
||||
const MOTION_QUEUE_SCHEMA = "sim_motion_queue_state.v1";
|
||||
const DEFAULT_CONTROLS = Object.freeze({ profile: 0, lookahead: 1, acceleration_limit: 1e9, jerk_limit: 1e12, paused: false });
|
||||
const DEFAULT_CONTROLS = Object.freeze({ profile: 0, lookahead: 1, acceleration_limit: 1e9, jerk_limit: 1e12, feed_override: 1, paused: false });
|
||||
const MOTION_ERROR_REASONS = Object.freeze({
|
||||
[-600]: "wasm_motion_invalid_argument", [-601]: "wasm_motion_nonfinite_value", [-602]: "wasm_motion_limit_range",
|
||||
[-603]: "wasm_motion_invalid_state", [-604]: "wasm_motion_queue_full", [-605]: "wasm_motion_paused",
|
||||
@@ -53,9 +53,10 @@ function controls(value = {}) {
|
||||
lookahead: value.lookahead ?? DEFAULT_CONTROLS.lookahead,
|
||||
acceleration_limit: value.acceleration_limit ?? DEFAULT_CONTROLS.acceleration_limit,
|
||||
jerk_limit: value.jerk_limit ?? DEFAULT_CONTROLS.jerk_limit,
|
||||
feed_override: value.feed_override ?? DEFAULT_CONTROLS.feed_override,
|
||||
paused: value.paused ?? DEFAULT_CONTROLS.paused,
|
||||
};
|
||||
if (![0, 1].includes(result.profile) || !Number.isSafeInteger(result.lookahead) || result.lookahead < 1 || !Number.isFinite(result.acceleration_limit) || result.acceleration_limit <= 0 || !Number.isFinite(result.jerk_limit) || result.jerk_limit <= 0 || typeof result.paused !== "boolean") throw new Error("wasm_motion_controls_invalid");
|
||||
if (![0, 1].includes(result.profile) || !Number.isSafeInteger(result.lookahead) || result.lookahead < 1 || !Number.isFinite(result.acceleration_limit) || result.acceleration_limit <= 0 || !Number.isFinite(result.jerk_limit) || result.jerk_limit <= 0 || !Number.isFinite(result.feed_override) || result.feed_override < 0 || result.feed_override > 2 || typeof result.paused !== "boolean") throw new Error("wasm_motion_controls_invalid");
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -101,19 +102,22 @@ export class WasmMotionQueue {
|
||||
[...poseToArray(segment.start), ...poseToArray(segment.end), Number(segment.center?.x ?? 0), Number(segment.center?.y ?? 0), Number(segment.center?.z ?? 0), Number(segment.normal?.x ?? 0), Number(segment.normal?.y ?? 0), Number(segment.normal?.z ?? 0), Number(segment.velocity ?? 0), Number(segment.acceleration ?? 0)].forEach((value, index) => { doubles[base + index] = value; });
|
||||
ints[this.command / 4 + SEGMENT_INT_OFFSET] = Number(segment.motion_type ?? 1);
|
||||
ints[this.command / 4 + SEGMENT_INT_OFFSET + 1] = Number(segment.turn ?? 0);
|
||||
// sim_motion_command layout is segment (216 bytes), id (4), tag (4).
|
||||
// sim_motion_command layout is segment (216 bytes), id (4), tag (4), flags (4).
|
||||
// Keep the WASM switchkins mode in the tag field; the C queue assigns id.
|
||||
ints[this.command / 4 + SEGMENT_INT_OFFSET + 3] = Number(segment.kinematics_mode ?? segment.tag ?? 0);
|
||||
ints[this.command / 4 + SEGMENT_INT_OFFSET + 4] = segment.kind === "rapid" ? 1 : 0;
|
||||
const result = call(this.core, "sim_motion_enqueue", "number", ["number", "number", "number"], [this.queue, this.command, 0]);
|
||||
return result === 0 ? { status: "verified", source_line: segment.source_line ?? null } : { status: "error", code: `wasm_motion_enqueue_${result}`, reason: MOTION_ERROR_REASONS[result] ?? `wasm_motion_enqueue_${result}`, message_key: MOTION_ERROR_REASONS[result] ?? `wasm_motion_enqueue_${result}`, source: "linuxcnc-wasm-motion" };
|
||||
}
|
||||
|
||||
reset() {
|
||||
if (!this.available) return this.read();
|
||||
const previousFeedOverride = this.controls.feed_override;
|
||||
this.core.HEAPU8.fill(0, this.queue, this.queue + this.queueSize);
|
||||
const result = call(this.core, "sim_motion_init", "number", ["number", "number", "number"], [this.queue, this.cycleTime, this.velocityLimit]);
|
||||
if (result !== 0) throw new Error(`wasm_motion_reset_${result}`);
|
||||
this.controls = controls();
|
||||
this.setFeedOverride(previousFeedOverride);
|
||||
return this.read();
|
||||
}
|
||||
|
||||
@@ -144,6 +148,15 @@ export class WasmMotionQueue {
|
||||
return this.read();
|
||||
}
|
||||
|
||||
setFeedOverride(feedOverride) {
|
||||
const next = controls({ ...this.controls, feed_override: feedOverride });
|
||||
if (!this.available) { this.controls = next; return this.read(); }
|
||||
const result = optionalCall(this.core, "sim_motion_set_feed_override", "number", ["number", "number"], [this.queue, next.feed_override]);
|
||||
if (result !== null && result !== 0) throw new Error(`wasm_motion_feed_override_${result}`);
|
||||
this.controls = next;
|
||||
return this.read();
|
||||
}
|
||||
|
||||
setPaused(paused) {
|
||||
const next = controls({ ...this.controls, paused: Boolean(paused) });
|
||||
if (!this.available) { this.controls = next; return this.read(); }
|
||||
@@ -158,11 +171,13 @@ export class WasmMotionQueue {
|
||||
const current = this.read();
|
||||
if (current.status !== "verified") throw new Error("wasm_motion_abort_state");
|
||||
const previous = this.core.HEAPU8.slice(this.queue, this.queue + this.queueSize);
|
||||
const previousFeedOverride = this.controls.feed_override;
|
||||
const headerOffset = this.queueCapacity * this.commandCapacity;
|
||||
const stateOffset = headerOffset + QUEUE_HEADER_BYTES;
|
||||
const previousView = new DataView(previous.buffer, previous.byteOffset, previous.byteLength);
|
||||
const nextId = previousView.getUint32(headerOffset + 8, true);
|
||||
this.reset();
|
||||
this.setFeedOverride(previousFeedOverride);
|
||||
const bytes = new Uint8Array(this.queueSize);
|
||||
const view = new DataView(bytes.buffer);
|
||||
// sim_motion_queue stores commands first, followed by its 32-byte header.
|
||||
@@ -239,6 +254,7 @@ export class WasmMotionQueue {
|
||||
this.setProfile(nextControls.profile);
|
||||
this.setLookahead(nextControls.lookahead);
|
||||
this.setDynamics(nextControls.acceleration_limit, nextControls.jerk_limit);
|
||||
this.setFeedOverride(nextControls.feed_override);
|
||||
this.setPaused(nextControls.paused);
|
||||
this.core.HEAPU8.set(bytes, this.queue);
|
||||
const actual = this.read();
|
||||
|
||||
Reference in New Issue
Block a user