Keep G-code matched to imported AC or BC machine type

This commit is contained in:
2026-09-21 19:52:31 +08:00
parent 86cfa2b7f0
commit 0de96bbc72
20 changed files with 315 additions and 4 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

View File

@@ -0,0 +1,18 @@
{
"schema": "boat-trt-programs.v1",
"status": "verified",
"rows": [
{
"coordinates": "XYZAC",
"program": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/boat-xyzac.ngc",
"status": "completed",
"rotary": "a"
},
{
"coordinates": "XYZBC",
"program": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/boat-xyzbc.ngc",
"status": "completed",
"rotary": "b"
}
]
}

View File

@@ -0,0 +1,15 @@
{
"report_id": "TRT-PROGRAM-COMPAT-BROWSER",
"status": "verified",
"url": "http://127.0.0.1:5180/",
"checks": [
"AC to BC model import replaces source/path/preview with BC boat program",
"renamed AC file rejected on BC; previous loaded taskfile remains intact",
"editor reload also rejects wrong-axis program without replacing loaded taskfile",
"same-type reimport preserves custom compatible program",
"BC to AC model import loads AC boat",
"manager apply also switches program to matching BC family",
"BC machine runs BC G-code after home; header/controller/program agree"
],
"errors": []
}

View File

@@ -0,0 +1,72 @@
{
"schema": "rtcp-program-catalog-load.v1",
"status": "verified",
"programs": [
{
"id": "trt-boat-xyzac",
"coordinates": "XYZAC",
"source_hash": "fnv1a64:bdc95b491cd714d9",
"preview_segments": 1825
},
{
"id": "trt-impeller-xyzac",
"coordinates": "XYZAC",
"source_hash": "fnv1a64:76b5e2017a1b0a93",
"preview_segments": 4492
},
{
"id": "trt-xyzac-switchkins",
"coordinates": "XYZAC",
"source_hash": "fnv1a64:522e219683194ad2",
"preview_segments": 29
},
{
"id": "trt-xyzac-switchkins-test-1",
"coordinates": "XYZAC",
"source_hash": "fnv1a64:f1d649350d9dd63d",
"preview_segments": 17
},
{
"id": "trt-xyzac-switchkins-test-2",
"coordinates": "XYZAC",
"source_hash": "fnv1a64:00aebdd97af4c5d8",
"preview_segments": 32
},
{
"id": "trt-xyzac-switchkins-test-3",
"coordinates": "XYZAC",
"source_hash": "fnv1a64:cf7e5987249c1bf8",
"preview_segments": 3
},
{
"id": "gmoccapy-boat-xyzac",
"coordinates": "XYZAC",
"source_hash": "fnv1a64:9d09b727bef67c7c",
"preview_segments": 1825
},
{
"id": "gmoccapy-impeller-xyzac",
"coordinates": "XYZAC",
"source_hash": "fnv1a64:3e4cbd31dd06ec9c",
"preview_segments": 4492
},
{
"id": "trt-boat-xyzbc",
"coordinates": "XYZBC",
"source_hash": "fnv1a64:b1887a8c8fd29012",
"preview_segments": 1820
},
{
"id": "trt-xyzbc-switchkins",
"coordinates": "XYZBC",
"source_hash": "fnv1a64:622cce6b5c7277f1",
"preview_segments": 29
},
{
"id": "gmoccapy-boat-xyzbc",
"coordinates": "XYZBC",
"source_hash": "fnv1a64:7cef899039522872",
"preview_segments": 1820
}
]
}

View File

@@ -0,0 +1,8 @@
{
"schema": "xyzac-program-replacement.v1",
"status": "verified",
"first_segments": 1,
"failed_segments": 0,
"second_segments": 1,
"old_preview_retained_after_failure": false
}

View File

@@ -0,0 +1,18 @@
{
"schema": "xyzac-step-flow.v2",
"status": "verified",
"source": [
"axis.py::task_step",
"emctaskmain.cc::EMC_TASK_PLAN_STEP_TYPE",
"sim_linuxcnc_tp_step_line"
],
"phases": [
"start_pause",
"source_line_pause",
"source_line_motion",
"motion_step_pending",
"terminal"
],
"duplicate_step_idempotent": true,
"completed_and_aborted_rerun": true
}

