82 lines
8.4 KiB
TypeScript
82 lines
8.4 KiB
TypeScript
import type { ReactNode } from "react";
|
|
import { type XyzacOperationTrace, type XyzacPanelView } from "./xyzac-panel-contract";
|
|
import type { SimSnapshotV1 } from "./protocol";
|
|
import { DEFAULT_MACHINE } from "../../workspace/machine-definitions.mjs";
|
|
|
|
type Command = (kind: string, payload?: Record<string, unknown>, action?: string) => void;
|
|
|
|
export function XyzacKinematicsControl({ view, command, loading = false }: { view: XyzacPanelView; command: Command; loading?: boolean }) {
|
|
const active = view.kinematics?.active_type;
|
|
const machine = view.machine ?? DEFAULT_MACHINE;
|
|
const modes = [{ type: 0, action: "switchkinsIdentity", label: "Identity", mcode: "M429" }, { type: 1, action: "switchkinsXyzac", label: `TCP: ${machine.coordinates}`, mcode: "M428" }, { type: 2, action: "switchkinsUserk", label: "User K", mcode: "M430" }];
|
|
return <section className="xyzac-kinematics" aria-label={`${machine.coordinates} kinematics`}>
|
|
<div className="xyzac-section-title"><strong>{machine.label}</strong><span>{view.kinematics?.native_type ?? "Unavailable"}</span></div>
|
|
<div className="xyzac-segments" role="group" aria-label="Kinematics mode">
|
|
{modes.map((item) => {
|
|
const gate = view.gates[item.action];
|
|
const disabled = loading || view.status !== "ready" || gate?.enabled === false;
|
|
return <button key={item.type} type="button" data-axis-control={`switchkins-${item.type}`} className={active === item.type ? "active" : ""} aria-pressed={active === item.type} disabled={disabled} title={gate?.reason ?? item.mcode} onClick={() => command("KINEMATICS_SELECT", { type: item.type }, item.action)}><span>{item.label}</span><small>{item.mcode}</small></button>;
|
|
})}
|
|
</div>
|
|
</section>;
|
|
}
|
|
|
|
export function XyzacManualPanel({ view }: { view: XyzacPanelView }) {
|
|
const pose = view.pose;
|
|
const machine = view.machine ?? DEFAULT_MACHINE;
|
|
const axes = machine.axes;
|
|
const offsets = view.kinematics?.offsets;
|
|
const offsetRows = machine.coordinates === "XYZBC"
|
|
? [`X offset ${offsets ? Number(offsets.x_offset ?? 0).toFixed(3) : "-"}`, `Z offset ${offsets ? Number(offsets.z_offset).toFixed(3) : "-"}`, `Tool ${offsets ? Number(offsets.tool_offset).toFixed(3) : "-"}`]
|
|
: [`Y offset ${offsets ? Number(offsets.y_offset).toFixed(3) : "-"}`, `Z offset ${offsets ? Number(offsets.z_offset).toFixed(3) : "-"}`, `Tool ${offsets ? Number(offsets.tool_offset).toFixed(3) : "-"}`];
|
|
return <section className="xyzac-manual-summary" aria-label={`${machine.coordinates} authoritative position`}>
|
|
<div className="xyzac-section-title"><strong>{machine.coordinates} authority</strong><span>{view.kinematics?.linear_units ?? "mm"} / {view.kinematics?.angular_units ?? "deg"}</span></div>
|
|
<div className="xyzac-position-grid">
|
|
{axes.map((axis, index) => <div key={axis}><b>{axis.toUpperCase()}</b><output>{pose ? Number(pose[axis as keyof typeof pose] ?? Number.NaN).toFixed(["a", "b", "c"].includes(axis) ? 3 : 4) : "-"}</output><small>J{index} {view.joints.length === machine.joint_count ? Number(view.joints[index]).toFixed(4) : "-"}</small></div>)}
|
|
</div>
|
|
<div className="xyzac-offset-row">{offsetRows.map((row) => <span key={row}>{row}</span>)}</div>
|
|
</section>;
|
|
}
|
|
|
|
export function XyzacProgramPane({ status, currentLine, sourceHash, children }: { status: string; currentLine: number | null; sourceHash?: string | null; children: ReactNode }) {
|
|
return <section className="xyzac-program" aria-label="TRT program"><header><strong>TRT Program</strong><span>{status}</span><span>Line {currentLine ?? "-"}</span><code>{sourceHash?.slice(0, 18) ?? "no source"}</code></header>{children}</section>;
|
|
}
|
|
|
|
export function XyzacLiveParameters({ snapshot }: { snapshot: SimSnapshotV1 | null }) {
|
|
const ready = snapshot?.motion_authority === "wasm" && Boolean(snapshot?.task_authority);
|
|
const motion = (snapshot?.motion ?? {}) as Record<string, unknown>;
|
|
const program = snapshot?.program ?? null;
|
|
const number = (value: unknown, digits = 2, unit = "") => ready && typeof value === "number" && Number.isFinite(value) ? `${value.toFixed(digits)}${unit ? ` ${unit}` : ""}` : "-";
|
|
const integer = (value: unknown) => ready && Number.isSafeInteger(value) ? String(value) : "-";
|
|
const wasmState = (snapshot?.wasm_motion as { state?: Record<string, unknown> } | undefined)?.state;
|
|
return <section className="xyzac-live-parameters" data-axis-feature="AXIS-LIVE-PARAMETERS" data-xyzac-parameter-source={ready ? "linuxcnc-wasm" : "pending"} aria-label="Live machining parameters">
|
|
<div className="xyzac-section-title"><strong>Live parameters</strong><span>{ready ? "LinuxCNC WASM" : "WASM PENDING"}</span></div>
|
|
<div className="xyzac-parameter-grid">
|
|
<div><b>Spindle</b><output>{number(motion.spindle_speed_rpm, 1, "rpm")}</output><small>Cmd {number(motion.spindle_command_rpm, 1, "rpm")} / S {number(program?.speed, 1, "rpm")}</small></div>
|
|
<div><b>Feed actual</b><output>{number(motion.velocity_mm_min, 1, "mm/min")}</output><small>Program {number(motion.feed_command_mm_min, 1, "mm/min")}</small></div>
|
|
<div><b>TP velocity</b><output>{number(motion.velocity, 3, "mm/s")}</output><small>Limit {number(motion.max_velocity, 3, "mm/s")}</small></div>
|
|
<div><b>Overrides</b><output>F {number(typeof motion.feed_override === "number" ? motion.feed_override * 100 : Number.NaN, 0, "%")}</output><small>S {number(typeof motion.spindle_override === "number" ? motion.spindle_override * 100 : Number.NaN, 0, "%")}</small></div>
|
|
<div><b>Planner</b><output>{integer(wasmState?.queue_depth)} queued</output><small>Segment {integer(wasmState?.active_id)}</small></div>
|
|
<div><b>Execution</b><output>Line {integer(program?.execution_line ?? program?.current_line)}</output><small>{ready ? String(program?.program_status ?? "-") : "-"}</small></div>
|
|
</div>
|
|
</section>;
|
|
}
|
|
|
|
export function XyzacDiagnostics({ view, operations, alarms, error }: { view: XyzacPanelView; operations: XyzacOperationTrace[]; alarms: Array<Record<string, unknown>>; error: { code: string; message?: string } | null }) {
|
|
const disabled = Object.entries(view.gates).filter(([, gate]) => gate.enabled === false);
|
|
return <section className="xyzac-diagnostics" aria-label={`${view.machine.coordinates} diagnostics`}>
|
|
<div className="xyzac-diagnostic-summary"><strong>{view.status.toUpperCase()}</strong><span>Tick {view.tick}</span><code>{view.stateHash || "no hash"}</code></div>
|
|
{error && <div className="xyzac-error" role="alert"><b>{error.code}</b><span>{error.message}</span></div>}
|
|
<div><h3>Operation trace</h3>{operations.length ? operations.slice(-8).reverse().map((item, index) => <div className="xyzac-diagnostic-row" key={`${item.operation_id ?? "operation"}-${index}`}><code>{item.operation_id ?? "-"}</code><span>{item.kind}</span><b>{item.phase}</b><span>{item.reason ?? item.response_kind}</span></div>) : <span>No operations</span>}</div>
|
|
<div><h3>Alarms / recovery</h3>{alarms.length ? alarms.slice(-5).map((alarm, index) => <div className="xyzac-diagnostic-row" key={`${String(alarm.id ?? alarm.code ?? "alarm")}-${index}`}><code>{String(alarm.code ?? "alarm")}</code><span>{String(alarm.severity ?? "error")}</span></div>) : <span>No alarms</span>}<div className="xyzac-diagnostic-row"><code>recovery</code><span>{view.recovery ? JSON.stringify(view.recovery) : "idle"}</span></div></div>
|
|
<div><h3>Gate reasons</h3>{disabled.length ? disabled.slice(0, 10).map(([name, gate]) => <div className="xyzac-diagnostic-row" key={name}><code>{name}</code><span>{gate.reason}</span></div>) : <span>All {view.machine.coordinates} gates enabled</span>}</div>
|
|
</section>;
|
|
}
|
|
|
|
export function XyzacTrtPanel({ view, command, loading = false, machineOptions = [], selectedMachine, onMachineChange }: { view: XyzacPanelView; command: Command; loading?: boolean; machineOptions?: readonly { entrypoint: string; label: string }[]; selectedMachine?: string; onMachineChange?: (entrypoint: string) => void }) {
|
|
return <section className="xyzac-control-band" aria-label={`${view.machine?.coordinates ?? "TRT"} controls`}>
|
|
{machineOptions.length > 1 && <label className="xyzac-machine-selector">Machine type<select data-axis-control="machine-type-select" value={selectedMachine ?? view.machine?.entrypoint} onChange={(event) => onMachineChange?.(event.target.value)}>{machineOptions.map((machine) => <option key={machine.entrypoint} value={machine.entrypoint}>{machine.label}</option>)}</select></label>}
|
|
<XyzacKinematicsControl view={view} command={command} loading={loading} /><XyzacManualPanel view={view} />
|
|
</section>;
|
|
}
|