完善虚拟HAL仿真替代能力

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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