View File

@@ -0,0 +1,18 @@
{
"report_id": "TRT-PROGRAM-COMPAT-WORKER",
"status": "verified",
"rows": [
{
"coordinates": "XYZAC",
"load_rejected": true,
"legacy_execution_rejected": true,
"generic_allowed": true
},
{
"coordinates": "XYZBC",
"load_rejected": true,
"legacy_execution_rejected": true,
"generic_allowed": true
}
]
}

View File

@@ -56,3 +56,5 @@
17. 工程工作台:独立滚动的结构/运动/标定页签,固定预览与底部操作区;层级搜索、轴定位、相机预设;30 步草稿撤销/重做;单轴步进;无效输入保持上一个有效值并禁止应用/导出。
18. MGR-19:对话框第一步独立选择设计机床类型 XYZAC/XYZBC;控制面板切换不覆盖设计草稿。机型限制轴绑定、运动验证和标定。保存完整机床 GLB,供控制面板“导入机床模型”自动识别机型并使用。
19. MGR-20:导入/应用跨机型模型时同步加载目标机型默认 G 代码,程序路径、源码、预览与运行机型保持一致;同机型标定保留兼容程序。不兼容轴程序在加载及执行入口拒绝。

View File

@@ -21,3 +21,5 @@
15. `MGR-18`:工作区布局与层级导航;草稿历史/资源生命周期;数值错误状态和机型标定校验;重复选中输入回归;桌面/窄屏/矮屏及 AC/BC 实时驱动验收。
16. `MGR-19`:把机型纳入草稿/撤销历史;添加首步机型选择;切换清理不适用倾转绑定;完整模型打开恢复机型;下载前调用控制面板读取器校验;验证从 AC 工作区导入 BC GLB 自动切换及 MILL700 两机型保存/运动闭环。
17. `MGR-20`:修正 applyGeometry 跨机型程序选择;加载前读取并验证目标程序;添加 Worker 轴兼容门控;测试双向模型导入、管理器应用、自定义程序保留、文件/编辑器拒绝和旧程序运行门控。

View File

@@ -111,3 +111,11 @@
- 打开已保存完整机床作为单一文档事务,自动恢复文件中的机型、关节、层级、物理零点、RTCP 参数;原草稿可撤销恢复。普通 GLB 部件导入仍为追加。
- 保存入口改为“保存机床模型 GLB”,使用 `inspectMachineGlb` 和 32 MiB 限制校验结果,确保标准导入器可读取;成功消息提示去控制面板“导入机床模型”加载。
- E15 针对跨机型闭环测试通过:在 AC 控制器下设计 BC、切换撤销、保存、重新打开、控制面板标准导入自动识别 BC。MILL700 两机型实测通过,分别 8 个同时五轴运动 tick、浏览器 warning/error=0。保存两份完整 GLB 验收示例;MGR-19 verified。
## R14 / 2026-09-21 / 机型与 G 代码匹配
- 根因:模型导入/管理器应用经 `applyGeometry` 更换工作区后,仍使用旧 `programText/programPath` 重载,导致 BC 运行 AC 程序。
- 跨机型时在工作区提交前准备目标机型 catalog 默认程序,验证资源和轴兼容性;提交后同步程序编辑器、路径、预览和 Worker。同机型保留兼容自定义程序。
- 新增共享 `programMachineMismatch`:检查文件机型标识和有效块中的不支持 A/B 轴字,忽略注释、命名参数、O 标签及表达式内部。通用三轴程序继续可用,不改写 A/B 指令。
- Worker 在 PROGRAM_LOAD 以及 RUN/STEP/RESUME 检查源程序与展开子程序;覆盖绕过 UI 和旧恢复程序。门控缓存源文本/机型/路径,避免每帧重复扫描大程序。
- E16 本地验证通过:AC→BC、BC→AC、管理器应用、错轴文件/编辑器拒绝、同机型保留、自定义程序与正确 BC 运行。11 个可加载 RTCP 目录程序、程序替换和单步回归通过。

View File

