Files
cnc_wams/wasm-port/runtime/sdk/src/linuxcnc-hal.js
wangdequan fbf9dade9c 按text25.txt规划,实现L4-PYTHON-REMAP接入数控系统仿真系统
结论:已接入Python remap runtime proof chain,覆盖native、WASM、browser与release gate证据链;继续保持promotion_allowed=0,不批量解锁L4-PYTHON-REMAP。
2026-06-19 18:52:24 +08:00

4035 lines
180 KiB
JavaScript

const VIRTUAL_HAL_AXES_INTERNAL = ["x", "y", "z", "a", "b", "c", "u", "v", "w"];
const VIRTUAL_HAL_DISPLAY_AXES = ["x", "y", "z", "a", "b", "c"];
export const VIRTUAL_HAL_AXES = [...VIRTUAL_HAL_AXES_INTERNAL];
export const VIRTUAL_HAL_AXISUI_PINS = Object.freeze({
component: "axisui",
pins: Object.freeze([
...VIRTUAL_HAL_AXES.map((axis) => ({ name: `jog.${axis}`, type: "HAL_BIT", direction: "HAL_OUT" })),
{ name: "jog.increment", type: "HAL_FLOAT", direction: "HAL_OUT" },
{ name: "notifications-clear", type: "HAL_BIT", direction: "HAL_IN" },
{ name: "notifications-clear-info", type: "HAL_BIT", direction: "HAL_IN" },
{ name: "notifications-clear-error", type: "HAL_BIT", direction: "HAL_IN" },
{ name: "resume-inhibit", type: "HAL_BIT", direction: "HAL_IN" },
{ name: "error", type: "HAL_BIT", direction: "HAL_OUT" },
{ name: "abort", type: "HAL_BIT", direction: "HAL_OUT" },
]),
});
export const VIRTUAL_HAL_SOURCE_FILES = Object.freeze({
axisui: "linuxcnc/bin/axis",
axisuiScript: "linuxcnc/src/emc/usr_intf/axis/scripts/axis.py",
halui: "linuxcnc/src/emc/usr_intf/halui.cc",
iocontrol: "linuxcnc/src/emc/task/taskclass.cc",
motion: "linuxcnc/src/emc/motion/motion.c",
motionAxis: "linuxcnc/src/emc/motion/axis.c",
motionHoming: "linuxcnc/src/emc/motion/homing.c",
halcmd: "linuxcnc/src/hal/utils/halcmd.c",
halcmdCommands: "linuxcnc/src/hal/utils/halcmd_commands.cc",
halcmdMain: "linuxcnc/src/hal/utils/halcmd_main.c",
halcmdBin: "linuxcnc/bin/halcmd",
vendoredMotionHeader: "wasm-port/vendor/linuxcnc/src/emc/motion/motion.h",
});
export const VIRTUAL_HAL_SIM_CONFIG_SOURCE_TARGETS = Object.freeze([
Object.freeze({
id: "axis-foam",
label: "AXIS foam sim",
sourceFiles: Object.freeze([
"linuxcnc/configs/sim/axis/foam/axis_foam.ini",
"linuxcnc/configs/sim/axis/foam/foam.ngc",
]),
capabilities: Object.freeze(["axis-joint-position-feedback", "hal-pin-signal-param-store"]),
evidence: "Vendored LinuxCNC AXIS foam sim config covers axis motion pins and simple program execution.",
}),
Object.freeze({
id: "axis-geometry-user-m",
label: "AXIS geometry user-M sim",
sourceFiles: Object.freeze([
"linuxcnc/configs/sim/axis/geometry/xyzc.ini",
"linuxcnc/configs/sim/axis/geometry/xyzc.ngc",
"linuxcnc/configs/sim/axis/geometry/M110",
]),
capabilities: Object.freeze(["userspace-load-command-stubs", "halcmd-simulation-replacement"]),
evidence: "Vendored LinuxCNC geometry sim config declares USER_M_PATH behavior represented as a simulation boundary.",
}),
Object.freeze({
id: "external-offsets",
label: "External offsets sim",
sourceFiles: Object.freeze([
"linuxcnc/configs/sim/axis/external_offsets/dynamic_offsets.ini",
"linuxcnc/configs/sim/axis/external_offsets/eoffsets.ini",
"linuxcnc/configs/sim/axis/external_offsets/M111",
"linuxcnc/configs/sim/axis/external_offsets/dyn_demo.ngc",
"linuxcnc/configs/sim/axis/external_offsets/eoffsets.ngc",
]),
capabilities: Object.freeze(["motion-controller-simulation-replacement", "servo-period-motion-step"]),
evidence: "Vendored LinuxCNC external-offset configs exercise motion/HAL pins and deterministic user-M boundary accounting.",
}),
Object.freeze({
id: "gladevcp-probe",
label: "GladeVCP probe sim",
sourceFiles: Object.freeze([
"linuxcnc/configs/sim/axis/gladevcp/gladevcp_panel.ini",
"linuxcnc/configs/sim/axis/gladevcp/probe.ngc",
"linuxcnc/configs/sim/axis/gladevcp/sim.tbl",
]),
capabilities: Object.freeze(["interpreter-hal-bridge-snapshot", "hal-pin-signal-param-store"]),
evidence: "Vendored LinuxCNC probe sim config covers probe input and tool-table staging without browser-owned CNC semantics.",
}),
Object.freeze({
id: "woodpecker",
label: "Woodpecker sim",
sourceFiles: Object.freeze([
"linuxcnc/configs/sim/woodpecker/woodpecker.ini",
"linuxcnc/configs/sim/woodpecker/on_abort.ngc",
"linuxcnc/configs/sim/woodpecker/tool.tbl",
]),
capabilities: Object.freeze(["halcmd-simulation-replacement", "spindle-coolant-tool-status"]),
evidence: "Vendored LinuxCNC woodpecker sim config supplies deterministic on-abort and tool-table workflows.",
}),
Object.freeze({
id: "qtdragon-on-abort",
label: "QtDragon on-abort sims",
sourceFiles: Object.freeze([
"linuxcnc/configs/sim/qtdragon/qtdragon_xyz/qtdragon_inch.ini",
"linuxcnc/configs/sim/qtdragon/qtdragon_xyz/on_abort.ngc",
"linuxcnc/configs/sim/qtdragon/qtdragon_multi_joint/qtdragon_xyyz.ini",
"linuxcnc/configs/sim/qtdragon/qtdragon_multi_joint/on_abort.ngc",
"linuxcnc/configs/sim/qtdragon/qtdragon_xyz45/qtdragon_xyza.ini",
"linuxcnc/configs/sim/qtdragon/qtdragon_xyz45/on_abort.ngc",
"linuxcnc/configs/sim/qtdragon_hd/qtdragon_hd_xyz/qtdragon_hd_vertical.ini",
"linuxcnc/configs/sim/qtdragon_hd/qtdragon_hd_xyz/on_abort.ngc",
"linuxcnc/configs/sim/qtdragon_hd/qtdragon_hd_z_compensation/qtdragon_hd_z_compensation.ini",
"linuxcnc/configs/sim/qtdragon_hd/qtdragon_hd_z_compensation/on_abort.ngc",
"linuxcnc/configs/sim/qtvcp_screens/qtdragon/qtdragon_mpg.ini",
"linuxcnc/configs/sim/qtvcp_screens/qtdragon/on_abort.ngc",
]),
capabilities: Object.freeze(["realtime-hal-simulation-replacement", "axis-joint-position-feedback"]),
evidence: "Vendored LinuxCNC QtDragon sim configs provide multi-joint and on-abort machine/session coverage for Web simulation.",
}),
Object.freeze({
id: "vismach-remap-sims",
label: "Vismach remap sims",
sourceFiles: Object.freeze([
"linuxcnc/configs/sim/axis/vismach/melfa-sim/melfa.ini",
"linuxcnc/configs/sim/axis/vismach/melfa-sim/example.ngc",
"linuxcnc/configs/sim/axis/vismach/melfa-sim/remap_subs/428remap.ngc",
"linuxcnc/configs/sim/axis/vismach/puma/puma.ini",
"linuxcnc/configs/sim/axis/vismach/puma/puma_cube.ngc",
"linuxcnc/configs/sim/axis/vismach/puma/remap_subs/428remap.ngc",
]),
capabilities: Object.freeze(["interpreter-hal-bridge-snapshot", "motion-controller-simulation-replacement"]),
evidence: "Vendored LinuxCNC vismach remap configs remain LinuxCNC-owned remap/kinematics assets staged through the WASM boundary.",
}),
]);
export const VIRTUAL_HAL_SIM_CONFIG_INVENTORY_BASELINE = Object.freeze({
executed: 29,
passed: 29,
skipped: 130,
unexpectedFail: 0,
});
export const VIRTUAL_HAL_SIM_CONFIG_INVENTORY_ARTIFACT_HASHES = Object.freeze({
"wasm-port/build/wasm/sim-configs-inventory/boundary-summary.tsv":
"6cc15052cad03d04eff4506cd0a9d651cd45a2bd5da4d9f38633e9535826b9f3",
"wasm-port/build/wasm/sim-configs-inventory/ini-boundary-summary.tsv":
"17bff95b57c55ff4201a23c7286e733550e4df513470c66dd5f24329e7781a35",
});
export const VIRTUAL_HAL_SIM_CONFIG_PROMOTION_CANDIDATES = Object.freeze([
Object.freeze({
id: "qtdragon-multi-joint-on-abort",
label: "QtDragon multi-joint on-abort",
simConfigTarget: "qtdragon-on-abort",
sourceFiles: Object.freeze([
"linuxcnc/configs/sim/qtdragon/qtdragon_multi_joint/qtdragon_xyyz.ini",
"linuxcnc/configs/sim/qtdragon/qtdragon_multi_joint/on_abort.ngc",
]),
iniPath: "linuxcnc/configs/sim/qtdragon/qtdragon_multi_joint/qtdragon_xyyz.ini",
gcodePath: "linuxcnc/configs/sim/qtdragon/qtdragon_multi_joint/on_abort.ngc",
currentNodeInventoryStatus: "PASS",
currentMatrixBrowserStatus: "explicit-browser-diagnostics",
dependencyClass: "hal-ui-multi-joint",
targetBrowserEvidence: "explicit-browser-diagnostics",
requiredVirtualHalReports: Object.freeze([
"source-compliance",
"sim-config-source-coverage",
"command-script-fixtures",
"manifest-backed-motion-matrix",
]),
blockedKind: "-",
nonMainFixture: false,
evidence: "Node inventory already covers the LinuxCNC QtDragon multi-joint on-abort program; virtual HAL supplies the HAL/UI/motion evidence needed for explicit browser diagnostics.",
}),
Object.freeze({
id: "qtdragon-xyz-on-abort",
label: "QtDragon XYZ on-abort",
simConfigTarget: "qtdragon-on-abort",
sourceFiles: Object.freeze([
"linuxcnc/configs/sim/qtdragon/qtdragon_xyz/qtdragon_inch.ini",
"linuxcnc/configs/sim/qtdragon/qtdragon_xyz/on_abort.ngc",
]),
iniPath: "linuxcnc/configs/sim/qtdragon/qtdragon_xyz/qtdragon_inch.ini",
gcodePath: "linuxcnc/configs/sim/qtdragon/qtdragon_xyz/on_abort.ngc",
currentNodeInventoryStatus: "PASS",
currentMatrixBrowserStatus: "explicit-browser-diagnostics",
dependencyClass: "hal-ui-on-abort",
targetBrowserEvidence: "explicit-browser-diagnostics",
requiredVirtualHalReports: Object.freeze([
"source-compliance",
"sim-config-source-coverage",
"command-script-fixtures",
"manifest-backed-motion-matrix",
]),
blockedKind: "-",
nonMainFixture: false,
evidence: "Node inventory already covers the LinuxCNC QtDragon XYZ on-abort program; virtual HAL can carry explicit browser diagnostics without changing CNC semantics.",
}),
Object.freeze({
id: "qtdragon-xyz45-on-abort",
label: "QtDragon XYZA on-abort",
simConfigTarget: "qtdragon-on-abort",
sourceFiles: Object.freeze([
"linuxcnc/configs/sim/qtdragon/qtdragon_xyz45/qtdragon_xyza.ini",
"linuxcnc/configs/sim/qtdragon/qtdragon_xyz45/on_abort.ngc",
]),
iniPath: "linuxcnc/configs/sim/qtdragon/qtdragon_xyz45/qtdragon_xyza.ini",
gcodePath: "linuxcnc/configs/sim/qtdragon/qtdragon_xyz45/on_abort.ngc",
currentNodeInventoryStatus: "PASS",
currentMatrixBrowserStatus: "explicit-browser-diagnostics",
dependencyClass: "hal-ui-rotary-axis",
targetBrowserEvidence: "explicit-browser-diagnostics",
requiredVirtualHalReports: Object.freeze([
"source-compliance",
"sim-config-source-coverage",
"command-script-fixtures",
"manifest-backed-motion-matrix",
]),
blockedKind: "-",
nonMainFixture: false,
evidence: "The LinuxCNC QtDragon XYZA on-abort case adds rotary-axis machine context without Python remap or external process runtime.",
}),
Object.freeze({
id: "qtdragon-hd-xyz-on-abort",
label: "QtDragon HD XYZ on-abort",
simConfigTarget: "qtdragon-on-abort",
sourceFiles: Object.freeze([
"linuxcnc/configs/sim/qtdragon_hd/qtdragon_hd_xyz/qtdragon_hd_vertical.ini",
"linuxcnc/configs/sim/qtdragon_hd/qtdragon_hd_xyz/on_abort.ngc",
]),
iniPath: "linuxcnc/configs/sim/qtdragon_hd/qtdragon_hd_xyz/qtdragon_hd_vertical.ini",
gcodePath: "linuxcnc/configs/sim/qtdragon_hd/qtdragon_hd_xyz/on_abort.ngc",
currentNodeInventoryStatus: "PASS",
currentMatrixBrowserStatus: "explicit-browser-diagnostics",
dependencyClass: "hal-ui-on-abort",
targetBrowserEvidence: "explicit-browser-diagnostics",
requiredVirtualHalReports: Object.freeze([
"source-compliance",
"sim-config-source-coverage",
"command-script-fixtures",
"manifest-backed-motion-matrix",
]),
blockedKind: "-",
nonMainFixture: false,
evidence: "The LinuxCNC QtDragon HD XYZ on-abort case is a deterministic HAL/UI workflow suitable for browser diagnostics evidence.",
}),
Object.freeze({
id: "qtdragon-hd-z-compensation-on-abort",
label: "QtDragon HD z-compensation on-abort",
simConfigTarget: "qtdragon-on-abort",
sourceFiles: Object.freeze([
"linuxcnc/configs/sim/qtdragon_hd/qtdragon_hd_z_compensation/qtdragon_hd_z_compensation.ini",
"linuxcnc/configs/sim/qtdragon_hd/qtdragon_hd_z_compensation/on_abort.ngc",
]),
iniPath: "linuxcnc/configs/sim/qtdragon_hd/qtdragon_hd_z_compensation/qtdragon_hd_z_compensation.ini",
gcodePath: "linuxcnc/configs/sim/qtdragon_hd/qtdragon_hd_z_compensation/on_abort.ngc",
currentNodeInventoryStatus: "PASS",
currentMatrixBrowserStatus: "explicit-browser-diagnostics",
dependencyClass: "hal-ui-z-compensation",
targetBrowserEvidence: "explicit-browser-diagnostics",
requiredVirtualHalReports: Object.freeze([
"source-compliance",
"sim-config-source-coverage",
"command-script-fixtures",
"manifest-backed-motion-matrix",
]),
blockedKind: "-",
nonMainFixture: false,
evidence: "The LinuxCNC QtDragon HD z-compensation on-abort case should only promote with explicit source evidence for the z-compensation INI.",
}),
Object.freeze({
id: "qtvcp-screens-qtdragon-on-abort",
label: "QTVCP screens QtDragon on-abort",
simConfigTarget: "qtdragon-on-abort",
sourceFiles: Object.freeze([
"linuxcnc/configs/sim/qtvcp_screens/qtdragon/qtdragon_mpg.ini",
"linuxcnc/configs/sim/qtvcp_screens/qtdragon/on_abort.ngc",
]),
iniPath: "linuxcnc/configs/sim/qtvcp_screens/qtdragon/qtdragon_mpg.ini",
gcodePath: "linuxcnc/configs/sim/qtvcp_screens/qtdragon/on_abort.ngc",
currentNodeInventoryStatus: "PASS",
currentMatrixBrowserStatus: "explicit-browser-diagnostics",
dependencyClass: "hal-ui-on-abort",
targetBrowserEvidence: "explicit-browser-diagnostics",
requiredVirtualHalReports: Object.freeze([
"source-compliance",
"sim-config-source-coverage",
"command-script-fixtures",
"manifest-backed-motion-matrix",
]),
blockedKind: "-",
nonMainFixture: false,
evidence: "The LinuxCNC QTVCP QtDragon on-abort case has no hard runtime block and can be promoted as explicit browser diagnostics evidence.",
}),
Object.freeze({
id: "puma-seam-weld",
label: "PUMA seam weld",
simConfigTarget: "vismach-remap-sims",
sourceFiles: Object.freeze([
"linuxcnc/configs/sim/axis/vismach/puma/puma.ini",
"linuxcnc/configs/sim/axis/vismach/puma/puma_seam_weld.ngc",
"linuxcnc/configs/sim/axis/vismach/puma/remap_subs/428remap.ngc",
]),
iniPath: "linuxcnc/configs/sim/axis/vismach/puma/puma.ini",
gcodePath: "linuxcnc/configs/sim/axis/vismach/puma/puma_seam_weld.ngc",
currentNodeInventoryStatus: "PASS",
currentMatrixBrowserStatus: "explicit-browser-diagnostics",
dependencyClass: "halui-vismach-remap",
targetBrowserEvidence: "explicit-browser-diagnostics",
requiredVirtualHalReports: Object.freeze([
"source-compliance",
"sim-config-source-coverage",
"command-script-fixtures",
"manifest-backed-motion-matrix",
]),
blockedKind: "-",
nonMainFixture: false,
evidence: "Node inventory covers this LinuxCNC PUMA program with the same vendored machine context as puma_cube; browser evidence can pair both cases.",
}),
Object.freeze({
id: "rose-engine-rcone-demo",
label: "Rose engine cone demo",
simConfigTarget: null,
sourceFiles: Object.freeze([
"linuxcnc/configs/sim/axis/rose_engine/rose_engine.ini",
"linuxcnc/configs/sim/axis/rose_engine/rcone_demo.ngc",
]),
iniPath: "linuxcnc/configs/sim/axis/rose_engine/rose_engine.ini",
gcodePath: "linuxcnc/configs/sim/axis/rose_engine/rcone_demo.ngc",
currentNodeInventoryStatus: "PASS",
currentMatrixBrowserStatus: "explicit-browser-diagnostics",
dependencyClass: "hal-process-only",
targetBrowserEvidence: "explicit-browser-diagnostics",
requiredVirtualHalReports: Object.freeze([
"source-compliance",
"sim-config-source-coverage",
"command-script-fixtures",
"manifest-backed-motion-matrix",
]),
blockedKind: "-",
nonMainFixture: false,
evidence: "The LinuxCNC rose-engine demo is a HAL-process-only Node inventory pass and is useful as a non-Qt/non-vismach diagnostics candidate.",
}),
]);
export const VIRTUAL_HAL_SIM_CONFIG_EVIDENCE_EXPANSION_CANDIDATES = Object.freeze([
Object.freeze({
id: "woodpecker-on-abort",
label: "Woodpecker on-abort",
simConfigTarget: "woodpecker",
sourceFiles: Object.freeze([
"linuxcnc/configs/sim/woodpecker/woodpecker.ini",
"linuxcnc/configs/sim/woodpecker/on_abort.ngc",
"linuxcnc/configs/sim/woodpecker/tool.tbl",
]),
iniPath: "linuxcnc/configs/sim/woodpecker/woodpecker.ini",
gcodePath: "linuxcnc/configs/sim/woodpecker/on_abort.ngc",
currentNodeInventoryStatus: "PASS",
currentMatrixBrowserStatus: "REP",
dependencyClass: "hal_process,ui_process",
targetBrowserEvidence: "browser-diagnostics-expansion",
promotionAllowed: false,
blockedKind: "-",
evidence: "Current inventory already covers Woodpecker as a non-hard-block representative; next step is explicit browser diagnostics binding only.",
}),
Object.freeze({
id: "puma-cube",
label: "PUMA cube",
simConfigTarget: "vismach-remap-sims",
sourceFiles: Object.freeze([
"linuxcnc/configs/sim/axis/vismach/puma/puma_cube.ini",
"linuxcnc/configs/sim/axis/vismach/puma/puma_cube.ngc",
"linuxcnc/configs/sim/axis/vismach/puma/remap_subs/428remap.ngc",
]),
iniPath: "linuxcnc/configs/sim/axis/vismach/puma/puma_cube.ini",
gcodePath: "linuxcnc/configs/sim/axis/vismach/puma/puma_cube.ngc",
currentNodeInventoryStatus: "PASS",
currentMatrixBrowserStatus: "REP",
dependencyClass: "hal_process,halui_mdi_process,ui_process",
targetBrowserEvidence: "browser-diagnostics-expansion",
promotionAllowed: false,
blockedKind: "-",
evidence: "PUMA cube is already the representative browser row for the PUMA machine context; it remains an expansion candidate, not a baseline promotion.",
}),
Object.freeze({
id: "melfa-example",
label: "MELFA example",
simConfigTarget: "vismach-remap-sims",
sourceFiles: Object.freeze([
"linuxcnc/configs/sim/axis/vismach/melfa-sim/melfa.ini",
"linuxcnc/configs/sim/axis/vismach/melfa-sim/example.ngc",
"linuxcnc/configs/sim/axis/vismach/melfa-sim/remap_subs/428remap.ngc",
]),
iniPath: "linuxcnc/configs/sim/axis/vismach/melfa-sim/melfa.ini",
gcodePath: "linuxcnc/configs/sim/axis/vismach/melfa-sim/example.ngc",
currentNodeInventoryStatus: "PASS",
currentMatrixBrowserStatus: "REP",
dependencyClass: "hal_process,halui_mdi_process,ui_process",
targetBrowserEvidence: "browser-diagnostics-expansion",
promotionAllowed: false,
blockedKind: "-",
evidence: "MELFA example has current representative coverage and can receive explicit browser diagnostics evidence without unlocking any hard runtime family.",
}),
]);
export const VIRTUAL_HAL_SIM_CONFIG_MACRO_LOAD_FIXTURES = Object.freeze([
Object.freeze({
id: "rose-engine-rcone-macro-load",
label: "Rose engine rcone macro/load",
fixturePath: "linuxcnc/configs/sim/axis/rose_engine/rcone.ngc",
sourceFiles: Object.freeze([
"linuxcnc/configs/sim/axis/rose_engine/rose_engine.ini",
"linuxcnc/configs/sim/axis/rose_engine/rcone.ngc",
"linuxcnc/configs/sim/axis/rose_engine/rcone_demo.ngc",
]),
owningIniPath: "linuxcnc/configs/sim/axis/rose_engine/rose_engine.ini",
pairedMainProgram: "linuxcnc/configs/sim/axis/rose_engine/rcone_demo.ngc",
declarationSource: "NGCGUI_SUBFILE",
declarationEvidence: Object.freeze({
sourceFile: "linuxcnc/configs/sim/axis/rose_engine/rose_engine.ini",
section: "DISPLAY",
key: "NGCGUI_SUBFILE",
value: "rcone.ngc",
line: 34,
sourceLine: "NGCGUI_SUBFILE = rcone.ngc",
}),
currentNodeInventoryStatus: "PASS",
dependencyClass: "macro-load-non-main",
targetBrowserEvidence: "non-main-fixture-diagnostics",
blockedKind: "-",
nonMainFixture: true,
declaredOnly: false,
evidence: "LinuxCNC rose_engine.ini declares rcone.ngc through NGCGUI_SUBFILE; the fixture is validated as a non-main macro/load asset paired with rcone_demo.ngc.",
}),
Object.freeze({
id: "external-offsets-queuebuster-macro-load",
label: "External offsets queuebuster macro/load declaration",
fixturePath: "linuxcnc/configs/sim/axis/external_offsets/queuebuster.ngc",
sourceFiles: Object.freeze([
"linuxcnc/configs/sim/axis/external_offsets/eoffsets.ini",
"linuxcnc/configs/sim/axis/external_offsets/eoffsets.ngc",
]),
owningIniPath: "linuxcnc/configs/sim/axis/external_offsets/eoffsets.ini",
pairedMainProgram: "linuxcnc/configs/sim/axis/external_offsets/eoffsets.ngc",
declarationSource: "NGCGUI_SUBFILE",
declarationEvidence: Object.freeze({
sourceFile: "linuxcnc/configs/sim/axis/external_offsets/eoffsets.ini",
section: "DISPLAY",
key: "NGCGUI_SUBFILE",
value: "queuebuster.ngc",
line: 40,
sourceLine: "NGCGUI_SUBFILE = queuebuster.ngc",
}),
currentNodeInventoryStatus: "NON_MAIN_CLASS",
dependencyClass: "macro-load-non-main-declared-only",
targetBrowserEvidence: "non-main-fixture-diagnostics",
blockedKind: "-",
nonMainFixture: true,
declaredOnly: true,
evidence: "LinuxCNC eoffsets.ini declares queuebuster.ngc through NGCGUI_SUBFILE; the fixture records the owning INI declaration without executing it as a standalone browser main program.",
}),
]);
export const VIRTUAL_HAL_SIM_CONFIG_MACRO_LOAD_BLOCKED_FIXTURES = Object.freeze([
Object.freeze({
id: "silverdragon-tool-sensor-python-ui-boundary",
fixturePath: "linuxcnc/configs/sim/gscreen/silverdragon/macros/tool_sensor.ngc",
owningIniPath: "linuxcnc/configs/sim/gscreen/silverdragon/silverdragon.ini",
blockedKind: "PYTHON-UI-PROCESS",
dependencyClass: "macro-load-python-ui-boundary",
boundaryEvidence: Object.freeze({
sourceArtifacts: Object.freeze([
"wasm-port/build/wasm/sim-configs-inventory/boundary-summary.tsv",
"wasm-port/build/wasm/sim-configs-inventory/ini-boundary-summary.tsv",
]),
sourceArtifactHashes: VIRTUAL_HAL_SIM_CONFIG_INVENTORY_ARTIFACT_HASHES,
baselineSummary: VIRTUAL_HAL_SIM_CONFIG_INVENTORY_BASELINE,
boundarySummary: Object.freeze({
path: "gscreen/silverdragon/macros/tool_sensor.ngc",
ini: "gscreen/silverdragon/silverdragon.ini",
recommendedBlocked: "UNAVAILABLE",
dependencies: "missing_vendored_ini:configs/sim/gscreen/silverdragon/silverdragon.ini",
}),
iniBoundarySummary: Object.freeze({
ini: "gscreen/silverdragon/silverdragon.ini",
vendored: 0,
reportAvailable: 0,
recommendedBlocked: "UNAVAILABLE",
dependencies: "missing_vendored_ini:configs/sim/gscreen/silverdragon/silverdragon.ini",
}),
}),
reason: "Owning SilverDragon UI machine file is not available in the vendored manifest; promotion would require Python UI process boundary proof outside virtual HAL.",
}),
Object.freeze({
id: "gmoccapy-on-abort-python-remap-boundary",
fixturePath: "linuxcnc/configs/sim/gmoccapy/macros/on_abort.ngc",
owningIniPath: "linuxcnc/configs/sim/gmoccapy/6_axis.ini",
blockedKind: "L4-PYTHON-REMAP",
dependencyClass: "macro-load-python-remap-boundary",
boundaryEvidence: Object.freeze({
sourceArtifacts: Object.freeze([
"wasm-port/build/wasm/sim-configs-inventory/boundary-summary.tsv",
"wasm-port/build/wasm/sim-configs-inventory/ini-boundary-summary.tsv",
]),
sourceArtifactHashes: VIRTUAL_HAL_SIM_CONFIG_INVENTORY_ARTIFACT_HASHES,
baselineSummary: VIRTUAL_HAL_SIM_CONFIG_INVENTORY_BASELINE,
boundarySummary: Object.freeze({
path: "gmoccapy/macros/on_abort.ngc",
ini: "gmoccapy/6_axis.ini",
recommendedBlocked: "UNAVAILABLE",
dependencies: "missing_vendored_ini:configs/sim/gmoccapy/6_axis.ini",
}),
iniBoundarySummary: Object.freeze({
ini: "gmoccapy/6_axis.ini",
vendored: 0,
reportAvailable: 0,
recommendedBlocked: "UNAVAILABLE",
dependencies: "missing_vendored_ini:configs/sim/gmoccapy/6_axis.ini",
}),
}),
reason: "gmoccapy macro/load rows remain locked behind LinuxCNC-owned Python remap/prolog/epilog runtime proof.",
}),
]);
export const VIRTUAL_HAL_SIM_CONFIG_MACRO_LOAD_AUDIT_CANDIDATES = Object.freeze([
Object.freeze({
id: "gscreen-industrial-lathe-wear-toolchange-audit",
fixturePath: "linuxcnc/configs/sim/gscreen/industrial_lathe_wear/toolchange.ngc",
owningIniPath: "linuxcnc/configs/sim/gscreen/industrial_lathe_wear/industrial_lathe_wear.ini",
dependencyClass: "macro-load-ui-family-audit",
auditStatus: "blocked-missing-owning-ini",
promotionAllowed: false,
boundaryEvidence: Object.freeze({
sourceArtifacts: Object.freeze([
"wasm-port/build/wasm/sim-configs-inventory/boundary-summary.tsv",
"wasm-port/build/wasm/sim-configs-inventory/ini-boundary-summary.tsv",
]),
sourceArtifactHashes: VIRTUAL_HAL_SIM_CONFIG_INVENTORY_ARTIFACT_HASHES,
baselineSummary: VIRTUAL_HAL_SIM_CONFIG_INVENTORY_BASELINE,
boundarySummary: Object.freeze({
path: "gscreen/industrial_lathe_wear/toolchange.ngc",
ini: "gscreen/industrial_lathe_wear/industrial_lathe_wear.ini",
recommendedBlocked: "UNAVAILABLE",
dependencies: "missing_vendored_ini:configs/sim/gscreen/industrial_lathe_wear/industrial_lathe_wear.ini",
}),
iniBoundarySummary: Object.freeze({
ini: "gscreen/industrial_lathe_wear/industrial_lathe_wear.ini",
vendored: 0,
reportAvailable: 0,
recommendedBlocked: "UNAVAILABLE",
dependencies: "missing_vendored_ini:configs/sim/gscreen/industrial_lathe_wear/industrial_lathe_wear.ini",
}),
}),
reason: "Generated boundary evidence reports missing vendored owning INI for gscreen industrial_lathe_wear; keep as audit-only until machine files and UI/process declaration have LinuxCNC source evidence.",
}),
Object.freeze({
id: "qtvcp-industrial-lathe-wear-toolchange-audit",
fixturePath: "linuxcnc/configs/sim/qtvcp_screens/industrial_lathe_wear/toolchange.ngc",
owningIniPath: "linuxcnc/configs/sim/qtvcp_screens/industrial_lathe_wear/industrial_lathe_wear.ini",
dependencyClass: "macro-load-ui-family-audit",
auditStatus: "blocked-missing-owning-ini",
promotionAllowed: false,
boundaryEvidence: Object.freeze({
sourceArtifacts: Object.freeze([
"wasm-port/build/wasm/sim-configs-inventory/boundary-summary.tsv",
"wasm-port/build/wasm/sim-configs-inventory/ini-boundary-summary.tsv",
]),
sourceArtifactHashes: VIRTUAL_HAL_SIM_CONFIG_INVENTORY_ARTIFACT_HASHES,
baselineSummary: VIRTUAL_HAL_SIM_CONFIG_INVENTORY_BASELINE,
boundarySummary: Object.freeze({
path: "qtvcp_screens/industrial_lathe_wear/toolchange.ngc",
ini: "qtvcp_screens/industrial_lathe_wear/industrial_lathe_wear.ini",
recommendedBlocked: "UNAVAILABLE",
dependencies: "missing_vendored_ini:configs/sim/qtvcp_screens/industrial_lathe_wear/industrial_lathe_wear.ini",
}),
iniBoundarySummary: Object.freeze({
ini: "qtvcp_screens/industrial_lathe_wear/industrial_lathe_wear.ini",
vendored: 0,
reportAvailable: 0,
recommendedBlocked: "UNAVAILABLE",
dependencies: "missing_vendored_ini:configs/sim/qtvcp_screens/industrial_lathe_wear/industrial_lathe_wear.ini",
}),
}),
reason: "Generated boundary evidence reports missing vendored owning INI for qtvcp_screens industrial_lathe_wear; keep as audit-only until machine files and UI/process declaration have LinuxCNC source evidence.",
}),
]);
export const VIRTUAL_HAL_COVERAGE_STATES = Object.freeze({
simulated: "simulated",
bridgeOnly: "bridge-only",
runtimeBoundary: "runtime-boundary",
});
export const VIRTUAL_HAL_SYSTEM_PIN_FAMILIES = Object.freeze([
{
id: "axisui",
component: "axisui",
label: "AXIS UI pins",
sourceFiles: Object.freeze([
VIRTUAL_HAL_SOURCE_FILES.axisui,
VIRTUAL_HAL_SOURCE_FILES.axisuiScript,
]),
sourceEvidence: "axisui component newpin calls for jog, notification, error, and abort pins.",
coverage: VIRTUAL_HAL_COVERAGE_STATES.simulated,
pins: Object.freeze(VIRTUAL_HAL_AXISUI_PINS.pins.map((pin) => ({
...pin,
name: `${VIRTUAL_HAL_AXISUI_PINS.component}.${pin.name}`,
}))),
},
{
id: "halui-task",
component: "halui",
label: "HALUI task, mode, and program pins",
sourceFiles: Object.freeze([VIRTUAL_HAL_SOURCE_FILES.halui]),
sourceEvidence: "halui_hal_init exports machine, estop, mode, coolant, program, override, and MDI pins.",
coverage: VIRTUAL_HAL_COVERAGE_STATES.bridgeOnly,
pins: Object.freeze([
{ name: "halui.machine.units-per-mm", type: "HAL_FLOAT", direction: "HAL_OUT" },
{ name: "halui.machine.is-on", type: "HAL_BIT", direction: "HAL_OUT" },
{ name: "halui.machine.on", type: "HAL_BIT", direction: "HAL_IN" },
{ name: "halui.machine.off", type: "HAL_BIT", direction: "HAL_IN" },
{ name: "halui.estop.is-activated", type: "HAL_BIT", direction: "HAL_OUT" },
{ name: "halui.estop.activate", type: "HAL_BIT", direction: "HAL_IN" },
{ name: "halui.estop.reset", type: "HAL_BIT", direction: "HAL_IN" },
{ name: "halui.mode.is-manual", type: "HAL_BIT", direction: "HAL_OUT" },
{ name: "halui.mode.is-auto", type: "HAL_BIT", direction: "HAL_OUT" },
{ name: "halui.mode.is-mdi", type: "HAL_BIT", direction: "HAL_OUT" },
{ name: "halui.mode.is-teleop", type: "HAL_BIT", direction: "HAL_OUT" },
{ name: "halui.mode.is-joint", type: "HAL_BIT", direction: "HAL_OUT" },
{ name: "halui.mode.manual", type: "HAL_BIT", direction: "HAL_IN" },
{ name: "halui.mode.auto", type: "HAL_BIT", direction: "HAL_IN" },
{ name: "halui.mode.mdi", type: "HAL_BIT", direction: "HAL_IN" },
{ name: "halui.mist.is-on", type: "HAL_BIT", direction: "HAL_OUT" },
{ name: "halui.flood.is-on", type: "HAL_BIT", direction: "HAL_OUT" },
{ name: "halui.mist.on", type: "HAL_BIT", direction: "HAL_IN" },
{ name: "halui.mist.off", type: "HAL_BIT", direction: "HAL_IN" },
{ name: "halui.flood.on", type: "HAL_BIT", direction: "HAL_IN" },
{ name: "halui.flood.off", type: "HAL_BIT", direction: "HAL_IN" },
{ name: "halui.program.is-idle", type: "HAL_BIT", direction: "HAL_OUT" },
{ name: "halui.program.is-running", type: "HAL_BIT", direction: "HAL_OUT" },
{ name: "halui.program.is-paused", type: "HAL_BIT", direction: "HAL_OUT" },
{ name: "halui.halui-mdi-is-running", type: "HAL_BIT", direction: "HAL_OUT" },
{ name: "halui.program.run", type: "HAL_BIT", direction: "HAL_IN" },
{ name: "halui.program.pause", type: "HAL_BIT", direction: "HAL_IN" },
{ name: "halui.program.resume", type: "HAL_BIT", direction: "HAL_IN" },
{ name: "halui.program.step", type: "HAL_BIT", direction: "HAL_IN" },
{ name: "halui.program.stop", type: "HAL_BIT", direction: "HAL_IN" },
{ name: "halui.program.optional-stop.is-on", type: "HAL_BIT", direction: "HAL_OUT" },
{ name: "halui.program.optional-stop.on", type: "HAL_BIT", direction: "HAL_IN" },
{ name: "halui.program.optional-stop.off", type: "HAL_BIT", direction: "HAL_IN" },
{ name: "halui.program.block-delete.is-on", type: "HAL_BIT", direction: "HAL_OUT" },
{ name: "halui.program.block-delete.on", type: "HAL_BIT", direction: "HAL_IN" },
{ name: "halui.program.block-delete.off", type: "HAL_BIT", direction: "HAL_IN" },
{ name: "halui.abort", type: "HAL_BIT", direction: "HAL_IN" },
{ name: "halui.feed-override.value", type: "HAL_FLOAT", direction: "HAL_OUT" },
{ name: "halui.rapid-override.value", type: "HAL_FLOAT", direction: "HAL_OUT" },
{ name: "halui.max-velocity.value", type: "HAL_FLOAT", direction: "HAL_OUT" },
{ name: "halui.feed-override.counts", type: "HAL_S32", direction: "HAL_IN" },
{ name: "halui.feed-override.scale", type: "HAL_FLOAT", direction: "HAL_IN" },
{ name: "halui.feed-override.increase", type: "HAL_BIT", direction: "HAL_IN" },
{ name: "halui.feed-override.decrease", type: "HAL_BIT", direction: "HAL_IN" },
{ name: "halui.feed-override.reset", type: "HAL_BIT", direction: "HAL_IN" },
{ name: "halui.rapid-override.counts", type: "HAL_S32", direction: "HAL_IN" },
{ name: "halui.rapid-override.scale", type: "HAL_FLOAT", direction: "HAL_IN" },
{ name: "halui.rapid-override.increase", type: "HAL_BIT", direction: "HAL_IN" },
{ name: "halui.rapid-override.decrease", type: "HAL_BIT", direction: "HAL_IN" },
{ name: "halui.rapid-override.reset", type: "HAL_BIT", direction: "HAL_IN" },
{ name: "halui.mdi-command-00", type: "HAL_BIT", direction: "HAL_IN", template: true },
]),
},
{
id: "halui-axis-joint",
component: "halui",
label: "HALUI axis and joint pins",
sourceFiles: Object.freeze([VIRTUAL_HAL_SOURCE_FILES.halui]),
sourceEvidence: "halui_hal_init exports per-axis and per-joint status, selection, jogging, homing, and limit pins.",
coverage: VIRTUAL_HAL_COVERAGE_STATES.bridgeOnly,
pins: Object.freeze([
{ name: "halui.axis.{axis}.is-selected", type: "HAL_BIT", direction: "HAL_OUT", axes: true },
{ name: "halui.axis.{axis}.pos-commanded", type: "HAL_FLOAT", direction: "HAL_OUT", axes: true },
{ name: "halui.axis.{axis}.pos-feedback", type: "HAL_FLOAT", direction: "HAL_OUT", axes: true },
{ name: "halui.axis.{axis}.pos-relative", type: "HAL_FLOAT", direction: "HAL_OUT", axes: true },
{ name: "halui.axis.{axis}.select", type: "HAL_BIT", direction: "HAL_IN", axes: true },
{ name: "halui.axis.{axis}.plus", type: "HAL_BIT", direction: "HAL_IN", axes: true },
{ name: "halui.axis.{axis}.minus", type: "HAL_BIT", direction: "HAL_IN", axes: true },
{ name: "halui.axis.{axis}.analog", type: "HAL_FLOAT", direction: "HAL_IN", axes: true },
{ name: "halui.axis.{axis}.increment", type: "HAL_FLOAT", direction: "HAL_IN", axes: true },
{ name: "halui.joint.{joint}.is-homed", type: "HAL_BIT", direction: "HAL_OUT", joints: true },
{ name: "halui.joint.{joint}.is-selected", type: "HAL_BIT", direction: "HAL_OUT", joints: true },
{ name: "halui.joint.{joint}.home", type: "HAL_BIT", direction: "HAL_IN", joints: true },
{ name: "halui.joint.{joint}.unhome", type: "HAL_BIT", direction: "HAL_IN", joints: true },
{ name: "halui.joint.{joint}.plus", type: "HAL_BIT", direction: "HAL_IN", joints: true },
{ name: "halui.joint.{joint}.minus", type: "HAL_BIT", direction: "HAL_IN", joints: true },
{ name: "halui.joint.selected.is-homed", type: "HAL_BIT", direction: "HAL_OUT" },
{ name: "halui.joint.selected.home", type: "HAL_BIT", direction: "HAL_IN" },
{ name: "halui.axis.selected.plus", type: "HAL_BIT", direction: "HAL_IN" },
{ name: "halui.axis.selected.minus", type: "HAL_BIT", direction: "HAL_IN" },
]),
},
{
id: "iocontrol",
component: "iocontrol.0",
label: "IO/task, coolant, and tool-change pins",
sourceFiles: Object.freeze([VIRTUAL_HAL_SOURCE_FILES.iocontrol]),
sourceEvidence: "Task::iocontrol_hal_init adds iocontrol.0 enable, coolant, tool prepare, and tool change pins.",
coverage: VIRTUAL_HAL_COVERAGE_STATES.simulated,
pins: Object.freeze([
{ name: "iocontrol.0.user-enable-out", type: "HAL_BIT", direction: "HAL_OUT" },
{ name: "iocontrol.0.emc-enable-in", type: "HAL_BIT", direction: "HAL_IN" },
{ name: "iocontrol.0.user-request-enable", type: "HAL_BIT", direction: "HAL_OUT" },
{ name: "iocontrol.0.coolant-mist", type: "HAL_BIT", direction: "HAL_OUT" },
{ name: "iocontrol.0.coolant-flood", type: "HAL_BIT", direction: "HAL_OUT" },
{ name: "iocontrol.0.tool-prep-pocket", type: "HAL_S32", direction: "HAL_OUT" },
{ name: "iocontrol.0.tool-from-pocket", type: "HAL_S32", direction: "HAL_OUT" },
{ name: "iocontrol.0.tool-prep-index", type: "HAL_S32", direction: "HAL_OUT" },
{ name: "iocontrol.0.tool-prep-number", type: "HAL_S32", direction: "HAL_OUT" },
{ name: "iocontrol.0.tool-number", type: "HAL_S32", direction: "HAL_OUT" },
{ name: "iocontrol.0.tool-prepare", type: "HAL_BIT", direction: "HAL_OUT" },
{ name: "iocontrol.0.tool-prepared", type: "HAL_BIT", direction: "HAL_IN" },
{ name: "iocontrol.0.tool-change", type: "HAL_BIT", direction: "HAL_OUT" },
{ name: "iocontrol.0.tool-changed", type: "HAL_BIT", direction: "HAL_IN" },
]),
},
{
id: "motion-core",
component: "motion",
label: "Motion controller status pins",
sourceFiles: Object.freeze([VIRTUAL_HAL_SOURCE_FILES.motion, VIRTUAL_HAL_SOURCE_FILES.vendoredMotionHeader]),
sourceEvidence: "init_hal_io exports machine-wide motion enable, mode, velocity, feed, interpreter, and IO pins.",
coverage: VIRTUAL_HAL_COVERAGE_STATES.runtimeBoundary,
pins: Object.freeze([
{ name: "motion.enable", type: "HAL_BIT", direction: "HAL_IN" },
{ name: "motion.motion-enabled", type: "HAL_BIT", direction: "HAL_OUT" },
{ name: "motion.in-position", type: "HAL_BIT", direction: "HAL_OUT" },
{ name: "motion.motion-type", type: "HAL_S32", direction: "HAL_OUT" },
{ name: "motion.coord-mode", type: "HAL_BIT", direction: "HAL_OUT" },
{ name: "motion.teleop-mode", type: "HAL_BIT", direction: "HAL_OUT" },
{ name: "motion.current-vel", type: "HAL_FLOAT", direction: "HAL_OUT" },
{ name: "motion.requested-vel", type: "HAL_FLOAT", direction: "HAL_OUT" },
{ name: "motion.distance-to-go", type: "HAL_FLOAT", direction: "HAL_OUT" },
{ name: "motion.program-line", type: "HAL_S32", direction: "HAL_OUT" },
{ name: "motion.probe-input", type: "HAL_BIT", direction: "HAL_IN" },
{ name: "motion.adaptive-feed", type: "HAL_FLOAT", direction: "HAL_IN" },
{ name: "motion.feed-hold", type: "HAL_BIT", direction: "HAL_IN" },
{ name: "motion.feed-inhibit", type: "HAL_BIT", direction: "HAL_IN" },
{ name: "motion.homing-inhibit", type: "HAL_BIT", direction: "HAL_IN" },
{ name: "motion.jog-inhibit", type: "HAL_BIT", direction: "HAL_IN" },
{ name: "motion.switchkins-type", type: "HAL_FLOAT", direction: "HAL_IN", conditional: "switchable kinematics" },
]),
},
{
id: "axis-motion",
component: "axis",
label: "Motion axis pins",
sourceFiles: Object.freeze([VIRTUAL_HAL_SOURCE_FILES.motionAxis]),
sourceEvidence: "axis_init_hal_io exports per-axis position, teleop, jog, and external-offset pins.",
coverage: VIRTUAL_HAL_COVERAGE_STATES.simulated,
pins: Object.freeze([
{ name: "axis.{axis}.pos-cmd", type: "HAL_FLOAT", direction: "HAL_OUT", axes: true },
{ name: "axis.{axis}.teleop-vel-cmd", type: "HAL_FLOAT", direction: "HAL_OUT", axes: true },
{ name: "axis.{axis}.teleop-pos-cmd", type: "HAL_FLOAT", direction: "HAL_OUT", axes: true },
{ name: "axis.{axis}.teleop-vel-lim", type: "HAL_FLOAT", direction: "HAL_OUT", axes: true },
{ name: "axis.{axis}.teleop-tp-enable", type: "HAL_BIT", direction: "HAL_OUT", axes: true },
{ name: "axis.{axis}.jog-enable", type: "HAL_BIT", direction: "HAL_IN", axes: true },
{ name: "axis.{axis}.jog-scale", type: "HAL_FLOAT", direction: "HAL_IN", axes: true },
{ name: "axis.{axis}.jog-counts", type: "HAL_S32", direction: "HAL_IN", axes: true },
{ name: "axis.{axis}.jog-vel-mode", type: "HAL_BIT", direction: "HAL_IN", axes: true },
{ name: "axis.{axis}.eoffset-enable", type: "HAL_BIT", direction: "HAL_IN", axes: true },
{ name: "axis.{axis}.eoffset", type: "HAL_FLOAT", direction: "HAL_OUT", axes: true },
]),
},
{
id: "joint-motion",
component: "joint",
label: "Motion joint and homing pins",
sourceFiles: Object.freeze([VIRTUAL_HAL_SOURCE_FILES.motion, VIRTUAL_HAL_SOURCE_FILES.motionHoming]),
sourceEvidence: "export_joint and base_make_joint_home_pins export per-joint command, feedback, homing, limit, jog, and fault pins.",
coverage: VIRTUAL_HAL_COVERAGE_STATES.simulated,
pins: Object.freeze([
{ name: "joint.{joint}.coarse-pos-cmd", type: "HAL_FLOAT", direction: "HAL_OUT", joints: true },
{ name: "joint.{joint}.pos-cmd", type: "HAL_FLOAT", direction: "HAL_OUT", joints: true },
{ name: "joint.{joint}.pos-fb", type: "HAL_FLOAT", direction: "HAL_OUT", joints: true },
{ name: "joint.{joint}.motor-pos-cmd", type: "HAL_FLOAT", direction: "HAL_OUT", joints: true },
{ name: "joint.{joint}.motor-pos-fb", type: "HAL_FLOAT", direction: "HAL_IN", joints: true },
{ name: "joint.{joint}.pos-lim-sw-in", type: "HAL_BIT", direction: "HAL_IN", joints: true },
{ name: "joint.{joint}.neg-lim-sw-in", type: "HAL_BIT", direction: "HAL_IN", joints: true },
{ name: "joint.{joint}.amp-enable-out", type: "HAL_BIT", direction: "HAL_OUT", joints: true },
{ name: "joint.{joint}.amp-fault-in", type: "HAL_BIT", direction: "HAL_IN", joints: true },
{ name: "joint.{joint}.homed", type: "HAL_BIT", direction: "HAL_OUT", joints: true },
{ name: "joint.{joint}.homing", type: "HAL_BIT", direction: "HAL_OUT", joints: true },
{ name: "joint.{joint}.home-state", type: "HAL_S32", direction: "HAL_OUT", joints: true },
{ name: "joint.{joint}.home-sw-in", type: "HAL_BIT", direction: "HAL_IN", joints: true },
{ name: "joint.{joint}.pos-hard-limit", type: "HAL_BIT", direction: "HAL_OUT", joints: true },
{ name: "joint.{joint}.neg-hard-limit", type: "HAL_BIT", direction: "HAL_OUT", joints: true },
{ name: "joint.{joint}.error", type: "HAL_BIT", direction: "HAL_OUT", joints: true },
{ name: "joint.{joint}.faulted", type: "HAL_BIT", direction: "HAL_OUT", joints: true },
]),
},
{
id: "spindle-motion",
component: "spindle",
label: "Motion spindle pins",
sourceFiles: Object.freeze([VIRTUAL_HAL_SOURCE_FILES.motion, VIRTUAL_HAL_SOURCE_FILES.halui]),
sourceEvidence: "export_spindle and halui_hal_init export spindle command, status, override, orient, and feedback pins.",
coverage: VIRTUAL_HAL_COVERAGE_STATES.simulated,
pins: Object.freeze([
{ name: "spindle.0.on", type: "HAL_BIT", direction: "HAL_OUT" },
{ name: "spindle.0.forward", type: "HAL_BIT", direction: "HAL_OUT" },
{ name: "spindle.0.reverse", type: "HAL_BIT", direction: "HAL_OUT" },
{ name: "spindle.0.brake", type: "HAL_BIT", direction: "HAL_OUT" },
{ name: "spindle.0.speed-out", type: "HAL_FLOAT", direction: "HAL_OUT" },
{ name: "spindle.0.speed-out-abs", type: "HAL_FLOAT", direction: "HAL_OUT" },
{ name: "spindle.0.speed-out-rps", type: "HAL_FLOAT", direction: "HAL_OUT" },
{ name: "spindle.0.speed-in", type: "HAL_FLOAT", direction: "HAL_IN" },
{ name: "spindle.0.at-speed", type: "HAL_BIT", direction: "HAL_IN" },
{ name: "spindle.0.orient", type: "HAL_BIT", direction: "HAL_OUT" },
{ name: "spindle.0.is-oriented", type: "HAL_BIT", direction: "HAL_IN" },
{ name: "halui.spindle.0.is-on", type: "HAL_BIT", direction: "HAL_OUT" },
{ name: "halui.spindle.0.start", type: "HAL_BIT", direction: "HAL_IN" },
{ name: "halui.spindle.0.stop", type: "HAL_BIT", direction: "HAL_IN" },
{ name: "halui.spindle.0.override.value", type: "HAL_FLOAT", direction: "HAL_OUT" },
]),
},
{
id: "tool-status",
component: "tool",
label: "Tool status and tool-offset pins",
sourceFiles: Object.freeze([VIRTUAL_HAL_SOURCE_FILES.halui, VIRTUAL_HAL_SOURCE_FILES.iocontrol, VIRTUAL_HAL_SOURCE_FILES.motion]),
sourceEvidence: "HALUI tool status, iocontrol tool-change pins, and motion.tooloffset pins expose current tool and offsets.",
coverage: VIRTUAL_HAL_COVERAGE_STATES.bridgeOnly,
pins: Object.freeze([
{ name: "halui.tool.number", type: "HAL_U32", direction: "HAL_OUT" },
{ name: "halui.tool.diameter", type: "HAL_FLOAT", direction: "HAL_OUT" },
{ name: "halui.tool.length_offset.{axis}", type: "HAL_FLOAT", direction: "HAL_OUT", axes: true },
{ name: "motion.tooloffset.{axis}", type: "HAL_FLOAT", direction: "HAL_OUT", axes: true },
{ name: "iocontrol.0.tool-number", type: "HAL_S32", direction: "HAL_OUT" },
{ name: "iocontrol.0.tool-prepare", type: "HAL_BIT", direction: "HAL_OUT" },
{ name: "iocontrol.0.tool-prepared", type: "HAL_BIT", direction: "HAL_IN" },
{ name: "iocontrol.0.tool-change", type: "HAL_BIT", direction: "HAL_OUT" },
{ name: "iocontrol.0.tool-changed", type: "HAL_BIT", direction: "HAL_IN" },
]),
},
]);
export const VIRTUAL_HAL_WASM_BRIDGE_FUNCTIONS = Object.freeze([
"lcinterp_hal_reset",
"lcinterp_hal_set_value",
"lcinterp_probe_hal_named",
]);
export const VIRTUAL_HAL_PROJECT_PIN_GROUPS = Object.freeze([
{
id: "axisui",
label: "AXIS UI pins",
description: "Browser/manual AXIS component pins mirrored from linuxcnc/bin/axis.",
},
{
id: "axis-motion",
label: "Axis and joint motion pins",
description: "LinuxCNC motion axis/joint state pins exposed through source-derived virtual HAL coverage.",
},
{
id: "task",
label: "Task, HALUI, and iocontrol pins",
description: "ESTOP, power, mode, program, and IO pins from halui.cc and taskclass.cc.",
},
{
id: "spindle-coolant",
label: "Spindle and coolant pins",
description: "Spindle speed/on/orient and iocontrol/HALUI coolant pins used by project UI/status workflows.",
},
{
id: "tool",
label: "Tool pins",
description: "Tool status, tool change, and motion tool-offset pins from HALUI, iocontrol, and motion.",
},
]);
export const VIRTUAL_HAL_SIMULATION_RUNTIME_CAPABILITIES = Object.freeze([
"realtime-hal-simulation-replacement",
"halcmd-simulation-replacement",
"motion-controller-simulation-replacement",
"hal-pin-signal-param-store",
"halcmd-setp-sets-net-show-getp-gets",
"userspace-load-command-stubs",
"servo-period-motion-step",
"axis-joint-position-feedback",
"spindle-coolant-tool-status",
"interpreter-hal-bridge-snapshot",
]);
export const VIRTUAL_HAL_SIMULATION_REPLACEMENT_TARGETS = Object.freeze([
"linuxcnc-realtime-hal",
"halcmd",
"motion-controller",
]);
export const VIRTUAL_HAL_COMMAND_SCRIPT_FIXTURES = Object.freeze([
Object.freeze({
id: "pin-signal-net-show",
label: "HAL pin, signal, net, and show workflow",
sourceFiles: Object.freeze([
VIRTUAL_HAL_SOURCE_FILES.halcmd,
VIRTUAL_HAL_SOURCE_FILES.halcmdCommands,
VIRTUAL_HAL_SOURCE_FILES.halcmdMain,
VIRTUAL_HAL_SOURCE_FILES.halcmdBin,
]),
commands: Object.freeze([
"newsig x-pos HAL_FLOAT",
"setp axis.x.pos-cmd 2.5",
"sets x-pos 1.25",
"net x-pos axis.x.pos-cmd joint.0.pos-fb",
"show pin axis.x.*",
"show sig x-pos",
"getp axis.x.pos-cmd",
"gets x-pos",
]),
requiredActions: Object.freeze(["newsig", "setp", "sets", "net", "show", "getp", "gets"]),
expectedOutput: Object.freeze(["axis.x.pos-cmd", "x-pos"]),
}),
Object.freeze({
id: "load-thread-start-stop",
label: "HAL load, function, and thread workflow",
sourceFiles: Object.freeze([
VIRTUAL_HAL_SOURCE_FILES.halcmd,
VIRTUAL_HAL_SOURCE_FILES.halcmdCommands,
VIRTUAL_HAL_SOURCE_FILES.halcmdMain,
VIRTUAL_HAL_SOURCE_FILES.halcmdBin,
]),
commands: Object.freeze([
"loadrt trivkins",
"loadusr halui",
"addf motion-controller servo-thread",
"start servo-thread",
"show function motion-controller",
"stop servo-thread",
]),
requiredActions: Object.freeze(["loadrt", "loadusr", "addf", "start", "show", "stop"]),
expectedOutput: Object.freeze(["motion-controller"]),
}),
]);
export const VIRTUAL_HAL_MOTION_CONTROLLER_MATRIX_FIXTURES = Object.freeze([
Object.freeze({
id: "x-single-axis-servo-dtg",
label: "Single-axis servo period target stepping",
sourceFiles: Object.freeze([
VIRTUAL_HAL_SOURCE_FILES.motion,
VIRTUAL_HAL_SOURCE_FILES.motionAxis,
VIRTUAL_HAL_SOURCE_FILES.vendoredMotionHeader,
]),
initialPosition: Object.freeze({ x: 0, y: 0, z: 0 }),
target: Object.freeze({ x: 1 }),
maxVelocity: 10,
dt: 0.05,
cycleCount: 1,
requiredPins: Object.freeze([
"motion.distance-to-go",
"motion.in-position",
"axis.x.pos-cmd",
"joint.0.pos-fb",
]),
simConfigTargets: Object.freeze(["external-offsets"]),
expected: Object.freeze({
moving: true,
inPosition: false,
distanceToGo: 0.5,
axisFeedback: Object.freeze({ x: 0.5 }),
}),
}),
Object.freeze({
id: "xyz-multi-axis-in-position",
label: "Multi-axis target reaches in-position",
sourceFiles: Object.freeze([
VIRTUAL_HAL_SOURCE_FILES.motion,
VIRTUAL_HAL_SOURCE_FILES.motionAxis,
VIRTUAL_HAL_SOURCE_FILES.motionHoming,
VIRTUAL_HAL_SOURCE_FILES.vendoredMotionHeader,
]),
initialPosition: Object.freeze({ x: 0, y: 0, z: 0 }),
target: Object.freeze({ x: 1, y: -0.5, z: 0.25 }),
maxVelocity: 10,
dt: 0.1,
cycleCount: 4,
requiredPins: Object.freeze([
"motion.distance-to-go",
"motion.in-position",
"axis.x.pos-cmd",
"axis.y.pos-cmd",
"axis.z.pos-cmd",
"halui.axis.x.pos-feedback",
"joint.0.pos-fb",
"joint.1.pos-fb",
"joint.2.pos-fb",
]),
simConfigTargets: Object.freeze(["qtdragon-on-abort"]),
expected: Object.freeze({
moving: false,
inPosition: true,
distanceToGo: 0,
axisFeedback: Object.freeze({ x: 1, y: -0.5, z: 0.25 }),
}),
}),
Object.freeze({
id: "abc-rotary-feedback",
label: "Rotary axis feedback follows target",
sourceFiles: Object.freeze([
VIRTUAL_HAL_SOURCE_FILES.motion,
VIRTUAL_HAL_SOURCE_FILES.motionAxis,
VIRTUAL_HAL_SOURCE_FILES.vendoredMotionHeader,
]),
initialPosition: Object.freeze({ a: 0, b: 0, c: 0 }),
target: Object.freeze({ a: 0.4, b: -0.2, c: 0.1 }),
maxVelocity: 8,
dt: 0.1,
cycleCount: 2,
requiredPins: Object.freeze([
"motion.current-vel",
"motion.requested-vel",
"axis.a.pos-cmd",
"axis.b.pos-cmd",
"axis.c.pos-cmd",
"joint.3.pos-fb",
"joint.4.pos-fb",
"joint.5.pos-fb",
]),
simConfigTargets: Object.freeze(["vismach-remap-sims"]),
expected: Object.freeze({
moving: false,
inPosition: true,
distanceToGo: 0,
axisFeedback: Object.freeze({ a: 0.4, b: -0.2, c: 0.1 }),
}),
}),
]);
export const VIRTUAL_HAL_SOURCE_DERIVED_CAPABILITIES = Object.freeze({
"realtime-hal-simulation-replacement": Object.freeze({
label: "LinuxCNC realtime HAL simulation replacement",
sourceFiles: Object.freeze([
VIRTUAL_HAL_SOURCE_FILES.axisui,
VIRTUAL_HAL_SOURCE_FILES.axisuiScript,
VIRTUAL_HAL_SOURCE_FILES.halui,
VIRTUAL_HAL_SOURCE_FILES.iocontrol,
VIRTUAL_HAL_SOURCE_FILES.motion,
VIRTUAL_HAL_SOURCE_FILES.motionAxis,
VIRTUAL_HAL_SOURCE_FILES.motionHoming,
VIRTUAL_HAL_SOURCE_FILES.vendoredMotionHeader,
]),
evidence: "Virtual HAL pin families are derived from LinuxCNC AXIS, HALUI, iocontrol, and motion source exports.",
}),
"halcmd-simulation-replacement": Object.freeze({
label: "LinuxCNC halcmd simulation replacement",
sourceFiles: Object.freeze([
VIRTUAL_HAL_SOURCE_FILES.halcmd,
VIRTUAL_HAL_SOURCE_FILES.halcmdCommands,
VIRTUAL_HAL_SOURCE_FILES.halcmdMain,
VIRTUAL_HAL_SOURCE_FILES.halcmdBin,
]),
evidence: "Virtual halcmd command verbs mirror LinuxCNC halcmd command surface for simulation workflows.",
}),
"motion-controller-simulation-replacement": Object.freeze({
label: "LinuxCNC motion controller simulation replacement",
sourceFiles: Object.freeze([
VIRTUAL_HAL_SOURCE_FILES.motion,
VIRTUAL_HAL_SOURCE_FILES.motionAxis,
VIRTUAL_HAL_SOURCE_FILES.motionHoming,
VIRTUAL_HAL_SOURCE_FILES.vendoredMotionHeader,
]),
evidence: "Virtual motion controller state and pins are derived from LinuxCNC motion, axis, homing, and motion header exports.",
}),
"hal-pin-signal-param-store": Object.freeze({
label: "HAL pin/signal/param store",
sourceFiles: Object.freeze([
VIRTUAL_HAL_SOURCE_FILES.halcmd,
VIRTUAL_HAL_SOURCE_FILES.halcmdCommands,
VIRTUAL_HAL_SOURCE_FILES.axisui,
VIRTUAL_HAL_SOURCE_FILES.halui,
]),
evidence: "HAL object store behavior is constrained to LinuxCNC HAL pin families and halcmd pin/signal/param verbs.",
}),
"halcmd-setp-sets-net-show-getp-gets": Object.freeze({
label: "halcmd setp/sets/net/show/getp/gets verbs",
sourceFiles: Object.freeze([
VIRTUAL_HAL_SOURCE_FILES.halcmd,
VIRTUAL_HAL_SOURCE_FILES.halcmdCommands,
VIRTUAL_HAL_SOURCE_FILES.halcmdMain,
VIRTUAL_HAL_SOURCE_FILES.halcmdBin,
]),
evidence: "Virtual halcmd supports the LinuxCNC halcmd verbs needed by Web simulation fixtures.",
}),
"userspace-load-command-stubs": Object.freeze({
label: "loadrt/loadusr/addf/start/stop command stubs",
sourceFiles: Object.freeze([
VIRTUAL_HAL_SOURCE_FILES.halcmd,
VIRTUAL_HAL_SOURCE_FILES.halcmdCommands,
VIRTUAL_HAL_SOURCE_FILES.halcmdMain,
]),
evidence: "Userspace/realtime load command records follow LinuxCNC halcmd command names while remaining simulation stubs.",
}),
"servo-period-motion-step": Object.freeze({
label: "Servo-period motion step",
sourceFiles: Object.freeze([
VIRTUAL_HAL_SOURCE_FILES.motion,
VIRTUAL_HAL_SOURCE_FILES.motionAxis,
VIRTUAL_HAL_SOURCE_FILES.vendoredMotionHeader,
]),
evidence: "Servo-period stepping exposes LinuxCNC motion pin concepts: target, velocity, distance-to-go, and in-position.",
}),
"axis-joint-position-feedback": Object.freeze({
label: "Axis/joint position feedback",
sourceFiles: Object.freeze([
VIRTUAL_HAL_SOURCE_FILES.motionAxis,
VIRTUAL_HAL_SOURCE_FILES.motion,
VIRTUAL_HAL_SOURCE_FILES.motionHoming,
]),
evidence: "Axis and joint feedback rows are derived from LinuxCNC axis/joint/homing HAL pin families.",
}),
"spindle-coolant-tool-status": Object.freeze({
label: "Spindle, coolant, and tool status",
sourceFiles: Object.freeze([
VIRTUAL_HAL_SOURCE_FILES.motion,
VIRTUAL_HAL_SOURCE_FILES.halui,
VIRTUAL_HAL_SOURCE_FILES.iocontrol,
]),
evidence: "Spindle, coolant, and tool state rows are derived from LinuxCNC motion, HALUI, and iocontrol pin families.",
}),
"interpreter-hal-bridge-snapshot": Object.freeze({
label: "Interpreter HAL bridge snapshot",
sourceFiles: Object.freeze([
VIRTUAL_HAL_SOURCE_FILES.axisui,
VIRTUAL_HAL_SOURCE_FILES.halui,
VIRTUAL_HAL_SOURCE_FILES.iocontrol,
VIRTUAL_HAL_SOURCE_FILES.motion,
VIRTUAL_HAL_SOURCE_FILES.motionAxis,
VIRTUAL_HAL_SOURCE_FILES.motionHoming,
VIRTUAL_HAL_SOURCE_FILES.vendoredMotionHeader,
]),
evidence: "Bridge snapshots only serialize source-derived virtual HAL pins and values into the LinuxCNC-backed interpreter HAL adapter.",
}),
});
export function createVirtualHalState(overrides = {}) {
const base = {
apiName: "linuxcnc-wasm-virtual-hal-state",
halVersion: 1,
source: "browser-virtual-hal",
component: VIRTUAL_HAL_AXISUI_PINS.component,
axisuiPins: VIRTUAL_HAL_AXISUI_PINS.pins.map((pin) => ({ ...pin })),
revision: 0,
positionDirty: false,
machineDirty: false,
activeAxis: "x",
task: {
estop: true,
powered: false,
state: "ESTOP",
mode: "manual",
},
pins: Object.fromEntries(VIRTUAL_HAL_AXISUI_PINS.pins.map((pin) => [pin.name, pin.type === "HAL_FLOAT" ? 0 : false])),
externalPins: {},
position: Object.fromEntries(VIRTUAL_HAL_AXES.map((axis) => [axis, 0])),
homed: Object.fromEntries(VIRTUAL_HAL_AXES.map((axis) => [axis, false])),
limits: Object.fromEntries(VIRTUAL_HAL_AXES.map((axis) => [
axis,
{ minLimit: false, maxLimit: false, fault: "none" },
])),
spindle: {
state: "off",
direction: "stopped",
speed: 0,
brake: false,
},
coolant: {
mist: "off",
flood: "off",
},
overrides: {
feed: 100,
rapid: 100,
spindle: 100,
limits: false,
},
program: {
state: "idle",
optionalStop: false,
blockDelete: false,
haluiMdiRunning: false,
line: 0,
},
tool: {
selected: 0,
current: 0,
pocket: 0,
prepPocket: 0,
fromPocket: 0,
prepIndex: 0,
prepNumber: 0,
prepare: false,
prepared: true,
change: false,
changed: true,
diameter: 0,
lengthOffset: Object.fromEntries(VIRTUAL_HAL_AXES.map((axis) => [axis, 0])),
},
motion: {
enabled: false,
inPosition: true,
motionType: 0,
coordMode: false,
teleopMode: false,
currentVel: 0,
requestedVel: 0,
distanceToGo: 0,
programLine: 0,
probeInput: false,
adaptiveFeed: 1,
feedHold: false,
feedInhibit: false,
homingInhibit: false,
jogInhibit: false,
jogStop: false,
switchkinsType: 0,
target: Object.fromEntries(VIRTUAL_HAL_AXES.map((axis) => [axis, 0])),
servoPeriod: 0.001,
},
io: {
digitalIn: {},
digitalOut: {},
analogIn: {},
analogOut: {},
},
hal: {
signals: {},
params: {},
nets: {},
functions: [],
threads: {
"servo-thread": {
periodNs: 1000000,
running: true,
},
},
commandLog: [],
loadedComponents: [],
},
jog: {
increment: "Continuous",
incrementValue: 0,
lastAction: "idle",
},
notifications: {
clear: false,
clearInfo: false,
clearError: false,
resumeInhibit: false,
error: false,
abort: false,
},
};
return normalizeVirtualHalState({
...base,
...overrides,
task: { ...base.task, ...(overrides.task ?? {}) },
pins: { ...base.pins, ...(overrides.pins ?? {}) },
externalPins: { ...base.externalPins, ...(overrides.externalPins ?? {}) },
position: { ...base.position, ...(overrides.position ?? {}) },
homed: { ...base.homed, ...(overrides.homed ?? {}) },
limits: mergeAxisObject(base.limits, overrides.limits),
spindle: { ...base.spindle, ...(overrides.spindle ?? {}) },
coolant: { ...base.coolant, ...(overrides.coolant ?? {}) },
overrides: { ...base.overrides, ...(overrides.overrides ?? {}) },
program: { ...base.program, ...(overrides.program ?? {}) },
tool: {
...base.tool,
...(overrides.tool ?? {}),
lengthOffset: { ...base.tool.lengthOffset, ...(overrides.tool?.lengthOffset ?? {}) },
},
motion: { ...base.motion, ...(overrides.motion ?? {}) },
io: {
digitalIn: { ...base.io.digitalIn, ...(overrides.io?.digitalIn ?? {}) },
digitalOut: { ...base.io.digitalOut, ...(overrides.io?.digitalOut ?? {}) },
analogIn: { ...base.io.analogIn, ...(overrides.io?.analogIn ?? {}) },
analogOut: { ...base.io.analogOut, ...(overrides.io?.analogOut ?? {}) },
},
hal: {
signals: { ...base.hal.signals, ...(overrides.hal?.signals ?? {}) },
params: { ...base.hal.params, ...(overrides.hal?.params ?? {}) },
nets: { ...base.hal.nets, ...(overrides.hal?.nets ?? {}) },
functions: [...base.hal.functions, ...(overrides.hal?.functions ?? [])],
threads: { ...base.hal.threads, ...(overrides.hal?.threads ?? {}) },
commandLog: [...base.hal.commandLog, ...(overrides.hal?.commandLog ?? [])],
loadedComponents: [...base.hal.loadedComponents, ...(overrides.hal?.loadedComponents ?? [])],
},
jog: { ...base.jog, ...(overrides.jog ?? {}) },
notifications: { ...base.notifications, ...(overrides.notifications ?? {}) },
});
}
function mergeAxisObject(base, override = {}) {
return Object.fromEntries(Object.entries(base).map(([axis, value]) => [
axis,
{ ...value, ...(override?.[axis] ?? {}) },
]));
}
export function cloneVirtualHalState(state) {
return {
...state,
task: { ...(state.task ?? {}) },
axisuiPins: (state.axisuiPins ?? VIRTUAL_HAL_AXISUI_PINS.pins).map((pin) => ({ ...pin })),
pins: { ...(state.pins ?? {}) },
externalPins: { ...(state.externalPins ?? {}) },
position: { ...(state.position ?? {}) },
homed: { ...(state.homed ?? {}) },
limits: Object.fromEntries(Object.entries(state.limits ?? {}).map(([axis, limit]) => [axis, { ...limit }])),
spindle: { ...(state.spindle ?? {}) },
coolant: { ...(state.coolant ?? {}) },
overrides: { ...(state.overrides ?? {}) },
program: { ...(state.program ?? {}) },
tool: {
...(state.tool ?? {}),
lengthOffset: { ...(state.tool?.lengthOffset ?? {}) },
},
motion: { ...(state.motion ?? {}) },
io: {
digitalIn: { ...(state.io?.digitalIn ?? {}) },
digitalOut: { ...(state.io?.digitalOut ?? {}) },
analogIn: { ...(state.io?.analogIn ?? {}) },
analogOut: { ...(state.io?.analogOut ?? {}) },
},
hal: {
signals: Object.fromEntries(Object.entries(state.hal?.signals ?? {}).map(([name, signal]) => [name, { ...signal, pins: [...(signal.pins ?? [])] }])),
params: { ...(state.hal?.params ?? {}) },
nets: Object.fromEntries(Object.entries(state.hal?.nets ?? {}).map(([name, pins]) => [name, [...pins]])),
functions: (state.hal?.functions ?? []).map((item) => ({ ...item })),
threads: Object.fromEntries(Object.entries(state.hal?.threads ?? {}).map(([name, thread]) => [name, { ...thread }])),
commandLog: (state.hal?.commandLog ?? []).map((item) => ({ ...item })),
loadedComponents: (state.hal?.loadedComponents ?? []).map((item) => ({ ...item })),
},
jog: { ...(state.jog ?? {}) },
notifications: { ...(state.notifications ?? {}) },
};
}
function normalizeVirtualHalState(state) {
const next = cloneVirtualHalState({
...state,
axisuiPins: state.axisuiPins ?? VIRTUAL_HAL_AXISUI_PINS.pins,
pins: state.pins ?? {},
externalPins: state.externalPins ?? {},
hal: state.hal ?? {},
notifications: state.notifications ?? {},
});
next.task.state = next.task.estop ? "ESTOP" : (next.task.powered ? "ON" : "ESTOP_RESET");
next.task.powered = next.task.state === "ON";
next.task.estop = next.task.state === "ESTOP";
next.task.mode = ["manual", "auto", "mdi"].includes(next.task.mode) ? next.task.mode : "manual";
next.spindle.speed = clampNumber(next.spindle.speed, 0, 99999);
if (next.spindle.state !== "on") {
next.spindle.direction = "stopped";
} else if (!["cw", "ccw"].includes(next.spindle.direction)) {
next.spindle.direction = "cw";
}
next.coolant.mist = next.coolant.mist === "on" ? "on" : "off";
next.coolant.flood = next.coolant.flood === "on" ? "on" : "off";
next.overrides.feed = clampNumber(next.overrides.feed, 0, 200);
next.overrides.rapid = clampNumber(next.overrides.rapid, 0, 200);
next.overrides.spindle = clampNumber(next.overrides.spindle, 0, 200);
next.program.state = ["idle", "running", "paused"].includes(next.program.state) ? next.program.state : "idle";
next.program.optionalStop = next.program.optionalStop === true;
next.program.blockDelete = next.program.blockDelete === true;
next.program.haluiMdiRunning = next.program.haluiMdiRunning === true;
next.program.line = clampInteger(next.program.line, 0, 2147483647);
next.tool.selected = clampInteger(next.tool.selected, 0, 2147483647);
next.tool.current = clampInteger(next.tool.current, 0, 2147483647);
next.tool.pocket = clampInteger(next.tool.pocket, 0, 2147483647);
next.tool.prepPocket = clampInteger(next.tool.prepPocket, 0, 2147483647);
next.tool.fromPocket = clampInteger(next.tool.fromPocket, 0, 2147483647);
next.tool.prepIndex = clampInteger(next.tool.prepIndex, 0, 2147483647);
next.tool.prepNumber = clampInteger(next.tool.prepNumber, 0, 2147483647);
next.tool.prepare = next.tool.prepare === true;
next.tool.prepared = next.tool.prepared !== false;
next.tool.change = next.tool.change === true;
next.tool.changed = next.tool.changed !== false;
next.tool.diameter = clampNumber(next.tool.diameter, 0, 99999);
next.motion.enabled = next.task.powered && !next.task.estop;
next.motion.inPosition = next.motion.inPosition !== false;
next.motion.motionType = clampInteger(next.motion.motionType, 0, 2147483647);
next.motion.currentVel = clampNumber(next.motion.currentVel, 0, 999999);
next.motion.requestedVel = clampNumber(next.motion.requestedVel, 0, 999999);
next.motion.distanceToGo = clampNumber(next.motion.distanceToGo, 0, 999999);
next.motion.programLine = clampInteger(next.motion.programLine || next.program.line, 0, 2147483647);
next.motion.probeInput = next.motion.probeInput === true;
next.motion.adaptiveFeed = clampNumber(next.motion.adaptiveFeed, 0, 1);
next.motion.feedHold = next.motion.feedHold === true;
next.motion.feedInhibit = next.motion.feedInhibit === true;
next.motion.homingInhibit = next.motion.homingInhibit === true;
next.motion.jogInhibit = next.motion.jogInhibit === true;
next.motion.jogStop = next.motion.jogStop === true;
next.motion.switchkinsType = clampNumber(next.motion.switchkinsType, 0, 999);
next.motion.servoPeriod = clampNumber(next.motion.servoPeriod, 0.000001, 1);
next.motion.target = { ...(next.motion.target ?? {}) };
next.jog.incrementValue = parseVirtualHalJogIncrement(next.jog.increment);
next.pins["jog.increment"] = next.jog.incrementValue;
next.pins["notifications-clear"] = next.notifications.clear === true;
next.pins["notifications-clear-info"] = next.notifications.clearInfo === true;
next.pins["notifications-clear-error"] = next.notifications.clearError === true;
next.pins["resume-inhibit"] = next.notifications.resumeInhibit === true;
next.pins.error = next.notifications.error === true;
next.pins.abort = next.notifications.abort === true;
for (const axis of VIRTUAL_HAL_AXES) {
next.position[axis] = Number.isFinite(Number(next.position[axis])) ? Number(next.position[axis]) : 0;
next.homed[axis] = next.homed[axis] === true;
next.limits[axis] = {
minLimit: next.limits[axis]?.minLimit === true,
maxLimit: next.limits[axis]?.maxLimit === true,
fault: next.limits[axis]?.fault ?? "none",
};
next.pins[`jog.${axis}`] = next.pins[`jog.${axis}`] === true;
next.tool.lengthOffset[axis] = Number.isFinite(Number(next.tool.lengthOffset[axis])) ? Number(next.tool.lengthOffset[axis]) : 0;
next.motion.target[axis] = Number.isFinite(Number(next.motion.target[axis])) ? Number(next.motion.target[axis]) : next.position[axis];
}
next.hal.signals = Object.fromEntries(Object.entries(next.hal.signals ?? {}).map(([name, signal]) => [
name,
{
name,
type: signal.type ?? inferVirtualHalPinType(signal.value),
value: normalizeHalNumericValue(signal.value),
pins: [...new Set(signal.pins ?? [])],
},
]));
next.hal.params = Object.fromEntries(Object.entries(next.hal.params ?? {}).map(([name, entry]) => [
name,
typeof entry === "object" && entry !== null
? { ...entry, value: normalizeHalNumericValue(entry.value) }
: { kind: "param", name, type: inferVirtualHalPinType(entry), value: normalizeHalNumericValue(entry), connected: true },
]));
next.hal.nets = Object.fromEntries(Object.entries(next.hal.nets ?? {}).map(([name, pins]) => [name, [...new Set(pins ?? [])]]));
next.hal.functions = (next.hal.functions ?? []).map((item) => ({
name: item.name,
thread: item.thread ?? "servo-thread",
})).filter(({ name }) => name);
next.hal.threads = Object.fromEntries(Object.entries(next.hal.threads ?? {}).map(([name, thread]) => [
name,
{
periodNs: clampInteger(thread.periodNs ?? 1000000, 1, 1000000000),
running: thread.running !== false,
},
]));
next.hal.commandLog = (next.hal.commandLog ?? []).slice(-200).map((item) => ({ ...item }));
next.hal.loadedComponents = (next.hal.loadedComponents ?? []).map((item) => ({ ...item }));
return next;
}
function clampNumber(value, min, max) {
const numeric = Number(value);
if (!Number.isFinite(numeric)) {
return min;
}
return Math.min(Math.max(numeric, min), max);
}
function clampInteger(value, min, max) {
return Math.trunc(clampNumber(value, min, max));
}
function normalizeHalNumericValue(value) {
if (typeof value === "boolean") {
return value ? 1 : 0;
}
const numeric = Number(value);
return Number.isFinite(numeric) ? numeric : 0;
}
export function parseVirtualHalJogIncrement(increment) {
const text = String(increment ?? "Continuous").trim();
if (!text || /^continuous$/i.test(text)) {
return 0;
}
const match = text.match(/^([-+0-9./]+)\s*(in|inch|mm|cm|um|mil)?$/i);
if (!match) {
return Number(text) || 0;
}
let value = 0;
if (match[1].includes("/")) {
const [numerator, denominator] = match[1].split("/").map(Number);
value = denominator ? numerator / denominator : 0;
} else {
value = Number(match[1]);
}
const unit = (match[2] ?? "in").toLowerCase();
if (unit === "mm") {
value /= 25.4;
} else if (unit === "cm") {
value /= 2.54;
} else if (unit === "um") {
value /= 25400;
} else if (unit === "mil") {
value /= 1000;
}
return Number.isFinite(value) ? value : 0;
}
export function createVirtualHalDroState(halState) {
const state = normalizeVirtualHalState(halState);
const actual = Object.fromEntries(VIRTUAL_HAL_DISPLAY_AXES.map((axis) => [axis, formatAxis(state.position[axis])]));
const zeroAxes = Object.fromEntries(VIRTUAL_HAL_DISPLAY_AXES.map((axis) => [axis, formatAxis(0)]));
return {
apiName: "linuxcnc-wasm-virtual-hal-dro-state",
droVersion: 1,
source: "browser-virtual-hal",
actual,
distanceToGo: { ...zeroAxes },
workOffsetG54: { ...zeroAxes },
g92Offset: { ...zeroAxes },
toolLengthOffset: { ...zeroAxes },
velocity: formatAxis(0),
};
}
export function createVirtualHalLimitsHomeState(halState) {
const state = normalizeVirtualHalState(halState);
return {
apiName: "linuxcnc-wasm-virtual-hal-limits-home-state",
stateVersion: 1,
source: "browser-virtual-hal",
axes: Object.fromEntries(VIRTUAL_HAL_DISPLAY_AXES.map((axis) => {
const limit = state.limits[axis];
return [axis, {
homed: state.homed[axis] ? "yes" : "no",
minLimit: limit.minLimit ? "on" : "off",
maxLimit: limit.maxLimit ? "on" : "off",
fault: limit.fault,
}];
})),
};
}
export function createVirtualHalMachineStatusState(halState) {
const state = normalizeVirtualHalState(halState);
return {
apiName: "linuxcnc-wasm-virtual-hal-machine-status-state",
statusVersion: 1,
source: "browser-virtual-hal",
spindle: {
direction: state.spindle.direction,
speed: `${state.spindle.speed}`,
state: state.spindle.state,
},
coolant: { ...state.coolant },
tool: {
selected: "0",
current: "0",
pocket: "0",
lengthOffset: "z=0.000",
},
overrides: {
feed: state.overrides.feed > 0 ? "enabled" : "disabled",
speed: state.overrides.spindle > 0 ? "enabled" : "disabled",
adaptiveFeed: "disabled",
feedHold: "enabled",
},
};
}
export function createVirtualHalSystemCoverageReport(options = {}) {
const halState = options.halState ?? createVirtualHalState();
const snapshot = options.snapshot ?? createVirtualHalWasmBridgeSnapshot(halState);
const families = options.families ?? VIRTUAL_HAL_SYSTEM_PIN_FAMILIES;
const requiredFamilyIds = options.requiredFamilyIds ?? families.map(({ id }) => id);
const familyIds = new Set(families.map(({ id }) => id));
const missingFamilies = requiredFamilyIds.filter((id) => !familyIds.has(id));
const snapshotPins = new Set((snapshot.values ?? [])
.filter(({ kind }) => kind === "pin")
.map(({ name }) => name));
const declaredRows = families.flatMap((family) => expandVirtualHalFamilyPins(family).map((pin) => ({
id: `${family.id}:${pin.name}`,
familyId: family.id,
familyLabel: family.label,
component: family.component,
name: pin.name,
type: pin.type,
direction: pin.direction,
coverage: family.coverage,
sourceFiles: [...family.sourceFiles],
sourceEvidence: family.sourceEvidence,
representedInSnapshot: snapshotPins.has(pin.name),
template: pin.template === true,
conditional: pin.conditional ?? null,
})));
const declaredPinNames = new Set(declaredRows.map(({ name }) => name));
const unclassifiedSnapshotPins = [...snapshotPins].filter((name) => {
if (declaredPinNames.has(name)) {
return false;
}
return !isInternalAxisUiPinName(name);
});
const missingSourceFamilies = families
.filter((family) => !Array.isArray(family.sourceFiles) || family.sourceFiles.length === 0)
.map(({ id }) => id);
const coverageCounts = Object.fromEntries(
Object.values(VIRTUAL_HAL_COVERAGE_STATES).map((coverage) => [
coverage,
families.filter((family) => family.coverage === coverage).length,
]),
);
const boundaryFamilies = families.filter((family) => family.coverage !== VIRTUAL_HAL_COVERAGE_STATES.simulated);
const complete = missingFamilies.length === 0 &&
missingSourceFamilies.length === 0 &&
unclassifiedSnapshotPins.length === 0 &&
declaredRows.length > 0;
return {
apiName: "linuxcnc-wasm-virtual-hal-system-coverage-report",
coverageVersion: 1,
source: "linuxcnc-source-derived-virtual-hal",
phase: complete ? "covered-with-runtime-boundaries" : "incomplete",
complete,
realtimeHalRuntime: false,
sourceFiles: [...new Set(families.flatMap((family) => family.sourceFiles))],
familyCount: families.length,
declaredPinCount: declaredRows.length,
snapshotPinCount: snapshot.values?.length ?? 0,
coverageCounts,
requiredFamilyIds: [...requiredFamilyIds],
missingFamilies,
missingSourceFamilies,
unclassifiedSnapshotPins,
families: families.map((family) => {
const familyRows = declaredRows.filter((row) => row.familyId === family.id);
return {
id: family.id,
component: family.component,
label: family.label,
coverage: family.coverage,
sourceFiles: [...family.sourceFiles],
sourceEvidence: family.sourceEvidence,
declaredPinCount: familyRows.length,
representedPinCount: familyRows.filter(({ representedInSnapshot }) => representedInSnapshot).length,
};
}),
boundaryRows: boundaryFamilies.map((family) => ({
id: family.id,
label: family.label,
coverage: family.coverage,
reason: family.coverage === VIRTUAL_HAL_COVERAGE_STATES.runtimeBoundary
? "Requires LinuxCNC realtime task/motion/HAL process evidence, not JavaScript emulation."
: "Source pin family is inventoried and bridged where useful, but process semantics remain LinuxCNC-owned.",
sourceFiles: [...family.sourceFiles],
})),
declaredRows,
rows: [
{
id: "families",
label: "Source-derived HAL families",
value: `${families.length}`,
},
{
id: "declared-pins",
label: "Declared source pins",
value: `${declaredRows.length}`,
},
{
id: "snapshot-pins",
label: "Bridge snapshot pins",
value: `${snapshot.values?.length ?? 0}`,
},
{
id: "runtime-boundaries",
label: "Explicit runtime boundaries",
value: `${boundaryFamilies.length}`,
},
],
};
}
export function applyVirtualHalAction(halState, action, options = {}) {
const next = normalizeVirtualHalState(halState);
const axis = VIRTUAL_HAL_AXES.includes(String(options.axis ?? next.activeAxis).toLowerCase())
? String(options.axis ?? next.activeAxis).toLowerCase()
: "x";
const increment = options.increment ?? next.jog.increment;
const direction = Number(options.direction ?? 1) >= 0 ? 1 : -1;
next.activeAxis = axis;
next.positionDirty = false;
next.machineDirty = true;
if (action === "estop") {
if (next.task.estop) {
next.task.estop = false;
next.task.powered = false;
next.task.state = "ESTOP_RESET";
} else {
next.task.estop = true;
next.task.powered = false;
next.task.state = "ESTOP";
next.notifications.abort = true;
next.spindle.state = "off";
next.coolant.mist = "off";
next.coolant.flood = "off";
}
next.jog.lastAction = next.task.state;
} else if (action === "power") {
if (next.task.estop) {
next.task.powered = false;
next.task.state = "ESTOP";
} else {
next.task.powered = !next.task.powered;
next.task.state = next.task.powered ? "ON" : "ESTOP_RESET";
}
next.jog.lastAction = next.task.state;
} else if (action === "home" || action === "home-all") {
if (next.task.powered && !next.task.estop) {
const axes = action === "home-all" ? VIRTUAL_HAL_AXES : [axis];
for (const item of axes) {
next.homed[item] = true;
next.limits[item] = { minLimit: false, maxLimit: false, fault: "none" };
}
next.jog.lastAction = action;
} else {
next.notifications.error = true;
next.limits[axis].fault = "machine-not-on";
next.jog.lastAction = `${action}-blocked`;
}
} else if (action === "unhome-all") {
for (const item of VIRTUAL_HAL_AXES) {
next.homed[item] = false;
}
next.jog.lastAction = action;
} else if (action === "jog") {
for (const item of VIRTUAL_HAL_AXES) {
next.pins[`jog.${item}`] = false;
}
next.jog.increment = increment;
next.jog.incrementValue = parseVirtualHalJogIncrement(increment);
next.pins[`jog.${axis}`] = true;
next.pins["jog.increment"] = next.jog.incrementValue;
const distance = next.jog.incrementValue || Number(options.continuousStep ?? 0.1);
if (next.task.powered && !next.task.estop) {
next.position[axis] += direction * distance;
next.positionDirty = true;
next.jog.lastAction = `jog ${axis} ${direction > 0 ? "+" : "-"}`;
} else {
next.notifications.error = true;
next.jog.lastAction = `jog ${axis} blocked`;
}
} else if (action === "touch-off" || action === "zero-g54") {
next.position[axis] = Number(options.value ?? 0) || 0;
next.positionDirty = true;
next.jog.lastAction = `${action} ${axis}`;
} else if (action === "tool-touch-off") {
next.position.z = Number(options.value ?? next.position.z) || 0;
next.positionDirty = true;
next.jog.lastAction = "tool touch off";
} else if (action === "spindle") {
const command = options.command ?? "toggle";
if (command === "stop" || (command === "toggle" && next.spindle.state === "on")) {
next.spindle.state = "off";
next.spindle.direction = "stopped";
} else {
next.spindle.state = "on";
next.spindle.direction = command === "ccw" ? "ccw" : "cw";
}
next.spindle.speed = clampNumber(options.speed ?? (next.spindle.speed || 1200), 0, 99999);
next.jog.lastAction = `spindle ${next.spindle.direction}`;
} else if (action === "coolant") {
const name = options.name === "flood" ? "flood" : "mist";
next.coolant[name] = options.enabled === true ? "on" : "off";
next.jog.lastAction = `coolant ${name} ${next.coolant[name]}`;
} else if (action === "override-limits") {
next.overrides.limits = options.enabled === true;
next.jog.lastAction = `override limits ${next.overrides.limits ? "on" : "off"}`;
} else if (action === "spindle-brake") {
next.spindle.brake = options.enabled === true;
next.jog.lastAction = `spindle brake ${next.spindle.brake ? "on" : "off"}`;
} else if (action === "clear-notifications") {
next.notifications.clear = true;
next.notifications.clearInfo = options.level === "info";
next.notifications.clearError = options.level === "error";
next.notifications.error = false;
next.notifications.abort = false;
next.jog.lastAction = "clear notifications";
} else {
next.jog.lastAction = String(action ?? "manual");
}
next.revision += 1;
return normalizeVirtualHalState(next);
}
export function createVirtualHalWasmBridgeSnapshot(halState) {
const state = normalizeVirtualHalState(halState);
return {
apiName: "linuxcnc-wasm-virtual-hal-bridge-snapshot",
bridgeVersion: 1,
source: "browser-virtual-hal",
values: [
...Object.entries(state.pins).flatMap(([name, value]) => {
const entry = {
kind: "pin",
name,
type: typeof value === "number" ? "HAL_FLOAT" : "HAL_BIT",
value: typeof value === "number" ? value : value === true ? 1 : 0,
connected: true,
};
return [
entry,
{
...entry,
name: `${state.component}.${name}`,
},
];
}),
...Object.entries(state.position).map(([axis, value]) => ({
kind: "pin",
name: `axis.${axis}.pos-cmd`,
type: "HAL_FLOAT",
value,
connected: true,
})),
...Object.entries(state.homed).map(([axis, value]) => ({
kind: "pin",
name: `axis.${axis}.homed`,
type: "HAL_BIT",
value: value ? 1 : 0,
connected: true,
})),
{ kind: "pin", name: "iocontrol.0.user-enable-out", type: "HAL_BIT", value: state.task.powered ? 1 : 0, connected: true },
{ kind: "pin", name: "iocontrol.0.emc-enable-in", type: "HAL_BIT", value: state.task.estop ? 0 : 1, connected: true },
{ kind: "pin", name: "iocontrol.0.user-request-enable", type: "HAL_BIT", value: state.task.estop ? 0 : 1, connected: true },
{ kind: "pin", name: "halui.machine.is-on", type: "HAL_BIT", value: state.task.powered ? 1 : 0, connected: true },
{ kind: "pin", name: "halui.estop.is-activated", type: "HAL_BIT", value: state.task.estop ? 1 : 0, connected: true },
{ kind: "pin", name: "halui.mode.is-manual", type: "HAL_BIT", value: state.task.mode === "manual" ? 1 : 0, connected: true },
{ kind: "pin", name: "halui.mode.is-auto", type: "HAL_BIT", value: state.task.mode === "auto" ? 1 : 0, connected: true },
{ kind: "pin", name: "halui.mode.is-mdi", type: "HAL_BIT", value: state.task.mode === "mdi" ? 1 : 0, connected: true },
{ kind: "pin", name: "halui.program.is-idle", type: "HAL_BIT", value: state.program.state === "idle" ? 1 : 0, connected: true },
{ kind: "pin", name: "halui.program.is-running", type: "HAL_BIT", value: state.program.state === "running" ? 1 : 0, connected: true },
{ kind: "pin", name: "halui.program.is-paused", type: "HAL_BIT", value: state.program.state === "paused" ? 1 : 0, connected: true },
{ kind: "pin", name: "halui.program.optional-stop.is-on", type: "HAL_BIT", value: state.program.optionalStop ? 1 : 0, connected: true },
{ kind: "pin", name: "halui.program.block-delete.is-on", type: "HAL_BIT", value: state.program.blockDelete ? 1 : 0, connected: true },
{ kind: "pin", name: "halui.halui-mdi-is-running", type: "HAL_BIT", value: state.program.haluiMdiRunning ? 1 : 0, connected: true },
{ kind: "pin", name: "motion.enable", type: "HAL_BIT", value: state.motion.enabled ? 1 : 0, connected: true },
{ kind: "pin", name: "motion.motion-enabled", type: "HAL_BIT", value: state.motion.enabled ? 1 : 0, connected: true },
{ kind: "pin", name: "motion.in-position", type: "HAL_BIT", value: state.motion.inPosition ? 1 : 0, connected: true },
{ kind: "pin", name: "motion.motion-type", type: "HAL_S32", value: state.motion.motionType, connected: true },
{ kind: "pin", name: "motion.coord-mode", type: "HAL_BIT", value: state.motion.coordMode ? 1 : 0, connected: true },
{ kind: "pin", name: "motion.teleop-mode", type: "HAL_BIT", value: state.motion.teleopMode ? 1 : 0, connected: true },
{ kind: "pin", name: "motion.current-vel", type: "HAL_FLOAT", value: state.motion.currentVel, connected: true },
{ kind: "pin", name: "motion.requested-vel", type: "HAL_FLOAT", value: state.motion.requestedVel, connected: true },
{ kind: "pin", name: "motion.distance-to-go", type: "HAL_FLOAT", value: state.motion.distanceToGo, connected: true },
{ kind: "pin", name: "motion.program-line", type: "HAL_S32", value: state.motion.programLine, connected: true },
{ kind: "pin", name: "motion.probe-input", type: "HAL_BIT", value: state.motion.probeInput ? 1 : 0, connected: true },
{ kind: "pin", name: "motion.adaptive-feed", type: "HAL_FLOAT", value: state.motion.adaptiveFeed, connected: true },
{ kind: "pin", name: "motion.feed-hold", type: "HAL_BIT", value: state.motion.feedHold ? 1 : 0, connected: true },
{ kind: "pin", name: "motion.feed-inhibit", type: "HAL_BIT", value: state.motion.feedInhibit ? 1 : 0, connected: true },
{ kind: "pin", name: "motion.homing-inhibit", type: "HAL_BIT", value: state.motion.homingInhibit ? 1 : 0, connected: true },
{ kind: "pin", name: "motion.jog-inhibit", type: "HAL_BIT", value: state.motion.jogInhibit ? 1 : 0, connected: true },
{ kind: "pin", name: "motion.switchkins-type", type: "HAL_FLOAT", value: state.motion.switchkinsType, connected: true },
{ kind: "pin", name: "spindle.0.speed-out", type: "HAL_FLOAT", value: state.spindle.speed, connected: true },
{ kind: "pin", name: "spindle.0.speed-out-abs", type: "HAL_FLOAT", value: Math.abs(state.spindle.speed), connected: true },
{ kind: "pin", name: "spindle.0.on", type: "HAL_BIT", value: state.spindle.state === "on" ? 1 : 0, connected: true },
{ kind: "pin", name: "spindle.0.forward", type: "HAL_BIT", value: state.spindle.direction === "cw" ? 1 : 0, connected: true },
{ kind: "pin", name: "spindle.0.reverse", type: "HAL_BIT", value: state.spindle.direction === "ccw" ? 1 : 0, connected: true },
{ kind: "pin", name: "spindle.0.brake", type: "HAL_BIT", value: state.spindle.brake ? 1 : 0, connected: true },
{ kind: "pin", name: "spindle.0.at-speed", type: "HAL_BIT", value: state.spindle.state === "on" ? 1 : 0, connected: true },
{ kind: "pin", name: "halui.spindle.0.is-on", type: "HAL_BIT", value: state.spindle.state === "on" ? 1 : 0, connected: true },
{ kind: "pin", name: "halui.spindle.0.override.value", type: "HAL_FLOAT", value: state.overrides.spindle / 100, connected: true },
{ kind: "pin", name: "iocontrol.0.coolant-mist", type: "HAL_BIT", value: state.coolant.mist === "on" ? 1 : 0, connected: true },
{ kind: "pin", name: "iocontrol.0.coolant-flood", type: "HAL_BIT", value: state.coolant.flood === "on" ? 1 : 0, connected: true },
{ kind: "pin", name: "halui.mist.is-on", type: "HAL_BIT", value: state.coolant.mist === "on" ? 1 : 0, connected: true },
{ kind: "pin", name: "halui.flood.is-on", type: "HAL_BIT", value: state.coolant.flood === "on" ? 1 : 0, connected: true },
{ kind: "pin", name: "halui.feed-override.value", type: "HAL_FLOAT", value: state.overrides.feed / 100, connected: true },
{ kind: "pin", name: "halui.rapid-override.value", type: "HAL_FLOAT", value: state.overrides.rapid / 100, connected: true },
{ kind: "pin", name: "halui.tool.number", type: "HAL_U32", value: state.tool.current, connected: true },
{ kind: "pin", name: "halui.tool.diameter", type: "HAL_FLOAT", value: state.tool.diameter, connected: true },
{ kind: "pin", name: "iocontrol.0.tool-prep-pocket", type: "HAL_S32", value: state.tool.prepPocket, connected: true },
{ kind: "pin", name: "iocontrol.0.tool-from-pocket", type: "HAL_S32", value: state.tool.fromPocket, connected: true },
{ kind: "pin", name: "iocontrol.0.tool-prep-index", type: "HAL_S32", value: state.tool.prepIndex, connected: true },
{ kind: "pin", name: "iocontrol.0.tool-prep-number", type: "HAL_S32", value: state.tool.prepNumber || state.tool.selected, connected: true },
{ kind: "pin", name: "iocontrol.0.tool-number", type: "HAL_S32", value: state.tool.current, connected: true },
{ kind: "pin", name: "iocontrol.0.tool-prepare", type: "HAL_BIT", value: state.tool.prepare ? 1 : 0, connected: true },
{ kind: "pin", name: "iocontrol.0.tool-prepared", type: "HAL_BIT", value: state.tool.prepared ? 1 : 0, connected: true },
{ kind: "pin", name: "iocontrol.0.tool-change", type: "HAL_BIT", value: state.tool.change ? 1 : 0, connected: true },
{ kind: "pin", name: "iocontrol.0.tool-changed", type: "HAL_BIT", value: state.tool.changed ? 1 : 0, connected: true },
...Object.entries(state.position).flatMap(([axis, value], index) => [
{ kind: "pin", name: `axis.${axis}.teleop-pos-cmd`, type: "HAL_FLOAT", value, connected: true },
{ kind: "pin", name: `axis.${axis}.teleop-vel-cmd`, type: "HAL_FLOAT", value: 0, connected: true },
{ kind: "pin", name: `axis.${axis}.teleop-vel-lim`, type: "HAL_FLOAT", value: state.motion.requestedVel, connected: true },
{ kind: "pin", name: `axis.${axis}.teleop-tp-enable`, type: "HAL_BIT", value: state.task.powered ? 1 : 0, connected: true },
{ kind: "pin", name: `axis.${axis}.jog-enable`, type: "HAL_BIT", value: state.pins[`jog.${axis}`] ? 1 : 0, connected: true },
{ kind: "pin", name: `axis.${axis}.jog-scale`, type: "HAL_FLOAT", value: state.jog.incrementValue, connected: true },
{ kind: "pin", name: `axis.${axis}.jog-counts`, type: "HAL_S32", value: state.pins[`jog.${axis}`] ? 1 : 0, connected: true },
{ kind: "pin", name: `axis.${axis}.jog-vel-mode`, type: "HAL_BIT", value: state.jog.incrementValue === 0 ? 1 : 0, connected: true },
{ kind: "pin", name: `halui.axis.${axis}.pos-commanded`, type: "HAL_FLOAT", value, connected: true },
{ kind: "pin", name: `halui.axis.${axis}.pos-feedback`, type: "HAL_FLOAT", value, connected: true },
{ kind: "pin", name: `halui.axis.${axis}.pos-relative`, type: "HAL_FLOAT", value, connected: true },
{ kind: "pin", name: `halui.axis.${axis}.is-selected`, type: "HAL_BIT", value: state.activeAxis === axis ? 1 : 0, connected: true },
{ kind: "pin", name: `motion.tooloffset.${axis}`, type: "HAL_FLOAT", value: state.tool.lengthOffset[axis], connected: true },
{ kind: "pin", name: `halui.tool.length_offset.${axis}`, type: "HAL_FLOAT", value: state.tool.lengthOffset[axis], connected: true },
{ kind: "pin", name: `joint.${index}.pos-cmd`, type: "HAL_FLOAT", value, connected: true },
{ kind: "pin", name: `joint.${index}.coarse-pos-cmd`, type: "HAL_FLOAT", value, connected: true },
{ kind: "pin", name: `joint.${index}.pos-fb`, type: "HAL_FLOAT", value, connected: true },
{ kind: "pin", name: `joint.${index}.motor-pos-cmd`, type: "HAL_FLOAT", value, connected: true },
{ kind: "pin", name: `joint.${index}.motor-pos-fb`, type: "HAL_FLOAT", value, connected: true },
{ kind: "pin", name: `joint.${index}.amp-enable-out`, type: "HAL_BIT", value: state.task.powered ? 1 : 0, connected: true },
{ kind: "pin", name: `joint.${index}.homed`, type: "HAL_BIT", value: state.homed[axis] ? 1 : 0, connected: true },
{ kind: "pin", name: `joint.${index}.homing`, type: "HAL_BIT", value: 0, connected: true },
{ kind: "pin", name: `joint.${index}.home-state`, type: "HAL_S32", value: state.homed[axis] ? 1 : 0, connected: true },
{ kind: "pin", name: `joint.${index}.home-sw-in`, type: "HAL_BIT", value: state.homed[axis] ? 1 : 0, connected: true },
{ kind: "pin", name: `joint.${index}.pos-lim-sw-in`, type: "HAL_BIT", value: state.limits[axis]?.maxLimit ? 1 : 0, connected: true },
{ kind: "pin", name: `joint.${index}.neg-lim-sw-in`, type: "HAL_BIT", value: state.limits[axis]?.minLimit ? 1 : 0, connected: true },
{ kind: "pin", name: `joint.${index}.pos-hard-limit`, type: "HAL_BIT", value: state.limits[axis]?.maxLimit ? 1 : 0, connected: true },
{ kind: "pin", name: `joint.${index}.neg-hard-limit`, type: "HAL_BIT", value: state.limits[axis]?.minLimit ? 1 : 0, connected: true },
{ kind: "pin", name: `joint.${index}.error`, type: "HAL_BIT", value: state.limits[axis]?.fault === "none" ? 0 : 1, connected: true },
{ kind: "pin", name: `joint.${index}.faulted`, type: "HAL_BIT", value: state.limits[axis]?.fault === "none" ? 0 : 1, connected: true },
{ kind: "pin", name: `halui.joint.${index}.is-homed`, type: "HAL_BIT", value: state.homed[axis] ? 1 : 0, connected: true },
]),
...Object.values(state.externalPins ?? {}).map((entry) => ({
kind: entry.kind ?? "pin",
name: entry.name,
type: entry.type ?? "HAL_FLOAT",
value: Number(entry.value) || 0,
connected: entry.connected !== false,
})),
...Object.values(state.hal.signals ?? {}).map((entry) => ({
kind: "signal",
name: entry.name,
type: entry.type ?? "HAL_FLOAT",
value: normalizeHalNumericValue(entry.value),
connected: (entry.pins ?? []).length > 0,
})),
...Object.entries(state.hal.params ?? {}).map(([name, entry]) => ({
kind: "param",
name,
type: entry.type ?? "HAL_FLOAT",
value: normalizeHalNumericValue(entry.value),
connected: entry.connected !== false,
})),
],
};
}
export function createVirtualHalPinRegistry(halState = createVirtualHalState()) {
const state = normalizeVirtualHalState(halState);
const snapshot = createVirtualHalWasmBridgeSnapshot(state);
const systemCoverage = createVirtualHalSystemCoverageReport({ halState: state, snapshot });
const snapshotRows = snapshot.values.map((value) => ({
id: `${value.kind}:${value.name}`,
kind: value.kind,
name: value.name,
type: value.type,
value: value.value,
connected: value.connected !== false,
group: classifyVirtualHalPinGroup(value.name),
coverage: resolveVirtualHalCoverageForPin(value.name),
writable: isVirtualHalPinWritable(value.name),
sourceFiles: resolveVirtualHalSourceFilesForPin(value.name),
representedInSnapshot: true,
}));
const snapshotNames = new Set(snapshotRows.map(({ name }) => name));
const declaredRows = systemCoverage.declaredRows
.filter(({ name }) => !snapshotNames.has(name))
.map((row) => ({
id: `pin:${row.name}`,
kind: "pin",
name: row.name,
type: row.type,
value: resolveVirtualHalDeclaredPinValue(state, row.name, row.type),
connected: true,
group: classifyVirtualHalPinGroup(row.name),
coverage: row.coverage,
writable: isVirtualHalPinWritable(row.name),
sourceFiles: [...row.sourceFiles],
representedInSnapshot: false,
template: row.template,
conditional: row.conditional,
}));
const rows = [...snapshotRows, ...declaredRows];
return {
apiName: "linuxcnc-wasm-virtual-hal-pin-registry",
registryVersion: 1,
source: "linuxcnc-source-derived-virtual-hal",
pinCount: rows.length,
snapshotPinCount: snapshotRows.length,
declaredOnlyPinCount: declaredRows.length,
groups: VIRTUAL_HAL_PROJECT_PIN_GROUPS.map((group) => ({
...group,
pinCount: rows.filter((row) => row.group === group.id).length,
})),
rows,
byName: Object.fromEntries(rows.map((row) => [row.name, row])),
};
}
export function readVirtualHalPin(halState, name) {
const registry = createVirtualHalPinRegistry(halState);
return registry.byName[String(name ?? "")] ?? null;
}
export function writeVirtualHalPin(halState, update, options = {}) {
const name = typeof update === "string" ? update : update?.name;
if (!name) {
throw new Error("writeVirtualHalPin requires a HAL pin name");
}
const next = normalizeVirtualHalState(halState);
const value = typeof update === "string" ? options.value : update.value;
const numeric = Number(value) || 0;
const enabled = value === true || numeric !== 0;
const shortAxisui = name.startsWith(`${next.component}.`) ? name.slice(next.component.length + 1) : name;
if (shortAxisui in next.pins) {
next.pins[shortAxisui] = typeof next.pins[shortAxisui] === "number" ? numeric : enabled;
} else if (name === "iocontrol.0.emc-enable-in" || name === "halui.estop.reset") {
next.task.estop = !enabled;
} else if (name === "halui.estop.activate") {
next.task.estop = enabled;
if (enabled) {
next.task.powered = false;
}
} else if (name === "iocontrol.0.user-enable-out" || name === "halui.machine.is-on" || name === "halui.machine.on") {
next.task.powered = enabled && !next.task.estop;
} else if (name === "halui.machine.off") {
next.task.powered = enabled ? false : next.task.powered;
} else if (name === "halui.mode.manual") {
next.task.mode = enabled ? "manual" : next.task.mode;
} else if (name === "halui.mode.auto") {
next.task.mode = enabled ? "auto" : next.task.mode;
} else if (name === "halui.mode.mdi") {
next.task.mode = enabled ? "mdi" : next.task.mode;
} else if (name === "halui.program.run") {
next.program.state = enabled ? "running" : next.program.state;
} else if (name === "halui.program.pause") {
next.program.state = enabled ? "paused" : next.program.state;
} else if (name === "halui.program.resume") {
next.program.state = enabled ? "running" : next.program.state;
} else if (name === "halui.program.stop") {
next.program.state = enabled ? "idle" : next.program.state;
} else if (name === "halui.program.optional-stop.on") {
next.program.optionalStop = enabled;
} else if (name === "halui.program.optional-stop.off") {
next.program.optionalStop = enabled ? false : next.program.optionalStop;
} else if (name === "halui.program.block-delete.on") {
next.program.blockDelete = enabled;
} else if (name === "halui.program.block-delete.off") {
next.program.blockDelete = enabled ? false : next.program.blockDelete;
} else if (name === "iocontrol.0.coolant-mist" || name === "halui.mist.is-on" || name === "halui.mist.on") {
next.coolant.mist = enabled ? "on" : "off";
} else if (name === "halui.mist.off") {
next.coolant.mist = enabled ? "off" : next.coolant.mist;
} else if (name === "iocontrol.0.coolant-flood" || name === "halui.flood.is-on" || name === "halui.flood.on") {
next.coolant.flood = enabled ? "on" : "off";
} else if (name === "halui.flood.off") {
next.coolant.flood = enabled ? "off" : next.coolant.flood;
} else if (name === "spindle.0.speed-out" || name === "spindle.0.speed-out-abs") {
next.spindle.speed = numeric;
next.spindle.state = numeric > 0 ? "on" : next.spindle.state;
} else if (name === "spindle.0.on" || name === "halui.spindle.0.is-on" || name === "halui.spindle.0.start") {
next.spindle.state = enabled ? "on" : "off";
} else if (name === "halui.spindle.0.stop") {
next.spindle.state = enabled ? "off" : next.spindle.state;
} else if (name === "spindle.0.forward" || name === "halui.spindle.0.forward") {
next.spindle.state = enabled ? "on" : next.spindle.state;
next.spindle.direction = enabled ? "cw" : next.spindle.direction;
} else if (name === "spindle.0.reverse" || name === "halui.spindle.0.reverse") {
next.spindle.state = enabled ? "on" : next.spindle.state;
next.spindle.direction = enabled ? "ccw" : next.spindle.direction;
} else if (name === "spindle.0.brake") {
next.spindle.brake = enabled;
} else if (name === "halui.feed-override.value") {
next.overrides.feed = numeric <= 2 ? numeric * 100 : numeric;
} else if (name === "halui.rapid-override.value") {
next.overrides.rapid = numeric <= 2 ? numeric * 100 : numeric;
} else if (name === "halui.spindle.0.override.value") {
next.overrides.spindle = numeric <= 2 ? numeric * 100 : numeric;
} else if (name === "halui.tool.number" || name === "iocontrol.0.tool-number") {
next.tool.current = numeric;
next.tool.selected = numeric;
} else if (name === "halui.tool.diameter") {
next.tool.diameter = numeric;
} else if (name === "iocontrol.0.tool-prepare") {
next.tool.prepare = enabled;
} else if (name === "iocontrol.0.tool-prepared") {
next.tool.prepared = enabled;
} else if (name === "iocontrol.0.tool-change") {
next.tool.change = enabled;
} else if (name === "iocontrol.0.tool-changed") {
next.tool.changed = enabled;
} else if (name === "motion.current-vel") {
next.motion.currentVel = numeric;
} else if (name === "motion.requested-vel") {
next.motion.requestedVel = numeric;
} else if (name === "motion.distance-to-go") {
next.motion.distanceToGo = numeric;
} else if (name === "motion.program-line") {
next.motion.programLine = numeric;
next.program.line = numeric;
} else if (name === "motion.probe-input") {
next.motion.probeInput = enabled;
} else if (name === "motion.adaptive-feed") {
next.motion.adaptiveFeed = numeric;
} else if (name === "motion.feed-hold") {
next.motion.feedHold = enabled;
} else if (name === "motion.switchkins-type") {
next.motion.switchkinsType = numeric;
} else if (applyAxisPinWrite(next, name, numeric, enabled) === false) {
if (options.allowExternal === false) {
throw new Error(`unknown virtual HAL pin: ${name}`);
}
next.externalPins[name] = {
kind: update.kind ?? "pin",
name,
type: update.type ?? inferVirtualHalPinType(value),
value: numeric,
connected: update.connected !== false,
};
}
next.revision += 1;
return normalizeVirtualHalState(next);
}
export function applyVirtualHalPinUpdates(halState, updates = [], options = {}) {
return [...updates].reduce((state, update) => writeVirtualHalPin(state, update, options), halState);
}
export function createVirtualHalIntegrityReport(options = {}) {
const state = normalizeVirtualHalState(options.halState ?? createVirtualHalState());
const registry = options.registry ?? createVirtualHalPinRegistry(state);
const systemCoverage = options.systemCoverage ?? createVirtualHalSystemCoverageReport({
halState: state,
snapshot: createVirtualHalWasmBridgeSnapshot(state),
});
const requiredPins = options.requiredPins ?? [
"axisui.jog.x",
"halui.machine.is-on",
"iocontrol.0.emc-enable-in",
"motion.current-vel",
"axis.x.pos-cmd",
"joint.0.homed",
"spindle.0.speed-out",
"iocontrol.0.tool-change",
];
const pinNames = registry.rows.map(({ name }) => name);
const duplicatePins = pinNames.filter((name, index) => pinNames.indexOf(name) !== index);
const missingPins = requiredPins.filter((name) => !registry.byName[name]);
const externalPinCount = Object.keys(state.externalPins ?? {}).length;
const complete = systemCoverage.complete &&
duplicatePins.length === 0 &&
missingPins.length === 0 &&
registry.pinCount > 0;
return {
apiName: "linuxcnc-wasm-virtual-hal-integrity-report",
integrityVersion: 1,
source: "linuxcnc-source-derived-virtual-hal",
phase: complete ? "complete" : "incomplete",
complete,
realtimeHalRuntime: false,
registryPinCount: registry.pinCount,
declaredPinCount: systemCoverage.declaredPinCount,
requiredPins,
missingPins,
duplicatePins: [...new Set(duplicatePins)],
externalPinCount,
systemCoveragePhase: systemCoverage.phase,
rows: [
{ id: "registry", label: "Virtual HAL registry", value: `${registry.pinCount} pins` },
{ id: "coverage", label: "Source coverage", value: systemCoverage.phase },
{ id: "required-pins", label: "Required pins", value: missingPins.length === 0 ? "covered" : `missing ${missingPins.length}` },
{ id: "external-pins", label: "External pins", value: `${externalPinCount}` },
],
};
}
export function createVirtualHalPinInventory(halState = createVirtualHalState()) {
const state = normalizeVirtualHalState(halState);
const snapshot = createVirtualHalWasmBridgeSnapshot(state);
const systemCoverage = createVirtualHalSystemCoverageReport({ halState: state, snapshot });
const rows = snapshot.values.map((value) => ({
id: `${value.kind}:${value.name}`,
kind: value.kind,
name: value.name,
type: value.type,
connected: value.connected !== false,
group: classifyVirtualHalPinGroup(value.name),
coverage: resolveVirtualHalCoverageForPin(value.name),
}));
return {
apiName: "linuxcnc-wasm-virtual-hal-pin-inventory",
inventoryVersion: 1,
source: "linuxcnc-source-derived-virtual-hal",
component: state.component,
axisCount: VIRTUAL_HAL_AXES.length,
displayedAxisCount: VIRTUAL_HAL_DISPLAY_AXES.length,
pinCount: rows.length,
declaredPinCount: systemCoverage.declaredPinCount,
systemCoverageComplete: systemCoverage.complete,
groups: VIRTUAL_HAL_PROJECT_PIN_GROUPS.map((group) => ({
...group,
pinCount: rows.filter((row) => row.group === group.id).length,
})),
rows,
};
}
export function createVirtualHalBridgeReadiness(options = {}) {
const snapshot = options.snapshot ?? createVirtualHalWasmBridgeSnapshot(options.halState ?? createVirtualHalState());
const systemCoverage = options.systemCoverage ?? createVirtualHalSystemCoverageReport({
halState: options.halState ?? createVirtualHalState(),
snapshot,
});
const requiredFunctions = options.requiredFunctions ?? VIRTUAL_HAL_WASM_BRIDGE_FUNCTIONS;
const availableFunctions = resolveAvailableVirtualHalFunctions(options, requiredFunctions);
const missingWasmFunctions = requiredFunctions.filter((name) => !availableFunctions.includes(name));
const requiredPins = options.requiredPins ?? [
"axisui.jog.x",
"axisui.jog.increment",
"halui.machine.is-on",
"halui.estop.is-activated",
"spindle.0.speed-out",
"iocontrol.0.coolant-flood",
];
const snapshotPins = new Set((snapshot.values ?? []).map(({ name }) => name));
const missingPins = requiredPins.filter((name) => !snapshotPins.has(name));
const missingCoverage = systemCoverage.complete ? [] : ["system-coverage"];
const evidenceProvided = Boolean(
options.interpSdk ||
options.hasWasmFunction ||
options.availableFunctions ||
options.availableWasmFunctions,
);
const missing = [
...missingWasmFunctions.map((name) => `wasm-function:${name}`),
...missingPins.map((name) => `pin:${name}`),
...missingCoverage,
];
if (!evidenceProvided) {
missing.unshift("wasm-function-evidence");
}
return {
apiName: "linuxcnc-wasm-virtual-hal-bridge-readiness",
readinessVersion: 1,
source: snapshot.source ?? "browser-virtual-hal",
phase: missing.length === 0 ? "ready" : "blocked",
ready: missing.length === 0,
evidenceProvided,
requiredFunctions: [...requiredFunctions],
availableFunctions,
missingWasmFunctions,
requiredPins,
missingPins,
snapshotValueCount: snapshot.values?.length ?? 0,
systemCoverageComplete: systemCoverage.complete,
systemCoveragePhase: systemCoverage.phase,
runtimeBoundaryCount: systemCoverage.boundaryRows?.length ?? 0,
missing,
rows: [
{
id: "wasm-functions",
label: "WASM HAL bridge functions",
value: missingWasmFunctions.length === 0 && evidenceProvided ? "ready" : `missing ${missingWasmFunctions.length || "evidence"}`,
},
{
id: "bridge-pins",
label: "Bridge pins",
value: missingPins.length === 0 ? `${snapshot.values?.length ?? 0} values` : `missing ${missingPins.length}`,
},
{
id: "system-coverage",
label: "System HAL coverage",
value: systemCoverage.complete ? systemCoverage.phase : "incomplete",
},
{
id: "source",
label: "Source",
value: systemCoverage.source ?? snapshot.source ?? "unknown",
},
],
};
}
export function createVirtualHalBridgeActionPlan(readiness = createVirtualHalBridgeReadiness()) {
const ready = readiness.ready === true;
const nextCommand = ready ? null : "wasm-port/tools/build_wasm_core.sh";
return {
apiName: "linuxcnc-wasm-virtual-hal-bridge-action-plan",
actionPlanVersion: 1,
phase: ready ? "ready" : "blocked",
ready,
nextCommand,
missing: [...(readiness.missing ?? [])],
actions: ready
? []
: [
{
id: "build-wasm-core",
label: "Build interpreter WASM with HAL bridge exports",
command: nextCommand,
},
{
id: "verify-interp-wasm",
label: "Verify interpreter HAL bridge smoke",
command: "SKIP_INTERP_BUILD=1 wasm-port/tests/wasm/node/verify_interp_wasm.sh",
},
],
shellScript: ready
? "# LinuxCNC virtual HAL bridge is already ready.\n"
: [
"# Build and verify LinuxCNC virtual HAL bridge.",
"wasm-port/tools/build_wasm_core.sh",
"SKIP_INTERP_BUILD=1 wasm-port/tests/wasm/node/verify_interp_wasm.sh",
"",
].join("\n"),
rows: [
{
id: "readiness",
label: "Virtual HAL bridge",
value: ready ? "ready" : "blocked",
},
{
id: "next-command",
label: "Next command",
value: nextCommand ?? "-",
},
],
};
}
export function createVirtualHalProjectReport(options = {}) {
const state = normalizeVirtualHalState(options.halState ?? createVirtualHalState());
const snapshot = createVirtualHalWasmBridgeSnapshot(state);
const systemCoverage = createVirtualHalSystemCoverageReport({
...options,
halState: state,
snapshot,
});
const readiness = createVirtualHalBridgeReadiness({
...options,
snapshot,
systemCoverage,
});
const actionPlan = createVirtualHalBridgeActionPlan(readiness);
const pinRegistry = createVirtualHalPinRegistry(state);
const integrity = createVirtualHalIntegrityReport({
halState: state,
registry: pinRegistry,
systemCoverage,
});
return {
apiName: "linuxcnc-wasm-virtual-hal-project-report",
reportVersion: 1,
source: "browser-virtual-hal",
phase: readiness.ready && integrity.complete ? "ready" : "blocked",
ready: readiness.ready && integrity.complete,
missing: [...readiness.missing, ...integrity.missingPins.map((name) => `integrity-pin:${name}`)],
state: cloneVirtualHalState(state),
dro: createVirtualHalDroState(state),
limitsHome: createVirtualHalLimitsHomeState(state),
machineStatus: createVirtualHalMachineStatusState(state),
pinInventory: createVirtualHalPinInventory(state),
pinRegistry,
integrity,
systemCoverage,
simulationRuntime: createVirtualHalSimulationRuntimeReport(state),
wasmBridgeSnapshot: snapshot,
bridgeReadiness: readiness,
bridgeActionPlan: actionPlan,
rows: [
{
id: "state",
label: "Virtual HAL state",
value: state.task.state,
},
{
id: "pins",
label: "Bridge pins",
value: `${snapshot.values.length}`,
},
{
id: "system-coverage",
label: "System coverage",
value: systemCoverage.phase,
},
{
id: "integrity",
label: "HAL integrity",
value: integrity.phase,
},
{
id: "bridge",
label: "WASM bridge",
value: readiness.ready && integrity.complete ? "ready" : "blocked",
},
{
id: "simulation-runtime",
label: "Simulation HAL runtime",
value: "ready",
},
],
};
}
export function applyVirtualHalToInterpSdk(interpSdk, halState = createVirtualHalState(), options = {}) {
if (!interpSdk) {
throw new Error("applyVirtualHalToInterpSdk requires an interpreter SDK");
}
if (typeof interpSdk.applyVirtualHalState === "function") {
return interpSdk.applyVirtualHalState(halState, options);
}
if (typeof interpSdk.applyVirtualHalSnapshot === "function") {
return interpSdk.applyVirtualHalSnapshot(createVirtualHalWasmBridgeSnapshot(halState), options);
}
throw new Error("interpreter SDK missing virtual HAL apply methods");
}
export function stepVirtualHalMotion(halState = createVirtualHalState(), options = {}) {
const next = normalizeVirtualHalState(halState);
const dt = clampNumber(options.dt ?? options.period ?? next.motion.servoPeriod, 0.000001, 1);
const maxVelocity = clampNumber(options.maxVelocity ?? next.motion.requestedVel ?? 1, 0.000001, 999999);
const target = { ...next.motion.target, ...(options.target ?? {}) };
let totalMoved = 0;
let totalRemaining = 0;
for (const axis of VIRTUAL_HAL_AXES) {
const current = next.position[axis];
const goal = Number.isFinite(Number(target[axis])) ? Number(target[axis]) : current;
const delta = goal - current;
const step = Math.sign(delta) * Math.min(Math.abs(delta), maxVelocity * dt);
next.position[axis] = current + step;
next.motion.target[axis] = goal;
totalMoved += Math.abs(step);
totalRemaining += Math.abs(goal - next.position[axis]);
}
next.motion.servoPeriod = dt;
next.motion.requestedVel = maxVelocity;
next.motion.currentVel = totalMoved / dt;
next.motion.distanceToGo = totalRemaining;
next.motion.inPosition = totalRemaining < 0.000001;
next.motion.motionType = next.motion.inPosition ? 0 : 1;
next.positionDirty = totalMoved > 0;
next.revision += 1;
return normalizeVirtualHalState(next);
}
export function stepVirtualHalMotionController(halState = createVirtualHalState(), options = {}) {
const cycleCount = clampInteger(options.cycleCount ?? options.cycles ?? 1, 1, 100000);
let state = normalizeVirtualHalState(halState);
const frames = [];
for (let index = 0; index < cycleCount; index += 1) {
state = stepVirtualHalMotion(state, options);
frames.push({
cycle: index + 1,
position: { ...state.position },
motion: { ...state.motion, target: { ...state.motion.target } },
});
if (state.motion.inPosition && options.stopOnInPosition !== false) {
break;
}
}
return {
apiName: "linuxcnc-wasm-virtual-motion-controller-step",
stepVersion: 1,
source: "simulation-grade-virtual-motion-controller",
ok: true,
cycleCount: frames.length,
inPosition: state.motion.inPosition,
distanceToGo: state.motion.distanceToGo,
frames,
state: cloneVirtualHalState(state),
};
}
export function executeVirtualHalcmd(halState = createVirtualHalState(), commandText = "", options = {}) {
return executeVirtualHalCommand(halState, commandText, options);
}
export function executeVirtualHalCommand(halState = createVirtualHalState(), commandText = "", options = {}) {
let state = normalizeVirtualHalState(halState);
const rows = [];
const output = [];
const commands = String(commandText)
.split(/\r?\n/)
.map((line) => line.replace(/#.*/, "").trim())
.filter(Boolean);
for (const command of commands) {
const tokens = command.split(/\s+/);
const verb = tokens[0]?.toLowerCase();
try {
if (verb === "setp" && tokens.length >= 3) {
const name = tokens[1];
const value = parseHalCommandValue(tokens.slice(2).join(" "));
state = writeVirtualHalPin(state, { name, value, type: inferVirtualHalPinType(value) });
syncVirtualHalSignalsFromPin(state, name, value);
rows.push({ command, action: "setp", name, value: normalizeHalNumericValue(value), ok: true });
} else if ((verb === "sets" || verb === "newsig") && tokens.length >= 2) {
const name = tokens[1];
const value = verb === "sets" ? parseHalCommandValue(tokens.slice(2).join(" ")) : 0;
state.hal.signals[name] = {
name,
type: inferVirtualHalPinType(value),
value: normalizeHalNumericValue(value),
pins: [...(state.hal.signals[name]?.pins ?? [])],
};
state = propagateVirtualHalSignal(state, name);
rows.push({ command, action: verb, name, value: normalizeHalNumericValue(value), ok: true });
} else if (verb === "net" && tokens.length >= 3) {
const signalName = tokens[1];
const pinNames = tokens.slice(2).filter((token) => token !== "<=" && token !== "=>" && token !== "<=>" && token !== "=");
const existing = state.hal.signals[signalName] ?? { name: signalName, type: "HAL_FLOAT", value: 0, pins: [] };
state.hal.signals[signalName] = {
...existing,
pins: [...new Set([...(existing.pins ?? []), ...pinNames])],
};
state.hal.nets[signalName] = [...state.hal.signals[signalName].pins];
state = propagateVirtualHalSignal(state, signalName);
rows.push({ command, action: "net", name: signalName, pins: pinNames, ok: true });
} else if ((verb === "getp" || verb === "gets") && tokens.length >= 2) {
const name = tokens[1];
const value = verb === "gets" ? state.hal.signals[name]?.value : readVirtualHalPin(state, name)?.value;
output.push(`${name} ${normalizeHalNumericValue(value)}`);
rows.push({ command, action: verb, name, value: normalizeHalNumericValue(value), ok: true });
} else if (verb === "show") {
const kind = tokens[1] ?? "pin";
const pattern = tokens[2] ?? "";
const showRows = createVirtualHalShowRows(state, kind, pattern);
output.push(...showRows.map((row) => `${row.kind} ${row.name} ${row.value}`));
rows.push({ command, action: "show", kind, count: showRows.length, ok: true });
} else if ((verb === "loadrt" || verb === "loadusr") && tokens.length >= 2) {
const component = tokens.slice(1).filter((token) => !token.startsWith("-")).join(" ");
state.hal.loadedComponents.push({ command: verb, component, realtime: verb === "loadrt" });
rows.push({ command, action: verb, component, ok: true });
} else if (verb === "addf" && tokens.length >= 3) {
state.hal.functions.push({ name: tokens[1], thread: tokens[2] });
rows.push({ command, action: "addf", name: tokens[1], thread: tokens[2], ok: true });
} else if ((verb === "start" || verb === "stop") && tokens.length >= 2) {
const thread = tokens[1];
state.hal.threads[thread] = {
...(state.hal.threads[thread] ?? { periodNs: 1000000 }),
running: verb === "start",
};
rows.push({ command, action: verb, thread, ok: true });
} else if (verb === "unlinkp" && tokens.length >= 2) {
const pin = tokens[1];
for (const signal of Object.values(state.hal.signals)) {
signal.pins = (signal.pins ?? []).filter((name) => name !== pin);
}
for (const [signalName, pins] of Object.entries(state.hal.nets)) {
state.hal.nets[signalName] = pins.filter((name) => name !== pin);
}
rows.push({ command, action: "unlinkp", name: pin, ok: true });
} else if (verb === "stepgen" || verb === "motion-step") {
const target = parseHalCommandTarget(tokens.slice(1));
state = stepVirtualHalMotion(state, { ...options, target });
rows.push({ command, action: "motion-step", target, ok: true });
} else {
rows.push({ command, action: "unsupported", ok: false, error: `unsupported halcmd verb: ${verb}` });
}
} catch (error) {
rows.push({ command, action: verb ?? "unknown", ok: false, error: error.message });
}
}
state.hal.commandLog = [
...(state.hal.commandLog ?? []),
...rows.map((row) => ({ ...row, source: "virtual-halcmd" })),
].slice(-200);
state.revision += rows.length > 0 ? 1 : 0;
const finalState = normalizeVirtualHalState(state);
return {
apiName: "linuxcnc-wasm-virtual-halcmd-result",
commandVersion: 1,
source: "simulation-grade-virtual-hal",
ok: rows.every((row) => row.ok),
commandCount: rows.length,
output: output.join("\n"),
rows,
state: cloneVirtualHalState(finalState),
};
}
export function createVirtualHalCommandScriptFixtureReport(options = {}) {
const fixtures = options.fixtures ?? VIRTUAL_HAL_COMMAND_SCRIPT_FIXTURES;
let state = normalizeVirtualHalState(options.halState ?? createVirtualHalState());
const fixtureRows = fixtures.map((fixture) => {
const commandText = fixture.commands.join("\n");
const result = executeVirtualHalCommand(state, commandText, options);
state = result.state;
const actions = result.rows.map(({ action }) => action);
const missingActions = fixture.requiredActions.filter((action) => !actions.includes(action));
const missingOutput = fixture.expectedOutput.filter((token) => !result.output.includes(token));
const sourceFiles = [...(fixture.sourceFiles ?? [])];
return {
id: fixture.id,
label: fixture.label,
commandText,
commandCount: result.commandCount,
ok: result.ok === true,
sourceDerived: sourceFiles.length > 0,
sourceFiles,
requiredActions: [...fixture.requiredActions],
actions,
missingActions,
expectedOutput: [...fixture.expectedOutput],
missingOutput,
output: result.output,
rows: result.rows,
ready: result.ok === true &&
sourceFiles.length > 0 &&
missingActions.length === 0 &&
missingOutput.length === 0,
};
});
const missingFixtures = fixtureRows
.filter((row) => row.ready !== true)
.map((row) => row.id);
const coveredActions = [...new Set(fixtureRows.flatMap((row) => row.actions))].sort();
const requiredActions = [...new Set(fixtures.flatMap((fixture) => fixture.requiredActions))].sort();
const missingActions = requiredActions.filter((action) => !coveredActions.includes(action));
const complete = fixtureRows.length > 0 &&
missingFixtures.length === 0 &&
missingActions.length === 0;
return {
apiName: "linuxcnc-wasm-virtual-hal-command-script-fixture-report",
fixtureVersion: 1,
source: "linuxcnc-halcmd-source-derived-virtual-hal",
phase: complete ? "ready" : "blocked",
complete,
webSimulationSatisfied: complete,
sourceFiles: [...new Set(fixtureRows.flatMap((row) => row.sourceFiles))],
requiredActions,
coveredActions,
missingActions,
missingFixtures,
fixtureCount: fixtureRows.length,
commandCount: fixtureRows.reduce((sum, row) => sum + row.commandCount, 0),
finalState: cloneVirtualHalState(state),
rows: fixtureRows,
summaryRows: [
{ id: "fixtures", label: "HAL command fixtures", value: `${fixtureRows.length}` },
{ id: "commands", label: "HAL commands", value: `${fixtureRows.reduce((sum, row) => sum + row.commandCount, 0)}` },
{ id: "actions", label: "Covered halcmd actions", value: missingActions.length === 0 ? "covered" : `missing ${missingActions.length}` },
{ id: "source", label: "LinuxCNC halcmd source", value: complete ? "source-derived" : "incomplete" },
],
};
}
function readVirtualHalSnapshotValue(snapshot, name) {
return snapshot.values.find((entry) => entry.name === name)?.value;
}
function valuesClose(actual, expected, tolerance = 0.000001) {
return Math.abs(Number(actual) - Number(expected)) <= tolerance;
}
function virtualHalSimConfigTargetsById(targetIds = []) {
const ids = new Set(targetIds);
return VIRTUAL_HAL_SIM_CONFIG_SOURCE_TARGETS.filter((target) => ids.has(target.id));
}
export function createVirtualHalMotionControllerMatrixReport(options = {}) {
const fixtures = options.motionFixtures ?? options.fixtures ?? VIRTUAL_HAL_MOTION_CONTROLLER_MATRIX_FIXTURES;
const tolerance = clampNumber(options.tolerance ?? 0.000001, 0, 1);
const manifestProvided = typeof options.manifestText === "string" || Array.isArray(options.manifestEntries);
const manifestSet = new Set([
...sourceManifestSetFromText(options.manifestText ?? ""),
...(options.manifestEntries ?? []).map(normalizeLinuxCncSourcePath),
]);
const rows = fixtures.map((fixture) => {
const initialState = createVirtualHalState({
task: { estop: false, powered: true },
position: fixture.initialPosition ?? {},
motion: {
requestedVel: fixture.maxVelocity,
target: fixture.initialPosition ?? {},
servoPeriod: fixture.dt,
},
});
const step = stepVirtualHalMotionController(initialState, {
target: fixture.target,
maxVelocity: fixture.maxVelocity,
dt: fixture.dt,
cycleCount: fixture.cycleCount,
stopOnInPosition: fixture.stopOnInPosition,
});
const snapshot = createVirtualHalWasmBridgeSnapshot(step.state);
const missingPins = (fixture.requiredPins ?? [])
.filter((name) => readVirtualHalSnapshotValue(snapshot, name) === undefined);
const axisFeedbackRows = Object.entries(fixture.expected?.axisFeedback ?? {}).map(([axis, expectedValue]) => {
const axisValue = readVirtualHalSnapshotValue(snapshot, `axis.${axis}.pos-cmd`);
const jointIndex = VIRTUAL_HAL_AXES.indexOf(axis);
const jointValue = jointIndex >= 0
? readVirtualHalSnapshotValue(snapshot, `joint.${jointIndex}.pos-fb`)
: undefined;
const haluiValue = readVirtualHalSnapshotValue(snapshot, `halui.axis.${axis}.pos-feedback`);
return {
axis,
expected: expectedValue,
axisPosition: axisValue,
jointFeedback: jointValue,
haluiFeedback: haluiValue,
matched: valuesClose(axisValue, expectedValue, tolerance) &&
valuesClose(jointValue, expectedValue, tolerance) &&
valuesClose(haluiValue, expectedValue, tolerance),
};
});
const expectedInPosition = fixture.expected?.inPosition;
const expectedDistanceToGo = fixture.expected?.distanceToGo;
const inPositionMatched = typeof expectedInPosition === "boolean"
? step.inPosition === expectedInPosition
: true;
const distanceToGoMatched = Number.isFinite(Number(expectedDistanceToGo))
? valuesClose(step.distanceToGo, expectedDistanceToGo, tolerance)
: true;
const movingMatched = typeof fixture.expected?.moving === "boolean"
? (step.frames.some((frame) => frame.motion.motionType === 1) === fixture.expected.moving)
: true;
const complete = step.ok === true &&
missingPins.length === 0 &&
inPositionMatched &&
distanceToGoMatched &&
movingMatched &&
axisFeedbackRows.every((row) => row.matched);
const simConfigEvidenceRows = virtualHalSimConfigTargetsById(fixture.simConfigTargets ?? [])
.map((target) => ({
id: target.id,
label: target.label,
sourceFiles: [...target.sourceFiles],
capabilities: [...target.capabilities],
evidence: target.evidence,
}));
const missingSimConfigTargets = (fixture.simConfigTargets ?? [])
.filter((targetId) => !simConfigEvidenceRows.some((target) => target.id === targetId));
const simConfigSourceFiles = [...new Set(simConfigEvidenceRows.flatMap((target) => target.sourceFiles))];
const normalizedSimConfigSourceFiles = simConfigSourceFiles.map(normalizeLinuxCncSourcePath);
const missingManifestFiles = manifestProvided
? simConfigSourceFiles.filter((file, index) => !manifestSet.has(normalizedSimConfigSourceFiles[index]))
: [];
return {
id: fixture.id,
label: fixture.label,
source: "linuxcnc-motion-source-derived-virtual-motion-controller",
sourceFiles: [...(fixture.sourceFiles ?? [])],
simConfigTargets: [...(fixture.simConfigTargets ?? [])],
simConfigEvidenceRows,
simConfigSourceFiles,
missingSimConfigTargets,
manifestChecked: manifestProvided,
missingManifestFiles,
complete: complete && missingManifestFiles.length === 0,
target: { ...(fixture.target ?? {}) },
initialPosition: { ...(fixture.initialPosition ?? {}) },
maxVelocity: fixture.maxVelocity,
dt: fixture.dt,
cycleCount: step.cycleCount,
inPosition: step.inPosition,
distanceToGo: step.distanceToGo,
currentVel: step.state.motion.currentVel,
requestedVel: step.state.motion.requestedVel,
servoPeriod: step.state.motion.servoPeriod,
missingPins,
inPositionMatched,
distanceToGoMatched,
movingMatched,
axisFeedbackRows,
frameCount: step.frames.length,
};
});
const missingFixtures = rows.filter((row) => !row.complete).map((row) => row.id);
const missingSimConfigTargets = rows.flatMap((row) => row.missingSimConfigTargets);
const missingManifestFiles = [...new Set(rows.flatMap((row) => row.missingManifestFiles))];
const complete = rows.length > 0 &&
missingFixtures.length === 0 &&
missingSimConfigTargets.length === 0 &&
missingManifestFiles.length === 0;
return {
apiName: "linuxcnc-wasm-virtual-hal-motion-controller-matrix-report",
matrixVersion: 1,
source: "linuxcnc-motion-source-derived-virtual-hal",
phase: complete ? "ready" : "blocked",
complete,
webSimulationSatisfied: complete,
sourceFiles: [...new Set(rows.flatMap((row) => row.sourceFiles))],
simConfigSourceFiles: [...new Set(rows.flatMap((row) => row.simConfigSourceFiles))],
simConfigTargets: [...new Set(rows.flatMap((row) => row.simConfigTargets))],
manifestChecked: manifestProvided,
requiredPins: [...new Set(fixtures.flatMap((fixture) => fixture.requiredPins ?? []))],
fixtureCount: rows.length,
missingFixtures,
missingSimConfigTargets,
missingManifestFiles,
rows,
summaryRows: [
{ id: "fixtures", label: "Motion matrix fixtures", value: `${rows.length}` },
{ id: "source", label: "LinuxCNC motion source", value: complete ? "source-derived" : "incomplete" },
{ id: "sim-config-source", label: "LinuxCNC sim config source", value: missingSimConfigTargets.length === 0 ? "source-derived" : `missing ${missingSimConfigTargets.length}` },
{ id: "manifest", label: "Sim config manifest check", value: manifestProvided ? (missingManifestFiles.length === 0 ? "passed" : "failed") : "not provided" },
{ id: "servo-period", label: "Servo period stepping", value: complete ? "covered" : "blocked" },
{ id: "axis-joint-feedback", label: "Axis/joint feedback", value: complete ? "covered" : "blocked" },
],
};
}
export function createVirtualHalSimulationReplacementReport(halState = createVirtualHalState(), options = {}) {
const state = normalizeVirtualHalState(halState);
const runtime = createVirtualHalSimulationRuntimeReport(state, options);
const sourceCompliance = createVirtualHalSourceComplianceReport({ halState: state });
const simConfigSourceCoverage = createVirtualHalSimConfigSourceCoverageReport(options);
const commandScriptFixtures = createVirtualHalCommandScriptFixtureReport({ ...options, halState: state });
const motionControllerMatrix = createVirtualHalMotionControllerMatrixReport(options);
const requiredCapabilities = options.requiredCapabilities ?? [
"realtime-hal-simulation-replacement",
"halcmd-simulation-replacement",
"motion-controller-simulation-replacement",
"hal-pin-signal-param-store",
"halcmd-setp-sets-net-show-getp-gets",
"servo-period-motion-step",
"axis-joint-position-feedback",
];
const missingCapabilities = requiredCapabilities
.filter((capability) => !runtime.capabilities.includes(capability));
const replacements = {
"linuxcnc-realtime-hal": {
ready: missingCapabilities.length === 0,
replacement: "virtual-hal-pin-signal-param-store",
evidence: ["pin registry", "signal/net store", "thread/function registry"],
},
halcmd: {
ready: runtime.capabilities.includes("halcmd-setp-sets-net-show-getp-gets"),
replacement: "executeVirtualHalcmd / executeVirtualHalCommand",
evidence: ["setp", "sets", "newsig", "net", "show", "getp", "gets", "loadrt", "loadusr", "addf", "start", "stop"],
},
"motion-controller": {
ready: runtime.capabilities.includes("servo-period-motion-step"),
replacement: "stepVirtualHalMotionController / stepVirtualHalMotion",
evidence: ["servo period", "target position", "velocity limit", "distance-to-go", "in-position"],
},
};
const ready = missingCapabilities.length === 0 &&
Object.values(replacements).every((item) => item.ready === true) &&
motionControllerMatrix.complete === true;
return {
apiName: "linuxcnc-wasm-virtual-hal-simulation-replacement-report",
replacementVersion: 1,
source: "simulation-grade-virtual-hal",
phase: ready ? "ready" : "blocked",
ready,
replacesHostRuntimeForSimulation: true,
replacementTargets: [...VIRTUAL_HAL_SIMULATION_REPLACEMENT_TARGETS],
hardRealtime: false,
realtimeKernelAbi: false,
requiredCapabilities,
missingCapabilities,
replacements,
sourceCompliance,
simConfigSourceCoverage,
commandScriptFixtures,
motionControllerMatrix,
runtime,
rows: [
{ id: "realtime-hal", label: "LinuxCNC realtime HAL", value: replacements["linuxcnc-realtime-hal"].ready ? "virtual replacement ready" : "blocked" },
{ id: "halcmd", label: "halcmd", value: replacements.halcmd.ready ? "virtual replacement ready" : "blocked" },
{ id: "motion-controller", label: "motion controller", value: replacements["motion-controller"].ready ? "virtual replacement ready" : "blocked" },
{ id: "hard-realtime", label: "Hard realtime", value: "not required for simulation" },
],
note: options.note ?? "Complete simulation replacement for browser/project workflows; not a Linux kernel realtime ABI or external hardware driver runtime.",
};
}
function normalizeLinuxCncSourcePath(path) {
const clean = String(path ?? "")
.replace(/^wasm-port\/vendor\/linuxcnc\//, "linuxcnc/")
.replace(/^vendor\/linuxcnc\//, "linuxcnc/")
.replace(/^linuxcnc\//, "");
const parts = [];
for (const part of clean.split("/")) {
if (!part || part === ".") {
continue;
}
if (part === "..") {
parts.pop();
continue;
}
parts.push(part);
}
return parts.join("/");
}
function normalizeSimConfigInventoryPath(path) {
return normalizeLinuxCncSourcePath(path).replace(/^configs\/sim\//, "");
}
function sourceManifestSetFromText(manifestText = "") {
return new Set(String(manifestText)
.split("\n")
.map((line) => line.trim())
.filter((line) => line && !line.startsWith("#"))
.map(normalizeLinuxCncSourcePath));
}
function isGeneratedBoundaryEvidenceReady(boundaryEvidence = {}, fixturePath = "", owningIniPath = "") {
const boundarySummary = boundaryEvidence.boundarySummary ?? {};
const iniBoundarySummary = boundaryEvidence.iniBoundarySummary ?? {};
const baselineSummary = boundaryEvidence.baselineSummary ?? {};
const sourceArtifactHashes = boundaryEvidence.sourceArtifactHashes ?? {};
const sourceArtifacts = [...(boundaryEvidence.sourceArtifacts ?? [])];
const normalizedFixturePath = normalizeSimConfigInventoryPath(fixturePath);
const normalizedOwningIniPath = normalizeSimConfigInventoryPath(owningIniPath);
return sourceArtifacts.includes("wasm-port/build/wasm/sim-configs-inventory/boundary-summary.tsv") &&
sourceArtifacts.includes("wasm-port/build/wasm/sim-configs-inventory/ini-boundary-summary.tsv") &&
Object.entries(VIRTUAL_HAL_SIM_CONFIG_INVENTORY_ARTIFACT_HASHES)
.every(([artifactPath, expectedHash]) => sourceArtifactHashes[artifactPath] === expectedHash) &&
baselineSummary.executed === VIRTUAL_HAL_SIM_CONFIG_INVENTORY_BASELINE.executed &&
baselineSummary.passed === VIRTUAL_HAL_SIM_CONFIG_INVENTORY_BASELINE.passed &&
baselineSummary.skipped === VIRTUAL_HAL_SIM_CONFIG_INVENTORY_BASELINE.skipped &&
baselineSummary.unexpectedFail === VIRTUAL_HAL_SIM_CONFIG_INVENTORY_BASELINE.unexpectedFail &&
normalizeSimConfigInventoryPath(boundarySummary.path) === normalizedFixturePath &&
normalizeSimConfigInventoryPath(boundarySummary.ini) === normalizedOwningIniPath &&
boundarySummary.recommendedBlocked === "UNAVAILABLE" &&
typeof boundarySummary.dependencies === "string" &&
boundarySummary.dependencies.includes("missing_vendored_ini:") &&
normalizeSimConfigInventoryPath(iniBoundarySummary.ini) === normalizedOwningIniPath &&
iniBoundarySummary.vendored === 0 &&
iniBoundarySummary.reportAvailable === 0 &&
iniBoundarySummary.recommendedBlocked === "UNAVAILABLE" &&
typeof iniBoundarySummary.dependencies === "string" &&
iniBoundarySummary.dependencies.includes("missing_vendored_ini:");
}
export function createVirtualHalSimConfigSourceCoverageReport(options = {}) {
const targets = options.targets ?? VIRTUAL_HAL_SIM_CONFIG_SOURCE_TARGETS;
const manifestProvided = typeof options.manifestText === "string" || Array.isArray(options.manifestEntries);
const manifestSet = new Set([
...sourceManifestSetFromText(options.manifestText ?? ""),
...(options.manifestEntries ?? []).map(normalizeLinuxCncSourcePath),
]);
const rows = targets.map((target) => {
const sourceFiles = [...(target.sourceFiles ?? [])];
const normalizedSourceFiles = sourceFiles.map(normalizeLinuxCncSourcePath);
const missingManifestFiles = manifestProvided
? sourceFiles.filter((file, index) => !manifestSet.has(normalizedSourceFiles[index]))
: [];
return {
id: target.id,
label: target.label,
source: "linuxcnc/configs/sim",
sourceFiles,
capabilities: [...(target.capabilities ?? [])],
evidence: target.evidence ?? "",
sourceDerived: sourceFiles.length > 0 && missingManifestFiles.length === 0,
manifestChecked: manifestProvided,
missingManifestFiles,
};
});
const missingTargets = rows
.filter((row) => !row.sourceDerived)
.map((row) => row.id);
const coveredCapabilities = [...new Set(rows.flatMap((row) => row.capabilities))].sort();
const requiredCapabilities = options.requiredCapabilities ?? [
"realtime-hal-simulation-replacement",
"halcmd-simulation-replacement",
"motion-controller-simulation-replacement",
"hal-pin-signal-param-store",
"userspace-load-command-stubs",
"servo-period-motion-step",
"axis-joint-position-feedback",
"spindle-coolant-tool-status",
"interpreter-hal-bridge-snapshot",
];
const missingCapabilities = requiredCapabilities.filter((capability) => !coveredCapabilities.includes(capability));
const complete = rows.length > 0 && missingTargets.length === 0 && missingCapabilities.length === 0;
return {
apiName: "linuxcnc-wasm-virtual-hal-sim-config-source-coverage-report",
coverageVersion: 1,
source: "linuxcnc-configs-sim-source-derived-virtual-hal",
phase: complete ? "source-covered" : "incomplete",
complete,
webSimulationSatisfied: complete,
manifestChecked: manifestProvided,
targetCount: rows.length,
sourceFiles: [...new Set(rows.flatMap((row) => row.sourceFiles))],
coveredCapabilities,
requiredCapabilities,
missingCapabilities,
missingTargets,
rows,
summaryRows: [
{ id: "targets", label: "LinuxCNC sim config targets", value: `${rows.length}` },
{ id: "source-files", label: "Source files", value: `${new Set(rows.flatMap((row) => row.sourceFiles)).size}` },
{ id: "capabilities", label: "Covered capabilities", value: missingCapabilities.length === 0 ? "covered" : `missing ${missingCapabilities.length}` },
{ id: "manifest", label: "Manifest check", value: manifestProvided ? (missingTargets.length === 0 ? "passed" : "failed") : "not provided" },
],
};
}
export function createVirtualHalSimConfigPromotionCandidateReport(options = {}) {
const candidates = options.candidates ?? VIRTUAL_HAL_SIM_CONFIG_PROMOTION_CANDIDATES;
const manifestProvided = typeof options.manifestText === "string" || Array.isArray(options.manifestEntries);
const manifestSet = new Set([
...sourceManifestSetFromText(options.manifestText ?? ""),
...(options.manifestEntries ?? []).map(normalizeLinuxCncSourcePath),
]);
const blockedKinds = new Set(options.blockedKinds ?? [
"L4-PYTHON-REMAP",
"L4-TOOL-DB",
"L4-USER-M-PROCESS",
"UPSTREAM-DEMO",
]);
const allowedNodeStatuses = new Set(options.allowedNodeStatuses ?? ["PASS"]);
const requiredVirtualHalReports = options.requiredVirtualHalReports ?? [
"source-compliance",
"sim-config-source-coverage",
"command-script-fixtures",
"manifest-backed-motion-matrix",
];
const simConfigCoverage = options.simConfigSourceCoverage ?? createVirtualHalSimConfigSourceCoverageReport(options);
const sourceCompliance = options.sourceCompliance ?? createVirtualHalSourceComplianceReport(options);
const commandScriptFixtures = options.commandScriptFixtures ?? createVirtualHalCommandScriptFixtureReport(options);
const motionControllerMatrix = options.motionControllerMatrix ?? createVirtualHalMotionControllerMatrixReport(options);
const availableReports = new Set([
...(sourceCompliance.complete ? ["source-compliance"] : []),
...(simConfigCoverage.complete ? ["sim-config-source-coverage"] : []),
...(commandScriptFixtures.complete ? ["command-script-fixtures"] : []),
...(motionControllerMatrix.complete && motionControllerMatrix.manifestChecked ? ["manifest-backed-motion-matrix"] : []),
]);
const rows = candidates.map((candidate) => {
const sourceFiles = [...(candidate.sourceFiles ?? [])];
const normalizedSourceFiles = sourceFiles.map(normalizeLinuxCncSourcePath);
const missingManifestFiles = manifestProvided
? sourceFiles.filter((file, index) => !manifestSet.has(normalizedSourceFiles[index]))
: [];
const missingRequiredReports = requiredVirtualHalReports
.filter((report) => !availableReports.has(report));
const blockedKindExcluded = !blockedKinds.has(candidate.blockedKind);
const nodeStatusReady = allowedNodeStatuses.has(candidate.currentNodeInventoryStatus) ||
candidate.nonMainFixture === true;
const simConfigTargetReady = !candidate.simConfigTarget ||
VIRTUAL_HAL_SIM_CONFIG_SOURCE_TARGETS.some((target) => target.id === candidate.simConfigTarget);
const complete = sourceFiles.length > 0 &&
missingManifestFiles.length === 0 &&
missingRequiredReports.length === 0 &&
blockedKindExcluded &&
nodeStatusReady &&
simConfigTargetReady;
return {
id: candidate.id,
label: candidate.label,
source: "linuxcnc-configs-sim-source-derived-virtual-hal-promotion-candidate",
simConfigTarget: candidate.simConfigTarget ?? null,
sourceFiles,
iniPath: candidate.iniPath,
gcodePath: candidate.gcodePath,
currentNodeInventoryStatus: candidate.currentNodeInventoryStatus,
currentMatrixBrowserStatus: candidate.currentMatrixBrowserStatus,
dependencyClass: candidate.dependencyClass,
targetBrowserEvidence: candidate.targetBrowserEvidence,
requiredVirtualHalReports: [...(candidate.requiredVirtualHalReports ?? requiredVirtualHalReports)],
blockedKind: candidate.blockedKind,
blockedKindExcluded,
nonMainFixture: candidate.nonMainFixture === true,
nodeStatusReady,
simConfigTargetReady,
manifestChecked: manifestProvided,
missingManifestFiles,
missingRequiredReports,
evidence: candidate.evidence,
complete,
};
});
const missingCandidates = rows.filter((row) => !row.complete).map((row) => row.id);
const blockedCandidateIds = rows.filter((row) => !row.blockedKindExcluded).map((row) => row.id);
const missingManifestFiles = [...new Set(rows.flatMap((row) => row.missingManifestFiles))];
const missingRequiredReports = [...new Set(rows.flatMap((row) => row.missingRequiredReports))];
const familyRows = [...new Set(rows.map((row) => row.simConfigTarget ?? row.id))].map((familyId) => {
const familyCandidates = rows.filter((row) => (row.simConfigTarget ?? row.id) === familyId);
const completeCount = familyCandidates.filter((row) => row.complete).length;
const explicitBrowserDiagnosticsCount = familyCandidates
.filter((row) => row.targetBrowserEvidence === "explicit-browser-diagnostics").length;
const sourceFiles = [...new Set(familyCandidates.flatMap((row) => row.sourceFiles))];
return {
id: familyId,
candidateIds: familyCandidates.map((row) => row.id),
candidateCount: familyCandidates.length,
completeCount,
explicitBrowserDiagnosticsCount,
explicitBrowserDiagnosticsReady: explicitBrowserDiagnosticsCount === familyCandidates.length,
complete: completeCount === familyCandidates.length,
sourceFiles,
};
});
const complete = rows.length > 0 &&
missingCandidates.length === 0 &&
blockedCandidateIds.length === 0 &&
missingManifestFiles.length === 0 &&
missingRequiredReports.length === 0;
return {
apiName: "linuxcnc-wasm-virtual-hal-sim-config-promotion-candidate-report",
reportVersion: 1,
source: "linuxcnc-configs-sim-source-derived-virtual-hal",
phase: complete ? "ready" : "blocked",
complete,
webSimulationSatisfied: complete,
inventoryBaselineUnchanged: true,
manifestChecked: manifestProvided,
candidateCount: rows.length,
sourceFiles: [...new Set(rows.flatMap((row) => row.sourceFiles))],
requiredVirtualHalReports,
availableVirtualHalReports: [...availableReports],
blockedKinds: [...blockedKinds],
missingCandidates,
blockedCandidateIds,
missingManifestFiles,
missingRequiredReports,
familyRows,
rows,
summaryRows: [
{ id: "candidates", label: "Promotion candidates", value: `${rows.length}` },
{ id: "families", label: "Promotion families", value: `${familyRows.length}` },
{ id: "blocked-family", label: "Blocked-family exclusion", value: blockedCandidateIds.length === 0 ? "passed" : `blocked ${blockedCandidateIds.length}` },
{ id: "reports", label: "Virtual HAL reports", value: missingRequiredReports.length === 0 ? "complete" : `missing ${missingRequiredReports.length}` },
{ id: "manifest", label: "Manifest check", value: manifestProvided ? (missingManifestFiles.length === 0 ? "passed" : "failed") : "not provided" },
{ id: "inventory-baseline", label: "Inventory baseline", value: "unchanged" },
],
};
}
export function createVirtualHalSimConfigEvidenceExpansionReport(options = {}) {
const candidates = options.candidates ?? VIRTUAL_HAL_SIM_CONFIG_EVIDENCE_EXPANSION_CANDIDATES;
const manifestProvided = typeof options.manifestText === "string" || Array.isArray(options.manifestEntries);
const manifestSet = new Set([
...sourceManifestSetFromText(options.manifestText ?? ""),
...(options.manifestEntries ?? []).map(normalizeLinuxCncSourcePath),
]);
const requiredVirtualHalReports = options.requiredVirtualHalReports ?? [
"source-compliance",
"sim-config-source-coverage",
"command-script-fixtures",
"manifest-backed-motion-matrix",
];
const simConfigCoverage = options.simConfigSourceCoverage ?? createVirtualHalSimConfigSourceCoverageReport(options);
const sourceCompliance = options.sourceCompliance ?? createVirtualHalSourceComplianceReport(options);
const commandScriptFixtures = options.commandScriptFixtures ?? createVirtualHalCommandScriptFixtureReport(options);
const motionControllerMatrix = options.motionControllerMatrix ?? createVirtualHalMotionControllerMatrixReport(options);
const availableReports = new Set([
...(sourceCompliance.complete ? ["source-compliance"] : []),
...(simConfigCoverage.complete ? ["sim-config-source-coverage"] : []),
...(commandScriptFixtures.complete ? ["command-script-fixtures"] : []),
...(motionControllerMatrix.complete && motionControllerMatrix.manifestChecked ? ["manifest-backed-motion-matrix"] : []),
]);
const rows = candidates.map((candidate) => {
const sourceFiles = [...(candidate.sourceFiles ?? [])];
const normalizedSourceFiles = sourceFiles.map(normalizeLinuxCncSourcePath);
const missingManifestFiles = manifestProvided
? sourceFiles.filter((file, index) => !manifestSet.has(normalizedSourceFiles[index]))
: [];
const missingRequiredReports = requiredVirtualHalReports
.filter((report) => !availableReports.has(report));
const sourceReady = sourceFiles.length > 0 &&
missingManifestFiles.length === 0 &&
missingRequiredReports.length === 0;
return {
id: candidate.id,
label: candidate.label,
source: "linuxcnc-configs-sim-source-derived-virtual-hal-evidence-expansion-candidate",
simConfigTarget: candidate.simConfigTarget ?? null,
sourceFiles,
iniPath: candidate.iniPath,
gcodePath: candidate.gcodePath,
currentNodeInventoryStatus: candidate.currentNodeInventoryStatus,
currentMatrixBrowserStatus: candidate.currentMatrixBrowserStatus,
dependencyClass: candidate.dependencyClass,
targetBrowserEvidence: candidate.targetBrowserEvidence,
requiredVirtualHalReports,
blockedKind: candidate.blockedKind,
promotionAllowed: candidate.promotionAllowed === true,
nodeStatusReady: candidate.currentNodeInventoryStatus === "PASS",
browserRepresentativeReady: candidate.currentMatrixBrowserStatus === "REP",
manifestChecked: manifestProvided,
missingManifestFiles,
missingRequiredReports,
evidence: candidate.evidence,
sourceReady,
complete: sourceReady &&
candidate.blockedKind === "-" &&
candidate.promotionAllowed !== true &&
candidate.currentNodeInventoryStatus === "PASS" &&
candidate.currentMatrixBrowserStatus === "REP",
};
});
const missingCandidates = rows.filter((row) => !row.complete).map((row) => row.id);
const promotionAllowedViolations = rows.filter((row) => row.promotionAllowed === true).map((row) => row.id);
const blockedCandidateIds = rows.filter((row) => row.blockedKind !== "-").map((row) => row.id);
const missingManifestFiles = [...new Set(rows.flatMap((row) => row.missingManifestFiles))];
const missingRequiredReports = [...new Set(rows.flatMap((row) => row.missingRequiredReports))];
const complete = rows.length > 0 &&
missingCandidates.length === 0 &&
promotionAllowedViolations.length === 0 &&
blockedCandidateIds.length === 0 &&
missingManifestFiles.length === 0 &&
missingRequiredReports.length === 0;
return {
apiName: "linuxcnc-wasm-virtual-hal-sim-config-evidence-expansion-report",
reportVersion: 1,
source: "linuxcnc-configs-sim-source-derived-virtual-hal",
phase: complete ? "ready" : "blocked",
complete,
webSimulationSatisfied: complete,
inventoryBaselineUnchanged: true,
promotionAllowed: false,
manifestChecked: manifestProvided,
candidateCount: rows.length,
readyCandidateCount: rows.filter((row) => row.complete).length,
sourceFiles: [...new Set(rows.flatMap((row) => row.sourceFiles))],
requiredVirtualHalReports,
availableVirtualHalReports: [...availableReports],
missingCandidates,
promotionAllowedViolations,
blockedCandidateIds,
missingManifestFiles,
missingRequiredReports,
rows,
summaryRows: [
{ id: "candidates", label: "Expansion candidates", value: `${rows.length}` },
{ id: "ready-candidates", label: "Ready candidates", value: `${rows.filter((row) => row.complete).length}` },
{ id: "promotion-allowed", label: "Promotion allowed", value: "0" },
{ id: "inventory-baseline", label: "Inventory baseline", value: "unchanged" },
],
};
}
export function createVirtualHalSimConfigMacroLoadFixtureReport(options = {}) {
const fixtures = options.fixtures ?? VIRTUAL_HAL_SIM_CONFIG_MACRO_LOAD_FIXTURES;
const blockedFixtures = options.blockedFixtures ?? VIRTUAL_HAL_SIM_CONFIG_MACRO_LOAD_BLOCKED_FIXTURES;
const auditCandidates = options.auditCandidates ?? VIRTUAL_HAL_SIM_CONFIG_MACRO_LOAD_AUDIT_CANDIDATES;
const manifestProvided = typeof options.manifestText === "string" || Array.isArray(options.manifestEntries);
const manifestSet = new Set([
...sourceManifestSetFromText(options.manifestText ?? ""),
...(options.manifestEntries ?? []).map(normalizeLinuxCncSourcePath),
]);
const blockedKinds = new Set(options.blockedKinds ?? [
"L4-PYTHON-REMAP",
"L4-TOOL-DB",
"L4-USER-M-PROCESS",
"UPSTREAM-DEMO",
]);
const allowedStatuses = new Set(options.allowedStatuses ?? ["PASS", "NON_MAIN_CLASS"]);
const rows = fixtures.map((fixture) => {
const sourceFiles = [...(fixture.sourceFiles ?? [])];
const normalizedSourceFiles = sourceFiles.map(normalizeLinuxCncSourcePath);
const missingManifestFiles = manifestProvided
? sourceFiles.filter((file, index) => !manifestSet.has(normalizedSourceFiles[index]))
: [];
const blockedKindExcluded = !blockedKinds.has(fixture.blockedKind);
const statusReady = allowedStatuses.has(fixture.currentNodeInventoryStatus);
const nonMainFixtureReady = fixture.nonMainFixture === true &&
fixture.targetBrowserEvidence === "non-main-fixture-diagnostics";
const declarationEvidence = fixture.declarationEvidence ?? null;
const requiresDeclarationEvidence = typeof fixture.declarationSource === "string" &&
fixture.declarationSource.length > 0;
const declarationEvidenceReady = !requiresDeclarationEvidence || (
declarationEvidence !== null &&
declarationEvidence.sourceFile === fixture.owningIniPath &&
declarationEvidence.key === fixture.declarationSource &&
declarationEvidence.value === fixture.fixturePath.split("/").at(-1) &&
Number.isInteger(declarationEvidence.line) &&
declarationEvidence.line > 0 &&
typeof declarationEvidence.sourceLine === "string" &&
declarationEvidence.sourceLine.includes(`${declarationEvidence.key} = ${declarationEvidence.value}`)
);
const complete = sourceFiles.length > 0 &&
missingManifestFiles.length === 0 &&
blockedKindExcluded &&
statusReady &&
nonMainFixtureReady &&
declarationEvidenceReady;
return {
id: fixture.id,
label: fixture.label,
source: "linuxcnc-configs-sim-source-derived-virtual-hal-macro-load-fixture",
fixturePath: fixture.fixturePath,
sourceFiles,
owningIniPath: fixture.owningIniPath,
pairedMainProgram: fixture.pairedMainProgram,
declarationSource: fixture.declarationSource,
declarationEvidence,
declarationEvidenceReady,
requiresDeclarationEvidence,
currentNodeInventoryStatus: fixture.currentNodeInventoryStatus,
dependencyClass: fixture.dependencyClass,
targetBrowserEvidence: fixture.targetBrowserEvidence,
blockedKind: fixture.blockedKind,
blockedKindExcluded,
nonMainFixture: fixture.nonMainFixture === true,
declaredOnly: fixture.declaredOnly === true,
statusReady,
manifestChecked: manifestProvided,
missingManifestFiles,
evidence: fixture.evidence,
complete,
};
});
const missingFixtures = rows.filter((row) => !row.complete).map((row) => row.id);
const blockedFixtureIds = rows.filter((row) => !row.blockedKindExcluded).map((row) => row.id);
const declarationEvidenceViolations = rows
.filter((row) => row.requiresDeclarationEvidence === true && row.declarationEvidenceReady !== true)
.map((row) => row.id);
const standaloneMainViolations = rows
.filter((row) => row.nonMainFixture !== true || row.targetBrowserEvidence !== "non-main-fixture-diagnostics")
.map((row) => row.id);
const missingManifestFiles = [...new Set(rows.flatMap((row) => row.missingManifestFiles))];
const positiveFixturePaths = new Set(rows.map((row) => normalizeLinuxCncSourcePath(row.fixturePath)));
const blockedRows = blockedFixtures.map((fixture) => {
const excludedFromPositiveFixtures = !positiveFixturePaths.has(normalizeLinuxCncSourcePath(fixture.fixturePath));
const boundaryEvidence = fixture.boundaryEvidence ?? {};
const boundaryEvidenceReady = isGeneratedBoundaryEvidenceReady(boundaryEvidence, fixture.fixturePath, fixture.owningIniPath);
return {
id: fixture.id,
source: "linuxcnc-configs-sim-source-derived-virtual-hal-macro-load-blocked-fixture",
fixturePath: fixture.fixturePath,
owningIniPath: fixture.owningIniPath,
blockedKind: fixture.blockedKind,
dependencyClass: fixture.dependencyClass,
excludedFromPositiveFixtures,
boundaryEvidence,
boundaryEvidenceReady,
reason: fixture.reason,
complete: excludedFromPositiveFixtures && boundaryEvidenceReady,
};
});
const blockedFixturePromotionViolations = blockedRows
.filter((row) => row.excludedFromPositiveFixtures !== true)
.map((row) => row.id);
const blockedBoundaryEvidenceViolations = blockedRows
.filter((row) => row.boundaryEvidenceReady !== true)
.map((row) => row.id);
const auditRows = auditCandidates.map((candidate) => {
const excludedFromPositiveFixtures = !positiveFixturePaths.has(normalizeLinuxCncSourcePath(candidate.fixturePath));
const boundaryEvidence = candidate.boundaryEvidence ?? {};
const boundaryEvidenceReady = isGeneratedBoundaryEvidenceReady(boundaryEvidence, candidate.fixturePath, candidate.owningIniPath);
return {
id: candidate.id,
source: "linuxcnc-configs-sim-source-derived-virtual-hal-macro-load-audit-candidate",
fixturePath: candidate.fixturePath,
owningIniPath: candidate.owningIniPath,
dependencyClass: candidate.dependencyClass,
auditStatus: candidate.auditStatus,
promotionAllowed: candidate.promotionAllowed === true,
excludedFromPositiveFixtures,
boundaryEvidence,
boundaryEvidenceReady,
reason: candidate.reason,
complete: excludedFromPositiveFixtures && candidate.promotionAllowed !== true && boundaryEvidenceReady,
};
});
const missingAuditRows = auditRows
.filter((row) => !row.complete)
.map((row) => row.id);
const auditPromotionViolations = auditRows
.filter((row) => row.promotionAllowed !== true && row.excludedFromPositiveFixtures !== true)
.map((row) => row.id);
const auditBoundaryEvidenceViolations = auditRows
.filter((row) => row.boundaryEvidenceReady !== true)
.map((row) => row.id);
const auditPromotionAllowedViolations = auditRows
.filter((row) => row.promotionAllowed === true && row.boundaryEvidenceReady !== true)
.map((row) => row.id);
const complete = rows.length > 0 &&
missingFixtures.length === 0 &&
missingAuditRows.length === 0 &&
blockedFixtureIds.length === 0 &&
declarationEvidenceViolations.length === 0 &&
standaloneMainViolations.length === 0 &&
missingManifestFiles.length === 0 &&
blockedFixturePromotionViolations.length === 0 &&
blockedBoundaryEvidenceViolations.length === 0 &&
auditPromotionViolations.length === 0 &&
auditBoundaryEvidenceViolations.length === 0 &&
auditPromotionAllowedViolations.length === 0;
return {
apiName: "linuxcnc-wasm-virtual-hal-sim-config-macro-load-fixture-report",
reportVersion: 1,
source: "linuxcnc-configs-sim-source-derived-virtual-hal",
phase: complete ? "ready" : "blocked",
complete,
webSimulationSatisfied: complete,
inventoryBaselineUnchanged: true,
manifestChecked: manifestProvided,
fixtureCount: rows.length,
sourceFiles: [...new Set(rows.flatMap((row) => row.sourceFiles))],
fixturePaths: rows.map((row) => row.fixturePath),
missingFixtures,
blockedFixtureIds,
declarationEvidenceViolations,
standaloneMainViolations,
blockedFixturePromotionViolations,
blockedBoundaryEvidenceViolations,
missingAuditRows,
auditPromotionViolations,
auditBoundaryEvidenceViolations,
auditPromotionAllowedViolations,
missingManifestFiles,
rows,
blockedRows,
auditRows,
summaryRows: [
{ id: "fixtures", label: "Macro/load fixtures", value: `${rows.length}` },
{ id: "declarations", label: "Declaration evidence", value: declarationEvidenceViolations.length === 0 ? "passed" : `violations ${declarationEvidenceViolations.length}` },
{ id: "non-main", label: "Non-main fixture gate", value: standaloneMainViolations.length === 0 ? "passed" : `violations ${standaloneMainViolations.length}` },
{ id: "blocked-fixture-lock", label: "Blocked fixture lock", value: blockedFixturePromotionViolations.length === 0 && blockedBoundaryEvidenceViolations.length === 0 ? "passed" : "violations" },
{ id: "ui-family-audit", label: "UI-family macro/load audit", value: missingAuditRows.length === 0 && auditPromotionViolations.length === 0 && auditBoundaryEvidenceViolations.length === 0 && auditPromotionAllowedViolations.length === 0 ? "passed" : "violations" },
{ id: "blocked-family", label: "Blocked-family exclusion", value: blockedFixtureIds.length === 0 ? "passed" : `blocked ${blockedFixtureIds.length}` },
{ id: "manifest", label: "Manifest check", value: manifestProvided ? (missingManifestFiles.length === 0 ? "passed" : "failed") : "not provided" },
{ id: "inventory-baseline", label: "Inventory baseline", value: "unchanged" },
],
};
}
export function createVirtualHalSourceComplianceReport(options = {}) {
const halState = options.halState ?? createVirtualHalState();
const snapshot = options.snapshot ?? createVirtualHalWasmBridgeSnapshot(halState);
const systemCoverage = options.systemCoverage ?? createVirtualHalSystemCoverageReport({
halState,
snapshot,
});
const simConfigSourceCoverage = options.simConfigSourceCoverage ?? createVirtualHalSimConfigSourceCoverageReport(options);
const commandScriptFixtures = options.commandScriptFixtures ?? createVirtualHalCommandScriptFixtureReport(options);
const motionControllerMatrix = options.motionControllerMatrix ?? createVirtualHalMotionControllerMatrixReport(options);
const allowedSourceFiles = new Set([
...Object.values(VIRTUAL_HAL_SOURCE_FILES),
...VIRTUAL_HAL_SIM_CONFIG_SOURCE_TARGETS.flatMap((target) => target.sourceFiles),
...VIRTUAL_HAL_COMMAND_SCRIPT_FIXTURES.flatMap((fixture) => fixture.sourceFiles),
...VIRTUAL_HAL_MOTION_CONTROLLER_MATRIX_FIXTURES.flatMap((fixture) => fixture.sourceFiles),
...(options.allowedSourceFiles ?? []),
]);
const requiredCapabilities = options.requiredCapabilities ?? VIRTUAL_HAL_SIMULATION_RUNTIME_CAPABILITIES;
const capabilityRows = requiredCapabilities.map((capability) => {
const entry = VIRTUAL_HAL_SOURCE_DERIVED_CAPABILITIES[capability];
const sourceFiles = [...(entry?.sourceFiles ?? [])];
const missingSource = !entry || sourceFiles.length === 0;
const disallowedSourceFiles = sourceFiles.filter((file) => !allowedSourceFiles.has(file));
return {
id: capability,
label: entry?.label ?? capability,
capability,
sourceFiles,
evidence: entry?.evidence ?? "",
sourceDerived: !missingSource && disallowedSourceFiles.length === 0,
missingSource,
disallowedSourceFiles,
};
});
const targetRows = VIRTUAL_HAL_SIMULATION_REPLACEMENT_TARGETS.map((target) => {
const targetCapabilities = target === "linuxcnc-realtime-hal"
? ["realtime-hal-simulation-replacement", "hal-pin-signal-param-store"]
: target === "halcmd"
? ["halcmd-simulation-replacement", "halcmd-setp-sets-net-show-getp-gets", "userspace-load-command-stubs"]
: ["motion-controller-simulation-replacement", "servo-period-motion-step", "axis-joint-position-feedback"];
const rows = capabilityRows.filter((row) => targetCapabilities.includes(row.capability));
return {
id: target,
target,
sourceDerived: rows.length > 0 && rows.every((row) => row.sourceDerived),
capabilities: targetCapabilities,
sourceFiles: [...new Set(rows.flatMap((row) => row.sourceFiles))],
};
});
const missingCapabilitySources = capabilityRows
.filter((row) => !row.sourceDerived)
.map((row) => row.capability);
const missingFamilySources = systemCoverage.missingSourceFamilies ?? [];
const complete = missingCapabilitySources.length === 0 &&
missingFamilySources.length === 0 &&
simConfigSourceCoverage.complete === true &&
commandScriptFixtures.complete === true &&
motionControllerMatrix.complete === true &&
systemCoverage.complete === true &&
targetRows.every((row) => row.sourceDerived);
return {
apiName: "linuxcnc-wasm-virtual-hal-source-compliance-report",
complianceVersion: 1,
source: "linuxcnc-source-derived-virtual-hal",
phase: complete ? "source-complete" : "incomplete",
complete,
webSimulationSatisfied: complete,
sourceRule: "Virtual HAL functionality must be derived from LinuxCNC source files, configs, scripts, or LinuxCNC-backed runtime evidence.",
replacementTargets: [...VIRTUAL_HAL_SIMULATION_REPLACEMENT_TARGETS],
allowedSourceFiles: [...allowedSourceFiles],
sourceFiles: [...new Set([
...systemCoverage.sourceFiles,
...capabilityRows.flatMap((row) => row.sourceFiles),
...simConfigSourceCoverage.sourceFiles,
...commandScriptFixtures.sourceFiles,
...motionControllerMatrix.sourceFiles,
...motionControllerMatrix.simConfigSourceFiles,
])],
missingCapabilitySources,
missingFamilySources,
simConfigSourceCoverage,
commandScriptFixtures,
motionControllerMatrix,
targetRows,
capabilityRows,
familyRows: systemCoverage.families.map((family) => ({
id: family.id,
label: family.label,
coverage: family.coverage,
sourceFiles: [...family.sourceFiles],
sourceEvidence: family.sourceEvidence,
sourceDerived: family.sourceFiles.length > 0,
})),
rows: [
{ id: "targets", label: "Replacement targets", value: targetRows.every((row) => row.sourceDerived) ? "source-derived" : "incomplete" },
{ id: "capabilities", label: "Runtime capabilities", value: missingCapabilitySources.length === 0 ? "source-derived" : `missing ${missingCapabilitySources.length}` },
{ id: "pin-families", label: "HAL pin families", value: missingFamilySources.length === 0 ? "source-derived" : `missing ${missingFamilySources.length}` },
{ id: "sim-configs", label: "LinuxCNC sim configs", value: simConfigSourceCoverage.complete ? "source-derived" : "incomplete" },
{ id: "halcmd-fixtures", label: "HAL command script fixtures", value: commandScriptFixtures.complete ? "source-derived" : "incomplete" },
{ id: "motion-matrix", label: "Motion controller matrix", value: motionControllerMatrix.complete ? "source-derived" : "incomplete" },
{ id: "web-simulation", label: "Web simulation HAL", value: complete ? "satisfied" : "blocked" },
],
};
}
export function createVirtualHalSimulationRuntimeReport(halState = createVirtualHalState(), options = {}) {
const state = normalizeVirtualHalState(halState);
const snapshot = createVirtualHalWasmBridgeSnapshot(state);
return {
apiName: "linuxcnc-wasm-virtual-hal-simulation-runtime-report",
reportVersion: 1,
source: "simulation-grade-virtual-hal",
ready: true,
replacesHostRuntimeForSimulation: true,
replacementTargets: [...VIRTUAL_HAL_SIMULATION_REPLACEMENT_TARGETS],
hardRealtime: false,
hostRequirements: [],
unsupportedHostRequirements: ["kernel-realtime-scheduling", "external-device-drivers", "linuxcnc-realtime-module-abi"],
capabilities: [...VIRTUAL_HAL_SIMULATION_RUNTIME_CAPABILITIES],
pinCount: snapshot.values.filter(({ kind }) => kind === "pin").length,
signalCount: Object.keys(state.hal.signals ?? {}).length,
paramCount: Object.keys(state.hal.params ?? {}).length,
netCount: Object.keys(state.hal.nets ?? {}).length,
functionCount: state.hal.functions.length,
threadCount: Object.keys(state.hal.threads ?? {}).length,
commandCount: state.hal.commandLog.length,
motion: {
enabled: state.motion.enabled,
inPosition: state.motion.inPosition,
distanceToGo: state.motion.distanceToGo,
currentVel: state.motion.currentVel,
requestedVel: state.motion.requestedVel,
servoPeriod: state.motion.servoPeriod,
},
rows: [
{ id: "runtime", label: "Virtual HAL runtime", value: "ready" },
{ id: "host-requirements", label: "Host HAL requirements", value: "none for simulation" },
{ id: "hard-realtime", label: "Hard realtime", value: "no" },
{ id: "halcmd", label: "Virtual halcmd", value: "setp/sets/net/show/getp/gets/load/addf/start/stop" },
{ id: "motion-controller", label: "Virtual motion controller", value: "servo-period target stepping" },
{ id: "motion", label: "Motion step", value: state.motion.inPosition ? "in-position" : "moving" },
],
note: options.note ?? "Simulation-grade replacement for browser/project workflows; not a Linux kernel realtime ABI.",
};
}
export function createLinuxCncVirtualHalRuntime(initialState = createVirtualHalState()) {
let state = normalizeVirtualHalState(initialState);
return {
getState() {
return cloneVirtualHalState(state);
},
dispatch(action, options = {}) {
state = applyVirtualHalAction(state, action, options);
return cloneVirtualHalState(state);
},
getDroState() {
return createVirtualHalDroState(state);
},
getLimitsHomeState() {
return createVirtualHalLimitsHomeState(state);
},
getMachineStatusState() {
return createVirtualHalMachineStatusState(state);
},
getWasmBridgeSnapshot() {
return createVirtualHalWasmBridgeSnapshot(state);
},
getPinRegistry() {
return createVirtualHalPinRegistry(state);
},
readPin(name) {
return readVirtualHalPin(state, name);
},
writePin(update, options = {}) {
state = writeVirtualHalPin(state, update, options);
return cloneVirtualHalState(state);
},
applyPinUpdates(updates, options = {}) {
state = applyVirtualHalPinUpdates(state, updates, options);
return cloneVirtualHalState(state);
},
executeHalCommand(commandText, options = {}) {
const result = executeVirtualHalCommand(state, commandText, options);
state = result.state;
return result;
},
executeHalcmd(commandText, options = {}) {
const result = executeVirtualHalcmd(state, commandText, options);
state = result.state;
return result;
},
stepMotion(options = {}) {
state = stepVirtualHalMotion(state, options);
return cloneVirtualHalState(state);
},
stepMotionController(options = {}) {
const result = stepVirtualHalMotionController(state, options);
state = result.state;
return result;
},
getSimulationRuntimeReport(options = {}) {
return createVirtualHalSimulationRuntimeReport(state, options);
},
getSimulationReplacementReport(options = {}) {
return createVirtualHalSimulationReplacementReport(state, options);
},
getSourceComplianceReport(options = {}) {
return createVirtualHalSourceComplianceReport({ ...options, halState: state });
},
getSimConfigSourceCoverageReport(options = {}) {
return createVirtualHalSimConfigSourceCoverageReport(options);
},
getSimConfigPromotionCandidateReport(options = {}) {
return createVirtualHalSimConfigPromotionCandidateReport(options);
},
getSimConfigEvidenceExpansionReport(options = {}) {
return createVirtualHalSimConfigEvidenceExpansionReport(options);
},
getSimConfigMacroLoadFixtureReport(options = {}) {
return createVirtualHalSimConfigMacroLoadFixtureReport(options);
},
getCommandScriptFixtureReport(options = {}) {
return createVirtualHalCommandScriptFixtureReport({ ...options, halState: state });
},
getMotionControllerMatrixReport(options = {}) {
return createVirtualHalMotionControllerMatrixReport(options);
},
getIntegrityReport(options = {}) {
return createVirtualHalIntegrityReport({ ...options, halState: state });
},
getPinInventory() {
return createVirtualHalPinInventory(state);
},
getProjectReport(options = {}) {
return createVirtualHalProjectReport({ ...options, halState: state });
},
applyToInterpSdk(interpSdk, options = {}) {
return applyVirtualHalToInterpSdk(interpSdk, state, options);
},
};
}
function parseHalCommandValue(text) {
const value = String(text ?? "0").trim();
if (/^(true|on|yes)$/i.test(value)) {
return true;
}
if (/^(false|off|no)$/i.test(value)) {
return false;
}
return Number(value) || 0;
}
function parseHalCommandTarget(tokens) {
const target = {};
for (const token of tokens) {
const [rawName, rawValue] = token.split("=");
const name = rawName?.toLowerCase();
const value = Number(rawValue);
if (VIRTUAL_HAL_AXES.includes(name) && Number.isFinite(value)) {
target[name] = value;
}
}
return target;
}
function syncVirtualHalSignalsFromPin(state, pinName, value) {
for (const signal of Object.values(state.hal.signals ?? {})) {
if ((signal.pins ?? []).includes(pinName)) {
signal.value = normalizeHalNumericValue(value);
}
}
return state;
}
function propagateVirtualHalSignal(state, signalName) {
const signal = state.hal.signals?.[signalName];
if (!signal) {
return state;
}
let next = state;
for (const pinName of signal.pins ?? []) {
next = writeVirtualHalPin(next, {
name: pinName,
type: signal.type ?? "HAL_FLOAT",
value: signal.value,
});
}
next.hal.signals[signalName] = {
...signal,
value: normalizeHalNumericValue(signal.value),
pins: [...new Set(signal.pins ?? [])],
};
next.hal.nets[signalName] = [...next.hal.signals[signalName].pins];
return next;
}
function createVirtualHalShowRows(state, kind = "pin", pattern = "") {
const matcher = pattern ? new RegExp(pattern.replaceAll("*", ".*"), "i") : null;
const matches = (name) => !matcher || matcher.test(name);
if (kind === "sig" || kind === "signal") {
return Object.values(state.hal.signals ?? {})
.filter(({ name }) => matches(name))
.map((signal) => ({
kind: "signal",
name: signal.name,
type: signal.type ?? "HAL_FLOAT",
value: normalizeHalNumericValue(signal.value),
}));
}
if (kind === "param" || kind === "parameter") {
return Object.entries(state.hal.params ?? {})
.filter(([name]) => matches(name))
.map(([name, entry]) => ({
kind: "param",
name,
type: entry.type ?? "HAL_FLOAT",
value: normalizeHalNumericValue(entry.value),
}));
}
if (kind === "funct" || kind === "function") {
return (state.hal.functions ?? [])
.filter(({ name }) => matches(name))
.map((entry) => ({
kind: "function",
name: entry.name,
type: "HAL_FUNCT",
value: entry.thread ?? "",
}));
}
if (kind === "thread") {
return Object.entries(state.hal.threads ?? {})
.filter(([name]) => matches(name))
.map(([name, entry]) => ({
kind: "thread",
name,
type: "HAL_THREAD",
value: entry.running ? "running" : "stopped",
}));
}
return createVirtualHalWasmBridgeSnapshot(state).values
.filter(({ kind: rowKind, name }) => rowKind === "pin" && matches(name))
.map((row) => ({
kind: "pin",
name: row.name,
type: row.type,
value: normalizeHalNumericValue(row.value),
}));
}
function resolveAvailableVirtualHalFunctions(options, requiredFunctions) {
if (Array.isArray(options.availableFunctions)) {
return options.availableFunctions.filter((name) => requiredFunctions.includes(name));
}
if (Array.isArray(options.availableWasmFunctions)) {
return options.availableWasmFunctions.filter((name) => requiredFunctions.includes(name));
}
const hasWasmFunction = options.hasWasmFunction ?? options.interpSdk?.hasWasmFunction;
if (typeof hasWasmFunction === "function") {
return requiredFunctions.filter((name) => hasWasmFunction.call(options.interpSdk, name));
}
return [];
}
function expandVirtualHalFamilyPins(family) {
return (family.pins ?? []).flatMap((pin) => {
if (pin.axes) {
return VIRTUAL_HAL_AXES.map((axis) => ({
...pin,
name: pin.name.replace("{axis}", axis),
}));
}
if (pin.joints) {
return VIRTUAL_HAL_AXES.map((axis, index) => ({
...pin,
axis,
name: pin.name.replace("{joint}", `${index}`),
}));
}
return [{ ...pin }];
});
}
function isInternalAxisUiPinName(name) {
return name.startsWith("jog.") ||
name.startsWith("notifications-") ||
name === "resume-inhibit" ||
name === "error" ||
name === "abort" ||
/^axis\.[xyzabcuvw]\.homed$/.test(name);
}
function resolveVirtualHalCoverageForPin(name) {
const family = VIRTUAL_HAL_SYSTEM_PIN_FAMILIES.find((item) => (
expandVirtualHalFamilyPins(item).some((pin) => pin.name === name) ||
(name.startsWith("jog.") && item.id === "axisui")
));
return family?.coverage ?? VIRTUAL_HAL_COVERAGE_STATES.bridgeOnly;
}
function resolveVirtualHalSourceFilesForPin(name) {
const family = VIRTUAL_HAL_SYSTEM_PIN_FAMILIES.find((item) => (
expandVirtualHalFamilyPins(item).some((pin) => pin.name === name) ||
(name.startsWith("jog.") && item.id === "axisui")
));
return family ? [...family.sourceFiles] : [];
}
function isVirtualHalPinWritable(name) {
if (name.startsWith("jog.") || name.startsWith("axisui.jog.")) {
return true;
}
if (name.startsWith("halui.") || name.startsWith("iocontrol.") || name.startsWith("motion.")) {
return true;
}
if (/^(axis|joint)\./.test(name) || name.startsWith("spindle.")) {
return true;
}
return false;
}
function inferVirtualHalPinType(value) {
return typeof value === "boolean" ? "HAL_BIT" : "HAL_FLOAT";
}
function resolveVirtualHalDeclaredPinValue(state, name, type) {
const existing = readVirtualHalPinFromSnapshot(state, name);
if (existing) {
return existing.value;
}
if (name === "halui.machine.on") {
return state.task.powered ? 1 : 0;
}
if (name === "halui.machine.off") {
return state.task.powered ? 0 : 1;
}
if (name === "halui.estop.activate") {
return state.task.estop ? 1 : 0;
}
if (name === "halui.estop.reset") {
return state.task.estop ? 0 : 1;
}
if (name === "halui.mode.manual") {
return state.task.mode === "manual" ? 1 : 0;
}
if (name === "halui.mode.auto") {
return state.task.mode === "auto" ? 1 : 0;
}
if (name === "halui.mode.mdi") {
return state.task.mode === "mdi" ? 1 : 0;
}
if (name === "halui.program.run") {
return state.program.state === "running" ? 1 : 0;
}
if (name === "halui.program.pause") {
return state.program.state === "paused" ? 1 : 0;
}
if (name === "halui.program.resume") {
return state.program.state === "running" ? 1 : 0;
}
if (name === "halui.program.stop") {
return state.program.state === "idle" ? 1 : 0;
}
if (name.endsWith(".optional-stop.on")) {
return state.program.optionalStop ? 1 : 0;
}
if (name.endsWith(".optional-stop.off")) {
return state.program.optionalStop ? 0 : 1;
}
if (name.endsWith(".block-delete.on")) {
return state.program.blockDelete ? 1 : 0;
}
if (name.endsWith(".block-delete.off")) {
return state.program.blockDelete ? 0 : 1;
}
if (name === "halui.mist.on") {
return state.coolant.mist === "on" ? 1 : 0;
}
if (name === "halui.mist.off") {
return state.coolant.mist === "on" ? 0 : 1;
}
if (name === "halui.flood.on") {
return state.coolant.flood === "on" ? 1 : 0;
}
if (name === "halui.flood.off") {
return state.coolant.flood === "on" ? 0 : 1;
}
if (name === "halui.spindle.0.start") {
return state.spindle.state === "on" ? 1 : 0;
}
if (name === "halui.spindle.0.stop") {
return state.spindle.state === "on" ? 0 : 1;
}
if (name === "halui.spindle.0.forward") {
return state.spindle.direction === "cw" ? 1 : 0;
}
if (name === "halui.spindle.0.reverse") {
return state.spindle.direction === "ccw" ? 1 : 0;
}
if (name === "halui.spindle.0.brake-on") {
return state.spindle.brake ? 1 : 0;
}
if (name === "halui.spindle.0.brake-off") {
return state.spindle.brake ? 0 : 1;
}
const axisMatch = name.match(/^halui\.axis\.([xyzabcuvw])\.(select|plus|minus|analog|increment)$/);
if (axisMatch) {
if (axisMatch[2] === "select") {
return state.activeAxis === axisMatch[1] ? 1 : 0;
}
if (axisMatch[2] === "increment") {
return state.jog.incrementValue;
}
return state.pins[`jog.${axisMatch[1]}`] ? 1 : 0;
}
const jointMatch = name.match(/^halui\.joint\.(\d+)\.(home|unhome|plus|minus)$/);
if (jointMatch) {
const axis = VIRTUAL_HAL_AXES[Number(jointMatch[1])];
if (jointMatch[2] === "home") {
return axis && state.homed[axis] ? 1 : 0;
}
if (jointMatch[2] === "unhome") {
return axis && state.homed[axis] ? 0 : 1;
}
return 0;
}
return type === "HAL_FLOAT" ? 0 : 0;
}
function readVirtualHalPinFromSnapshot(state, name) {
return createVirtualHalWasmBridgeSnapshot(state).values.find((value) => value.name === name) ?? null;
}
function applyAxisPinWrite(state, name, numeric, enabled) {
const axisMatch = name.match(/^(axis|halui\.axis)\.([xyzabcuvw])\.(.+)$/);
if (axisMatch) {
const axis = axisMatch[2];
const suffix = axisMatch[3];
if (["pos-cmd", "teleop-pos-cmd", "pos-commanded", "pos-feedback", "pos-relative"].includes(suffix)) {
state.position[axis] = numeric;
state.positionDirty = true;
return true;
}
if (suffix === "is-selected" || suffix === "select") {
if (enabled) {
state.activeAxis = axis;
}
return true;
}
if (suffix === "jog-enable" || suffix === "plus" || suffix === "minus") {
state.pins[`jog.${axis}`] = enabled;
return true;
}
if (suffix === "jog-scale" || suffix === "increment") {
state.jog.increment = `${numeric}`;
state.jog.incrementValue = numeric;
return true;
}
if (suffix === "eoffset" || suffix === "eoffset-request") {
state.position[axis] += numeric;
state.positionDirty = true;
return true;
}
}
const jointMatch = name.match(/^joint\.(\d+)\.(.+)$/);
if (jointMatch) {
const index = Number(jointMatch[1]);
const axis = VIRTUAL_HAL_AXES[index];
const suffix = jointMatch[2];
if (!axis) {
return false;
}
if (["pos-cmd", "pos-fb", "coarse-pos-cmd", "motor-pos-cmd", "motor-pos-fb"].includes(suffix)) {
state.position[axis] = numeric;
state.positionDirty = true;
return true;
}
if (suffix === "homed" || suffix === "home-sw-in") {
state.homed[axis] = enabled;
return true;
}
if (suffix === "pos-lim-sw-in" || suffix === "pos-hard-limit") {
state.limits[axis].maxLimit = enabled;
return true;
}
if (suffix === "neg-lim-sw-in" || suffix === "neg-hard-limit") {
state.limits[axis].minLimit = enabled;
return true;
}
if (suffix === "faulted" || suffix === "error" || suffix === "amp-fault-in") {
state.limits[axis].fault = enabled ? suffix : "none";
return true;
}
}
const toolOffsetMatch = name.match(/^(motion\.tooloffset|halui\.tool\.length_offset)\.([xyzabcuvw])$/);
if (toolOffsetMatch) {
state.tool.lengthOffset[toolOffsetMatch[2]] = numeric;
return true;
}
return false;
}
function classifyVirtualHalPinGroup(name) {
if (name.startsWith("axisui.") || name.startsWith("jog.") || name.startsWith("notifications-") || name === "error" || name === "abort") {
return "axisui";
}
if (name.startsWith("axis.") || name.startsWith("joint.") || name.startsWith("motion.")) {
return "axis-motion";
}
if (name.startsWith("spindle.") || name.startsWith("halui.spindle.") || name.includes("coolant") || name.startsWith("halui.mist.") || name.startsWith("halui.flood.")) {
return "spindle-coolant";
}
if (name.includes(".tool") || name.startsWith("motion.tooloffset.")) {
return "tool";
}
return "task";
}
function formatAxis(value) {
return Number.isFinite(value) ? Number(value).toFixed(3) : "0.000";
}