完善 gmoccapy XYZAB 参考功能

This commit is contained in:
2026-06-26 17:53:45 -04:00
parent 4e4f1462f5
commit a81b40447d
154 changed files with 17194 additions and 51 deletions

View File

@@ -0,0 +1,75 @@
import assert from "node:assert/strict";
import {
createGmoccapyCommunicationSummary,
gmoccapyCommunicationModel,
} from "../../app/src/runtime/gmoccapy-communication-model.js";
const model = gmoccapyCommunicationModel;
assert.equal(model.apiName, "web-rtcp-5axis-gmoccapy-communication-model");
assert.equal(model.profileId, "gmoccapy-xyzab");
assert.equal(model.nativeConfigPath.endsWith("gmoccapy_XYZAB.ini"), true);
assert.equal(model.cycleTimeMs, 100);
assert.equal(model.semanticBoundary, "native_gmoccapy_nml_hal_reference_web_store_runtime_mapping");
assert.deepEqual(model.nativePaths.command.path, ["gmoccapy", "linuxcnc.command()", "NML emcCommand", "milltask"]);
assert.equal(model.nativePaths.status.path.includes("linuxcnc.stat()"), true);
assert.equal(model.nativePaths.status.path.includes("linuxcnc.error_channel()"), true);
assert.equal(model.nativePaths.hal.path.includes("HAL shared memory"), true);
assert.equal(model.nativePaths.remap.path.includes("Python remap prolog/ngc/epilog"), true);
assert.deepEqual(model.webPath.path, ["button", "store.dispatch", "linuxcnc-task-policy", "runtime/status snapshot", "UI/Three.js"]);
assert.equal(model.startupStages.length, 9);
assert.equal(model.startupStages[0].id, "parse-ini");
assert.equal(model.startupStages.at(-1).id, "postgui");
assert.equal(model.startupStages.at(-1).nativeStep.includes("halcomp.ready()"), true);
const actionIds = model.actionMappings.map((mapping) => mapping.actionId);
for (const actionId of [
"estop",
"estop-reset",
"power-on",
"mode-manual",
"mode-mdi",
"mode-auto",
"jog",
"home",
"mdi",
"auto-run",
"auto-pause",
"auto-resume",
"abort",
"spindle",
"coolant",
]) {
assert.equal(actionIds.includes(actionId), true, `${actionId} missing`);
}
const runMapping = model.actionMappings.find((mapping) => mapping.actionId === "auto-run");
assert.equal(runMapping.nativeCommand, "command.auto(AUTO_RUN, start_line)");
assert.equal(runMapping.webAction, "RUN");
assert.equal(runMapping.gate.includes("INI loaded"), true);
assert.equal(runMapping.gate.includes("runtime ready"), true);
const jogMapping = model.actionMappings.find((mapping) => mapping.actionId === "jog");
assert.equal(jogMapping.nativeCommand.includes("JOG_CONTINUOUS"), true);
assert.equal(jogMapping.gate, "machine on, manual mode, not running");
const spindleMapping = model.actionMappings.find((mapping) => mapping.actionId === "spindle");
assert.equal(spindleMapping.nativeStatus.includes("spindle.0.forward"), true);
assert.equal(spindleMapping.gate.includes("restricted during interpreter"), true);
const summary = createGmoccapyCommunicationSummary(model);
assert.equal(summary.apiName, "web-rtcp-5axis-gmoccapy-communication-model-summary");
assert.equal(summary.profileId, "gmoccapy-xyzab");
assert.equal(summary.nativePathCount, 4);
assert.equal(summary.startupStageCount, 9);
assert.equal(summary.actionCount, 15);
assert.equal(summary.postguiAfterHalcompReady, true);
assert.equal(summary.hasNativeNmlCommandPath, true);
assert.equal(summary.hasNativeHalPath, true);
assert.equal(summary.hasWebStorePath, true);
assert.equal(summary.semanticBoundary, model.semanticBoundary);
console.log("gmoccapy_communication_model_smoke=ok");

View File