@@ -22,3 +22,4 @@
|MGR-17 单自由度与运行轴映射|verified|MGR-16|每节点固定或唯一 XYZABC 自由度;单值编辑;JSON 保存到场景/底座;单 GLB 完整应用;导出往返与 AC/BC 实时轴映射验收|
|MGR-18 工程化工作台体验|verified|MGR-17|层级搜索与定位、绑定总览、撤销/重做、输入校验、步骤页签、视图预设、单轴步进、实时跟随锁定、响应式布局和失败导入保护通过|
|MGR-19 对话框机型与保存整机|verified|MGR-18|先在对话框选择设计机型;绑定/标定受机型约束;草稿独立/可撤销;保存完整 GLB 可由控制面板导入并自动切换机型|
|MGR-20 机型程序一致性|verified|MGR-19|导入/应用跨机型时加载目标默认程序;同机型保留;错轴程序加载/运行/单步/继续拒绝;AC↔BC 浏览器及 Worker 验收通过|

View File

@@ -157,3 +157,20 @@
|`saved-models/MILL700-xyzbc.glb`|`3b87cb8504815c13127608c513eacb3536e1be1c10f9a58fcd59920939fbe0c1`|
- E15 线上验证通过:`https://49.232.250.35:58015/`,代码 `338500d`,资源 `index-C8kh60HL.js`;独立选型、切换撤销、保存完整 GLB、标准导入自动切换及物理零点恢复全部 verified、浏览器错误 0。报告 `machine-type/cloud.report.json`、`machine-type/deployment.report.json`。
## E16 / R14 / 机型与 G 代码一致性
- `node scripts/test_machine_program_compatibility.mjs`:TRT-PROGRAM-COMPAT-BROWSER verified;AC→BC 与 BC→AC 标准模型导入同步默认程序、管理器应用同步、改名错误轴文件拒绝、编辑器重载拒绝、同机型保留通用程序、回零后 BC 执行 BC 程序;浏览器 warning/error=0。
- `node web/worker/test_trt_program_compatibility.mjs`:TRT-PROGRAM-COMPAT-WORKER verified;两机型分别检查零值/参数轴指令、忽略注释与参数名称、加载拒绝不替换旧程序、旧状态 RUN/STEP/RESUME 拒绝、允许通用程序。
- 回归:`test_rtcp_program_catalog_load.mjs`(11 个主程序);`test_xyzac_program_replace.mjs`;`test_xyzac_step_flow.mjs`,全部 verified。TypeScript 和生产构建通过。
|证据(相对 program-compatibility)|SHA-256|
|---|---|
|`browser.report.json`|`c931be222b8c3f541174c26f9892083481f9dfe5c75a372e3d20b222b8a9e89f`|
|`worker.report.json`|`986fb9e127894aaf477ac6e5879c06b7084a112c840549532d39fdd6d3e8694e`|
|`catalog.report.json`|`ff2e32be3b006e2d8de902012ed89724aada54c425503027ae5e027eaf9cafe2`|
|`replace.report.json`|`fe92778b144b226cea1e91393d10e4905cfd5becf56ba9b3021b5842514c8aa7`|
|`step.report.json`|`a109eceb646fe5b974c3ac668150c19d2bbedfc2c59dd9ee702fdad46402738d`|
|`bc-running.png`|`2d4f3a6e64a230155b761df4ef15db81b99b5664e8f5d9c71857e30939ac691a`|
- `node web/worker/test_boat_trt_programs.mjs`:AC/BC 船形程序均完整运行至 completed;报告 `program-compatibility/boats.report.json`。

View File

@@ -67,3 +67,7 @@ GLB 树节点 ID 使用导入时的子节点索引路径,不使用可能重复
## D17 / 设计类型属于机床文档
机型保存在 MachineDraft,控制面板当前类型只用于首次创建时的默认值和实时数据匹配。后续面板切换不得覆盖草稿。切换设计类型不自动把 A 轴重新命名为 B:保留通用轴及模型,清除不兼容绑定,提示重新配置,并允许撤销。标定保留并按新机型验证,不静默丢弃。完整机床 GLB 打开为一次替换事务,自动恢复文件机型,原草稿可撤销。保存 GLB 必须先通过标准导入器格式/机型校验;JSON 是辅助参数文件,完整可导入交付物为 GLB。
## D18 / 跨机型加载匹配程序,不转换旋转轴
工作区切换会改变解释器轴语义,不能继续加载旧机型的 G 代码。跨机型导入/应用自动使用目标机型 catalog 指定的默认程序,目标资源在提交配置前读取和校验;同机型标定保留兼容源程序。禁止通过把 A 文本替换为 B 冒充程序适配。加载和执行门控由 Worker 再次校验,注释和参数名不算轴指令。校验范围是 AC/BC 机型隔离,完整 G 代码解析仍由 LinuxCNC WASM 负责。

