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