diff --git a/coverage/glb-machine-manager/program-compatibility/bc-running.png b/coverage/glb-machine-manager/program-compatibility/bc-running.png new file mode 100644 index 0000000..f41387a Binary files /dev/null and b/coverage/glb-machine-manager/program-compatibility/bc-running.png differ diff --git a/coverage/glb-machine-manager/program-compatibility/boats.report.json b/coverage/glb-machine-manager/program-compatibility/boats.report.json new file mode 100644 index 0000000..a0b588a --- /dev/null +++ b/coverage/glb-machine-manager/program-compatibility/boats.report.json @@ -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" + } + ] +} diff --git a/coverage/glb-machine-manager/program-compatibility/browser.report.json b/coverage/glb-machine-manager/program-compatibility/browser.report.json new file mode 100644 index 0000000..9159bc9 --- /dev/null +++ b/coverage/glb-machine-manager/program-compatibility/browser.report.json @@ -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": [] +} diff --git a/coverage/glb-machine-manager/program-compatibility/catalog.report.json b/coverage/glb-machine-manager/program-compatibility/catalog.report.json new file mode 100644 index 0000000..1c1bc0c --- /dev/null +++ b/coverage/glb-machine-manager/program-compatibility/catalog.report.json @@ -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 + } + ] +} diff --git a/coverage/glb-machine-manager/program-compatibility/replace.report.json b/coverage/glb-machine-manager/program-compatibility/replace.report.json new file mode 100644 index 0000000..c14b140 --- /dev/null +++ b/coverage/glb-machine-manager/program-compatibility/replace.report.json @@ -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 +} diff --git a/coverage/glb-machine-manager/program-compatibility/step.report.json b/coverage/glb-machine-manager/program-compatibility/step.report.json new file mode 100644 index 0000000..5c613e9 --- /dev/null +++ b/coverage/glb-machine-manager/program-compatibility/step.report.json @@ -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 +} diff --git a/coverage/glb-machine-manager/program-compatibility/worker.report.json b/coverage/glb-machine-manager/program-compatibility/worker.report.json new file mode 100644 index 0000000..304927f --- /dev/null +++ b/coverage/glb-machine-manager/program-compatibility/worker.report.json @@ -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 + } + ] +} diff --git a/docs/glb-machine-manager/01-项目功能内容.md b/docs/glb-machine-manager/01-项目功能内容.md index 9d11bd7..93ee0d4 100644 --- a/docs/glb-machine-manager/01-项目功能内容.md +++ b/docs/glb-machine-manager/01-项目功能内容.md @@ -56,3 +56,5 @@ 17. 工程工作台:独立滚动的结构/运动/标定页签,固定预览与底部操作区;层级搜索、轴定位、相机预设;30 步草稿撤销/重做;单轴步进;无效输入保持上一个有效值并禁止应用/导出。 18. MGR-19:对话框第一步独立选择设计机床类型 XYZAC/XYZBC;控制面板切换不覆盖设计草稿。机型限制轴绑定、运动验证和标定。保存完整机床 GLB,供控制面板“导入机床模型”自动识别机型并使用。 + +19. MGR-20:导入/应用跨机型模型时同步加载目标机型默认 G 代码,程序路径、源码、预览与运行机型保持一致;同机型标定保留兼容程序。不兼容轴程序在加载及执行入口拒绝。 diff --git a/docs/glb-machine-manager/02-项目程序开发详细步骤.md b/docs/glb-machine-manager/02-项目程序开发详细步骤.md index 632eb0a..c576246 100644 --- a/docs/glb-machine-manager/02-项目程序开发详细步骤.md +++ b/docs/glb-machine-manager/02-项目程序开发详细步骤.md @@ -21,3 +21,5 @@ 15. `MGR-18`:工作区布局与层级导航;草稿历史/资源生命周期;数值错误状态和机型标定校验;重复选中输入回归;桌面/窄屏/矮屏及 AC/BC 实时驱动验收。 16. `MGR-19`:把机型纳入草稿/撤销历史;添加首步机型选择;切换清理不适用倾转绑定;完整模型打开恢复机型;下载前调用控制面板读取器校验;验证从 AC 工作区导入 BC GLB 自动切换及 MILL700 两机型保存/运动闭环。 + +17. `MGR-20`:修正 applyGeometry 跨机型程序选择;加载前读取并验证目标程序;添加 Worker 轴兼容门控;测试双向模型导入、管理器应用、自定义程序保留、文件/编辑器拒绝和旧程序运行门控。 diff --git a/docs/glb-machine-manager/03-推进台账.md b/docs/glb-machine-manager/03-推进台账.md index 191792f..ce0eec9 100644 --- a/docs/glb-machine-manager/03-推进台账.md +++ b/docs/glb-machine-manager/03-推进台账.md @@ -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 目录程序、程序替换和单步回归通过。 diff --git a/docs/glb-machine-manager/04-任务矩阵.md b/docs/glb-machine-manager/04-任务矩阵.md index c55ba94..c9a81a8 100644 --- a/docs/glb-machine-manager/04-任务矩阵.md +++ b/docs/glb-machine-manager/04-任务矩阵.md @@ -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 验收通过| diff --git a/docs/glb-machine-manager/05-验收证据.md b/docs/glb-machine-manager/05-验收证据.md index a5f2b1b..7038a0d 100644 --- a/docs/glb-machine-manager/05-验收证据.md +++ b/docs/glb-machine-manager/05-验收证据.md @@ -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`。 diff --git a/docs/glb-machine-manager/06-决策记录.md b/docs/glb-machine-manager/06-决策记录.md index a0f3f20..5631f40 100644 --- a/docs/glb-machine-manager/06-决策记录.md +++ b/docs/glb-machine-manager/06-决策记录.md @@ -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 负责。 diff --git a/docs/glb-machine-manager/README.md b/docs/glb-machine-manager/README.md index 88b9d72..dfc23b5 100644 --- a/docs/glb-machine-manager/README.md +++ b/docs/glb-machine-manager/README.md @@ -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/`。 文档: diff --git a/scripts/test_machine_program_compatibility.mjs b/scripts/test_machine_program_compatibility.mjs new file mode 100644 index 0000000..7bfe449 --- /dev/null +++ b/scripts/test_machine_program_compatibility.mjs @@ -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)); diff --git a/web/react/src/App.tsx b/web/react/src/App.tsx index 91d6a0b..fd61753 100644 --- a/web/react/src/App.tsx +++ b/web/react/src/App.tsx @@ -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"); diff --git a/web/react/src/legacy.d.ts b/web/react/src/legacy.d.ts index f71c403..8f54f4d 100644 --- a/web/react/src/legacy.d.ts +++ b/web/react/src/legacy.d.ts @@ -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 }): { kinematics: { coordinates: string; axis_limits: Record; axis_velocity_limits: Record }; home_limits: Record }; diff --git a/web/worker/sim-session.mjs b/web/worker/sim-session.mjs index 00bef50..02d9e67 100644 --- a/web/worker/sim-session.mjs +++ b/web/worker/sim-session.mjs @@ -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?.() ?? {}; diff --git a/web/worker/test_trt_program_compatibility.mjs b/web/worker/test_trt_program_compatibility.mjs new file mode 100644 index 0000000..f2e3681 --- /dev/null +++ b/web/worker/test_trt_program_compatibility.mjs @@ -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',' #']) assert.match(programMachineMismatch(`G1X2${forbidden}${value}\nM2`,coordinates,'renamed.ngc'),/不匹配/); + for (const source of ['(A10 B20)\nG1X1 ; A30 B40\nM2','#=2\nG1X#\nM2','G1X[ABS[-1]+ATAN[1]/[2]]\nM2','O sub\nG1X1\nO 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})); diff --git a/web/workspace/program-machine-compatibility.mjs b/web/workspace/program-machine-compatibility.mjs new file mode 100644 index 0000000..82c7049 --- /dev/null +++ b/web/workspace/program-machine-compatibility.mjs @@ -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); +}