View File

@@ -11,7 +11,7 @@
现有整机 GLB + 内嵌 URDF 导入能力继续保留。新管理器生成与现有 `linuxcnc.machine/1` 相同的严格结构,再通过已有配置、Worker、WASM 和 Three.js 投影链运行。
当前状态:MGR-19 对话框选择机型/整机保存已 verified;MGR-17、MGR-18 已 verified;MGR-01 至 MGR-16 已完成历史验收,MILL700 实测覆盖照明、Z-up、132 节点树、拖放、节点原点、关节局部坐标系、节点相对父级平移旋转、模型删除、轴绑定、XYZ/AC 层级运动和 WebGL 资源降级。最新台账 R13,证据 `GLB-MGR-E01/E02/E05/E07/E08/E09/E10/E12/E13/E14/E15` 位于 `coverage/glb-machine-manager/`。
当前状态:MGR-20 机型程序一致性 verified;MGR-19 对话框选择机型/整机保存已 verified;MGR-17、MGR-18 已 verified;MGR-01 至 MGR-16 已完成历史验收,MILL700 实测覆盖照明、Z-up、132 节点树、拖放、节点原点、关节局部坐标系、节点相对父级平移旋转、模型删除、轴绑定、XYZ/AC 层级运动和 WebGL 资源降级。最新台账 R14,证据 `GLB-MGR-E01/E02/E05/E07/E08/E09/E10/E12/E13/E14/E15/E16` 位于 `coverage/glb-machine-manager/`。
文档:

View File

