完善 LinuxCNC 对标矩阵与 gmoccapy TRT 仿真

This commit is contained in:
2026-07-01 08:19:21 -04:00
parent 1e66f170c9
commit 9803aadf0a
31 changed files with 3966 additions and 104 deletions

View File

@@ -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>

View 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"
}
}
}
}

View File

@@ -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"
}
}

View File

@@ -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");

View File

@@ -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") {

View File

@@ -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",

View File

@@ -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 {

View File

@@ -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),
);

View File

@@ -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))];
}

View File

@@ -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,12 +1778,10 @@ export function createSimulationStore(seed = {}) {
});
break;
case "SET_KINS_TYPE":
if (String(action.kinsType || "").startsWith("tcp-") && !profileSupportsTcp(state.profile)) {
setState({
kinsType: "identity",
rtcpState: "off",
operatorMessage: `kinematics ${action.kinsType} blocked: ${state.profile.id} is not TCP capable`,
});
{
const gate = gateKinsTypeChange(state, action.kinsType);
if (!gate.allowed) {
setState({ operatorMessage: gate.operatorMessage });
break;
}
setState({
@@ -1677,6 +1790,7 @@ export function createSimulationStore(seed = {}) {
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",

View File

@@ -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;

View File

@@ -63,25 +63,23 @@ function render(regions, state, dispatch) {
function renderTitlebar(element, state) {
const currentLine = currentGcodeExecutionLine(state);
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>${state.machineProfile} | ${state.sessionName} | ${state.sourceMode} | ${state.machine.mode}</span>
<span data-titlebar-meta></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>
<select data-action="select-profile"></select>
</label>
<div class="current-line-indicator" data-current-gcode-line="${currentLine}" data-current-gcode-source="${escapeHtml(currentGcodeLineSource(state))}">
<div class="current-line-indicator" data-titlebar-current-line>
<span>G-code line</span>
<strong>${currentLine}</strong>
<strong></strong>
</div>
<div class="run-state" data-run-state="${state.runState}">${state.runState}</div>
<div class="run-state"></div>
`;
element.querySelector('[data-action="select-profile"]').addEventListener("change", (event) => {
element.dispatchEvent(new CustomEvent("profile-change", {
@@ -89,6 +87,41 @@ function renderTitlebar(element, state) {
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>

View File

@@ -0,0 +1,55 @@
# LinuxCNC 对标程序与功能矩阵
本文记录 Web-RTCP 五轴数控仿真界面当前对标的 LinuxCNC 程序、配置文件和功能边界。运行时状态对象为 `linuxCncParityMatrix`,界面在 gmoccapy diagnostics 中显示摘要Node smoke 通过 `tests/node/verify_linuxcnc_parity_matrix.mjs` 持续校验。
## 1. 对标的 LinuxCNC 程序和配置
| 类别 | LinuxCNC 来源 | Web 对标内容 |
| --- | --- | --- |
| XYZAC TRT 配置 | `configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini` | `xyzac-trt` profile、TRAJ `XYZAC`、INI 解析、项目目录 staging |
| XYZBC TRT 配置 | `configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini` | `xyzbc-trt` profile、TRAJ `XYZBC`、INI 解析、项目目录 staging |
| PyVCP/POSTGUI/HAL | `xyzac-trt.xml``xyzbc-trt.xml``switchkins_postgui.hal``xyzac-trt_cmds.hal` | SWITCHKINS 按钮语义、HALUI MDI 命令、`motion.switchkins-type` 映射 |
| remap 子程序 | `remap_subs/428remap.ngc``429remap.ngc``430remap.ngc` | `M428` TCP、`M429` identity、`M430` userk 的 kinstype 切换语义 |
| Vismach Python | `src/hal/user_comps/vismach/xyzac-trt-gui.py``xyzbc-trt-gui.py` | 机床预览层级、关节反馈 pin、刀补/偏置显示语义 |
| Kinematics C 源码 | `src/emc/kinematics/xyzac-trt-kins.c``xyzbc-trt-kins.c``trtfuncs.c``switchkins.c` | 正逆解、TCP/identity/userk 切换、DRO/TCP frame 更新 |
| gmoccapy TRT 示例 | `configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/xyzac-trt.ini``postgui.hal` | gmoccapy 入口、POSTGUI 工具/主轴 HAL、TRT 示例程序来源 |
| gmoccapy 原生界面 | `src/emc/usr_intf/gmoccapy/gmoccapy.py``share/gmoccapy/gmoccapy.glade`、gmoccapy 图标清单 | 操作台布局、右侧状态入口、按钮颜色/禁用/激活规则、HAL pin 语义 |
| LinuxCNC task/NML | `src/emc/nml_intf/emc.hh``src/emc/task/emctaskmain.cc``src/emc/task/emctask.cc` | E-STOP/POWER/RESET、AUTO/MANUAL/JOG/MDI、RUN/PAUSE/RESUME/STEP/STOP 的互锁 |
| task/HAL/motion 状态 | `src/emc/motion/control.c``src/hal/hal_lib.c` | task cycle、servo cycle、HAL pin 变化、实时轴值反馈边界 |
## 2. 对标的真实五轴 G-code 程序
当前验证程序来源限定为 LinuxCNC 源目录中的五轴程序,不把普通用户文件误标为 LinuxCNC 对标程序:
| 程序 | 用途 |
| --- | --- |
| `demos/xyzac_switchkins.ngc` | XYZAC switchkins 基准程序 |
| `demos/xyzbc_switchkins.ngc` | XYZBC switchkins 基准程序 |
| `demos/boat-xyzac.ngc` | XYZAC TCP 船体程序 |
| `demos/boat-xyzbc.ngc` | XYZBC TCP 船体程序 |
| `demos/impeller-7bl-xyzac.ngc` | XYZAC 叶轮长路径程序 |
| `demos/xyzac_switchkins_test_1.ngc` | M428/M429 多次切换验证 |
| `demos/xyzac_switchkins_test_2.ngc` | TCP/identity 过渡验证 |
| `demos/xyzac_switchkins_test_3.ngc` | switchkins 程序验证覆盖 |
## 3. 已承接的功能
| 功能 | 当前 Web 实现 |
| --- | --- |
| 右侧纵向入口 | `E-STOP``POWER``RESET``AUTO``MANUAL``JOG``MDI``IDENTITY``TCP` 全部建模 |
| 模式互锁 | 未复位急停不能上电;未回零不能 AUTO/MDIAUTO 运行时阻止离开 AUTO、JOG、TCP/IDENTITY 切换 |
| 颜色规则 | emergency/power/reset/mode/identity/tcp 各自有 active、ready、blocked、active-blocked 状态 |
| 项目目录 | 按 profile 生成 `web-rtcp-5axis-sim-plan/machines/<profile>`,保存当前机床 INI、HAL、TBL、NGC/remap 文件 |
| INI 对标 | staged INI 与当前 profile INI 路径和文本比对,暴露 `sourceMatchesProfile``textMatchesLoadedIni` |
| 程序验证 | LinuxCNC interpreter runtime 产出 canonical motion、switchkins 事件、planner sample |
| 实时轴值 | G-code 执行反馈驱动 `axisPose``tcpPose`、DRO、当前行、feed/DTG |
| 预览路径 | Three.js/WebGL 预览程序路径、当前刀具位置、TCP 点和刀轴方向 |
| gmoccapy HAL 语义 | 硬按钮、jog、override、spindle、coolant、tool change、message/tool/user inputs 建模 |
| task/HAL 边界 | 保留 native task/HAL 与硬件实时边界,不把 Web 仿真误标为真实机床控制 |
## 4. 当前边界
- Web 端运行的是浏览器仿真状态机和 WASM/JS runtime不运行原生 GTK `gmoccapy.py`
- `gmoccapy.py`、Glade、Vismach Python 是界面/机床模型/HAL 语义参考,不是浏览器运行时。
- 硬件驱动、实时内核、真实 HAL shared memory、外部 user M 进程和真实 tool DB 不在浏览器内提升为 ready。
- `linuxCncParityMatrix``semanticBoundary` 固定为 `linuxcnc_source_function_parity_matrix_for_browser_simulation_not_hardware_control`

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -71,9 +71,12 @@
throw new Error(`executed G-code row should be light gray, got ${previousBackground}`);
}
}
const listText = list.textContent;
if (/\bpending\b|\bstopped\s+\|\s+F\b|\brunning\s+\|\s+F\b/.test(listText)) {
throw new Error("G-code list should not render execution status text");
const executionText = activeRow.querySelector("[data-line-execution]")?.textContent || "";
if (
executionText &&
!/\b(running|done|paused|stopped|mdi|ready)\b\s+\|\s+F\s+[-.\d]+\s+\|\s+cycle\s+\d+\/\d+/.test(executionText)
) {
throw new Error(`G-code line execution text has unexpected format: ${executionText}`);
}
const rowBounds = activeRow.getBoundingClientRect();
const listBounds = list.getBoundingClientRect();
@@ -82,6 +85,31 @@
}
}
function assertSidebarModeVisualState(doc, { manualActive, autoActive }) {
const expected = [
[doc.querySelector('[data-action="mode-manual"]'), "MANUAL", manualActive],
[doc.querySelector('[data-action="mode-auto"]'), "AUTO", autoActive],
];
for (const [button, label, active] of expected) {
if (!button) {
throw new Error(`missing ${label} mode button`);
}
if (button.dataset.active !== String(active)) {
throw new Error(`${label} mode button active flag should be ${active}, got ${button.dataset.active}`);
}
const style = getComputedStyle(button);
const backgroundImage = style.backgroundImage;
const color = style.color.replace(/\s/g, "");
if (active) {
if (!backgroundImage.includes("rgb(174, 233, 174)") || color !== "rgb(7,95,22)") {
throw new Error(`${label} active mode button should render green, got ${backgroundImage} / ${color}`);
}
} else if (!backgroundImage.includes("rgb(221, 217, 209)") || color !== "rgb(48,48,48)") {
throw new Error(`${label} inactive mode button should render gray, got ${backgroundImage} / ${color}`);
}
}
}
async function runSmoke() {
await new Promise((resolve, reject) => {
frame.addEventListener("load", resolve, { once: true });
@@ -216,6 +244,21 @@
if (!profileSelector || profileSelector.options.length < 2) {
throw new Error("missing five-axis profile selector");
}
const titlebar = doc.querySelector('[data-region="titlebar"]');
const currentLineIndicator = titlebar?.querySelector("[data-titlebar-current-line]");
if (!titlebar || !currentLineIndicator) {
throw new Error("missing stable titlebar nodes");
}
win.webRtcp5AxisSimulation.dispatch({ type: "SET_VIEW", view: "x" });
await wait(50);
if (profileSelector !== doc.querySelector('[data-action="select-profile"]')) {
throw new Error("titlebar profile selector should not be remounted on state updates");
}
if (currentLineIndicator !== doc.querySelector("[data-titlebar-current-line]")) {
throw new Error("titlebar current-line indicator should not be remounted on state updates");
}
win.webRtcp5AxisSimulation.dispatch({ type: "RESET_VIEW" });
await wait(50);
profileSelector.value = "xyzbc-trt";
profileSelector.dispatchEvent(new Event("change", { bubbles: true }));
await wait(500);
@@ -273,7 +316,7 @@
['[data-action="STOP"]', "stop"],
['[data-action="PAUSE"]', "pause"],
['[data-action="HOME"]', "ref_all"],
['[data-action="toggle-flood"]', "coolant_flood_active"],
['[data-action="toggle-flood"]', "coolant_flood_inactive"],
['[data-action="toggle-mist"]', "coolant_mist_inactive"],
['[data-action="view-x"]', "tool_axis_x"],
]) {
@@ -523,6 +566,18 @@
}
doc.querySelector('[data-action="mode-auto"]').click();
await wait(50);
const autoModeState = win.webRtcp5AxisSimulation.getState();
if (autoModeState.machine.mode !== "auto") {
throw new Error(`AUTO mode button did not switch task mode: ${autoModeState.machine.mode}`);
}
if (autoModeState.machine.powerOn !== true || autoModeState.machine.taskState !== "on") {
throw new Error(`AUTO mode button should preserve machine power: ${JSON.stringify(autoModeState.machine)}`);
}
const postAutoManualButton = doc.querySelector('[data-action="mode-manual"]');
if (postAutoManualButton?.disabled || postAutoManualButton.dataset.commandReady !== "true") {
throw new Error("MANUAL mode button should remain enabled after switching to AUTO");
}
assertSidebarModeVisualState(doc, { manualActive: false, autoActive: true });
if (!doc.querySelector('[data-linuxcnc-task-policy="gates"]')?.textContent.includes("auto")) {
throw new Error("LinuxCNC task policy did not expose auto run gate");
}
@@ -779,6 +834,10 @@
throw new Error("RUN did not highlight a LinuxCNC task/HAL motion line");
}
assertActiveGcodeRowVisible(doc);
const runExecutionText = doc.querySelector(".gcode-row.active [data-line-execution]")?.textContent || "";
if (!runExecutionText.includes("F ") || !runExecutionText.includes("cycle") || !runExecutionText.includes("linuxcnc-task-motion-hal-wasm")) {
throw new Error(`RUN did not render current G-code line execution details: ${runExecutionText}`);
}
const stopLineBefore = win.webRtcp5AxisSimulation.getState().activeLine;
doc.querySelector('[data-action="STOP"]').click();
await wait(150);
@@ -928,6 +987,7 @@
}
doc.querySelector('[data-action="mode-manual"]').click();
await wait(50);
assertSidebarModeVisualState(doc, { manualActive: true, autoActive: false });
doc.querySelector('[data-action="mode-jog"]').click();
await wait(50);
const xBeforeJog = win.webRtcp5AxisSimulation.getState().axisPose.x;
@@ -1248,7 +1308,7 @@
doc.querySelector('[data-action="toggle-mist"]').click();
await wait(50);
const coolantState = win.webRtcp5AxisSimulation.getState().coolant;
if (coolantState.flood !== false || coolantState.mist !== true) {
if (coolantState.flood !== true || coolantState.mist !== true) {
throw new Error("coolant buttons did not update state");
}
doc.querySelector('[data-action="view-x"]').click();

View File

@@ -0,0 +1,164 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { createMemorySessionStorage } from "../../app/src/runtime/five-axis-session.js";
import { createLinuxCncInterpreterRuntime } from "../../app/src/runtime/linuxcnc-interpreter-runtime.js";
import { parseLinuxCncIni } from "../../app/src/runtime/linuxcnc-ini-runtime.js";
import { getFiveAxisProfile } from "../../app/src/profiles/index.js";
import { createSimulationStore } from "../../app/src/state/store.js";
function sidebarEntry(state, id) {
const entry = state.rightSidebarEntrances.find((candidate) => candidate.id === id);
assert.ok(entry, `missing right sidebar entry ${id}`);
return entry;
}
async function waitForValidatedProgram(store) {
for (let attempt = 0; attempt < 30; attempt += 1) {
const state = store.getState();
if (!state.interpreterExecutionPending && state.programValidation?.ready) {
return state;
}
await new Promise((resolve) => setTimeout(resolve, 0));
}
return store.getState();
}
async function loadIniConfigFromVendoredSource(profile) {
const text = await readFile(
new URL(`../../../wasm-port/vendor/linuxcnc/${profile.iniPath}`, import.meta.url),
"utf8",
);
return parseLinuxCncIni(text, {
path: profile.iniPath,
profileId: profile.id,
});
}
const store = createSimulationStore();
let state = store.getState();
assert.equal(state.rightSidebarEntrances.length, 9);
assert.deepEqual(
state.rightSidebarEntrances.map((entry) => entry.label),
["E-STOP", "POWER", "RESET", "AUTO", "MANUAL", "JOG", "MDI", "IDENTITY", "TCP"],
);
assert.equal(sidebarEntry(state, "power").allowed, true);
assert.equal(sidebarEntry(state, "auto").allowed, false);
assert.equal(sidebarEntry(state, "manual").allowed, false);
assert.equal(sidebarEntry(state, "jog").allowed, false);
assert.equal(sidebarEntry(state, "mdi").allowed, false);
assert.equal(sidebarEntry(state, "identity").active, true);
assert.equal(sidebarEntry(state, "identity").status, "active-blocked");
assert.equal(sidebarEntry(state, "tcp").allowed, false);
store.dispatch({ type: "ESTOP" });
state = store.getState();
assert.equal(sidebarEntry(state, "estop").status, "emergency");
assert.equal(sidebarEntry(state, "power").allowed, false);
assert.equal(sidebarEntry(state, "power").operatorMessage, "power on blocked: reset estop first");
store.dispatch({ type: "RESET" });
store.dispatch({ type: "TOGGLE_POWER" });
state = store.getState();
assert.equal(state.machine.taskState, "on");
assert.equal(sidebarEntry(state, "power").active, true);
assert.equal(sidebarEntry(state, "manual").active, true);
assert.equal(sidebarEntry(state, "jog").active, false);
assert.equal(sidebarEntry(state, "auto").allowed, false);
assert.equal(sidebarEntry(state, "auto").operatorMessage, "mode blocked: home machine before AUTO");
store.dispatch({ type: "SET_MODE", mode: "jog" });
state = store.getState();
assert.equal(state.machine.mode, "manual");
assert.equal(state.machine.manualPanel, "jog");
assert.equal(sidebarEntry(state, "manual").active, false);
assert.equal(sidebarEntry(state, "jog").active, true);
store.dispatch({ type: "HOME" });
store.dispatch({ type: "SET_MODE", mode: "auto" });
state = store.getState();
assert.equal(sidebarEntry(state, "auto").active, true);
assert.equal(sidebarEntry(state, "mdi").allowed, true);
assert.equal(sidebarEntry(state, "tcp").allowed, true);
store.dispatch({ type: "SET_KINS_TYPE", kinsType: "tcp-xyzac" });
state = store.getState();
assert.equal(state.kinsType, "tcp-xyzac");
assert.equal(state.rtcpState, "on");
assert.equal(sidebarEntry(state, "tcp").active, true);
store.dispatch({ type: "SET_KINS_TYPE", kinsType: "identity" });
assert.equal(store.getState().kinsType, "identity");
store.dispatch({
type: "LOAD_PROGRAM",
filename: "sidebar-running-gate.ngc",
content: [
"G0 X0 Y0 Z0",
"G1 X1 F100",
"G1 X2",
"G1 X3",
"G1 X4",
"G1 X5",
"G1 X6",
"G1 X7",
"G1 X8",
"G1 X9",
"M2",
].join("\n"),
});
store.dispatch({ type: "RUN" });
state = store.getState();
assert.equal(state.runState, "running");
assert.equal(sidebarEntry(state, "manual").allowed, false);
assert.equal(sidebarEntry(state, "tcp").allowed, false);
assert.equal(sidebarEntry(state, "tcp").operatorMessage, "kinematics blocked: interpreter must be idle");
store.dispatch({ type: "STOP" });
store.dispatch({ type: "SET_MODE", mode: "manual" });
const profile = getFiveAxisProfile("xyzac-trt");
const iniConfig = await loadIniConfigFromVendoredSource(profile);
store.dispatch({ type: "ATTACH_INI_CONFIG", profileId: profile.id, iniConfig });
store.dispatch({
type: "ATTACH_INTERPRETER_RUNTIME",
runtime: await createLinuxCncInterpreterRuntime(),
});
await store.stageMachineFiles({ storage: createMemorySessionStorage() });
state = store.getState();
assert.equal(state.machineProject.profileId, "xyzac-trt");
assert.equal(state.machineProject.projectRoot, "web-rtcp-5axis-sim-plan/machines/xyzac-trt");
assert.equal(state.machineProject.ini.filename, "xyzac-trt.ini");
assert.equal(state.machineProject.ini.sourceMatchesProfile, true);
assert.equal(state.machineProject.ini.textMatchesLoadedIni, true);
assert.equal(state.machineProject.configFileCount > 0, true);
assert.equal(state.machineProject.gcodeFileCount, 16);
assert.equal(state.machineProject.gcodeFiles.some((file) => file.filename === "impeller-7bl-xyzac.ngc"), true);
store.dispatch({
type: "LOAD_LINUXCNC_GCODE_SOURCE",
sourceRel: "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/impeller-7bl-xyzac.ngc",
});
state = await waitForValidatedProgram(store);
assert.equal(state.programValidation.ready, true);
assert.equal(state.programValidation.sourceGuard, "linuxcnc_vendored_5axis_gcode_source_file");
assert.equal(state.programValidation.previewSource, "linuxcnc_interpreter_canonical_motion");
assert.equal(state.programValidation.motionEventCount > 0, true);
assert.equal(state.programValidation.plannerSampleCount > 0, true);
assert.equal(state.programValidation.realtimeAxisValues.x, state.axisPose.x);
assert.equal(state.machineProject.selectedProgram.filename, "impeller-7bl-xyzac.ngc");
const xyzbcStore = createSimulationStore();
xyzbcStore.dispatch({ type: "SET_PROFILE", profileId: "xyzbc-trt" });
const xyzbcProfile = getFiveAxisProfile("xyzbc-trt");
xyzbcStore.dispatch({
type: "ATTACH_INI_CONFIG",
profileId: xyzbcProfile.id,
iniConfig: await loadIniConfigFromVendoredSource(xyzbcProfile),
});
await xyzbcStore.stageMachineFiles({ storage: createMemorySessionStorage() });
const xyzbcState = xyzbcStore.getState();
assert.equal(xyzbcState.machineProject.profileId, "xyzbc-trt");
assert.equal(xyzbcState.machineProject.ini.filename, "xyzbc-trt.ini");
assert.equal(xyzbcState.machineProject.gcodeFiles.some((file) => file.filename === "boat-xyzbc.ngc"), true);
console.log("gmoccapy_trt_project_sidebar_smoke=ok");

View File

@@ -0,0 +1,131 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import {
LINUXCNC_PARITY_ITEMS,
RIGHT_SIDEBAR_PARITY_ENTRIES,
SWITCHKINS_PARITY_CODES,
} from "../../app/src/runtime/linuxcnc-parity-matrix.js";
import { createMemorySessionStorage } from "../../app/src/runtime/five-axis-session.js";
import { createLinuxCncInterpreterRuntime } from "../../app/src/runtime/linuxcnc-interpreter-runtime.js";
import { parseLinuxCncIni } from "../../app/src/runtime/linuxcnc-ini-runtime.js";
import { getFiveAxisProfile } from "../../app/src/profiles/index.js";
import { createSimulationStore } from "../../app/src/state/store.js";
function item(matrix, id) {
const found = matrix.items.find((entry) => entry.id === id);
assert.ok(found, `missing parity matrix item ${id}`);
return found;
}
async function waitForValidatedProgram(store) {
for (let attempt = 0; attempt < 30; attempt += 1) {
const state = store.getState();
if (!state.interpreterExecutionPending && state.programValidation?.ready) {
return state;
}
await new Promise((resolve) => setTimeout(resolve, 0));
}
return store.getState();
}
async function loadIniConfigFromVendoredSource(profile) {
const text = await readFile(
new URL(`../../../wasm-port/vendor/linuxcnc/${profile.iniPath}`, import.meta.url),
"utf8",
);
return parseLinuxCncIni(text, {
path: profile.iniPath,
profileId: profile.id,
});
}
assert.equal(LINUXCNC_PARITY_ITEMS.length >= 11, true);
assert.deepEqual(RIGHT_SIDEBAR_PARITY_ENTRIES, [
"E-STOP",
"POWER",
"RESET",
"AUTO",
"MANUAL",
"JOG",
"MDI",
"IDENTITY",
"TCP",
]);
assert.deepEqual(SWITCHKINS_PARITY_CODES, ["M428", "M429", "M430"]);
const store = createSimulationStore();
let state = store.getState();
let matrix = state.linuxCncParityMatrix;
assert.equal(matrix.apiName, "web-rtcp-5axis-linuxcnc-parity-matrix");
assert.equal(matrix.profileId, "xyzac-trt");
assert.equal(matrix.status, "implemented");
assert.equal(matrix.implementedCount, matrix.itemCount);
assert.equal(matrix.rightSidebarComplete, true);
assert.equal(matrix.switchkinsComplete, true);
assert.deepEqual(matrix.actualRightSidebarEntries, RIGHT_SIDEBAR_PARITY_ENTRIES);
assert.equal(matrix.requiredSwitchkinsCodes.every((code) => matrix.activeSwitchkinsCodes.includes(code)), true);
assert.equal(matrix.linuxCncPrograms.some((path) => path.endsWith("impeller-7bl-xyzac.ngc")), true);
assert.equal(matrix.linuxCncPrograms.some((path) => path.endsWith("boat-xyzbc.ngc")), true);
assert.equal(matrix.linuxCncFunctions.includes("E-STOP/POWER/RESET task-state entry"), true);
assert.equal(matrix.linuxCncFunctions.includes("M428 TCP"), true);
assert.equal(matrix.linuxCncFunctions.includes("M429 identity"), true);
assert.equal(matrix.linuxCncFunctions.includes("M430 userk"), true);
assert.equal(item(matrix, "xyzac-trt-axis-vismach-config").active, true);
assert.equal(item(matrix, "xyzac-trt-axis-vismach-config").sourceMapped, true);
assert.equal(item(matrix, "xyzac-trt-axis-vismach-config").linuxCncPaths.some((path) => path.endsWith("xyzac-trt.ini")), true);
assert.equal(item(matrix, "xyzbc-trt-axis-vismach-config").implemented, true);
assert.equal(item(matrix, "xyzbc-trt-axis-vismach-config").active, false);
assert.equal(item(matrix, "gmoccapy-trt-config").linuxCncPaths.some((path) => path.includes("gmoccapy/non_trivial_kinematics/table-rotary-tilting/xyzac-trt.ini")), true);
assert.equal(item(matrix, "gmoccapy-trt-config").functions.includes("DISPLAY gmoccapy"), true);
assert.equal(item(matrix, "gmoccapy-native-operator-ui").active, true);
assert.equal(item(matrix, "right-sidebar-task-interlocks").active, true);
assert.equal(item(matrix, "vismach-machine-preview").active, true);
assert.equal(item(matrix, "source-derived-kinematics-switchkins").active, true);
assert.equal(item(matrix, "linuxcnc-interpreter-program-validation").active, false);
assert.equal(item(matrix, "machine-project-directory").active, false);
assert.equal(item(matrix, "task-hal-status-loop").implemented, true);
assert.equal(item(matrix, "gmoccapy-postgui-tool-spindle").active, true);
store.dispatch({ type: "SET_PROFILE", profileId: "xyzbc-trt" });
state = store.getState();
matrix = state.linuxCncParityMatrix;
assert.equal(matrix.profileId, "xyzbc-trt");
assert.equal(item(matrix, "xyzac-trt-axis-vismach-config").active, false);
assert.equal(item(matrix, "xyzbc-trt-axis-vismach-config").active, true);
assert.equal(item(matrix, "source-derived-kinematics-switchkins").evidence.activeSwitchkinsCodes.includes("M428"), true);
store.dispatch({ type: "SET_PROFILE", profileId: "xyzac-trt" });
const profile = getFiveAxisProfile("xyzac-trt");
store.dispatch({
type: "ATTACH_INI_CONFIG",
profileId: profile.id,
iniConfig: await loadIniConfigFromVendoredSource(profile),
});
store.dispatch({
type: "ATTACH_INTERPRETER_RUNTIME",
runtime: await createLinuxCncInterpreterRuntime(),
});
await store.stageMachineFiles({ storage: createMemorySessionStorage() });
state = store.getState();
matrix = state.linuxCncParityMatrix;
assert.equal(item(matrix, "machine-project-directory").active, true);
assert.equal(item(matrix, "machine-project-directory").evidence.iniSourceMatchesProfile, true);
assert.equal(item(matrix, "machine-project-directory").evidence.gcodeFileCount >= 8, true);
store.dispatch({
type: "LOAD_LINUXCNC_GCODE_SOURCE",
sourceRel: "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/impeller-7bl-xyzac.ngc",
});
state = await waitForValidatedProgram(store);
matrix = state.linuxCncParityMatrix;
assert.equal(state.programValidation.ready, true);
assert.equal(item(matrix, "linuxcnc-interpreter-program-validation").active, true);
assert.equal(item(matrix, "linuxcnc-interpreter-program-validation").evidence.sourceGuard, "linuxcnc_vendored_5axis_gcode_source_file");
assert.equal(item(matrix, "linuxcnc-interpreter-program-validation").evidence.motionEventCount > 0, true);
assert.equal(item(matrix, "linuxcnc-interpreter-program-validation").evidence.plannerSampleCount > 0, true);
assert.equal(matrix.activeCount >= 9, true);
console.log("linuxcnc_parity_matrix_smoke=ok");

View File

@@ -127,6 +127,13 @@ assert.equal(kinematicsState.rtcpFrame.sourceMode, "source-derived-kinematics-wa
assert.equal(kinematicsState.rtcpFrame.semanticBoundary, "linuxcnc_kinematics_wasm_c_abi");
assert.equal(kinematicsState.rtcpFrame.readiness.linuxCncKinematicsReady, true);
kinematicsStore.dispatch({ type: "SET_RTCP", enabled: true });
kinematicsState = kinematicsStore.getState();
assert.equal(kinematicsState.rtcpState, "off");
assert.equal(kinematicsState.operatorMessage, "kinematics blocked: machine must be on");
kinematicsStore.dispatch({ type: "TOGGLE_POWER" });
kinematicsStore.dispatch({ type: "HOME" });
kinematicsStore.dispatch({ type: "SET_RTCP", enabled: true });
kinematicsState = kinematicsStore.getState();
assert.equal(kinematicsState.rtcpState, "on");
@@ -134,8 +141,6 @@ assert.equal(kinematicsState.rtcpFrame.sourceMode, "source-derived-kinematics-wa
assert.equal(kinematicsState.lastKinematicsResult.moduleId, "xyzac-trt");
assert.equal(kinematicsState.dro.tcpX, kinematicsState.rtcpFrame.tcpPose.x);
kinematicsStore.dispatch({ type: "TOGGLE_POWER" });
kinematicsStore.dispatch({ type: "HOME" });
kinematicsStore.dispatch({ type: "SET_MODE", mode: "auto" });
kinematicsStore.dispatch({ type: "STEP" });
kinematicsState = kinematicsStore.getState();
@@ -270,6 +275,8 @@ assert.equal(store.getState().machineProfile, "xyzbc-trt");
assert.equal(store.getState().profile.traj.coordinates, "XYZBC");
const xyzbcRuntime = await createLinuxCncKinematicsRuntime({ moduleId: "xyzbc-trt" });
store.dispatch({ type: "ATTACH_KINEMATICS_RUNTIME", runtime: xyzbcRuntime });
store.dispatch({ type: "TOGGLE_POWER" });
store.dispatch({ type: "HOME" });
store.dispatch({ type: "SET_RTCP", enabled: true });
state = store.getState();
assert.equal(state.kinsType, "tcp-xyzbc");
@@ -277,6 +284,7 @@ assert.equal(state.rtcpFrame.kinematicsModuleId, "xyzbc-trt");
assert.equal(state.rtcpFrame.jointPose[3].axis, "B");
store.dispatch({ type: "SET_PROFILE", profileId: "xyzac-trt" });
store.dispatch({ type: "RESET" });
store.dispatch({ type: "RUN" });
assert.equal(store.getState().activeLine, 501);
assert.equal(store.getState().operatorMessage, "run blocked: machine must be on");