完善虚拟HAL仿真替代能力

This commit is contained in:
2026-06-17 19:45:51 +08:00
parent 656cddf73a
commit a4d449ad48
17 changed files with 5005 additions and 302 deletions

View File

@@ -225,6 +225,26 @@
if (!api?.getMachineStatusState) {
throw new Error("simulation API missing AXIS machine status controls");
}
if (
!api?.getVirtualHalState ||
!api?.applyVirtualHalAction ||
!api?.getVirtualHalDroState ||
!api?.getVirtualHalLimitsHomeState ||
!api?.getVirtualHalMachineStatusState ||
!api?.getVirtualHalPinRegistry ||
!api?.readVirtualHalPin ||
!api?.writeVirtualHalPin ||
!api?.applyVirtualHalPinUpdates ||
!api?.getVirtualHalIntegrityReport ||
!api?.executeVirtualHalcmd ||
!api?.executeVirtualHalCommand ||
!api?.stepVirtualHalMotionController ||
!api?.getVirtualHalSimulationRuntimeReport ||
!api?.getVirtualHalSimulationReplacementReport ||
!api?.getVirtualRealtimeHalRuntimeReport
) {
throw new Error("simulation API missing browser virtual HAL controls");
}
if (!api?.getMachineSessionLoadState || !api?.loadReadyMachineSession) {
throw new Error("simulation API missing AXIS machine/session load controls");
}
@@ -278,7 +298,7 @@
throw new Error(`simulation initial built-in run should populate recent programs: ${JSON.stringify(initialRecentPrograms)}`);
}
api.clearRecentPrograms();
if (api.getRecentPrograms().length !== 0 || doc.body.dataset.recentProgramCount !== "0" || !doc.querySelector("[data-recent-program-list]")?.textContent.includes("n/a")) {
if (api.getRecentPrograms().length !== 0 || doc.body.dataset.recentProgramCount !== "0" || !doc.querySelector("[data-recent-program-list]")?.textContent.includes("No recent programs")) {
throw new Error(`simulation recent programs clear did not reset API/DOM state: ${JSON.stringify(api.getRecentPrograms())}`);
}
if (!doc.querySelector("[data-open-program]") || !doc.querySelector("[data-open-program-file]")) {
@@ -296,8 +316,8 @@
if (!doc.querySelector('[data-axis-shell="machine-session"]') || !doc.querySelector("[data-check-machine-session]") || !doc.querySelector("[data-load-machine-session]") || !doc.querySelector("[data-use-loaded-session]")) {
throw new Error("simulation page missing machine/session readiness controls");
}
if (!doc.querySelector("[data-axis-machine-control-state]")?.textContent.includes("not bound")) {
throw new Error("simulation page should explicitly mark unbound real-machine controls");
if (!doc.querySelector("[data-axis-machine-control-state]")?.textContent.includes("browser virtual HAL active")) {
throw new Error("simulation page should explicitly mark browser virtual HAL machine controls");
}
if (!doc.querySelector("[data-mdi-program-text]") || !doc.querySelector("[data-load-mdi-program]") || !doc.querySelector("[data-run-mdi-program]") || !doc.querySelector("[data-mdi-history-list]") || !doc.querySelector("[data-clear-mdi-history]")) {
throw new Error("simulation page missing MDI program text controls");
@@ -319,10 +339,10 @@
const initialMachineStatus = api.getMachineStatusState();
if (
initialMachineStatus?.source !== "linuxcnc-canonical-events" ||
initialMachineStatus.spindle.state !== "n/a" ||
doc.querySelector('[data-machine-status="spindle-state"]')?.textContent !== "n/a"
initialMachineStatus.spindle.state !== "off" ||
doc.querySelector('[data-machine-status="spindle-state"]')?.textContent !== "off"
) {
throw new Error(`simulation initial machine status should expose explicit unavailable state: ${JSON.stringify(initialMachineStatus)}`);
throw new Error(`simulation initial machine status should expose AXIS default state: ${JSON.stringify(initialMachineStatus)}`);
}
const modalState = api.getModalState();
if (modalState?.source !== "linuxcnc-update-tag" || !modalState.raw?.includes("canon_event=UPDATE_TAG")) {
@@ -334,7 +354,7 @@
}
const diagnosticsState = api.getDiagnosticsState();
if (
diagnosticsState.lastEvent === "n/a" ||
diagnosticsState.lastEvent === "pending" ||
diagnosticsState.lastError !== "none" ||
!doc.querySelector('[data-diagnostics="last-event"]')?.textContent.includes("canon_event=")
) {
@@ -346,18 +366,39 @@
}
const limitsHome = api.getLimitsHomeState();
if (
limitsHome.source !== "unavailable" ||
limitsHome.axes.x.homed !== "n/a" ||
limitsHome.source !== "browser-virtual-hal" ||
limitsHome.axes.x.homed !== "no" ||
doc.querySelector('[data-limits-home-row="x"]')?.children.length !== 5 ||
!doc.querySelector("[data-limits-home-summary]")?.textContent.includes("source unavailable")
!doc.querySelector("[data-limits-home-summary]")?.textContent.includes("source browser-virtual-hal")
) {
throw new Error(`simulation limits/home state should remain explicit unavailable: ${JSON.stringify(limitsHome)}`);
throw new Error(`simulation limits/home state should expose browser virtual HAL rows: ${JSON.stringify(limitsHome)}`);
}
const virtualHalInitial = api.getVirtualHalState();
if (
virtualHalInitial.source !== "browser-virtual-hal" ||
virtualHalInitial.component !== "axisui" ||
virtualHalInitial.task.state !== "ESTOP" ||
!virtualHalInitial.axisuiPins.some(({ name }) => name === "jog.x") ||
!virtualHalInitial.axisuiPins.some(({ name }) => name === "jog.increment")
) {
throw new Error(`simulation virtual HAL initial state drift: ${JSON.stringify(virtualHalInitial)}`);
}
const initialVirtualHalRegistry = api.getVirtualHalPinRegistry();
if (
initialVirtualHalRegistry.apiName !== "linuxcnc-wasm-virtual-hal-pin-registry" ||
!initialVirtualHalRegistry.byName["halui.machine.on"]?.writable ||
!initialVirtualHalRegistry.byName["motion.tooloffset.z"]?.writable ||
!api.getVirtualHalIntegrityReport().complete
) {
throw new Error(`simulation virtual HAL registry/integrity drift: ${JSON.stringify(initialVirtualHalRegistry)}`);
}
const diagnosticsArtifact = api.exportDiagnosticsArtifact();
if (
diagnosticsArtifact.apiName !== "real-browser-simulation-diagnostics-artifact" ||
diagnosticsArtifact.statusHistory.length === 0 ||
diagnosticsArtifact.limitsHome.source !== "unavailable" ||
diagnosticsArtifact.virtualHal.source !== "browser-virtual-hal" ||
diagnosticsArtifact.virtualHalSimulationReplacement?.ready !== true ||
diagnosticsArtifact.limitsHome.source !== "browser-virtual-hal" ||
diagnosticsArtifact.preview.renderer !== "threejs" ||
diagnosticsArtifact.toolTable.state !== "not-loaded"
) {
@@ -368,12 +409,12 @@
throw new Error(`simulation DRO source drift: ${JSON.stringify(droState)}`);
}
for (const field of ["distanceToGo", "workOffsetG54", "g92Offset", "toolLengthOffset"]) {
if (droState[field].x !== "n/a") {
throw new Error(`simulation DRO ${field} should remain explicit n/a when unavailable`);
if (droState[field].x !== "0.000") {
throw new Error(`simulation DRO ${field} should use zeroed AXIS default when unavailable`);
}
}
if (droState.velocity !== "n/a" || doc.querySelector("[data-dro-velocity]")?.textContent !== "n/a") {
throw new Error("simulation DRO velocity should remain explicit n/a when unavailable");
if (droState.velocity !== "0.000" || doc.querySelector("[data-dro-velocity]")?.textContent !== "0.000") {
throw new Error("simulation DRO velocity should use zeroed AXIS default when unavailable");
}
const previewExtents = doc.querySelector("[data-preview-extents]");
const previewOrigin = doc.querySelector("[data-preview-origin]");
@@ -619,6 +660,83 @@
if (doc.querySelector('[data-axis-panel="preview"]')?.hidden || doc.body.dataset.lastShortcut !== "F6 Preview") {
throw new Error("AXIS-style F6 shortcut did not switch to Preview");
}
doc.body.dispatchEvent(new frame.contentWindow.KeyboardEvent("keydown", { key: "F1", bubbles: true, cancelable: true }));
if (api.getVirtualHalState().task.state !== "ESTOP_RESET" || doc.body.dataset.virtualHalTaskState !== "ESTOP_RESET" || doc.body.dataset.lastShortcut !== "F1 ESTOP") {
throw new Error(`AXIS-style F1 did not reset browser virtual HAL ESTOP: ${JSON.stringify(api.getVirtualHalState())}`);
}
doc.body.dispatchEvent(new frame.contentWindow.KeyboardEvent("keydown", { key: "F2", bubbles: true, cancelable: true }));
if (api.getVirtualHalState().task.state !== "ON" || doc.body.dataset.virtualHalTaskState !== "ON" || doc.body.dataset.lastShortcut !== "F2 Power") {
throw new Error(`AXIS-style F2 did not power browser virtual HAL machine: ${JSON.stringify(api.getVirtualHalState())}`);
}
doc.body.dispatchEvent(new frame.contentWindow.KeyboardEvent("keydown", { key: "Home", ctrlKey: true, bubbles: true, cancelable: true }));
if (api.getVirtualHalLimitsHomeState().axes.x.homed !== "yes" || doc.body.dataset.lastShortcut !== "Ctrl-Home Home All") {
throw new Error(`AXIS-style Ctrl-Home did not home browser virtual HAL axes: ${JSON.stringify(api.getVirtualHalLimitsHomeState())}`);
}
doc.querySelector('[data-jog-increment]').value = "0.01 in";
doc.querySelector('[data-axis-jog="+"]')?.click();
if (api.getVirtualHalState().pins["jog.x"] !== true || api.getVirtualHalDroState().actual.x !== "0.010" || doc.querySelector('[data-dro-actual="x"]')?.textContent !== "0.010") {
throw new Error(`AXIS-style jog did not update browser virtual HAL DRO/pins: ${JSON.stringify(api.getVirtualHalState())}`);
}
doc.querySelector('[data-axis-spindle="cw"]')?.click();
doc.querySelector('[data-axis-coolant="flood"]').checked = true;
doc.querySelector('[data-axis-coolant="flood"]').dispatchEvent(new frame.contentWindow.Event("change", { bubbles: true }));
const virtualMachineStatus = api.getVirtualHalMachineStatusState();
if (
virtualMachineStatus.spindle.state !== "on" ||
virtualMachineStatus.spindle.direction !== "cw" ||
virtualMachineStatus.coolant.flood !== "on" ||
doc.querySelector('[data-axis-spindle-status]')?.dataset.state !== "ready" ||
doc.querySelector('[data-axis-coolant-status]')?.dataset.state !== "ready"
) {
throw new Error(`AXIS-style spindle/coolant did not update browser virtual HAL machine status: ${JSON.stringify(virtualMachineStatus)}`);
}
api.writeVirtualHalPin({ name: "spindle.0.speed-out", type: "HAL_FLOAT", value: 1500 });
api.applyVirtualHalPinUpdates([
{ name: "motion.tooloffset.z", type: "HAL_FLOAT", value: 1.5 },
{ name: "halui.tool.number", type: "HAL_U32", value: 8 },
]);
if (
api.readVirtualHalPin("spindle.0.speed-out")?.value !== 1500 ||
api.readVirtualHalPin("motion.tooloffset.z")?.value !== 1.5 ||
api.readVirtualHalPin("halui.tool.number")?.value !== 8 ||
api.getVirtualHalIntegrityReport().complete !== true
) {
throw new Error(`AXIS-style virtual HAL pin service did not update/read pins: ${JSON.stringify(api.getVirtualHalPinRegistry())}`);
}
const virtualHalcmdResult = api.executeVirtualHalcmd("loadrt trivkins\naddf motion-controller servo-thread\nsetp axis.x.pos-cmd 1.75\nshow pin axis.x.*");
if (
virtualHalcmdResult.apiName !== "linuxcnc-wasm-virtual-halcmd-result" ||
virtualHalcmdResult.ok !== true ||
!virtualHalcmdResult.output.includes("axis.x.pos-cmd") ||
api.readVirtualHalPin("axis.x.pos-cmd")?.value !== 1.75
) {
throw new Error(`AXIS-style virtual halcmd did not execute: ${JSON.stringify(virtualHalcmdResult)}`);
}
const motionControllerStep = api.stepVirtualHalMotionController({
target: { x: 2.25 },
maxVelocity: 10,
dt: 0.1,
cycleCount: 2,
});
if (
motionControllerStep.apiName !== "linuxcnc-wasm-virtual-motion-controller-step" ||
motionControllerStep.ok !== true ||
motionControllerStep.state.position.x <= 1.75
) {
throw new Error(`AXIS-style virtual motion controller did not step: ${JSON.stringify(motionControllerStep)}`);
}
const virtualHalReplacement = api.getVirtualHalSimulationReplacementReport();
if (
virtualHalReplacement.apiName !== "linuxcnc-wasm-virtual-hal-simulation-replacement-report" ||
virtualHalReplacement.ready !== true ||
virtualHalReplacement.replacements["linuxcnc-realtime-hal"].ready !== true ||
virtualHalReplacement.replacements.halcmd.ready !== true ||
virtualHalReplacement.replacements["motion-controller"].ready !== true ||
api.getVirtualRealtimeHalRuntimeReport().ready !== true ||
api.getVirtualHalSimulationRuntimeReport().replacesHostRuntimeForSimulation !== true
) {
throw new Error(`AXIS-style virtual HAL simulation replacement report drift: ${JSON.stringify(virtualHalReplacement)}`);
}
const urlView = api.applyAxisViewFromUrl("?axis_left=mdi&axis_right=dro");
if (
urlView.left !== "mdi" ||
@@ -899,7 +1017,7 @@
throw new Error("MDI history item did not restore text into the MDI pane");
}
api.clearMdiHistory();
if (api.getMdiHistory().length !== 0 || doc.body.dataset.mdiHistoryCount !== "0" || !doc.querySelector("[data-mdi-history-list]")?.textContent.includes("n/a")) {
if (api.getMdiHistory().length !== 0 || doc.body.dataset.mdiHistoryCount !== "0" || !doc.querySelector("[data-mdi-history-list]")?.textContent.includes("No MDI history")) {
throw new Error(`MDI history clear did not reset API/DOM state: ${JSON.stringify(api.getMdiHistory())}`);
}
@@ -1080,7 +1198,7 @@ TOOL_TABLE = tool.tbl
if (
loadedDiagnosticsArtifact.toolTable.state !== "loaded" ||
loadedDiagnosticsArtifact.toolTable.rows.length !== 1 ||
loadedDiagnosticsArtifact.limitsHome.axes.x.fault !== "n/a" ||
loadedDiagnosticsArtifact.limitsHome.axes.x.fault !== "none" ||
!loadedDiagnosticsArtifact.statusHistory.some(({ kind }) => kind === "session")
) {
throw new Error(`simulation diagnostics artifact did not include session/tool/limits state: ${JSON.stringify(loadedDiagnosticsArtifact)}`);
@@ -1157,7 +1275,7 @@ TOOL_TABLE = tool.tbl
if (api.getRunMode().mode !== "standalone" || doc.body.dataset.runMode !== "standalone") {
throw new Error("simulation run mode should stay standalone after disabled-session run");
}
if (api.getRunSummary().executionMode !== "linuxcnc-wasm" || doc.querySelector('[data-run-summary="session-ini"]')?.textContent !== "n/a") {
if (api.getRunSummary().executionMode !== "linuxcnc-wasm" || doc.querySelector('[data-run-summary="session-ini"]')?.textContent !== "-") {
throw new Error(`simulation run summary should return to standalone details: ${JSON.stringify(api.getRunSummary())}`);
}
if (api.getRunControlState().reason !== "Loaded machine session bypassed by Use Session toggle") {