完善 gmoccapy XYZAB 参考功能
This commit is contained in:
@@ -263,6 +263,37 @@
|
||||
if (!doc.querySelector('[data-linuxcnc-task-policy="source"]')?.textContent.includes("linuxcnc/src/emc/task/emctaskmain.cc")) {
|
||||
throw new Error("missing LinuxCNC emctaskmain source diagnostic");
|
||||
}
|
||||
for (const [selector, iconName] of [
|
||||
['[data-action="estop"]', "main_switch_off"],
|
||||
['[data-action="power"]', "power_off"],
|
||||
['[data-action="mode-manual"]', "mode_manual_active"],
|
||||
['[data-action="mode-auto"]', "mode_auto_inactive"],
|
||||
['[data-action="mode-mdi"]', "mode_mdi_inactive"],
|
||||
['[data-action="RUN"]', "play"],
|
||||
['[data-action="STOP"]', "stop"],
|
||||
['[data-action="PAUSE"]', "pause"],
|
||||
['[data-action="HOME"]', "ref_all"],
|
||||
['[data-action="toggle-flood"]', "coolant_flood_active"],
|
||||
['[data-action="toggle-mist"]', "coolant_mist_inactive"],
|
||||
['[data-action="view-x"]', "tool_axis_x"],
|
||||
]) {
|
||||
const button = doc.querySelector(selector);
|
||||
if (!button || button.dataset.iconName !== iconName || !button.querySelector("img")) {
|
||||
throw new Error(`missing gmoccapy icon ${selector} expected ${iconName}, got ${button?.dataset.iconName}`);
|
||||
}
|
||||
}
|
||||
if (!doc.querySelector('[data-gmoccapy-comm="boundary"]')?.textContent.includes("native_gmoccapy_nml_hal_reference")) {
|
||||
throw new Error("missing gmoccapy communication boundary diagnostic");
|
||||
}
|
||||
if (!doc.querySelector('[data-gmoccapy-comm="native-command"]')?.textContent.includes("NML emcCommand")) {
|
||||
throw new Error("missing gmoccapy NML command diagnostic");
|
||||
}
|
||||
if (!doc.querySelector('[data-gmoccapy-comm="web-path"]')?.textContent.includes("store.dispatch")) {
|
||||
throw new Error("missing gmoccapy Web runtime mapping");
|
||||
}
|
||||
if (!doc.querySelector('[data-gmoccapy-hal="postgui"]')?.textContent.includes("tool-change-loop")) {
|
||||
throw new Error("missing gmoccapy HAL postgui diagnostic");
|
||||
}
|
||||
if (!doc.querySelector('[data-tool-preview="summary"]')?.textContent.includes("T1")) {
|
||||
throw new Error("missing tool preview summary");
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
@@ -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");
|
||||
@@ -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");
|
||||
@@ -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");
|
||||
@@ -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");
|
||||
@@ -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");
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user