完善 LinuxCNC 对标矩阵与 gmoccapy TRT 仿真
This commit is contained in:
@@ -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
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>
|
||||
|
||||
Reference in New Issue
Block a user