@@ -0,0 +1,72 @@
import assert from "node:assert/strict";
import {
createGmoccapyHalSummary,
gmoccapyHalModel,
} from "../../app/src/runtime/gmoccapy-hal-model.js";
const model = gmoccapyHalModel;
assert.equal(model.apiName, "web-rtcp-5axis-gmoccapy-hal-model");
assert.equal(model.profileId, "gmoccapy-xyzab");
assert.equal(model.nativeConfigPath.endsWith("gmoccapy_XYZAB.ini"), true);
assert.equal(model.semanticBoundary, "gmoccapy_hal_pin_postgui_reference_web_diagnostic_only");
assert.equal(model.halFiles.length, 4);
assert.equal(model.halFiles.some((file) => file.path.endsWith("core_sim_XYZAB.hal") && file.stage === "HALFILE"), true);
assert.equal(model.halFiles.some((file) => file.path.endsWith("spindle_sim.hal") && file.stage === "HALFILE"), true);
assert.equal(model.halFiles.some((file) => file.path.endsWith("simulated_home.hal") && file.stage === "HALFILE"), true);
assert.equal(model.halFiles.some((file) => file.path.endsWith("gmoccapy_postgui.hal") && file.requiresHalcompReady), true);
const groups = new Map(model.nativePins.map((group) => [group.group, group]));
for (const groupName of [
"hard-buttons",
"jog",
"override",
"tool",
"program",
"message",
"spindle-postgui",
]) {
assert.equal(groups.has(groupName), true, `${groupName} missing`);
}
assert.equal(groups.get("hard-buttons").pins.includes("gmoccapy.h-button.button-0"), true);
assert.equal(groups.get("hard-buttons").pins.includes("gmoccapy.v-button.button-6"), true);
assert.equal(groups.get("jog").pins.includes("gmoccapy.jog.axis.jog-b-minus"), true);
assert.equal(groups.get("override").pins.includes("gmoccapy.rapid.rapid-override.direct-value"), true);
assert.equal(groups.get("tool").pins.includes("gmoccapy.toolchange-changed"), true);
assert.equal(groups.get("program").pins.includes("gmoccapy.program.current-line"), true);
assert.equal(groups.get("message").pins.includes("gmoccapy.error"), true);
assert.equal(groups.get("spindle-postgui").pins.includes("gmoccapy.spindle_at_speed_led"), true);
assert.equal(model.coreNets.length, 10);
assert.equal(model.coreNets.some((net) => net.signal === "Xpos" && net.target === "joint.0.motor-pos-fb"), true);
assert.equal(model.coreNets.some((net) => net.signal === "Bpos" && net.target === "joint.4.motor-pos-fb"), true);
assert.equal(model.coreNets.some((net) => net.signal === "estop-loop"), true);
assert.equal(model.coreNets.some((net) => net.signal === "tool-change-loop"), true);
assert.equal(model.spindleNets.length, 4);
assert.equal(model.spindleNets.some((net) => net.signal === "spindle-speed-cmd"), true);
assert.equal(model.spindleNets.some((net) => net.signal === "spindle-at-speed"), true);
assert.equal(model.postguiNets.length, 5);
assert.equal(model.postguiNets.every((net) => net.requiresHalcompReady), true);
assert.equal(model.postguiNets.some((net) => net.target === "gmoccapy.spindle_feedback_bar"), true);
assert.equal(model.postguiNets.some((net) => net.target === "gmoccapy.tooloffset-x"), true);
assert.equal(model.postguiNets.some((net) => net.simulationLoop && net.signal === "tool-change-loop"), true);
const summary = createGmoccapyHalSummary(model);
assert.equal(summary.apiName, "web-rtcp-5axis-gmoccapy-hal-model-summary");
assert.equal(summary.profileId, "gmoccapy-xyzab");
assert.equal(summary.halFileCount, 4);
assert.equal(summary.nativePinGroupCount, 7);
assert.equal(summary.nativePinCount >= 50, true);
assert.equal(summary.coreNetCount, 10);
assert.equal(summary.spindleNetCount, 4);
assert.equal(summary.postguiNetCount, 5);
assert.equal(summary.postguiRequiresHalcompReady, true);
assert.equal(summary.hasToolChangeSimulationLoop, true);
assert.equal(summary.semanticBoundary, model.semanticBoundary);
console.log("gmoccapy_hal_model_smoke=ok");

View File