@@ -0,0 +1,38 @@
import assert from 'node:assert/strict';
import { mkdir,readFile,writeFile } from 'node:fs/promises';
import { pathToFileURL } from 'node:url';
const imported=await import(pathToFileURL(process.env.PLAYWRIGHT_MODULE??'/home/mes006/.npm/_npx/e41f203b7505f1fb/node_modules/playwright/index.js'));
const browser=await(imported.default??imported).chromium.launch({headless:true,executablePath:'/usr/bin/google-chrome',args:['--no-sandbox','--use-angle=swiftshader','--enable-unsafe-swiftshader']});
const page=await browser.newPage({viewport:{width:1440,height:900}});const c=id=>page.locator(`[data-axis-control="${id}"]`);
const out=new URL('../coverage/glb-machine-manager/program-compatibility/',import.meta.url);await mkdir(out,{recursive:true});
const report={report_id:'TRT-PROGRAM-COMPAT-BROWSER',status:'running',url:process.env.MACHINE_MANAGER_URL??'http://127.0.0.1:5180/',checks:[],errors:[]};
page.on('pageerror',e=>report.errors.push(e.message));
page.on('console',m=>{if(['error','warning'].includes(m.type()))report.errors.push(m.text());});
const idle=()=>page.waitForFunction(()=>document.querySelector('main.axis-app')?.dataset.axisProgramLoading==='false'&&document.querySelector('main.axis-app')?.dataset.axisStateSource==='linuxcnc-wasm',null,{timeout:60000});
const checkProgram=async(coordinates)=>{
const path=`boat-${coordinates.toLowerCase()}.ngc`;
assert.match(await c('workspace-case-select').inputValue(),new RegExp(`${coordinates.toLowerCase()}-trt.ini`));
await page.waitForFunction(path=>document.querySelector('main.axis-app')?.dataset.axisProgramPath?.endsWith(path),path);
assert.match(await page.locator('.case-status').textContent(),new RegExp(path));
const text=await c('program-editor').inputValue();assert.match(text,new RegExp(`${coordinates[3]}[-+0-9.]`,'i'));
const other=coordinates[3]==='A'?'B':'A'; assert.equal(new RegExp(`\\b${other}[-+0-9.]`,'i').test(text.replace(/\([^)]*\)/g,'').replace(/;.*/g,'')),false);
};
const importBc=async()=>{await c('machine-glb-settings').click();await c('machine-glb-file').setInputFiles(new URL('../coverage/glb-machine-manager/machine-type/test-xyzbc.glb',import.meta.url).pathname);await idle();await page.waitForFunction(()=>document.querySelector('[data-axis-control="workspace-case-select"]')?.value.includes('xyzbc-trt'));await page.keyboard.press('Escape');};
try{
await page.goto(report.url);await idle();await checkProgram('XYZAC');
await importBc();await checkProgram('XYZBC');report.checks.push('AC to BC model import replaces source/path/preview with BC boat program');
await c('program-file-input').setInputFiles({name:'renamed.ngc',mimeType:'text/plain',buffer:Buffer.from('G21 G90\nG1 X1 A0 C10 F120\nM2\n')});await idle();assert.match(await page.locator('.case-status').textContent(),/不匹配/);assert.match(await page.locator('main.axis-app').getAttribute('data-axis-program-path'),/boat-xyzbc.ngc$/);
report.checks.push('renamed AC file rejected on BC; previous loaded taskfile remains intact');
const beforeReload=await c('program-editor').inputValue();await c('program-editor').fill('G21 G90\nG1 A10 F120\nM2\n');await c('toolbar-reload').click();await idle();assert.match(await page.locator('.preview-load-indicator').textContent(),/不匹配/);assert.match(await page.locator('main.axis-app').getAttribute('data-axis-program-path'),/boat-xyzbc.ngc$/);await c('program-editor').fill(beforeReload);
report.checks.push('editor reload also rejects wrong-axis program without replacing loaded taskfile');
const generic='G21 G90\nG1 X1 Y2 F120\nM2\n';await c('program-file-input').setInputFiles({name:'generic.ngc',mimeType:'text/plain',buffer:Buffer.from(generic)});await idle();
await importBc();assert.equal(await c('program-editor').inputValue(),generic);assert.equal(await page.locator('main.axis-app').getAttribute('data-axis-program-path'),'browser/generic.ngc');report.checks.push('same-type reimport preserves custom compatible program');
await c('machine-glb-settings').click();await c('machine-glb-ac').click();await idle();await page.keyboard.press('Escape');await checkProgram('XYZAC');report.checks.push('BC to AC model import loads AC boat');
await c('program-file-input').setInputFiles({name:'wrong-b.ngc',mimeType:'text/plain',buffer:Buffer.from('G21 G90\nG1 B0 X1 F120\nM2\n')});await idle();assert.match(await page.locator('.case-status').textContent(),/不匹配/);
await c('workspace-load-boat').click();await idle();await checkProgram('XYZAC');
// Saved model applied from manager must follow the same configuration transaction.
await c('machine-manager-settings').click();await c('machine-manager-files').setInputFiles(new URL('../coverage/glb-machine-manager/machine-type/test-xyzbc.glb',import.meta.url).pathname);await page.waitForFunction(()=>document.querySelector('[data-axis-control="machine-manager-type"]')?.value==='XYZBC');await c('machine-manager-apply').click();await idle();await page.keyboard.press('Escape');await checkProgram('XYZBC');report.checks.push('manager apply also switches program to matching BC family');
await c('toolbar-power').click();await page.waitForFunction(()=>document.querySelector('main.axis-app')?.dataset.axisStateMachine==='ON');await c('manual-home-all').click();await page.waitForFunction(()=>document.querySelector('main.axis-app')?.dataset.axisHomed==='[true,true,true,true,true]',null,{timeout:60000});await c('toolbar-run').click();await page.waitForFunction(()=>document.querySelector('main.axis-app')?.dataset.axisProgramStatus==='running');await checkProgram('XYZBC');await page.waitForFunction(()=>Number(document.querySelector('main.axis-app')?.dataset.axisCurrentLine)>0 && JSON.parse(document.querySelector('.xyzac-machine-view')?.dataset.xyzacJoints??'[]').some(v=>Math.abs(v)>1e-6),null,{timeout:60000});await page.screenshot({path:new URL('bc-running.png',out).pathname});
report.checks.push('BC machine runs BC G-code after home; header/controller/program agree');assert.deepEqual(report.errors,[]);report.status='verified';
}catch(e){report.status='failed';report.error=e.stack;await page.screenshot({path:new URL('failure.png',out).pathname}).catch(()=>{});throw e;}finally{await writeFile(new URL('browser.report.json',out),JSON.stringify(report,null,2)+'\n');await browser.close();}
console.log(JSON.stringify(report));

