Use native LinuxCNC homing and consolidate web panel status
This commit is contained in:
File diff suppressed because one or more lines are too long
16
web/react/src/MachineStatus.tsx
Normal file
16
web/react/src/MachineStatus.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { SourceStatus } from "./protocol";
|
||||
|
||||
export function MachineStatus({ status, commandStatus }: { status?: SourceStatus; commandStatus: string }) {
|
||||
return <section className="machine-status" aria-label="LinuxCNC status">
|
||||
<div className="machine-status-dimensions">
|
||||
<span>Task mode <output>{status?.task.mode ?? "PENDING"}</output></span>
|
||||
<span>Task state <output>{status?.task.state ?? "PENDING"}</output></span>
|
||||
<span>Interpreter <output>{status?.task.interpState ?? "PENDING"}</output></span>
|
||||
<span>Motion mode <output>{status?.traj.mode ?? "PENDING"}</output></span>
|
||||
<span>Motion type <output>{status?.traj.motion_type ?? "-"}</output></span>
|
||||
<span>Homing <output>{status ? status.homing_active ? "ACTIVE" : "IDLE" : "PENDING"}</output></span>
|
||||
<span>Machine sequence <output>{status ? status.joint.length > 0 && status.joint.every((joint) => joint.homed) ? "HOMED" : "NOT HOMED" : "WASM PENDING"}</output></span>
|
||||
</div>
|
||||
<span className="command-status" role="status" aria-live="polite" title={commandStatus}>{commandStatus}</span>
|
||||
</section>;
|
||||
}
|
||||
@@ -18,6 +18,7 @@ export type MessageEnvelope<TPayload = unknown> = {
|
||||
};
|
||||
|
||||
export type MotionSnapshot = {
|
||||
homing?: boolean;
|
||||
stepping?: boolean;
|
||||
axes?: number[];
|
||||
joints?: number[];
|
||||
@@ -42,6 +43,18 @@ export type MotionSnapshot = {
|
||||
max_velocity?: number;
|
||||
};
|
||||
|
||||
export type HomeJointStatus = {
|
||||
joint: number; homing: boolean; homed: boolean; home_state: number; home_state_name: string;
|
||||
position: number; motor_position: number; motor_offset: number; velocity: number; home_sequence: number;
|
||||
};
|
||||
export type SourceStatus = {
|
||||
task: { mode: string; state: string; interpState: string };
|
||||
traj: { mode: string; motion_type: number };
|
||||
homing_active: boolean;
|
||||
joint: HomeJointStatus[];
|
||||
homing_history: Array<{ tick: number; joint: HomeJointStatus[] }>;
|
||||
};
|
||||
|
||||
export type TrajectorySnapshot = {
|
||||
queue?: Array<{ id?: number; source_line?: number | null; start?: Record<string, number>; end?: Record<string, number> }>;
|
||||
active?: { id?: number; source_line?: number | null; start?: Record<string, number>; end?: Record<string, number> } | null;
|
||||
@@ -49,6 +62,7 @@ export type TrajectorySnapshot = {
|
||||
};
|
||||
|
||||
export type SimSnapshotV1 = {
|
||||
source_status?: SourceStatus;
|
||||
schema?: string;
|
||||
snapshot_id?: string;
|
||||
base_snapshot_id?: string;
|
||||
@@ -114,6 +128,7 @@ export type XyzacCapabilitySnapshot = {
|
||||
};
|
||||
|
||||
export type PanelSnapshotV1 = {
|
||||
source_status?: SourceStatus;
|
||||
schema: "axis-panel-snapshot.v1";
|
||||
context_id: string;
|
||||
session_id: string;
|
||||
|
||||
@@ -83,6 +83,12 @@ function projection(result: Record<string, unknown>, message: MessageEnvelope, p
|
||||
} : previous.snapshot.visual;
|
||||
const nextSnapshot = {
|
||||
...previous.snapshot,
|
||||
source_status: (result.source_status as SimSnapshotV1["source_status"]) ?? (previous.snapshot.source_status ? {
|
||||
...previous.snapshot.source_status,
|
||||
task: { ...previous.snapshot.source_status.task,
|
||||
mode: String((result.task as Record<string, unknown>)?.mode ?? previous.snapshot.source_status.task.mode),
|
||||
interpState: (result.axis_state as AxisStateView)?.source_interp_state ?? previous.snapshot.source_status.task.interpState },
|
||||
} : undefined),
|
||||
simulation_tick: message.simulation_tick ?? delta.tick ?? previous.snapshot.simulation_tick,
|
||||
state_hash: String(result.state_hash ?? previous.snapshot.state_hash),
|
||||
task: (result.task as SimSnapshotV1["task"]) ?? previous.snapshot.task,
|
||||
@@ -147,6 +153,7 @@ function projection(result: Record<string, unknown>, message: MessageEnvelope, p
|
||||
const axes = [pose.x, pose.y, pose.z, pose.a, pose.c].map((value) => Number.isFinite(value) ? value : 0);
|
||||
const snapshot = {
|
||||
schema: panel.schema,
|
||||
source_status: panel.source_status,
|
||||
simulation_tick: panel.simulation_tick,
|
||||
state_hash: panel.state_hash,
|
||||
task: panel.task as SimSnapshotV1["task"],
|
||||
|
||||
@@ -73,7 +73,7 @@ button { color: inherit; }
|
||||
.machine-sequence-heading { display: grid; gap: 2px; min-width: 148px; }
|
||||
.machine-sequence-heading strong { font-size: 12px; }
|
||||
.machine-sequence-heading span { color: #555; font: 10px ui-monospace, monospace; }
|
||||
.machine-sequence-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 5px; min-width: 0; }
|
||||
.machine-sequence-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 5px; min-width: 0; padding: 8px; border-bottom: 1px solid #aaa; }
|
||||
.machine-sequence-actions .axis-text-button { min-height: 28px; }
|
||||
.machine-sequence-actions .axis-text-button:first-child { border-color: #9b4a4a; }
|
||||
|
||||
@@ -245,14 +245,6 @@ output { font: 12px ui-monospace, monospace; text-align: right; }
|
||||
.visually-hidden-file-input { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }
|
||||
.axis-layer-controls { display: flex; flex-wrap: wrap; gap: 8px 12px; margin-top: 4px; color: #4d4d4d; font-size: 11px; }
|
||||
.axis-layer-controls input { margin: 0; }
|
||||
.axis-info-bar { display: flex; align-items: stretch; width: 100%; min-width: 0; min-height: 27px; margin-top: auto; border-top: 1px solid #777; background: #c4c4c4; }
|
||||
.axis-info-bar > span { display: flex; align-items: center; min-width: 0; min-height: 25px; padding: 2px 9px; overflow: hidden; border-right: 1px solid #999; font: 12px ui-monospace, monospace; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.task-state { min-width: 92px; background: #ececec; font-weight: 700 !important; }
|
||||
.task-state.running { color: #145d1c; }
|
||||
.task-state.blocked, .task-state.error { color: #9b111e; background: #f2d6d6; }
|
||||
.tool-state { min-width: 0; flex: 1; }
|
||||
.position-state { min-width: 230px; }
|
||||
.footer-status { min-width: 180px; color: #505050; }
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.xyzac-control-band { grid-template-columns: 1fr; }
|
||||
@@ -287,6 +279,7 @@ output { font: 12px ui-monospace, monospace; text-align: right; }
|
||||
.dro-mode-row { font-size: 9px; }
|
||||
.dro-row output { font-size: 13px; }
|
||||
.dro-footer { flex-direction: column; gap: 3px; }
|
||||
.axis-info-bar { flex-wrap: wrap; }
|
||||
.position-state, .footer-status { min-width: 0; flex: 1; }
|
||||
}
|
||||
.machine-status { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 18px; padding: 6px 8px; border-bottom: 1px solid #999; min-width: 0; }
|
||||
.machine-status-dimensions { display: flex; flex: 1 1 auto; flex-wrap: wrap; gap: 6px 18px; }
|
||||
.machine-status-dimensions output { font-weight: bold; margin-left: 5px; }
|
||||
|
||||
@@ -41,6 +41,7 @@ export function axisState({ task = {}, program = {}, motion = {}, devices = {},
|
||||
const machineState = hasIoState ? ioEstop ? "ESTOP" : ioReady ? "ON" : "OFF"
|
||||
: taskState === "ESTOP" ? "ESTOP" : ["READY", "RUNNING", "PAUSED", "STOPPED"].includes(taskState) ? "ON" : "OFF";
|
||||
let operationState = programActivity(task, program);
|
||||
if (motion.homing) operationState = "HOME";
|
||||
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 === "OFF" ? "STATE_ESTOP_RESET" : "STATE_ON";
|
||||
const sourceInterpState = operationState === "PAUSED" ? "INTERP_PAUSED" : operationState === "RUNNING" ? (program?.program_status === "completed" || String(task.mode).toUpperCase() === "MDI" ? "INTERP_WAITING" : "INTERP_READING") : "INTERP_IDLE";
|
||||
|
||||
@@ -25,6 +25,7 @@ import { axisSourceUi } from "./axis-source-ui.mjs";
|
||||
import { WasmMotionQueue } from "./wasm-motion-queue.mjs";
|
||||
import { WasmLinuxcncTp } from "./wasm-linuxcnc-tp.mjs";
|
||||
import { WasmHomeLimit } from "./wasm-home-limit.mjs";
|
||||
import { WasmNativeHoming } from "./wasm-native-homing.mjs";
|
||||
import { WasmXyzacKinematics, XYZAC_MODES } from "./wasm-xyzac-kinematics.mjs";
|
||||
import { WasmXyzacIo } from "./wasm-xyzac-io.mjs";
|
||||
import { WasmXyzacDeviceIo } from "./wasm-xyzac-device-io.mjs";
|
||||
@@ -33,7 +34,6 @@ import { axisGate, xyzacGate, xyzacGateMatrix } from "../app/axis-contract.mjs";
|
||||
|
||||
const PROTOCOL_SCHEMA = 1;
|
||||
const CONTEXT_ID = "linuxcnc-2.10.0-pre0-wasm-m1-py-c1";
|
||||
const XYZAC_AXIS_TO_JOINT = Object.freeze({ x: 0, y: 1, z: 2, a: 3, c: 4 });
|
||||
|
||||
const OPERATION_KINDS = new Set([
|
||||
"SESSION_ATTACH", "LOAD_WORKSPACE", "SIM_STEP", "DESTROY", "CHECKPOINT", "RECOVER", "ABORT",
|
||||
@@ -135,13 +135,8 @@ export class SimSession {
|
||||
this.xyzacIo = null;
|
||||
this.xyzacDeviceIo = null;
|
||||
this.xyzacProgram = null;
|
||||
this.homingHistory = [];
|
||||
this.xyzacProgramKinematicsMode = XYZAC_MODES.identity;
|
||||
// The Web panel presents fixed XYZAC world-axis controls. Homing still
|
||||
// follows AXIS's KINEMATICS_BOTH transaction and uses a numeric joint;
|
||||
// keep the presentation mode separate so the adapter can restore it
|
||||
// after the accepted home transaction.
|
||||
this.xyzacMotionMode = "TELEOP";
|
||||
this.pendingHome = null;
|
||||
this.xyzacRapidVelocity = 35;
|
||||
this.xyzacMotionCycleTime = 0.01;
|
||||
this.xyzacTaskCycleTicks = 1;
|
||||
@@ -192,7 +187,7 @@ export class SimSession {
|
||||
// Compact payloads are only valid for successful motion samples. Error
|
||||
// responses must retain their code/reason instead of being rewritten into
|
||||
// a delta with undefined fields.
|
||||
const motionDelta = kind === "COMMAND_RESULT" && lightweightXyzacStep && input.payload?.response === "motion_delta" && !completedMotion;
|
||||
const motionDelta = kind === "COMMAND_RESULT" && lightweightXyzacStep && input.payload?.response === "motion_delta" && !completedMotion && !statePayload.homing_sample && !this.wasmHomeLimit.active;
|
||||
// The frontend retains the canonical preview from PROGRAM_LOAD. Repeating
|
||||
// thousands of immutable segments on unrelated control responses makes a
|
||||
// loaded program delay abort/power/home commands behind structured-clone
|
||||
@@ -263,6 +258,13 @@ export class SimSession {
|
||||
simulation_tick: tick,
|
||||
payload: {
|
||||
...responsePayload,
|
||||
...(motionDelta ? { source_status: {
|
||||
task: { mode: task.mode, state: axisState({ task, program, io: this.xyzacIo.export() }).source_task_state,
|
||||
interpState: axisState({ task, program, io: this.xyzacIo.export() }).source_interp_state },
|
||||
traj: { mode: this.wasmHomeLimit.mode, motion_type: currentMotion.state.motion_type },
|
||||
homing_active: this.wasmHomeLimit.active, joint: this.wasmHomeLimit.read().state.joint,
|
||||
homing_history: this.homingHistory,
|
||||
} } : {}),
|
||||
...(panelSnapshot ? { panel_snapshot: panelSnapshot } : {}),
|
||||
motion_authority: responsePayload.motion_authority ?? this.motionAuthority,
|
||||
task_authority: responsePayload.task_authority ?? this.task.authority(),
|
||||
@@ -275,6 +277,7 @@ export class SimSession {
|
||||
wasmMotionProjection(program = null) {
|
||||
const wasmMotion = this.wasmMotion.read();
|
||||
const position = wasmMotion.status === "verified" ? wasmMotion.state.position : { x: 0, y: 0, z: 0, a: 0, b: 0, c: 0, u: 0, v: 0, w: 0 };
|
||||
if (this.xyzacProgram && !this.wasmHomeLimit.active) this.wasmHomeLimit.sync(wasmMotion.state.joint_commanded);
|
||||
const home = this.wasmHomeLimit.read();
|
||||
const io = this.xyzacIo?.canonical?.();
|
||||
const ioState = io && typeof io === "object" && io.status !== "blocked" ? io : null;
|
||||
@@ -308,7 +311,8 @@ export class SimSession {
|
||||
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,
|
||||
motion_mode: this.xyzacProgram ? this.xyzacMotionMode : (home.state?.homed ? "TELEOP" : "FREE"),
|
||||
motion_mode: this.xyzacProgram ? this.wasmHomeLimit.mode : (home.state?.homed ? "TELEOP" : "FREE"),
|
||||
homing: Boolean(home.state?.homing),
|
||||
jog: null,
|
||||
stepping: this.wasmMotion.isStepping?.() ?? false,
|
||||
estop: Boolean(ioState?.estop),
|
||||
@@ -401,9 +405,12 @@ export class SimSession {
|
||||
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"];
|
||||
const ui = axisSourceUi({ task_state: axis.source_task_state, interp_state: axis.source_interp_state,
|
||||
taskfile: Boolean(this.xyzacProgram?.path), queued_mdi_commands: task.queued_mdi_commands ?? 0,
|
||||
motion_mode: this.xyzacProgram ? this.xyzacMotionMode : (homeLimit.state?.homed ? "TELEOP" : "FREE"), kinematics_type: "BOTH",
|
||||
motion_mode: this.xyzacProgram ? this.wasmHomeLimit.mode : (homeLimit.state?.homed ? "TELEOP" : "FREE"), kinematics_type: "BOTH",
|
||||
spindle_direction: Number(devices.spindles?.[0]?.direction ?? 0), tool: Number(devices.tool?.current ?? 0),
|
||||
on_any_limit: motion.on_any_limit, paused: axis.operation_state === "PAUSED" });
|
||||
if (homeLimit.state?.homing) {
|
||||
for (const name of ["run", "step", "load", "reload", "home", "jog", "mdi", "touchOff"]) ui.gates[name] = { action: name, enabled: false, reason: "linuxcnc_homing_active" };
|
||||
}
|
||||
const gates = this.xyzacProgram
|
||||
? {
|
||||
load: this.xyzacCommandGate({ kind: "PROGRAM_LOAD", payload: {} }),
|
||||
@@ -483,6 +490,14 @@ export class SimSession {
|
||||
task,
|
||||
program,
|
||||
motion,
|
||||
...(this.xyzacProgram ? { source_status: {
|
||||
task: { mode: task.mode, state: axis.source_task_state, interpState: axis.source_interp_state },
|
||||
traj: { mode: this.wasmHomeLimit.mode, motion_type: wasmMotion.state.motion_type },
|
||||
homing_active: Boolean(homeLimit.state.homing),
|
||||
homing_history: this.homingHistory,
|
||||
joint: homeLimit.state.joint,
|
||||
authority: { task: "wasm-task-adapter", homing: "linuxcnc-homing.c", home_state: "joint.N.home-state" },
|
||||
} } : {}),
|
||||
coordinates: { machine: [...axisCoordinates], relative: axisCoordinates.map((value, index) => value - displayedOffsets[index]), commanded: [...axisCoordinates], offsets: displayedOffsets, system: coordinateContext?.system ?? 1 },
|
||||
overrides: { feed: motion.feed_override, spindle: [motion.spindle_override] },
|
||||
wasm_motion: wasmMotion,
|
||||
@@ -556,6 +571,8 @@ export class SimSession {
|
||||
const program = this.xyzacProgram.snapshot();
|
||||
const io = this.xyzacIo?.canonical?.() ?? {};
|
||||
const wasmMotion = this.wasmMotion.read();
|
||||
if (["HOME", "SET_MODE"].includes(input.kind) && Number(wasmMotion.state?.queue_depth) > 0) return { enabled: false, reason: "xyzac_motion_active" };
|
||||
if (input.kind === "JOG_START" && !this.wasmHomeLimit.read().state.homed) return { enabled: false, reason: "xyzac_not_all_homed" };
|
||||
if (input.kind === "PROGRAM_LOAD" && wasmMotion.status === "verified" && Number(wasmMotion.state.queue_depth ?? 0) > 0) {
|
||||
return { action: null, enabled: false, reason: "xyzac_motion_active", taskState: task.state, interpState: "READING", wasm: { queue_depth: wasmMotion.state.queue_depth, active_id: wasmMotion.state.active_id } };
|
||||
}
|
||||
@@ -563,7 +580,7 @@ export class SimSession {
|
||||
return { action: input.payload?.type === 0 ? "switchkinsIdentity" : input.payload?.type === 1 ? "switchkinsXyzac" : "switchkinsUserk", enabled: false, reason: "xyzac_motion_active", taskState: task.state, interpState: "READING", wasm: { queue_depth: wasmMotion.state.queue_depth, active_id: wasmMotion.state.active_id } };
|
||||
}
|
||||
const homeLimit = this.wasmHomeLimit.read();
|
||||
if (this.pendingHome && ["HOME", "JOG_START", "MDI_EXECUTE", "PROGRAM_LOAD", "PROGRAM_RUN", "PROGRAM_STEP", "KINEMATICS_SELECT", "SET_MODE", "TOUCH_OFF", "SPINDLE_COMMAND", "SPINDLE_CONSTANT", "SPINDLE_BRAKE", "COOLANT_COMMAND"].includes(input.kind)) {
|
||||
if (this.wasmHomeLimit.active && ["HOME", "JOG_START", "JOG_STOP", "MDI_EXECUTE", "PROGRAM_LOAD", "LOAD_WORKSPACE", "PROGRAM_RUN", "PROGRAM_STEP", "PROGRAM_RESTART", "PROGRAM_SEEK", "KINEMATICS_SELECT", "SET_MODE", "TOUCH_OFF", "RECOVER", "CHECKPOINT"].includes(input.kind)) {
|
||||
return { action: PANEL_GATE_ACTIONS[input.kind] ?? "home", enabled: false, reason: "xyzac_home_in_progress", taskState: task.state, interpState: "READING" };
|
||||
}
|
||||
const estop = Boolean(io.estop);
|
||||
@@ -659,8 +676,10 @@ export class SimSession {
|
||||
// canonical P-number form (G54=P1, ..., G59.3=P9).
|
||||
const block = `G10 L20 P${systemNumber} ${axis}${formatted}`;
|
||||
const savedMode = this.task.state.mode;
|
||||
const savedMotionMode = this.wasmHomeLimit.mode;
|
||||
try {
|
||||
if (savedMode !== "MDI") this.task.setMode("MDI");
|
||||
this.wasmHomeLimit.setMode("COORD");
|
||||
this.wasmMotion.setPaused(false);
|
||||
const synchronized = this.xyzacProgram.synchronizePosition(this.wasmMotion.read().state.position);
|
||||
if (synchronized.status !== "verified") return synchronized;
|
||||
@@ -675,6 +694,7 @@ export class SimSession {
|
||||
return { status: "error", code: String(error?.message ?? error) };
|
||||
} finally {
|
||||
if (this.task.state.mode !== savedMode) this.task.setMode(savedMode);
|
||||
this.wasmHomeLimit.setMode(savedMotionMode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1359,7 +1379,7 @@ export class SimSession {
|
||||
|
||||
const deviceResponse = (kind, payload) => {
|
||||
if (this.xyzacProgram && ["SPINDLE_COMMAND", "SPINDLE_BRAKE", "SPINDLE_CONSTANT"].includes(kind) && !payload?.source_keyboard && (kind !== "SPINDLE_COMMAND" || payload?.delta === undefined)) {
|
||||
try { if (this.task.state.mode !== "MANUAL") this.task.setMode("MANUAL"); }
|
||||
try { if (this.task.state.mode !== "MANUAL") { this.task.setMode("MANUAL"); this.wasmHomeLimit.setMode(this.wasmHomeLimit.read().state.homed ? "TELEOP" : "FREE"); } }
|
||||
catch (error) { return finish(this.error(input, "task_mode", String(error?.message ?? error)), "failed"); }
|
||||
}
|
||||
const result = this.xyzacIo ? this.xyzacIo.command(kind, payload ?? {}) : this.devices.command(kind, payload ?? {});
|
||||
@@ -1387,8 +1407,7 @@ export class SimSession {
|
||||
this.xyzacAppendDeferredOnActive = false;
|
||||
this.wasmMotion.abort();
|
||||
this.wasmMotion.setPaused(true);
|
||||
this.pendingHome = null;
|
||||
this.xyzacMotionMode = "TELEOP";
|
||||
this.wasmHomeLimit.cancel(true);
|
||||
const resetProgram = this.resetXyzacProgramToLoaded();
|
||||
if (resetProgram.status !== "verified") return finish(this.error(input, "program_restart", resetProgram.code ?? "program_reload_failed"), "failed");
|
||||
this.task.setProgram("loaded", 0);
|
||||
@@ -1460,7 +1479,11 @@ export class SimSession {
|
||||
}
|
||||
const mode = String(input.payload?.mode ?? "").toUpperCase();
|
||||
if (!["MANUAL", "MDI", "AUTO"].includes(mode)) return finish(this.error(input, "task_mode", "task_mode_invalid"), "failed");
|
||||
try { this.task.setMode(mode); } catch (error) { return finish(this.error(input, "task_mode", String(error?.message ?? error)), "failed"); }
|
||||
try {
|
||||
this.task.setMode(mode);
|
||||
if (this.xyzacProgram && mode === "MANUAL") this.wasmHomeLimit.setMode(this.wasmHomeLimit.read().state.homed ? "TELEOP" : "FREE");
|
||||
if (this.xyzacProgram && mode !== "MANUAL" && this.wasmHomeLimit.read().state.homed) this.wasmHomeLimit.setMode("COORD");
|
||||
} catch (error) { return finish(this.error(input, "task_mode", String(error?.message ?? error)), "failed"); }
|
||||
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, mode, task: this.task.export(), operation_id: input.operation_id ?? null }));
|
||||
}
|
||||
case "POWER": {
|
||||
@@ -1492,8 +1515,7 @@ export class SimSession {
|
||||
this.xyzacAppendDeferredOnActive = false;
|
||||
this.wasmMotion.abort();
|
||||
this.wasmMotion.setPaused(true);
|
||||
this.pendingHome = null;
|
||||
this.xyzacMotionMode = "TELEOP";
|
||||
this.wasmHomeLimit.cancel(true);
|
||||
const resetProgram = this.resetXyzacProgramToLoaded();
|
||||
if (resetProgram.status !== "verified") return finish(this.error(input, "program_restart", resetProgram.code ?? "program_reload_failed"), "failed");
|
||||
this.task.setProgram("loaded", 0);
|
||||
@@ -1612,9 +1634,8 @@ export class SimSession {
|
||||
? 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.homingHistory = [];
|
||||
this.xyzacProgramKinematicsMode = XYZAC_MODES.identity;
|
||||
this.xyzacMotionMode = "TELEOP";
|
||||
this.pendingHome = null;
|
||||
if (typeof this.wasmMotion.setKinematics === "function") this.wasmMotion.setKinematics(this.xyzacKinematics);
|
||||
this.xyzacRapidVelocity = Number(configuration.kinematics?.max_linear_velocity ?? 35);
|
||||
const linearLimit = Number(configuration.kinematics?.axis_linear_velocity_limit ?? 20);
|
||||
@@ -1627,7 +1648,7 @@ export class SimSession {
|
||||
}
|
||||
if (configuration?.home_limits && this.motionAuthority === "wasm") {
|
||||
this.wasmHomeLimit.destroy();
|
||||
this.wasmHomeLimit = new WasmHomeLimit(this.core, {
|
||||
this.wasmHomeLimit = this.xyzacProgram ? new WasmNativeHoming(this.core, configuration, this.xyzacKinematics) : new WasmHomeLimit(this.core, {
|
||||
jointCount: configuration.home_limits.joint_count ?? 3,
|
||||
kinematicsType: configuration.home_limits.kinematics_type ?? 1,
|
||||
coordinates: configuration.home_limits.coordinates ?? "XYZ",
|
||||
@@ -1681,6 +1702,7 @@ export class SimSession {
|
||||
})) : finish(this.coreError(input, loadResult, "sim_load_workspace"), "failed");
|
||||
}
|
||||
case "SIM_STEP": {
|
||||
const homingSample = Boolean(this.xyzacProgram && this.wasmHomeLimit.active);
|
||||
const cycles = input.payload?.cycles;
|
||||
const limit = validateLimit("max_tick_step", cycles);
|
||||
if (limit.status !== "accepted") return finish(this.envelope("ERROR", input, { ...limit, operation_id: input.operation_id ?? null, simulation_tick: this.tick, retryable: false }), "failed");
|
||||
@@ -1711,6 +1733,7 @@ export class SimSession {
|
||||
this.scheduler.step(cycles, {
|
||||
task: (nextTick) => {
|
||||
if (this.xyzacProgram) {
|
||||
if (this.wasmHomeLimit.active) return;
|
||||
const wasmState = this.xyzacProgram.snapshot();
|
||||
if (wasmState.status !== 2) return;
|
||||
if (this.pendingTaskSync !== 0) {
|
||||
@@ -1907,7 +1930,24 @@ export class SimSession {
|
||||
let wasmMotion = this.wasmMotion.read();
|
||||
const programPaused = this.xyzacProgram?.snapshot().status === 3;
|
||||
for (let servo = 0; servo < this.xyzacTaskCycleTicks; servo += 1) {
|
||||
if (this.xyzacProgram && this.wasmHomeLimit.active) {
|
||||
const home = this.wasmHomeLimit.step();
|
||||
if (home.status !== "verified") throw new Error(home.code);
|
||||
const transition = home.state.joint.map((j) => `${j.home_state}:${Number(j.homing)}:${Number(j.homed)}`).join(",");
|
||||
if (this.homingHistory.at(-1)?.transition !== transition) this.homingHistory = [...this.homingHistory.slice(-63), { tick: home.state.tick, transition, joint: home.state.joint }];
|
||||
// Coordinate synchronization after native motor-offset /
|
||||
// free_tp updates, not an artificial move to machine zero.
|
||||
this.wasmMotion.setActiveKinematicsTag(this.xyzacKinematics.activeMode);
|
||||
this.wasmMotion.setPosition(home.state.pose);
|
||||
wasmMotion = this.wasmMotion.read();
|
||||
if (!home.state.homing) {
|
||||
const sync = this.xyzacProgram.synchronizePosition(home.state.pose);
|
||||
if (sync.status !== "verified") throw new Error(sync.code);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (programPaused && !this.wasmMotion.isStepping()) break;
|
||||
if (this.wasmMotion.controls.paused && this.wasmMotion.read().state.queue_depth === 0) continue;
|
||||
if (this.planner === "linuxcnc-tp" && typeof this.wasmMotion.setExecutionState === "function") {
|
||||
this.wasmMotion.setExecutionState(this.xyzacExecutionWaitSamples > 0 ? 2 : 3);
|
||||
}
|
||||
@@ -1962,18 +2002,6 @@ export class SimSession {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.pendingHome && wasmMotion.status === "verified" && Number(wasmMotion.state.queue_depth ?? 0) === 0) {
|
||||
const pendingHome = this.pendingHome;
|
||||
const home = this.wasmHomeLimit.home(pendingHome.seq, pendingHome.all ? null : pendingHome.joint);
|
||||
if (home.status !== "verified") throw new Error(home.code ?? "wasm_home_commit_failed");
|
||||
if (this.xyzacProgram && typeof this.xyzacProgram.synchronizePosition === "function") {
|
||||
const synchronized = this.xyzacProgram.synchronizePosition(home.state.pose);
|
||||
if (synchronized.status !== "verified") throw new Error(synchronized.code ?? "home_position_sync_failed");
|
||||
}
|
||||
this.xyzacMotionMode = pendingHome.modeBefore;
|
||||
this.task.setMode("MANUAL");
|
||||
this.pendingHome = null;
|
||||
}
|
||||
if (motionState?.fault && this.task.state.state === "RUNNING") this.task.fault(-1205);
|
||||
if (this.motionAuthority === "wasm") {
|
||||
if (wasmMotion.status === "verified") {
|
||||
@@ -2051,7 +2079,7 @@ export class SimSession {
|
||||
? 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,
|
||||
status: "verified", cycles, program_steps, execution_trace, homing_sample: homingSample,
|
||||
...(lightweightXyzac && cycles <= 32 && motion_trace.length ? { motion_trace } : {}),
|
||||
motion_authority: this.motionAuthority,
|
||||
task: this.task.export(), scheduler: this.scheduler.export(), authoritative: snapshotState.authoritative,
|
||||
@@ -2143,7 +2171,7 @@ export class SimSession {
|
||||
if (this.xyzacProgram) {
|
||||
const previousProgramStatus = this.xyzacProgram.snapshot().program_status;
|
||||
const result = this.xyzacProgram.start();
|
||||
if (result.status === "verified") { this.wasmMotion.setPaused(false); this.task.setMode("AUTO"); if (this.task.state.state === "STOPPED") this.task.recover(); this.task.transition("RUNNING"); const program = this.xyzacProgram.snapshot(); this.task.setProgram("running", program.execution_source_line ?? program.current_line); }
|
||||
if (result.status === "verified") { this.wasmMotion.setPaused(false); this.task.setMode("AUTO"); this.wasmHomeLimit.setMode("COORD"); if (this.task.state.state === "STOPPED") this.task.recover(); this.task.transition("RUNNING"); const program = this.xyzacProgram.snapshot(); this.task.setProgram("running", program.execution_source_line ?? program.current_line); }
|
||||
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, reopened_taskfile: ["completed", "aborted"].includes(previousProgramStatus), start_line: 0, program: this.xyzacProgram.snapshot(), task: this.task.export(), wasm_motion: this.wasmMotion.read(), motion: this.wasmMotionProjection(), motion_authority: "wasm", command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
|
||||
}
|
||||
const result = this.program.start();
|
||||
@@ -2165,6 +2193,7 @@ export class SimSession {
|
||||
}), result.status === "verified" ? "verified" : "failed");
|
||||
}
|
||||
case "PROGRAM_PAUSE": case "PROGRAM_RESUME": case "PROGRAM_ABORT": {
|
||||
if (this.xyzacProgram && input.kind === "PROGRAM_ABORT" && this.wasmHomeLimit.active) this.wasmHomeLimit.cancel();
|
||||
const action = input.kind === "PROGRAM_PAUSE" ? "pause" : input.kind === "PROGRAM_RESUME" ? "resume" : "abort";
|
||||
if (this.xyzacProgram) {
|
||||
// PAUSE/RESUME are interpreter and TP state transitions. They must
|
||||
@@ -2275,6 +2304,7 @@ export class SimSession {
|
||||
const current = this.xyzacProgram.snapshot();
|
||||
const idle = !["RUNNING", "PAUSED"].includes(this.task.state.state);
|
||||
this.task.setMode("AUTO");
|
||||
this.wasmHomeLimit.setMode("COORD");
|
||||
if (idle) {
|
||||
const started = this.xyzacProgram.start();
|
||||
if (started.status !== "verified") return finish(this.error(input, "program_step", started.code), "failed");
|
||||
@@ -2364,6 +2394,7 @@ export class SimSession {
|
||||
const block = input.payload?.block ?? input.payload?.command;
|
||||
if (this.xyzacProgram) {
|
||||
this.task.setMode("MDI");
|
||||
this.wasmHomeLimit.setMode("COORD");
|
||||
// AXIS send_mdi_command() leaves the interpreter/TP runnable after
|
||||
// a previous manual jog stop. JOG_STOP intentionally pauses the
|
||||
// WASM TP, so clear that transport pause before queuing MDI motion;
|
||||
@@ -2416,6 +2447,7 @@ export class SimSession {
|
||||
const mode = Number(input.payload?.type);
|
||||
if (![XYZAC_MODES.identity, XYZAC_MODES.xyzac, XYZAC_MODES.userk].includes(mode)) return finish(this.error(input, "kinematics", "xyzac_kinematics_type_invalid"), "failed");
|
||||
this.task.setMode("MDI");
|
||||
this.wasmHomeLimit.setMode("COORD");
|
||||
const result = this.switchXyzacKinematics(mode);
|
||||
const code = mode === XYZAC_MODES.xyzac ? "M428" : mode === XYZAC_MODES.identity ? "M429" : "M430";
|
||||
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, command: input.kind, remap: { code, hal_pin: "motion.switchkins-type", hal_pin_vars: true, sync: "M66 E0 L0" }, kinematics: result.kinematics ?? this.xyzacKinematics.snapshot(), task: this.task.export(), operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
|
||||
@@ -2430,49 +2462,22 @@ export class SimSession {
|
||||
// that compatible while making the explicit Web contract
|
||||
// HOME {all:true}.
|
||||
const all = payload.all === true || (payload.all === undefined && payload.axis === undefined && payload.joint === undefined);
|
||||
const requestedAxis = all ? null : String(payload.axis ?? "").trim().toLowerCase() || null;
|
||||
let requestedJoint = payload.joint;
|
||||
if (requestedAxis !== null) {
|
||||
const mappedJoint = XYZAC_AXIS_TO_JOINT[requestedAxis];
|
||||
if (mappedJoint === undefined) return finish(this.error(input, "home_limit", "home_axis_invalid"), "failed");
|
||||
if (requestedJoint !== undefined && requestedJoint !== mappedJoint) return finish(this.error(input, "home_limit", "home_axis_joint_mismatch"), "failed");
|
||||
requestedJoint = mappedJoint;
|
||||
}
|
||||
if (all && payload.joint !== undefined) return finish(this.error(input, "home_limit", "home_target_ambiguous"), "failed");
|
||||
if (payload.axis !== undefined) return finish(this.error(input, "home_limit", "home_joint_requires_joint_mode"), "failed");
|
||||
const requestedJoint = payload.joint;
|
||||
if (!all && requestedJoint === undefined) return finish(this.error(input, "home_limit", "home_joint_required"), "failed");
|
||||
if (!all && (!Number.isInteger(requestedJoint) || requestedJoint < 0 || requestedJoint >= 5)) return finish(this.error(input, "home_limit", "home_joint_invalid"), "failed");
|
||||
// AXIS home_all_joints()/home_joint() calls ensure_mode(MANUAL)
|
||||
// before go_home() selects FREE/joint and issues c.home().
|
||||
try { if (this.task.state.mode !== "MANUAL") this.task.setMode("MANUAL"); } catch (error) { return finish(this.error(input, "task_mode", String(error?.message ?? error)), "failed"); }
|
||||
const modeBefore = this.xyzacMotionMode;
|
||||
const currentMotion = this.wasmMotion.read();
|
||||
const currentPose = currentMotion.state?.position;
|
||||
const currentJoints = Array.isArray(currentMotion.state?.joint_commanded) && currentMotion.state.joint_commanded.length === 5
|
||||
? [...currentMotion.state.joint_commanded]
|
||||
: [...(this.xyzacKinematics?.snapshot()?.joints ?? [currentPose.x, currentPose.y, currentPose.z, currentPose.a ?? 0, currentPose.c ?? 0])];
|
||||
const targetHome = this.wasmHomeLimit.previewHome(all ? null : requestedJoint);
|
||||
if (targetHome.status !== "verified") return finish(this.error(input, "home_limit", targetHome.code ?? "wasm_home_target_failed"), "failed");
|
||||
const targetPose = targetHome.state.pose;
|
||||
const needsMove = ["x", "y", "z", "a", "b", "c", "u", "v", "w"].some((axis) => Math.abs(Number(currentPose?.[axis] ?? 0) - Number(targetPose?.[axis] ?? 0)) > 1e-9);
|
||||
this.xyzacMotionMode = "FREE";
|
||||
if (needsMove) {
|
||||
const enqueue = this.wasmMotion.enqueue({
|
||||
start: currentPose,
|
||||
end: targetPose,
|
||||
velocity: Math.max(Number(this.xyzacRapidVelocity ?? 35), Number.EPSILON),
|
||||
acceleration: Math.max(Number(this.xyzacAxisAccelerationLimits?.x ?? 300), Number.EPSILON),
|
||||
motion_type: 1,
|
||||
source_line: 0,
|
||||
kinematics_mode: this.xyzacProgramKinematicsMode,
|
||||
});
|
||||
if (enqueue.status !== "verified") { this.xyzacMotionMode = modeBefore; return finish(this.error(input, "home_limit", enqueue.code ?? "home_motion_enqueue_failed"), "failed"); }
|
||||
this.pendingHome = { seq: input.seq, all, axis: requestedAxis, joint: all ? null : requestedJoint, modeBefore, currentJoints, targetPose };
|
||||
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, homing: { phase: "moving", target: all ? "all" : "joint", source_axis: requestedAxis, joint: all ? null : requestedJoint, motion_mode_before: modeBefore, motion_mode_during: "FREE", motion_mode_after: "FREE", target_pose: targetPose }, motion: this.wasmMotionProjection(), wasm_home_limit: this.wasmHomeLimit.read(), motion_authority: "wasm", operation_id: input.operation_id ?? null }), "verified");
|
||||
}
|
||||
const home = this.wasmHomeLimit.home(input.seq, all ? null : requestedJoint);
|
||||
if (home.status !== "verified") { this.xyzacMotionMode = modeBefore; return finish(this.error(input, "home_limit", home.code), "failed"); }
|
||||
this.xyzacMotionMode = modeBefore;
|
||||
this.task.setMode("MANUAL");
|
||||
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, homing: { phase: "complete", target: all ? "all" : "joint", source_axis: requestedAxis, joint: all ? null : requestedJoint, motion_mode_before: modeBefore, motion_mode_during: "FREE", motion_mode_after: this.xyzacMotionMode }, motion: this.wasmMotionProjection(), wasm_home_limit: home, motion_authority: "wasm", operation_id: input.operation_id ?? null }), "verified");
|
||||
try {
|
||||
this.wasmHomeLimit.sync(this.wasmMotion.read().state.joint_commanded);
|
||||
const home = this.wasmHomeLimit.home(input.seq, all ? null : requestedJoint);
|
||||
this.homingHistory = [{ tick: home.state.tick, transition: "command", joint: home.state.joint }];
|
||||
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind,
|
||||
homing: { phase: "accepted", joint: all ? -1 : requestedJoint, source: "linuxcnc-homing.c" },
|
||||
wasm_home_limit: home, motion_authority: "wasm", operation_id: input.operation_id ?? null }), "verified");
|
||||
} catch (error) { return finish(this.error(input, "home_limit", error.message), "failed"); }
|
||||
}
|
||||
const result = this.motion.home(input.payload?.axis ?? null);
|
||||
if (result.status === "verified" && this.motionAuthority === "wasm") {
|
||||
@@ -2520,9 +2525,7 @@ export class SimSession {
|
||||
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");
|
||||
// The Web jog buttons are world-axis controls. Mirror AXIS's
|
||||
// teleop selection for this adapter before submitting the TP jog.
|
||||
this.xyzacMotionMode = "TELEOP";
|
||||
if (this.xyzacProgram) this.wasmHomeLimit.setMode("TELEOP");
|
||||
this.wasmMotion.setPaused(false);
|
||||
const requestedIncrement = input.payload?.increment;
|
||||
const continuous = requestedIncrement === null || input.payload?.continuous === true;
|
||||
|
||||
@@ -29,9 +29,8 @@ assert.equal(panel().ui_gates.run.enabled, true);
|
||||
assert.equal(panel().ui_gates.step.enabled, true);
|
||||
assert.equal(send("PROGRAM_RUN").payload.gate_reason, "xyzac_not_all_homed");
|
||||
ok("HOME", { all: true });
|
||||
const homeAxis = send("HOME", { axis: "x", joint: 0 });
|
||||
assert.equal(homeAxis.payload.status, "verified");
|
||||
assert.deepEqual(homeAxis.payload.homing, { phase: "complete", target: "joint", source_axis: "x", joint: 0, motion_mode_before: "TELEOP", motion_mode_during: "FREE", motion_mode_after: "TELEOP" });
|
||||
ok("SIM_STEP", { cycles: 1 });
|
||||
assert.equal(send("HOME", { axis: "x" }).payload.message_key, "home_joint_requires_joint_mode");
|
||||
|
||||
ok("PROGRAM_STEP");
|
||||
assert.equal(panel().program.current_line, 0, "first native Step starts at line zero and pauses");
|
||||
|
||||
110
web/worker/test_native_homing.mjs
Normal file
110
web/worker/test_native_homing.mjs
Normal file
@@ -0,0 +1,110 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { createHash } from "node:crypto";
|
||||
import createModule from "../../build-wasm/linuxcnc-sim-core.js";
|
||||
import { loadSimulationCase, toWorkerConfiguration } from "../workspace/sim-case-loader.mjs";
|
||||
import { XyzacSession, CONTEXT_ID } from "./xyzac-session.mjs";
|
||||
|
||||
const path = "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini";
|
||||
const bundle = await loadSimulationCase({ path, fetchImpl: async (url) => {
|
||||
try { const body = await readFile(new URL(`../../${String(url).replace(/^\//, "")}`, import.meta.url), "utf8"); return { ok: true, text: async () => body }; }
|
||||
catch { return { ok: false, text: async () => "" }; }
|
||||
} });
|
||||
const binary = await readFile(new URL("../../build-wasm/linuxcnc-sim-core.wasm", import.meta.url));
|
||||
for (const [file, hash] of [["homing.c", "ee73d38f2feb8d5523b9a733cb17d635d3904667aaa219f8a02d7fbffee1ba39"], ["simple_tp.c", "5a0026f4d5bdd9d1bc55290c6ea9c399abd664633b69413c307f7353953d2603"]]) {
|
||||
assert.equal(createHash("sha256").update(await readFile(new URL(`../../linuxcnc/src/emc/motion/${file}`, import.meta.url))).digest("hex"), hash, `source drift: ${file}`);
|
||||
}
|
||||
async function setup(id, finalHome = 0) {
|
||||
const core = await createModule({ wasmBinary: binary });
|
||||
const session = new XyzacSession(core, { sessionId: id });
|
||||
let seq = 0;
|
||||
const send = (kind, payload = {}) => session.accept({ schema_version: 1, context_id: CONTEXT_ID, session_id: id, seq: ++seq, operation_id: `${id}-${seq}`, kind, payload });
|
||||
const ok = (kind, payload) => { const r = send(kind, payload); assert.notEqual(r.kind, "ERROR", `${kind}: ${r.payload.message_key}`); return r.payload; };
|
||||
ok("HELLO"); ok("SESSION_ATTACH");
|
||||
const configuration = toWorkerConfiguration(bundle);
|
||||
if (finalHome) configuration.files[path] = configuration.files[path].replace(/HOME =\s+0/g, `HOME = ${finalHome}`);
|
||||
ok("LOAD_WORKSPACE", { root_path: "/workspace", configuration });
|
||||
ok("PROGRAM_LOAD", { source: "G21 G90\nG1 X10 F600\nM2\n", path: "native-home.ngc" });
|
||||
ok("POWER", { active: true });
|
||||
const snapshot = () => ok("SNAPSHOT_REQUEST").panel_snapshot;
|
||||
const drainHome = () => {
|
||||
let ticks = 0;
|
||||
while (session.wasmHomeLimit.active && ticks++ < 1000) ok("SIM_STEP", { cycles: 1, response: "motion_delta" });
|
||||
assert.equal(session.wasmHomeLimit.active, false);
|
||||
return snapshot();
|
||||
};
|
||||
const jog = (axis, increment) => {
|
||||
ok("JOG_START", { axis, direction: 1, increment });
|
||||
let ticks = 0;
|
||||
while (session.wasmMotion.read().state.queue_depth > 0 && ticks++ < 5000) ok("SIM_STEP", { cycles: 1, response: "motion_delta" });
|
||||
assert.equal(session.wasmMotion.read().state.queue_depth, 0);
|
||||
};
|
||||
return { session, send, ok, snapshot, drainHome, jog };
|
||||
}
|
||||
|
||||
const s = await setup("native-homing");
|
||||
assert.equal(s.snapshot().source_status.traj.mode, "FREE");
|
||||
assert.equal(s.send("HOME", { axis: "x" }).payload.message_key, "home_joint_requires_joint_mode");
|
||||
for (const joint of [0, 1, 2, 3, 4]) {
|
||||
const ack = s.ok("HOME", { joint });
|
||||
assert.equal(ack.homing.phase, "accepted");
|
||||
assert.equal(ack.panel_snapshot.source_status.homing_active, true);
|
||||
assert.equal(ack.panel_snapshot.source_status.task.interpState, "INTERP_IDLE");
|
||||
assert.equal(ack.panel_snapshot.axis_state.operation_state, "HOME");
|
||||
assert.equal(ack.panel_snapshot.motion.homed_axes[joint], false);
|
||||
const done = s.drainHome();
|
||||
assert.equal(done.motion.homed_axes[joint], true);
|
||||
assert.equal(done.source_status.traj.mode, joint === 4 ? "TELEOP" : "FREE");
|
||||
assert.ok(done.source_status.homing_history.some((e) => e.joint[joint].home_state === 21 && e.joint[joint].homing));
|
||||
}
|
||||
for (const [joint, axis] of ["x", "y", "z", "a", "c"].entries()) {
|
||||
s.jog(axis, 3);
|
||||
const before = s.snapshot();
|
||||
s.ok("HOME", { joint });
|
||||
const after = s.drainHome();
|
||||
after.source_status.joint.forEach((j, index) => {
|
||||
assert.ok(Math.abs(j.motor_position - before.source_status.joint[index].motor_position) < 1e-8, "no-search home does not move a motor");
|
||||
assert.ok(Math.abs(j.position - (index === joint ? 0 : before.source_status.joint[index].position)) < 1e-8);
|
||||
});
|
||||
assert.equal(after.visual.live_path_epoch, before.visual.live_path_epoch);
|
||||
}
|
||||
s.jog("x", 4); s.jog("y", 2);
|
||||
const beforeAll = s.snapshot();
|
||||
s.ok("HOME", { all: true });
|
||||
for (const [kind, payload] of [["PROGRAM_RUN", {}], ["MDI_EXECUTE", { block: "G0 X1" }], ["SET_MODE", { mode: "AUTO" }]]) assert.equal(s.send(kind, payload).kind, "ERROR");
|
||||
const afterAll = s.drainHome();
|
||||
assert.deepEqual(afterAll.source_status.joint.map((j) => j.position), [0, 0, 0, 0, 0]);
|
||||
assert.deepEqual(afterAll.source_status.joint.map((j) => j.motor_position), beforeAll.source_status.joint.map((j) => j.motor_position));
|
||||
const nativeCheckpoint = s.session.wasmHomeLimit.exportState();
|
||||
s.session.wasmHomeLimit.reset();
|
||||
s.session.wasmHomeLimit.restoreState(nativeCheckpoint);
|
||||
assert.deepEqual(s.session.wasmHomeLimit.read().state.joint, afterAll.source_status.joint);
|
||||
s.ok("PROGRAM_RUN");
|
||||
assert.equal(s.snapshot().source_status.task.mode, "AUTO");
|
||||
assert.equal(s.snapshot().source_status.traj.mode, "COORD");
|
||||
s.ok("PROGRAM_PAUSE");
|
||||
assert.equal(s.snapshot().source_status.task.interpState, "INTERP_PAUSED");
|
||||
assert.equal(s.send("SET_MODE", { mode: "MANUAL" }).kind, "ERROR");
|
||||
s.ok("PROGRAM_ABORT"); s.ok("SET_MODE", { mode: "MANUAL" });
|
||||
assert.equal(s.snapshot().source_status.traj.mode, "TELEOP");
|
||||
s.ok("HOME", { joint: 0 }); s.ok("ESTOP", { active: true }); s.ok("SIM_STEP", { cycles: 1 });
|
||||
assert.equal(s.snapshot().source_status.homing_active, false);
|
||||
assert.equal(s.snapshot().source_status.joint[0].homed, false);
|
||||
s.ok("ESTOP", { active: false }); s.ok("POWER", { active: true }); s.ok("HOME", { joint: 0 }); s.drainHome();
|
||||
|
||||
// A configured final move uses the original joint free_tp, not coordinated TP.
|
||||
const moving = await setup("native-final-move", 5);
|
||||
moving.ok("HOME", { joint: 0 });
|
||||
const mid = moving.ok("SIM_STEP", { cycles: 1 }).panel_snapshot;
|
||||
assert.equal(mid.source_status.joint[0].home_state, 21);
|
||||
assert.ok(mid.source_status.joint[0].position > 0 && mid.source_status.joint[0].position < 5);
|
||||
assert.equal(mid.wasm_motion.state.queue_depth, 0);
|
||||
assert.equal(mid.source_status.joint[0].homed, false);
|
||||
const done = moving.drainHome();
|
||||
assert.ok(Math.abs(done.source_status.joint[0].position - 5) < 1e-8);
|
||||
assert.ok(Math.abs(done.source_status.joint[0].motor_position - 5) < 1e-8);
|
||||
moving.ok("HOME", { joint: 1 }); moving.ok("SIM_STEP", { cycles: 1 }); moving.ok("PROGRAM_ABORT");
|
||||
moving.drainHome();
|
||||
assert.equal(moving.snapshot().source_status.joint[1].homed, false);
|
||||
assert.equal(moving.snapshot().source_status.homing_active, false);
|
||||
console.log(JSON.stringify({ status: "verified", source: "homing.c + simple_tp.c", checks: ["five-joint-home", "no-search-coordinate-rebase", "motor-position-preserved", "native-final-move", "mode-interlocks", "abort-and-recovery", "servo-transition-history"] }));
|
||||
@@ -1,74 +1,3 @@
|
||||
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 { XyzacSession, CONTEXT_ID } from "./xyzac-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 wasmBinary = await readFile(new URL("../../build-wasm/linuxcnc-sim-core.wasm", import.meta.url));
|
||||
const core = await createModule({ wasmBinary });
|
||||
|
||||
function open(sessionId) {
|
||||
const session = new XyzacSession(core, { sessionId });
|
||||
const base = { schema_version: 1, context_id: CONTEXT_ID, session_id: sessionId };
|
||||
let seq = 0;
|
||||
const send = (kind, payload = {}) => session.accept({ ...base, kind, seq: ++seq, operation_id: `${sessionId}-${seq}`, payload });
|
||||
assert.equal(send("HELLO").kind, "CAPABILITIES");
|
||||
assert.equal(send("SESSION_ATTACH").kind, "SESSION_READY");
|
||||
assert.equal(send("LOAD_WORKSPACE", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) }).payload.status, "verified");
|
||||
assert.equal(send("POWER", { active: true }).payload.status, "verified");
|
||||
return { session, send };
|
||||
}
|
||||
|
||||
const partial = open("xyzac-home-axis-source");
|
||||
for (const [axis, joint] of [["x", 0], ["y", 1], ["z", 2], ["a", 3], ["c", 4]]) {
|
||||
const response = partial.send("HOME", { axis, joint });
|
||||
assert.equal(response.kind, "COMMAND_RESULT", `${axis}: ${JSON.stringify(response.payload)}`);
|
||||
assert.deepEqual(response.payload.homing, {
|
||||
phase: "complete", target: "joint", source_axis: axis, joint,
|
||||
motion_mode_before: "TELEOP", motion_mode_during: "FREE", motion_mode_after: "TELEOP",
|
||||
});
|
||||
assert.equal(response.payload.wasm_home_limit.state.homed_mask, (1 << (joint + 1)) - 1);
|
||||
}
|
||||
assert.equal(partial.send("HOME", { axis: "x", joint: 1 }).payload.message_key, "home_axis_joint_mismatch");
|
||||
assert.equal(partial.session.wasmHomeLimit.read().state.homed_mask, 31);
|
||||
|
||||
const all = open("xyzac-home-all-source");
|
||||
assert.equal(all.send("HOME", { all: true }).payload.wasm_home_limit.state.homed, true);
|
||||
const move = all.send("MDI_EXECUTE", { block: "G21 G90 G0 X10 Y5 Z20 A30 C45" });
|
||||
assert.equal(move.kind, "COMMAND_RESULT");
|
||||
for (let tick = 0; tick < 20000 && all.session.wasmMotion.read().state.queue_depth > 0; tick += 1) all.send("SIM_STEP", { cycles: 1 });
|
||||
assert.ok(Math.abs(all.session.wasmMotion.read().state.position.x - 10) < 1e-8);
|
||||
const rehome = all.send("HOME", { all: true });
|
||||
assert.equal(rehome.kind, "COMMAND_RESULT");
|
||||
assert.equal(rehome.payload.homing.phase, "moving");
|
||||
assert.notDeepEqual(rehome.payload.panel_snapshot.visual.pose, { x: 0, y: 0, z: 0, a: 0, b: 0, c: 0, u: 0, v: 0, w: 0 });
|
||||
for (let tick = 0; tick < 2000 && all.session.wasmMotion.read().state.queue_depth > 0; tick += 1) all.send("SIM_STEP", { cycles: 1 });
|
||||
const completed = all.send("SNAPSHOT_REQUEST");
|
||||
assert.deepEqual(completed.payload.panel_snapshot.visual.pose, { x: 0, y: 0, z: 0, a: 0, b: 0, c: 0, u: 0, v: 0, w: 0 });
|
||||
assert.deepEqual(completed.payload.wasm_home_limit.state.joints, [0, 0, 0, 0, 0]);
|
||||
assert.ok(completed.payload.wasm_home_limit.state.home_sequence > rehome.payload.wasm_home_limit.state.home_sequence);
|
||||
|
||||
const running = open("xyzac-home-gate-source");
|
||||
assert.equal(running.send("PROGRAM_LOAD", { source: "G21 G90\nG1 X10 F60\nM2\n", path: "home-gate.ngc" }).kind, "COMMAND_RESULT");
|
||||
assert.equal(running.send("HOME", { all: true }).kind, "COMMAND_RESULT");
|
||||
assert.equal(running.send("PROGRAM_RUN").kind, "COMMAND_RESULT");
|
||||
assert.equal(running.send("HOME", { axis: "x", joint: 0 }).payload.gate_reason, "xyzac_interp_running");
|
||||
|
||||
console.log(JSON.stringify({
|
||||
schema: "xyzac-home-source-flow.v1",
|
||||
status: "verified",
|
||||
source: ["axis.py:home_all_joints", "axis.py:home_joint", "axis.py:go_home", "axis.tcl:update_state"],
|
||||
checks: ["axis_to_joint_map", "free_joint_transaction", "pose_sync", "invalid_pair_rejection", "running_gate"],
|
||||
}));
|
||||
// Preserve the documented test entry point; the assertions now verify the
|
||||
// original homemod rather than the retired coordinated return-to-zero adapter.
|
||||
import "./test_native_homing.mjs";
|
||||
|
||||
@@ -34,6 +34,7 @@ assert.equal(loadEpoch, 0);
|
||||
assert.ok(previewCount > 0);
|
||||
send("POWER", { active: true });
|
||||
send("HOME", { all: true });
|
||||
send("SIM_STEP", { cycles: 1 });
|
||||
|
||||
const run = send("PROGRAM_RUN");
|
||||
const runEpoch = epoch(run);
|
||||
|
||||
@@ -24,7 +24,7 @@ const snapshot = () => session.xyzacProgram.snapshot();
|
||||
const sample = (cycles = 5) => send("SIM_STEP", { cycles, response: "motion_delta" });
|
||||
send("HELLO"); send("SESSION_ATTACH"); send("LOAD_WORKSPACE", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) });
|
||||
send("PROGRAM_LOAD", { path: "pause-reading.ngc", source: "G21 G90\n" + Array.from({ length: 80 }, (_, i) => `G1 X${i % 2 ? 0 : 2} F600`).join("\n") + "\nM2\n" });
|
||||
send("POWER", { active: true }); send("HOME", { all: true }); send("PROGRAM_RUN"); sample();
|
||||
send("POWER", { active: true }); send("HOME", { all: true }); sample(1); send("PROGRAM_RUN"); sample();
|
||||
for (let round = 0; round < 3; round++) {
|
||||
send("PROGRAM_PAUSE");
|
||||
const pose = motion().position, line = snapshot().current_line, queue = motion().queue_depth;
|
||||
|
||||
@@ -29,6 +29,7 @@ const source = "G21\nG90\nG0 X0 Y0 Z10 A0 C0\nG1 X10 Y5 Z5 A15 C20 F600\nG1 X0 Y
|
||||
assert.equal(send("PROGRAM_LOAD", { source, path: "rerun.ngc" }).payload.status, "verified");
|
||||
assert.equal(send("POWER", { active: true }).payload.status, "verified");
|
||||
assert.equal(send("HOME", { all: true }).payload.status, "verified");
|
||||
assert.equal(send("SIM_STEP", { cycles: 1 }).payload.status, "verified");
|
||||
|
||||
assert.equal(send("PROGRAM_RUN").payload.task.state, "RUNNING");
|
||||
let completed;
|
||||
|
||||
@@ -28,7 +28,8 @@ async function create(id) {
|
||||
const loaded = send("LOAD_WORKSPACE", "workspace", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) });
|
||||
assert.equal(loaded.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);
|
||||
assert.equal(send("HOME", "home-all", { all: true }).payload.homing.phase, "accepted");
|
||||
assert.equal(send("SIM_STEP", "home-complete", { cycles: 1 }).payload.panel_snapshot.motion.homed, true);
|
||||
return { core, session, send };
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ for (const estop of [false, true]) {
|
||||
assert.equal(snapshot().ui_gates.abort.enabled, false, "Stop widget must be disabled at IDLE");
|
||||
}
|
||||
send("ESTOP", { active: false }); send("POWER", { active: true }); send("HOME", { all: true });
|
||||
send("SIM_STEP", { cycles: 1 });
|
||||
send("SET_MODE", { mode: "MDI" });
|
||||
assert.equal(snapshot().gates.spindle.enabled, true);
|
||||
assert.equal(snapshot().gates.touchOff.enabled, true);
|
||||
|
||||
@@ -27,6 +27,7 @@ assert.equal(send("LOAD_WORKSPACE", { root_path: "/workspace", configuration: to
|
||||
assert.equal(send("PROGRAM_LOAD", { source: "G21 G90\nG0 X10 Y5 Z200 A30 C45\nG1 X20 Y10 Z180 A35 C50 F100\nM2\n", path: "touch.ngc" }).payload.status, "verified");
|
||||
assert.equal(send("POWER", { active: true }).payload.status, "verified");
|
||||
assert.equal(send("HOME", { all: true }).payload.status, "verified");
|
||||
assert.equal(send("SIM_STEP", { cycles: 1 }).payload.status, "verified");
|
||||
|
||||
const jogStart = send("JOG_START", { axis: "X", direction: 1, increment: null, continuous: true });
|
||||
assert.equal(jogStart.payload.status, "verified");
|
||||
|
||||
@@ -108,17 +108,6 @@ export class WasmHomeLimit {
|
||||
return this.read();
|
||||
}
|
||||
|
||||
// Resolve the canonical target of a home command without committing the
|
||||
// homed mask or pose. The actual commit is performed only after the LinuxCNC
|
||||
// TP has executed the queued move to this target.
|
||||
previewHome(axis = null) {
|
||||
const saved = this.exportState();
|
||||
const target = this.home(0, axis);
|
||||
try { this.restoreState(saved); } catch (error) { return { status: "error", code: String(error?.message ?? error) }; }
|
||||
if (target.status !== "verified") return target;
|
||||
return { status: "verified", state: target.state };
|
||||
}
|
||||
|
||||
check(joints) {
|
||||
if (!this.available) return this.read();
|
||||
if (!Array.isArray(joints) || joints.length < this.jointCount || joints.some((value) => typeof value !== "number" || !Number.isFinite(value))) return { status: "error", code: "wasm_home_joints_invalid" };
|
||||
|
||||
88
web/worker/wasm-native-homing.mjs
Normal file
88
web/worker/wasm-native-homing.mjs
Normal file
@@ -0,0 +1,88 @@
|
||||
import { fnv1a64 } from "./replay.mjs";
|
||||
import { parseIni } from "../workspace/ini-parser.mjs";
|
||||
|
||||
const STATES = ["IDLE", "START", "UNLOCK", "UNLOCK_WAIT", "INITIAL_BACKOFF_START", "INITIAL_BACKOFF_WAIT", "INITIAL_SEARCH_START", "INITIAL_SEARCH_WAIT", "SET_COARSE_POSITION", "FINAL_BACKOFF_START", "FINAL_BACKOFF_WAIT", "RISE_SEARCH_START", "RISE_SEARCH_WAIT", "FALL_SEARCH_START", "FALL_SEARCH_WAIT", "SET_SWITCH_POSITION", "INDEX_ONLY_START", "INDEX_SEARCH_START", "INDEX_SEARCH_WAIT", "SET_INDEX_POSITION", "FINAL_MOVE_START", "FINAL_MOVE_WAIT", "LOCK", "LOCK_WAIT", "FINISHED", "ABORT"];
|
||||
export class WasmNativeHoming {
|
||||
constructor(core, configuration, kinematics) {
|
||||
this.core = core;
|
||||
this.kinematics = kinematics;
|
||||
this.jointCount = 5;
|
||||
this.size = core._sim_native_home_size();
|
||||
this.pointer = core._malloc(this.size);
|
||||
this.buffer = core._malloc(12 * 8);
|
||||
if (!this.pointer || !this.buffer) throw new Error("native_home_allocation_failed");
|
||||
this.available = true;
|
||||
const parsed = parseIni({ entrypoint: configuration.entrypoint, workspace: { read: (path) => {
|
||||
const value = configuration.files[path.replace(/^\/workspace\//, "")];
|
||||
if (typeof value !== "string") throw new Error(`home_ini_missing:${path}`);
|
||||
return value;
|
||||
} } });
|
||||
this.parameters = Array.from({ length: 5 }, (_, j) => {
|
||||
const section = parsed.sections[`JOINT_${j}`] ?? {};
|
||||
const n = (key, fallback) => section[key] === undefined ? fallback : Number(section[key]);
|
||||
const flags = [["HOME_IGNORE_LIMITS", 1], ["HOME_USE_INDEX", 2], ["HOME_IS_SHARED", 4], ["LOCKING_INDEXER", 8], ["HOME_INDEX_NO_ENCODER_RESET", 128]].reduce((mask, [key, bit]) => mask | (n(key, 0) ? bit : 0), 0);
|
||||
const encoder = n("HOME_ABSOLUTE_ENCODER", 0);
|
||||
if (![0, 1, 2].includes(encoder)) throw new Error("native_home_absolute_encoder_invalid");
|
||||
return [n("HOME_OFFSET", 0), n("HOME", 0), n("HOME_FINAL_VEL", -1), n("HOME_SEARCH_VEL", 0), n("HOME_LATCH_VEL", 0), flags | (encoder ? 16 | 32 | (encoder === 2 ? 64 : 0) : 0), n("HOME_SEQUENCE", 999), n("VOLATILE_HOME", 0), n("MIN_LIMIT", -1e9), n("MAX_LIMIT", 1e9), n("MAX_VELOCITY", 1), n("MAX_ACCELERATION", 1)];
|
||||
});
|
||||
this.period = Number(parsed.sections.EMCMOT?.SERVO_PERIOD ?? 1000000) / 1e9;
|
||||
this.reset();
|
||||
}
|
||||
invoke(name, ...args) {
|
||||
const rc = this.core[`_sim_native_home_${name}`](this.pointer, ...args);
|
||||
if (rc !== 0) throw new Error(`native_home_${name}_${rc}`);
|
||||
}
|
||||
get(j, field) { return this.core._sim_native_home_get(this.pointer, j, field); }
|
||||
get active() { return Boolean(this.get(0, 0)); }
|
||||
get mode() { return [null, "FREE", "COORD", "TELEOP"][this.get(0, 1)]; }
|
||||
setMode(mode) { this.invoke("mode", { FREE: 1, COORD: 2, TELEOP: 3 }[mode]); }
|
||||
sync(joints) { this.core.HEAPF64.set(joints, this.buffer / 8); this.invoke("sync", this.buffer); }
|
||||
home(seq, joint = null) { this.invoke("start", joint ?? -1, seq); return this.read(); }
|
||||
step() { this.invoke("step"); return this.read(); }
|
||||
cancel(volatile = false) { this.invoke("cancel", Number(volatile)); return this.read(); }
|
||||
reset() {
|
||||
this.invoke("init", 5, this.period);
|
||||
this.parameters.forEach((p, j) => { this.core.HEAPF64.set(p, this.buffer / 8); this.invoke("config", j, this.buffer); });
|
||||
return this.read();
|
||||
}
|
||||
check(joints) { if (!this.active) this.sync(joints); return this.read(); }
|
||||
read() {
|
||||
const joint = Array.from({ length: 5 }, (_, j) => ({
|
||||
joint: j, homing: Boolean(this.get(j, 4)), homed: Boolean(this.get(j, 5)),
|
||||
home_state: this.get(j, 6), home_state_name: `HOME_${STATES[this.get(j, 6)]}`,
|
||||
position: this.get(j, 7), motor_position: this.get(j, 8), motor_offset: this.get(j, 9),
|
||||
home_sequence: this.get(j, 10), velocity: this.get(j, 11), index_enable: Boolean(this.get(j, 12)),
|
||||
}));
|
||||
const joints = joint.map((j) => j.position);
|
||||
const result = this.kinematics.forward(joints);
|
||||
return { status: result.status, ...(result.code ? { code: result.code } : {}), state: {
|
||||
schema: "sim_home_limit_state.v1", source: "linuxcnc-homing.c", joint_count: 5,
|
||||
homed: joint.every((j) => j.homed), homed_mask: joint.reduce((mask, j) => mask | (j.homed ? 1 << j.joint : 0), 0),
|
||||
homing: this.active, motion_mode: this.mode, joint, joints, pose: result.pose ?? null,
|
||||
home_sequence: this.get(0, 3), tick: this.get(0, 2), last_error: result.result ?? 0, limit_error: result.result === -702 ? -702 : 0, kinematics_error: result.result ?? 0,
|
||||
} };
|
||||
}
|
||||
exportState() {
|
||||
// emcmot_joint_t includes large unused compensation tables. Preserve the
|
||||
// complete native state while avoiding repeated zero bytes in checkpoints.
|
||||
const bytes = this.core.HEAPU8.subarray(this.pointer, this.pointer + this.size);
|
||||
const nonzero = [];
|
||||
for (let i = 0; i < bytes.length; i++) if (bytes[i] !== 0) nonzero.push([i, bytes[i]]);
|
||||
const value = { schema: "native-homing.v1", parameters: this.parameters, period: this.period,
|
||||
byte_length: this.size, nonzero_bytes: nonzero };
|
||||
return { ...value, state_hash: fnv1a64(value) };
|
||||
}
|
||||
restoreState(value) {
|
||||
const { state_hash, ...unsigned } = value;
|
||||
if (value.schema !== "native-homing.v1" || state_hash !== fnv1a64(unsigned) || value.byte_length !== this.size || value.period !== this.period || fnv1a64(value.parameters) !== fnv1a64(this.parameters) || !Array.isArray(value.nonzero_bytes)) throw new Error("native_home_restore_invalid");
|
||||
let previous = -1;
|
||||
const bytes = new Uint8Array(this.size);
|
||||
for (const [offset, byte] of value.nonzero_bytes) {
|
||||
if (!Number.isInteger(offset) || offset <= previous || offset >= this.size || !Number.isInteger(byte) || byte < 1 || byte > 255) throw new Error("native_home_restore_bytes_invalid");
|
||||
bytes[offset] = byte; previous = offset;
|
||||
}
|
||||
this.core.HEAPU8.set(bytes, this.pointer);
|
||||
return this.read();
|
||||
}
|
||||
destroy() { this.core._free(this.pointer); this.core._free(this.buffer); }
|
||||
}
|
||||
Reference in New Issue
Block a user