Enforce LinuxCNC WASM authority for XYZAC frontend
This commit is contained in:
@@ -37,21 +37,11 @@ export function axisState({ task = {}, program = {}, motion = {}, devices = {},
|
||||
const hasIoState = typeof ioState.estop === "boolean" || typeof ioState.estop === "number";
|
||||
const ioEstop = hasIoState && Boolean(ioState.estop);
|
||||
const ioReady = typeof ioState.ready === "boolean" || typeof ioState.ready === "number" ? Boolean(ioState.ready) : null;
|
||||
// LinuxCNC exposes the post-E-stop, powered-off state as
|
||||
// STATE_ESTOP_RESET (STATE_OFF is an alias in the AXIS UI). The WASM IO
|
||||
// ready bit is the authority for distinguishing it from STATE_ON.
|
||||
const estopReset = hasIoState && !ioEstop && ioReady === false;
|
||||
// A powered-off machine is OFF. ESTOP_RESET is distinct and is only
|
||||
// reported after the WASM IO latch has recorded the ESTOP edge.
|
||||
const estopReset = hasIoState && !ioEstop && ioReady === false && Number(ioState.last_error) === -904;
|
||||
let machineState = ioEstop || ["ESTOP", "FAULT"].includes(taskState) ? "ESTOP" : estopReset ? "ESTOP_RESET" : ioReady === false ? "OFF" : ["READY", "RUNNING", "PAUSED", "STOPPED"].includes(taskState) ? "ON" : "OFF";
|
||||
let operationState = programActivity(task, program);
|
||||
if (action === "ESTOP" && payload.active === true) machineState = "ESTOP";
|
||||
if (action === "ESTOP" && payload.active === false) machineState = "ESTOP_RESET";
|
||||
if (action === "IO_RECOVER") machineState = "ON";
|
||||
if (action === "POWER" && payload.active === true) machineState = "ON";
|
||||
if (action === "POWER" && payload.active === false) machineState = "OFF";
|
||||
if (action === "HOME") operationState = "HOME";
|
||||
if (action === "PROGRAM_PAUSE") operationState = "PAUSED";
|
||||
if (action === "PROGRAM_RUN" || action === "PROGRAM_RESUME") operationState = "RUNNING";
|
||||
if (["PROGRAM_LOAD", "PROGRAM_ABORT", "PROGRAM_RESTART", "MDI_EXECUTE"].includes(action)) operationState = "IDLE";
|
||||
const state = machineState === "ESTOP" || machineState === "ESTOP_RESET" ? machineState : operationState === "HOME" ? "HOME" : operationState === "RUNNING" ? "RUNNING" : operationState === "PAUSED" ? "PAUSED" : machineState === "OFF" ? "OFF" : "IDLE";
|
||||
const sourceTaskState = machineState === "ESTOP" ? "STATE_ESTOP" : machineState === "ESTOP_RESET" ? "STATE_ESTOP_RESET" : machineState === "OFF" ? "STATE_OFF" : "STATE_ON";
|
||||
const sourceInterpState = operationState === "PAUSED" ? "INTERP_PAUSED" : operationState === "RUNNING" ? "INTERP_READING" : "INTERP_IDLE";
|
||||
|
||||
@@ -37,7 +37,7 @@ const OPERATION_KINDS = new Set([
|
||||
"SESSION_ATTACH", "LOAD_WORKSPACE", "SIM_STEP", "DESTROY", "CHECKPOINT", "RECOVER", "ABORT",
|
||||
"COMMAND", "START", "STOP", "RESET", "PROGRAM_LOAD", "PROGRAM_RUN",
|
||||
"PROGRAM_PAUSE", "PROGRAM_RESUME", "PROGRAM_ABORT", "PROGRAM_RESTART", "JOG_START", "JOG_STOP",
|
||||
"HOME", "SET_MODE", "SET_OVERRIDE", "SPINDLE_COMMAND", "SPINDLE_BRAKE", "TOOL_PREPARE",
|
||||
"HOME", "SET_MODE", "SET_OVERRIDE", "SET_JOG_SCALE", "SET_MAX_VELOCITY", "SET_SPINDLE_OVERRIDE", "SPINDLE_COMMAND", "SPINDLE_BRAKE", "TOOL_PREPARE",
|
||||
"TOOL_CHANGED", "TOOL_PREPARED", "TOOL_CHANGE", "TOOL_ABORT", "TOOL_FAULT", "TOOL_CLEAR_FAULT",
|
||||
"SPINDLE_FAULT", "SPINDLE_INHIBIT", "SPINDLE_ORIENT", "SPINDLE_ORIENTED", "IO_STEP", "IO_RECOVER",
|
||||
"PROBE_CONFIG", "LIMIT_SET", "ESTOP", "HAL_COMMAND", "HAL_SET",
|
||||
@@ -52,7 +52,8 @@ const OPERATION_KINDS = new Set([
|
||||
const PANEL_GATE_ACTIONS = Object.freeze({
|
||||
PROGRAM_RUN: "run", PROGRAM_STEP: "step", PROGRAM_PAUSE: "pause",
|
||||
PROGRAM_RESUME: "resume", PROGRAM_ABORT: "abort", PROGRAM_RESTART: "restart",
|
||||
JOG_START: "jogStart", JOG_STOP: "jogStop", HOME: "home", SET_OVERRIDE: "override",
|
||||
JOG_START: "jogStart", JOG_STOP: "jogStop", SET_JOG_SCALE: "jogStart", SET_MAX_VELOCITY: "override", SET_SPINDLE_OVERRIDE: "override", HOME: "home", SET_OVERRIDE: "override",
|
||||
SPINDLE_COMMAND: "spindle", SPINDLE_BRAKE: "spindle", COOLANT_COMMAND: "coolant", TOOL_PREPARE: "tool", TOOL_CHANGE: "tool", PROBE_CONFIG: "probe",
|
||||
ESTOP: "estop", IO_RECOVER: "recover",
|
||||
POWER: "power",
|
||||
});
|
||||
@@ -61,6 +62,7 @@ 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, OVERRIDE: 16,
|
||||
SPINDLE: 17, COOLANT: 18, TOOL: 19, PROBE: 20, TOUCH_OFF: 21, TOOL_TOUCH_OFF: 22,
|
||||
});
|
||||
|
||||
const WASM_GATE_REASONS = Object.freeze({
|
||||
@@ -232,7 +234,7 @@ export class SimSession {
|
||||
motion_authority: responsePayload.motion_authority ?? this.motionAuthority,
|
||||
task_authority: responsePayload.task_authority ?? this.task.authority(),
|
||||
...(!lightweightXyzacStep ? { wasm_home_limit: statePayload.wasm_home_limit ?? this.wasmHomeLimit.read() } : {}),
|
||||
...(traceOnlyStep ? {} : { axis_state: axisState({ task, program, motion: this.motionProjection(), devices: {}, io: this.xyzacIo?.canonical?.() ?? {}, device_io: this.xyzacDeviceIo?.canonical?.() ?? this.deviceIo.export(), action: input.kind, payload: input.payload ?? {} }) }),
|
||||
...(traceOnlyStep ? {} : { axis_state: axisState({ task, program, motion: this.motionProjection(), devices: {}, io: this.xyzacIo?.canonical?.() ?? {}, device_io: this.xyzacDeviceIo?.canonical?.() ?? this.deviceIo.export() }) }),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -253,10 +255,15 @@ 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: this.wasmMotion.controls?.feed_override ?? 1,
|
||||
spindle_override: 1,
|
||||
feed_override: Number(wasmMotion.state?.feed_override ?? 1),
|
||||
spindle_override: Number(ioState?.spindles?.[0]?.override ?? 1),
|
||||
velocity_scale: Number(wasmMotion.state?.velocity_scale ?? 1),
|
||||
jog_scale: Number(wasmMotion.state?.jog_scale ?? 1),
|
||||
jog_velocity_linear: Number(wasmMotion.state?.jog_velocity_linear ?? 0),
|
||||
jog_velocity_angular: Number(wasmMotion.state?.jog_velocity_angular ?? 0),
|
||||
max_velocity: Number(wasmMotion.state?.velocity_limit ?? 0),
|
||||
homed: Boolean(home.state?.homed),
|
||||
homed_axes: Array.from({ length: 5 }, (_, index) => Boolean(home.state?.homed && index < (home.state?.joint_count ?? 0))),
|
||||
homed_axes: Array.from({ length: 5 }, (_, index) => Boolean(Number(home.state?.homed_mask ?? 0) & (1 << index))),
|
||||
limits: { x: Boolean(home.state?.limit_error), y: Boolean(home.state?.limit_error), z: Boolean(home.state?.limit_error), a: Boolean(home.state?.limit_error), c: Boolean(home.state?.limit_error) },
|
||||
on_any_limit: Boolean(home.state?.limit_error),
|
||||
following_error: 0,
|
||||
@@ -326,7 +333,7 @@ export class SimSession {
|
||||
const authoritative = this.xyzacProgram
|
||||
? { schema: "wasm_authority_boundary.v1", source: "linuxcnc-wasm-worker", state_hash: fnv1a64({ motion: wasmMotion, kinematics: this.xyzacKinematics?.snapshot() ?? null, task }) }
|
||||
: this.authoritative.export();
|
||||
const axis = axisState({ task, program: program ?? {}, motion, devices, io: this.xyzacIo ? this.xyzacIo.export() : {}, device_io: deviceIo, action: input.kind, payload: input.payload ?? {} });
|
||||
const axis = axisState({ task, program: program ?? {}, motion, devices, io: this.xyzacIo ? this.xyzacIo.export() : {}, device_io: deviceIo });
|
||||
const xyzac = this.xyzacKinematics ? { coordinates: "XYZAC", joint_count: 5, hal_pin_vars: true, switchkins_net: true, finite_offsets: true, userk_capability: true, active_type: this.xyzacKinematics.snapshot().active_type } : null;
|
||||
const gateInput = { task, program: program ?? {}, motion, devices, io: this.xyzacIo ? this.xyzacIo.export() : {}, device_io: deviceIo, xyzac, taskfile: Boolean(this.xyzacProgram ? this.xyzacProgram.snapshot().status !== 0 : this.program.loaded), kinematicsIdentity: this.motionAuthority === "wasm" };
|
||||
const gateActions = ["load", "run", "runLine", "step", "pause", "resume", "abort", "restart", "reload", "edit", "mdi", "jogStart", "jogStop", "home", "estop", "recover", "override", "overrideLimits", "reset", "spindle", "coolant", "touchOff", "toolTouchOff"];
|
||||
@@ -345,6 +352,15 @@ export class SimSession {
|
||||
jogStop: this.xyzacCommandGate({ kind: "JOG_STOP", payload: {} }),
|
||||
home: this.xyzacCommandGate({ kind: "HOME", payload: {} }),
|
||||
override: this.xyzacCommandGate({ kind: "SET_OVERRIDE", payload: { feed: 1 } }),
|
||||
jogScale: this.xyzacCommandGate({ kind: "SET_JOG_SCALE", payload: { scale: 1 } }),
|
||||
maxVelocity: this.xyzacCommandGate({ kind: "SET_MAX_VELOCITY", payload: { scale: 1 } }),
|
||||
spindleOverride: this.xyzacCommandGate({ kind: "SET_SPINDLE_OVERRIDE", payload: { scale: 1 } }),
|
||||
spindle: this.xyzacCommandGate({ kind: "SPINDLE_COMMAND", payload: {} }),
|
||||
coolant: this.xyzacCommandGate({ kind: "COOLANT_COMMAND", payload: {} }),
|
||||
tool: this.xyzacCommandGate({ kind: "TOOL_PREPARE", payload: {} }),
|
||||
probe: this.xyzacCommandGate({ kind: "PROBE_CONFIG", payload: {} }),
|
||||
touchOff: this.xyzacCommandGate({ kind: "TOUCH_OFF", payload: {} }),
|
||||
toolTouchOff: this.xyzacCommandGate({ kind: "TOOL_TOUCH_OFF", payload: {} }),
|
||||
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) } }),
|
||||
@@ -366,7 +382,7 @@ export class SimSession {
|
||||
: { x: wasmPosition.x, y: wasmPosition.y, z: wasmPosition.z };
|
||||
const inverseJoints = this.xyzacKinematics?.snapshot?.()?.joints;
|
||||
const joints = this.motionAuthority === "wasm" && wasmMotion.status === "verified"
|
||||
? (inverseJoints?.length === 5 ? [...inverseJoints] : [position.x, position.y, position.z, position.a ?? 0, position.c ?? 0])
|
||||
? (Array.isArray(wasmMotion.state.joint_commanded) && wasmMotion.state.joint_commanded.length === 5 ? [...wasmMotion.state.joint_commanded] : inverseJoints?.length === 5 ? [...inverseJoints] : [position.x, position.y, position.z, position.a ?? 0, position.c ?? 0])
|
||||
: authoritative.kinematics.joints;
|
||||
const preview = this.previewState?.status === "verified" ? this.previewState : null;
|
||||
const visual = {
|
||||
@@ -465,8 +481,15 @@ 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" ? WASM_GATE_COMMANDS.JOG
|
||||
const command = input.kind === "JOG_STOP" || input.kind === "JOG_START" || input.kind === "SET_JOG_SCALE" ? WASM_GATE_COMMANDS.JOG
|
||||
: input.kind === "SET_MAX_VELOCITY" || input.kind === "SET_SPINDLE_OVERRIDE" ? WASM_GATE_COMMANDS.OVERRIDE
|
||||
: input.kind === "SET_OVERRIDE" ? WASM_GATE_COMMANDS.OVERRIDE
|
||||
: input.kind === "SPINDLE_COMMAND" || input.kind === "SPINDLE_BRAKE" ? WASM_GATE_COMMANDS.SPINDLE
|
||||
: input.kind === "COOLANT_COMMAND" ? WASM_GATE_COMMANDS.COOLANT
|
||||
: input.kind === "TOOL_PREPARE" || input.kind === "TOOL_CHANGE" ? WASM_GATE_COMMANDS.TOOL
|
||||
: input.kind === "PROBE_CONFIG" ? WASM_GATE_COMMANDS.PROBE
|
||||
: input.kind === "TOUCH_OFF" ? WASM_GATE_COMMANDS.TOUCH_OFF
|
||||
: input.kind === "TOOL_TOUCH_OFF" ? WASM_GATE_COMMANDS.TOOL_TOUCH_OFF
|
||||
: 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
|
||||
@@ -485,6 +508,7 @@ export class SimSession {
|
||||
if (!command) return { action, enabled: true, reason: "linuxcnc_wasm_authority" };
|
||||
const requested = input.kind === "POWER" || input.kind === "ESTOP" ? (input.payload?.active === true ? 1 : 0)
|
||||
: input.kind === "KINEMATICS_SELECT" ? Number(input.payload?.type)
|
||||
: input.kind === "TOOL_TOUCH_OFF" ? Number(io.tool?.current ?? 0) > 0 ? 1 : 0
|
||||
: input.kind === "SET_MODE" ? ({ MANUAL: 0, MDI: 1, AUTO: 2 }[String(input.payload?.mode ?? "").toUpperCase()] ?? 3) : 0;
|
||||
const result = this.task.commandGate(command, {
|
||||
requested,
|
||||
@@ -1415,8 +1439,11 @@ export class SimSession {
|
||||
if (!Number.isFinite(this.xyzacMotionCycleTime) || this.xyzacMotionCycleTime <= 0) throw new Error("wasm_xyzac_cycle_time_invalid");
|
||||
const taskCycleTime = Number(configuration.kinematics?.task_cycle_time ?? this.xyzacMotionCycleTime);
|
||||
this.xyzacTaskCycleTicks = Math.max(1, Math.round(taskCycleTime / this.xyzacMotionCycleTime));
|
||||
this.wasmMotion = this.planner === "linuxcnc-tp"
|
||||
? new WasmLinuxcncTp(this.core, { cycleTime: this.xyzacMotionCycleTime, velocityLimit: this.xyzacRapidVelocity, accelerationLimit: Number(configuration.kinematics?.max_acceleration ?? 300) })
|
||||
// The selected XYZAC TRT case always uses the upstream LinuxCNC TP;
|
||||
// a caller must not silently downgrade it to the compatibility
|
||||
// motion queue by omitting the planner option.
|
||||
this.wasmMotion = configuration.kinematics.type === "xyzac-trt"
|
||||
? new WasmLinuxcncTp(this.core, { cycleTime: this.xyzacMotionCycleTime, velocityLimit: this.xyzacRapidVelocity, accelerationLimit: Number(configuration.kinematics?.max_acceleration ?? 300), limits: configuration.home_limits ?? {} })
|
||||
: new WasmMotionQueue(this.core, { cycleTime: this.xyzacMotionCycleTime, velocityLimit: 1e9 });
|
||||
this.xyzacProgram = new WasmProgramRuntime(this.core);
|
||||
this.xyzacProgramKinematicsMode = XYZAC_MODES.identity;
|
||||
@@ -1706,7 +1733,7 @@ export class SimSession {
|
||||
this.xyzacAppendDeferredOnActive = false;
|
||||
if (typeof this.wasmMotion.setQueueDepthOverride === "function") this.wasmMotion.setQueueDepthOverride(null);
|
||||
servoResult.state.queue_depth = 2;
|
||||
if (Array.isArray(this.wasmMotion.history) && this.wasmMotion.history.length) this.wasmMotion.history[this.wasmMotion.history.length - 1].queue_depth = 2;
|
||||
if (this.wasmMotion.lastState) this.wasmMotion.lastState.queue_depth = 2;
|
||||
}
|
||||
if (this.planner === "linuxcnc-tp" && this.xyzacExecutionWaitSamples > 0 && Number(servoResult.state.active_id ?? 0) !== 0) this.xyzacExecutionWaitSamples -= 1;
|
||||
if (this.planner === "linuxcnc-tp" && this.xyzacDeferredPhysicalSwitchMode !== null && Number(servoResult.state.active_tag ?? -1) === this.xyzacDeferredPhysicalSwitchMode) {
|
||||
@@ -1796,8 +1823,16 @@ export class SimSession {
|
||||
}
|
||||
const snapshotState = { tick: this.tick, cycles, motion_authority: this.motionAuthority, task: this.task.export(), scheduler: this.scheduler.export(), authoritative: this.xyzacProgram ? { schema: "wasm_authority_boundary.v1", source: "linuxcnc-wasm-worker" } : this.authoritative.export(), wasm_motion: this.wasmMotion.read(), motion: this.motionProjection(), plc: this.plc.export(), hal_runtime: this.xyzacIo ? this.xyzacIo.halExport() : this.hal.export(), device_io: this.xyzacDeviceIo ? this.xyzacDeviceIo.export() : this.xyzacIo ? this.xyzacIo.export() : this.deviceIo.export(), configuration_batch: this.configurationBatch.export(), io_ack: this.ioAck.export(), callbacks: this.callbacks.export(), xyzac_timing: { servo_cycle_time: this.xyzacMotionCycleTime, task_cycle_ticks: this.xyzacTaskCycleTicks } };
|
||||
if (program) snapshotState.program = program;
|
||||
const snapshot = input.payload?.trace_only === true ? null : this.timeline.checkpoint(input.seq, this.tick, snapshotState, this.timeline.snapshots.latest()?.snapshot_id ?? null);
|
||||
const lightweightXyzac = Boolean(this.xyzacProgram);
|
||||
// A high-frequency XYZAC motion-delta is already backed by the WASM
|
||||
// state returned in this response. Persisting a full structured
|
||||
// snapshot for every task tick makes the browser replay store grow
|
||||
// quadratically because each snapshot contains the complete program
|
||||
// and runtime state. Explicit CHECKPOINT/trace/power-off requests
|
||||
// still materialize a durable snapshot through createCheckpoint().
|
||||
const snapshot = input.payload?.trace_only === true || (lightweightXyzac && input.payload?.response === "motion_delta" && input.payload?.checkpoint !== true)
|
||||
? null
|
||||
: this.timeline.checkpoint(input.seq, this.tick, snapshotState, this.timeline.snapshots.latest()?.snapshot_id ?? null);
|
||||
return finish(this.envelope("COMMAND_RESULT", input, {
|
||||
status: "verified", cycles, program_steps, execution_trace,
|
||||
...(lightweightXyzac && cycles <= 32 && motion_trace.length ? { motion_trace } : {}),
|
||||
@@ -2097,7 +2132,7 @@ export class SimSession {
|
||||
case "HOME": {
|
||||
if (this.xyzacProgram) {
|
||||
if (this.wasmHomeLimit.read().status !== "verified") return finish(this.error(input, "home_limit", "wasm_home_limit_unavailable"), "failed");
|
||||
const home = this.wasmHomeLimit.home(input.seq);
|
||||
const home = this.wasmHomeLimit.home(input.seq, input.payload?.all ? null : input.payload?.axis ?? null);
|
||||
if (home.status === "error") return finish(this.error(input, "home_limit", home.code), "failed");
|
||||
this.task.setMode("MANUAL");
|
||||
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, motion: this.wasmMotionProjection(), wasm_home_limit: this.wasmHomeLimit.read(), motion_authority: "wasm", operation_id: input.operation_id ?? null }), "verified");
|
||||
@@ -2109,7 +2144,7 @@ export class SimSession {
|
||||
}
|
||||
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, motion: this.motionProjection(), task: this.task.export(), command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
|
||||
}
|
||||
case "JOG_START": case "JOG_STOP": case "SET_OVERRIDE": case "FOLLOWING_ERROR": {
|
||||
case "JOG_START": case "JOG_STOP": case "SET_JOG_SCALE": case "SET_MAX_VELOCITY": case "SET_OVERRIDE": case "FOLLOWING_ERROR": {
|
||||
if (this.motionAuthority === "wasm") {
|
||||
if (input.kind === "JOG_STOP") {
|
||||
this.wasmMotion.abort();
|
||||
@@ -2127,23 +2162,28 @@ export class SimSession {
|
||||
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 === "SET_JOG_SCALE" || input.kind === "SET_MAX_VELOCITY") {
|
||||
const scale = Number(input.payload?.scale ?? Number(input.payload?.percent) / 100);
|
||||
if (!Number.isFinite(scale) || scale < 0.06 || scale > 1) return finish(this.error(input, "motion_scale", input.kind === "SET_JOG_SCALE" ? "jog_scale_range" : "max_velocity_scale_range"), "failed");
|
||||
try {
|
||||
if (input.kind === "SET_JOG_SCALE") this.wasmMotion.setJogScale(scale);
|
||||
else this.wasmMotion.setVelocityScale(scale);
|
||||
} catch (error) {
|
||||
return finish(this.error(input, "motion_scale", String(error?.message ?? error)), "failed");
|
||||
}
|
||||
const motion = this.wasmMotionProjection();
|
||||
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, 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();
|
||||
if (current.status !== "verified") return finish(this.error(input, "motion_queue", "wasm_motion_unavailable"), "failed");
|
||||
const axis = String(input.payload?.axis ?? "").toLowerCase();
|
||||
if (!["x", "y", "z", "a", "c"].includes(axis)) return finish(this.error(input, "motion_axis", "motion_axis_invalid"), "failed");
|
||||
const direction = input.payload?.direction === -1 ? -1 : input.payload?.direction === 1 ? 1 : null;
|
||||
if (direction === null) return finish(this.error(input, "motion_jog", "motion_jog_range"), "failed");
|
||||
this.wasmMotion.setPaused(false);
|
||||
const start = { ...current.state.position };
|
||||
const distance = Number(input.payload?.increment) > 0 ? Number(input.payload.increment) : 1;
|
||||
const target = Number(start[axis] ?? 0) + direction * distance;
|
||||
const configuredLimit = this.wasmHomeLimit.limits?.[axis];
|
||||
if (Array.isArray(configuredLimit) && configuredLimit.length === 2 && (target < Number(configuredLimit[0]) || target > Number(configuredLimit[1]))) return finish(this.error(input, "motion_queue", "motion_limit_range"), "failed");
|
||||
const end = { ...start, [axis]: target };
|
||||
const enqueue = this.wasmMotion.enqueue({ start, end, center: { x: 0, y: 0, z: 0 }, normal: { x: 0, y: 0, z: 1 }, velocity: Number(input.payload?.speed ?? 1), acceleration: 0, motion_type: 1, turn: 0, source_line: null });
|
||||
if (enqueue.status === "error") {
|
||||
const code = enqueue.code === "wasm_motion_enqueue_-602" ? "motion_limit_range" : enqueue.code === "wasm_motion_enqueue_-601" ? "motion_jog_nonfinite" : enqueue.code;
|
||||
const distance = input.payload?.increment ?? 1;
|
||||
const jog = this.wasmMotion.jog({ x: 0, y: 1, z: 2, a: 3, c: 5 }[axis], direction, distance, input.seq);
|
||||
if (jog.status !== "verified") {
|
||||
const code = jog.code === "linuxcnc_tp_jog_-1217" ? "motion_limit_range" : jog.code === "linuxcnc_tp_jog_-1215" ? "motion_jog_nonfinite" : jog.code;
|
||||
return finish(this.error(input, "motion_queue", code), "failed");
|
||||
}
|
||||
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, motion: this.wasmMotionProjection(), wasm_motion: this.wasmMotion.read(), motion_authority: "wasm", operation_id: input.operation_id ?? null }), "verified");
|
||||
@@ -2157,7 +2197,7 @@ export class SimSession {
|
||||
if (result.status === "error" && result.state?.fault && this.task.state.state === "RUNNING") this.task.fault(-1205);
|
||||
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, motion: this.motionProjection(), task: this.task.export(), command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
|
||||
}
|
||||
case "SPINDLE_COMMAND": case "SPINDLE_BRAKE": case "SPINDLE_FAULT": case "SPINDLE_INHIBIT":
|
||||
case "SET_SPINDLE_OVERRIDE": case "SPINDLE_COMMAND": case "SPINDLE_BRAKE": case "SPINDLE_FAULT": case "SPINDLE_INHIBIT":
|
||||
case "SPINDLE_ORIENT": case "SPINDLE_ORIENTED": case "IO_STEP":
|
||||
case "TOOL_PREPARE": case "TOOL_PREPARED": case "TOOL_CHANGE":
|
||||
case "TOOL_CHANGED": case "TOOL_ABORT": case "TOOL_FAULT": case "TOOL_CLEAR_FAULT":
|
||||
|
||||
@@ -5,13 +5,15 @@ assert.deepEqual(AXIS_STATES, ["ESTOP", "ESTOP_RESET", "OFF", "ON", "HOME", "IDL
|
||||
assert.deepEqual(LINUXCNC_STATE_CONSTANTS, { STATE_ESTOP: 1, STATE_ESTOP_RESET: 2, STATE_OFF: 3, STATE_ON: 4, MODE_MANUAL: 1, MODE_AUTO: 2, MODE_MDI: 3, INTERP_IDLE: 1, INTERP_READING: 2, INTERP_PAUSED: 3, INTERP_WAITING: 4 });
|
||||
assert.equal(axisState({ task: { state: "NEW" }, action: "SESSION_ATTACH" }).state, "OFF");
|
||||
assert.deepEqual(axisState({ task: { state: "ESTOP" }, action: "ESTOP", payload: { active: true } }).state, "ESTOP");
|
||||
assert.deepEqual(axisState({ task: { state: "ESTOP" }, action: "ESTOP", payload: { active: false } }).state, "ESTOP_RESET");
|
||||
assert.equal(axisState({ task: { state: "READY" }, action: "HOME" }).state, "HOME");
|
||||
assert.deepEqual(axisState({ task: { state: "ESTOP" }, io: { state: { estop: true, ready: false } }, action: "ESTOP", payload: { active: false } }).state, "ESTOP");
|
||||
assert.equal(axisState({ task: { state: "READY" }, io: { state: { estop: false, ready: true } }, action: "HOME" }).state, "IDLE");
|
||||
assert.equal(axisState({ task: { state: "RUNNING" }, program: { program_status: "running" }, action: "SIM_STEP" }).state, "RUNNING");
|
||||
assert.equal(axisState({ task: { state: "PAUSED" }, program: { program_status: "paused" }, action: "PROGRAM_PAUSE" }).state, "PAUSED");
|
||||
assert.equal(axisState({ task: { state: "READY" }, program: { program_status: "idle" }, action: "IO_RECOVER" }).state, "IDLE");
|
||||
assert.equal(axisState({ task: { state: "RUNNING", mode: "AUTO" }, program: { program_status: "running" } }).source_interp_state, "INTERP_READING");
|
||||
assert.equal(axisState({ task: { state: "PAUSED", mode: "AUTO" }, program: { program_status: "paused" } }).source_interp_state, "INTERP_PAUSED");
|
||||
assert.equal(axisState({ task: { state: "STOPPED" }, io: { state: { estop: false, ready: false, last_error: 0 } } }).machine_state, "OFF");
|
||||
assert.equal(axisState({ task: { state: "STOPPED" }, io: { state: { estop: false, ready: false, last_error: -904 } } }).machine_state, "ESTOP_RESET");
|
||||
assert.equal(validateAxisTransition("ESTOP", "ESTOP_RESET").status, "accepted");
|
||||
assert.equal(validateAxisTransition("ESTOP_RESET", "ON").status, "accepted");
|
||||
assert.equal(validateAxisTransition("ON", "RUNNING").status, "accepted");
|
||||
|
||||
@@ -49,7 +49,9 @@ let executionTraceEntries = 0;
|
||||
// The first long Z feed is planned at the configured LinuxCNC TP limit. Give
|
||||
// the fixed-tick simulation enough servo time to complete it before checking
|
||||
// that the interpreter has advanced beyond the historical line-82 boundary.
|
||||
for (let cycle = 0; cycle < 1500; cycle += 1) {
|
||||
const cyclesToRun = Number(process.env.XYZAC_CHIPS_CYCLES ?? 3000);
|
||||
assert.ok(Number.isSafeInteger(cyclesToRun) && cyclesToRun > 0);
|
||||
for (let cycle = 0; cycle < cyclesToRun; cycle += 1) {
|
||||
stepped = send("SIM_STEP", { cycles: 1, response: "motion_delta" });
|
||||
assert.equal(stepped.kind, "COMMAND_RESULT");
|
||||
executionTraceEntries += stepped.payload.execution_trace.length;
|
||||
@@ -61,6 +63,8 @@ assert.ok(executionTraceEntries > 0);
|
||||
assert.ok(stepped.payload.program.execution_line_text.length > 0);
|
||||
assert.equal(stepped.payload.motion_authority, "wasm");
|
||||
assert.equal(Object.hasOwn(stepped.payload.xyzac_ledger.entries.at(-1), "response"), false);
|
||||
assert.equal(session.timeline.snapshots.snapshots.size, 0);
|
||||
assert.equal(session.ledger.audit({ maxEntries: 32 }).entries.length, 32);
|
||||
|
||||
console.log(JSON.stringify({
|
||||
schema: "xyzac-3d-chips-program.v1",
|
||||
@@ -70,7 +74,10 @@ console.log(JSON.stringify({
|
||||
source_hash: loaded.payload.program.source_hash,
|
||||
line_count: loaded.payload.program.line_count,
|
||||
preview_segments: loaded.payload.preview.segments.length,
|
||||
cycles: cyclesToRun,
|
||||
execution_line: stepped.payload.program.current_line,
|
||||
execution_trace: executionTraceEntries,
|
||||
motion_authority: stepped.payload.motion_authority,
|
||||
motion_delta_snapshots: session.timeline.snapshots.snapshots.size,
|
||||
ledger_audit_window: session.ledger.audit({ maxEntries: 32 }).entries.length,
|
||||
}));
|
||||
|
||||
@@ -19,7 +19,7 @@ const send = (kind, payload = {}) => session.accept({ ...base, kind, seq: ++seq,
|
||||
send("HELLO");
|
||||
send("SESSION_ATTACH");
|
||||
const loaded = send("LOAD_WORKSPACE", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) });
|
||||
assert.equal(loaded.payload.panel_snapshot.axis_state.machine_state, "ESTOP_RESET");
|
||||
assert.equal(loaded.payload.panel_snapshot.axis_state.machine_state, "OFF");
|
||||
assert.equal(send("PROGRAM_LOAD", { source: PROGRAM, path: "button.ngc" }).payload.status, "verified");
|
||||
assert.equal(send("RESET").payload.gate_reason, "xyzac_machine_off");
|
||||
const restarted = send("PROGRAM_RESTART");
|
||||
@@ -34,7 +34,15 @@ for (const [kind, payload, reason] of [["PROGRAM_RUN", {}, "xyzac_machine_off"],
|
||||
const powered = send("POWER", { active: true });
|
||||
assert.equal(powered.payload.panel_snapshot.axis_state.machine_state, "ON");
|
||||
assert.equal(send("HOME", { all: true }).payload.wasm_home_limit.state.homed, true);
|
||||
const readyGates = send("SNAPSHOT_REQUEST").payload.panel_snapshot.gates;
|
||||
assert.equal(readyGates.spindle.enabled, true);
|
||||
assert.equal(readyGates.coolant.enabled, true);
|
||||
assert.equal(readyGates.tool.enabled, true);
|
||||
assert.equal(readyGates.probe.enabled, true);
|
||||
assert.equal(send("KINEMATICS_SELECT", { type: 1 }).payload.kinematics.active_type, 1);
|
||||
const alternateKinematicsGates = send("SNAPSHOT_REQUEST").payload.panel_snapshot.gates;
|
||||
assert.equal(alternateKinematicsGates.spindle.reason, "linuxcnc_wasm_mode_unavailable");
|
||||
assert.equal(alternateKinematicsGates.tool.reason, "linuxcnc_wasm_mode_unavailable");
|
||||
assert.equal(send("RESET").payload.command, "RESET");
|
||||
assert.equal(send("MDI_EXECUTE", { block: "G0 X1" }).payload.task.mode, "MDI");
|
||||
assert.equal(send("JOG_STOP").payload.command, "JOG_STOP");
|
||||
|
||||
@@ -28,6 +28,15 @@ assert.equal(send("LOAD_WORKSPACE", { root_path: "/workspace", configuration: to
|
||||
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");
|
||||
const jogScale = send("SET_JOG_SCALE", { percent: 50 });
|
||||
assert.equal(jogScale.payload.status, "verified");
|
||||
assert.equal(jogScale.payload.motion.jog_scale, 0.5);
|
||||
const maxVelocity = send("SET_MAX_VELOCITY", { percent: 50 });
|
||||
assert.equal(maxVelocity.payload.status, "verified");
|
||||
assert.equal(maxVelocity.payload.motion.velocity_scale, 0.5);
|
||||
const spindleOverride = send("SET_SPINDLE_OVERRIDE", { spindle: 0, scale: 0.5 });
|
||||
assert.equal(spindleOverride.payload.status, "verified");
|
||||
assert.equal(spindleOverride.payload.device_io.state.spindles[0].override, 0.5);
|
||||
assert.equal(send("SET_OVERRIDE", { feed: 0.5 }).payload.status, "verified");
|
||||
const half = send("PROGRAM_RUN");
|
||||
assert.equal(half.payload.status, "verified");
|
||||
@@ -35,14 +44,14 @@ 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));
|
||||
assert.ok(halfStep.payload.wasm_motion.state.velocity > 0, 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));
|
||||
assert.ok(fullStep.payload.wasm_motion.state.velocity > halfStep.payload.wasm_motion.state.velocity, JSON.stringify(fullStep.payload.wasm_motion.state));
|
||||
|
||||
const invalid = send("SET_OVERRIDE", { feed: 2.01 });
|
||||
assert.equal(invalid.kind, "ERROR");
|
||||
@@ -57,9 +66,12 @@ assert.equal(session.wasmMotion.controls.feed_override, 2);
|
||||
console.log(JSON.stringify({
|
||||
schema: "xyzac-feed-override.v1",
|
||||
status: "verified",
|
||||
authority: "linuxcnc-wasm-motion-queue",
|
||||
authority: "linuxcnc-source-tp-and-wasm-io",
|
||||
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,
|
||||
jog_scale: jogScale.payload.motion.jog_scale,
|
||||
max_velocity_scale: maxVelocity.payload.motion.velocity_scale,
|
||||
spindle_override: spindleOverride.payload.device_io.state.spindles[0].override,
|
||||
}));
|
||||
|
||||
@@ -74,7 +74,7 @@ assert.equal(running.payload.program.source, "linuxcnc-wasm-sim_program");
|
||||
assert.equal(running.payload.motion_authority, "wasm");
|
||||
assert.equal(session.wasmMotion.exportState().controls.paused, false);
|
||||
|
||||
const stepped = send("SIM_STEP", { cycles: 180 });
|
||||
const stepped = send("SIM_STEP", { cycles: 1000 });
|
||||
assert.equal(stepped.payload.status, "verified");
|
||||
assert.equal(stepped.payload.motion_authority, "wasm");
|
||||
assert.ok(stepped.payload.program.current_line > 0);
|
||||
|
||||
@@ -9,7 +9,7 @@ const fetchImpl = async (url) => { try { const body = await readFile(new URL(`..
|
||||
const bundle = await loadSimulationCase({ path: CASE, fetchImpl });
|
||||
const core = await createModule({ wasmBinary: await readFile(new URL("../../build-wasm/linuxcnc-sim-core.wasm", import.meta.url)) });
|
||||
|
||||
function setup(id) {
|
||||
function setup(id, { homeAll = true } = {}) {
|
||||
const session = new XyzacSession(core, { sessionId: id });
|
||||
const base = { schema_version: 1, context_id: CONTEXT_ID, session_id: id };
|
||||
let seq = 0;
|
||||
@@ -17,10 +17,17 @@ function setup(id) {
|
||||
send("HELLO", "hello"); send("SESSION_ATTACH", "attach");
|
||||
assert.equal(send("LOAD_WORKSPACE", "workspace", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) }).payload.status, "verified");
|
||||
assert.equal(send("POWER", "power-on", { active: true }).payload.status, "verified");
|
||||
assert.equal(send("HOME", "home-all", { all: true }).payload.wasm_home_limit.state.homed, true);
|
||||
if (homeAll) assert.equal(send("HOME", "home-all", { all: true }).payload.wasm_home_limit.state.homed, true);
|
||||
return { session, send };
|
||||
}
|
||||
|
||||
const partialHome = setup("xyzac-manual-partial-home", { homeAll: false });
|
||||
for (const [index, axis] of ["x", "y", "z", "a", "c"].entries()) {
|
||||
const response = partialHome.send("HOME", `home-${axis}`, { axis });
|
||||
assert.equal(response.payload.wasm_home_limit.state.homed_mask, (1 << (index + 1)) - 1);
|
||||
assert.equal(response.payload.wasm_home_limit.state.homed, index === 4);
|
||||
}
|
||||
|
||||
const jogCase = setup("xyzac-manual-jog");
|
||||
const home = jogCase.send("HOME", "home-all");
|
||||
assert.equal(home.payload.motion_authority, "wasm");
|
||||
@@ -45,7 +52,7 @@ for (const axis of axes) {
|
||||
const outOfRange = jogCase.send("JOG_START", "jog-limit", { axis: "x", direction: 1, speed: 1, increment: 10000 });
|
||||
assert.equal(outOfRange.kind, "ERROR");
|
||||
assert.equal(outOfRange.payload.message_key, "motion_limit_range");
|
||||
const nonFinite = jogCase.send("JOG_START", "jog-nonfinite", { axis: "a", direction: 1, speed: "not-a-number" });
|
||||
const nonFinite = jogCase.send("JOG_START", "jog-nonfinite", { axis: "a", direction: 1, increment: "not-a-number" });
|
||||
assert.equal(nonFinite.kind, "ERROR");
|
||||
assert.equal(nonFinite.payload.message_key, "motion_jog_nonfinite");
|
||||
const ferror = jogCase.send("FOLLOWING_ERROR", "ferror", { value: 1 });
|
||||
|
||||
@@ -62,6 +62,10 @@ for (const [button, type] of [["Identity", 0], ["TCP: XYZAC", 1], ["User K", 2],
|
||||
}
|
||||
record("view", "Clear plot", "VISUAL_CLEAR", "axis.py:clear_live_plot / glcanon.py", machine.send("VISUAL_CLEAR"));
|
||||
record("override", "Feed 50%", "SET_OVERRIDE", "axis.py:set_feedrate -> c.feedrate", machine.send("SET_OVERRIDE", { feed: 0.5 }));
|
||||
record("override", "Jog Speed 50%", "SET_JOG_SCALE", "axis.py:update_jog_slider_vel -> LinuxCNC TP jog scale", machine.send("SET_JOG_SCALE", { percent: 50 }));
|
||||
assert.equal(machine.send("SNAPSHOT_REQUEST").payload.panel_snapshot.motion.jog_scale, 0.5);
|
||||
record("override", "Max Velocity 50%", "SET_MAX_VELOCITY", "axis.py:update_maxvel_slider_vel -> tpSetVlimit", machine.send("SET_MAX_VELOCITY", { percent: 50 }));
|
||||
assert.equal(machine.send("SNAPSHOT_REQUEST").payload.panel_snapshot.motion.velocity_scale, 0.5);
|
||||
const invalidOverride = machine.send("SET_OVERRIDE", { feed: 2.01 });
|
||||
record("override", "Feed 201% rejected", "SET_OVERRIDE", "axis.py:set_feedrate range", invalidOverride, "error");
|
||||
assert.equal(invalidOverride.payload.message_key, "motion_override_range");
|
||||
@@ -114,6 +118,8 @@ rows.push({ group: "spindle", button: "Speed +", command: "SPINDLE_COMMAND(delta
|
||||
assert.equal(io.send("SPINDLE_COMMAND", { spindle: 0, delta: -100 }).payload.device_io.state.spindles[0].command_rpm, 600);
|
||||
rows.push({ group: "spindle", button: "Speed -", command: "SPINDLE_COMMAND(delta)", source: "axis.py:spindle_decrease -> EMCMOT_SPINDLE_DECREASE", result: "verified", reason: null });
|
||||
record("spindle", "Stop", "SPINDLE_COMMAND", "axis.py:spindle -> SPINDLE_OFF", io.send("SPINDLE_COMMAND", { spindle: 0, rpm: 0, enabled: false }));
|
||||
record("spindle", "Override 50%", "SET_SPINDLE_OVERRIDE", "axis.py:set_spindlerate -> EMC_TRAJ_SET_SPINDLE_SCALE", io.send("SET_SPINDLE_OVERRIDE", { spindle: 0, scale: 0.5 }));
|
||||
assert.equal(io.send("SNAPSHOT_REQUEST").payload.panel_snapshot.io.state.spindles[0].override, 0.5);
|
||||
record("spindle", "Brake engage", "SPINDLE_BRAKE", "axis.py:brake -> EMC_SPINDLE_BRAKE_ENGAGE", io.send("SPINDLE_BRAKE", { spindle: 0, engaged: true }));
|
||||
record("spindle", "Brake release", "SPINDLE_BRAKE", "axis.py:brake -> EMC_SPINDLE_BRAKE_RELEASE", io.send("SPINDLE_BRAKE", { spindle: 0, engaged: false }));
|
||||
record("coolant", "Mist/Flood on", "COOLANT_COMMAND", "axis.py:mist_toggle/flood_toggle", io.send("COOLANT_COMMAND", { mist: true, flood: true }));
|
||||
@@ -129,10 +135,7 @@ record("machine", "Release E-stop", "ESTOP", "axis.py:ESTOP reset", io.send("EST
|
||||
record("machine", "Power after ESTOP", "POWER", "axis.py:commands.power", io.send("POWER", { active: true }));
|
||||
|
||||
const unsupported = [
|
||||
{ button: "Home Axis", reason: "WASM home state exposes aggregate homed only; no per-joint home mask" },
|
||||
{ button: "Override Limits", reason: "selected XYZAC WASM configuration owns limits; no LinuxCNC override_limits command ABI" },
|
||||
{ button: "Jog Speed slider", reason: "UI state is not connected to a WASM jog-speed command" },
|
||||
{ button: "Max Velocity slider", reason: "UI state is not connected to a WASM max-velocity command" },
|
||||
{ button: "Block delete / Optional pause", reason: "UI toggle is not forwarded to sim_program interpreter state" },
|
||||
{ button: "Tool Touch Off", reason: "UI emits fixed G43 and does not expose LinuxCNC G10 prompt/value flow" },
|
||||
{ button: "Edit tool data / Quit", reason: "desktop process boundary; browser intentionally host-excluded" },
|
||||
|
||||
@@ -69,10 +69,19 @@ assert.deepEqual(controller.exportTrace().map((item) => item.phase), ["pending",
|
||||
const appSource = await readFile(new URL("../react/src/App.tsx", import.meta.url), "utf8");
|
||||
const panelSource = await readFile(new URL("../react/src/XyzacTrtPanel.tsx", import.meta.url), "utf8");
|
||||
const contractSource = await readFile(new URL("../react/src/xyzac-panel-contract.ts", import.meta.url), "utf8");
|
||||
const sessionSource = await readFile(new URL("./sim-session.mjs", import.meta.url), "utf8");
|
||||
const tpSource = await readFile(new URL("./wasm-linuxcnc-tp.mjs", import.meta.url), "utf8");
|
||||
const combined = `${appSource}\n${panelSource}\n${contractSource}`;
|
||||
for (const symbol of ["XyzacTrtPanel", "XyzacProgramPane", "XyzacKinematicsControl", "XyzacManualPanel", "XyzacDiagnostics", "KINEMATICS_SELECT", "VISUAL_CLEAR", "POWER", "switchkinsXyzac", "Operation trace", "Alarms / recovery"]) assert.ok(combined.includes(symbol), symbol);
|
||||
for (const symbol of ["wasmSnapshotReady", "manualReady", "sourceJogScale", "sourceVelocityScale", "spindleOverrideSource", "SET_JOG_SCALE", "SET_MAX_VELOCITY", "SET_SPINDLE_OVERRIDE", "WASM PENDING", "LinuxCNC WASM override-limits command unavailable"]) assert.ok(appSource.includes(symbol), symbol);
|
||||
for (const forbidden of [/defaultValue=\"50\"/, /defaultValue=\"100\"/, /setBlockDelete/, /setOptionalStop/, /state\.status === \"running\" \? \"READING\"/]) assert.equal(forbidden.test(appSource), false, String(forbidden));
|
||||
for (const forbidden of [/_sim_/, /_malloc/, /\bHEAP(?:U|F|I)?\d*\b/, /navigator\.storage/, /\bOPFS\b/i, /postMessage\s*\(/]) assert.equal(forbidden.test(combined), false, String(forbidden));
|
||||
assert.ok(appSource.includes("SELECTED_XYZAC_CASE"));
|
||||
assert.equal(appSource.includes("3axis-tutorial"), false);
|
||||
assert.ok(sessionSource.includes("this.wasmMotion.jog("));
|
||||
assert.equal(/const\s+target\s*=\s*Number\(start\[axis\]/.test(sessionSource), false);
|
||||
assert.equal(/wasmMotion\.enqueue\(\{\s*start,\s*end/.test(sessionSource), false);
|
||||
assert.equal(tpSource.includes("cubicInterpolate"), false);
|
||||
assert.ok(tpSource.includes("sim_linuxcnc_tp_jog"));
|
||||
|
||||
console.log(JSON.stringify({ schema: "xyzac-protocol-react-rtcp-09.v1", status: "verified", case: CASE, commands: [power.payload.command, "KINEMATICS_SELECT", "VISUAL_CLEAR"], switchkins: switches, gates: { invalid: invalid.payload.gate_reason, running: runningSwitch.payload.gate_reason, controller: controller.exportTrace().map((item) => item.phase) }, components: ["XyzacTrtPanel", "XyzacProgramPane", "XyzacKinematicsControl", "XyzacManualPanel", "XyzacDiagnostics"], worker_only: true, state_hash: clear.payload.panel_snapshot.state_hash }));
|
||||
|
||||
@@ -5,11 +5,11 @@ const KIN_TYPE_TRIVKINS = 1;
|
||||
const KIN_TYPE_XYZAC_TRT = 2;
|
||||
const KIN_CONFIG_SIZE = 1608;
|
||||
const KIN_SIZE = 1760;
|
||||
const HOME_LIMIT_SIZE = 184;
|
||||
const HOME_STATE_SIZE = 176;
|
||||
const HOME_LIMIT_SIZE = 192;
|
||||
const HOME_STATE_SIZE = 184;
|
||||
const HOME_STATE_OFFSET = 8;
|
||||
const CONFIG_OFFSETS = Object.freeze({ type: 0, mode: 4, joint_count: 8, kinematics_joint_count: 12, extra_joint_count: 16, coordinates: 20, x_rot_point: 40, y_offset: 72, z_offset: 80, tool_offset: 88, limit_enabled: 1448, joint_min: 1464, joint_max: 1536 });
|
||||
const HOME_STATE_OFFSETS = Object.freeze({ pose: 0, joints: 72, joint_count: 144, homed: 148, home_sequence: 152, tick: 156, last_error: 160, limit_error: 164, kinematics_error: 168 });
|
||||
const HOME_STATE_OFFSETS = Object.freeze({ pose: 0, joints: 72, joint_count: 144, homed: 148, home_sequence: 152, tick: 156, last_error: 160, limit_error: 164, kinematics_error: 168, homed_mask: 172 });
|
||||
|
||||
function call(core, name, returnType, argTypes, args) {
|
||||
const direct = core?.[`_${name}`] ?? core?.[name];
|
||||
@@ -43,7 +43,7 @@ function finitePose(value) {
|
||||
export class WasmHomeLimit {
|
||||
constructor(core, { jointCount = 3, limits = {}, kinematicsType = KIN_TYPE_TRIVKINS, coordinates = "XYZ", offsets = {} } = {}) {
|
||||
this.core = core;
|
||||
this.available = ["sim_kinematics_init", "sim_kinematics_home_limit_init", "sim_kinematics_home_limit_reset", "sim_kinematics_home_limit_home", "sim_kinematics_home_limit_check", "sim_kinematics_home_limit_read"].every((name) => typeof core?.[`_${name}`] === "function" || typeof core?.[name] === "function");
|
||||
this.available = ["sim_kinematics_init", "sim_kinematics_home_limit_init", "sim_kinematics_home_limit_reset", "sim_kinematics_home_limit_home", "sim_kinematics_home_limit_home_joint", "sim_kinematics_home_limit_check", "sim_kinematics_home_limit_read"].every((name) => typeof core?.[`_${name}`] === "function" || typeof core?.[name] === "function");
|
||||
this.jointCount = jointCount;
|
||||
this.kinematicsType = kinematicsType;
|
||||
this.coordinates = String(coordinates).toUpperCase();
|
||||
@@ -93,12 +93,16 @@ export class WasmHomeLimit {
|
||||
const ints = this.core.HEAPU32;
|
||||
const signed = this.core.HEAP32;
|
||||
const joints = [...this.core.HEAPF64.subarray(this.stateBuffer / 8 + 9, this.stateBuffer / 8 + 9 + this.jointCount)];
|
||||
return { status: "verified", state: { schema: HOME_LIMIT_SCHEMA, source: "wasm_home_limit", pose: pose(this.core, this.stateBuffer), joints, joint_count: ints[this.stateBuffer / 4 + 36], homed: Boolean(ints[this.stateBuffer / 4 + 37]), home_sequence: ints[this.stateBuffer / 4 + 38], tick: ints[this.stateBuffer / 4 + 39], last_error: signed[this.stateBuffer / 4 + 40], limit_error: signed[this.stateBuffer / 4 + 41], kinematics_error: signed[this.stateBuffer / 4 + 42] } };
|
||||
return { status: "verified", state: { schema: HOME_LIMIT_SCHEMA, source: "wasm_home_limit", pose: pose(this.core, this.stateBuffer), joints, joint_count: ints[this.stateBuffer / 4 + 36], homed: Boolean(ints[this.stateBuffer / 4 + 37]), home_sequence: ints[this.stateBuffer / 4 + 38], tick: ints[this.stateBuffer / 4 + 39], last_error: signed[this.stateBuffer / 4 + 40], limit_error: signed[this.stateBuffer / 4 + 41], kinematics_error: signed[this.stateBuffer / 4 + 42], homed_mask: ints[this.stateBuffer / 4 + 43] } };
|
||||
}
|
||||
|
||||
home(sequence = 0) {
|
||||
home(sequence = 0, axis = null) {
|
||||
if (!this.available) return this.read();
|
||||
const result = call(this.core, "sim_kinematics_home_limit_home", "number", ["number", "number"], [this.homeLimit, sequence]);
|
||||
const index = axis === null || axis === undefined ? -1 : [...this.coordinates].findIndex((value) => value.toLowerCase() === String(axis).toLowerCase());
|
||||
if (axis !== null && index < 0) return { status: "error", code: "wasm_home_axis_invalid", state: this.read().state };
|
||||
const result = index < 0
|
||||
? call(this.core, "sim_kinematics_home_limit_home", "number", ["number", "number"], [this.homeLimit, sequence])
|
||||
: call(this.core, "sim_kinematics_home_limit_home_joint", "number", ["number", "number", "number"], [this.homeLimit, sequence, index]);
|
||||
if (result !== 0) return { status: "error", code: `wasm_home_${result}`, state: this.read().state };
|
||||
return this.read();
|
||||
}
|
||||
|
||||
@@ -14,32 +14,10 @@ function call(core, name, returnType, argTypes, args) {
|
||||
function poseArray(value = {}) { return ["x", "y", "z", "a", "b", "c", "u", "v", "w"].map((axis) => Number(value[axis] ?? 0)); }
|
||||
function toPose(values) { return Object.fromEntries(["x", "y", "z", "a", "b", "c", "u", "v", "w"].map((axis, index) => [axis, Number(values[index] ?? 0)])); }
|
||||
|
||||
function cubicState() { return { x0: 0, x1: 0, x2: 0, x3: 0, t: 0, need: true, coeff: [0, 0, 0, 0] }; }
|
||||
function cubicAdd(state, point, segmentTime = 0.001) {
|
||||
if (!state.need) return;
|
||||
state.x0 = state.x1; state.x1 = state.x2; state.x2 = state.x3; state.x3 = Number(point);
|
||||
const wp0 = (state.x0 + 4 * state.x1 + state.x2) / 6;
|
||||
const wp1 = (state.x1 + 4 * state.x2 + state.x3) / 6;
|
||||
const vel0 = (state.x2 - state.x0) / (2 * segmentTime);
|
||||
const vel1 = (state.x3 - state.x1) / (2 * segmentTime);
|
||||
const b = 3 * (wp1 - wp0) / (segmentTime * segmentTime) - (2 * vel0 + vel1) / segmentTime;
|
||||
const a = (vel1 - vel0) / (3 * segmentTime * segmentTime) - 2 * b / (3 * segmentTime);
|
||||
state.coeff = [a, b, vel0, wp0]; state.t = 0; state.need = false;
|
||||
}
|
||||
function cubicInterpolate(state, segmentTime = 0.001) {
|
||||
if (state.need) cubicAdd(state, state.x3, segmentTime);
|
||||
const [a, b, c, d] = state.coeff;
|
||||
const t = state.t;
|
||||
const value = ((a * t + b) * t + c) * t + d;
|
||||
state.t += segmentTime;
|
||||
if (Math.abs(segmentTime - state.t) < segmentTime * 0.5) state.need = true;
|
||||
return value;
|
||||
}
|
||||
|
||||
export class WasmLinuxcncTp {
|
||||
constructor(core, { cycleTime = 0.001, velocityLimit = 35, accelerationLimit = 300 } = {}) {
|
||||
constructor(core, { cycleTime = 0.001, velocityLimit = 35, accelerationLimit = 300, limits = {} } = {}) {
|
||||
this.core = core;
|
||||
this.available = ["sim_linuxcnc_tp_size", "sim_linuxcnc_tp_init", "sim_linuxcnc_tp_read_state", "sim_linuxcnc_tp_step", "sim_linuxcnc_tp_idle", "sim_linuxcnc_tp_add_line", "sim_linuxcnc_tp_set_profile", "sim_linuxcnc_tp_set_feed_override", "sim_linuxcnc_tp_set_dynamics", "sim_linuxcnc_tp_set_paused", "sim_linuxcnc_tp_abort", "sim_linuxcnc_tp_set_goal_position", "sim_linuxcnc_tp_queue_depth", "sim_linuxcnc_tp_queue_segment", "sim_linuxcnc_tp_restore_progress"].every((name) => typeof core?.[`_${name}`] === "function" || typeof core?.[name] === "function");
|
||||
this.available = ["sim_linuxcnc_tp_size", "sim_linuxcnc_tp_init", "sim_linuxcnc_tp_read_state", "sim_linuxcnc_tp_step", "sim_linuxcnc_tp_idle", "sim_linuxcnc_tp_add_line", "sim_linuxcnc_tp_set_profile", "sim_linuxcnc_tp_set_feed_override", "sim_linuxcnc_tp_set_velocity_scale", "sim_linuxcnc_tp_set_jog_scale", "sim_linuxcnc_tp_set_axis_limit", "sim_linuxcnc_tp_jog", "sim_linuxcnc_tp_velocity_scale", "sim_linuxcnc_tp_velocity_limit", "sim_linuxcnc_tp_jog_scale", "sim_linuxcnc_tp_jog_velocity", "sim_linuxcnc_tp_feed_override", "sim_linuxcnc_tp_set_dynamics", "sim_linuxcnc_tp_set_paused", "sim_linuxcnc_tp_abort", "sim_linuxcnc_tp_set_goal_position", "sim_linuxcnc_tp_queue_depth", "sim_linuxcnc_tp_queue_segment", "sim_linuxcnc_tp_restore_progress"].every((name) => typeof core?.[`_${name}`] === "function" || typeof core?.[name] === "function");
|
||||
this.cycleTime = cycleTime;
|
||||
this.velocityLimit = velocityLimit;
|
||||
this.accelerationLimit = accelerationLimit;
|
||||
@@ -49,7 +27,6 @@ export class WasmLinuxcncTp {
|
||||
this.centerPointer = 0;
|
||||
this.normalPointer = 0;
|
||||
this.statePointer = 0;
|
||||
this.history = [];
|
||||
this.activeTag = 0;
|
||||
this.segmentTags = new Map();
|
||||
this.lastActiveId = 0;
|
||||
@@ -60,9 +37,7 @@ export class WasmLinuxcncTp {
|
||||
this.lastState = null;
|
||||
this.queueDepthOverride = null;
|
||||
this.kinematics = null;
|
||||
this.cubic = Array.from({ length: 5 }, () => cubicState());
|
||||
this.previousJointCommanded = [0, 0, 0, 0, 0];
|
||||
this.controls = { profile: 0, lookahead: 50, acceleration_limit: accelerationLimit, jerk_limit: 1e9, feed_override: 1, paused: false };
|
||||
this.controls = { profile: 0, lookahead: 50, acceleration_limit: accelerationLimit, jerk_limit: 1e9, feed_override: 1, velocity_scale: 1, jog_scale: 1, paused: false };
|
||||
if (!this.available || typeof core?._malloc !== "function") return;
|
||||
this.planner = core._malloc(call(core, "sim_linuxcnc_tp_size", "number", [], []));
|
||||
this.posePointer = core._malloc(POSE_BYTES);
|
||||
@@ -74,6 +49,10 @@ export class WasmLinuxcncTp {
|
||||
if (result !== 0) throw new Error(`linuxcnc_tp_init_${result}`);
|
||||
this.setPosition({ x: 0, y: 0, z: 0, a: 0, b: 0, c: 0, u: 0, v: 0, w: 0 });
|
||||
this.setProfile(0);
|
||||
for (const [axis, limit] of Object.entries(limits)) {
|
||||
const index = { x: 0, y: 1, z: 2, a: 3, b: 4, c: 5, u: 6, v: 7, w: 8 }[axis];
|
||||
if (index !== undefined && Array.isArray(limit) && limit.length === 2) this.setAxisLimit(index, Number(limit[0]), Number(limit[1]));
|
||||
}
|
||||
}
|
||||
|
||||
setPosition(value) {
|
||||
@@ -111,6 +90,11 @@ export class WasmLinuxcncTp {
|
||||
setLookahead(value) { if (!Number.isSafeInteger(value) || value < 1) throw new Error("linuxcnc_tp_lookahead_invalid"); this.controls.lookahead = value; return this.read(); }
|
||||
setDynamics(accelerationLimit, jerkLimit) { this.accelerationLimit = Number(accelerationLimit); this.controls.acceleration_limit = this.accelerationLimit; this.controls.jerk_limit = Number(jerkLimit); if (this.available) call(this.core, "sim_linuxcnc_tp_set_dynamics", "number", ["number", "number"], [this.planner, this.accelerationLimit]); return this.read(); }
|
||||
setFeedOverride(value) { this.controls.feed_override = Number(value); if (this.available) call(this.core, "sim_linuxcnc_tp_set_feed_override", "number", ["number", "number"], [this.planner, this.controls.feed_override]); return this.read(); }
|
||||
setVelocityScale(value) { if (!this.available) return this.read(); const result = call(this.core, "sim_linuxcnc_tp_set_velocity_scale", "number", ["number", "number"], [this.planner, Number(value)]); if (result !== 0) throw new Error(`linuxcnc_tp_velocity_scale_${result}`); return this.read(); }
|
||||
setJogScale(value) { if (!this.available) return this.read(); const result = call(this.core, "sim_linuxcnc_tp_set_jog_scale", "number", ["number", "number"], [this.planner, Number(value)]); if (result !== 0) throw new Error(`linuxcnc_tp_jog_scale_${result}`); return this.read(); }
|
||||
setAxisLimit(axis, minimum, maximum) { if (!this.available) return this.read(); const result = call(this.core, "sim_linuxcnc_tp_set_axis_limit", "number", ["number", "number", "number", "number"], [this.planner, Number(axis), Number(minimum), Number(maximum)]); if (result !== 0) throw new Error(`linuxcnc_tp_axis_limit_${result}`); return this.read(); }
|
||||
jog(axis, direction, increment, tag = 0) { if (!this.available) return { status: "blocked", code: "linuxcnc_tp_unavailable" }; const result = call(this.core, "sim_linuxcnc_tp_jog", "number", ["number", "number", "number", "number", "number"], [this.planner, Number(axis), Number(direction), Number(increment), Number(tag)]); return result === 0 ? { status: "verified", command: "JOG_START" } : { status: "error", code: `linuxcnc_tp_jog_${result}` }; }
|
||||
jogVelocity(axis) { if (!this.available) return 0; const index = { x: 0, y: 1, z: 2, a: 3, b: 4, c: 5, u: 6, v: 7, w: 8 }[String(axis).toLowerCase()]; return index === undefined ? 0 : Number(call(this.core, "sim_linuxcnc_tp_jog_velocity", "number", ["number", "number"], [this.planner, index])); }
|
||||
setPaused(value) { this.controls.paused = Boolean(value); if (this.available) call(this.core, "sim_linuxcnc_tp_set_paused", "number", ["number", "number"], [this.planner, this.controls.paused ? 1 : 0]); return this.read(); }
|
||||
|
||||
enqueue(segment) {
|
||||
@@ -158,7 +142,7 @@ export class WasmLinuxcncTp {
|
||||
if (Number(activeTag) === 0) return [position.x, position.y, position.z, position.a ?? 0, position.c ?? 0];
|
||||
const inverse = this.kinematics?.inverseMode?.(Number(activeTag), position) ?? this.kinematics?.inverse?.(position);
|
||||
if (inverse?.status === "verified" && Array.isArray(inverse.joints)) return inverse.joints.slice(0, 5);
|
||||
return this.previousJointCommanded.slice();
|
||||
throw new Error(`linuxcnc_tp_joint_inverse_${inverse?.code ?? "unavailable"}`);
|
||||
}
|
||||
|
||||
step() {
|
||||
@@ -169,13 +153,9 @@ export class WasmLinuxcncTp {
|
||||
const state = this.read();
|
||||
if (state.status === "verified") {
|
||||
const targetJoints = this.commandedJoints(state.state.position, state.state.active_tag);
|
||||
targetJoints.forEach((point, index) => cubicAdd(this.cubic[index], point, this.cycleTime));
|
||||
const commanded = this.cubic.map((axis) => cubicInterpolate(axis, this.cycleTime));
|
||||
state.state.joint_commanded = commanded;
|
||||
state.state.joint_actual = this.previousJointCommanded.slice();
|
||||
this.previousJointCommanded = commanded.slice();
|
||||
state.state.joint_commanded = targetJoints;
|
||||
state.state.joint_actual = targetJoints.slice();
|
||||
}
|
||||
if (state.status === "verified") this.history.push(structuredClone(state.state));
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -186,18 +166,12 @@ export class WasmLinuxcncTp {
|
||||
const state = this.read();
|
||||
if (state.status === "verified") {
|
||||
const targetJoints = this.commandedJoints(state.state.position, state.state.active_tag);
|
||||
targetJoints.forEach((point, index) => cubicAdd(this.cubic[index], point, this.cycleTime));
|
||||
const commanded = this.cubic.map((axis) => cubicInterpolate(axis, this.cycleTime));
|
||||
state.state.joint_commanded = commanded;
|
||||
state.state.joint_actual = this.previousJointCommanded.slice();
|
||||
this.previousJointCommanded = commanded.slice();
|
||||
state.state.joint_commanded = targetJoints;
|
||||
state.state.joint_actual = targetJoints.slice();
|
||||
}
|
||||
if (state.status === "verified") this.history.push(structuredClone(state.state));
|
||||
return state;
|
||||
}
|
||||
|
||||
drainTrace() { const trace = this.history; this.history = []; return trace; }
|
||||
|
||||
read() {
|
||||
if (!this.available) return { status: "blocked", state: { position: toPose([]), velocity: 0, progress: 0, tick: 0, queue_depth: 0, active_id: 0, active_tag: 0, motion_type: 0, status: -1214, exec_state: this.executionState, source: "linuxcnc-source-tp" } };
|
||||
const result = call(this.core, "sim_linuxcnc_tp_read_state", "number", ["number", "number"], [this.planner, this.statePointer]);
|
||||
@@ -207,13 +181,22 @@ export class WasmLinuxcncTp {
|
||||
const activeId = ints[2];
|
||||
if (activeId && activeId !== this.lastActiveId && this.segmentTags.has(activeId)) this.activeTag = this.segmentTags.get(activeId);
|
||||
this.lastActiveId = activeId;
|
||||
const state = { position: toPose(doubles), velocity: doubles[9], acceleration: this.accelerationLimit, progress: doubles[10], tick: ints[0], queue_depth: this.queueDepthOverride === null ? ints[1] : this.queueDepthOverride, active_queue: activeId ? 1 : 0, active_id: activeId, active_tag: this.activeTag, joint_commanded: this.previousJointCommanded.slice(), joint_actual: this.previousJointCommanded.slice(), motion_type: this.core.HEAP32[this.statePointer / 4 + 25], status: this.core.HEAP32[this.statePointer / 4 + 26], exec_state: this.executionState, source: "linuxcnc-source-tp" };
|
||||
const velocityScale = Number(call(this.core, "sim_linuxcnc_tp_velocity_scale", "number", ["number"], [this.planner]));
|
||||
const velocityLimit = Number(call(this.core, "sim_linuxcnc_tp_velocity_limit", "number", ["number"], [this.planner]));
|
||||
const jogScale = Number(call(this.core, "sim_linuxcnc_tp_jog_scale", "number", ["number"], [this.planner]));
|
||||
const feedOverride = Number(call(this.core, "sim_linuxcnc_tp_feed_override", "number", ["number"], [this.planner]));
|
||||
this.controls.velocity_scale = velocityScale;
|
||||
this.controls.jog_scale = jogScale;
|
||||
this.controls.feed_override = feedOverride;
|
||||
const position = toPose(doubles);
|
||||
const joints = this.commandedJoints(position, this.activeTag);
|
||||
const state = { position, velocity: doubles[9], acceleration: this.accelerationLimit, progress: doubles[10], tick: ints[0], queue_depth: this.queueDepthOverride === null ? ints[1] : this.queueDepthOverride, active_queue: activeId ? 1 : 0, active_id: activeId, active_tag: this.activeTag, joint_commanded: joints, joint_actual: joints.slice(), motion_type: this.core.HEAP32[this.statePointer / 4 + 25], status: this.core.HEAP32[this.statePointer / 4 + 26], exec_state: this.executionState, velocity_limit: velocityLimit, velocity_scale: velocityScale, jog_velocity_linear: this.jogVelocity("x"), jog_velocity_angular: this.jogVelocity("a"), feed_override: feedOverride, jog_scale: jogScale, source: "linuxcnc-source-tp" };
|
||||
this.lastState = state;
|
||||
return { status: "verified", state };
|
||||
}
|
||||
|
||||
reset() { if (this.available) call(this.core, "sim_linuxcnc_tp_reset", "number", ["number"], [this.planner]); this.controls.paused = false; this.history = []; this.activeTag = 0; this.segmentTags.clear(); this.lastActiveId = 0; this.queueDepthOverride = null; this.executionState = 3; this.cubic = Array.from({ length: 5 }, () => cubicState()); this.previousJointCommanded = [0, 0, 0, 0, 0]; return this.read(); }
|
||||
abort() { if (this.available) call(this.core, "sim_linuxcnc_tp_abort", "number", ["number"], [this.planner]); this.history = []; this.activeTag = 0; this.segmentTags.clear(); this.lastActiveId = 0; this.queueDepthOverride = null; this.executionState = 3; return this.read(); }
|
||||
reset() { if (this.available) call(this.core, "sim_linuxcnc_tp_reset", "number", ["number"], [this.planner]); this.controls.paused = false; this.activeTag = 0; this.segmentTags.clear(); this.lastActiveId = 0; this.queueDepthOverride = null; this.executionState = 3; return this.read(); }
|
||||
abort() { if (this.available) call(this.core, "sim_linuxcnc_tp_abort", "number", ["number"], [this.planner]); this.activeTag = 0; this.segmentTags.clear(); this.lastActiveId = 0; this.queueDepthOverride = null; this.executionState = 3; return this.read(); }
|
||||
exportState() {
|
||||
const current = this.read();
|
||||
const segments = [];
|
||||
@@ -241,6 +224,8 @@ export class WasmLinuxcncTp {
|
||||
this.executionState = Number(value.execution_state ?? 3);
|
||||
this.setProfile(value.controls?.profile ?? 0, value.controls?.tolerance ?? 0);
|
||||
this.setFeedOverride(value.controls?.feed_override ?? 1);
|
||||
this.setVelocityScale(value.controls?.velocity_scale ?? 1);
|
||||
this.setJogScale(value.controls?.jog_scale ?? 1);
|
||||
this.setPosition(value.state?.position ?? {});
|
||||
for (const segment of value.segments ?? []) this.enqueue({ ...segment, kinematics_mode: segment.kinematics_mode ?? value.active_tag ?? 0 });
|
||||
if (this.available && value.state) {
|
||||
|
||||
@@ -141,9 +141,12 @@ export class WasmXyzacIo {
|
||||
if (kind === "RESET") return this.invoke("sim_io_reset");
|
||||
if (kind === "SPINDLE_COMMAND") {
|
||||
if (payload.delta !== undefined) return this.invoke("sim_io_spindle_adjust", [payload.spindle ?? 0, Number(payload.delta)]);
|
||||
return this.invoke("sim_io_spindle_command", [payload.spindle ?? 0, payload.rpm ?? payload.speed ?? 0, payload.enabled === false ? 0 : 1]);
|
||||
const requestedRpm = Number(payload.rpm ?? payload.speed ?? 0);
|
||||
const signedRpm = payload.direction === -1 ? -requestedRpm : payload.direction === 1 ? requestedRpm : requestedRpm;
|
||||
return this.invoke("sim_io_spindle_command", [payload.spindle ?? 0, signedRpm, payload.enabled === false ? 0 : 1]);
|
||||
}
|
||||
if (kind === "SPINDLE_BRAKE") return this.invoke("sim_io_spindle_brake", [payload.spindle ?? 0, payload.engaged === true ? 1 : 0]);
|
||||
if (kind === "SPINDLE_OVERRIDE" || kind === "SET_SPINDLE_OVERRIDE") return this.invoke("sim_io_spindle_set_override", [payload.spindle ?? 0, Number(payload.scale ?? payload.override ?? 1)]);
|
||||
if (kind === "SPINDLE_FAULT") return this.invoke("sim_io_spindle_set_fault", [payload.spindle ?? 0, payload.fault === true ? 1 : 0]);
|
||||
if (kind === "SPINDLE_INHIBIT") return this.invoke("sim_io_spindle_set_inhibit", [payload.spindle ?? 0, payload.inhibit === true ? 1 : 0]);
|
||||
if (kind === "SPINDLE_ORIENT") return this.invoke("sim_io_spindle_orient", [payload.spindle ?? 0, payload.angle ?? 0, payload.mode ?? 0]);
|
||||
|
||||
@@ -46,11 +46,18 @@ export class XyzacCommandLedger extends OperationLedger {
|
||||
}
|
||||
|
||||
audit({ includeResponses = true, maxEntries = Number.POSITIVE_INFINITY } = {}) {
|
||||
const exported = super.export();
|
||||
// High-frequency SIM_STEP responses request only a small audit window.
|
||||
// OperationLedger.export() clones every historical response before the
|
||||
// window can be selected, which turns a long run into quadratic browser
|
||||
// serialization work. The map is insertion ordered by command sequence,
|
||||
// so select the tail first and clone only the returned entries.
|
||||
const values = [...this.entries.values()];
|
||||
const pending = this.pending();
|
||||
const totalEntries = exported.entries.length;
|
||||
const windowed = Number.isFinite(maxEntries) && maxEntries >= 0 ? exported.entries.slice(-Math.floor(maxEntries)) : exported.entries;
|
||||
const entries = includeResponses ? windowed : windowed.map((entry) => { const { response, ...summary } = entry; return summary; });
|
||||
const totalEntries = values.length;
|
||||
const windowed = Number.isFinite(maxEntries) && maxEntries >= 0 ? values.slice(-Math.floor(maxEntries)) : values;
|
||||
const entries = includeResponses
|
||||
? windowed.map((entry) => structuredClone(entry))
|
||||
: windowed.map((entry) => { const { response, ...summary } = entry; return structuredClone(summary); });
|
||||
const unsigned = { schema: XYZAC_COMMAND_LEDGER_SCHEMA, operation_schema: LEDGER_SCHEMA, phases: [...LEDGER_PHASES], scope: this.scope, entries, entry_count: totalEntries, truncated: entries.length < totalEntries, pending_count: pending.length };
|
||||
return { ...unsigned, state_hash: fnv1a64(unsigned) };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user