@@ -0,0 +1,84 @@
import assert from "node:assert/strict";
import { access, readFile } from "node:fs/promises";
import { join } from "node:path";
const repoRoot = new URL("../../..", import.meta.url).pathname.replace(/\/$/, "");
const manifestPath = join(repoRoot, "web-rtcp-5axis-sim-plan/app/src/ui-reference/gmoccapy-button-icons.json");
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
assert.equal(manifest.schema, "web-rtcp-5axis-gmoccapy-button-icons-v1");
assert.equal(manifest.semanticBoundary, "gmoccapy_button_icon_asset_reference_only_not_control_semantics");
assert.equal(manifest.generatedFrom.csv, "work/working3/gmoccapy_button_icons/button_icon_inventory.csv");
assert.equal(manifest.rowCount, 211);
assert.equal(manifest.entries.length, 211);
assert.equal(manifest.copiedIconCount, 112);
const entriesByButton = new Map();
for (const entry of manifest.entries) {
if (!entriesByButton.has(entry.buttonId)) entriesByButton.set(entry.buttonId, []);
entriesByButton.get(entry.buttonId).push(entry);
if (entry.projectAsset) {
await access(join(repoRoot, "web-rtcp-5axis-sim-plan", entry.projectAsset.replace(/^app\//, "app/")));
}
}
const coreButtons = [
"tbtn_estop",
"tbtn_on",
"rbt_manual",
"rbt_mdi",
"rbt_auto",
"tbtn_setup",
"tbtn_user_tabs",
"btn_homing",
"btn_tool",
"btn_touch",
"btn_load",
"btn_reload",
"btn_run",
"btn_stop",
"tbtn_pause",
"btn_step",
"ref_all",
"home_axis_x",
"home_axis_y",
"home_axis_z",
"home_axis_a",
"home_axis_b",
"touch_x",
"touch_y",
"touch_z",
"touch_a",
"touch_b",
"rbt_forward",
"rbt_reverse",
"rbt_stop",
"tbtn_flood",
"tbtn_mist",
"btn_zoom_in",
"btn_zoom_out",
"rbt_view_x",
"rbt_view_y",
"rbt_view_z",
"rbt_view_p",
"tbtn_view_tool_path",
"tbtn_view_dimension",
];
for (const buttonId of coreButtons) {
const entries = entriesByButton.get(buttonId) || [];
assert.equal(entries.length > 0, true, `${buttonId} missing from manifest`);
assert.equal(entries.some((entry) => entry.projectAsset || entry.sourceType === "none"), true, `${buttonId} lacks asset/fallback`);
}
assert.equal(entriesByButton.get("rbt_auto").some((entry) => entry.stateOrVariant === "active" && entry.iconName === "mode_auto_active"), true);
assert.equal(entriesByButton.get("rbt_auto").some((entry) => entry.iconName === "mode_auto_inactive"), true);
assert.equal(entriesByButton.get("tbtn_estop").some((entry) => entry.iconName === "main_switch_on"), true);
assert.equal(entriesByButton.get("tbtn_estop").some((entry) => entry.iconName === "main_switch_off"), true);
assert.equal(entriesByButton.get("tbtn_flood").some((entry) => entry.iconName === "coolant_flood_active"), true);
assert.equal(entriesByButton.get("tbtn_mist").some((entry) => entry.iconName === "coolant_mist_inactive"), true);
const copiedAssets = new Set(manifest.entries.map((entry) => entry.projectAsset).filter(Boolean));
assert.equal(copiedAssets.size, 112);
console.log("gmoccapy_icon_manifest_smoke=ok");

View File

@@ -0,0 +1,63 @@
import assert from "node:assert/strict";
import { access, readFile } from "node:fs/promises";
import { join } from "node:path";
import {
GMOCAPY_ICON_REGISTRY_BOUNDARY,
getGmoccapyIcon,
listGmoccapyIconRegistryEntries,
} from "../../app/src/ui/gmoccapy-icon-registry.js";
const repoRoot = new URL("../../..", import.meta.url).pathname.replace(/\/$/, "");
const manifestPath = join(repoRoot, "web-rtcp-5axis-sim-plan/app/src/ui-reference/gmoccapy-button-icons.json");
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
const manifestIconNames = new Set(manifest.entries.map((entry) => entry.iconName).filter(Boolean));
const registryEntries = listGmoccapyIconRegistryEntries();
assert.equal(GMOCAPY_ICON_REGISTRY_BOUNDARY, "gmoccapy_icon_registry_from_working3_manifest_core_ui_subset");
assert.equal(registryEntries.length >= 40, true);
const required = [
["tbtn_estop", "active", "main_switch_on"],
["tbtn_estop", "inactive", "main_switch_off"],
["tbtn_on", "active", "power_on"],
["rbt_manual", "active", "mode_manual_active"],
["rbt_mdi", "active", "mode_mdi_active"],
["rbt_auto", "active", "mode_auto_active"],
["btn_load", "default", "open_file"],
["btn_reload", "default", "refresh"],
["btn_run", "default", "play"],
["btn_stop", "default", "stop"],
["tbtn_pause", "active", "pause_active"],
["btn_step", "default", "step"],
["btn_homing", "default", "ref_all"],
["btn_tool", "default", "hsk_mill_tool"],
["btn_touch", "default", "touch_off"],
["rbt_forward", "active", "spindle_right_on"],
["rbt_reverse", "active", "spindle_left_on"],
["rbt_stop", "active", "spindle_stop_on"],
["tbtn_flood", "active", "coolant_flood_active"],
["tbtn_mist", "inactive", "coolant_mist_inactive"],
["rbt_view_x", "default", "tool_axis_x"],
["rbt_view_y", "default", "tool_axis_y"],
["rbt_view_z", "default", "tool_axis_z"],
["rbt_view_p", "default", "tool_axis_p"],
["tbtn_view_tool_path", "default", "toolpath"],
["tbtn_view_dimension", "default", "dimensions"],
];
for (const [buttonId, variant, expectedIconName] of required) {
const icon = getGmoccapyIcon(buttonId, variant);
assert.equal(icon.iconName, expectedIconName, `${buttonId}/${variant}`);
assert.equal(icon.fallback, false, `${buttonId}/${variant} should use an asset`);
assert.equal(icon.semanticBoundary, GMOCAPY_ICON_REGISTRY_BOUNDARY);
assert.equal(manifestIconNames.has(icon.iconName), true, `${icon.iconName} missing from manifest`);
assert.match(icon.path, /\/assets\/gmoccapy-icons\//);
await access(new URL(icon.path));
}
const fallback = getGmoccapyIcon("unknown_button", "active");
assert.equal(fallback.iconName, "text_fallback");
assert.equal(fallback.fallback, true);
console.log("gmoccapy_icon_registry_smoke=ok");

View File

@@ -0,0 +1,180 @@
import assert from "node:assert/strict";
import { getFiveAxisProfile } from "../../app/src/profiles/index.js";
import { createSimulationStore, validateRunPreconditions } from "../../app/src/state/store.js";
import { gateLinuxCncTaskAction } from "../../app/src/state/linuxcnc-task-policy.js";
const profile = getFiveAxisProfile("gmoccapy-xyzab");
const store = createSimulationStore();
store.dispatch({ type: "SET_PROFILE", profileId: "gmoccapy-xyzab" });
assert.equal(store.getState().profile.id, profile.id);
assert.equal(store.getState().machine.noForceHoming, false);
let gate = gateLinuxCncTaskAction(store.getState(), { type: "RUN" });
assert.equal(gate.allowed, false);
assert.equal(gate.operatorMessage, "run blocked: machine must be on");
store.dispatch({ type: "TOGGLE_POWER" });
gate = gateLinuxCncTaskAction(store.getState(), { type: "RUN" });
assert.equal(gate.allowed, false);
assert.equal(gate.operatorMessage, "run blocked: switch to auto mode first");
store.dispatch({ type: "SET_MODE", mode: "auto" });
gate = gateLinuxCncTaskAction(store.getState(), { type: "RUN" });
assert.equal(gate.allowed, false);
assert.equal(gate.operatorMessage, "run blocked: home machine first");
store.dispatch({ type: "SET_MODE", mode: "manual" });
store.dispatch({ type: "HOME" });
store.dispatch({ type: "SET_MODE", mode: "auto" });
gate = gateLinuxCncTaskAction(store.getState(), { type: "RUN" });
assert.equal(gate.allowed, false);
assert.equal(gate.operatorMessage, "run blocked: LinuxCNC INI not loaded");
store.dispatch({
type: "ATTACH_INI_CONFIG",
profileId: "gmoccapy-xyzab",
iniConfig: {
profileId: "gmoccapy-xyzab",
path: profile.iniPath,
sourceText: "[TRAJ]\nCOORDINATES = X Y Z A B\nNO_FORCE_HOMING = 0\n[KINS]\nKINEMATICS = trivkins coordinates=xyzab\n",
validation: { ready: true },
traj: { coordinates: "XYZAB", noForceHoming: false },
kinematics: { name: "trivkins", sparm: "coordinates=xyzab" },
kinematicsParameters: profile.kinematicsParameters,
kinematicsModuleId: profile.kinematicsModuleId,
axisLimits: profile.axisLimits,
jointConfig: profile.jointConfig,
display: profile.display,
rs274ngc: profile.rs274ngc,
hal: {
...profile.hal,
halcmd: profile.hal.halcmd,
initialSets: [],
},
halui: profile.halui,
emcmot: { servoPeriodNs: 1000000 },
emcio: {},
task: { cycleTimeSeconds: 0.001 },
},
});
gate = gateLinuxCncTaskAction(store.getState(), { type: "RUN" });
assert.equal(gate.allowed, false);
assert.equal(gate.operatorMessage, "run blocked: LinuxCNC machine files not staged");
store.dispatch({
type: "MACHINE_FILE_STAGING_COMPLETE",
plan: {
profileId: "gmoccapy-xyzab",
selectedProgramSourceRel: null,
},
save: {
fileCount: 2,
files: [
{
sourceRel: "configs/sim/gmoccapy/gmoccapy_XYZAB.ini",
wasmPath: "/work/sim/gmoccapy/gmoccapy_XYZAB.ini",
kind: "ini",
text: "",
bytes: 0,
},
],
summary: { gcodeFileCount: 0 },
},
});
gate = gateLinuxCncTaskAction(store.getState(), { type: "RUN" });
assert.equal(gate.allowed, false);
assert.equal(gate.operatorMessage, "run blocked: no machine-file G-code opened for task/HAL session");
store.dispatch({
type: "MACHINE_FILE_STAGING_COMPLETE",
plan: {
profileId: "gmoccapy-xyzab",
selectedProgramSourceRel: "configs/sim/gmoccapy/demos/xyzab-demo.ngc",
},
save: {
fileCount: 2,
files: [
{
sourceRel: "configs/sim/gmoccapy/demos/xyzab-demo.ngc",
wasmPath: "/work/sim/gmoccapy/demos/xyzab-demo.ngc",
kind: "demo",
text: "G0 X0\nM2\n",
bytes: 9,
},
],
summary: { gcodeFileCount: 1 },
},
selectedGcodeSourceRel: "configs/sim/gmoccapy/demos/xyzab-demo.ngc",
});
gate = gateLinuxCncTaskAction(store.getState(), { type: "RUN" });
assert.equal(gate.allowed, false);
assert.equal(gate.operatorMessage, "run blocked: task/HAL runtime not ready");
store.dispatch({
type: "ATTACH_TASK_HAL_RUNTIME",
runtime: {
loaded: true,
readiness() {
return {
loaded: true,
taskRuntimeReady: true,
motionRuntimeReady: true,
halRuntimeReady: true,
};
},
},
});
gate = gateLinuxCncTaskAction(store.getState(), { type: "RUN" });
assert.equal(gate.allowed, true);
assert.equal(gate.status.canRunAutoStrict, true);
assert.equal(gate.status.profileId, "gmoccapy-xyzab");
const runPreconditions = validateRunPreconditions(store.getState(), {
requireTaskHalRuntime: false,
requireTaskHalSession: false,
});
assert.equal(runPreconditions.ok, false);
assert.equal(runPreconditions.operatorMessage, "run blocked: unsupported five-axis profile gmoccapy-xyzab");
const manualStore = createSimulationStore();
manualStore.dispatch({ type: "SET_PROFILE", profileId: "gmoccapy-xyzab" });
manualStore.dispatch({ type: "TOGGLE_POWER" });
gate = gateLinuxCncTaskAction(manualStore.getState(), { type: "JOG", axis: "x", direction: 1 });
assert.equal(gate.allowed, true);
gate = gateLinuxCncTaskAction(manualStore.getState(), { type: "HOME" });
assert.equal(gate.allowed, true);
manualStore.dispatch({ type: "HOME" });
manualStore.dispatch({ type: "SET_MODE", mode: "mdi" });
gate = gateLinuxCncTaskAction(manualStore.getState(), { type: "RUN_MDI" });
assert.equal(gate.allowed, true);
const controlStore = createSimulationStore();
controlStore.dispatch({ type: "SET_PROFILE", profileId: "gmoccapy-xyzab" });
controlStore.dispatch({ type: "ESTOP" });
gate = gateLinuxCncTaskAction(controlStore.getState(), { type: "TOGGLE_COOLANT", kind: "flood" });
assert.equal(gate.allowed, false);
assert.equal(gate.operatorMessage, "coolant blocked: machine must be on");
gate = gateLinuxCncTaskAction(controlStore.getState(), { type: "SET_SPINDLE_DIRECTION", direction: "forward" });
assert.equal(gate.allowed, false);
assert.equal(gate.operatorMessage, "spindle blocked: machine must be on");
controlStore.dispatch({ type: "RESET" });
controlStore.dispatch({ type: "TOGGLE_POWER" });
controlStore.dispatch({ type: "SET_SPINDLE_DIRECTION", direction: "forward" });
assert.equal(controlStore.getState().spindle.enabled, true);
assert.equal(controlStore.getState().spindle.direction, "forward");
controlStore.dispatch({ type: "SET_SPINDLE_DIRECTION", direction: "stop" });
assert.equal(controlStore.getState().spindle.enabled, false);
assert.equal(controlStore.getState().spindle.direction, "stop");
controlStore.dispatch({ type: "TOGGLE_COOLANT", kind: "flood" });
assert.equal(controlStore.getState().coolant.flood, true);
console.log("gmoccapy_xyzab_gates_smoke=ok");

View File

@@ -0,0 +1,116 @@
import assert from "node:assert/strict";
import { getFiveAxisProfile, fiveAxisProfiles } from "../../app/src/profiles/index.js";
import { buildRtcpFrame } from "../../app/src/runtime/rtcp-frame.js";
import { createLinuxCncBoundaryAdapter, createLinuxCncBoundaryReadiness } from "../../app/src/runtime/linuxcnc-boundary-adapter.js";
import { createProfileSourceReferenceSummary } from "../../app/src/profiles/source-reference-map.js";
import { createSimulationStore, validateRunPreconditions } from "../../app/src/state/store.js";
const profile = getFiveAxisProfile("gmoccapy-xyzab");
assert.equal(profile.id, "gmoccapy-xyzab");
assert.equal(profile.title, "gmoccapy XYZAB trivkins reference");
assert.equal(profile.iniPath, "linuxcnc/configs/sim/gmoccapy/gmoccapy_XYZAB.ini");
assert.deepEqual(profile.coordinates, ["X", "Y", "Z", "A", "B"]);
assert.equal(profile.traj.coordinates, "XYZAB");
assert.equal(profile.joints.length, 5);
assert.equal(profile.jointConfig.length, 5);
assert.equal(profile.kinematics, "trivkins coordinates=xyzab");
assert.equal(profile.kinematicsModuleId, "gmoccapy-xyzab-trivkins-reference");
assert.equal(profile.tcpCapable, false);
assert.equal(profile.rtcpProof, false);
assert.equal(profile.linuxCncKinematicsReady, false);
assert.equal(profile.promotionAllowed, false);
assert.equal(profile.semanticBoundary, "gmoccapy_xyzab_trivkins_reference_only_not_rtcp_proof");
assert.equal(profile.homing.noForceHoming, false);
assert.equal(profile.homing.noForceHomingIniValue, 0);
assert.equal(profile.hal.postguiRequiresHalcompReady, true);
assert.deepEqual(profile.hal.halFiles, ["core_sim_XYZAB.hal", "spindle_sim.hal", "simulated_home.hal"]);
assert.deepEqual(profile.hal.postguiHalFiles, ["gmoccapy_postgui.hal"]);
assert.equal(profile.nativeRuntime.task, "milltask");
assert.equal(profile.nativeRuntime.emcmot, "motmod");
assert.equal(profile.nativeRuntime.halui, "halui");
assert.equal(profile.nativeRuntime.displayCycleTimeMs, 100);
assert.equal(profile.nativeRuntime.servoPeriodNs, 1000000);
assert.equal(profile.axisLimits.X.max, 600);
assert.equal(profile.axisLimits.Y.max, 400);
assert.equal(profile.axisLimits.Z.min, -400);
assert.equal(profile.axisLimits.B.maxVelocity, 90);
assert.equal(profile.jointConfig[4].axis, "B");
assert.equal(profile.jointConfig[4].homeOffset, 45);
assert.equal(profile.hal.halcmd.feedbackNets.length, 5);
assert.equal(profile.hal.halcmd.offsetNets.some((net) => net.target === "gmoccapy.tooloffset-z"), true);
assert.equal(profile.hal.halcmd.simulationLoops.some((net) => net.signal === "tool-change-loop"), true);
assert.equal(profile.halPins.includes("gmoccapy.program.current-line"), true);
assert.equal(profile.halPins.includes("gmoccapy.spindle_at_speed_led"), true);
assert.equal(profile.toolTable.toolCount, 17);
assert.equal(profile.macros.includes("go_to_position X-pos Y-pos Z-pos"), true);
assert.equal(profile.panelSchema.id, "gmoccapy-xyzab-hal-component");
assert.equal(profile.panelSchema.boundary, "gmoccapy_hal_component_reference_only");
assert.equal(profile.panelSchema.groups.length, 2);
assert.deepEqual(fiveAxisProfiles.map(({ id }) => id), ["xyzac-trt", "xyzbc-trt", "gmoccapy-xyzab"]);
const sourceSummary = createProfileSourceReferenceSummary("gmoccapy-xyzab");
assert.equal(sourceSummary.profileId, "gmoccapy-xyzab");
assert.equal(sourceSummary.referenceCount >= 10, true);
assert.equal(sourceSummary.kinds.includes("ini"), true);
assert.equal(sourceSummary.kinds.includes("halfile"), true);
assert.equal(sourceSummary.kinds.includes("postgui_hal"), true);
assert.equal(sourceSummary.kinds.includes("icon_manifest"), true);
assert.equal(sourceSummary.kinds.includes("python_gui"), true);
assert.equal(sourceSummary.promotionAllowed, false);
assert.equal(sourceSummary.semanticBoundary, "profile_source_map_only_not_runtime_proof");
assert.ok(sourceSummary.references.some((reference) => reference.path.endsWith("gmoccapy_XYZAB.ini")));
assert.ok(sourceSummary.references.some((reference) => reference.path.endsWith("gmoccapy_postgui.hal")));
assert.ok(sourceSummary.references.some((reference) => reference.path.endsWith("gmoccapy-button-icons.json")));
const adapter = createLinuxCncBoundaryAdapter({ profile });
assert.equal(adapter.profileId, "gmoccapy-xyzab");
assert.equal(adapter.profileSummary.coordinates, "XYZAB");
assert.equal(adapter.profileSummary.jointCount, 5);
assert.equal(adapter.profileSummary.mdiCommandCount, 0);
assert.equal(adapter.profileSummary.remapCount, 2);
assert.equal(adapter.profileSummary.toolCount, 17);
assert.equal(adapter.profileSummary.feedbackNetCount, 5);
assert.equal(adapter.profileSummary.halFileCount, 4);
assert.equal(adapter.promotionAllowed, false);
assert.equal(adapter.linuxCncKinematicsReady, false);
const readiness = createLinuxCncBoundaryReadiness(adapter);
assert.equal(readiness.ready, false);
assert.equal(readiness.promotionAllowed, false);
assert.equal(readiness.missing.includes("kinematics runtime"), true);
const frame = buildRtcpFrame({
axisPose: { x: 10, y: 20, z: -5, a: 30, b: 45, c: 99 },
activeLine: 10,
profile,
});
assert.equal(frame.profileId, "gmoccapy-xyzab");
assert.deepEqual(frame.jointPose.map((joint) => joint.axis), ["X", "Y", "Z", "A", "B"]);
assert.equal(frame.jointPose[4].value, 45);
assert.equal(frame.readiness.linuxCncKinematicsReady, false);
assert.equal(frame.readiness.promotionAllowed, false);
const store = createSimulationStore();
store.dispatch({ type: "SET_PROFILE", profileId: "gmoccapy-xyzab" });
assert.equal(store.getState().machineProfile, "gmoccapy-xyzab");
assert.equal(store.getState().profile.tcpCapable, false);
assert.equal(store.getState().rtcpState, "off");
store.dispatch({ type: "SET_KINS_TYPE", kinsType: "tcp-xyzac" });
assert.equal(store.getState().kinsType, "identity");
assert.equal(store.getState().rtcpState, "off");
assert.equal(store.getState().operatorMessage, "kinematics tcp-xyzac blocked: gmoccapy-xyzab is not TCP capable");
store.dispatch({ type: "SET_RTCP", enabled: true });
assert.equal(store.getState().rtcpState, "off");
assert.equal(store.getState().operatorMessage, "RTCP blocked: gmoccapy-xyzab is trivkins coordinates=xyzab reference only");
const preconditions = validateRunPreconditions(store.getState(), {
requireTaskHalRuntime: false,
requireTaskHalSession: false,
});
assert.equal(preconditions.ok, false);
assert.equal(preconditions.operatorMessage, "run blocked: unsupported five-axis profile gmoccapy-xyzab");
console.log("gmoccapy_xyzab_profile_smoke=ok");

View File

@@ -30,7 +30,7 @@ assert.equal(xyzacTrtProfile.halPins.includes("motion.switchkins-type"), true);
assert.equal(xyzacTrtProfile.halPins.includes("xyzac-trt-kins.tool-offset"), true);
assert.equal(xyzacTrtProfile.promotionAllowed, false);
assert.equal(xyzacTrtProfile.linuxCncKinematicsReady, false);
assert.deepEqual(fiveAxisProfiles.map(({ id }) => id), ["xyzac-trt", "xyzbc-trt"]);
assert.deepEqual(fiveAxisProfiles.map(({ id }) => id), ["xyzac-trt", "xyzbc-trt", "gmoccapy-xyzab"]);
const xyzbcTrtProfile = getFiveAxisProfile("xyzbc-trt");
assert.equal(xyzbcTrtProfile.id, "xyzbc-trt");
@@ -59,6 +59,19 @@ assert.equal(xyzbcSourceSummary.referenceCount >= 7, true);
assert.ok(xyzbcSourceSummary.references.some((reference) => reference.path.endsWith("xyzbc-trt.ini")));
assert.ok(xyzbcSourceSummary.references.some((reference) => reference.path.endsWith("xyzbc-trt-kins.c")));
const gmoccapyXyzabProfile = getFiveAxisProfile("gmoccapy-xyzab");
assert.equal(gmoccapyXyzabProfile.id, "gmoccapy-xyzab");
assert.deepEqual(gmoccapyXyzabProfile.coordinates, ["X", "Y", "Z", "A", "B"]);
assert.equal(gmoccapyXyzabProfile.kinematics, "trivkins coordinates=xyzab");
assert.equal(gmoccapyXyzabProfile.tcpCapable, false);
assert.equal(gmoccapyXyzabProfile.rtcpProof, false);
assert.equal(gmoccapyXyzabProfile.promotionAllowed, false);
assert.equal(gmoccapyXyzabProfile.hal.postguiRequiresHalcompReady, true);
const gmoccapySourceSummary = createProfileSourceReferenceSummary("gmoccapy-xyzab");
assert.equal(gmoccapySourceSummary.referenceCount >= 10, true);
assert.ok(gmoccapySourceSummary.references.some((reference) => reference.path.endsWith("gmoccapy_XYZAB.ini")));
assert.ok(gmoccapySourceSummary.references.some((reference) => reference.path.endsWith("gmoccapy_postgui.hal")));
const panelSummary = createPyvcpHalBindingSummary(xyzacTrtPyvcpPanelSchema);
assert.equal(panelSummary.schemaId, "xyzac-trt-switchkins-pyvcp");
assert.equal(panelSummary.controlCount, 5);

View File

@@ -261,7 +261,10 @@ assert.equal(kinematicsState.rtcpFrame.readiness.linuxCncKinematicsReady, false)
const store = createSimulationStore();
let state;
assert.equal(store.getState().availableProfiles.length, 2);
assert.deepEqual(
store.getState().availableProfiles.map((profile) => profile.id),
["xyzac-trt", "xyzbc-trt", "gmoccapy-xyzab"],
);
store.dispatch({ type: "SET_PROFILE", profileId: "xyzbc-trt" });
assert.equal(store.getState().machineProfile, "xyzbc-trt");
assert.equal(store.getState().profile.traj.coordinates, "XYZBC");