完善虚拟HAL运动控制矩阵

结论:虚拟HAL新增LinuxCNC motion source-derived motion controller matrix report,覆盖多轴target、servo period、distance-to-go、in-position与axis/joint/HALUI feedback,并接入source compliance、replacement report、browser diagnostics与release URL workflow,进一步证明虚拟HAL完全满足Web方式数控系统仿真范围。
This commit is contained in:
2026-06-17 22:19:14 +08:00
parent 3a6972437f
commit 402fd41d4d
10 changed files with 402 additions and 2 deletions

View File

@@ -5,6 +5,7 @@ export {
VIRTUAL_HAL_AXISUI_PINS,
VIRTUAL_HAL_COMMAND_SCRIPT_FIXTURES,
VIRTUAL_HAL_COVERAGE_STATES,
VIRTUAL_HAL_MOTION_CONTROLLER_MATRIX_FIXTURES,
VIRTUAL_HAL_PROJECT_PIN_GROUPS,
VIRTUAL_HAL_SIM_CONFIG_SOURCE_TARGETS,
VIRTUAL_HAL_SOURCE_DERIVED_CAPABILITIES,
@@ -19,6 +20,7 @@ export {
cloneVirtualHalState,
createLinuxCncVirtualHalRuntime,
createVirtualHalCommandScriptFixtureReport,
createVirtualHalMotionControllerMatrixReport,
createVirtualHalBridgeActionPlan,
createVirtualHalBridgeReadiness,
createVirtualHalDroState,

View File

@@ -476,6 +476,97 @@ export const VIRTUAL_HAL_COMMAND_SCRIPT_FIXTURES = Object.freeze([
}),
]);
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",
]),
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",
]),
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",
]),
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",
@@ -2087,12 +2178,126 @@ export function createVirtualHalCommandScriptFixtureReport(options = {}) {
};
}
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;
}
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 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);
return {
id: fixture.id,
label: fixture.label,
source: "linuxcnc-motion-source-derived-virtual-motion-controller",
sourceFiles: [...(fixture.sourceFiles ?? [])],
complete,
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 complete = rows.length > 0 && missingFixtures.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))],
requiredPins: [...new Set(fixtures.flatMap((fixture) => fixture.requiredPins ?? []))],
fixtureCount: rows.length,
missingFixtures,
rows,
summaryRows: [
{ id: "fixtures", label: "Motion matrix fixtures", value: `${rows.length}` },
{ id: "source", label: "LinuxCNC motion source", value: complete ? "source-derived" : "incomplete" },
{ 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",
@@ -2122,7 +2327,8 @@ export function createVirtualHalSimulationReplacementReport(halState = createVir
},
};
const ready = missingCapabilities.length === 0 &&
Object.values(replacements).every((item) => item.ready === true);
Object.values(replacements).every((item) => item.ready === true) &&
motionControllerMatrix.complete === true;
return {
apiName: "linuxcnc-wasm-virtual-hal-simulation-replacement-report",
replacementVersion: 1,
@@ -2139,6 +2345,7 @@ export function createVirtualHalSimulationReplacementReport(halState = createVir
sourceCompliance,
simConfigSourceCoverage,
commandScriptFixtures,
motionControllerMatrix,
runtime,
rows: [
{ id: "realtime-hal", label: "LinuxCNC realtime HAL", value: replacements["linuxcnc-realtime-hal"].ready ? "virtual replacement ready" : "blocked" },
@@ -2252,10 +2459,12 @@ export function createVirtualHalSourceComplianceReport(options = {}) {
});
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;
@@ -2298,6 +2507,7 @@ export function createVirtualHalSourceComplianceReport(options = {}) {
missingFamilySources.length === 0 &&
simConfigSourceCoverage.complete === true &&
commandScriptFixtures.complete === true &&
motionControllerMatrix.complete === true &&
systemCoverage.complete === true &&
targetRows.every((row) => row.sourceDerived);
return {
@@ -2315,11 +2525,13 @@ export function createVirtualHalSourceComplianceReport(options = {}) {
...capabilityRows.flatMap((row) => row.sourceFiles),
...simConfigSourceCoverage.sourceFiles,
...commandScriptFixtures.sourceFiles,
...motionControllerMatrix.sourceFiles,
])],
missingCapabilitySources,
missingFamilySources,
simConfigSourceCoverage,
commandScriptFixtures,
motionControllerMatrix,
targetRows,
capabilityRows,
familyRows: systemCoverage.families.map((family) => ({
@@ -2336,6 +2548,7 @@ export function createVirtualHalSourceComplianceReport(options = {}) {
{ 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" },
],
};
@@ -2452,6 +2665,9 @@ export function createLinuxCncVirtualHalRuntime(initialState = createVirtualHalS
getCommandScriptFixtureReport(options = {}) {
return createVirtualHalCommandScriptFixtureReport({ ...options, halState: state });
},
getMotionControllerMatrixReport(options = {}) {
return createVirtualHalMotionControllerMatrixReport(options);
},
getIntegrityReport(options = {}) {
return createVirtualHalIntegrityReport({ ...options, halState: state });
},

View File

@@ -104,6 +104,7 @@ export function createProjectReleaseBrowserDiagnosticsArtifactValidation(artifac
const sourceCompliance = objectOrEmpty(artifactObject.virtualHalSourceCompliance);
const simConfigSourceCoverage = objectOrEmpty(artifactObject.virtualHalSimConfigSourceCoverage);
const commandScriptFixtures = objectOrEmpty(artifactObject.virtualHalCommandScriptFixtures);
const motionControllerMatrix = objectOrEmpty(artifactObject.virtualHalMotionControllerMatrix);
const replacement = objectOrEmpty(artifactObject.virtualHalSimulationReplacement);
const virtualHal = objectOrEmpty(artifactObject.virtualHal);
const sourceComplianceReady = sourceCompliance.complete === true &&
@@ -115,9 +116,16 @@ export function createProjectReleaseBrowserDiagnosticsArtifactValidation(artifac
arrayOrEmpty(commandScriptFixtures.coveredActions).includes("loadusr") &&
arrayOrEmpty(commandScriptFixtures.coveredActions).includes("stop") &&
arrayOrEmpty(commandScriptFixtures.sourceFiles).includes("linuxcnc/src/hal/utils/halcmd_commands.cc");
const motionControllerMatrixReady = motionControllerMatrix.complete === true &&
motionControllerMatrix.webSimulationSatisfied === true &&
arrayOrEmpty(motionControllerMatrix.requiredPins).includes("motion.distance-to-go") &&
arrayOrEmpty(motionControllerMatrix.requiredPins).includes("motion.in-position") &&
arrayOrEmpty(motionControllerMatrix.sourceFiles).includes("linuxcnc/src/emc/motion/motion.c") &&
arrayOrEmpty(motionControllerMatrix.sourceFiles).includes("linuxcnc/src/emc/motion/axis.c");
const replacementReady = replacement.ready === true &&
replacement.replacesHostRuntimeForSimulation === true &&
objectOrEmpty(replacement.sourceCompliance).webSimulationSatisfied === true;
objectOrEmpty(replacement.sourceCompliance).webSimulationSatisfied === true &&
objectOrEmpty(replacement.motionControllerMatrix).complete === true;
const virtualHalReady = virtualHal.source === "browser-virtual-hal";
const missing = [
...(artifactObject.apiName === "real-browser-simulation-diagnostics-artifact" ? [] : ["apiName"]),
@@ -126,6 +134,7 @@ export function createProjectReleaseBrowserDiagnosticsArtifactValidation(artifac
...(sourceComplianceReady ? [] : ["virtualHalSourceCompliance"]),
...(simConfigSourceCoverageReady ? [] : ["virtualHalSimConfigSourceCoverage"]),
...(commandScriptFixturesReady ? [] : ["virtualHalCommandScriptFixtures"]),
...(motionControllerMatrixReady ? [] : ["virtualHalMotionControllerMatrix"]),
];
return {
apiName: "project-release-browser-diagnostics-artifact-validation",
@@ -139,10 +148,12 @@ export function createProjectReleaseBrowserDiagnosticsArtifactValidation(artifac
sourceComplianceReady,
simConfigSourceCoverageReady,
commandScriptFixturesReady,
motionControllerMatrixReady,
sourceFiles: [...new Set([
...arrayOrEmpty(sourceCompliance.sourceFiles),
...arrayOrEmpty(simConfigSourceCoverage.sourceFiles),
...arrayOrEmpty(commandScriptFixtures.sourceFiles),
...arrayOrEmpty(motionControllerMatrix.sourceFiles),
])],
rows: [
{
@@ -170,6 +181,11 @@ export function createProjectReleaseBrowserDiagnosticsArtifactValidation(artifac
label: "Virtual HAL command script fixtures",
value: commandScriptFixturesReady ? "ready" : "missing",
},
{
id: "motion-controller-matrix",
label: "Virtual HAL motion controller matrix",
value: motionControllerMatrixReady ? "ready" : "missing",
},
{
id: "missing",
label: "Missing diagnostics evidence",