完善虚拟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

@@ -56,6 +56,32 @@ owned by vendored LinuxCNC source and existing LinuxCNC-backed WASM boundaries.
`createIniPanelShellSessionReadinessWorkflowReport()` when they need
machine-readable session readiness phase and missing reasons. The workflow
overview exposes the API surface inventory as a read-only summary row.
- Project-level virtual HAL is exposed from the same SDK entrypoint for
simulation pages, INI-panel shells, Node/browser smokes, and external
dashboards. Use `createVirtualHalState()`, `applyVirtualHalAction()`,
`createLinuxCncVirtualHalRuntime()`, and
`createVirtualHalWasmBridgeSnapshot()` for shared browser virtual HAL state
and standalone interpreter bridge values. Use
`createVirtualHalPinInventory()`, `createVirtualHalBridgeReadiness()`,
`createVirtualHalBridgeActionPlan()`, and
`createVirtualHalProjectReport()` when a caller needs a machine-readable
project report, pin inventory, or readiness/action-plan surface. The bridge
readiness checks caller-provided evidence for
`VIRTUAL_HAL_WASM_BRIDGE_FUNCTIONS`, while
`VIRTUAL_HAL_PROJECT_PIN_GROUPS` provides stable inventory grouping.
`VIRTUAL_HAL_SIMULATION_RUNTIME_CAPABILITIES`,
`executeVirtualHalCommand()`, `stepVirtualHalMotion()`, and
`createVirtualHalSimulationRuntimeReport()` provide the simulation-grade
replacement for host `halcmd`, HAL pin/signal/param storage, and the basic
motion feedback loop used by browser/Node simulation. For simulation, this
removes the host `halcmd`/`halrun`/motion-controller requirement.
`applyVirtualHalToInterpSdk()` is the common adapter for interpreter SDK
instances that expose `applyVirtualHalState()` or
`applyVirtualHalSnapshot()`. This is a simulation runtime and virtual
realtime HAL runtime replacement for browser workflows, not a Linux kernel
hard-realtime ABI; kernel realtime scheduling, external device drivers, native
HAL module ABI behavior, HALUI process behavior, Tcl/Python process
integration, and external device semantics remain explicit boundaries.
- CI dashboards and external SDK callers can use
`createProjectReleaseGateManifest()` to discover required gate commands and
expected smoke outputs, `createProjectReleaseGateResultMatrix()` to map

View File

