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"; }