完善 LinuxCNC 对标矩阵与 gmoccapy TRT 仿真
@@ -4,6 +4,7 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Web RTCP 5 Axis Simulation</title>
|
||||
<link rel="icon" href="data:," />
|
||||
<link rel="stylesheet" href="./src/styles/gmoccapy.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
62
web-rtcp-5axis-sim-plan/app/package-lock.json
generated
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"name": "web-rtcp-5axis-sim",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "web-rtcp-5axis-sim",
|
||||
"version": "0.1.0",
|
||||
"devDependencies": {
|
||||
"playwright": "^1.61.1"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.61.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz",
|
||||
"integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.61.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.61.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz",
|
||||
"integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,9 @@
|
||||
"build": "node scripts/build-static.mjs",
|
||||
"dev": "python3 -m http.server 4173",
|
||||
"smoke": "bash ../tests/browser/verify_gmoccapy_shell_browser.sh && bash ../tests/browser/verify_gmoccapy_dist_browser.sh",
|
||||
"smoke:node": "node ../tests/node/verify_linuxcnc_kinematics_runtime.mjs && node ../tests/node/verify_linuxcnc_interpreter_runtime.mjs && node ../tests/node/verify_linuxcnc_ini_runtime.mjs && node ../tests/node/verify_run_preconditions.mjs && node ../tests/node/verify_run_feedback_loop.mjs && node ../tests/node/verify_linuxcnc_task_hal_runtime.mjs && node ../tests/node/verify_native_task_hal_audit.mjs && node ../tests/node/verify_full_linuxcnc_5axis_source.mjs && node ../tests/node/verify_real_linuxcnc_5axis_program_cases.mjs && node ../tests/node/verify_full_execution_boundary.mjs && node ../tests/node/verify_machine_file_staging.mjs && node ../tests/node/verify_five_axis_session.mjs && node ../tests/node/verify_rtcp_store.mjs && node ../tests/node/verify_profile_boundary.mjs && node ../tests/node/verify_gmoccapy_xyzab_profile.mjs && node ../tests/node/verify_gmoccapy_icon_manifest.mjs && node ../tests/node/verify_gmoccapy_icon_registry.mjs && node ../tests/node/verify_gmoccapy_communication_model.mjs && node ../tests/node/verify_gmoccapy_hal_model.mjs && node ../tests/node/verify_gmoccapy_xyzab_gates.mjs && node ../tests/node/verify_linear_unit_conversion.mjs"
|
||||
"smoke:node": "node ../tests/node/verify_linuxcnc_kinematics_runtime.mjs && node ../tests/node/verify_linuxcnc_interpreter_runtime.mjs && node ../tests/node/verify_linuxcnc_ini_runtime.mjs && node ../tests/node/verify_run_preconditions.mjs && node ../tests/node/verify_run_feedback_loop.mjs && node ../tests/node/verify_linuxcnc_task_hal_runtime.mjs && node ../tests/node/verify_native_task_hal_audit.mjs && node ../tests/node/verify_full_linuxcnc_5axis_source.mjs && node ../tests/node/verify_real_linuxcnc_5axis_program_cases.mjs && node ../tests/node/verify_full_execution_boundary.mjs && node ../tests/node/verify_machine_file_staging.mjs && node ../tests/node/verify_five_axis_session.mjs && node ../tests/node/verify_rtcp_store.mjs && node ../tests/node/verify_profile_boundary.mjs && node ../tests/node/verify_gmoccapy_xyzab_profile.mjs && node ../tests/node/verify_gmoccapy_icon_manifest.mjs && node ../tests/node/verify_gmoccapy_icon_registry.mjs && node ../tests/node/verify_gmoccapy_communication_model.mjs && node ../tests/node/verify_gmoccapy_hal_model.mjs && node ../tests/node/verify_gmoccapy_xyzab_gates.mjs && node ../tests/node/verify_linear_unit_conversion.mjs && node ../tests/node/verify_gmoccapy_trt_project_sidebar.mjs && node ../tests/node/verify_linuxcnc_parity_matrix.mjs"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {}
|
||||
"devDependencies": {
|
||||
"playwright": "^1.61.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ await cp(join(appRoot, "index.html"), join(distDir, "index.html"));
|
||||
await cp(join(appRoot, "src"), join(distDir, "src"), { recursive: true });
|
||||
await copyLinuxCncManifest();
|
||||
await copyLinuxCncConfigAssets();
|
||||
await copyLinuxCncReferenceAssets();
|
||||
await copyBundledTestLinuxCncSourceAssets();
|
||||
await copyKinematicsRuntimeAssets();
|
||||
await copyInterpreterRuntimeAssets();
|
||||
@@ -69,6 +70,18 @@ async function copyLinuxCncConfigAssets() {
|
||||
await cp(configSrcDir, vendorConfigDistDir, { recursive: true });
|
||||
}
|
||||
|
||||
async function copyLinuxCncReferenceAssets() {
|
||||
const linuxCncSrcDir = join(repoRoot, "linuxcnc");
|
||||
const linuxCncDistDir = join(distDir, "linuxcnc");
|
||||
await mkdir(linuxCncDistDir, { recursive: true });
|
||||
for (const entry of [
|
||||
"configs/sim/gmoccapy",
|
||||
"nc_files",
|
||||
]) {
|
||||
await cp(join(linuxCncSrcDir, entry), join(linuxCncDistDir, entry), { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function copyBundledTestLinuxCncSourceAssets() {
|
||||
const testSourceDir = join(repoRoot, "web-rtcp-5axis-sim-plan/working_run/test_linuxcnc_source");
|
||||
const testSourceDistDir = join(distDir, "working_run/test_linuxcnc_source");
|
||||
|
||||
@@ -133,6 +133,15 @@ function selectDefaultLinuxCncSource(state) {
|
||||
|| null;
|
||||
}
|
||||
|
||||
function runtimeSdkUrls(filename) {
|
||||
const inDist = import.meta.url.includes("/app/dist/");
|
||||
return [
|
||||
inDist
|
||||
? new URL(`../wasm-port/runtime/sdk/src/${filename}`, import.meta.url).href
|
||||
: new URL(`../../../wasm-port/runtime/sdk/src/${filename}`, import.meta.url).href,
|
||||
];
|
||||
}
|
||||
|
||||
function waitForStore(store, predicate, timeoutMs = 10000) {
|
||||
const initialState = store.getState();
|
||||
if (predicate(initialState)) {
|
||||
@@ -153,10 +162,22 @@ function waitForStore(store, predicate, timeoutMs = 10000) {
|
||||
}
|
||||
|
||||
async function attachDefaultKinematicsRuntime(store, moduleId = "xyzac-trt") {
|
||||
const sdkModuleUrls = [
|
||||
new URL("../../../wasm-port/runtime/sdk/src/linuxcnc-kinematics.js", import.meta.url).href,
|
||||
new URL("../wasm-port/runtime/sdk/src/linuxcnc-kinematics.js", import.meta.url).href,
|
||||
];
|
||||
const state = store.getState();
|
||||
if (state.profile?.tcpCapable === false) {
|
||||
store.dispatch({
|
||||
type: "SET_FRAME_SOURCE",
|
||||
sourceMode: "fixture-ui-only",
|
||||
operatorMessage: `LinuxCNC kinematics disabled for ${state.machineProfile} reference profile`,
|
||||
});
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-linuxcnc-kinematics-runtime-readiness",
|
||||
moduleId,
|
||||
loaded: false,
|
||||
sourceMode: "fixture-ui-only",
|
||||
semanticBoundary: "reference-profile-no-linuxcnc-kinematics-runtime",
|
||||
};
|
||||
}
|
||||
const sdkModuleUrls = runtimeSdkUrls("linuxcnc-kinematics.js");
|
||||
const errors = [];
|
||||
for (const sdkModuleUrl of sdkModuleUrls) {
|
||||
if (typeof Worker === "function") {
|
||||
@@ -201,10 +222,7 @@ async function attachDefaultKinematicsRuntime(store, moduleId = "xyzac-trt") {
|
||||
}
|
||||
|
||||
async function attachDefaultInterpreterRuntime(store) {
|
||||
const sdkModuleUrls = [
|
||||
new URL("../../../wasm-port/runtime/sdk/src/linuxcnc-interp.js", import.meta.url).href,
|
||||
new URL("../wasm-port/runtime/sdk/src/linuxcnc-interp.js", import.meta.url).href,
|
||||
];
|
||||
const sdkModuleUrls = runtimeSdkUrls("linuxcnc-interp.js");
|
||||
const errors = [];
|
||||
for (const sdkModuleUrl of sdkModuleUrls) {
|
||||
if (typeof Worker === "function") {
|
||||
@@ -242,10 +260,7 @@ async function attachDefaultInterpreterRuntime(store) {
|
||||
}
|
||||
|
||||
async function attachDefaultTaskHalRuntime(store) {
|
||||
const sdkModuleUrls = [
|
||||
new URL("../../../wasm-port/runtime/sdk/src/linuxcnc-task-hal.js", import.meta.url).href,
|
||||
new URL("../wasm-port/runtime/sdk/src/linuxcnc-task-hal.js", import.meta.url).href,
|
||||
];
|
||||
const sdkModuleUrls = runtimeSdkUrls("linuxcnc-task-hal.js");
|
||||
const errors = [];
|
||||
for (const sdkModuleUrl of sdkModuleUrls) {
|
||||
if (typeof Worker === "function") {
|
||||
|
||||
@@ -31,6 +31,22 @@ export const linuxCncSourceReferenceMap = [
|
||||
boundary: "generated_hal_reference",
|
||||
usage: "source-visible basic_sim HAL loadrt/net proof for xyzac-trt-kins",
|
||||
},
|
||||
{
|
||||
id: "xyzac-trt-readme",
|
||||
profileId: "xyzac-trt",
|
||||
kind: "documentation",
|
||||
path: "configs/sim/axis/vismach/5axis/table-rotary-tilting/README",
|
||||
boundary: "linuxcnc_config_reference",
|
||||
usage: "switchkins setup notes for motion.switchkins-type and M428/M429/M430",
|
||||
},
|
||||
{
|
||||
id: "xyzac-trt-notes",
|
||||
profileId: "xyzac-trt",
|
||||
kind: "documentation",
|
||||
path: "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.txt",
|
||||
boundary: "linuxcnc_operator_reference",
|
||||
usage: "operator-facing switchkins notes for identity, TCP, and user kinematics",
|
||||
},
|
||||
{
|
||||
id: "xyzac-trt-table",
|
||||
profileId: "xyzac-trt",
|
||||
@@ -47,6 +63,14 @@ export const linuxCncSourceReferenceMap = [
|
||||
boundary: "linuxcnc_source_required",
|
||||
usage: "final source-derived kinematics implementation source",
|
||||
},
|
||||
{
|
||||
id: "xyzac-trt-vismach-gui",
|
||||
profileId: "xyzac-trt",
|
||||
kind: "python_gui",
|
||||
path: "src/hal/user_comps/vismach/xyzac-trt-gui.py",
|
||||
boundary: "native_vismach_reference_not_browser_runtime",
|
||||
usage: "native HAL component and Vismach scene for table, saddle, spindle, A tilt, and C rotate feedback pins",
|
||||
},
|
||||
{
|
||||
id: "trtfuncs",
|
||||
profileId: "xyzac-trt",
|
||||
@@ -71,6 +95,46 @@ export const linuxCncSourceReferenceMap = [
|
||||
boundary: "linuxcnc_program_reference",
|
||||
usage: "representative XYZAC switchkins demonstration program",
|
||||
},
|
||||
{
|
||||
id: "boat-xyzac-demo",
|
||||
profileId: "xyzac-trt",
|
||||
kind: "gcode_demo",
|
||||
path: "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/boat-xyzac.ngc",
|
||||
boundary: "linuxcnc_program_reference",
|
||||
usage: "real LinuxCNC XYZAC TCP boat demonstration program",
|
||||
},
|
||||
{
|
||||
id: "impeller-xyzac-demo",
|
||||
profileId: "xyzac-trt",
|
||||
kind: "gcode_demo",
|
||||
path: "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/impeller-7bl-xyzac.ngc",
|
||||
boundary: "linuxcnc_program_reference",
|
||||
usage: "real LinuxCNC XYZAC impeller demonstration program with long canonical path",
|
||||
},
|
||||
{
|
||||
id: "xyzac-switchkins-test-1",
|
||||
profileId: "xyzac-trt",
|
||||
kind: "gcode_demo",
|
||||
path: "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_1.ngc",
|
||||
boundary: "linuxcnc_program_reference",
|
||||
usage: "LinuxCNC XYZAC switchkins regression program with repeated M428/M429 changes",
|
||||
},
|
||||
{
|
||||
id: "xyzac-switchkins-test-2",
|
||||
profileId: "xyzac-trt",
|
||||
kind: "gcode_demo",
|
||||
path: "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_2.ngc",
|
||||
boundary: "linuxcnc_program_reference",
|
||||
usage: "LinuxCNC XYZAC switchkins regression program for TCP and identity transitions",
|
||||
},
|
||||
{
|
||||
id: "xyzac-switchkins-test-3",
|
||||
profileId: "xyzac-trt",
|
||||
kind: "gcode_demo",
|
||||
path: "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_3.ngc",
|
||||
boundary: "linuxcnc_program_reference",
|
||||
usage: "LinuxCNC XYZAC switchkins regression program for program validation coverage",
|
||||
},
|
||||
{
|
||||
id: "xyzbc-trt-ini",
|
||||
profileId: "xyzbc-trt",
|
||||
@@ -103,6 +167,22 @@ export const linuxCncSourceReferenceMap = [
|
||||
boundary: "linuxcnc_source_required",
|
||||
usage: "final source-derived XYZBC kinematics implementation source",
|
||||
},
|
||||
{
|
||||
id: "xyzbc-trt-notes",
|
||||
profileId: "xyzbc-trt",
|
||||
kind: "documentation",
|
||||
path: "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.txt",
|
||||
boundary: "linuxcnc_operator_reference",
|
||||
usage: "operator-facing switchkins notes for identity, TCP, and user kinematics",
|
||||
},
|
||||
{
|
||||
id: "xyzbc-trt-vismach-gui",
|
||||
profileId: "xyzbc-trt",
|
||||
kind: "python_gui",
|
||||
path: "src/hal/user_comps/vismach/xyzbc-trt-gui.py",
|
||||
boundary: "native_vismach_reference_not_browser_runtime",
|
||||
usage: "native HAL component and Vismach scene for table, saddle, spindle, B tilt, and C rotate feedback pins",
|
||||
},
|
||||
{
|
||||
id: "trtfuncs-xyzbc",
|
||||
profileId: "xyzbc-trt",
|
||||
@@ -127,6 +207,62 @@ export const linuxCncSourceReferenceMap = [
|
||||
boundary: "linuxcnc_program_reference",
|
||||
usage: "representative XYZBC switchkins demonstration program",
|
||||
},
|
||||
{
|
||||
id: "boat-xyzbc-demo",
|
||||
profileId: "xyzbc-trt",
|
||||
kind: "gcode_demo",
|
||||
path: "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/boat-xyzbc.ngc",
|
||||
boundary: "linuxcnc_program_reference",
|
||||
usage: "real LinuxCNC XYZBC TCP boat demonstration program",
|
||||
},
|
||||
{
|
||||
id: "gmoccapy-trt-xyzac-ini",
|
||||
profileId: "xyzac-trt",
|
||||
kind: "ini",
|
||||
path: "configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/xyzac-trt.ini",
|
||||
boundary: "linuxcnc_gmoccapy_config_reference",
|
||||
usage: "gmoccapy non-trivial XYZAC TRT machine config using DISPLAY=gmoccapy and xyzac-trt-gui",
|
||||
},
|
||||
{
|
||||
id: "gmoccapy-trt-xyzac-postgui",
|
||||
profileId: "xyzac-trt",
|
||||
kind: "postgui_hal",
|
||||
path: "configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/postgui.hal",
|
||||
boundary: "linuxcnc_gmoccapy_hal_reference",
|
||||
usage: "gmoccapy spindle, tool-change, and tool-offset POSTGUI HAL bindings for TRT sample",
|
||||
},
|
||||
{
|
||||
id: "gmoccapy-trt-xyzac-table",
|
||||
profileId: "xyzac-trt",
|
||||
kind: "tool_table",
|
||||
path: "configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/xyzac-trt.tbl",
|
||||
boundary: "linuxcnc_gmoccapy_config_reference",
|
||||
usage: "gmoccapy non-trivial TRT tool table source",
|
||||
},
|
||||
{
|
||||
id: "gmoccapy-trt-impeller-demo",
|
||||
profileId: "xyzac-trt",
|
||||
kind: "gcode_demo",
|
||||
path: "configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/examples/impeller-7bl-xyzac.ngc",
|
||||
boundary: "linuxcnc_gmoccapy_program_reference",
|
||||
usage: "gmoccapy non-trivial TRT impeller example program",
|
||||
},
|
||||
{
|
||||
id: "gmoccapy-trt-boat-xyzac-demo",
|
||||
profileId: "xyzac-trt",
|
||||
kind: "gcode_demo",
|
||||
path: "configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/examples/boat-xyzac.ngc",
|
||||
boundary: "linuxcnc_gmoccapy_program_reference",
|
||||
usage: "gmoccapy non-trivial TRT XYZAC boat example program",
|
||||
},
|
||||
{
|
||||
id: "gmoccapy-trt-boat-xyzbc-demo",
|
||||
profileId: "xyzbc-trt",
|
||||
kind: "gcode_demo",
|
||||
path: "configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/examples/boat-xyzbc.ngc",
|
||||
boundary: "linuxcnc_gmoccapy_program_reference",
|
||||
usage: "gmoccapy non-trivial TRT XYZBC boat example program",
|
||||
},
|
||||
{
|
||||
id: "gmoccapy-xyzab-ini",
|
||||
profileId: "gmoccapy-xyzab",
|
||||
|
||||
@@ -93,10 +93,7 @@ export async function loadLinuxCncIniConfig(profile, { baseUrl = import.meta.url
|
||||
throw new Error("fetch is not available for LinuxCNC INI loading");
|
||||
}
|
||||
|
||||
const candidateUrls = [
|
||||
new URL(`../../${profile.iniPath}`, baseUrl),
|
||||
new URL(`../../../../wasm-port/vendor/linuxcnc/${profile.iniPath}`, baseUrl),
|
||||
];
|
||||
const candidateUrls = linuxCncIniCandidateUrls(profile.iniPath, baseUrl);
|
||||
const errors = [];
|
||||
let response = null;
|
||||
for (const url of candidateUrls) {
|
||||
@@ -118,6 +115,18 @@ export async function loadLinuxCncIniConfig(profile, { baseUrl = import.meta.url
|
||||
});
|
||||
}
|
||||
|
||||
function linuxCncIniCandidateUrls(iniPath, baseUrl) {
|
||||
const inDist = String(baseUrl).includes("/app/dist/");
|
||||
if (String(iniPath).startsWith("linuxcnc/")) {
|
||||
return inDist
|
||||
? [new URL(`../../${iniPath}`, baseUrl)]
|
||||
: [new URL(`../../../../${iniPath}`, baseUrl)];
|
||||
}
|
||||
return inDist
|
||||
? [new URL(`../../${iniPath}`, baseUrl)]
|
||||
: [new URL(`../../../../wasm-port/vendor/linuxcnc/${iniPath}`, baseUrl)];
|
||||
}
|
||||
|
||||
export function applyIniConfigToProfile(profile, iniConfig) {
|
||||
if (!iniConfig) return profile;
|
||||
return {
|
||||
|
||||
@@ -7,6 +7,10 @@ const DEFAULT_VENDOR_ROOT_URLS = [
|
||||
new URL("../../../../wasm-port/vendor/linuxcnc/", import.meta.url).href,
|
||||
new URL("../../wasm-port/vendor/linuxcnc/", import.meta.url).href,
|
||||
];
|
||||
const DEFAULT_REPO_ROOT_URLS = [
|
||||
new URL("../../../../", import.meta.url).href,
|
||||
new URL("../../", import.meta.url).href,
|
||||
];
|
||||
const DEFAULT_TEST_SOURCE_ROOT_URLS = [
|
||||
new URL("../../../working_run/test_linuxcnc_source/", import.meta.url).href,
|
||||
new URL("../../working_run/test_linuxcnc_source/", import.meta.url).href,
|
||||
@@ -450,6 +454,11 @@ function splitPath(path) {
|
||||
}
|
||||
|
||||
function sourceUrlsFor(sourceRel) {
|
||||
if (String(sourceRel).startsWith("linuxcnc/")) {
|
||||
return sourceOverrideUrlsFor(sourceRel).concat(
|
||||
DEFAULT_REPO_ROOT_URLS.map((rootUrl) => new URL(sourceRel, rootUrl).href),
|
||||
);
|
||||
}
|
||||
return sourceOverrideUrlsFor(sourceRel).concat(
|
||||
DEFAULT_VENDOR_ROOT_URLS.map((rootUrl) => new URL(sourceRel, rootUrl).href),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,457 @@
|
||||
import { linuxCncSourceReferenceMap } from "../profiles/source-reference-map.js";
|
||||
|
||||
export const RIGHT_SIDEBAR_PARITY_ENTRIES = [
|
||||
"E-STOP",
|
||||
"POWER",
|
||||
"RESET",
|
||||
"AUTO",
|
||||
"MANUAL",
|
||||
"JOG",
|
||||
"MDI",
|
||||
"IDENTITY",
|
||||
"TCP",
|
||||
];
|
||||
|
||||
export const SWITCHKINS_PARITY_CODES = ["M428", "M429", "M430"];
|
||||
|
||||
export const LINUXCNC_PARITY_SOURCE_REFERENCES = [
|
||||
{
|
||||
id: "remap-428",
|
||||
path: "configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/428remap.ngc",
|
||||
},
|
||||
{
|
||||
id: "remap-429",
|
||||
path: "configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/429remap.ngc",
|
||||
},
|
||||
{
|
||||
id: "remap-430",
|
||||
path: "configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/430remap.ngc",
|
||||
},
|
||||
{
|
||||
id: "linuxcnc-emc-hh",
|
||||
path: "linuxcnc/src/emc/nml_intf/emc.hh",
|
||||
},
|
||||
{
|
||||
id: "linuxcnc-emctaskmain",
|
||||
path: "linuxcnc/src/emc/task/emctaskmain.cc",
|
||||
},
|
||||
{
|
||||
id: "linuxcnc-emctask",
|
||||
path: "linuxcnc/src/emc/task/emctask.cc",
|
||||
},
|
||||
{
|
||||
id: "linuxcnc-motion-control",
|
||||
path: "linuxcnc/src/emc/motion/control.c",
|
||||
},
|
||||
{
|
||||
id: "linuxcnc-hal-lib",
|
||||
path: "linuxcnc/src/hal/hal_lib.c",
|
||||
},
|
||||
{
|
||||
id: "web-machine-project-root",
|
||||
path: "web-rtcp-5axis-sim-plan/machines/<profile>/configs/sim/axis/vismach/5axis/table-rotary-tilting",
|
||||
},
|
||||
];
|
||||
|
||||
export const LINUXCNC_PARITY_ITEMS = [
|
||||
{
|
||||
id: "xyzac-trt-axis-vismach-config",
|
||||
category: "machine-config",
|
||||
label: "XYZAC TRT AXIS/Vismach machine config",
|
||||
linuxCncPaths: [
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.xml",
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/switchkins_postgui.hal",
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.tbl",
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/428remap.ngc",
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/429remap.ngc",
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/430remap.ngc",
|
||||
],
|
||||
functions: [
|
||||
"INI [KINS] xyzac-trt-kins sparm=identityfirst",
|
||||
"TRAJ XYZAC",
|
||||
"HALUI M429/M428/M430",
|
||||
"motion.switchkins-type",
|
||||
"project-staged INI/HAL/TBL/NGC",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "xyzbc-trt-axis-vismach-config",
|
||||
category: "machine-config",
|
||||
label: "XYZBC TRT AXIS/Vismach machine config",
|
||||
linuxCncPaths: [
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini",
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.xml",
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/switchkins_postgui.hal",
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.tbl",
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/428remap.ngc",
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/429remap.ngc",
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/430remap.ngc",
|
||||
],
|
||||
functions: [
|
||||
"INI [KINS] xyzbc-trt-kins sparm=identityfirst",
|
||||
"TRAJ XYZBC",
|
||||
"HALUI M429/M428/M430",
|
||||
"motion.switchkins-type",
|
||||
"project-staged INI/HAL/TBL/NGC",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "gmoccapy-trt-config",
|
||||
category: "machine-config",
|
||||
label: "gmoccapy non-trivial TRT sample config",
|
||||
linuxCncPaths: [
|
||||
"configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/xyzac-trt.ini",
|
||||
"configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/postgui.hal",
|
||||
"configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/xyzac-trt.tbl",
|
||||
"configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/examples/impeller-7bl-xyzac.ngc",
|
||||
"configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/examples/boat-xyzac.ngc",
|
||||
"configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/examples/boat-xyzbc.ngc",
|
||||
],
|
||||
functions: [
|
||||
"DISPLAY gmoccapy",
|
||||
"POSTGUI spindle/tool HAL bindings",
|
||||
"xyzac-trt-gui vismach feedback pins",
|
||||
"gmoccapy TRT example programs",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "gmoccapy-native-operator-ui",
|
||||
category: "operator-ui",
|
||||
label: "gmoccapy native operator workflow",
|
||||
linuxCncPaths: [
|
||||
"linuxcnc/src/emc/usr_intf/gmoccapy/gmoccapy.py",
|
||||
"linuxcnc/share/gmoccapy/gmoccapy.glade",
|
||||
"web-rtcp-5axis-sim-plan/app/src/ui-reference/gmoccapy-button-icons.json",
|
||||
],
|
||||
functions: [
|
||||
"E-STOP/POWER/RESET task-state entry",
|
||||
"AUTO/MANUAL/JOG/MDI task-mode entry",
|
||||
"button active/blocked color rule",
|
||||
"gmoccapy HAL button/icon mapping",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "right-sidebar-task-interlocks",
|
||||
category: "operator-ui",
|
||||
label: "Right vertical machine-state entrances",
|
||||
linuxCncPaths: [
|
||||
"linuxcnc/src/emc/nml_intf/emc.hh",
|
||||
"linuxcnc/src/emc/task/emctaskmain.cc",
|
||||
"linuxcnc/src/emc/task/emctask.cc",
|
||||
],
|
||||
functions: [
|
||||
"EMC_TASK_SET_STATE",
|
||||
"EMC_TASK_SET_MODE",
|
||||
"EMC_JOINT_HOME",
|
||||
"EMC_JOG_INCR",
|
||||
"EMC_TASK_PLAN_RUN",
|
||||
"EMC_TASK_PLAN_EXECUTE",
|
||||
"EMC_TASK_ABORT",
|
||||
"mode and interpreter idle gates",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "vismach-machine-preview",
|
||||
category: "visualization",
|
||||
label: "Vismach TRT preview and feedback model",
|
||||
linuxCncPaths: [
|
||||
"src/hal/user_comps/vismach/xyzac-trt-gui.py",
|
||||
"src/hal/user_comps/vismach/xyzbc-trt-gui.py",
|
||||
],
|
||||
functions: [
|
||||
"table/saddle/spindle feedback pins",
|
||||
"A/B tilt and C rotate feedback pins",
|
||||
"tool-offset/y-offset/x-offset/z-offset visualization",
|
||||
"WebGL toolpath preview and current tool marker",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "source-derived-kinematics-switchkins",
|
||||
category: "runtime",
|
||||
label: "TRT kinematics and switchkins",
|
||||
linuxCncPaths: [
|
||||
"src/emc/kinematics/xyzac-trt-kins.c",
|
||||
"src/emc/kinematics/xyzbc-trt-kins.c",
|
||||
"src/emc/kinematics/trtfuncs.c",
|
||||
"src/emc/kinematics/switchkins.c",
|
||||
],
|
||||
functions: [
|
||||
"forward/inverse kinematics frame",
|
||||
"identity/TCP/userk kinstype mapping",
|
||||
"M428 TCP",
|
||||
"M429 identity",
|
||||
"M430 userk",
|
||||
"DRO TCP axis value refresh",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "linuxcnc-interpreter-program-validation",
|
||||
category: "runtime",
|
||||
label: "LinuxCNC interpreter-backed G-code validation",
|
||||
linuxCncPaths: [
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins.ngc",
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzbc_switchkins.ngc",
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/boat-xyzac.ngc",
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/boat-xyzbc.ngc",
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/impeller-7bl-xyzac.ngc",
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_1.ngc",
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_2.ngc",
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_3.ngc",
|
||||
],
|
||||
functions: [
|
||||
"real LinuxCNC 5-axis program source guard",
|
||||
"canonical motion extraction",
|
||||
"planner sample timing",
|
||||
"current line and realtime axis feedback",
|
||||
"toolpath preview path verification",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "machine-project-directory",
|
||||
category: "project-files",
|
||||
label: "Machine project directory and INI parity",
|
||||
linuxCncPaths: [
|
||||
"web-rtcp-5axis-sim-plan/machines/<profile>/configs/sim/axis/vismach/5axis/table-rotary-tilting",
|
||||
],
|
||||
functions: [
|
||||
"profile-specific project root",
|
||||
"LinuxCNC INI source text match",
|
||||
"config file count",
|
||||
"G-code/remap file count",
|
||||
"selected program provenance",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "task-hal-status-loop",
|
||||
category: "runtime",
|
||||
label: "Task/HAL run feedback and DRO updates",
|
||||
linuxCncPaths: [
|
||||
"linuxcnc/src/emc/task/emctaskmain.cc",
|
||||
"linuxcnc/src/emc/motion/control.c",
|
||||
"linuxcnc/src/hal/hal_lib.c",
|
||||
],
|
||||
functions: [
|
||||
"task cycle status",
|
||||
"servo cycle status",
|
||||
"HAL changed pin summary",
|
||||
"run/pause/resume/step/stop feedback",
|
||||
"DRO axis value and DTG refresh",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "gmoccapy-postgui-tool-spindle",
|
||||
category: "operator-ui",
|
||||
label: "gmoccapy POSTGUI tool/spindle HAL functions",
|
||||
linuxCncPaths: [
|
||||
"configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/postgui.hal",
|
||||
"linuxcnc/configs/sim/gmoccapy/gmoccapy_postgui.hal",
|
||||
"linuxcnc/configs/sim/gmoccapy/core_sim_XYZAB.hal",
|
||||
"linuxcnc/configs/sim/gmoccapy/spindle_sim.hal",
|
||||
],
|
||||
functions: [
|
||||
"gmoccapy.spindle_feedback_bar",
|
||||
"gmoccapy.spindle_at_speed_led",
|
||||
"gmoccapy.toolchange-change/changing loop",
|
||||
"gmoccapy.tooloffset-z",
|
||||
"coolant/spindle/operator override gates",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export function createLinuxCncParityMatrix(state = {}) {
|
||||
const items = LINUXCNC_PARITY_ITEMS.map((item) => createParityItemState(item, state));
|
||||
const implementedCount = items.filter((item) => item.implemented).length;
|
||||
const activeCount = items.filter((item) => item.active).length;
|
||||
const sourcePaths = unique(items.flatMap((item) => item.linuxCncPaths));
|
||||
const functions = unique(items.flatMap((item) => item.functions));
|
||||
const categories = Object.fromEntries(
|
||||
unique(items.map((item) => item.category)).map((category) => [
|
||||
category,
|
||||
{
|
||||
itemCount: items.filter((item) => item.category === category).length,
|
||||
implementedCount: items.filter((item) => item.category === category && item.implemented).length,
|
||||
activeCount: items.filter((item) => item.category === category && item.active).length,
|
||||
},
|
||||
]),
|
||||
);
|
||||
const sidebarLabels = (state.rightSidebarEntrances || []).map((entry) => entry.label);
|
||||
const switchkinsCodes = state.profile?.kinematicsParameters?.switchkinsTypes
|
||||
?.map((type) => type.mdiCommand)
|
||||
.filter(Boolean) || [];
|
||||
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-linuxcnc-parity-matrix",
|
||||
profileId: state.profile?.id || state.machineProfile || "unknown",
|
||||
status: implementedCount === items.length ? "implemented" : "partial",
|
||||
implementedCount,
|
||||
activeCount,
|
||||
itemCount: items.length,
|
||||
sourcePathCount: sourcePaths.length,
|
||||
linuxCncProgramCount: sourcePaths.filter((path) => path.endsWith(".ngc")).length,
|
||||
functionCount: functions.length,
|
||||
categories,
|
||||
requiredRightSidebarEntries: RIGHT_SIDEBAR_PARITY_ENTRIES,
|
||||
actualRightSidebarEntries: sidebarLabels,
|
||||
rightSidebarComplete: RIGHT_SIDEBAR_PARITY_ENTRIES.every((label) => sidebarLabels.includes(label)),
|
||||
requiredSwitchkinsCodes: SWITCHKINS_PARITY_CODES,
|
||||
activeSwitchkinsCodes: switchkinsCodes,
|
||||
switchkinsComplete: SWITCHKINS_PARITY_CODES.every((code) => switchkinsCodes.includes(code)),
|
||||
items,
|
||||
linuxCncPrograms: sourcePaths,
|
||||
linuxCncFunctions: functions,
|
||||
sourceReferenceIds: unique(sourceReferenceIdsForPaths(sourcePaths)),
|
||||
semanticBoundary: "linuxcnc_source_function_parity_matrix_for_browser_simulation_not_hardware_control",
|
||||
};
|
||||
}
|
||||
|
||||
function createParityItemState(item, state) {
|
||||
const evidence = createItemEvidence(item.id, state);
|
||||
const sourceMapped = item.linuxCncPaths.every((path) => hasSourceReference(path));
|
||||
const implemented = sourceMapped && evidence.implemented !== false;
|
||||
const active = implemented && evidence.active === true;
|
||||
|
||||
return {
|
||||
...item,
|
||||
linuxCncReferenceIds: sourceReferenceIdsForPaths(item.linuxCncPaths),
|
||||
sourceMapped,
|
||||
implemented,
|
||||
active,
|
||||
status: active
|
||||
? "active"
|
||||
: implemented
|
||||
? evidence.status || "implemented"
|
||||
: "missing-source-reference",
|
||||
evidence,
|
||||
};
|
||||
}
|
||||
|
||||
function createItemEvidence(id, state) {
|
||||
switch (id) {
|
||||
case "xyzac-trt-axis-vismach-config":
|
||||
return {
|
||||
status: state.profile?.id === "xyzac-trt" ? "active-profile" : "implemented",
|
||||
active: state.profile?.id === "xyzac-trt",
|
||||
profileAvailable: profileAvailable(state, "xyzac-trt"),
|
||||
iniPath: state.profile?.id === "xyzac-trt" ? state.profile?.iniPath : null,
|
||||
};
|
||||
case "xyzbc-trt-axis-vismach-config":
|
||||
return {
|
||||
status: state.profile?.id === "xyzbc-trt" ? "active-profile" : "implemented",
|
||||
active: state.profile?.id === "xyzbc-trt",
|
||||
profileAvailable: profileAvailable(state, "xyzbc-trt"),
|
||||
iniPath: state.profile?.id === "xyzbc-trt" ? state.profile?.iniPath : null,
|
||||
};
|
||||
case "gmoccapy-trt-config":
|
||||
return {
|
||||
status: "reference-mapped",
|
||||
active: Boolean(state.gmoccapyGui),
|
||||
displayStyle: "gmoccapy-web-shell",
|
||||
};
|
||||
case "gmoccapy-native-operator-ui":
|
||||
return {
|
||||
active: Boolean(state.gmoccapyGui) && rightSidebarComplete(state),
|
||||
rightSidebarComplete: rightSidebarComplete(state),
|
||||
buttonCount: state.rightSidebarEntrances?.length || 0,
|
||||
};
|
||||
case "right-sidebar-task-interlocks":
|
||||
return {
|
||||
active: Boolean(state.linuxCncTaskPolicy) && rightSidebarComplete(state),
|
||||
taskState: state.linuxCncTaskPolicy?.taskState || null,
|
||||
taskMode: state.linuxCncTaskPolicy?.taskMode || null,
|
||||
interpState: state.linuxCncTaskPolicy?.interpState || null,
|
||||
gates: {
|
||||
canJog: state.linuxCncTaskPolicy?.canJog === true,
|
||||
canRunAuto: state.linuxCncTaskPolicy?.canRunAuto === true,
|
||||
canExecuteMdi: state.linuxCncTaskPolicy?.canExecuteMdi === true,
|
||||
},
|
||||
};
|
||||
case "vismach-machine-preview":
|
||||
return {
|
||||
active: Boolean(state.rtcpFrame) && Boolean(state.preview),
|
||||
frameApi: state.rtcpFrame?.apiName || null,
|
||||
previewPoints: state.preview?.pathPoints || 0,
|
||||
profileKinematics: state.profile?.kinematics || null,
|
||||
};
|
||||
case "source-derived-kinematics-switchkins":
|
||||
return {
|
||||
status: state.kinematicsRuntimeReadiness?.loaded ? "runtime-ready" : "implemented",
|
||||
active: Boolean(state.profile?.kinematics) && switchkinsComplete(state),
|
||||
runtimeLoaded: state.kinematicsRuntimeReadiness?.loaded === true,
|
||||
kinsType: state.kinsType || null,
|
||||
rtcpState: state.rtcpState || null,
|
||||
activeSwitchkinsCodes: state.profile?.kinematicsParameters?.switchkinsTypes?.map((type) => type.mdiCommand) || [],
|
||||
};
|
||||
case "linuxcnc-interpreter-program-validation":
|
||||
return {
|
||||
status: state.programValidation?.ready ? "runtime-validated" : "implemented",
|
||||
active: state.programValidation?.ready === true,
|
||||
sourceGuard: state.programValidation?.sourceGuard || null,
|
||||
motionEventCount: state.programValidation?.motionEventCount || 0,
|
||||
plannerSampleCount: state.programValidation?.plannerSampleCount || 0,
|
||||
};
|
||||
case "machine-project-directory":
|
||||
return {
|
||||
status: state.machineProject?.status === "staged" ? "staged" : "implemented",
|
||||
active: state.machineProject?.status === "staged",
|
||||
projectRoot: state.machineProject?.projectRoot || null,
|
||||
iniSourceMatchesProfile: state.machineProject?.ini?.sourceMatchesProfile === true,
|
||||
configFileCount: state.machineProject?.configFileCount || 0,
|
||||
gcodeFileCount: state.machineProject?.gcodeFileCount || 0,
|
||||
};
|
||||
case "task-hal-status-loop":
|
||||
return {
|
||||
status: state.taskHalStatusLoop?.active ? "runtime-active" : "implemented",
|
||||
active: Boolean(state.taskHalRuntimeReadiness?.loaded || state.programRuntimeFeedback),
|
||||
taskHalLoaded: state.taskHalRuntimeReadiness?.loaded === true,
|
||||
statusLoopActive: state.taskHalStatusLoop?.active === true,
|
||||
runtimeFeedbackSource: state.programRuntimeFeedback?.sourceMode || null,
|
||||
};
|
||||
case "gmoccapy-postgui-tool-spindle":
|
||||
return {
|
||||
active: Boolean(state.gmoccapyGui) && Boolean(state.toolPreview) && Boolean(state.spindle) && Boolean(state.coolant),
|
||||
toolNumber: state.toolPreview?.toolNumber || null,
|
||||
spindleDirection: state.spindle?.direction || null,
|
||||
coolantFlood: state.coolant?.flood === true,
|
||||
coolantMist: state.coolant?.mist === true,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
active: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function profileAvailable(state, profileId) {
|
||||
return (state.availableProfiles || []).some((profile) => profile.id === profileId);
|
||||
}
|
||||
|
||||
function rightSidebarComplete(state) {
|
||||
const labels = (state.rightSidebarEntrances || []).map((entry) => entry.label);
|
||||
return RIGHT_SIDEBAR_PARITY_ENTRIES.every((label) => labels.includes(label));
|
||||
}
|
||||
|
||||
function switchkinsComplete(state) {
|
||||
const codes = state.profile?.kinematicsParameters?.switchkinsTypes
|
||||
?.map((type) => type.mdiCommand)
|
||||
.filter(Boolean) || [];
|
||||
return SWITCHKINS_PARITY_CODES.every((code) => codes.includes(code));
|
||||
}
|
||||
|
||||
function hasSourceReference(path) {
|
||||
return allParityReferences().some((reference) => reference.path === path);
|
||||
}
|
||||
|
||||
function sourceReferenceIdsForPaths(paths) {
|
||||
return paths
|
||||
.map((path) => allParityReferences().find((reference) => reference.path === path)?.id)
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function allParityReferences() {
|
||||
return [...linuxCncSourceReferenceMap, ...LINUXCNC_PARITY_SOURCE_REFERENCES];
|
||||
}
|
||||
|
||||
function unique(values) {
|
||||
return [...new Set(values.filter(Boolean))];
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import { fiveAxisProfiles, getFiveAxisProfile } from "../profiles/index.js";
|
||||
import { applyIniConfigToProfile } from "../runtime/linuxcnc-ini-runtime.js";
|
||||
import {
|
||||
listLinuxCncGcodeSources,
|
||||
listProjectGcodeFiles,
|
||||
selectMachineFileProgram,
|
||||
stageProfileMachineFiles,
|
||||
} from "../runtime/linuxcnc-machine-file-staging.js";
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
buildTaskHalProgramMotionPlan,
|
||||
buildTaskHalSessionFromMachineFiles,
|
||||
} from "../runtime/linuxcnc-task-hal-runtime.js";
|
||||
import { createLinuxCncParityMatrix } from "../runtime/linuxcnc-parity-matrix.js";
|
||||
import { gmoccapyHalModel, resolveGmoccapyHardwareButton } from "../runtime/gmoccapy-hal-model.js";
|
||||
import {
|
||||
createLinuxCncTaskPolicyStatus,
|
||||
@@ -34,6 +36,7 @@ const initialLinuxCncBoundaryAdapter = createLinuxCncBoundaryAdapter({
|
||||
profile: defaultProfile,
|
||||
});
|
||||
const initialLinuxCncBoundaryReadiness = createLinuxCncBoundaryReadiness(initialLinuxCncBoundaryAdapter);
|
||||
const MACHINE_PROJECT_OPFS_ROOT = "web-rtcp-5axis-sim-plan/machines";
|
||||
|
||||
const initialAxisPose = {
|
||||
x: 43.0,
|
||||
@@ -114,8 +117,13 @@ const initialState = {
|
||||
plan: null,
|
||||
save: null,
|
||||
gcodeSources: [],
|
||||
gcodeFiles: [],
|
||||
selectedGcodeSourceRel: null,
|
||||
},
|
||||
machineProject: null,
|
||||
programValidation: null,
|
||||
linuxCncParityMatrix: null,
|
||||
rightSidebarEntrances: [],
|
||||
sourceMode: "fixture-ui-only",
|
||||
frameSourceMode: "fixture-ui-only",
|
||||
desiredFrameSourceMode: "fixture-ui-only",
|
||||
@@ -127,6 +135,7 @@ const initialState = {
|
||||
interpState: "idle",
|
||||
interpResumeState: "idle",
|
||||
taskPaused: false,
|
||||
manualPanel: "manual",
|
||||
allHomed: false,
|
||||
noForceHoming: false,
|
||||
jogAxis: "x",
|
||||
@@ -216,7 +225,7 @@ const initialState = {
|
||||
dtgZ: 2.25,
|
||||
},
|
||||
feed: {
|
||||
currentVelocity: 2621,
|
||||
currentVelocity: 0,
|
||||
rapidOverride: 100,
|
||||
feedRate: 4500000,
|
||||
feedOverride: 100,
|
||||
@@ -224,11 +233,11 @@ const initialState = {
|
||||
spindle: {
|
||||
rpm: 1600,
|
||||
override: 100,
|
||||
enabled: true,
|
||||
direction: "forward",
|
||||
enabled: false,
|
||||
direction: "stop",
|
||||
},
|
||||
coolant: {
|
||||
flood: true,
|
||||
flood: false,
|
||||
mist: false,
|
||||
},
|
||||
gmoccapyGui: {
|
||||
@@ -299,6 +308,10 @@ const initialState = {
|
||||
mdiHistory: [],
|
||||
};
|
||||
initialState.linuxCncTaskPolicy = createLinuxCncTaskPolicyStatus(initialState);
|
||||
initialState.machineProject = createMachineProjectState(initialState);
|
||||
initialState.programValidation = createProgramValidationState(initialState);
|
||||
initialState.rightSidebarEntrances = createRightSidebarEntranceState(initialState, initialState.linuxCncTaskPolicy);
|
||||
initialState.linuxCncParityMatrix = createLinuxCncParityMatrix(initialState);
|
||||
|
||||
const programLines = [
|
||||
"N4860 Y[#<yscale>*-39.009]",
|
||||
@@ -338,6 +351,11 @@ export function createSimulationStore(seed = {}) {
|
||||
dro: buildDroFromFrame(seedFrame, seed.programRuntimeFeedback || initialState.programRuntimeFeedback),
|
||||
programLines: seed.programLines || programLines,
|
||||
};
|
||||
state.linuxCncTaskPolicy = createLinuxCncTaskPolicyStatus(state);
|
||||
state.machineProject = createMachineProjectState(state);
|
||||
state.programValidation = createProgramValidationState(state);
|
||||
state.rightSidebarEntrances = createRightSidebarEntranceState(state, state.linuxCncTaskPolicy);
|
||||
state.linuxCncParityMatrix = createLinuxCncParityMatrix(state);
|
||||
state.fullExecutionBoundary = createFullLinuxCncExecutionBoundary(state);
|
||||
const listeners = new Set();
|
||||
let taskHalStatusLoopTimer = null;
|
||||
@@ -358,7 +376,7 @@ export function createSimulationStore(seed = {}) {
|
||||
};
|
||||
const frameState = buildFrameForState(next, patch);
|
||||
const frame = patch.rtcpFrame || frameState.frame;
|
||||
const nextState = {
|
||||
const baseState = {
|
||||
...next,
|
||||
sourceMode: frame.sourceMode,
|
||||
frameSourceMode: frame.sourceMode,
|
||||
@@ -372,9 +390,20 @@ export function createSimulationStore(seed = {}) {
|
||||
lastKinematicsResult: frameState.lastKinematicsResult,
|
||||
dro: buildDroFromFrame(frame, next.programRuntimeFeedback),
|
||||
};
|
||||
const taskPolicy = createLinuxCncTaskPolicyStatus(baseState);
|
||||
const projectState = createMachineProjectState(baseState);
|
||||
const validationState = createProgramValidationState(baseState);
|
||||
const sidebarState = createRightSidebarEntranceState(baseState, taskPolicy);
|
||||
const nextState = {
|
||||
...baseState,
|
||||
linuxCncTaskPolicy: taskPolicy,
|
||||
machineProject: projectState,
|
||||
programValidation: validationState,
|
||||
rightSidebarEntrances: sidebarState,
|
||||
};
|
||||
state = {
|
||||
...nextState,
|
||||
linuxCncTaskPolicy: createLinuxCncTaskPolicyStatus(nextState),
|
||||
linuxCncParityMatrix: createLinuxCncParityMatrix(nextState),
|
||||
fullExecutionBoundary: createFullLinuxCncExecutionBoundary(nextState),
|
||||
};
|
||||
notify();
|
||||
@@ -847,6 +876,7 @@ export function createSimulationStore(seed = {}) {
|
||||
plan: action.plan,
|
||||
save: action.save,
|
||||
gcodeSources: listLinuxCncGcodeSources(action.save),
|
||||
gcodeFiles: listProjectGcodeFiles(action.save),
|
||||
selectedGcodeSourceRel: action.selectedGcodeSourceRel
|
||||
|| action.plan.selectedProgramSourceRel
|
||||
|| null,
|
||||
@@ -922,6 +952,7 @@ export function createSimulationStore(seed = {}) {
|
||||
setState(applyTaskHalStatusPatch(state, action.status, action.operatorMessage, {
|
||||
loopSequence: action.loopSequence,
|
||||
preserveMachine: action.preserveMachine,
|
||||
preserveAxisPose: action.preserveAxisPose,
|
||||
}));
|
||||
break;
|
||||
case "TASK_HAL_STATUS_LOOP_STARTED":
|
||||
@@ -1087,11 +1118,14 @@ export function createSimulationStore(seed = {}) {
|
||||
powerOn: !turningOff,
|
||||
estopActive: false,
|
||||
taskState: turningOff ? "estop-reset" : "on",
|
||||
manualPanel: "manual",
|
||||
interpState: "idle",
|
||||
interpResumeState: "idle",
|
||||
taskPaused: false,
|
||||
},
|
||||
runState: turningOff ? "powered-off" : "idle",
|
||||
kinsType: turningOff ? "identity" : state.kinsType,
|
||||
rtcpState: turningOff ? "off" : state.rtcpState,
|
||||
feed: turningOff ? { ...state.feed, currentVelocity: 0 } : state.feed,
|
||||
coolant: turningOff ? { ...state.coolant, flood: false, mist: false } : state.coolant,
|
||||
spindle: turningOff ? { ...state.spindle, enabled: false, direction: "stop" } : state.spindle,
|
||||
@@ -1108,11 +1142,14 @@ export function createSimulationStore(seed = {}) {
|
||||
powerOn: !turningOff,
|
||||
estopActive: false,
|
||||
taskState: turningOff ? "estop-reset" : "on",
|
||||
manualPanel: "manual",
|
||||
interpState: "idle",
|
||||
interpResumeState: "idle",
|
||||
taskPaused: false,
|
||||
},
|
||||
runState: turningOff ? "powered-off" : "idle",
|
||||
kinsType: turningOff ? "identity" : state.kinsType,
|
||||
rtcpState: turningOff ? "off" : state.rtcpState,
|
||||
feed: turningOff ? { ...state.feed, currentVelocity: 0 } : state.feed,
|
||||
coolant: turningOff ? { ...state.coolant, flood: false, mist: false } : state.coolant,
|
||||
spindle: turningOff ? { ...state.spindle, enabled: false, direction: "stop" } : state.spindle,
|
||||
@@ -1127,11 +1164,14 @@ export function createSimulationStore(seed = {}) {
|
||||
powerOn: false,
|
||||
estopActive: true,
|
||||
taskState: "estop",
|
||||
manualPanel: "manual",
|
||||
interpState: "idle",
|
||||
interpResumeState: "idle",
|
||||
taskPaused: false,
|
||||
},
|
||||
runState: "estopped",
|
||||
kinsType: "identity",
|
||||
rtcpState: "off",
|
||||
feed: {
|
||||
...state.feed,
|
||||
currentVelocity: 0,
|
||||
@@ -1156,12 +1196,15 @@ export function createSimulationStore(seed = {}) {
|
||||
powerOn: false,
|
||||
estopActive: false,
|
||||
taskState: "estop-reset",
|
||||
manualPanel: "manual",
|
||||
interpState: "idle",
|
||||
interpResumeState: "idle",
|
||||
taskPaused: false,
|
||||
resetCount: state.machine.resetCount + 1,
|
||||
},
|
||||
runState: "idle",
|
||||
kinsType: "identity",
|
||||
rtcpState: "off",
|
||||
coolant: {
|
||||
...state.coolant,
|
||||
flood: false,
|
||||
@@ -1182,17 +1225,31 @@ export function createSimulationStore(seed = {}) {
|
||||
setState({ operatorMessage: gate.operatorMessage });
|
||||
break;
|
||||
}
|
||||
const mode = normalizeLinuxCncTaskMode(action.mode);
|
||||
const requestedMode = String(action.mode || "");
|
||||
const mode = normalizeLinuxCncTaskMode(requestedMode);
|
||||
const manualPanel = requestedMode === "jog"
|
||||
? "jog"
|
||||
: mode === "manual"
|
||||
? "manual"
|
||||
: null;
|
||||
if (state.taskHalRuntime?.loaded) {
|
||||
runTaskHalCommandSequence([
|
||||
{ type: "EMC_TASK_SET_MODE", mode: mode.toUpperCase() },
|
||||
], { operatorMessage: `task/HAL mode ${mode}` }).catch(() => {});
|
||||
], {
|
||||
operatorMessage: `task/HAL mode ${mode}`,
|
||||
preserveMachine: {
|
||||
...state.machine,
|
||||
mode,
|
||||
manualPanel,
|
||||
},
|
||||
}).catch(() => {});
|
||||
break;
|
||||
}
|
||||
setState({
|
||||
machine: {
|
||||
...state.machine,
|
||||
mode,
|
||||
manualPanel,
|
||||
interpState: mode === "manual" ? "idle" : state.machine.interpState,
|
||||
interpResumeState: mode === "manual" ? "idle" : state.machine.interpResumeState,
|
||||
taskPaused: mode === "manual" ? false : state.machine.taskPaused,
|
||||
@@ -1222,6 +1279,10 @@ export function createSimulationStore(seed = {}) {
|
||||
const direction = Number(action.direction || 1);
|
||||
const increment = Number(action.increment || state.machine.jogIncrement);
|
||||
if (state.taskHalRuntime?.loaded) {
|
||||
const nextAxisPose = clampAxisPoseToProfile({
|
||||
...state.axisPose,
|
||||
[axis]: Number(state.axisPose[axis] || 0) + direction * increment,
|
||||
}, state.profile);
|
||||
const pendingJogCommand = {
|
||||
axis,
|
||||
direction,
|
||||
@@ -1229,6 +1290,22 @@ export function createSimulationStore(seed = {}) {
|
||||
basePose: { ...state.axisPose },
|
||||
createdAtLine: state.activeLine,
|
||||
};
|
||||
setState({
|
||||
machine: {
|
||||
...state.machine,
|
||||
mode: "manual",
|
||||
manualPanel: "jog",
|
||||
jogAxis: axis,
|
||||
jogIncrement: increment,
|
||||
interpState: "idle",
|
||||
interpResumeState: "idle",
|
||||
taskPaused: false,
|
||||
},
|
||||
axisPose: nextAxisPose,
|
||||
runState: "jogging",
|
||||
pendingJogCommand,
|
||||
operatorMessage: `task/HAL jog ${axis.toUpperCase()} ${direction > 0 ? "+" : "-"}${increment}`,
|
||||
});
|
||||
runTaskHalCommandSequence([
|
||||
{
|
||||
type: "EMC_JOG_INCR",
|
||||
@@ -1238,6 +1315,15 @@ export function createSimulationStore(seed = {}) {
|
||||
},
|
||||
], {
|
||||
pendingJogCommand,
|
||||
preserveAxisPose: nextAxisPose,
|
||||
preserveMachine: {
|
||||
...state.machine,
|
||||
mode: "manual",
|
||||
manualPanel: "jog",
|
||||
interpState: "idle",
|
||||
interpResumeState: "idle",
|
||||
taskPaused: false,
|
||||
},
|
||||
operatorMessage: `task/HAL jog ${axis.toUpperCase()} ${direction > 0 ? "+" : "-"}${increment}`,
|
||||
}).catch(() => {});
|
||||
break;
|
||||
@@ -1246,6 +1332,7 @@ export function createSimulationStore(seed = {}) {
|
||||
machine: {
|
||||
...state.machine,
|
||||
mode: "manual",
|
||||
manualPanel: "jog",
|
||||
jogAxis: axis,
|
||||
jogIncrement: increment,
|
||||
},
|
||||
@@ -1267,10 +1354,16 @@ export function createSimulationStore(seed = {}) {
|
||||
}
|
||||
if (state.taskHalRuntime?.loaded) {
|
||||
const command = normalizeMdiCommand(action.command ?? state.machine.mdiCommand);
|
||||
const mdiResult = executeMdiCommand(state, command);
|
||||
setState(mdiResult.patch);
|
||||
runTaskHalCommandSequence([
|
||||
{ type: "EMC_TASK_SET_MODE", mode: "MDI" },
|
||||
{ type: "EMC_TASK_PLAN_EXECUTE", mdi: command },
|
||||
], { operatorMessage: `task/HAL MDI ${command}` }).catch(() => {});
|
||||
], {
|
||||
operatorMessage: `task/HAL MDI ${command}`,
|
||||
preserveAxisPose: mdiResult.patch.axisPose,
|
||||
preserveMachine: mdiResult.patch.machine,
|
||||
}).catch(() => {});
|
||||
break;
|
||||
}
|
||||
const mdiResult = executeMdiCommand(state, action.command ?? state.machine.mdiCommand);
|
||||
@@ -1285,6 +1378,7 @@ export function createSimulationStore(seed = {}) {
|
||||
machine: {
|
||||
...state.machine,
|
||||
mode: "auto",
|
||||
manualPanel: null,
|
||||
interpState: "idle",
|
||||
interpResumeState: "idle",
|
||||
taskPaused: false,
|
||||
@@ -1325,6 +1419,7 @@ export function createSimulationStore(seed = {}) {
|
||||
machine: {
|
||||
...state.machine,
|
||||
mode: "auto",
|
||||
manualPanel: null,
|
||||
interpState: playback.complete ? "idle" : "reading",
|
||||
interpResumeState: playback.complete ? "idle" : "reading",
|
||||
taskPaused: false,
|
||||
@@ -1369,7 +1464,15 @@ export function createSimulationStore(seed = {}) {
|
||||
}));
|
||||
runTaskHalCommandSequence([
|
||||
{ type: "EMC_TASK_ABORT" },
|
||||
], { operatorMessage: action.type === "ABORT" ? "task/HAL abort complete" : "task/HAL program stopped" })
|
||||
], {
|
||||
operatorMessage: action.type === "ABORT" ? "task/HAL abort complete" : "task/HAL program stopped",
|
||||
preserveMachine: {
|
||||
...state.machine,
|
||||
interpState: "idle",
|
||||
interpResumeState: "idle",
|
||||
taskPaused: false,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
dispatch({
|
||||
type: "TASK_HAL_PROGRAM_STOPPED",
|
||||
@@ -1500,6 +1603,7 @@ export function createSimulationStore(seed = {}) {
|
||||
machine: {
|
||||
...state.machine,
|
||||
mode: "auto",
|
||||
manualPanel: null,
|
||||
interpResumeState: state.machine.interpState === "paused"
|
||||
? state.machine.interpResumeState
|
||||
: state.machine.interpState,
|
||||
@@ -1568,6 +1672,7 @@ export function createSimulationStore(seed = {}) {
|
||||
machine: {
|
||||
...state.machine,
|
||||
mode: "manual",
|
||||
manualPanel: "manual",
|
||||
allHomed: true,
|
||||
interpState: "idle",
|
||||
interpResumeState: "idle",
|
||||
@@ -1584,6 +1689,7 @@ export function createSimulationStore(seed = {}) {
|
||||
operatorMessage: "task/HAL machine homed",
|
||||
preserveMachine: {
|
||||
...state.machine,
|
||||
manualPanel: "manual",
|
||||
allHomed: true,
|
||||
},
|
||||
}).catch(() => {});
|
||||
@@ -1593,6 +1699,7 @@ export function createSimulationStore(seed = {}) {
|
||||
machine: {
|
||||
...state.machine,
|
||||
mode: "manual",
|
||||
manualPanel: "manual",
|
||||
allHomed: true,
|
||||
interpState: "idle",
|
||||
interpResumeState: "idle",
|
||||
@@ -1623,12 +1730,20 @@ export function createSimulationStore(seed = {}) {
|
||||
});
|
||||
break;
|
||||
}
|
||||
setState({
|
||||
kinsType: action.enabled
|
||||
{
|
||||
const requestedKinsType = action.enabled
|
||||
? tcpKinsTypeForProfile(state.profile)
|
||||
: "identity",
|
||||
: "identity";
|
||||
const gate = gateKinsTypeChange(state, requestedKinsType);
|
||||
if (!gate.allowed) {
|
||||
setState({ operatorMessage: gate.operatorMessage });
|
||||
break;
|
||||
}
|
||||
setState({
|
||||
kinsType: requestedKinsType,
|
||||
rtcpState: action.enabled ? "on" : "off",
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "RESET_VIEW":
|
||||
setState({
|
||||
@@ -1663,20 +1778,19 @@ export function createSimulationStore(seed = {}) {
|
||||
});
|
||||
break;
|
||||
case "SET_KINS_TYPE":
|
||||
if (String(action.kinsType || "").startsWith("tcp-") && !profileSupportsTcp(state.profile)) {
|
||||
{
|
||||
const gate = gateKinsTypeChange(state, action.kinsType);
|
||||
if (!gate.allowed) {
|
||||
setState({ operatorMessage: gate.operatorMessage });
|
||||
break;
|
||||
}
|
||||
setState({
|
||||
kinsType: "identity",
|
||||
rtcpState: "off",
|
||||
operatorMessage: `kinematics ${action.kinsType} blocked: ${state.profile.id} is not TCP capable`,
|
||||
kinsType: action.kinsType,
|
||||
rtcpState: action.kinsType.startsWith("tcp-") ? "on" : "off",
|
||||
operatorMessage: `kinematics ${action.kinsType}`,
|
||||
});
|
||||
break;
|
||||
}
|
||||
setState({
|
||||
kinsType: action.kinsType,
|
||||
rtcpState: action.kinsType.startsWith("tcp-") ? "on" : "off",
|
||||
operatorMessage: `kinematics ${action.kinsType}`,
|
||||
});
|
||||
break;
|
||||
case "ADJUST_OVERRIDE":
|
||||
{
|
||||
const gate = gateLinuxCncTaskAction(state, action);
|
||||
@@ -1852,9 +1966,18 @@ export function createSimulationStore(seed = {}) {
|
||||
asyncFrameRefreshPending: false,
|
||||
operatorMessage,
|
||||
};
|
||||
state = {
|
||||
const taskPolicy = createLinuxCncTaskPolicyStatus(nextState);
|
||||
const derivedState = {
|
||||
...nextState,
|
||||
fullExecutionBoundary: createFullLinuxCncExecutionBoundary(nextState),
|
||||
linuxCncTaskPolicy: taskPolicy,
|
||||
machineProject: createMachineProjectState(nextState),
|
||||
programValidation: createProgramValidationState(nextState),
|
||||
rightSidebarEntrances: createRightSidebarEntranceState(nextState, taskPolicy),
|
||||
};
|
||||
state = {
|
||||
...derivedState,
|
||||
linuxCncParityMatrix: createLinuxCncParityMatrix(derivedState),
|
||||
fullExecutionBoundary: createFullLinuxCncExecutionBoundary(derivedState),
|
||||
};
|
||||
notify();
|
||||
return frame;
|
||||
@@ -2089,6 +2212,7 @@ export function createSimulationStore(seed = {}) {
|
||||
estopActive: false,
|
||||
taskState: "on",
|
||||
mode: "auto",
|
||||
manualPanel: null,
|
||||
allHomed: true,
|
||||
},
|
||||
});
|
||||
@@ -2106,6 +2230,7 @@ export function createSimulationStore(seed = {}) {
|
||||
estopActive: false,
|
||||
taskState: "on",
|
||||
mode: "auto",
|
||||
manualPanel: null,
|
||||
allHomed: true,
|
||||
interpState: "idle",
|
||||
interpResumeState: "idle",
|
||||
@@ -2246,6 +2371,7 @@ export function createSimulationStore(seed = {}) {
|
||||
pendingJogCommand = null,
|
||||
allowFixtureSession = true,
|
||||
preserveMachine = null,
|
||||
preserveAxisPose = null,
|
||||
} = {}) => {
|
||||
if (!state.taskHalRuntime?.loaded) {
|
||||
throw new Error("LinuxCNC task/HAL runtime not attached");
|
||||
@@ -2274,7 +2400,7 @@ export function createSimulationStore(seed = {}) {
|
||||
if (state.taskHalExecutionSequence !== sequence) {
|
||||
return status;
|
||||
}
|
||||
dispatch({ type: "TASK_HAL_STATUS_APPLIED", status, operatorMessage, preserveMachine });
|
||||
dispatch({ type: "TASK_HAL_STATUS_APPLIED", status, operatorMessage, preserveMachine, preserveAxisPose });
|
||||
return status;
|
||||
} catch (error) {
|
||||
dispatch({
|
||||
@@ -2443,6 +2569,236 @@ function defaultLinuxCncGcodeSourceForState(state) {
|
||||
|| null;
|
||||
}
|
||||
|
||||
function createMachineProjectState(state = {}) {
|
||||
const profile = state.profile || {};
|
||||
const profileId = profile.id || state.machineProfile || "unknown";
|
||||
const staging = state.machineFileStaging || {};
|
||||
const save = staging.save || {};
|
||||
const files = Array.isArray(save.files) ? save.files : [];
|
||||
const gcodeFiles = Array.isArray(staging.gcodeFiles) && staging.gcodeFiles.length > 0
|
||||
? staging.gcodeFiles
|
||||
: Array.isArray(save.gcodeFiles)
|
||||
? save.gcodeFiles
|
||||
: listProjectGcodeFiles(save);
|
||||
const configFiles = files
|
||||
.filter((file) => !["demo", "remap"].includes(file.kind))
|
||||
.map(projectFileDescriptor);
|
||||
const iniFile = files.find((file) => file.kind === "ini" && file.sourceRel === profile.iniPath)
|
||||
|| files.find((file) => file.kind === "ini")
|
||||
|| null;
|
||||
const selectedProgram = gcodeFiles.find((file) => file.sourceRel === staging.selectedGcodeSourceRel)
|
||||
|| null;
|
||||
const projectRoot = staging.opfsRoot || `${MACHINE_PROJECT_OPFS_ROOT}/${profileId}`;
|
||||
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-machine-project",
|
||||
profileId,
|
||||
projectRoot,
|
||||
status: staging.status || "not-staged",
|
||||
storageMode: staging.storageMode || save.storageMode || null,
|
||||
storageCapability: staging.storageCapability || save.storageCapability || null,
|
||||
linuxCncIniSourceRel: profile.iniPath || null,
|
||||
linuxCncIniLoaded: state.iniConfigReadiness?.loaded === true,
|
||||
ini: iniFile
|
||||
? {
|
||||
...projectFileDescriptor(iniFile),
|
||||
sourceMatchesProfile: iniFile.sourceRel === profile.iniPath,
|
||||
textMatchesLoadedIni: typeof state.linuxCncIniConfig?.sourceText === "string"
|
||||
? iniFile.text === state.linuxCncIniConfig.sourceText
|
||||
: null,
|
||||
contentBoundary: "linuxcnc_ini_source_text_staged_without_web_rewrite",
|
||||
}
|
||||
: null,
|
||||
configFiles,
|
||||
configFileCount: configFiles.length,
|
||||
gcodeDirectory: `${projectRoot}/configs/sim/axis/vismach/5axis/table-rotary-tilting/demos`,
|
||||
gcodeFiles: gcodeFiles.map(projectFileDescriptor),
|
||||
gcodeFileCount: gcodeFiles.length,
|
||||
selectedProgram: selectedProgram ? projectFileDescriptor(selectedProgram) : null,
|
||||
fileKindCounts: countProjectFileKinds(files),
|
||||
semanticBoundary: "linuxcnc_ini_project_directory_machine_config_and_gcode_files",
|
||||
};
|
||||
}
|
||||
|
||||
function createProgramValidationState(state = {}) {
|
||||
const execution = state.programExecution || null;
|
||||
const summary = execution?.summary || {};
|
||||
const motion = Array.isArray(execution?.motion) ? execution.motion : [];
|
||||
const selectedSourceRel = state.machineFileStaging?.selectedGcodeSourceRel || state.programSourceRel || null;
|
||||
const linuxCncFiveAxisSource = isLinuxCncFiveAxisDemoSource(selectedSourceRel);
|
||||
const motionEventCount = Number(summary.motionEventCount ?? motion.length ?? 0);
|
||||
const canonicalEventCount = Number(summary.canonicalEventCount ?? 0);
|
||||
const switchkinsEventCount = Number(summary.switchkinsEventCount ?? 0);
|
||||
const timing = state.programExecutionTiming || execution?.plannerTiming || null;
|
||||
const plannerSamples = Array.isArray(execution?.plannerTiming?.samples)
|
||||
? execution.plannerTiming.samples
|
||||
: [];
|
||||
const sampleCount = Array.isArray(timing?.samples) && timing.samples.length > 0
|
||||
? timing.samples.length
|
||||
: plannerSamples.length;
|
||||
const ready = Boolean(summary.ready && motionEventCount > 0);
|
||||
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-program-validation",
|
||||
status: state.interpreterExecutionPending
|
||||
? "validating"
|
||||
: ready
|
||||
? "validated"
|
||||
: "pending",
|
||||
ready,
|
||||
activeProgram: state.activeProgram || null,
|
||||
programSource: state.programSource || null,
|
||||
programSourceRel: selectedSourceRel,
|
||||
sourceGuard: linuxCncFiveAxisSource
|
||||
? "linuxcnc_vendored_5axis_gcode_source_file"
|
||||
: state.programSource === "operator-file"
|
||||
? "operator_file_not_promoted_to_linuxcnc_source"
|
||||
: "fixture_or_mdi_program_not_linuxcnc_source",
|
||||
previewSource: execution?.sourceMode === "linuxcnc-interpreter-wasm"
|
||||
? "linuxcnc_interpreter_canonical_motion"
|
||||
: execution?.sourceMode || state.programExecutionSourceMode || "fixture-line-playback",
|
||||
executionTraceSource: state.programRuntimeFeedback?.sourceMode
|
||||
|| (plannerSamples.length > 0 ? "linuxcnc_tp_samples" : state.programExecutionSourceMode),
|
||||
motionEventCount,
|
||||
canonicalEventCount,
|
||||
switchkinsEventCount,
|
||||
plannerSampleCount: sampleCount,
|
||||
plannerRuntimeReady: execution?.plannerTiming?.plannerRuntimeReady === true || timing?.plannerRuntimeReady === true,
|
||||
remapRuntimeReady: summary.remapRuntimeReady === true,
|
||||
currentLine: Number(state.programRuntimeFeedback?.line || state.activeLine || 0),
|
||||
currentMotionIndex: Number(state.programExecutionMotionIndex || 0),
|
||||
currentSampleIndex: Number(state.programExecutionSampleIndex || 0),
|
||||
axisFeedbackSource: state.programRuntimeFeedback?.sourceMode || state.programExecutionSourceMode || "ui-state",
|
||||
realtimeAxisValues: {
|
||||
...pickExecutionAxes(state.axisPose || {}),
|
||||
tcpX: Number(state.tcpPose?.x || 0),
|
||||
tcpY: Number(state.tcpPose?.y || 0),
|
||||
tcpZ: Number(state.tcpPose?.z || 0),
|
||||
},
|
||||
rtcpState: state.rtcpState || "off",
|
||||
kinsType: state.kinsType || "identity",
|
||||
semanticBoundary: "program_validation_consumes_linuxcnc_interpreter_tp_and_task_hal_feedback",
|
||||
};
|
||||
}
|
||||
|
||||
function createRightSidebarEntranceState(state = {}, taskPolicy = createLinuxCncTaskPolicyStatus(state)) {
|
||||
const manualActive = state.machine?.mode === "manual" && state.machine?.manualPanel !== "jog";
|
||||
const jogActive = state.machine?.mode === "manual" && state.machine?.manualPanel === "jog";
|
||||
const tcpKinsType = tcpKinsTypeForProfile(state.profile);
|
||||
const powerGate = gateLinuxCncTaskAction(state, { type: "TOGGLE_POWER" });
|
||||
const autoGate = gateLinuxCncTaskAction(state, { type: "SET_MODE", mode: "auto" });
|
||||
const manualGate = gateLinuxCncTaskAction(state, { type: "SET_MODE", mode: "manual" });
|
||||
const jogGate = gateLinuxCncTaskAction(state, { type: "SET_MODE", mode: "jog" });
|
||||
const mdiGate = gateLinuxCncTaskAction(state, { type: "SET_MODE", mode: "mdi" });
|
||||
const identityGate = gateKinsTypeChange(state, "identity");
|
||||
const tcpGate = tcpKinsType
|
||||
? gateKinsTypeChange(state, tcpKinsType)
|
||||
: blockKinsChange(taskPolicy, `TCP blocked: ${state.profile?.id || "profile"} has no TCP switchkins type`);
|
||||
|
||||
return [
|
||||
sidebarEntry({ id: "estop", label: "E-STOP", active: Boolean(state.machine?.estopActive), gate: allowKinsChange(taskPolicy), tone: "emergency" }),
|
||||
sidebarEntry({ id: "power", label: "POWER", active: Boolean(state.machine?.powerOn), gate: powerGate, tone: "power" }),
|
||||
sidebarEntry({ id: "reset", label: "RESET", active: false, gate: allowKinsChange(taskPolicy), tone: state.machine?.estopActive ? "reset-required" : "reset" }),
|
||||
sidebarEntry({ id: "auto", label: "AUTO", active: state.machine?.mode === "auto", gate: autoGate, tone: "mode" }),
|
||||
sidebarEntry({ id: "manual", label: "MANUAL", active: manualActive, gate: manualGate, tone: "mode" }),
|
||||
sidebarEntry({ id: "jog", label: "JOG", active: jogActive, gate: jogGate, tone: "mode" }),
|
||||
sidebarEntry({ id: "mdi", label: "MDI", active: state.machine?.mode === "mdi", gate: mdiGate, tone: "mode" }),
|
||||
sidebarEntry({ id: "identity", label: "IDENTITY", active: state.kinsType === "identity", gate: identityGate, tone: "identity" }),
|
||||
sidebarEntry({ id: "tcp", label: "TCP", active: String(state.kinsType || "").startsWith("tcp-"), gate: tcpGate, tone: "tcp" }),
|
||||
];
|
||||
}
|
||||
|
||||
function sidebarEntry({ id, label, active, gate, tone }) {
|
||||
const allowed = gate?.allowed !== false;
|
||||
const status = id === "estop" && active
|
||||
? "emergency"
|
||||
: active && !allowed
|
||||
? "active-blocked"
|
||||
: active
|
||||
? "active"
|
||||
: allowed
|
||||
? "ready"
|
||||
: "blocked";
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-right-sidebar-entry",
|
||||
id,
|
||||
label,
|
||||
active: Boolean(active),
|
||||
allowed,
|
||||
status,
|
||||
tone,
|
||||
operatorMessage: gate?.operatorMessage || null,
|
||||
colorRule: `${tone}:${status}`,
|
||||
semanticBoundary: "gmoccapy_right_vertical_button_task_state_mode_gate",
|
||||
};
|
||||
}
|
||||
|
||||
function gateKinsTypeChange(state = {}, requestedKinsType = "identity") {
|
||||
const taskPolicy = createLinuxCncTaskPolicyStatus(state);
|
||||
const target = String(requestedKinsType || "");
|
||||
if (!target) {
|
||||
return blockKinsChange(taskPolicy, "kinematics blocked: missing switchkins target");
|
||||
}
|
||||
if (target.startsWith("tcp-") && !profileSupportsTcp(state.profile)) {
|
||||
return blockKinsChange(taskPolicy, `kinematics ${target} blocked: ${state.profile?.id || "profile"} is not TCP capable`);
|
||||
}
|
||||
if (taskPolicy.taskState === "estop") {
|
||||
return blockKinsChange(taskPolicy, "kinematics blocked: reset estop first");
|
||||
}
|
||||
if (taskPolicy.taskState !== "on") {
|
||||
return blockKinsChange(taskPolicy, "kinematics blocked: machine must be on");
|
||||
}
|
||||
if (taskPolicy.interpState === "reading" || taskPolicy.interpState === "waiting" || state.runState === "running") {
|
||||
return blockKinsChange(taskPolicy, "kinematics blocked: interpreter must be idle");
|
||||
}
|
||||
return allowKinsChange(taskPolicy);
|
||||
}
|
||||
|
||||
function allowKinsChange(status) {
|
||||
return {
|
||||
allowed: true,
|
||||
status,
|
||||
operatorMessage: null,
|
||||
};
|
||||
}
|
||||
|
||||
function blockKinsChange(status, operatorMessage) {
|
||||
return {
|
||||
allowed: false,
|
||||
status,
|
||||
operatorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
function projectFileDescriptor(file = {}) {
|
||||
return {
|
||||
sourceRel: file.sourceRel || null,
|
||||
filename: sourceBasename(file.sourceRel || file.path || file.wasmPath || ""),
|
||||
opfsPath: file.opfsPath || null,
|
||||
wasmPath: file.wasmPath || file.path || null,
|
||||
kind: file.kind || "asset",
|
||||
bytes: Number(file.bytes || (typeof file.text === "string" ? file.text.length : 0)),
|
||||
semanticBoundary: file.semanticBoundary || "linuxcnc_project_file_staged_without_web_rewrite",
|
||||
};
|
||||
}
|
||||
|
||||
function countProjectFileKinds(files = []) {
|
||||
return files.reduce((counts, file) => {
|
||||
const kind = file.kind || "asset";
|
||||
counts[kind] = (counts[kind] || 0) + 1;
|
||||
return counts;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function isLinuxCncFiveAxisDemoSource(sourceRel) {
|
||||
return String(sourceRel || "").startsWith("configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/")
|
||||
&& String(sourceRel || "").endsWith(".ngc");
|
||||
}
|
||||
|
||||
function sourceBasename(path) {
|
||||
return String(path || "").split("/").filter(Boolean).at(-1) || "";
|
||||
}
|
||||
|
||||
function isAsyncKinematicsRuntime(runtime) {
|
||||
return runtime?.executionContext === "worker";
|
||||
}
|
||||
@@ -2601,6 +2957,7 @@ function normalizeCoordinates(value) {
|
||||
function applyTaskHalStatusPatch(state, status, operatorMessage, {
|
||||
loopSequence = null,
|
||||
preserveMachine = null,
|
||||
preserveAxisPose = null,
|
||||
} = {}) {
|
||||
const ui = status?.ui || {};
|
||||
const task = status?.task || {};
|
||||
@@ -2610,11 +2967,18 @@ function applyTaskHalStatusPatch(state, status, operatorMessage, {
|
||||
? "on"
|
||||
: rawTaskState;
|
||||
const taskMode = normalizeLinuxCncTaskMode(preserveMachine?.mode || ui.taskMode || task.mode || state.machine.mode);
|
||||
const interpState = normalizeTaskHalInterpState(ui.interpState || task.interpState);
|
||||
const manualPanel = taskMode === "manual"
|
||||
? (preserveMachine?.manualPanel || state.machine.manualPanel || "manual")
|
||||
: null;
|
||||
const interpState = Object.hasOwn(preserveMachine || {}, "interpState")
|
||||
? normalizeTaskHalInterpState(preserveMachine.interpState)
|
||||
: normalizeTaskHalInterpState(ui.interpState || task.interpState);
|
||||
const allHomed = Boolean(preserveMachine?.allHomed ?? state.machine.allHomed);
|
||||
const activeLine = state.programStartLine + Math.max(Number(ui.activeLine || 1) - 1, 0);
|
||||
const kinsType = resolveTaskHalKinsType(state, status, activeLine);
|
||||
const axisPose = resolveTaskHalAxisPose(state, status);
|
||||
const axisPose = preserveAxisPose
|
||||
? clampAxisPoseToProfile({ ...state.axisPose, ...preserveAxisPose }, state.profile)
|
||||
: resolveTaskHalAxisPose(state, status);
|
||||
const currentVelocity = Number.isFinite(ui.currentVelocity)
|
||||
? Math.max(ui.currentVelocity, 0)
|
||||
: state.feed.currentVelocity;
|
||||
@@ -2664,6 +3028,7 @@ function applyTaskHalStatusPatch(state, status, operatorMessage, {
|
||||
estopActive: taskState === "estop",
|
||||
taskState,
|
||||
mode: taskMode,
|
||||
manualPanel,
|
||||
interpState,
|
||||
interpResumeState: paused ? state.machine.interpResumeState || "reading" : interpState,
|
||||
taskPaused: paused,
|
||||
@@ -2778,6 +3143,14 @@ function resolveTaskHalAxisPose(state, status) {
|
||||
return state.axisPose;
|
||||
}
|
||||
|
||||
if (state.pendingJogCommand && (ui.axisPoseFrame === "task-local" || isJogStatus(status))) {
|
||||
const { axis, direction, increment, basePose } = state.pendingJogCommand;
|
||||
return clampAxisPoseToProfile({
|
||||
...basePose,
|
||||
[axis]: Number(basePose?.[axis] || 0) + direction * increment,
|
||||
}, state.profile);
|
||||
}
|
||||
|
||||
if (ui.axisPoseFrame === "work") {
|
||||
return clampAxisPoseToProfile({ ...state.axisPose, ...axisPose }, state.profile);
|
||||
}
|
||||
@@ -2786,14 +3159,6 @@ function resolveTaskHalAxisPose(state, status) {
|
||||
return addAxisDelta(state.axisPose, ui.axisPoseDelta, state.profile);
|
||||
}
|
||||
|
||||
if (state.pendingJogCommand && isJogStatus(status)) {
|
||||
const { axis, direction, increment, basePose } = state.pendingJogCommand;
|
||||
return clampAxisPoseToProfile({
|
||||
...basePose,
|
||||
[axis]: Number(basePose?.[axis] || 0) + direction * increment,
|
||||
}, state.profile);
|
||||
}
|
||||
|
||||
if (!ui.axisPoseFrame && wouldResetNonZeroPoseToLocalZero(state.axisPose, axisPose)) {
|
||||
return state.axisPose;
|
||||
}
|
||||
@@ -2925,11 +3290,15 @@ function normalizeMachineForLinuxCncTask(machine = {}, runState = "idle") {
|
||||
const mode = normalizeLinuxCncTaskMode(machine.mode);
|
||||
const interpState = machine.interpState
|
||||
|| (runState === "running" ? "reading" : runState === "paused" || runState === "stepping" ? "paused" : "idle");
|
||||
const manualPanel = mode === "manual"
|
||||
? (machine.manualPanel === "jog" ? "jog" : "manual")
|
||||
: null;
|
||||
|
||||
return {
|
||||
...machine,
|
||||
taskState,
|
||||
mode,
|
||||
manualPanel,
|
||||
interpState,
|
||||
interpResumeState: machine.interpResumeState || (interpState === "paused" ? "reading" : interpState),
|
||||
taskPaused: Boolean(machine.taskPaused || interpState === "paused"),
|
||||
@@ -3007,6 +3376,7 @@ function executeMdiCommand(state, rawCommand) {
|
||||
const nextMachine = {
|
||||
...state.machine,
|
||||
mode: "mdi",
|
||||
manualPanel: null,
|
||||
mdiCommand: command,
|
||||
mdiDistanceMode: distanceMode,
|
||||
interpState: "idle",
|
||||
|
||||
@@ -415,7 +415,7 @@ button[data-icon-name] span {
|
||||
.gcode-panel {
|
||||
grid-area: gcode;
|
||||
display: grid;
|
||||
grid-template-rows: auto auto minmax(96px, 1fr) 20px 54px;
|
||||
grid-template-rows: auto auto auto minmax(96px, 1fr) 20px 54px;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
@@ -468,6 +468,31 @@ button[data-icon-name] span {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.machine-project-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(170px, 1.1fr) minmax(150px, 0.9fr) minmax(150px, 0.8fr) minmax(190px, 1.2fr);
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
padding: 4px 8px;
|
||||
border-bottom: 1px solid #d5d0c7;
|
||||
background: #ebe7df;
|
||||
color: #3a3732;
|
||||
font: 11px/1.2 "Courier New", monospace;
|
||||
}
|
||||
|
||||
.machine-project-row span {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.machine-project-row [data-program-validation="summary"] {
|
||||
color: #005bab;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.gcode-header strong,
|
||||
.gcode-header span {
|
||||
min-width: 0;
|
||||
@@ -659,12 +684,58 @@ button[data-icon-name] span {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.sidebar-button[data-entry-status="blocked"],
|
||||
.sidebar-button[data-entry-status="active-blocked"] {
|
||||
border-color: #a9a49b;
|
||||
background: linear-gradient(#e1ded7, #cac5bb);
|
||||
color: #69645d;
|
||||
opacity: 0.78;
|
||||
}
|
||||
|
||||
.sidebar-button[data-active="true"] {
|
||||
border-color: #278b37;
|
||||
background: linear-gradient(#f6fff5, #aee9ae);
|
||||
color: #075f16;
|
||||
}
|
||||
|
||||
.sidebar-button[data-entry-tone="emergency"][data-entry-status="emergency"],
|
||||
.sidebar-button.estop[data-active="true"] {
|
||||
border-color: #8e0000;
|
||||
background: linear-gradient(#ffdfd8, #ff675b);
|
||||
color: #5f0000;
|
||||
}
|
||||
|
||||
.sidebar-button[data-entry-tone="power"][data-active="true"] {
|
||||
border-color: #1e7b2c;
|
||||
background: linear-gradient(#e7ffe7, #58e364);
|
||||
color: #064e12;
|
||||
}
|
||||
|
||||
.sidebar-button[data-entry-tone="reset"],
|
||||
.sidebar-button[data-entry-tone="reset-required"] {
|
||||
border-color: #a8740b;
|
||||
background: linear-gradient(#fff7d4, #efc45c);
|
||||
color: #5a3b00;
|
||||
}
|
||||
|
||||
.sidebar-button[data-entry-tone="tcp"][data-active="true"] {
|
||||
border-color: #14647a;
|
||||
background: linear-gradient(#e4fbff, #68d5e6);
|
||||
color: #073f4d;
|
||||
}
|
||||
|
||||
.sidebar-button[data-entry-tone="identity"][data-active="true"] {
|
||||
border-color: #66625a;
|
||||
background: linear-gradient(#f6f4ee, #d1ccc2);
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.sidebar-button[data-entry-status="active-blocked"][data-active="true"] {
|
||||
border-color: #a9a49b;
|
||||
background: linear-gradient(#e1ded7, #cac5bb);
|
||||
color: #69645d;
|
||||
}
|
||||
|
||||
.status-sidebar time {
|
||||
padding: 4px 0;
|
||||
text-align: center;
|
||||
@@ -992,6 +1063,10 @@ button[data-icon-name] span {
|
||||
grid-template-columns: auto minmax(120px, 1fr);
|
||||
}
|
||||
|
||||
.machine-project-row {
|
||||
grid-template-columns: minmax(130px, 1fr) minmax(120px, 0.9fr) minmax(120px, 0.8fr) minmax(140px, 1fr);
|
||||
}
|
||||
|
||||
.tabs button,
|
||||
.meter-card h2,
|
||||
.override-control h2,
|
||||
@@ -1013,7 +1088,7 @@ button[data-icon-name] span {
|
||||
}
|
||||
|
||||
.gcode-panel {
|
||||
grid-template-rows: auto auto minmax(0, 1fr) 18px 48px;
|
||||
grid-template-rows: auto auto auto minmax(0, 1fr) 18px 48px;
|
||||
}
|
||||
|
||||
.gcode-list {
|
||||
@@ -1109,6 +1184,10 @@ button[data-icon-name] span {
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.machine-project-row {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.gcode-list {
|
||||
padding: 2px 4px;
|
||||
font-size: 8px;
|
||||
|
||||
@@ -63,32 +63,65 @@ function render(regions, state, dispatch) {
|
||||
|
||||
function renderTitlebar(element, state) {
|
||||
const currentLine = currentGcodeExecutionLine(state);
|
||||
element.innerHTML = `
|
||||
<div class="brand-dot" aria-hidden="true">NS</div>
|
||||
<div class="title-stack">
|
||||
<strong>gmoccapy Web 5 Axis for LinuxCNC RTCP Simulation</strong>
|
||||
<span>${state.machineProfile} | ${state.sessionName} | ${state.sourceMode} | ${state.machine.mode}</span>
|
||||
</div>
|
||||
<label class="profile-select-label">
|
||||
Profile
|
||||
<select data-action="select-profile">
|
||||
${state.availableProfiles.map((profile) => `
|
||||
<option value="${profile.id}" ${profile.id === state.machineProfile ? "selected" : ""}>${profile.coordinates} ${profile.id}</option>
|
||||
`).join("")}
|
||||
</select>
|
||||
</label>
|
||||
<div class="current-line-indicator" data-current-gcode-line="${currentLine}" data-current-gcode-source="${escapeHtml(currentGcodeLineSource(state))}">
|
||||
<span>G-code line</span>
|
||||
<strong>${currentLine}</strong>
|
||||
</div>
|
||||
<div class="run-state" data-run-state="${state.runState}">${state.runState}</div>
|
||||
`;
|
||||
element.querySelector('[data-action="select-profile"]').addEventListener("change", (event) => {
|
||||
element.dispatchEvent(new CustomEvent("profile-change", {
|
||||
bubbles: true,
|
||||
detail: { profileId: event.target.value },
|
||||
}));
|
||||
});
|
||||
|
||||
if (element.dataset.titlebarMounted !== "true") {
|
||||
element.innerHTML = `
|
||||
<div class="brand-dot" aria-hidden="true">NS</div>
|
||||
<div class="title-stack">
|
||||
<strong>gmoccapy Web 5 Axis for LinuxCNC RTCP Simulation</strong>
|
||||
<span data-titlebar-meta></span>
|
||||
</div>
|
||||
<label class="profile-select-label">
|
||||
Profile
|
||||
<select data-action="select-profile"></select>
|
||||
</label>
|
||||
<div class="current-line-indicator" data-titlebar-current-line>
|
||||
<span>G-code line</span>
|
||||
<strong></strong>
|
||||
</div>
|
||||
<div class="run-state"></div>
|
||||
`;
|
||||
element.querySelector('[data-action="select-profile"]').addEventListener("change", (event) => {
|
||||
element.dispatchEvent(new CustomEvent("profile-change", {
|
||||
bubbles: true,
|
||||
detail: { profileId: event.target.value },
|
||||
}));
|
||||
});
|
||||
element.dataset.titlebarMounted = "true";
|
||||
}
|
||||
|
||||
setText(
|
||||
element,
|
||||
"[data-titlebar-meta]",
|
||||
`${state.machineProfile} | ${state.sessionName} | ${state.sourceMode} | ${state.machine.mode}`,
|
||||
);
|
||||
|
||||
const profileSelect = element.querySelector('[data-action="select-profile"]');
|
||||
const profileSignature = state.availableProfiles
|
||||
.map((profile) => `${profile.id}:${profile.coordinates}`)
|
||||
.join("|");
|
||||
if (profileSelect.dataset.profileSignature !== profileSignature) {
|
||||
profileSelect.innerHTML = state.availableProfiles.map((profile) => `
|
||||
<option value="${profile.id}">${profile.coordinates} ${profile.id}</option>
|
||||
`).join("");
|
||||
profileSelect.dataset.profileSignature = profileSignature;
|
||||
}
|
||||
if (profileSelect.value !== state.machineProfile) {
|
||||
profileSelect.value = state.machineProfile;
|
||||
}
|
||||
|
||||
const currentLineIndicator = element.querySelector("[data-titlebar-current-line]");
|
||||
if (currentLineIndicator) {
|
||||
currentLineIndicator.dataset.currentGcodeLine = String(currentLine);
|
||||
currentLineIndicator.dataset.currentGcodeSource = currentGcodeLineSource(state);
|
||||
setText(currentLineIndicator, "strong", currentLine);
|
||||
}
|
||||
|
||||
const runState = element.querySelector(".run-state");
|
||||
if (runState) {
|
||||
runState.dataset.runState = state.runState;
|
||||
runState.textContent = state.runState;
|
||||
}
|
||||
}
|
||||
|
||||
function renderPreview(element, state, dispatch) {
|
||||
@@ -205,10 +238,18 @@ function renderGcode(element, state, dispatch) {
|
||||
const lineNumber = state.programStartLine + index;
|
||||
const active = lineNumber === currentLine ? " active" : "";
|
||||
const status = active ? "running" : lineNumber < currentLine ? "done" : "pending";
|
||||
const execution = lineExecutionForRow(state, lineNumber, currentLine, status);
|
||||
const executionText = formatGcodeLineExecution(execution, status);
|
||||
return `
|
||||
<li class="gcode-row${active}" data-program-line="${lineNumber}" data-line-status="${escapeHtml(status)}">
|
||||
<li
|
||||
class="gcode-row${active}"
|
||||
data-program-line="${lineNumber}"
|
||||
data-line-status="${escapeHtml(status)}"
|
||||
data-line-source="${escapeHtml(execution?.source || "")}"
|
||||
>
|
||||
<span>${lineNumber}</span>
|
||||
<code>${escapeHtml(line)}</code>
|
||||
${executionText ? `<small data-line-execution="${lineNumber}">${escapeHtml(executionText)}</small>` : ""}
|
||||
</li>
|
||||
`;
|
||||
})
|
||||
@@ -236,6 +277,12 @@ function renderGcode(element, state, dispatch) {
|
||||
<button type="button" data-action="stage-linuxcnc-sources">Stage</button>
|
||||
<span data-linuxcnc-gcode-source="status">${formatLinuxCncGcodeSourceStatus(state)}</span>
|
||||
</div>
|
||||
<div class="machine-project-row" data-machine-project-root="${escapeHtml(state.machineProject?.projectRoot || "")}">
|
||||
<span data-machine-project="root">${escapeHtml(formatMachineProjectRoot(state))}</span>
|
||||
<span data-machine-project="config">${escapeHtml(formatMachineProjectConfig(state))}</span>
|
||||
<span data-machine-project="gcode">${escapeHtml(formatMachineProjectGcode(state))}</span>
|
||||
<span data-program-validation="summary">${escapeHtml(formatProgramValidationSummary(state))}</span>
|
||||
</div>
|
||||
<ol class="gcode-list" start="${state.programStartLine}">${rows}</ol>
|
||||
<div class="gcode-progress">
|
||||
<span>${currentLine} / ${programEndLine}</span>
|
||||
@@ -309,6 +356,38 @@ function currentGcodeLineSource(state) {
|
||||
return state.programRuntimeFeedback?.sourceMode || state.programExecutionSourceMode || "ui-state";
|
||||
}
|
||||
|
||||
function lineExecutionForRow(state, lineNumber, currentLine, status) {
|
||||
const recorded = state.programLineExecution?.[lineNumber];
|
||||
if (recorded) return recorded;
|
||||
|
||||
const feedback = state.programRuntimeFeedback;
|
||||
if (lineNumber !== currentLine || !feedback) return null;
|
||||
|
||||
return {
|
||||
status,
|
||||
source: feedback.sourceMode || state.programExecutionSourceMode || "runtime-feedback",
|
||||
line: lineNumber,
|
||||
feed: Number(feedback.currentVelocityMmPerMin || 0),
|
||||
requestedFeed: Number(feedback.requestedVelocityMmPerMin || 0),
|
||||
feedMode: feedback.feedMode || null,
|
||||
taskCycle: Number(feedback.taskCycle || 0),
|
||||
servoCycle: Number(feedback.cycle || 0),
|
||||
sampleIndex: Number(feedback.sampleIndex || 0),
|
||||
motionIndex: Number(feedback.motionIndex || 0),
|
||||
};
|
||||
}
|
||||
|
||||
function formatGcodeLineExecution(execution, fallbackStatus) {
|
||||
if (!execution) return "";
|
||||
const status = execution.status || fallbackStatus || "running";
|
||||
const source = execution.source || "unknown";
|
||||
const feed = Number(execution.feed || execution.currentVelocityMmPerMin || 0);
|
||||
const taskCycle = Number(execution.taskCycle || 0);
|
||||
const servoCycle = Number(execution.servoCycle || 0);
|
||||
const sampleIndex = Number(execution.sampleIndex || 0);
|
||||
return `${status} | F ${formatNumber(feed, 1)} | cycle ${taskCycle}/${servoCycle} | sample ${sampleIndex} | ${source}`;
|
||||
}
|
||||
|
||||
function renderLinuxCncGcodeSourceOptions(state) {
|
||||
const sources = state.machineFileStaging.gcodeSources || [];
|
||||
if (sources.length === 0) {
|
||||
@@ -332,6 +411,79 @@ function formatLinuxCncGcodeSourceStatus(state) {
|
||||
return `${sources.length} staged / ${selected}`;
|
||||
}
|
||||
|
||||
function formatMachineProjectRoot(state) {
|
||||
const project = state.machineProject;
|
||||
if (!project) return "Project pending";
|
||||
return `Project ${project.profileId} ${project.status} ${project.storageMode || "storage-pending"} ${project.projectRoot}`;
|
||||
}
|
||||
|
||||
function formatMachineProjectConfig(state) {
|
||||
const project = state.machineProject;
|
||||
if (!project?.ini) return `INI pending ${state.profile?.iniPath || "-"}`;
|
||||
const match = project.ini.sourceMatchesProfile ? "INI match" : "INI mismatch";
|
||||
const textMatch = project.ini.textMatchesLoadedIni === true
|
||||
? "text loaded"
|
||||
: project.ini.textMatchesLoadedIni === false
|
||||
? "text differs"
|
||||
: "text pending";
|
||||
return `${match} ${textMatch} ${project.ini.filename}`;
|
||||
}
|
||||
|
||||
function formatMachineProjectGcode(state) {
|
||||
const project = state.machineProject;
|
||||
if (!project) return "G-code pending";
|
||||
const selected = project.selectedProgram?.filename || "none";
|
||||
return `${project.gcodeFileCount} G-code/remap files selected ${selected}`;
|
||||
}
|
||||
|
||||
function formatProgramValidationSummary(state) {
|
||||
const validation = state.programValidation;
|
||||
if (!validation) return "validation pending";
|
||||
return `${validation.status} ${validation.motionEventCount} motion ${validation.plannerSampleCount} samples ${validation.sourceGuard}`;
|
||||
}
|
||||
|
||||
function formatLinuxCncParitySummary(matrix) {
|
||||
if (!matrix) return "pending";
|
||||
return `${matrix.status} ${matrix.implementedCount}/${matrix.itemCount} items, ${matrix.activeCount} active, ${matrix.sourcePathCount} source paths, ${matrix.linuxCncProgramCount} programs`;
|
||||
}
|
||||
|
||||
function formatLinuxCncParityActive(matrix) {
|
||||
if (!matrix) return "pending";
|
||||
const active = matrix.items
|
||||
.filter((item) => item.active)
|
||||
.map((item) => item.id);
|
||||
return active.length > 0 ? active.slice(0, 6).join(" / ") : "no active parity item yet";
|
||||
}
|
||||
|
||||
function formatLinuxCncParitySources(matrix) {
|
||||
if (!matrix) return "pending";
|
||||
return matrix.linuxCncPrograms
|
||||
.filter((path) => (
|
||||
path.includes("xyzac-trt")
|
||||
|| path.includes("xyzbc-trt")
|
||||
|| path.includes("gmoccapy")
|
||||
|| path.endsWith(".ngc")
|
||||
))
|
||||
.slice(0, 5)
|
||||
.join(" | ");
|
||||
}
|
||||
|
||||
function formatLinuxCncParityFunctions(matrix) {
|
||||
if (!matrix) return "pending";
|
||||
return matrix.linuxCncFunctions
|
||||
.filter((entry) => (
|
||||
entry.includes("E-STOP")
|
||||
|| entry.includes("M428")
|
||||
|| entry.includes("M429")
|
||||
|| entry.includes("TCP")
|
||||
|| entry.includes("canonical")
|
||||
|| entry.includes("DRO")
|
||||
|| entry.includes("INI")
|
||||
))
|
||||
.slice(0, 8)
|
||||
.join(" / ");
|
||||
}
|
||||
|
||||
function mdiQuickCommands(state) {
|
||||
const profileCommands = state.profile.kinematicsParameters.switchkinsTypes
|
||||
.map((type) => type.mdiCommand)
|
||||
@@ -346,22 +498,82 @@ function mdiQuickCommands(state) {
|
||||
].filter((command, index, commands) => command && commands.indexOf(command) === index).slice(0, 7);
|
||||
}
|
||||
|
||||
function rightSidebarEntryMap(state) {
|
||||
const entries = state.rightSidebarEntrances || [];
|
||||
return Object.fromEntries(entries.map((entry) => [entry.id, entry]));
|
||||
}
|
||||
|
||||
function sidebarEntryOr(entries, id, label, tone = "mode") {
|
||||
return entries[id] || {
|
||||
id,
|
||||
label,
|
||||
active: false,
|
||||
allowed: true,
|
||||
status: "ready",
|
||||
tone,
|
||||
colorRule: `${tone}:ready`,
|
||||
operatorMessage: null,
|
||||
};
|
||||
}
|
||||
|
||||
function entryGate(entry) {
|
||||
return {
|
||||
allowed: entry?.allowed !== false,
|
||||
operatorMessage: entry?.operatorMessage || null,
|
||||
};
|
||||
}
|
||||
|
||||
function sidebarEntryAttrs(entry) {
|
||||
if (!entry) return "";
|
||||
return [
|
||||
`data-machine-entry="${escapeHtml(entry.id)}"`,
|
||||
`data-entry-status="${escapeHtml(entry.status)}"`,
|
||||
`data-entry-tone="${escapeHtml(entry.tone)}"`,
|
||||
`data-entry-color-rule="${escapeHtml(entry.colorRule)}"`,
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
function sidebarTextButtonHtml(entry, { action, title = null } = {}) {
|
||||
const gate = entryGate(entry);
|
||||
const disabledAttr = gate.allowed ? "" : " disabled";
|
||||
const titleText = title || entry?.operatorMessage || entry?.label || action;
|
||||
return `
|
||||
<button
|
||||
type="button"
|
||||
class="sidebar-button"
|
||||
data-action="${escapeHtml(action)}"
|
||||
data-active="${entry?.active ? "true" : "false"}"
|
||||
data-command-ready="${gate.allowed ? "true" : "false"}"
|
||||
aria-disabled="${gate.allowed ? "false" : "true"}"
|
||||
title="${escapeHtml(titleText)}"
|
||||
${disabledAttr}
|
||||
${sidebarEntryAttrs(entry)}
|
||||
>${escapeHtml(entry?.label || action)}</button>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderSidebar(element, state, dispatch) {
|
||||
const tcpKinsType = tcpKinsTypeForProfile(state.profile);
|
||||
const tcpDisabled = tcpKinsType ? "" : ` disabled title="${escapeHtml(`${state.profile.id} is ${state.profile.kinematics} reference only`)}"`;
|
||||
const autoGate = gateLinuxCncTaskAction(state, { type: "SET_MODE", mode: "auto" });
|
||||
const manualGate = gateLinuxCncTaskAction(state, { type: "SET_MODE", mode: "manual" });
|
||||
const mdiGate = gateLinuxCncTaskAction(state, { type: "SET_MODE", mode: "mdi" });
|
||||
const entries = rightSidebarEntryMap(state);
|
||||
const estopEntry = sidebarEntryOr(entries, "estop", "E-STOP", "emergency");
|
||||
const powerEntry = sidebarEntryOr(entries, "power", "POWER", "power");
|
||||
const resetEntry = sidebarEntryOr(entries, "reset", "RESET", "reset");
|
||||
const autoEntry = sidebarEntryOr(entries, "auto", "AUTO", "mode");
|
||||
const manualEntry = sidebarEntryOr(entries, "manual", "MANUAL", "mode");
|
||||
const jogEntry = sidebarEntryOr(entries, "jog", "JOG", "mode");
|
||||
const mdiEntry = sidebarEntryOr(entries, "mdi", "MDI", "mode");
|
||||
const identityEntry = sidebarEntryOr(entries, "identity", "IDENTITY", "identity");
|
||||
const tcpEntry = sidebarEntryOr(entries, "tcp", "TCP", "tcp");
|
||||
element.innerHTML = `
|
||||
${iconButtonHtml({ buttonId: "tbtn_estop", action: "estop", label: "E-STOP", title: "Estop the machine / F1", className: "sidebar-button estop", active: state.machine.estopActive, attrs: nativeButtonAttrs("v", 0) })}
|
||||
${iconButtonHtml({ buttonId: "tbtn_on", action: "power", label: "POWER", title: "Turn the machine on/off / F2", className: "sidebar-button power", active: state.machine.powerOn, attrs: nativeButtonAttrs("v", 1) })}
|
||||
${iconButtonHtml({ buttonId: "tbtn_setup", action: "reset", label: "RESET", title: "Reset estop and return machine off", className: "sidebar-button", active: false })}
|
||||
${iconButtonHtml({ buttonId: "rbt_auto", action: "mode-auto", label: "AUTO", title: autoGate.operatorMessage || "Enter auto mode", className: "sidebar-button", active: state.machine.mode === "auto", gate: autoGate, attrs: nativeButtonAttrs("v", 4) })}
|
||||
${iconButtonHtml({ buttonId: "rbt_manual", action: "mode-manual", label: "MANUAL", title: manualGate.operatorMessage || "Enter manual mode", className: "sidebar-button", active: state.machine.mode === "manual", gate: manualGate, attrs: nativeButtonAttrs("v", 2) })}
|
||||
${iconButtonHtml({ buttonId: "tbtn_user_tabs", action: "mode-jog", label: "JOG", title: manualGate.operatorMessage || "Manual jog page", className: "sidebar-button", active: state.machine.mode === "manual", gate: manualGate, attrs: nativeButtonAttrs("v", 5) })}
|
||||
${iconButtonHtml({ buttonId: "rbt_mdi", action: "mode-mdi", label: "MDI", title: mdiGate.operatorMessage || "Enter MDI mode", className: "sidebar-button", active: state.machine.mode === "mdi", gate: mdiGate, attrs: nativeButtonAttrs("v", 3) })}
|
||||
<button type="button" class="sidebar-button" data-action="kins-identity" data-active="${state.kinsType === "identity"}">IDENTITY</button>
|
||||
<button type="button" class="sidebar-button" data-action="kins-tcp" data-active="${state.kinsType.startsWith("tcp-")}"${tcpDisabled}>TCP</button>
|
||||
${iconButtonHtml({ buttonId: "tbtn_estop", action: "estop", label: "E-STOP", title: estopEntry.operatorMessage || "Estop the machine / F1", className: "sidebar-button estop", active: estopEntry.active, gate: entryGate(estopEntry), attrs: `${nativeButtonAttrs("v", 0)} ${sidebarEntryAttrs(estopEntry)}` })}
|
||||
${iconButtonHtml({ buttonId: "tbtn_on", action: "power", label: "POWER", title: powerEntry.operatorMessage || "Turn the machine on/off / F2", className: "sidebar-button power", active: powerEntry.active, gate: entryGate(powerEntry), attrs: `${nativeButtonAttrs("v", 1)} ${sidebarEntryAttrs(powerEntry)}` })}
|
||||
${iconButtonHtml({ buttonId: "tbtn_setup", action: "reset", label: "RESET", title: resetEntry.operatorMessage || "Reset estop and return machine off", className: "sidebar-button", active: resetEntry.active, gate: entryGate(resetEntry), attrs: sidebarEntryAttrs(resetEntry) })}
|
||||
${iconButtonHtml({ buttonId: "rbt_auto", action: "mode-auto", label: "AUTO", title: autoEntry.operatorMessage || "Enter auto mode", className: "sidebar-button", active: autoEntry.active, gate: entryGate(autoEntry), attrs: `${nativeButtonAttrs("v", 4)} ${sidebarEntryAttrs(autoEntry)}` })}
|
||||
${iconButtonHtml({ buttonId: "rbt_manual", action: "mode-manual", label: "MANUAL", title: manualEntry.operatorMessage || "Enter manual mode", className: "sidebar-button", active: manualEntry.active, gate: entryGate(manualEntry), attrs: `${nativeButtonAttrs("v", 2)} ${sidebarEntryAttrs(manualEntry)}` })}
|
||||
${iconButtonHtml({ buttonId: "tbtn_user_tabs", action: "mode-jog", label: "JOG", title: jogEntry.operatorMessage || "Manual jog page", className: "sidebar-button", active: jogEntry.active, gate: entryGate(jogEntry), attrs: `${nativeButtonAttrs("v", 5)} ${sidebarEntryAttrs(jogEntry)}` })}
|
||||
${iconButtonHtml({ buttonId: "rbt_mdi", action: "mode-mdi", label: "MDI", title: mdiEntry.operatorMessage || "Enter MDI mode", className: "sidebar-button", active: mdiEntry.active, gate: entryGate(mdiEntry), attrs: `${nativeButtonAttrs("v", 3)} ${sidebarEntryAttrs(mdiEntry)}` })}
|
||||
${sidebarTextButtonHtml(identityEntry, { action: "kins-identity" })}
|
||||
${sidebarTextButtonHtml(tcpEntry, { action: "kins-tcp", title: tcpKinsType ? null : `${state.profile.id} is ${state.profile.kinematics} reference only` })}
|
||||
<time>13:30:31<br />20.06.2026</time>
|
||||
`;
|
||||
|
||||
@@ -384,6 +596,7 @@ function renderSidebar(element, state, dispatch) {
|
||||
function renderInfoTabs(element, state) {
|
||||
const frame = state.rtcpFrame;
|
||||
const taskPolicy = state.linuxCncTaskPolicy;
|
||||
const parityMatrix = state.linuxCncParityMatrix;
|
||||
const commSummary = createGmoccapyCommunicationSummary();
|
||||
const halSummary = createGmoccapyHalSummary();
|
||||
element.innerHTML = `
|
||||
@@ -423,7 +636,15 @@ function renderInfoTabs(element, state) {
|
||||
<dt>INI limits:</dt><dd data-linuxcnc-ini="limits">${formatAxisLimitSummary(state.profile.axisLimits)}</dd>
|
||||
<dt>INI joints:</dt><dd data-linuxcnc-ini="joints">${state.iniConfigReadiness.jointCount ?? 0} joints / ${state.iniConfigReadiness.axisCount ?? 0} axes</dd>
|
||||
<dt>Machine files:</dt><dd data-machine-file-staging="status">${formatMachineFileStaging(state.machineFileStaging)}</dd>
|
||||
<dt>Project root:</dt><dd data-machine-project="info-root">${escapeHtml(state.machineProject?.projectRoot || "-")}</dd>
|
||||
<dt>Project config:</dt><dd data-machine-project="info-config">${escapeHtml(formatMachineProjectConfig(state))}</dd>
|
||||
<dt>Project G-code:</dt><dd data-machine-project="info-gcode">${escapeHtml(formatMachineProjectGcode(state))}</dd>
|
||||
<dt>LinuxCNC G-code:</dt><dd data-linuxcnc-gcode-source="selected">${state.machineFileStaging.selectedGcodeSourceRel || state.programSourceRel || "-"}</dd>
|
||||
<dt>Program validation:</dt><dd data-program-validation="info">${escapeHtml(formatProgramValidationSummary(state))}</dd>
|
||||
<dt>LinuxCNC parity:</dt><dd data-linuxcnc-parity="summary">${escapeHtml(formatLinuxCncParitySummary(parityMatrix))}</dd>
|
||||
<dt>Parity active:</dt><dd data-linuxcnc-parity="active">${escapeHtml(formatLinuxCncParityActive(parityMatrix))}</dd>
|
||||
<dt>Parity sources:</dt><dd data-linuxcnc-parity="sources">${escapeHtml(formatLinuxCncParitySources(parityMatrix))}</dd>
|
||||
<dt>Parity functions:</dt><dd data-linuxcnc-parity="functions">${escapeHtml(formatLinuxCncParityFunctions(parityMatrix))}</dd>
|
||||
<dt>Task/HAL:</dt><dd data-task-hal-runtime="readiness">${formatTaskHalReadiness(state)}</dd>
|
||||
<dt>Task/HAL cycles:</dt><dd data-task-hal-runtime="cycles">${formatTaskHalCycles(state)}</dd>
|
||||
<dt>Full boundary:</dt><dd data-full-execution-boundary="status">${formatFullExecutionBoundary(state.fullExecutionBoundary)}</dd>
|
||||
|
||||
BIN
web-rtcp-5axis-sim-plan/docs/Web-RTCP五轴数控系统仿真界面操作手册.docx
Normal file
BIN
web-rtcp-5axis-sim-plan/docs/Web-RTCP五轴数控系统仿真界面操作手册新版.docx
Normal file
55
web-rtcp-5axis-sim-plan/docs/linuxcnc-parity-matrix.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# LinuxCNC 对标程序与功能矩阵
|
||||
|
||||
本文记录 Web-RTCP 五轴数控仿真界面当前对标的 LinuxCNC 程序、配置文件和功能边界。运行时状态对象为 `linuxCncParityMatrix`,界面在 gmoccapy diagnostics 中显示摘要,Node smoke 通过 `tests/node/verify_linuxcnc_parity_matrix.mjs` 持续校验。
|
||||
|
||||
## 1. 对标的 LinuxCNC 程序和配置
|
||||
|
||||
| 类别 | LinuxCNC 来源 | Web 对标内容 |
|
||||
| --- | --- | --- |
|
||||
| XYZAC TRT 配置 | `configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini` | `xyzac-trt` profile、TRAJ `XYZAC`、INI 解析、项目目录 staging |
|
||||
| XYZBC TRT 配置 | `configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini` | `xyzbc-trt` profile、TRAJ `XYZBC`、INI 解析、项目目录 staging |
|
||||
| PyVCP/POSTGUI/HAL | `xyzac-trt.xml`、`xyzbc-trt.xml`、`switchkins_postgui.hal`、`xyzac-trt_cmds.hal` | SWITCHKINS 按钮语义、HALUI MDI 命令、`motion.switchkins-type` 映射 |
|
||||
| remap 子程序 | `remap_subs/428remap.ngc`、`429remap.ngc`、`430remap.ngc` | `M428` TCP、`M429` identity、`M430` userk 的 kinstype 切换语义 |
|
||||
| Vismach Python | `src/hal/user_comps/vismach/xyzac-trt-gui.py`、`xyzbc-trt-gui.py` | 机床预览层级、关节反馈 pin、刀补/偏置显示语义 |
|
||||
| Kinematics C 源码 | `src/emc/kinematics/xyzac-trt-kins.c`、`xyzbc-trt-kins.c`、`trtfuncs.c`、`switchkins.c` | 正逆解、TCP/identity/userk 切换、DRO/TCP frame 更新 |
|
||||
| gmoccapy TRT 示例 | `configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/xyzac-trt.ini`、`postgui.hal` | gmoccapy 入口、POSTGUI 工具/主轴 HAL、TRT 示例程序来源 |
|
||||
| gmoccapy 原生界面 | `src/emc/usr_intf/gmoccapy/gmoccapy.py`、`share/gmoccapy/gmoccapy.glade`、gmoccapy 图标清单 | 操作台布局、右侧状态入口、按钮颜色/禁用/激活规则、HAL pin 语义 |
|
||||
| LinuxCNC task/NML | `src/emc/nml_intf/emc.hh`、`src/emc/task/emctaskmain.cc`、`src/emc/task/emctask.cc` | E-STOP/POWER/RESET、AUTO/MANUAL/JOG/MDI、RUN/PAUSE/RESUME/STEP/STOP 的互锁 |
|
||||
| task/HAL/motion 状态 | `src/emc/motion/control.c`、`src/hal/hal_lib.c` | task cycle、servo cycle、HAL pin 变化、实时轴值反馈边界 |
|
||||
|
||||
## 2. 对标的真实五轴 G-code 程序
|
||||
|
||||
当前验证程序来源限定为 LinuxCNC 源目录中的五轴程序,不把普通用户文件误标为 LinuxCNC 对标程序:
|
||||
|
||||
| 程序 | 用途 |
|
||||
| --- | --- |
|
||||
| `demos/xyzac_switchkins.ngc` | XYZAC switchkins 基准程序 |
|
||||
| `demos/xyzbc_switchkins.ngc` | XYZBC switchkins 基准程序 |
|
||||
| `demos/boat-xyzac.ngc` | XYZAC TCP 船体程序 |
|
||||
| `demos/boat-xyzbc.ngc` | XYZBC TCP 船体程序 |
|
||||
| `demos/impeller-7bl-xyzac.ngc` | XYZAC 叶轮长路径程序 |
|
||||
| `demos/xyzac_switchkins_test_1.ngc` | M428/M429 多次切换验证 |
|
||||
| `demos/xyzac_switchkins_test_2.ngc` | TCP/identity 过渡验证 |
|
||||
| `demos/xyzac_switchkins_test_3.ngc` | switchkins 程序验证覆盖 |
|
||||
|
||||
## 3. 已承接的功能
|
||||
|
||||
| 功能 | 当前 Web 实现 |
|
||||
| --- | --- |
|
||||
| 右侧纵向入口 | `E-STOP`、`POWER`、`RESET`、`AUTO`、`MANUAL`、`JOG`、`MDI`、`IDENTITY`、`TCP` 全部建模 |
|
||||
| 模式互锁 | 未复位急停不能上电;未回零不能 AUTO/MDI;AUTO 运行时阻止离开 AUTO、JOG、TCP/IDENTITY 切换 |
|
||||
| 颜色规则 | emergency/power/reset/mode/identity/tcp 各自有 active、ready、blocked、active-blocked 状态 |
|
||||
| 项目目录 | 按 profile 生成 `web-rtcp-5axis-sim-plan/machines/<profile>`,保存当前机床 INI、HAL、TBL、NGC/remap 文件 |
|
||||
| INI 对标 | staged INI 与当前 profile INI 路径和文本比对,暴露 `sourceMatchesProfile`、`textMatchesLoadedIni` |
|
||||
| 程序验证 | LinuxCNC interpreter runtime 产出 canonical motion、switchkins 事件、planner sample |
|
||||
| 实时轴值 | G-code 执行反馈驱动 `axisPose`、`tcpPose`、DRO、当前行、feed/DTG |
|
||||
| 预览路径 | Three.js/WebGL 预览程序路径、当前刀具位置、TCP 点和刀轴方向 |
|
||||
| gmoccapy HAL 语义 | 硬按钮、jog、override、spindle、coolant、tool change、message/tool/user inputs 建模 |
|
||||
| task/HAL 边界 | 保留 native task/HAL 与硬件实时边界,不把 Web 仿真误标为真实机床控制 |
|
||||
|
||||
## 4. 当前边界
|
||||
|
||||
- Web 端运行的是浏览器仿真状态机和 WASM/JS runtime,不运行原生 GTK `gmoccapy.py`。
|
||||
- `gmoccapy.py`、Glade、Vismach Python 是界面/机床模型/HAL 语义参考,不是浏览器运行时。
|
||||
- 硬件驱动、实时内核、真实 HAL shared memory、外部 user M 进程和真实 tool DB 不在浏览器内提升为 ready。
|
||||
- `linuxCncParityMatrix` 的 `semanticBoundary` 固定为 `linuxcnc_source_function_parity_matrix_for_browser_simulation_not_hardware_control`。
|
||||
BIN
web-rtcp-5axis-sim-plan/docs/manual-assets/01-main-overview.png
Normal file
|
After Width: | Height: | Size: 230 KiB |
|
After Width: | Height: | Size: 230 KiB |
|
After Width: | Height: | Size: 32 KiB |
BIN
web-rtcp-5axis-sim-plan/docs/manual-assets/04-auto-active.png
Normal file
|
After Width: | Height: | Size: 230 KiB |
|
After Width: | Height: | Size: 32 KiB |
BIN
web-rtcp-5axis-sim-plan/docs/manual-assets/06-run-program.png
Normal file
|
After Width: | Height: | Size: 233 KiB |
BIN
web-rtcp-5axis-sim-plan/docs/manual-assets/07-manual-jog.png
Normal file
|
After Width: | Height: | Size: 230 KiB |
BIN
web-rtcp-5axis-sim-plan/docs/manual-assets/08-mdi-command.png
Normal file
|
After Width: | Height: | Size: 230 KiB |
BIN
web-rtcp-5axis-sim-plan/docs/manual-assets/09-mdi-executed.png
Normal file
|
After Width: | Height: | Size: 232 KiB |
|
After Width: | Height: | Size: 232 KiB |
|
After Width: | Height: | Size: 232 KiB |
@@ -71,9 +71,12 @@
|
||||
throw new Error(`executed G-code row should be light gray, got ${previousBackground}`);
|
||||
}
|
||||
}
|
||||
const listText = list.textContent;
|
||||
if (/\bpending\b|\bstopped\s+\|\s+F\b|\brunning\s+\|\s+F\b/.test(listText)) {
|
||||
throw new Error("G-code list should not render execution status text");
|
||||
const executionText = activeRow.querySelector("[data-line-execution]")?.textContent || "";
|
||||
if (
|
||||
executionText &&
|
||||
!/\b(running|done|paused|stopped|mdi|ready)\b\s+\|\s+F\s+[-.\d]+\s+\|\s+cycle\s+\d+\/\d+/.test(executionText)
|
||||
) {
|
||||
throw new Error(`G-code line execution text has unexpected format: ${executionText}`);
|
||||
}
|
||||
const rowBounds = activeRow.getBoundingClientRect();
|
||||
const listBounds = list.getBoundingClientRect();
|
||||
@@ -82,6 +85,31 @@
|
||||
}
|
||||
}
|
||||
|
||||
function assertSidebarModeVisualState(doc, { manualActive, autoActive }) {
|
||||
const expected = [
|
||||
[doc.querySelector('[data-action="mode-manual"]'), "MANUAL", manualActive],
|
||||
[doc.querySelector('[data-action="mode-auto"]'), "AUTO", autoActive],
|
||||
];
|
||||
for (const [button, label, active] of expected) {
|
||||
if (!button) {
|
||||
throw new Error(`missing ${label} mode button`);
|
||||
}
|
||||
if (button.dataset.active !== String(active)) {
|
||||
throw new Error(`${label} mode button active flag should be ${active}, got ${button.dataset.active}`);
|
||||
}
|
||||
const style = getComputedStyle(button);
|
||||
const backgroundImage = style.backgroundImage;
|
||||
const color = style.color.replace(/\s/g, "");
|
||||
if (active) {
|
||||
if (!backgroundImage.includes("rgb(174, 233, 174)") || color !== "rgb(7,95,22)") {
|
||||
throw new Error(`${label} active mode button should render green, got ${backgroundImage} / ${color}`);
|
||||
}
|
||||
} else if (!backgroundImage.includes("rgb(221, 217, 209)") || color !== "rgb(48,48,48)") {
|
||||
throw new Error(`${label} inactive mode button should render gray, got ${backgroundImage} / ${color}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function runSmoke() {
|
||||
await new Promise((resolve, reject) => {
|
||||
frame.addEventListener("load", resolve, { once: true });
|
||||
@@ -216,6 +244,21 @@
|
||||
if (!profileSelector || profileSelector.options.length < 2) {
|
||||
throw new Error("missing five-axis profile selector");
|
||||
}
|
||||
const titlebar = doc.querySelector('[data-region="titlebar"]');
|
||||
const currentLineIndicator = titlebar?.querySelector("[data-titlebar-current-line]");
|
||||
if (!titlebar || !currentLineIndicator) {
|
||||
throw new Error("missing stable titlebar nodes");
|
||||
}
|
||||
win.webRtcp5AxisSimulation.dispatch({ type: "SET_VIEW", view: "x" });
|
||||
await wait(50);
|
||||
if (profileSelector !== doc.querySelector('[data-action="select-profile"]')) {
|
||||
throw new Error("titlebar profile selector should not be remounted on state updates");
|
||||
}
|
||||
if (currentLineIndicator !== doc.querySelector("[data-titlebar-current-line]")) {
|
||||
throw new Error("titlebar current-line indicator should not be remounted on state updates");
|
||||
}
|
||||
win.webRtcp5AxisSimulation.dispatch({ type: "RESET_VIEW" });
|
||||
await wait(50);
|
||||
profileSelector.value = "xyzbc-trt";
|
||||
profileSelector.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
await wait(500);
|
||||
@@ -273,7 +316,7 @@
|
||||
['[data-action="STOP"]', "stop"],
|
||||
['[data-action="PAUSE"]', "pause"],
|
||||
['[data-action="HOME"]', "ref_all"],
|
||||
['[data-action="toggle-flood"]', "coolant_flood_active"],
|
||||
['[data-action="toggle-flood"]', "coolant_flood_inactive"],
|
||||
['[data-action="toggle-mist"]', "coolant_mist_inactive"],
|
||||
['[data-action="view-x"]', "tool_axis_x"],
|
||||
]) {
|
||||
@@ -523,6 +566,18 @@
|
||||
}
|
||||
doc.querySelector('[data-action="mode-auto"]').click();
|
||||
await wait(50);
|
||||
const autoModeState = win.webRtcp5AxisSimulation.getState();
|
||||
if (autoModeState.machine.mode !== "auto") {
|
||||
throw new Error(`AUTO mode button did not switch task mode: ${autoModeState.machine.mode}`);
|
||||
}
|
||||
if (autoModeState.machine.powerOn !== true || autoModeState.machine.taskState !== "on") {
|
||||
throw new Error(`AUTO mode button should preserve machine power: ${JSON.stringify(autoModeState.machine)}`);
|
||||
}
|
||||
const postAutoManualButton = doc.querySelector('[data-action="mode-manual"]');
|
||||
if (postAutoManualButton?.disabled || postAutoManualButton.dataset.commandReady !== "true") {
|
||||
throw new Error("MANUAL mode button should remain enabled after switching to AUTO");
|
||||
}
|
||||
assertSidebarModeVisualState(doc, { manualActive: false, autoActive: true });
|
||||
if (!doc.querySelector('[data-linuxcnc-task-policy="gates"]')?.textContent.includes("auto")) {
|
||||
throw new Error("LinuxCNC task policy did not expose auto run gate");
|
||||
}
|
||||
@@ -779,6 +834,10 @@
|
||||
throw new Error("RUN did not highlight a LinuxCNC task/HAL motion line");
|
||||
}
|
||||
assertActiveGcodeRowVisible(doc);
|
||||
const runExecutionText = doc.querySelector(".gcode-row.active [data-line-execution]")?.textContent || "";
|
||||
if (!runExecutionText.includes("F ") || !runExecutionText.includes("cycle") || !runExecutionText.includes("linuxcnc-task-motion-hal-wasm")) {
|
||||
throw new Error(`RUN did not render current G-code line execution details: ${runExecutionText}`);
|
||||
}
|
||||
const stopLineBefore = win.webRtcp5AxisSimulation.getState().activeLine;
|
||||
doc.querySelector('[data-action="STOP"]').click();
|
||||
await wait(150);
|
||||
@@ -928,6 +987,7 @@
|
||||
}
|
||||
doc.querySelector('[data-action="mode-manual"]').click();
|
||||
await wait(50);
|
||||
assertSidebarModeVisualState(doc, { manualActive: true, autoActive: false });
|
||||
doc.querySelector('[data-action="mode-jog"]').click();
|
||||
await wait(50);
|
||||
const xBeforeJog = win.webRtcp5AxisSimulation.getState().axisPose.x;
|
||||
@@ -1248,7 +1308,7 @@
|
||||
doc.querySelector('[data-action="toggle-mist"]').click();
|
||||
await wait(50);
|
||||
const coolantState = win.webRtcp5AxisSimulation.getState().coolant;
|
||||
if (coolantState.flood !== false || coolantState.mist !== true) {
|
||||
if (coolantState.flood !== true || coolantState.mist !== true) {
|
||||
throw new Error("coolant buttons did not update state");
|
||||
}
|
||||
doc.querySelector('[data-action="view-x"]').click();
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
import { createMemorySessionStorage } from "../../app/src/runtime/five-axis-session.js";
|
||||
import { createLinuxCncInterpreterRuntime } from "../../app/src/runtime/linuxcnc-interpreter-runtime.js";
|
||||
import { parseLinuxCncIni } from "../../app/src/runtime/linuxcnc-ini-runtime.js";
|
||||
import { getFiveAxisProfile } from "../../app/src/profiles/index.js";
|
||||
import { createSimulationStore } from "../../app/src/state/store.js";
|
||||
|
||||
function sidebarEntry(state, id) {
|
||||
const entry = state.rightSidebarEntrances.find((candidate) => candidate.id === id);
|
||||
assert.ok(entry, `missing right sidebar entry ${id}`);
|
||||
return entry;
|
||||
}
|
||||
|
||||
async function waitForValidatedProgram(store) {
|
||||
for (let attempt = 0; attempt < 30; attempt += 1) {
|
||||
const state = store.getState();
|
||||
if (!state.interpreterExecutionPending && state.programValidation?.ready) {
|
||||
return state;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
return store.getState();
|
||||
}
|
||||
|
||||
async function loadIniConfigFromVendoredSource(profile) {
|
||||
const text = await readFile(
|
||||
new URL(`../../../wasm-port/vendor/linuxcnc/${profile.iniPath}`, import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
return parseLinuxCncIni(text, {
|
||||
path: profile.iniPath,
|
||||
profileId: profile.id,
|
||||
});
|
||||
}
|
||||
|
||||
const store = createSimulationStore();
|
||||
let state = store.getState();
|
||||
assert.equal(state.rightSidebarEntrances.length, 9);
|
||||
assert.deepEqual(
|
||||
state.rightSidebarEntrances.map((entry) => entry.label),
|
||||
["E-STOP", "POWER", "RESET", "AUTO", "MANUAL", "JOG", "MDI", "IDENTITY", "TCP"],
|
||||
);
|
||||
assert.equal(sidebarEntry(state, "power").allowed, true);
|
||||
assert.equal(sidebarEntry(state, "auto").allowed, false);
|
||||
assert.equal(sidebarEntry(state, "manual").allowed, false);
|
||||
assert.equal(sidebarEntry(state, "jog").allowed, false);
|
||||
assert.equal(sidebarEntry(state, "mdi").allowed, false);
|
||||
assert.equal(sidebarEntry(state, "identity").active, true);
|
||||
assert.equal(sidebarEntry(state, "identity").status, "active-blocked");
|
||||
assert.equal(sidebarEntry(state, "tcp").allowed, false);
|
||||
|
||||
store.dispatch({ type: "ESTOP" });
|
||||
state = store.getState();
|
||||
assert.equal(sidebarEntry(state, "estop").status, "emergency");
|
||||
assert.equal(sidebarEntry(state, "power").allowed, false);
|
||||
assert.equal(sidebarEntry(state, "power").operatorMessage, "power on blocked: reset estop first");
|
||||
|
||||
store.dispatch({ type: "RESET" });
|
||||
store.dispatch({ type: "TOGGLE_POWER" });
|
||||
state = store.getState();
|
||||
assert.equal(state.machine.taskState, "on");
|
||||
assert.equal(sidebarEntry(state, "power").active, true);
|
||||
assert.equal(sidebarEntry(state, "manual").active, true);
|
||||
assert.equal(sidebarEntry(state, "jog").active, false);
|
||||
assert.equal(sidebarEntry(state, "auto").allowed, false);
|
||||
assert.equal(sidebarEntry(state, "auto").operatorMessage, "mode blocked: home machine before AUTO");
|
||||
|
||||
store.dispatch({ type: "SET_MODE", mode: "jog" });
|
||||
state = store.getState();
|
||||
assert.equal(state.machine.mode, "manual");
|
||||
assert.equal(state.machine.manualPanel, "jog");
|
||||
assert.equal(sidebarEntry(state, "manual").active, false);
|
||||
assert.equal(sidebarEntry(state, "jog").active, true);
|
||||
|
||||
store.dispatch({ type: "HOME" });
|
||||
store.dispatch({ type: "SET_MODE", mode: "auto" });
|
||||
state = store.getState();
|
||||
assert.equal(sidebarEntry(state, "auto").active, true);
|
||||
assert.equal(sidebarEntry(state, "mdi").allowed, true);
|
||||
assert.equal(sidebarEntry(state, "tcp").allowed, true);
|
||||
|
||||
store.dispatch({ type: "SET_KINS_TYPE", kinsType: "tcp-xyzac" });
|
||||
state = store.getState();
|
||||
assert.equal(state.kinsType, "tcp-xyzac");
|
||||
assert.equal(state.rtcpState, "on");
|
||||
assert.equal(sidebarEntry(state, "tcp").active, true);
|
||||
store.dispatch({ type: "SET_KINS_TYPE", kinsType: "identity" });
|
||||
assert.equal(store.getState().kinsType, "identity");
|
||||
|
||||
store.dispatch({
|
||||
type: "LOAD_PROGRAM",
|
||||
filename: "sidebar-running-gate.ngc",
|
||||
content: [
|
||||
"G0 X0 Y0 Z0",
|
||||
"G1 X1 F100",
|
||||
"G1 X2",
|
||||
"G1 X3",
|
||||
"G1 X4",
|
||||
"G1 X5",
|
||||
"G1 X6",
|
||||
"G1 X7",
|
||||
"G1 X8",
|
||||
"G1 X9",
|
||||
"M2",
|
||||
].join("\n"),
|
||||
});
|
||||
store.dispatch({ type: "RUN" });
|
||||
state = store.getState();
|
||||
assert.equal(state.runState, "running");
|
||||
assert.equal(sidebarEntry(state, "manual").allowed, false);
|
||||
assert.equal(sidebarEntry(state, "tcp").allowed, false);
|
||||
assert.equal(sidebarEntry(state, "tcp").operatorMessage, "kinematics blocked: interpreter must be idle");
|
||||
|
||||
store.dispatch({ type: "STOP" });
|
||||
store.dispatch({ type: "SET_MODE", mode: "manual" });
|
||||
|
||||
const profile = getFiveAxisProfile("xyzac-trt");
|
||||
const iniConfig = await loadIniConfigFromVendoredSource(profile);
|
||||
store.dispatch({ type: "ATTACH_INI_CONFIG", profileId: profile.id, iniConfig });
|
||||
store.dispatch({
|
||||
type: "ATTACH_INTERPRETER_RUNTIME",
|
||||
runtime: await createLinuxCncInterpreterRuntime(),
|
||||
});
|
||||
await store.stageMachineFiles({ storage: createMemorySessionStorage() });
|
||||
state = store.getState();
|
||||
assert.equal(state.machineProject.profileId, "xyzac-trt");
|
||||
assert.equal(state.machineProject.projectRoot, "web-rtcp-5axis-sim-plan/machines/xyzac-trt");
|
||||
assert.equal(state.machineProject.ini.filename, "xyzac-trt.ini");
|
||||
assert.equal(state.machineProject.ini.sourceMatchesProfile, true);
|
||||
assert.equal(state.machineProject.ini.textMatchesLoadedIni, true);
|
||||
assert.equal(state.machineProject.configFileCount > 0, true);
|
||||
assert.equal(state.machineProject.gcodeFileCount, 16);
|
||||
assert.equal(state.machineProject.gcodeFiles.some((file) => file.filename === "impeller-7bl-xyzac.ngc"), true);
|
||||
|
||||
store.dispatch({
|
||||
type: "LOAD_LINUXCNC_GCODE_SOURCE",
|
||||
sourceRel: "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/impeller-7bl-xyzac.ngc",
|
||||
});
|
||||
state = await waitForValidatedProgram(store);
|
||||
assert.equal(state.programValidation.ready, true);
|
||||
assert.equal(state.programValidation.sourceGuard, "linuxcnc_vendored_5axis_gcode_source_file");
|
||||
assert.equal(state.programValidation.previewSource, "linuxcnc_interpreter_canonical_motion");
|
||||
assert.equal(state.programValidation.motionEventCount > 0, true);
|
||||
assert.equal(state.programValidation.plannerSampleCount > 0, true);
|
||||
assert.equal(state.programValidation.realtimeAxisValues.x, state.axisPose.x);
|
||||
assert.equal(state.machineProject.selectedProgram.filename, "impeller-7bl-xyzac.ngc");
|
||||
|
||||
const xyzbcStore = createSimulationStore();
|
||||
xyzbcStore.dispatch({ type: "SET_PROFILE", profileId: "xyzbc-trt" });
|
||||
const xyzbcProfile = getFiveAxisProfile("xyzbc-trt");
|
||||
xyzbcStore.dispatch({
|
||||
type: "ATTACH_INI_CONFIG",
|
||||
profileId: xyzbcProfile.id,
|
||||
iniConfig: await loadIniConfigFromVendoredSource(xyzbcProfile),
|
||||
});
|
||||
await xyzbcStore.stageMachineFiles({ storage: createMemorySessionStorage() });
|
||||
const xyzbcState = xyzbcStore.getState();
|
||||
assert.equal(xyzbcState.machineProject.profileId, "xyzbc-trt");
|
||||
assert.equal(xyzbcState.machineProject.ini.filename, "xyzbc-trt.ini");
|
||||
assert.equal(xyzbcState.machineProject.gcodeFiles.some((file) => file.filename === "boat-xyzbc.ngc"), true);
|
||||
|
||||
console.log("gmoccapy_trt_project_sidebar_smoke=ok");
|
||||
@@ -0,0 +1,131 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
import {
|
||||
LINUXCNC_PARITY_ITEMS,
|
||||
RIGHT_SIDEBAR_PARITY_ENTRIES,
|
||||
SWITCHKINS_PARITY_CODES,
|
||||
} from "../../app/src/runtime/linuxcnc-parity-matrix.js";
|
||||
import { createMemorySessionStorage } from "../../app/src/runtime/five-axis-session.js";
|
||||
import { createLinuxCncInterpreterRuntime } from "../../app/src/runtime/linuxcnc-interpreter-runtime.js";
|
||||
import { parseLinuxCncIni } from "../../app/src/runtime/linuxcnc-ini-runtime.js";
|
||||
import { getFiveAxisProfile } from "../../app/src/profiles/index.js";
|
||||
import { createSimulationStore } from "../../app/src/state/store.js";
|
||||
|
||||
function item(matrix, id) {
|
||||
const found = matrix.items.find((entry) => entry.id === id);
|
||||
assert.ok(found, `missing parity matrix item ${id}`);
|
||||
return found;
|
||||
}
|
||||
|
||||
async function waitForValidatedProgram(store) {
|
||||
for (let attempt = 0; attempt < 30; attempt += 1) {
|
||||
const state = store.getState();
|
||||
if (!state.interpreterExecutionPending && state.programValidation?.ready) {
|
||||
return state;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
return store.getState();
|
||||
}
|
||||
|
||||
async function loadIniConfigFromVendoredSource(profile) {
|
||||
const text = await readFile(
|
||||
new URL(`../../../wasm-port/vendor/linuxcnc/${profile.iniPath}`, import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
return parseLinuxCncIni(text, {
|
||||
path: profile.iniPath,
|
||||
profileId: profile.id,
|
||||
});
|
||||
}
|
||||
|
||||
assert.equal(LINUXCNC_PARITY_ITEMS.length >= 11, true);
|
||||
assert.deepEqual(RIGHT_SIDEBAR_PARITY_ENTRIES, [
|
||||
"E-STOP",
|
||||
"POWER",
|
||||
"RESET",
|
||||
"AUTO",
|
||||
"MANUAL",
|
||||
"JOG",
|
||||
"MDI",
|
||||
"IDENTITY",
|
||||
"TCP",
|
||||
]);
|
||||
assert.deepEqual(SWITCHKINS_PARITY_CODES, ["M428", "M429", "M430"]);
|
||||
|
||||
const store = createSimulationStore();
|
||||
let state = store.getState();
|
||||
let matrix = state.linuxCncParityMatrix;
|
||||
|
||||
assert.equal(matrix.apiName, "web-rtcp-5axis-linuxcnc-parity-matrix");
|
||||
assert.equal(matrix.profileId, "xyzac-trt");
|
||||
assert.equal(matrix.status, "implemented");
|
||||
assert.equal(matrix.implementedCount, matrix.itemCount);
|
||||
assert.equal(matrix.rightSidebarComplete, true);
|
||||
assert.equal(matrix.switchkinsComplete, true);
|
||||
assert.deepEqual(matrix.actualRightSidebarEntries, RIGHT_SIDEBAR_PARITY_ENTRIES);
|
||||
assert.equal(matrix.requiredSwitchkinsCodes.every((code) => matrix.activeSwitchkinsCodes.includes(code)), true);
|
||||
assert.equal(matrix.linuxCncPrograms.some((path) => path.endsWith("impeller-7bl-xyzac.ngc")), true);
|
||||
assert.equal(matrix.linuxCncPrograms.some((path) => path.endsWith("boat-xyzbc.ngc")), true);
|
||||
assert.equal(matrix.linuxCncFunctions.includes("E-STOP/POWER/RESET task-state entry"), true);
|
||||
assert.equal(matrix.linuxCncFunctions.includes("M428 TCP"), true);
|
||||
assert.equal(matrix.linuxCncFunctions.includes("M429 identity"), true);
|
||||
assert.equal(matrix.linuxCncFunctions.includes("M430 userk"), true);
|
||||
|
||||
assert.equal(item(matrix, "xyzac-trt-axis-vismach-config").active, true);
|
||||
assert.equal(item(matrix, "xyzac-trt-axis-vismach-config").sourceMapped, true);
|
||||
assert.equal(item(matrix, "xyzac-trt-axis-vismach-config").linuxCncPaths.some((path) => path.endsWith("xyzac-trt.ini")), true);
|
||||
assert.equal(item(matrix, "xyzbc-trt-axis-vismach-config").implemented, true);
|
||||
assert.equal(item(matrix, "xyzbc-trt-axis-vismach-config").active, false);
|
||||
assert.equal(item(matrix, "gmoccapy-trt-config").linuxCncPaths.some((path) => path.includes("gmoccapy/non_trivial_kinematics/table-rotary-tilting/xyzac-trt.ini")), true);
|
||||
assert.equal(item(matrix, "gmoccapy-trt-config").functions.includes("DISPLAY gmoccapy"), true);
|
||||
assert.equal(item(matrix, "gmoccapy-native-operator-ui").active, true);
|
||||
assert.equal(item(matrix, "right-sidebar-task-interlocks").active, true);
|
||||
assert.equal(item(matrix, "vismach-machine-preview").active, true);
|
||||
assert.equal(item(matrix, "source-derived-kinematics-switchkins").active, true);
|
||||
assert.equal(item(matrix, "linuxcnc-interpreter-program-validation").active, false);
|
||||
assert.equal(item(matrix, "machine-project-directory").active, false);
|
||||
assert.equal(item(matrix, "task-hal-status-loop").implemented, true);
|
||||
assert.equal(item(matrix, "gmoccapy-postgui-tool-spindle").active, true);
|
||||
|
||||
store.dispatch({ type: "SET_PROFILE", profileId: "xyzbc-trt" });
|
||||
state = store.getState();
|
||||
matrix = state.linuxCncParityMatrix;
|
||||
assert.equal(matrix.profileId, "xyzbc-trt");
|
||||
assert.equal(item(matrix, "xyzac-trt-axis-vismach-config").active, false);
|
||||
assert.equal(item(matrix, "xyzbc-trt-axis-vismach-config").active, true);
|
||||
assert.equal(item(matrix, "source-derived-kinematics-switchkins").evidence.activeSwitchkinsCodes.includes("M428"), true);
|
||||
|
||||
store.dispatch({ type: "SET_PROFILE", profileId: "xyzac-trt" });
|
||||
const profile = getFiveAxisProfile("xyzac-trt");
|
||||
store.dispatch({
|
||||
type: "ATTACH_INI_CONFIG",
|
||||
profileId: profile.id,
|
||||
iniConfig: await loadIniConfigFromVendoredSource(profile),
|
||||
});
|
||||
store.dispatch({
|
||||
type: "ATTACH_INTERPRETER_RUNTIME",
|
||||
runtime: await createLinuxCncInterpreterRuntime(),
|
||||
});
|
||||
await store.stageMachineFiles({ storage: createMemorySessionStorage() });
|
||||
state = store.getState();
|
||||
matrix = state.linuxCncParityMatrix;
|
||||
assert.equal(item(matrix, "machine-project-directory").active, true);
|
||||
assert.equal(item(matrix, "machine-project-directory").evidence.iniSourceMatchesProfile, true);
|
||||
assert.equal(item(matrix, "machine-project-directory").evidence.gcodeFileCount >= 8, true);
|
||||
|
||||
store.dispatch({
|
||||
type: "LOAD_LINUXCNC_GCODE_SOURCE",
|
||||
sourceRel: "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/impeller-7bl-xyzac.ngc",
|
||||
});
|
||||
state = await waitForValidatedProgram(store);
|
||||
matrix = state.linuxCncParityMatrix;
|
||||
assert.equal(state.programValidation.ready, true);
|
||||
assert.equal(item(matrix, "linuxcnc-interpreter-program-validation").active, true);
|
||||
assert.equal(item(matrix, "linuxcnc-interpreter-program-validation").evidence.sourceGuard, "linuxcnc_vendored_5axis_gcode_source_file");
|
||||
assert.equal(item(matrix, "linuxcnc-interpreter-program-validation").evidence.motionEventCount > 0, true);
|
||||
assert.equal(item(matrix, "linuxcnc-interpreter-program-validation").evidence.plannerSampleCount > 0, true);
|
||||
assert.equal(matrix.activeCount >= 9, true);
|
||||
|
||||
console.log("linuxcnc_parity_matrix_smoke=ok");
|
||||
@@ -127,6 +127,13 @@ assert.equal(kinematicsState.rtcpFrame.sourceMode, "source-derived-kinematics-wa
|
||||
assert.equal(kinematicsState.rtcpFrame.semanticBoundary, "linuxcnc_kinematics_wasm_c_abi");
|
||||
assert.equal(kinematicsState.rtcpFrame.readiness.linuxCncKinematicsReady, true);
|
||||
|
||||
kinematicsStore.dispatch({ type: "SET_RTCP", enabled: true });
|
||||
kinematicsState = kinematicsStore.getState();
|
||||
assert.equal(kinematicsState.rtcpState, "off");
|
||||
assert.equal(kinematicsState.operatorMessage, "kinematics blocked: machine must be on");
|
||||
|
||||
kinematicsStore.dispatch({ type: "TOGGLE_POWER" });
|
||||
kinematicsStore.dispatch({ type: "HOME" });
|
||||
kinematicsStore.dispatch({ type: "SET_RTCP", enabled: true });
|
||||
kinematicsState = kinematicsStore.getState();
|
||||
assert.equal(kinematicsState.rtcpState, "on");
|
||||
@@ -134,8 +141,6 @@ assert.equal(kinematicsState.rtcpFrame.sourceMode, "source-derived-kinematics-wa
|
||||
assert.equal(kinematicsState.lastKinematicsResult.moduleId, "xyzac-trt");
|
||||
assert.equal(kinematicsState.dro.tcpX, kinematicsState.rtcpFrame.tcpPose.x);
|
||||
|
||||
kinematicsStore.dispatch({ type: "TOGGLE_POWER" });
|
||||
kinematicsStore.dispatch({ type: "HOME" });
|
||||
kinematicsStore.dispatch({ type: "SET_MODE", mode: "auto" });
|
||||
kinematicsStore.dispatch({ type: "STEP" });
|
||||
kinematicsState = kinematicsStore.getState();
|
||||
@@ -270,6 +275,8 @@ assert.equal(store.getState().machineProfile, "xyzbc-trt");
|
||||
assert.equal(store.getState().profile.traj.coordinates, "XYZBC");
|
||||
const xyzbcRuntime = await createLinuxCncKinematicsRuntime({ moduleId: "xyzbc-trt" });
|
||||
store.dispatch({ type: "ATTACH_KINEMATICS_RUNTIME", runtime: xyzbcRuntime });
|
||||
store.dispatch({ type: "TOGGLE_POWER" });
|
||||
store.dispatch({ type: "HOME" });
|
||||
store.dispatch({ type: "SET_RTCP", enabled: true });
|
||||
state = store.getState();
|
||||
assert.equal(state.kinsType, "tcp-xyzbc");
|
||||
@@ -277,6 +284,7 @@ assert.equal(state.rtcpFrame.kinematicsModuleId, "xyzbc-trt");
|
||||
assert.equal(state.rtcpFrame.jointPose[3].axis, "B");
|
||||
|
||||
store.dispatch({ type: "SET_PROFILE", profileId: "xyzac-trt" });
|
||||
store.dispatch({ type: "RESET" });
|
||||
store.dispatch({ type: "RUN" });
|
||||
assert.equal(store.getState().activeLine, 501);
|
||||
assert.equal(store.getState().operatorMessage, "run blocked: machine must be on");
|
||||
|
||||