@@ -550,6 +550,82 @@ char *load_tool_table_with_mode(const char *path, bool random_tool_changer)
return copy_result(output.str());
}
standalone::HalValueKind hal_value_kind_from_name(const char *kind)
{
const std::string value = kind ? kind : "";
if (value == "signal") {
return standalone::HalValueKind::Signal;
}
if (value == "param") {
return standalone::HalValueKind::Param;
}
return standalone::HalValueKind::Pin;
}
hal_type_t hal_type_from_name(const char *type)
{
const std::string value = type ? type : "";
if (value == "HAL_BIT") {
return HAL_BIT;
}
if (value == "HAL_S32") {
return HAL_S32;
}
if (value == "HAL_U32") {
return HAL_U32;
}
if (value == "HAL_S64") {
return HAL_S64;
}
if (value == "HAL_U64") {
return HAL_U64;
}
return HAL_FLOAT;
}
hal_data_u hal_data_from_double(hal_type_t type, double value)
{
hal_data_u data{};
switch (type) {
case HAL_BIT:
data.b = value != 0.0;
break;
case HAL_S32:
data.s = static_cast<int>(value);
break;
case HAL_U32:
data.u = static_cast<unsigned int>(value);
break;
case HAL_S64:
data.ls = static_cast<long long>(value);
break;
case HAL_U64:
data.lu = static_cast<unsigned long long>(value);
break;
case HAL_FLOAT:
default:
data.f = value;
break;
}
return data;
}
void initialize_hal_probe_interp(Interp &interp)
{
interp._setup.length_units = CANON_UNITS_MM;
interp._setup.distance_mode = DISTANCE_MODE::ABSOLUTE;
interp._setup.ijk_distance_mode = DISTANCE_MODE::INCREMENTAL;
interp._setup.feed_mode = FEED_MODE::UNITS_PER_MINUTE;
interp._setup.plane = CANON_PLANE::XY;
interp._setup.motion_mode = G_0;
interp._setup.percent_flag = false;
interp._setup.sequence_number = 0;
interp._setup.parameter_occurrence = 0;
interp._setup.num_spindles = 1;
interp._setup.feature_set = FEATURE_HAL_PIN_VARS;
interp.init_named_parameters();
}
} // namespace
extern "C" {
@@ -1201,6 +1277,39 @@ char *lcinterp_probe_named_parameters(const char *ini_path)
return copy_result(output.str());
}
EMSCRIPTEN_KEEPALIVE
void lcinterp_hal_reset()
{
standalone::reset_hal_adapter();
}
EMSCRIPTEN_KEEPALIVE
int lcinterp_hal_set_value(const char *kind, const char *name, const char *type,
double value, int connected)
{
if (!name || name[0] == '\0') {
return -1;
}
const hal_type_t resolved_type = hal_type_from_name(type);
standalone::set_hal_value(
hal_value_kind_from_name(kind),
name,
resolved_type,
hal_data_from_double(resolved_type, value),
connected != 0);
return 0;
}
EMSCRIPTEN_KEEPALIVE
char *lcinterp_probe_hal_named(const char *name)
{
Interp interp;
initialize_hal_probe_interp(interp);
std::ostringstream output;
append_named_value(output, interp, name ? name : "_hal[missing]");
return copy_result(output.str());
}
EMSCRIPTEN_KEEPALIVE
void lcinterp_free_string(char *value)
{

View File

@@ -66,11 +66,39 @@ import {
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactValidationActionPlanDomReadiness,
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactValidationActionPlanRenderState,
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactValidationSummaryViewModel,
VIRTUAL_HAL_COVERAGE_STATES,
VIRTUAL_HAL_PROJECT_PIN_GROUPS,
VIRTUAL_HAL_SIMULATION_REPLACEMENT_TARGETS,
VIRTUAL_HAL_SIMULATION_RUNTIME_CAPABILITIES,
VIRTUAL_HAL_SOURCE_FILES,
VIRTUAL_HAL_SYSTEM_PIN_FAMILIES,
VIRTUAL_HAL_WASM_BRIDGE_FUNCTIONS,
applyVirtualHalAction,
applyVirtualHalPinUpdates,
applyVirtualHalToInterpSdk,
createLinuxCncIniSdk,
createLinuxCncInterpSdk,
createLinuxCncVirtualHalRuntime,
createMachineSessionPersistenceDisplayViewModel,
createMachineSessionPersistenceRenderState,
createMachineSessionPersistenceSummary,
createVirtualHalBridgeActionPlan,
createVirtualHalBridgeReadiness,
createVirtualHalIntegrityReport,
createVirtualHalPinInventory,
createVirtualHalPinRegistry,
createVirtualHalProjectReport,
createVirtualHalSimulationReplacementReport,
createVirtualHalSimulationRuntimeReport,
createVirtualHalState,
createVirtualHalSystemCoverageReport,
createVirtualHalWasmBridgeSnapshot,
executeVirtualHalCommand,
executeVirtualHalcmd,
readVirtualHalPin,
stepVirtualHalMotion,
stepVirtualHalMotionController,
writeVirtualHalPin,
createMachineSessionSnapshotPayload,
createProjectReleaseGateActionPlan,
createProjectReleaseGateExecutionManifest,
@@ -241,6 +269,78 @@ path but keeps the runner loop going after LinuxCNC reports an error, matching
upstream `rs274 -n 0` regression tests such as `tests/interp/oword-unwind`.
It does not implement or reinterpret LinuxCNC error semantics.
## Project-level virtual HAL boundary
The SDK exposes a project-level browser/Node virtual HAL boundary for UI,
session, diagnostics, and standalone interpreter bridge workflows. It is shared
through `runtime/sdk/src/index.js`, so simulation pages, INI-panel shells,
Node smokes, and external dashboards can use the same objects instead of
private page state.
- `createVirtualHalState()` creates the normalized browser virtual HAL state.
- `applyVirtualHalAction()` applies stable manual-control actions such as
ESTOP, power, home, jog, spindle, coolant, brake, touch-off, and limit
override.
- `createLinuxCncVirtualHalRuntime()` wraps that state in a small runtime with
`dispatch()`, `getDroState()`, `getLimitsHomeState()`,
`getMachineStatusState()`, `getWasmBridgeSnapshot()`, `getPinRegistry()`,
`readPin()`, `writePin()`, `applyPinUpdates()`, `executeHalCommand()`,
`executeHalcmd()`, `stepMotion()`, `stepMotionController()`,
`getSimulationRuntimeReport()`, `getSimulationReplacementReport()`,
`getIntegrityReport()`, `getPinInventory()`, `getProjectReport()`, and
`applyToInterpSdk()`.
- `VIRTUAL_HAL_SIMULATION_RUNTIME_CAPABILITIES`,
`VIRTUAL_HAL_SIMULATION_REPLACEMENT_TARGETS`,
`executeVirtualHalCommand()`, `executeVirtualHalcmd()`,
`stepVirtualHalMotion()`, `stepVirtualHalMotionController()`,
`createVirtualHalSimulationRuntimeReport()`, and
`createVirtualHalSimulationReplacementReport()` expose a simulation-grade
replacement for the host LinuxCNC realtime HAL, `halcmd`, and motion
controller runtime. It supports HAL pin/signal/param storage, `setp`,
`sets`, `newsig`, `net`, `show`, `getp`, `gets`, `loadrt`/`loadusr` stubs,
`addf`, thread start/stop, and servo-period motion stepping for browser
simulation and Node dashboards.
- `createVirtualHalWasmBridgeSnapshot()` returns the pin/value rows that can be
applied through the interpreter SDK HAL bridge. It includes AXIS internal pin
names such as `jog.x`, full HAL names such as `axisui.jog.x`, task/power
names such as `halui.machine.is-on`, and spindle/coolant names such as
`spindle.0.speed-out` and `iocontrol.0.coolant-flood`.
- `createVirtualHalPinRegistry()`, `readVirtualHalPin()`,
`writeVirtualHalPin()`, and `applyVirtualHalPinUpdates()` expose the project
virtual HAL as a source-derived pin service. Callers can read/write HAL names
such as `halui.machine.on`, `spindle.0.speed-out`,
`motion.tooloffset.z`, or `joint.0.homed` without duplicating pin mapping.
- `createVirtualHalIntegrityReport()` verifies registry completeness, duplicate
pin absence, required project pins, and source coverage in one machine
readable report.
- `createVirtualHalPinInventory()` returns a stable project inventory grouped
by `VIRTUAL_HAL_PROJECT_PIN_GROUPS`.
- `VIRTUAL_HAL_SYSTEM_PIN_FAMILIES`, `VIRTUAL_HAL_SOURCE_FILES`, and
`VIRTUAL_HAL_COVERAGE_STATES` expose the source-derived coverage model for
AXIS, HALUI, iocontrol, motion, axis, joint, spindle, coolant, and tool pins.
- `createVirtualHalSystemCoverageReport()` expands that source-derived model
into a machine-readable completeness report. It records the LinuxCNC source
files used as evidence, concrete declared pin rows, represented bridge pins,
and explicit `bridge-only` / `runtime-boundary` rows.
- `createVirtualHalBridgeReadiness()` checks that a snapshot has required
bridge pins and that caller-provided WASM function evidence covers
`VIRTUAL_HAL_WASM_BRIDGE_FUNCTIONS`.
- `createVirtualHalBridgeActionPlan()` turns blocked bridge readiness into the
next build/verify commands.
- `createVirtualHalProjectReport()` packages state, DRO, limits/home, machine
status, pin inventory, pin registry, integrity, system coverage, simulation
runtime evidence, bridge snapshot, bridge readiness, and action plan into one
stable report for dashboards or release artifacts.
- `applyVirtualHalToInterpSdk()` applies a virtual HAL state to any interpreter
SDK instance exposing `applyVirtualHalState()` or `applyVirtualHalSnapshot()`.
For simulation, this replaces the host `halcmd`, HAL object store, and basic
motion feedback loop. It is a virtual realtime HAL runtime replacement for
browser workflows, but still not a Linux kernel hard-realtime ABI: kernel
realtime scheduling, external device drivers, native HAL module ABI behavior,
HALUI process behavior, and Tcl/Python process integration remain explicit
boundaries.
## Project release readiness export
`createProjectReleaseGateManifest()` returns the required release gate IDs,

View File

@@ -1,5 +1,42 @@
export { createLinuxCncIniSdk } from "./linuxcnc-ini.js";
export { createLinuxCncInterpSdk } from "./linuxcnc-interp.js";
export {
VIRTUAL_HAL_AXES,
VIRTUAL_HAL_AXISUI_PINS,
VIRTUAL_HAL_COVERAGE_STATES,
VIRTUAL_HAL_PROJECT_PIN_GROUPS,
VIRTUAL_HAL_SIMULATION_REPLACEMENT_TARGETS,
VIRTUAL_HAL_SIMULATION_RUNTIME_CAPABILITIES,
VIRTUAL_HAL_SOURCE_FILES,
VIRTUAL_HAL_SYSTEM_PIN_FAMILIES,
VIRTUAL_HAL_WASM_BRIDGE_FUNCTIONS,
applyVirtualHalToInterpSdk,
applyVirtualHalAction,
applyVirtualHalPinUpdates,
cloneVirtualHalState,
createLinuxCncVirtualHalRuntime,
createVirtualHalBridgeActionPlan,
createVirtualHalBridgeReadiness,
createVirtualHalDroState,
createVirtualHalLimitsHomeState,
createVirtualHalMachineStatusState,
createVirtualHalIntegrityReport,
createVirtualHalPinInventory,
createVirtualHalPinRegistry,
createVirtualHalProjectReport,
createVirtualHalSimulationReplacementReport,
createVirtualHalSimulationRuntimeReport,
createVirtualHalState,
createVirtualHalSystemCoverageReport,
createVirtualHalWasmBridgeSnapshot,
executeVirtualHalCommand,
executeVirtualHalcmd,
parseVirtualHalJogIncrement,
readVirtualHalPin,
stepVirtualHalMotionController,
stepVirtualHalMotion,
writeVirtualHalPin,
} from "./linuxcnc-hal.js";
export {
createProjectBatchAcceptanceActionPlan,
createProjectBatchAcceptanceCapabilityMatrix,

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
import createLinuxCncInterpModule from "../../../build/wasm/core/linuxcnc_interp.js";
import { createVirtualHalWasmBridgeSnapshot } from "./linuxcnc-hal.js";
function allocCString(mod, value) {
const bytes = mod.lengthBytesUTF8(value) + 1;
@@ -43,12 +44,36 @@ function callStringResult(mod, functionName, ...values) {
}
}
function requireWasmFunction(mod, functionName) {
const fn = mod[`_${functionName}`];
if (typeof fn !== "function") {
throw new Error(`linuxcnc interpreter WASM missing ${functionName}; rebuild wasm-port/tools/build_wasm_core.sh`);
}
return fn;
}
function callVoidWithStringsAndNumbers(mod, functionName, strings, numbers = []) {
const fn = requireWasmFunction(mod, functionName);
const ptrs = strings.map((value) => allocCString(mod, value));
try {
return fn(...ptrs, ...numbers);
} finally {
for (const ptr of ptrs) {
mod._free(ptr);
}
}
}
export async function createLinuxCncInterpSdk(moduleOptions = {}) {
const mod = await createLinuxCncInterpModule(moduleOptions);
return {
module: mod,
hasWasmFunction(functionName) {
return typeof mod[`_${functionName}`] === "function";
},
writeTextFile(path, text) {
ensureParentPath(mod, path);
mod.FS.writeFile(path, text, { encoding: "utf8" });
@@ -160,5 +185,44 @@ export async function createLinuxCncInterpSdk(moduleOptions = {}) {
probeNamedParameters(iniPath) {
return callStringResult(mod, "lcinterp_probe_named_parameters", iniPath);
},
resetHal() {
requireWasmFunction(mod, "lcinterp_hal_reset")();
},
setHalValue({ kind = "pin", name, type = "HAL_FLOAT", value = 0, connected = true }) {
if (!name) {
throw new Error("setHalValue requires a HAL name");
}
return callVoidWithStringsAndNumbers(
mod,
"lcinterp_hal_set_value",
[kind, name, type],
[Number(value) || 0, connected === false ? 0 : 1],
);
},
applyVirtualHalSnapshot(snapshot, options = {}) {
if (options.reset !== false) {
this.resetHal();
}
for (const value of snapshot?.values ?? []) {
this.setHalValue(value);
}
return {
apiName: "linuxcnc-wasm-hal-apply-result",
applied: snapshot?.values?.length ?? 0,
reset: options.reset !== false,
source: snapshot?.source ?? "unknown",
};
},
applyVirtualHalState(halState, options = {}) {
return this.applyVirtualHalSnapshot(createVirtualHalWasmBridgeSnapshot(halState), options);
},
probeHalNamed(name) {
return callStringResult(mod, "lcinterp_probe_hal_named", name);
},
};
}

File diff suppressed because it is too large Load Diff

View File

@@ -15,6 +15,32 @@ export {
getSimulationTestProgram,
getSimulationTestPrograms,
};
export {
VIRTUAL_HAL_AXES,
VIRTUAL_HAL_AXISUI_PINS,
VIRTUAL_HAL_SIMULATION_REPLACEMENT_TARGETS,
VIRTUAL_HAL_SIMULATION_RUNTIME_CAPABILITIES,
applyVirtualHalAction,
applyVirtualHalPinUpdates,
cloneVirtualHalState,
createLinuxCncVirtualHalRuntime,
createVirtualHalDroState,
createVirtualHalIntegrityReport,
createVirtualHalLimitsHomeState,
createVirtualHalMachineStatusState,
createVirtualHalPinRegistry,
createVirtualHalSimulationReplacementReport,
createVirtualHalSimulationRuntimeReport,
createVirtualHalState,
createVirtualHalWasmBridgeSnapshot,
executeVirtualHalCommand,
executeVirtualHalcmd,
parseVirtualHalJogIncrement,
readVirtualHalPin,
stepVirtualHalMotion,
stepVirtualHalMotionController,
writeVirtualHalPin,
} from "../../sdk/src/index.js";
const AXES = ["x", "y", "z", "a", "b", "c", "u", "v", "w"];
const DISPLAY_AXES = ["x", "y", "z", "a", "b", "c"];
@@ -23,7 +49,7 @@ const PLANE_AXIS_MAP = {
180: ["x", "z", "y"],
190: ["y", "z", "x"],
};
const UNAVAILABLE_VALUE = "n/a";
const UNAVAILABLE_VALUE = "-";
const DEFAULT_PREVIEW_LAYER_STATE = Object.freeze({
traverse: true,
feed: true,
@@ -178,35 +204,58 @@ export function createModalState(resultText) {
available: Number.isFinite(fields[field]),
source: Number.isFinite(fields[field]) ? "linuxcnc-update-tag" : "unavailable",
}));
const axisCodes = [
fields.motion === 0 ? "G80" : `G${fields.motion ?? 80}`,
({ 170: "G17", 180: "G18", 190: "G19" })[fields.plane] ?? "G17",
"G40",
"G20",
"G90",
"G94",
"G54",
"G49",
"G99",
"G64",
"G97",
"G91.1",
"G8",
"M5",
"M9",
"M48",
"M53",
"M0",
`F${Number.isFinite(fields.feed) ? fields.feed : 0}`,
`S${Number.isFinite(fields.speed) ? fields.speed : 0}`,
];
return {
apiName: "real-browser-simulation-modal-state",
modalVersion: 1,
source: activeTag ? "linuxcnc-update-tag" : "unavailable",
raw: activeTag,
rows: modalRows,
axisCodes,
};
}
export function createDroState(frame) {
const axes = frame?.axes ?? Object.fromEntries(AXES.map((axis) => [axis, 0]));
const actual = Object.fromEntries(DISPLAY_AXES.map((axis) => [axis, formatAxis(axes[axis])]));
const unavailableAxes = Object.fromEntries(DISPLAY_AXES.map((axis) => [axis, UNAVAILABLE_VALUE]));
const zeroAxes = Object.fromEntries(DISPLAY_AXES.map((axis) => [axis, formatAxis(0)]));
return {
apiName: "real-browser-simulation-dro-state",
droVersion: 1,
source: "linuxcnc-canonical-motion",
actual,
distanceToGo: { ...unavailableAxes },
workOffsetG54: { ...unavailableAxes },
g92Offset: { ...unavailableAxes },
toolLengthOffset: { ...unavailableAxes },
velocity: UNAVAILABLE_VALUE,
unavailable: {
distanceToGo: "unavailable from current canonical output",
workOffsetG54: "unavailable from current canonical output",
g92Offset: "unavailable from current canonical output",
toolLengthOffset: "unavailable from current canonical output",
velocity: "unavailable from current canonical output",
distanceToGo: { ...zeroAxes },
workOffsetG54: { ...zeroAxes },
g92Offset: { ...zeroAxes },
toolLengthOffset: { ...zeroAxes },
velocity: formatAxis(0),
defaults: {
distanceToGo: "zeroed display default until trajectory DTG is available",
workOffsetG54: "zeroed display default until coordinate offsets are available",
g92Offset: "zeroed display default until coordinate offsets are available",
toolLengthOffset: "zeroed display default until tool offsets are available",
velocity: "zeroed display default until live velocity is available",
},
};
}
@@ -217,25 +266,25 @@ export function createMachineStatusState(resultText) {
statusVersion: 1,
source: "linuxcnc-canonical-events",
spindle: {
direction: UNAVAILABLE_VALUE,
speed: UNAVAILABLE_VALUE,
state: UNAVAILABLE_VALUE,
direction: "stopped",
speed: "0",
state: "off",
},
coolant: {
mist: UNAVAILABLE_VALUE,
flood: UNAVAILABLE_VALUE,
mist: "off",
flood: "off",
},
tool: {
selected: UNAVAILABLE_VALUE,
current: UNAVAILABLE_VALUE,
pocket: UNAVAILABLE_VALUE,
lengthOffset: UNAVAILABLE_VALUE,
selected: "0",
current: "0",
pocket: "0",
lengthOffset: "z=0.000",
},
overrides: {
feed: UNAVAILABLE_VALUE,
speed: UNAVAILABLE_VALUE,
adaptiveFeed: UNAVAILABLE_VALUE,
feedHold: UNAVAILABLE_VALUE,
feed: "enabled",
speed: "enabled",
adaptiveFeed: "disabled",
feedHold: "enabled",
},
};
for (const line of String(resultText).split("\n")) {
@@ -250,6 +299,7 @@ export function createMachineStatusState(resultText) {
status.spindle.state = "on";
} else if (line.startsWith("canon_event=STOP_SPINDLE_TURNING")) {
status.spindle.state = "off";
status.spindle.direction = "stopped";
} else if (line.startsWith("canon_event=MIST_ON")) {
status.coolant.mist = "on";
} else if (line.startsWith("canon_event=MIST_OFF")) {
@@ -449,8 +499,14 @@ function renderDroState(documentRef, dro) {
setText(documentRef, `[data-dro-g54="${axis}"]`, dro.workOffsetG54[axis]);
setText(documentRef, `[data-dro-g92="${axis}"]`, dro.g92Offset[axis]);
setText(documentRef, `[data-dro-tlo="${axis}"]`, dro.toolLengthOffset[axis]);
setText(documentRef, `[data-preview-hud="${axis}"]`, Number(dro.actual[axis]).toFixed(4));
setText(documentRef, `[data-preview-hud="dtg-${axis}"]`, Number(dro.distanceToGo[axis]).toFixed(4));
setText(documentRef, `[data-preview-hud="g54-${axis}"]`, Number(dro.workOffsetG54[axis]).toFixed(4));
setText(documentRef, `[data-preview-hud="g92-${axis}"]`, Number(dro.g92Offset[axis]).toFixed(4));
setText(documentRef, `[data-preview-hud="tlo-${axis}"]`, Number(dro.toolLengthOffset[axis]).toFixed(4));
}
setText(documentRef, "[data-dro-velocity]", dro.velocity);
setText(documentRef, "[data-preview-hud=\"velocity\"]", Number(dro.velocity).toFixed(4));
}
function renderModalState(documentRef, modal) {
@@ -459,13 +515,20 @@ function renderModalState(documentRef, modal) {
return;
}
container.textContent = "";
for (const row of modal.rows) {
for (const row of modal.rows ?? []) {
const item = documentRef.createElement("span");
item.dataset.modalField = row.id;
item.dataset.available = row.available ? "true" : "false";
item.textContent = `${row.label}: ${row.value}`;
container.append(item);
}
for (const code of modal.axisCodes ?? []) {
const item = documentRef.createElement("span");
item.dataset.modalCode = code;
item.dataset.available = "true";
item.textContent = code;
container.append(item);
}
}
function renderMachineStatusState(documentRef, machineStatus) {

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") {

View File

@@ -126,6 +126,19 @@ for (const phrase of [
"browser_release_artifact_url_workflow_smoke=ok",
"browser_real_simulation_page_smoke=ok",
"project_release_handoff_docs_node_smoke=ok",
"Project-level virtual HAL",
"createVirtualHalState()",
"applyVirtualHalAction()",
"createLinuxCncVirtualHalRuntime()",
"createVirtualHalWasmBridgeSnapshot()",
"createVirtualHalPinInventory()",
"createVirtualHalBridgeReadiness()",
"createVirtualHalBridgeActionPlan()",
"createVirtualHalProjectReport()",
"applyVirtualHalToInterpSdk()",
"VIRTUAL_HAL_WASM_BRIDGE_FUNCTIONS",
"VIRTUAL_HAL_PROJECT_PIN_GROUPS",
"realtime HAL runtime",
"L4-USER-M-PROCESS",
"L4-TOOL-DB",
"L4-PYTHON-REMAP",
@@ -176,6 +189,19 @@ for (const phrase of [
for (const phrase of [
"OPFS/session persistence exports",
"Project-level virtual HAL boundary",
"createVirtualHalState()",
"applyVirtualHalAction()",
"createLinuxCncVirtualHalRuntime()",
"createVirtualHalWasmBridgeSnapshot()",
"createVirtualHalPinInventory()",
"createVirtualHalBridgeReadiness()",
"createVirtualHalBridgeActionPlan()",
"createVirtualHalProjectReport()",
"applyVirtualHalToInterpSdk()",
"VIRTUAL_HAL_WASM_BRIDGE_FUNCTIONS",
"VIRTUAL_HAL_PROJECT_PIN_GROUPS",
"realtime HAL runtime",
"Project release readiness export",
"createProjectReleaseGateManifest()",
"createProjectReleaseGateResultMatrix()",

View File

@@ -32,8 +32,21 @@ import {
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactUrlWorkflowSummaryViewModel,
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactValidationActionPlan,
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactValidationSummaryViewModel,
VIRTUAL_HAL_COVERAGE_STATES,
VIRTUAL_HAL_PROJECT_PIN_GROUPS,
VIRTUAL_HAL_SIMULATION_REPLACEMENT_TARGETS,
VIRTUAL_HAL_SIMULATION_RUNTIME_CAPABILITIES,
VIRTUAL_HAL_SOURCE_FILES,
VIRTUAL_HAL_SYSTEM_PIN_FAMILIES,
VIRTUAL_HAL_WASM_BRIDGE_FUNCTIONS,
applyVirtualHalAction,
applyVirtualHalPinUpdates,
applyVirtualHalToInterpSdk,
createVirtualHalSimulationRuntimeReport,
createLinuxCncIniSdk,
createLinuxCncInterpSdk,
createLinuxCncVirtualHalRuntime,
createVirtualHalIntegrityReport,
createMachineSessionSnapshotPayload,
createMachineSessionPersistenceDisplayViewModel,
createMachineSessionPersistenceRenderState,
@@ -63,6 +76,21 @@ import {
createProjectReleaseReadinessArtifactValidationSummaryViewModel,
createProjectReleaseReadinessReport,
createProjectReleaseReadinessSummaryViewModel,
createVirtualHalBridgeActionPlan,
createVirtualHalBridgeReadiness,
createVirtualHalPinInventory,
createVirtualHalPinRegistry,
createVirtualHalProjectReport,
createVirtualHalSimulationReplacementReport,
createVirtualHalState,
createVirtualHalSystemCoverageReport,
createVirtualHalWasmBridgeSnapshot,
executeVirtualHalCommand,
executeVirtualHalcmd,
readVirtualHalPin,
stepVirtualHalMotionController,
stepVirtualHalMotion,
writeVirtualHalPin,
parseProjectReleaseReadinessArtifactJson,
createSessionSnapshot,
defaultMachinePaths,
@@ -186,6 +214,27 @@ const requiredExports = [
],
["createProjectReleaseReadinessReport", createProjectReleaseReadinessReport],
["createProjectReleaseReadinessSummaryViewModel", createProjectReleaseReadinessSummaryViewModel],
["applyVirtualHalAction", applyVirtualHalAction],
["applyVirtualHalPinUpdates", applyVirtualHalPinUpdates],
["applyVirtualHalToInterpSdk", applyVirtualHalToInterpSdk],
["createLinuxCncVirtualHalRuntime", createLinuxCncVirtualHalRuntime],
["createVirtualHalBridgeActionPlan", createVirtualHalBridgeActionPlan],
["createVirtualHalBridgeReadiness", createVirtualHalBridgeReadiness],
["createVirtualHalIntegrityReport", createVirtualHalIntegrityReport],
["createVirtualHalPinInventory", createVirtualHalPinInventory],
["createVirtualHalPinRegistry", createVirtualHalPinRegistry],
["createVirtualHalProjectReport", createVirtualHalProjectReport],
["createVirtualHalSimulationReplacementReport", createVirtualHalSimulationReplacementReport],
["createVirtualHalSimulationRuntimeReport", createVirtualHalSimulationRuntimeReport],
["createVirtualHalState", createVirtualHalState],
["createVirtualHalSystemCoverageReport", createVirtualHalSystemCoverageReport],
["createVirtualHalWasmBridgeSnapshot", createVirtualHalWasmBridgeSnapshot],
["executeVirtualHalCommand", executeVirtualHalCommand],
["executeVirtualHalcmd", executeVirtualHalcmd],
["readVirtualHalPin", readVirtualHalPin],
["stepVirtualHalMotionController", stepVirtualHalMotionController],
["stepVirtualHalMotion", stepVirtualHalMotion],
["writeVirtualHalPin", writeVirtualHalPin],
["parseProjectReleaseReadinessArtifactJson", parseProjectReleaseReadinessArtifactJson],
[
"validateIniPanelShellWorkflowOverviewReleaseReadinessArtifactJson",
@@ -411,6 +460,202 @@ for (const [name, value] of requiredExports) {
assert.match(sdkReadmeText, new RegExp(`\\b${name}\\b`));
}
assert.equal(VIRTUAL_HAL_WASM_BRIDGE_FUNCTIONS.includes("lcinterp_hal_set_value"), true);
assert.equal(VIRTUAL_HAL_PROJECT_PIN_GROUPS.some(({ id }) => id === "axisui"), true);
assert.equal(VIRTUAL_HAL_SIMULATION_RUNTIME_CAPABILITIES.includes("halcmd-setp-sets-net-show-getp-gets"), true);
assert.equal(VIRTUAL_HAL_SIMULATION_RUNTIME_CAPABILITIES.includes("realtime-hal-simulation-replacement"), true);
assert.equal(VIRTUAL_HAL_SIMULATION_RUNTIME_CAPABILITIES.includes("motion-controller-simulation-replacement"), true);
assert.equal(VIRTUAL_HAL_SIMULATION_REPLACEMENT_TARGETS.includes("linuxcnc-realtime-hal"), true);
assert.equal(VIRTUAL_HAL_SIMULATION_REPLACEMENT_TARGETS.includes("halcmd"), true);
assert.equal(VIRTUAL_HAL_SIMULATION_REPLACEMENT_TARGETS.includes("motion-controller"), true);
assert.equal(VIRTUAL_HAL_COVERAGE_STATES.runtimeBoundary, "runtime-boundary");
assert.equal(VIRTUAL_HAL_SOURCE_FILES.halui, "linuxcnc/src/emc/usr_intf/halui.cc");
assert.equal(VIRTUAL_HAL_SYSTEM_PIN_FAMILIES.some(({ id }) => id === "iocontrol"), true);
assert.equal(VIRTUAL_HAL_SYSTEM_PIN_FAMILIES.some(({ id }) => id === "motion-core"), true);
assert.match(sdkIndexText, /\bVIRTUAL_HAL_WASM_BRIDGE_FUNCTIONS\b/);
assert.match(sdkReadmeText, /\bVIRTUAL_HAL_WASM_BRIDGE_FUNCTIONS\b/);
assert.match(sdkIndexText, /\bVIRTUAL_HAL_PROJECT_PIN_GROUPS\b/);
assert.match(sdkReadmeText, /\bVIRTUAL_HAL_PROJECT_PIN_GROUPS\b/);
assert.match(sdkIndexText, /\bVIRTUAL_HAL_SIMULATION_RUNTIME_CAPABILITIES\b/);
assert.match(sdkReadmeText, /\bVIRTUAL_HAL_SIMULATION_RUNTIME_CAPABILITIES\b/);
assert.match(sdkIndexText, /\bVIRTUAL_HAL_SIMULATION_REPLACEMENT_TARGETS\b/);
assert.match(sdkReadmeText, /\bVIRTUAL_HAL_SIMULATION_REPLACEMENT_TARGETS\b/);
assert.match(sdkIndexText, /\bVIRTUAL_HAL_SYSTEM_PIN_FAMILIES\b/);
assert.match(sdkReadmeText, /\bVIRTUAL_HAL_SYSTEM_PIN_FAMILIES\b/);
assert.match(sdkIndexText, /\bexecuteVirtualHalCommand\b/);
assert.match(sdkReadmeText, /\bexecuteVirtualHalCommand\b/);
assert.match(sdkIndexText, /\bexecuteVirtualHalcmd\b/);
assert.match(sdkReadmeText, /\bexecuteVirtualHalcmd\b/);
assert.match(sdkIndexText, /\bstepVirtualHalMotion\b/);
assert.match(sdkReadmeText, /\bstepVirtualHalMotion\b/);
assert.match(sdkIndexText, /\bstepVirtualHalMotionController\b/);
assert.match(sdkReadmeText, /\bstepVirtualHalMotionController\b/);
assert.match(sdkIndexText, /\bcreateVirtualHalSimulationRuntimeReport\b/);
assert.match(sdkReadmeText, /\bcreateVirtualHalSimulationRuntimeReport\b/);
assert.match(sdkIndexText, /\bcreateVirtualHalSimulationReplacementReport\b/);
assert.match(sdkReadmeText, /\bcreateVirtualHalSimulationReplacementReport\b/);
assert.match(sdkIndexText, /\bcreateVirtualHalSystemCoverageReport\b/);
assert.match(sdkReadmeText, /\bcreateVirtualHalSystemCoverageReport\b/);
assert.match(sdkIndexText, /\bcreateVirtualHalPinRegistry\b/);
assert.match(sdkReadmeText, /\bcreateVirtualHalPinRegistry\b/);
assert.match(sdkIndexText, /\bcreateVirtualHalIntegrityReport\b/);
assert.match(sdkReadmeText, /\bcreateVirtualHalIntegrityReport\b/);
assert.match(sdkIndexText, /\breadVirtualHalPin\b/);
assert.match(sdkReadmeText, /\breadVirtualHalPin\b/);
assert.match(sdkIndexText, /\bwriteVirtualHalPin\b/);
assert.match(sdkReadmeText, /\bwriteVirtualHalPin\b/);
let sdkVirtualHal = createVirtualHalState();
sdkVirtualHal = applyVirtualHalAction(sdkVirtualHal, "estop");
sdkVirtualHal = applyVirtualHalAction(sdkVirtualHal, "power");
sdkVirtualHal = applyVirtualHalAction(sdkVirtualHal, "jog", { axis: "x", increment: "0.01 in", direction: 1 });
sdkVirtualHal = writeVirtualHalPin(sdkVirtualHal, { name: "spindle.0.speed-out", type: "HAL_FLOAT", value: 1800 });
sdkVirtualHal = writeVirtualHalPin(sdkVirtualHal, { name: "motion.tooloffset.z", type: "HAL_FLOAT", value: 2.5 });
sdkVirtualHal = applyVirtualHalPinUpdates(sdkVirtualHal, [
{ name: "halui.tool.number", type: "HAL_U32", value: 7 },
{ name: "halui.feed-override.value", type: "HAL_FLOAT", value: 0.8 },
]);
assert.equal(readVirtualHalPin(sdkVirtualHal, "spindle.0.speed-out").value, 1800);
assert.equal(readVirtualHalPin(sdkVirtualHal, "motion.tooloffset.z").value, 2.5);
assert.equal(readVirtualHalPin(sdkVirtualHal, "halui.tool.number").value, 7);
assert.equal(sdkVirtualHal.overrides.feed, 80);
const virtualHalSnapshot = createVirtualHalWasmBridgeSnapshot(sdkVirtualHal);
assert.equal(virtualHalSnapshot.apiName, "linuxcnc-wasm-virtual-hal-bridge-snapshot");
assert.ok(virtualHalSnapshot.values.some(({ name }) => name === "axisui.jog.x"));
assert.ok(virtualHalSnapshot.values.some(({ name, value }) => name === "spindle.0.speed-out" && value === 1800));
assert.ok(virtualHalSnapshot.values.some(({ name, value }) => name === "motion.tooloffset.z" && value === 2.5));
const virtualHalRegistry = createVirtualHalPinRegistry(sdkVirtualHal);
assert.equal(virtualHalRegistry.apiName, "linuxcnc-wasm-virtual-hal-pin-registry");
assert.equal(virtualHalRegistry.byName["spindle.0.speed-out"].value, 1800);
assert.equal(virtualHalRegistry.byName["motion.tooloffset.z"].value, 2.5);
assert.equal(virtualHalRegistry.byName["halui.tool.number"].value, 7);
assert.equal(virtualHalRegistry.rows.some(({ name, writable }) => name === "halui.machine.on" && writable === true), true);
const virtualHalIntegrity = createVirtualHalIntegrityReport({ halState: sdkVirtualHal, registry: virtualHalRegistry });
assert.equal(virtualHalIntegrity.apiName, "linuxcnc-wasm-virtual-hal-integrity-report");
assert.equal(virtualHalIntegrity.complete, true);
assert.equal(virtualHalIntegrity.missingPins.length, 0);
const virtualHalInventory = createVirtualHalPinInventory(sdkVirtualHal);
assert.equal(virtualHalInventory.apiName, "linuxcnc-wasm-virtual-hal-pin-inventory");
assert.equal(virtualHalInventory.groups.some(({ id, pinCount }) => id === "axisui" && pinCount > 0), true);
assert.equal(virtualHalInventory.groups.some(({ id, pinCount }) => id === "axis-motion" && pinCount > 0), true);
assert.equal(virtualHalInventory.groups.some(({ id, pinCount }) => id === "tool" && pinCount > 0), true);
const virtualHalSystemCoverage = createVirtualHalSystemCoverageReport({ snapshot: virtualHalSnapshot });
assert.equal(virtualHalSystemCoverage.apiName, "linuxcnc-wasm-virtual-hal-system-coverage-report");
assert.equal(virtualHalSystemCoverage.complete, true);
assert.equal(virtualHalSystemCoverage.realtimeHalRuntime, false);
assert.equal(virtualHalSystemCoverage.sourceFiles.includes("linuxcnc/bin/axis"), true);
assert.equal(virtualHalSystemCoverage.sourceFiles.includes("linuxcnc/src/emc/usr_intf/halui.cc"), true);
assert.equal(virtualHalSystemCoverage.sourceFiles.includes("linuxcnc/src/emc/task/taskclass.cc"), true);
assert.equal(virtualHalSystemCoverage.sourceFiles.includes("linuxcnc/src/emc/motion/motion.c"), true);
assert.equal(virtualHalSystemCoverage.families.some(({ id, coverage }) => id === "motion-core" && coverage === "runtime-boundary"), true);
assert.equal(virtualHalSystemCoverage.boundaryRows.some(({ id }) => id === "halui-task"), true);
const virtualHalBlockedReadiness = createVirtualHalBridgeReadiness({ snapshot: virtualHalSnapshot });
assert.equal(virtualHalBlockedReadiness.ready, false);
assert.equal(virtualHalBlockedReadiness.missing.includes("wasm-function-evidence"), true);
const virtualHalReadyReadiness = createVirtualHalBridgeReadiness({
snapshot: virtualHalSnapshot,
availableFunctions: VIRTUAL_HAL_WASM_BRIDGE_FUNCTIONS,
});
assert.equal(virtualHalReadyReadiness.ready, true);
assert.equal(createVirtualHalBridgeActionPlan(virtualHalReadyReadiness).nextCommand, null);
const virtualHalReport = createVirtualHalProjectReport({
halState: sdkVirtualHal,
availableFunctions: VIRTUAL_HAL_WASM_BRIDGE_FUNCTIONS,
});
assert.equal(virtualHalReport.apiName, "linuxcnc-wasm-virtual-hal-project-report");
assert.equal(virtualHalReport.ready, true);
assert.equal(virtualHalReport.pinInventory.pinCount, virtualHalReport.wasmBridgeSnapshot.values.length);
assert.equal(virtualHalReport.systemCoverage.phase, "covered-with-runtime-boundaries");
assert.equal(virtualHalReport.systemCoverage.unclassifiedSnapshotPins.length, 0);
assert.equal(virtualHalReport.pinRegistry.byName["spindle.0.speed-out"].value, 1800);
assert.equal(virtualHalReport.integrity.complete, true);
const virtualHalRuntime = createLinuxCncVirtualHalRuntime(sdkVirtualHal);
assert.equal(virtualHalRuntime.getProjectReport({ availableFunctions: VIRTUAL_HAL_WASM_BRIDGE_FUNCTIONS }).ready, true);
const virtualHalcmdResult = virtualHalRuntime.executeHalCommand(`
loadrt trivkins
addf motion-controller servo-thread
net x-pos axis.x.pos-cmd joint.0.pos-fb
setp axis.x.pos-cmd 2.5
sets x-pos 1.25
show pin axis.x.*
motion-step x=2.0
`);
assert.equal(virtualHalcmdResult.apiName, "linuxcnc-wasm-virtual-halcmd-result");
assert.equal(virtualHalcmdResult.ok, true);
assert.equal(virtualHalcmdResult.rows.some(({ action }) => action === "net"), true);
assert.equal(virtualHalRuntime.getState().hal.signals["x-pos"].value, 1.25);
assert.equal(virtualHalRuntime.getState().hal.nets["x-pos"].includes("axis.x.pos-cmd"), true);
assert.equal(virtualHalRuntime.getState().hal.loadedComponents.some(({ component }) => component === "trivkins"), true);
assert.equal(virtualHalRuntime.getState().hal.functions.some(({ name }) => name === "motion-controller"), true);
assert.ok(virtualHalRuntime.getState().position.x > 1.25);
const directHalcmdResult = executeVirtualHalCommand(virtualHalRuntime.getState(), "getp axis.x.pos-cmd\ngets x-pos");
assert.equal(directHalcmdResult.ok, true);
assert.match(directHalcmdResult.output, /axis\.x\.pos-cmd/);
assert.match(directHalcmdResult.output, /x-pos/);
const directHalcmdAliasResult = executeVirtualHalcmd(virtualHalRuntime.getState(), "setp halui.machine.on 1\nshow pin halui.machine.*");
assert.equal(directHalcmdAliasResult.ok, true);
assert.equal(directHalcmdAliasResult.apiName, "linuxcnc-wasm-virtual-halcmd-result");
assert.match(directHalcmdAliasResult.output, /halui\.machine\.is-on/);
const steppedVirtualHal = stepVirtualHalMotion(virtualHalRuntime.getState(), {
target: { x: 3 },
maxVelocity: 10,
dt: 0.1,
});
assert.ok(steppedVirtualHal.position.x > virtualHalRuntime.getState().position.x);
const motionControllerStep = stepVirtualHalMotionController(virtualHalRuntime.getState(), {
target: { x: 4 },
maxVelocity: 10,
dt: 0.1,
cycleCount: 3,
});
assert.equal(motionControllerStep.apiName, "linuxcnc-wasm-virtual-motion-controller-step");
assert.equal(motionControllerStep.ok, true);
assert.ok(motionControllerStep.cycleCount >= 1);
assert.ok(motionControllerStep.state.position.x > virtualHalRuntime.getState().position.x);
const simulationRuntimeReport = virtualHalRuntime.getSimulationRuntimeReport();
assert.equal(simulationRuntimeReport.apiName, "linuxcnc-wasm-virtual-hal-simulation-runtime-report");
assert.equal(simulationRuntimeReport.replacesHostRuntimeForSimulation, true);
assert.deepEqual(simulationRuntimeReport.replacementTargets, VIRTUAL_HAL_SIMULATION_REPLACEMENT_TARGETS);
assert.equal(simulationRuntimeReport.hardRealtime, false);
assert.deepEqual(simulationRuntimeReport.hostRequirements, []);
assert.equal(simulationRuntimeReport.capabilities.includes("servo-period-motion-step"), true);
assert.equal(createVirtualHalSimulationRuntimeReport(virtualHalRuntime.getState()).ready, true);
const runtimeMotionControllerStep = virtualHalRuntime.stepMotionController({
target: { x: 4.5 },
maxVelocity: 20,
dt: 0.1,
cycleCount: 2,
});
assert.equal(runtimeMotionControllerStep.apiName, "linuxcnc-wasm-virtual-motion-controller-step");
assert.ok(virtualHalRuntime.getState().position.x > motionControllerStep.state.position.x);
const replacementReport = createVirtualHalSimulationReplacementReport(virtualHalRuntime.getState());
assert.equal(replacementReport.apiName, "linuxcnc-wasm-virtual-hal-simulation-replacement-report");
assert.equal(replacementReport.ready, true);
assert.equal(replacementReport.replacesHostRuntimeForSimulation, true);
assert.equal(replacementReport.replacements["linuxcnc-realtime-hal"].ready, true);
assert.equal(replacementReport.replacements.halcmd.ready, true);
assert.equal(replacementReport.replacements["motion-controller"].ready, true);
assert.equal(replacementReport.hardRealtime, false);
assert.equal(virtualHalRuntime.getSimulationReplacementReport().ready, true);
const runtimeProjectReport = virtualHalRuntime.getProjectReport({ availableFunctions: VIRTUAL_HAL_WASM_BRIDGE_FUNCTIONS });
assert.equal(runtimeProjectReport.simulationRuntime.replacesHostRuntimeForSimulation, true);
assert.equal(runtimeProjectReport.systemCoverage.unclassifiedSnapshotPins.length, 0);
const fakeInterpApplyResult = applyVirtualHalToInterpSdk(
{
applyVirtualHalSnapshot(snapshot, options) {
return {
apiName: "fake-virtual-hal-apply-result",
applied: snapshot.values.length,
reset: options.reset !== false,
};
},
},
sdkVirtualHal,
{ reset: false },
);
assert.equal(fakeInterpApplyResult.apiName, "fake-virtual-hal-apply-result");
assert.equal(fakeInterpApplyResult.reset, false);
assert.equal(machineIniPath("sdk-mill"), "linuxcnc/machines/sdk-mill/machine.ini");
assert.deepEqual(defaultMachinePaths("sdk-mill"), machineFilePaths("sdk-mill"));
assert.equal(gcodeProgramPath("demo.ngc"), "linuxcnc/gcode/demo.ngc");

View File

@@ -21,6 +21,23 @@ import {
runRealBrowserSimulation,
zoomToolpathViewBox,
} from "../../../runtime/ui/simulation/simulation-app.js";
import {
VIRTUAL_HAL_AXISUI_PINS,
applyVirtualHalAction,
applyVirtualHalPinUpdates,
createLinuxCncInterpSdk,
createLinuxCncVirtualHalRuntime,
createVirtualHalDroState,
createVirtualHalIntegrityReport,
createVirtualHalLimitsHomeState,
createVirtualHalMachineStatusState,
createVirtualHalPinRegistry,
createVirtualHalState,
createVirtualHalWasmBridgeSnapshot,
parseVirtualHalJogIncrement,
readVirtualHalPin,
writeVirtualHalPin,
} from "../../../runtime/sdk/src/index.js";
import {
SIMULATION_TEST_PROGRAMS as directoryPrograms,
} from "../../../runtime/ui/simulation/programs/index.js";
@@ -56,6 +73,62 @@ const ids = new Set(programs.map(({ id }) => id));
assert.equal(ids.size, programs.length, "program ids should be unique");
assert.ok(ids.has("arc-g2-g3"), "program inventory should include an arc fixture");
assert.ok(ids.has("drill-g81"), "program inventory should include a canned-cycle fixture");
assert.equal(VIRTUAL_HAL_AXISUI_PINS.component, "axisui");
assert.ok(VIRTUAL_HAL_AXISUI_PINS.pins.some(({ name }) => name === "jog.x"), "virtual HAL should mirror AXIS axisui jog pins");
assert.ok(VIRTUAL_HAL_AXISUI_PINS.pins.some(({ name }) => name === "jog.increment"), "virtual HAL should expose AXIS jog increment pin");
assert.equal(parseVirtualHalJogIncrement("Continuous"), 0);
assert.equal(parseVirtualHalJogIncrement("0.01 in"), 0.01);
assert.equal(Number(parseVirtualHalJogIncrement("1 mm").toFixed(6)), 0.03937);
let virtualHal = createVirtualHalState();
assert.equal(virtualHal.task.state, "ESTOP");
virtualHal = applyVirtualHalAction(virtualHal, "estop");
assert.equal(virtualHal.task.state, "ESTOP_RESET");
virtualHal = applyVirtualHalAction(virtualHal, "power");
assert.equal(virtualHal.task.state, "ON");
virtualHal = applyVirtualHalAction(virtualHal, "home-all");
assert.equal(createVirtualHalLimitsHomeState(virtualHal).axes.x.homed, "yes");
virtualHal = applyVirtualHalAction(virtualHal, "jog", { axis: "x", increment: "0.01 in", direction: 1 });
assert.equal(virtualHal.pins["jog.x"], true);
assert.equal(virtualHal.pins["jog.increment"], 0.01);
assert.equal(createVirtualHalDroState(virtualHal).actual.x, "0.010");
const jogSnapshot = createVirtualHalWasmBridgeSnapshot(virtualHal);
assert.ok(jogSnapshot.values.some(({ name, type, value }) => name === "jog.x" && type === "HAL_BIT" && value === 1));
assert.ok(jogSnapshot.values.some(({ name, type, value }) => name === "axisui.jog.x" && type === "HAL_BIT" && value === 1));
assert.ok(jogSnapshot.values.some(({ name, type, value }) => name === "jog.increment" && type === "HAL_FLOAT" && value === 0.01));
assert.ok(jogSnapshot.values.some(({ name, type, value }) => name === "axisui.jog.increment" && type === "HAL_FLOAT" && value === 0.01));
virtualHal = applyVirtualHalAction(virtualHal, "spindle", { command: "cw", speed: 1200 });
virtualHal = applyVirtualHalAction(virtualHal, "coolant", { name: "flood", enabled: true });
virtualHal = writeVirtualHalPin(virtualHal, { name: "motion.tooloffset.z", type: "HAL_FLOAT", value: 1.25 });
virtualHal = applyVirtualHalPinUpdates(virtualHal, [
{ name: "halui.tool.number", type: "HAL_U32", value: 4 },
{ name: "halui.feed-override.value", type: "HAL_FLOAT", value: 0.9 },
]);
assert.equal(readVirtualHalPin(virtualHal, "motion.tooloffset.z").value, 1.25);
assert.equal(readVirtualHalPin(virtualHal, "halui.tool.number").value, 4);
assert.equal(virtualHal.overrides.feed, 90);
const virtualHalRegistry = createVirtualHalPinRegistry(virtualHal);
assert.equal(virtualHalRegistry.apiName, "linuxcnc-wasm-virtual-hal-pin-registry");
assert.equal(virtualHalRegistry.byName["halui.machine.on"].writable, true);
assert.equal(virtualHalRegistry.byName["motion.tooloffset.z"].value, 1.25);
const virtualHalIntegrity = createVirtualHalIntegrityReport({ halState: virtualHal, registry: virtualHalRegistry });
assert.equal(virtualHalIntegrity.complete, true);
const virtualMachineStatus = createVirtualHalMachineStatusState(virtualHal);
assert.equal(virtualMachineStatus.source, "browser-virtual-hal");
assert.equal(virtualMachineStatus.spindle.direction, "cw");
assert.equal(virtualMachineStatus.spindle.speed, "1200");
assert.equal(virtualMachineStatus.coolant.flood, "on");
const virtualHalRuntime = createLinuxCncVirtualHalRuntime(virtualHal);
virtualHalRuntime.writePin({ name: "spindle.0.speed-out", type: "HAL_FLOAT", value: 1400 });
assert.equal(virtualHalRuntime.readPin("spindle.0.speed-out").value, 1400);
assert.equal(virtualHalRuntime.getIntegrityReport().complete, true);
const runtimeSnapshot = virtualHalRuntime.getWasmBridgeSnapshot();
assert.equal(runtimeSnapshot.apiName, "linuxcnc-wasm-virtual-hal-bridge-snapshot");
assert.ok(runtimeSnapshot.values.some(({ name, type, value }) => name === "jog.x" && type === "HAL_BIT" && value === 1));
assert.ok(runtimeSnapshot.values.some(({ name, type, value }) => name === "axisui.jog.x" && type === "HAL_BIT" && value === 1));
assert.ok(runtimeSnapshot.values.some(({ name, type, value }) => name === "halui.machine.is-on" && type === "HAL_BIT" && value === 1));
assert.ok(runtimeSnapshot.values.some(({ name, type, value }) => name === "spindle.0.speed-out" && type === "HAL_FLOAT" && value === 1400));
assert.ok(runtimeSnapshot.values.some(({ name, type, value }) => name === "iocontrol.0.coolant-flood" && type === "HAL_BIT" && value === 1));
assert.throws(
() => getSimulationTestProgram("missing-program"),
@@ -91,8 +164,8 @@ assert.equal(arcModal.apiName, "real-browser-simulation-modal-state");
assert.equal(arcModal.source, "linuxcnc-update-tag");
assert.ok(arcModal.raw.includes("canon_event=UPDATE_TAG line=1"));
assert.ok(arcModal.rows.some(({ id, value, source }) => id === "plane" && value === "plane=170" && source === "linuxcnc-update-tag"));
assert.equal(unavailableMachineStatus.spindle.state, "n/a");
assert.equal(unavailableMachineStatus.coolant.mist, "n/a");
assert.equal(unavailableMachineStatus.spindle.state, "off");
assert.equal(unavailableMachineStatus.coolant.mist, "off");
const runtimeMachineStatus = createMachineStatusState([
"canon_event=SET_SPINDLE_SPEED spindle=0 speed=1200",
@@ -108,7 +181,7 @@ const runtimeMachineStatus = createMachineStatusState([
"canon_event=ENABLE_SPEED_OVERRIDE spindle=0",
].join("\n"));
assert.equal(runtimeMachineStatus.spindle.state, "off");
assert.equal(runtimeMachineStatus.spindle.direction, "cw");
assert.equal(runtimeMachineStatus.spindle.direction, "stopped");
assert.equal(runtimeMachineStatus.spindle.speed, "1200");
assert.equal(runtimeMachineStatus.coolant.mist, "off");
assert.equal(runtimeMachineStatus.coolant.flood, "off");
@@ -160,11 +233,11 @@ assert.equal(firstFrame.activeLine, 1);
assert.equal(firstFrame.visibleMotion.length, 1);
assert.equal(firstDro.apiName, "real-browser-simulation-dro-state");
assert.equal(firstDro.actual.x, "0.000");
assert.equal(firstDro.distanceToGo.x, "n/a");
assert.equal(firstDro.workOffsetG54.x, "n/a");
assert.equal(firstDro.g92Offset.x, "n/a");
assert.equal(firstDro.toolLengthOffset.x, "n/a");
assert.equal(firstDro.velocity, "n/a");
assert.equal(firstDro.distanceToGo.x, "0.000");
assert.equal(firstDro.workOffsetG54.x, "0.000");
assert.equal(firstDro.g92Offset.x, "0.000");
assert.equal(firstDro.toolLengthOffset.x, "0.000");
assert.equal(firstDro.velocity, "0.000");
const arcFrame = createPlaybackFrame(playbackState, 1);
assert.equal(arcFrame.index, 1);

View File

@@ -4,7 +4,9 @@ import { dirname, resolve } from "node:path";
import assert from "node:assert/strict";
import {
applyVirtualHalAction,
createLinuxCncInterpSdk,
createVirtualHalState,
planIniFileContextStaging,
} from "../../../runtime/sdk/src/index.js";
import {
@@ -726,6 +728,46 @@ verifyExpectedOutput(
].join("\n"),
);
if (
interp.hasWasmFunction("lcinterp_hal_reset") &&
interp.hasWasmFunction("lcinterp_hal_set_value") &&
interp.hasWasmFunction("lcinterp_probe_hal_named")
) {
let virtualHal = createVirtualHalState();
virtualHal = applyVirtualHalAction(virtualHal, "estop");
virtualHal = applyVirtualHalAction(virtualHal, "power");
virtualHal = applyVirtualHalAction(virtualHal, "jog", { axis: "x", increment: "0.01 in", direction: 1 });
virtualHal = applyVirtualHalAction(virtualHal, "spindle", { command: "cw", speed: 1200 });
virtualHal = applyVirtualHalAction(virtualHal, "coolant", { name: "flood", enabled: true });
const applyResult = interp.applyVirtualHalState(virtualHal);
assert.equal(applyResult.apiName, "linuxcnc-wasm-hal-apply-result");
assert.equal(applyResult.source, "browser-virtual-hal");
assert.ok(applyResult.applied > 0, "virtual HAL bridge should apply values");
verifyExpectedOutput(
"probe_virtual_hal_bridge",
[
interp.probeHalNamed("_hal[jog.x]"),
interp.probeHalNamed("_hal[jog.increment]"),
interp.probeHalNamed("_hal[axisui.jog.x]"),
interp.probeHalNamed("_hal[axisui.jog.increment]"),
interp.probeHalNamed("_hal[axis.x.pos-cmd]"),
interp.probeHalNamed("_hal[halui.machine.is-on]"),
interp.probeHalNamed("_hal[spindle.0.speed-out]"),
interp.probeHalNamed("_hal[iocontrol.0.coolant-flood]"),
].join(""),
[
"_hal[jog.x]: rc=0 found=1 value=1",
"_hal[jog.increment]: rc=0 found=1 value=0.01",
"_hal[axisui.jog.x]: rc=0 found=1 value=1",
"_hal[axisui.jog.increment]: rc=0 found=1 value=0.01",
"_hal[axis.x.pos-cmd]: rc=0 found=1 value=0.01",
"_hal[halui.machine.is-on]: rc=0 found=1 value=1",
"_hal[spindle.0.speed-out]: rc=0 found=1 value=1200",
"_hal[iocontrol.0.coolant-flood]: rc=0 found=1 value=1",
].join("\n"),
);
}
for (const fixtureName of INTERP_MDI_FIXTURES) {
const programText = readFileSync(
resolve(rootDir, `tests/fixtures/gcode/${fixtureName}.ngc`),

View File

@@ -98,5 +98,5 @@ link_wasm_module \
-s STACK_SIZE=2MB \
-s NO_EXIT_RUNTIME=1 \
-s FORCE_FILESYSTEM=1 \
-s EXPORTED_FUNCTIONS='["_malloc","_free","_lcinterp_run_program","_lcinterp_run_program_with_ini","_lcinterp_run_file","_lcinterp_run_file_with_ini","_lcinterp_run_file_with_ini_continue_on_error","_lcinterp_run_fiveaxis_remap_file","_lcinterp_run_remap_file","_lcinterp_run_remap_file_continue_on_error","_lcinterp_run_remap_io_mdi_sequence","_lcinterp_restore_parameters","_lcinterp_save_parameters","_lcinterp_load_tool_table","_lcinterp_load_tool_table_random","_lcinterp_save_tool_table","_lcinterp_probe_init_and_synch","_lcinterp_probe_indexer","_lcinterp_probe_named_parameters","_lcinterp_free_string"]' \
-s EXPORTED_FUNCTIONS='["_malloc","_free","_lcinterp_run_program","_lcinterp_run_program_with_ini","_lcinterp_run_file","_lcinterp_run_file_with_ini","_lcinterp_run_file_with_ini_continue_on_error","_lcinterp_run_fiveaxis_remap_file","_lcinterp_run_remap_file","_lcinterp_run_remap_file_continue_on_error","_lcinterp_run_remap_io_mdi_sequence","_lcinterp_restore_parameters","_lcinterp_save_parameters","_lcinterp_load_tool_table","_lcinterp_load_tool_table_random","_lcinterp_save_tool_table","_lcinterp_probe_init_and_synch","_lcinterp_probe_indexer","_lcinterp_probe_named_parameters","_lcinterp_hal_reset","_lcinterp_hal_set_value","_lcinterp_probe_hal_named","_lcinterp_free_string"]' \
-s EXPORTED_RUNTIME_METHODS='["FS","UTF8ToString","stringToUTF8","lengthBytesUTF8"]'