View File

@@ -18,6 +18,7 @@ import { XyzacDiagnostics, XyzacLiveParameters, XyzacProgramPane, XyzacTrtPanel
import { projectXyzacPanel, type XyzacOperationTrace } from "./xyzac-panel-contract";
import { DEFAULT_MACHINE, MACHINE_DEFINITIONS, machineDefinition } from "../../workspace/machine-definitions.mjs";
import { RtcpProgramLibrary, type RtcpProgram } from "./RtcpProgramLibrary";
import { assertProgramMachine } from "../../workspace/program-machine-compatibility.mjs";
import { MachineManagerPanel } from "./MachineManagerPanel";
export const SELECTED_XYZAC_CASE = DEFAULT_MACHINE.entrypoint;
@@ -536,6 +537,13 @@ export function App() {
const bundle = typeof source === "function" ? await source() : source;
readTrtGeometry(bundle);
const nextModel = model ? model.current : importedMachine ? updateImportedMachineGeometry(importedMachine, bundle) : null;
const targetMachine = machineDefinition(bundle.entrypoint);
if (!targetMachine) throw new Error("目标机床类型无法识别");
const machineChanged = targetMachine.coordinates !== selectedMachine.coordinates;
const nextProgramPath = machineChanged ? targetMachine.program : programPath;
const nextProgramSource = machineChanged ? normalizeProgramSource(bundle.files?.[nextProgramPath] ?? "") : programText;
if (!nextProgramSource.trim()) throw new Error("目标机型程序资源缺失");
assertProgramMachine(nextProgramSource, targetMachine.coordinates, nextProgramPath);
const configuration = toWorkerConfiguration(bundle);
startPreviewProgress("机床参数");
workspaceRequested = true;
@@ -545,9 +553,11 @@ export function App() {
setCasePath(bundle.entrypoint);
setImportedMachine(nextModel);
if (model) model.committed = true;
const loaded = await operationController.execute("PROGRAM_LOAD", { source: programText, path: programPath }, { gate: false });
finishPreviewProgress(programPath, loaded.response);
setCommandStatus("机床参数已应用,程序已重新解析,待重新回零");
setProgramText(nextProgramSource); setProgramPath(nextProgramPath);
const loaded = await operationController.execute("PROGRAM_LOAD", { source: nextProgramSource, path: nextProgramPath }, { gate: false });
finishPreviewProgress(nextProgramPath, loaded.response);
setCaseStatus(`${targetMachine.coordinates}: ${nextProgramPath} loaded through linuxcnc-wasm`);
setCommandStatus(machineChanged ? `已切换为 ${targetMachine.coordinates} 并加载对应程序,请重新回零` : "机床参数已应用,程序已重新解析,待重新回零");
} catch (error) {
if (workspaceRequested) failPreviewProgress(error instanceof Error ? error.message : String(error));
else setCommandStatus(`机床配置未应用:${error instanceof Error ? error.message : String(error)}`);
@@ -656,6 +666,7 @@ export function App() {
.then(() => {
if (!latestLoadGate.current.enabled) throw new Error(latestLoadGate.current.reason);
markPreviewProgress(38, "wasm", "LinuxCNC WASM 正在解析刀具路径");
assertProgramMachine(programText, selectedMachine.coordinates, programPath);
return operationController.execute("PROGRAM_LOAD", { source: programText, path: programPath }, { state: axisState, gate: false });
})
.then((operation) => {
@@ -683,6 +694,7 @@ export function App() {
if (!source.trim()) throw new Error("program_file_empty");
if (!latestLoadGate.current.enabled) throw new Error(latestLoadGate.current.reason);
const path = file.name === "3D_Chips.ngc" ? "linuxcnc/nc_files/3D_Chips.ngc" : `browser/${file.name}`;
assertProgramMachine(source, selectedMachine.coordinates, path);
setProgramText(source);
setProgramPath(path);
setCommandStatus("PROGRAM_LOAD pending: waiting for LinuxCNC WASM preview");

View File

@@ -1,4 +1,5 @@
declare module "*.mjs" {
export function assertProgramMachine(source: string, coordinates: string, path?: string): void;
export const WEB_SETTINGS: typeof import("../../workspace/web-settings.json");
export function axisImagePath(name: string, base?: string): string;
export function readTrtConfiguration(configuration: { entrypoint: string; files: Record<string, string> }): { kinematics: { coordinates: string; axis_limits: Record<string, number[]>; axis_velocity_limits: Record<string, number> }; home_limits: Record<string, any> };

View File

@@ -1,3 +1,4 @@
import { programMachineMismatch } from "../workspace/program-machine-compatibility.mjs";
import { exportNativeCheckpoint, restoreNativeCheckpoint, validateNativeCheckpoint } from "./native-checkpoint.mjs";
import { ReplayTimeline, fnv1a64 } from "./replay.mjs";
import { capabilities, validateLimit } from "./capabilities.mjs";
@@ -373,6 +374,8 @@ export class SimSession {
const path = `${basePath.replace(/\/demos$/, "")}/${suffix}`;
const source = this.workspaceFiles[path];
if (typeof source !== "string") return { status: "error", code: "program_subroutine_missing", path };
const mismatch = programMachineMismatch(source, coordinates, path);
if (mismatch) return { status: "error", code: "program_machine_mismatch", reason: mismatch, path };
const added = this.xyzacProgram.addSubroutine(name, source, path);
if (added.status !== "verified") return added;
}
@@ -604,6 +607,18 @@ export class SimSession {
}
xyzacCommandGate(input) {
const coordinates = this.machineDefinition?.coordinates ?? DEFAULT_MACHINE.coordinates;
let mismatch = "";
if (input.kind === "PROGRAM_LOAD") {
mismatch = programMachineMismatch(input.payload?.source ?? input.payload?.text ?? input.payload?.program_source, coordinates, input.payload?.path);
} else if (["PROGRAM_RUN", "PROGRAM_STEP", "PROGRAM_RESUME"].includes(input.kind)) {
const source = this.xyzacProgram.executionSource || this.xyzacProgram.source;
const path = this.xyzacProgram.path;
const previous = this.programCompatibility;
if (!previous || previous.source !== source || previous.path !== path || previous.coordinates !== coordinates) this.programCompatibility = { source, path, coordinates, mismatch: programMachineMismatch(source, coordinates, path) };
mismatch = this.programCompatibility.mismatch;
}
if (mismatch) return { enabled: false, reason: mismatch, action: PANEL_GATE_ACTIONS[input.kind] ?? null };
const task = this.task.export();
const program = this.xyzacProgram.snapshot();
const io = this.xyzacIo?.canonical?.() ?? {};

View File

@@ -0,0 +1,32 @@
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import createModule from '../../build-wasm/linuxcnc-sim-core.js';
import { loadSimulationCase, toWorkerConfiguration } from '../workspace/sim-case-loader.mjs';
import { SimSession, CONTEXT_ID } from './sim-session.mjs';
import { programMachineMismatch } from '../workspace/program-machine-compatibility.mjs';
for (const [coordinates, forbidden] of [['XYZAC','B'], ['XYZBC','A']]) {
for (const value of ['0','-12','[1+2]','#1',' #<angle>']) assert.match(programMachineMismatch(`G1X2${forbidden}${value}\nM2`,coordinates,'renamed.ngc'),/不匹配/);
for (const source of ['(A10 B20)\nG1X1 ; A30 B40\nM2','#<A1>=2\nG1X#<A1>\nM2','G1X[ABS[-1]+ATAN[1]/[2]]\nM2','O<ABC1> sub\nG1X1\nO<ABC1> endsub']) assert.equal(programMachineMismatch(source,coordinates,'generic.ngc'),'');
}
const fetchImpl=async(url)=>{try{const body=await readFile(new URL(`../../${String(url).replace(/^\//,'')}`,import.meta.url));return{ok:true,text:async()=>body.toString()};}catch{return{ok:false,text:async()=>''};}};
const core=await createModule({wasmBinary:await readFile(new URL('../../build-wasm/linuxcnc-sim-core.wasm',import.meta.url))});
const session=new SimSession(core,{sessionId:'compatibility'});let seq=0;
const send=(kind,payload={})=>session.accept({schema_version:1,context_id:CONTEXT_ID,session_id:session.sessionId,kind,seq:++seq,operation_id:`compat-${seq}`,payload});
send('HELLO');send('SESSION_ATTACH');const rows=[];
for(const coordinates of ['XYZAC','XYZBC']){
const bundle=await loadSimulationCase({path:`linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/${coordinates.toLowerCase()}-trt.ini`,fetchImpl});
assert.equal(send('LOAD_WORKSPACE',{root_path:'/workspace',configuration:toWorkerConfiguration(bundle)}).kind,'COMMAND_RESULT');
const own=coordinates[3],other=own==='A'?'B':'A';
assert.equal(send('PROGRAM_LOAD',{source:`G21 G90\nG1 X1 ${own}5 C3 F120\nM2\n`,path:'compatible.ngc'}).kind,'COMMAND_RESULT');
const previous=session.xyzacProgram.source;
for(const source of [`G1${other}0 F120\nM2\n`,`G1${other}#1 F120\nM2\n`]){
const rejected=send('PROGRAM_LOAD',{source,path:'renamed.ngc'});assert.equal(rejected.kind,'ERROR');assert.match(rejected.payload.gate_reason,/不匹配/);assert.equal(session.xyzacProgram.source,previous);
}
// Simulate a legacy/recovered taskfile bypassing the loading gate.
session.xyzacProgram.source=`G1 ${other}10 F120\nM2\n`;session.xyzacProgram.executionSource=session.xyzacProgram.source;
for(const kind of ['PROGRAM_RUN','PROGRAM_STEP','PROGRAM_RESUME']){const rejected=send(kind);assert.equal(rejected.kind,'ERROR');assert.match(rejected.payload.gate_reason,/不匹配/);}
session.xyzacProgram.source=previous;session.xyzacProgram.executionSource=previous;
assert.equal(send('PROGRAM_LOAD',{source:'(A and B mentioned in comments)\nG21 G90\nG1 X1 F120\nM2\n',path:'generic.ngc'}).kind,'COMMAND_RESULT');
rows.push({coordinates,load_rejected:true,legacy_execution_rejected:true,generic_allowed:true});
}
console.log(JSON.stringify({report_id:'TRT-PROGRAM-COMPAT-WORKER',status:'verified',rows}));

View File

@@ -0,0 +1,30 @@
// This is a machine-family guard, not a replacement for the LinuxCNC parser.
// Ignore comments, named parameters, O labels and bracket expression contents;
// inspect address words at block level, including zero and parameter values.
export function programMachineMismatch(source, coordinates, path = "") {
if (!["XYZAC", "XYZBC"].includes(coordinates) || typeof source !== "string") return "";
const family = String(path).match(/xyz[ab]c/ig)?.map((s) => s.toUpperCase()) ?? [];
if (family.some((value) => value !== coordinates)) return `程序机型不匹配:${path} 不适用于 ${coordinates}`;
let comment = 0, bracket = 0, semicolon = false, named = false, code = "";
for (const char of source) {
if (char === '\n' || char === '\r') { semicolon = false; code += '\n'; continue; }
if (semicolon) continue;
if (char === '(') { comment++; continue; }
if (comment) { if (char === ')') comment--; continue; }
if (char === ';') { semicolon = true; continue; }
if (char === '<') { named = true; code += ' '; continue; }
if (named) { if (char === '>') named = false; continue; }
if (char === '[') { if (!bracket) code += '['; bracket++; continue; }
if (bracket) { if (char === ']') bracket--; continue; }
code += char;
}
const forbidden = coordinates === "XYZBC" ? "A" : "B";
const pattern = new RegExp(`${forbidden}\\s*(?=[+\\-\\d.#\\[])`, "i");
const lines = code.split('\n');
const index = lines.findIndex((line) => pattern.test(line));
return index < 0 ? "" : `程序机型不匹配:${coordinates} 不支持 ${forbidden} 轴指令(第 ${index + 1} 行),请选择对应机型的 G 代码`;
}
export function assertProgramMachine(source, coordinates, path = "") {
const error = programMachineMismatch(source, coordinates, path);
if (error) throw new Error(error);
}