Complete 3axis tutorial native and Web/WASM parity tools
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import type { AxisStateView, Capabilities, MessageEnvelope, SimSnapshotV1 } from "./protocol";
|
||||
import type { AxisStateView, Capabilities, MessageEnvelope, PanelSnapshotV1, SimSnapshotV1 } from "./protocol";
|
||||
|
||||
export type SessionStatus = "disconnected" | "handshaking" | "attaching" | "loading" | "ready" | "running" | "paused" | "recovering" | "blocked" | "error";
|
||||
|
||||
@@ -22,6 +22,8 @@ export type FrontendState = {
|
||||
python: Record<string, unknown> | null;
|
||||
ioAck: Record<string, unknown> | null;
|
||||
axisState: AxisStateView | null;
|
||||
gates: Record<string, { action: string; enabled: boolean; reason: string }>;
|
||||
toolpath: Array<{ id: number; source_line?: number | null; start: Record<string, number>; end: Record<string, number>; active?: boolean }>;
|
||||
};
|
||||
|
||||
export const INITIAL_STATE: FrontendState = {
|
||||
@@ -44,6 +46,8 @@ export const INITIAL_STATE: FrontendState = {
|
||||
python: null,
|
||||
ioAck: null,
|
||||
axisState: null,
|
||||
gates: {},
|
||||
toolpath: [],
|
||||
};
|
||||
|
||||
function payload<T>(message: MessageEnvelope): T {
|
||||
@@ -51,15 +55,91 @@ function payload<T>(message: MessageEnvelope): T {
|
||||
}
|
||||
|
||||
function projection(result: Record<string, unknown>, message: MessageEnvelope, previous: FrontendState) {
|
||||
const rawMotion = (result.motion ?? previous.snapshot?.motion ?? {}) as Record<string, unknown>;
|
||||
const panel = result.panel_snapshot as PanelSnapshotV1 | undefined;
|
||||
if (panel?.schema === "axis-panel-snapshot.v1") {
|
||||
const visualSegments = panel.visual.toolpath
|
||||
.filter((segment) => segment && typeof segment === "object" && Number.isSafeInteger((segment as { id?: unknown }).id ?? 0))
|
||||
.map((segment, index) => {
|
||||
const value = segment as Record<string, unknown>;
|
||||
return {
|
||||
id: Number(value.id ?? index + 1),
|
||||
source_line: typeof value.source_line === "number" ? value.source_line : null,
|
||||
start: value.start as Record<string, number>,
|
||||
end: value.end as Record<string, number>,
|
||||
...(value.center ? { center: value.center as Record<string, number> } : {}),
|
||||
...(value.normal ? { normal: value.normal as Record<string, number> } : {}),
|
||||
...(typeof value.motion_type === "number" ? { motion_type: value.motion_type } : {}),
|
||||
...(typeof value.turn === "number" ? { turn: value.turn } : {}),
|
||||
active: value.id === panel.visual.active_segment_id,
|
||||
};
|
||||
});
|
||||
const pose = panel.visual.pose;
|
||||
const axes = [pose.x, pose.y, pose.z].map((value) => Number.isFinite(value) ? value : 0);
|
||||
const snapshot = {
|
||||
schema: panel.schema,
|
||||
simulation_tick: panel.simulation_tick,
|
||||
state_hash: panel.state_hash,
|
||||
task: panel.task as SimSnapshotV1["task"],
|
||||
motion: { ...(panel.motion as SimSnapshotV1["motion"]), axes, joints: panel.visual.joints },
|
||||
coordinates: panel.coordinates,
|
||||
overrides: panel.overrides,
|
||||
wasm_motion: panel.wasm_motion,
|
||||
authoritative: panel.authoritative as SimSnapshotV1["authoritative"],
|
||||
configuration: panel.configuration as SimSnapshotV1["configuration"],
|
||||
visual: panel.visual,
|
||||
} satisfies SimSnapshotV1;
|
||||
return {
|
||||
snapshot,
|
||||
toolpath: visualSegments,
|
||||
task: panel.task,
|
||||
program: panel.program,
|
||||
devices: panel.devices,
|
||||
deviceIo: panel.device_io,
|
||||
hal: panel.hal,
|
||||
axisState: panel.axis_state,
|
||||
gates: { ...panel.gates, jog: panel.gates.jogStart, load: panel.gates.load, run: panel.gates.run, step: panel.gates.step, pause: panel.gates.pause, resume: panel.gates.resume, abort: panel.gates.abort, mdi: panel.gates.mdi, home: panel.gates.home },
|
||||
};
|
||||
}
|
||||
const wasmMotion = (result.wasm_motion as { state?: Record<string, unknown> } | undefined)?.state;
|
||||
const rawMotion = (wasmMotion ?? result.motion ?? previous.snapshot?.motion ?? {}) as Record<string, unknown>;
|
||||
const motionState = (rawMotion.state ?? rawMotion) as Record<string, unknown>;
|
||||
const axesObject = motionState.axes as Record<string, unknown> | undefined;
|
||||
const axes = Array.isArray(motionState.axes)
|
||||
? motionState.axes as number[]
|
||||
: [axesObject?.x, axesObject?.y, axesObject?.z].map((value) => typeof value === "number" ? value : 0);
|
||||
const axesObject = (motionState.axes ?? motionState.position) as Record<string, unknown> | undefined;
|
||||
const interpreterState = (result.state ?? result.interpreter_state) as Record<string, unknown> | undefined;
|
||||
const axes = result.motion !== undefined
|
||||
? Array.isArray(motionState.axes)
|
||||
? motionState.axes as number[]
|
||||
: axesObject && [axesObject.x, axesObject.y, axesObject.z].some((value) => typeof value === "number")
|
||||
? [axesObject.x, axesObject.y, axesObject.z].map((value) => typeof value === "number" ? value : 0)
|
||||
: [0, 0, 0]
|
||||
: interpreterState && [interpreterState.x, interpreterState.y, interpreterState.z].some((value) => typeof value === "number")
|
||||
? [interpreterState.x, interpreterState.y, interpreterState.z].map((value, index) => typeof value === "number" ? value : Number(previous.snapshot?.motion?.axes?.[index] ?? 0))
|
||||
: [Number(previous.snapshot?.motion?.axes?.[0] ?? 0), Number(previous.snapshot?.motion?.axes?.[1] ?? 0), Number(previous.snapshot?.motion?.axes?.[2] ?? 0)];
|
||||
const joints = Array.isArray(motionState.joints) ? motionState.joints as number[] : [];
|
||||
const snapshot = { simulation_tick: message.simulation_tick ?? previous.tick, state_hash: String(result.state_hash ?? previous.snapshot?.state_hash ?? ""), task: result.task as SimSnapshotV1["task"], motion: { axes, joints, velocity: Array.isArray(motionState.velocity) ? motionState.velocity as number[] : [] }, authoritative: result.authoritative as SimSnapshotV1["authoritative"] } satisfies SimSnapshotV1;
|
||||
return { snapshot, task: (result.task as Record<string, unknown>) ?? previous.task, program: (result.program as Record<string, unknown>) ?? previous.program, devices: (result.devices as Record<string, unknown>) ?? previous.devices, deviceIo: (result.device_io as Record<string, unknown>) ?? previous.deviceIo, hal: (result.hal_runtime as Record<string, unknown>) ?? previous.hal, plc: (result.plc as Record<string, unknown>) ?? previous.plc, python: (result.python as Record<string, unknown>) ?? previous.python, ioAck: (result.io_ack as Record<string, unknown>) ?? previous.ioAck, axisState: (result.axis_state as AxisStateView) ?? previous.axisState };
|
||||
const authoritative = result.authoritative as SimSnapshotV1["authoritative"];
|
||||
const programResult = result.program ?? (typeof result.program_status === "string" ? result : previous.program);
|
||||
const planner = authoritative?.planner;
|
||||
const preview = result.preview as { status?: string; segments?: Array<Record<string, unknown>> } | undefined;
|
||||
const previewSegments = preview?.status === "verified" && Array.isArray(preview.segments) ? preview.segments : [];
|
||||
const plannerSegments = [
|
||||
...(planner?.queue ?? []),
|
||||
...(planner?.active ? [planner.active] : []),
|
||||
].filter((segment): segment is NonNullable<typeof segment> & { id: number; start: Record<string, number>; end: Record<string, number> } => Number.isSafeInteger(segment.id) && Boolean(segment.start) && Boolean(segment.end));
|
||||
const command = String(result.command ?? "");
|
||||
const resetPath = command === "PROGRAM_LOAD" || command === "PROGRAM_RESTART";
|
||||
const known = resetPath ? new Set<number>() : new Set(previous.toolpath.map((segment) => segment.id));
|
||||
const nextToolpath = resetPath
|
||||
? previewSegments.map((segment, index) => ({ id: index + 1, source_line: typeof segment.source_line === "number" ? segment.source_line : null, start: segment.start as Record<string, number>, end: segment.end as Record<string, number>, ...(segment.center ? { center: segment.center as Record<string, number> } : {}), ...(segment.normal ? { normal: segment.normal as Record<string, number> } : {}), ...(typeof segment.motion_type === "number" ? { motion_type: segment.motion_type } : {}), ...(typeof segment.turn === "number" ? { turn: segment.turn } : {}), active: false }))
|
||||
: previous.toolpath.map((segment) => ({ ...segment, start: { ...segment.start }, end: { ...segment.end } }));
|
||||
for (const segment of plannerSegments) {
|
||||
if (!known.has(segment.id)) nextToolpath.push({ id: segment.id, source_line: segment.source_line, start: segment.start, end: segment.end, active: segment.id === planner?.active?.id });
|
||||
}
|
||||
if (planner?.active?.id !== undefined) {
|
||||
for (const segment of nextToolpath) segment.active = segment.id === planner.active?.id;
|
||||
} else {
|
||||
for (const segment of nextToolpath) segment.active = false;
|
||||
}
|
||||
const snapshot = { simulation_tick: message.simulation_tick ?? previous.tick, state_hash: String(result.state_hash ?? previous.snapshot?.state_hash ?? ""), task: result.task as SimSnapshotV1["task"], motion: { axes, joints, velocity: Array.isArray(motionState.velocity) ? motionState.velocity as number[] : [], homed: typeof motionState.homed === "boolean" ? motionState.homed : undefined, homed_axes: Array.isArray(motionState.homed_axes) ? motionState.homed_axes as boolean[] : undefined }, authoritative } satisfies SimSnapshotV1;
|
||||
return { snapshot, toolpath: nextToolpath, task: (result.task as Record<string, unknown>) ?? previous.task, program: programResult as Record<string, unknown> ?? previous.program, devices: (result.devices as Record<string, unknown>) ?? previous.devices, deviceIo: (result.device_io as Record<string, unknown>) ?? previous.deviceIo, hal: (result.hal_runtime as Record<string, unknown>) ?? previous.hal, plc: (result.plc as Record<string, unknown>) ?? previous.plc, python: (result.python as Record<string, unknown>) ?? previous.python, ioAck: (result.io_ack as Record<string, unknown>) ?? previous.ioAck, axisState: (result.axis_state as AxisStateView) ?? previous.axisState, gates: (result.gates as FrontendState["gates"]) ?? previous.gates };
|
||||
}
|
||||
|
||||
export function reduceFrontendState(state: FrontendState, message: MessageEnvelope): FrontendState {
|
||||
@@ -67,7 +147,7 @@ export function reduceFrontendState(state: FrontendState, message: MessageEnvelo
|
||||
const base = { ...state, contextId: message.context_id, sessionId: message.session_id, lastSeq: Math.max(state.lastSeq, message.seq), tick, error: null };
|
||||
switch (message.kind) {
|
||||
case "CAPABILITIES": return { ...base, status: "handshaking", capabilities: payload<Capabilities>(message) };
|
||||
case "SESSION_READY": return { ...base, status: "ready" };
|
||||
case "SESSION_READY": { const readyPayload = payload<Record<string, unknown>>(message); return { ...base, status: "ready", task: (readyPayload.task as Record<string, unknown>) ?? state.task, axisState: (readyPayload.axis_state as AxisStateView) ?? state.axisState }; }
|
||||
case "RECOVERY_STATUS": return { ...base, status: payload<{ status?: string }>(message).status === "verified" ? "ready" : "recovering" };
|
||||
case "STATE_SNAPSHOT": {
|
||||
const value = payload<Record<string, unknown>>(message);
|
||||
@@ -77,13 +157,16 @@ export function reduceFrontendState(state: FrontendState, message: MessageEnvelo
|
||||
case "COMMAND_RESULT": {
|
||||
const result = payload<Record<string, unknown>>(message);
|
||||
const task = result.task as { state?: string } | undefined;
|
||||
const status = result.status === "blocked" ? "blocked" : task?.state === "RUNNING" ? "running" : state.status === "running" ? "running" : "ready";
|
||||
const status = result.status === "blocked" || ["ESTOP", "FAULT"].includes(String(task?.state)) ? "blocked" : task?.state === "RUNNING" ? "running" : task?.state === "PAUSED" ? "paused" : "ready";
|
||||
return { ...base, status, ...projection(result, message, state) };
|
||||
}
|
||||
case "ALARM": return { ...base, status: "blocked", alarms: [...state.alarms, payload<Record<string, unknown>>(message)] };
|
||||
case "ERROR": {
|
||||
const value = payload<{ code?: string; message_key?: string }>(message);
|
||||
return { ...base, status: "error", error: { code: value.code ?? "worker_error", message: JSON.stringify(value) } };
|
||||
const value = payload<Record<string, unknown>>(message);
|
||||
const projected = projection(value, message, state);
|
||||
const taskState = String((value.task as Record<string, unknown> | undefined)?.state ?? "").toUpperCase();
|
||||
const status = ["ESTOP", "FAULT"].includes(taskState) ? "blocked" : "error";
|
||||
return { ...base, status, error: { code: String(value.code ?? "worker_error"), message: JSON.stringify(value) }, ...projected };
|
||||
}
|
||||
default: return base;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user