Plan full LinuxCNC TP numerical parity
This commit is contained in:
29
coverage/task-packets/TP-PARITY-00.handoff.json
Normal file
29
coverage/task-packets/TP-PARITY-00.handoff.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"schema": "task-handoff.v1",
|
||||
"task_id": "TP-PARITY-00",
|
||||
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
|
||||
"status": "blocked",
|
||||
"scope": "xyzac_only",
|
||||
"result": "run-5axis-rtcp.sh launches native LinuxCNC services and AXIS, but linuxcnc.stat cannot attach to the launcher status buffer on this host; no native per-tick oracle was generated",
|
||||
"report": {"path": "coverage/xyzac-native-tick-trace.report.json", "status": "blocked"},
|
||||
"outputs": [
|
||||
{"path": "scripts/run_xyzac_native_tick_trace.py", "status": "created"},
|
||||
{"path": "doc-rtcp/xyzac-trt-full-tp-parity-plan.md", "status": "created"},
|
||||
{"path": "coverage/task-packets/TP-PARITY-00.json", "status": "created"}
|
||||
],
|
||||
"checks": {
|
||||
"launcher": "run-5axis-rtcp.sh started native linuxcncsvr/io/halui/milltask/axis",
|
||||
"axis_window": "visible during manual probe",
|
||||
"stat_attach": "blocked: emcStatusBuffer invalid err=3 / taskbeat invalid",
|
||||
"native_trace": "blocked",
|
||||
"source_hashes": "recorded by collector",
|
||||
"full_parity_claim": "forbidden"
|
||||
},
|
||||
"failure_samples": [
|
||||
{"code": "NATIVE_STAT_BUFFER_INVALID", "detail": "linuxcnc.stat().poll() returned emcStatusBuffer invalid err=3 or SystemError without exception while run-5axis-rtcp.sh services were alive"},
|
||||
{"code": "NATIVE_TASKBEAT_INVALID", "detail": "taskbeat remained 4294967296 and no valid native tick could be selected"}
|
||||
],
|
||||
"next_task": "TP-PARITY-01",
|
||||
"next_task_status": "blocked_by_TP-PARITY-00",
|
||||
"release_status": "STOP_RELEASE"
|
||||
}
|
||||
59
coverage/task-packets/TP-PARITY-00.json
Normal file
59
coverage/task-packets/TP-PARITY-00.json
Normal file
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"schema": "task-packet.v1",
|
||||
"task_id": "TP-PARITY-00",
|
||||
"status": "blocked",
|
||||
"title": "Native LinuxCNC XYZAC per-tick oracle",
|
||||
"milestone": "M7-TP-NUMERICAL-PARITY",
|
||||
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
|
||||
"case": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",
|
||||
"scope": "xyzac_only",
|
||||
"depends_on": ["RELEASE-00"],
|
||||
"direct_inputs": [
|
||||
"run-5axis-rtcp.sh",
|
||||
"scripts/run_axis3_tutorial_native_sequence.py",
|
||||
"linuxcnc/src/emc/usr_intf/axis/extensions/emcmodule.cc",
|
||||
"linuxcnc/src/emc/task/taskintf.cc",
|
||||
"linuxcnc/src/emc/motion/motion.h",
|
||||
"linuxcnc/src/emc/tp/tp_debug.h",
|
||||
"linuxcnc/src/emc/tp/tp.c",
|
||||
"linuxcnc/src/emc/tp/tcq.c",
|
||||
"linuxcnc/src/emc/tp/tc.c",
|
||||
"doc-rtcp/xyzac-trt-full-tp-parity-plan.md"
|
||||
],
|
||||
"required_outputs": [
|
||||
"scripts/run_xyzac_native_tick_trace.py",
|
||||
"coverage/xyzac-native-tick-trace.report.json",
|
||||
"coverage/task-packets/TP-PARITY-00.handoff.json"
|
||||
],
|
||||
"observed": {
|
||||
"report": "coverage/xyzac-native-tick-trace.report.json",
|
||||
"report_sha256": "812a0bb8de8438aa18bc16789c40a33bcaadfaf3fcdc704ecf109c4b77bfdbfd",
|
||||
"row_count": 0,
|
||||
"first_blocker": "NATIVE_STAT_BUFFER_INVALID"
|
||||
},
|
||||
"acceptance": [
|
||||
"run-5axis-rtcp.sh launches native LinuxCNC",
|
||||
"linuxcnc.stat captures task/interpreter/motion/queue/pose/joint/current-line fields",
|
||||
"tick source is native taskbeat or explicitly blocked with evidence",
|
||||
"empty queue and queue drain ticks are retained",
|
||||
"source/config/program hashes are recorded",
|
||||
"native trace report is self-hashed",
|
||||
"no claim of full parity before TP-PARITY-06 passes"
|
||||
],
|
||||
"next_tasks": [
|
||||
"TP-PARITY-01",
|
||||
"TP-PARITY-02",
|
||||
"TP-PARITY-03",
|
||||
"TP-PARITY-04",
|
||||
"TP-PARITY-05",
|
||||
"TP-PARITY-06",
|
||||
"TP-PARITY-07",
|
||||
"TP-PARITY-08"
|
||||
],
|
||||
"known_blockers": [
|
||||
"native stat API may not expose TP segment geometry; map motion_id and retain explicit unavailable field",
|
||||
"native GUI process requires DISPLAY and cleanup",
|
||||
"full LinuxCNC TP source integration may require ABI-compatible C port rather than simplified queue extension"
|
||||
],
|
||||
"release_status": "STOP_RELEASE"
|
||||
}
|
||||
83
coverage/xyzac-native-tick-trace.report.json
Normal file
83
coverage/xyzac-native-tick-trace.report.json
Normal file
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"case": {
|
||||
"path": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",
|
||||
"scope": "xyzac_only"
|
||||
},
|
||||
"cleanup": {
|
||||
"commands": [
|
||||
{
|
||||
"command": [
|
||||
"axis-remote",
|
||||
"--quit"
|
||||
],
|
||||
"returncode": 1,
|
||||
"stdout_sha256": "e887c3a096d768c45173b5087b76b19f871abedface8d6a05f107a2dbb93b3f9"
|
||||
},
|
||||
{
|
||||
"command": [
|
||||
"halcmd",
|
||||
"stop"
|
||||
],
|
||||
"returncode": 0,
|
||||
"stdout_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"command": [
|
||||
"halcmd",
|
||||
"unload",
|
||||
"all"
|
||||
],
|
||||
"returncode": 0,
|
||||
"stdout_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"command": [
|
||||
"/usr/lib/linuxcnc/realtime",
|
||||
"stop"
|
||||
],
|
||||
"returncode": 0,
|
||||
"stdout_sha256": "91338655380567a02b2f78410c6af8bb22361915c68b22572d9a09389d9599ab"
|
||||
}
|
||||
],
|
||||
"killed_pids": [],
|
||||
"lock_path": "/tmp/linuxcnc.lock",
|
||||
"lock_removed": false,
|
||||
"remaining_services": []
|
||||
},
|
||||
"errors": [
|
||||
"RuntimeError:native_stat_transition_timeout"
|
||||
],
|
||||
"known_boundaries": [
|
||||
"native stat exposes motion_id but not full TP segment geometry; active_segment_geometry is explicit null",
|
||||
"taskbeat is the public native task-cycle counter; wall-clock is recorded only as diagnostic elapsed_ms",
|
||||
"this report is an oracle input and does not claim native/WASM parity by itself"
|
||||
],
|
||||
"launcher": [
|
||||
"./run-5axis-rtcp.sh",
|
||||
"xyzac-trt.ini"
|
||||
],
|
||||
"launcher_output_sha256": "8e18cd62ead0174b8c9c7c70b4fa62716ee066bb0984de99e288322ae6aecaa2",
|
||||
"launcher_output_tail": "Starting 5-axis RTCP case: /home/mes006/linuxcncWebWASM/linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini\nDemo program: /home/mes006/linuxcncWebWASM/linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins.ngc\nRTCP subroutine: /home/mes006/linuxcncWebWASM/linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/helix_ac.ngc\nMachine configuration file is 'xyzac-trt.ini'\ncheck_config: Unchecked: [KINS]KINEMATICS={xyzac-trt-kins sparm=identityfirst}\nStarting LinuxCNC...\nlinuxcnc TPMOD=tpmod HOMEMOD=homemod EMCMOT=motmod\nNote: Using POSIX non-realtime\nFound file(LIB): /home/mes006/linuxcncWebWASM/linuxcnc/lib/hallib/basic_sim.tcl\nsetup_kins: cmd=loadrt xyzac-trt-kins sparm=identityfirst\nnote: MAXV max: 1000.000 units/sec 60000.000 units/min\nnote: LJOG max: 1000.000 units/sec 60000.000 units/min\nnote: LJOG default: 20.000 units/sec 1200.000 units/min\nnote: AJOG max: 360.000 units/sec 21600.000 units/min\nnote: AJOG default: 360.000 units/sec 21600.000 units/min\nnote: jog_order='XYZC'\nnote: jog_invert=set()\ntask: 0 cycles, min=0.000004, max=0.062398, avg=-1.000000, 0 latency excursions (> 10x expected cycle time of 0.010000s)\nTraceback (most recent call last):\n File \"/home/mes006/linuxcncWebWASM/linuxcnc/bin/axis\", line 4249, in <module>\n o.mainloop()\n File \"/usr/lib/python3.12/tkinter/__init__.py\", line 1504, in mainloop\n self.tk.mainloop(n)\n File \"/usr/lib/python3.12/tkinter/__init__.py\", line 1962, in __call__\n def __call__(self, *args):\n\nKeyboardInterrupt\nShutting down and cleaning up LinuxCNC...\n\n!!! switchkins-type 0 is IDENTITY\n\nidentityKinematicsSetup: coordinates:xyzac\n Joint 0 ==> Axis X\n Joint 1 ==> Axis Y\n Joint 2 ==> Axis Z\n Joint 3 ==> Axis A\n Joint 4 ==> Axis C\n\nxyzac-trt-kins coordinates=xyzac assigns:\n Joint 0 ==> Axis X\n Joint 1 ==> Axis Y\n Joint 2 ==> Axis Z\n Joint 3 ==> Axis A\n Joint 4 ==> Axis C\n\nuserkKinematicsSetup:\n emc/kinematics/userkfuncs.c <xyzac> max_joints=16 allow_duplicates=1\n\nNote: Using POSIX non-realtime",
|
||||
"native_axis_window": {
|
||||
"height": 1377,
|
||||
"raw": "0x4c0001f \"AXIS 2.10.0~pre0 on sim-xyzac-trt-kins (switchkins) (no file)\": (\"axis\" \"Axis\") 1552x1377+28+98 +280+286",
|
||||
"title": "AXIS 2.10.0~pre0 on sim-xyzac-trt-kins (switchkins) (no file)",
|
||||
"width": 1552
|
||||
},
|
||||
"native_linuxcnc": {
|
||||
"display": ":1",
|
||||
"python_module": "/usr/lib/python3/dist-packages/linuxcnc.cpython-312-x86_64-linux-gnu.so"
|
||||
},
|
||||
"program": {
|
||||
"path": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins.ngc",
|
||||
"sha256": "5afc03d5c1b692edd4154a6d37e5f94d74936a7bd5e82dc9c022f7bc21c7dc17"
|
||||
},
|
||||
"release_status": "STOP_RELEASE",
|
||||
"report_sha256": "812a0bb8de8438aa18bc16789c40a33bcaadfaf3fcdc704ecf109c4b77bfdbfd",
|
||||
"row_count": 0,
|
||||
"rows": [],
|
||||
"schema": "xyzac-native-linuxcnc-tick-trace.v1",
|
||||
"status": "blocked",
|
||||
"tick_source": "linuxcnc.stat.taskbeat",
|
||||
"trace_sha256": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945"
|
||||
}
|
||||
178
doc-rtcp/xyzac-trt-full-tp-parity-plan.md
Normal file
178
doc-rtcp/xyzac-trt-full-tp-parity-plan.md
Normal file
@@ -0,0 +1,178 @@
|
||||
# XYZAC TRT 完整 TP/前瞻数值对标任务轨道
|
||||
|
||||
状态:`TP-PARITY-00` 已建立,完整 LinuxCNC 数值对标尚未完成。
|
||||
|
||||
目标不是证明页面能绘制一条曲线,而是对同一 `xyzac-trt.ini`、同一 G-code、同一初始状态,比较 native LinuxCNC 与 WASM Worker 的每个运动 tick:
|
||||
|
||||
```text
|
||||
source G-code
|
||||
-> LinuxCNC RS-274 canonical output
|
||||
-> LinuxCNC TP/lookahead/blend
|
||||
-> motion/kinematics
|
||||
-> pose/joints/velocity/active motion/queue/current line
|
||||
```
|
||||
|
||||
Web 端只有在该链路和 native trace 逐字段一致后,才能声明 `PARITY_VERIFIED`。Three.js、React 和浏览器时间不能作为运动数据来源。
|
||||
|
||||
## 任务分解
|
||||
|
||||
### TP-PARITY-00:Native LinuxCNC 逐 tick oracle
|
||||
|
||||
来源:
|
||||
|
||||
- `run-5axis-rtcp.sh`
|
||||
- `linuxcnc/src/emc/usr_intf/axis/extensions/emcmodule.cc`
|
||||
- `linuxcnc/src/emc/task/taskintf.cc`
|
||||
- `linuxcnc/src/emc/motion/motion.h`
|
||||
- `linuxcnc/src/emc/tp/tp_debug.h`
|
||||
- `linuxcnc/src/emc/tp/tp.c`
|
||||
- `linuxcnc/src/emc/tp/tcq.c`
|
||||
- `linuxcnc/src/emc/tp/tc.c`
|
||||
|
||||
交付:
|
||||
|
||||
- `scripts/run_xyzac_native_tick_trace.py`
|
||||
- `coverage/xyzac-native-tick-trace.report.json`
|
||||
- native trace JSONL,包含 `tick/taskbeat`、task/interpreter/exec state、current/motion/read line、actual/commanded pose、joint actual pose、velocity、acceleration、queue、active queue、queue full、motion id、feed/rapid override 和 G-code source hash。
|
||||
|
||||
验收:
|
||||
|
||||
- 必须由 `./run-5axis-rtcp.sh` 启动 native LinuxCNC。
|
||||
- 使用 `linuxcnc.stat()` 读取状态,不能从 GUI 截图推断运动值。
|
||||
- trace 必须保留重复 tick、空 queue tick 和程序结束排空阶段。
|
||||
- `motion_id` 映射为 native active motion identifier;若 native API 不提供 segment geometry,必须显式记录 `active_segment_geometry: unavailable`,不能伪造。
|
||||
|
||||
### TP-PARITY-01:锁定 LinuxCNC TP 数据模型和 C ABI
|
||||
|
||||
来源:`linuxcnc/src/emc/tp/{tp.c,tp.h,tp_types.h,tc.c,tcq.c,tc_types.h,blendmath.c,blendmath.h,spherical_arc.c}`。
|
||||
|
||||
工作:
|
||||
|
||||
- 逐项登记 `TP_STRUCT`、`TC_STRUCT`、termination condition、queue depth、active depth、current velocity、acceleration、jerk、tolerance 和 motion id。
|
||||
- 决定采用源码等价移植还是直接编译可移植 LinuxCNC TP 源码到 WASM;优先保留原始 C 算法和数据布局,避免再写第二套近似 planner。
|
||||
- 为每个 ABI 增加 native/WASM size、endianness、state hash 和 checkpoint restore 测试。
|
||||
|
||||
退出条件:native 和 WASM 对同一静态 TC 队列的 enqueue、activate、step、pop 逐字段一致。
|
||||
|
||||
### TP-PARITY-02:G64/G61/G61.1 完整路径控制
|
||||
|
||||
来源:
|
||||
|
||||
- `linuxcnc/src/emc/rs274ngc/interp_convert.cc` 的 G64/G61 处理
|
||||
- `linuxcnc/src/emc/rs274ngc/interp_internal.hh` 的 `tolerance`/`naivecam_tolerance`
|
||||
- `linuxcnc/src/emc/tp/tp.c` 的 `tpSetTermCond` 和 termination condition
|
||||
|
||||
工作:
|
||||
|
||||
- `G61`:exact path;`G61.1`:exact stop;`G64`:blend;`G64 P... Q...`:路径和 naive-cam tolerance。
|
||||
- 保存 modal 状态、每个 canonical motion 的 termination condition 和 tolerance。
|
||||
- 增加直线-直线、直线-圆弧、圆弧-直线、圆弧-圆弧、急转角、短段、零容差和非法 P/Q 样本。
|
||||
|
||||
退出条件:native/WASM 每条 TC 的 termination mode、tolerance、blend point、速度和完成 tick 一致。
|
||||
|
||||
### TP-PARITY-03:Lookahead、corner blending、acceleration/jerk
|
||||
|
||||
来源:
|
||||
|
||||
- `linuxcnc/src/emc/tp/tp.c`:target/final velocity、activate、queue starvation、periodic update
|
||||
- `linuxcnc/src/emc/tp/blendmath.c`:cornering speed、blend geometry、acceleration/jerk bound
|
||||
- `linuxcnc/src/emc/tp/tc.c`:segment acceleration and interpolation
|
||||
- `linuxcnc/src/emc/motion/control.c`:trajectory period and current velocity
|
||||
|
||||
工作:
|
||||
|
||||
- 移除当前 `lookahead=1` 的最小近似,复用或等价移植 LinuxCNC 的完整 active-depth/lookahead。
|
||||
- 按 LinuxCNC 的 blend geometry 计算 corner speed 和 transition segment。
|
||||
- 保持 queue starvation、pause、abort、feed hold、queue drain 的真实语义。
|
||||
- acceleration、jerk、cycle time、velocity limit 从 INI/TRAJ 配置进入同一 planner。
|
||||
|
||||
退出条件:至少覆盖 2、4、8、32 段 lookahead,连续/精确路径,短段和速度突变;逐 tick pose/velocity/acceleration/queue 一致。
|
||||
|
||||
### TP-PARITY-04:XYZAC rotary velocity semantics 和 RTCP
|
||||
|
||||
来源:
|
||||
|
||||
- `linuxcnc/src/emc/task/emccanon.cc`
|
||||
- `linuxcnc/src/emc/kinematics/{xyzac-trt-kins.c,trtfuncs.c,switchkins.c,kins_util.c}`
|
||||
- `linuxcnc/src/emc/motion/control.c`
|
||||
|
||||
工作:
|
||||
|
||||
- 统一线性轴 mm、旋转轴 deg 的速度单位和合成路径长度。
|
||||
- 验证 A/C 单独运动、XYZ+AC 同时运动、G93 inverse time、G94 units/min、RTCP pivot/tool offset。
|
||||
- native trace 同时导出 Cartesian actual/commanded pose 和五 joint actual/commanded pose。
|
||||
- 将 M428/M429/M430 的切换点纳入 planner queue 和 trace,而不是只比较最终姿态。
|
||||
|
||||
退出条件:同一切换点前后 pose 连续、joint 速度/路径长度/active motion id 与 native 一致。
|
||||
|
||||
### TP-PARITY-05:WASM Worker 真实 planner 接入
|
||||
|
||||
工作:
|
||||
|
||||
- `WasmProgramRuntime` 输出 LinuxCNC canonical motion/termination 条件。
|
||||
- Worker 只调用 WASM TP/kinematics/motion ABI;删除 selected-case 中的简化 `sim_motion_queue` 速度决策。
|
||||
- React 继续只消费 snapshot/delta,不参与 tick、前瞻、姿态补偿或路径重建。
|
||||
- Feed/rapid override、pause/resume、abort、restart、checkpoint 都必须落到同一 WASM planner state。
|
||||
|
||||
退出条件:Worker trace 与 WASM C trace byte-identical,且不会回退到 `AuthoritativeRuntime` 或 `ProgramRunner` 的 authority path。
|
||||
|
||||
### TP-PARITY-06:Native/WASM 逐 tick comparator
|
||||
|
||||
交付:
|
||||
|
||||
- `scripts/compare_xyzac_native_wasm_tick_trace.py`
|
||||
- `coverage/xyzac-native-wasm-tick-parity.report.json`
|
||||
|
||||
比较字段:
|
||||
|
||||
```text
|
||||
tick
|
||||
task_state/mode/interp_state/exec_state
|
||||
current_line/motion_line/read_line
|
||||
actual_pose/commanded_pose
|
||||
joint_actual/joint_commanded
|
||||
velocity/acceleration
|
||||
queue/active_queue/queue_full
|
||||
motion_id/active_segment
|
||||
feed_override/rapid_override
|
||||
kinematics_mode/RTCP offsets
|
||||
state_hash
|
||||
```
|
||||
|
||||
比较规则:
|
||||
|
||||
- 浮点使用固定绝对/相对容差,并记录原始值和差异值。
|
||||
- 列表长度、字段缺失、状态顺序不允许静默忽略。
|
||||
- 保存 `first_divergent_tick`、`difference_path`、native/WASM 两侧样本。
|
||||
- 最终坐标相同但中间 tick 不同必须判定为 `diverged`。
|
||||
|
||||
### TP-PARITY-07:预览和实际执行一致性
|
||||
|
||||
工作:
|
||||
|
||||
- 预览使用同一 canonical/TP 输入生成 planned path,不使用独立的几何近似。
|
||||
- 实时路径使用同一 planner 的 actual motion trace。
|
||||
- 对比 planned TC、executed TC、active segment 和完成后的 endpoint;明确 rapid、blend、exact-stop 的颜色/线型只属于显示层。
|
||||
|
||||
退出条件:预览不改变 planner state;执行 trace 的 segment/source line 可回指预览 TC;同一程序重复运行 preview hash 稳定。
|
||||
|
||||
### TP-PARITY-08:最终门禁
|
||||
|
||||
必须通过:
|
||||
|
||||
- native `run-5axis-rtcp.sh` trace
|
||||
- native CTest 和 WASM CTest
|
||||
- Worker trace
|
||||
- native/WASM 逐 tick comparator
|
||||
- XYZAC M428/M429/M430、G64/G61/G61.1、G93/G94、短段、拐角、queue full、pause/abort/restart/recovery
|
||||
- Chromium 浏览器只读显示验证
|
||||
|
||||
只有 `first_divergent_tick: null`、所有字段状态 hash 通过、所有输入和报告自哈希一致,才允许将状态写为 `PARITY_VERIFIED`。否则保持 `TP-PARITY-*` 对应任务的 `blocked` 或 `diverged`。
|
||||
|
||||
## 当前明确边界
|
||||
|
||||
- 当前 `coverage/xyzac-trt-trace-parity.report.json` 证明的是项目内 native C/WASM C/Worker 实现之间的 selected-case 一致,不是 native LinuxCNC TP oracle 对标。
|
||||
- 当前 `wasm-program-runtime.mjs` 的 preview 是 WASM interpreter canonical segments;完整 LinuxCNC TP 前瞻和 G64 corner blend 尚未接入。
|
||||
- 当前 `sim_motion_queue.c` 的 lookahead/blend 是简化实现,不能作为真实 LinuxCNC TP 数值等价证明。
|
||||
- `run-5axis-rtcp.sh` 当前只启动 native AXIS;TP-PARITY-00 必须补齐 `linuxcnc.stat()` trace 导出后,才有资格进行逐 tick comparator。
|
||||
- Safari、物理断电和真实硬件仍按项目范围排除,不影响本仿真任务的 native/WASM 数值闭环。
|
||||
269
scripts/run_xyzac_native_tick_trace.py
Normal file
269
scripts/run_xyzac_native_tick_trace.py
Normal file
@@ -0,0 +1,269 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run the repository's native XYZAC RTCP case and record LinuxCNC stat ticks.
|
||||
|
||||
This is an oracle collector, not a simulator. It intentionally starts the
|
||||
same run-5axis-rtcp.sh launcher used by the native case and reads values only
|
||||
from linuxcnc.stat(). Fields unavailable through the public stat API remain
|
||||
explicitly null.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import linuxcnc
|
||||
|
||||
from run_axis3_tutorial_native import cleanup_linuxcnc_services
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
CASE = "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini"
|
||||
PROGRAM = ROOT / "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins.ngc"
|
||||
SCHEMA = "xyzac-native-linuxcnc-tick-trace.v1"
|
||||
|
||||
|
||||
def canonical(value: Any) -> str:
|
||||
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
||||
|
||||
|
||||
def sha256_bytes(value: bytes) -> str:
|
||||
return hashlib.sha256(value).hexdigest()
|
||||
|
||||
|
||||
def finite_list(value: Any) -> list[float] | None:
|
||||
try:
|
||||
return [float(item) for item in value]
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def scalar(value: Any) -> float | int | bool | str | None:
|
||||
if value is None or isinstance(value, (bool, int, str)):
|
||||
return value
|
||||
try:
|
||||
number = float(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
return number if number == number and abs(number) != float("inf") else None
|
||||
|
||||
|
||||
def stat_row(stat: Any, sample_index: int, previous_taskbeat: int | None) -> dict[str, Any]:
|
||||
stat.poll()
|
||||
taskbeat = scalar(getattr(stat, "taskbeat", None))
|
||||
return {
|
||||
"sample_index": sample_index,
|
||||
"taskbeat": taskbeat,
|
||||
"taskbeat_changed": previous_taskbeat is None or taskbeat != previous_taskbeat,
|
||||
"task_state": scalar(getattr(stat, "task_state", None)),
|
||||
"task_mode": scalar(getattr(stat, "task_mode", None)),
|
||||
"interp_state": scalar(getattr(stat, "interp_state", None)),
|
||||
"exec_state": scalar(getattr(stat, "exec_state", None)),
|
||||
"current_line": scalar(getattr(stat, "current_line", None)),
|
||||
"motion_line": scalar(getattr(stat, "motion_line", None)),
|
||||
"read_line": scalar(getattr(stat, "read_line", None)),
|
||||
"actual_pose": finite_list(getattr(stat, "actual_position", None)),
|
||||
"commanded_pose": finite_list(getattr(stat, "command_position", None)),
|
||||
"joint_actual": finite_list(getattr(stat, "joint_actual_position", None)),
|
||||
"joint_commanded": finite_list(getattr(stat, "joint_position", None)),
|
||||
"velocity": scalar(getattr(stat, "current_vel", None)),
|
||||
"acceleration": scalar(getattr(stat, "acceleration", None)),
|
||||
"feed_override": scalar(getattr(stat, "feedrate", None)),
|
||||
"rapid_override": scalar(getattr(stat, "rapidrate", None)),
|
||||
"feed_override_enabled": scalar(getattr(stat, "feed_override_enabled", None)),
|
||||
"queue_depth": scalar(getattr(stat, "queue", None)),
|
||||
"active_queue": scalar(getattr(stat, "active_queue", None)),
|
||||
"queue_full": scalar(getattr(stat, "queue_full", None)),
|
||||
"motion_id": scalar(getattr(stat, "motion_id", None)),
|
||||
"active_segment_id": scalar(getattr(stat, "motion_id", None)),
|
||||
"active_segment_geometry": None,
|
||||
"active_segment_geometry_status": "unavailable_linuxcnc_stat_api",
|
||||
"distance_to_go": finite_list(getattr(stat, "distance_to_go", None)),
|
||||
"position": finite_list(getattr(stat, "position", None)),
|
||||
"homed": list(getattr(stat, "homed", [])) if getattr(stat, "homed", None) is not None else None,
|
||||
"limits": list(getattr(stat, "limit", [])) if getattr(stat, "limit", None) is not None else None,
|
||||
"kinematics_type": scalar(getattr(stat, "kinematics_type", None)),
|
||||
"motion_type": scalar(getattr(stat, "motion_type", None)),
|
||||
"feed_hold_enabled": scalar(getattr(stat, "feed_hold_enabled", None)),
|
||||
"paused": scalar(getattr(stat, "paused", None)),
|
||||
}
|
||||
|
||||
|
||||
def wait_stat(stat: Any, predicate: Any, timeout: float) -> None:
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
stat.poll()
|
||||
if predicate(stat):
|
||||
return
|
||||
time.sleep(0.02)
|
||||
raise RuntimeError("native_stat_transition_timeout")
|
||||
|
||||
|
||||
def terminate_process(process: subprocess.Popen[str]) -> None:
|
||||
if process.poll() is not None:
|
||||
return
|
||||
try:
|
||||
os.killpg(process.pid, signal.SIGTERM)
|
||||
process.wait(timeout=5)
|
||||
except (ProcessLookupError, subprocess.TimeoutExpired):
|
||||
try:
|
||||
os.killpg(process.pid, signal.SIGKILL)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
try:
|
||||
process.wait(timeout=2)
|
||||
except subprocess.TimeoutExpired:
|
||||
pass
|
||||
|
||||
|
||||
def native_axis_window(environment: dict[str, str]) -> dict[str, Any] | None:
|
||||
result = subprocess.run(["xwininfo", "-root", "-tree"], env=environment, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True, check=False)
|
||||
for line in result.stdout.splitlines():
|
||||
if '("axis" "Axis")' not in line or "xyzac-trt" not in line.lower():
|
||||
continue
|
||||
marker = line.strip().split('"', 2)
|
||||
if len(marker) < 2:
|
||||
continue
|
||||
size = re.search(r"\)\s+(\d+)x(\d+)\+", line)
|
||||
if not size or int(size.group(1)) < 600 or int(size.group(2)) < 400:
|
||||
continue
|
||||
return {"title": marker[1], "width": int(size.group(1)), "height": int(size.group(2)), "raw": line.strip()}
|
||||
return None
|
||||
|
||||
|
||||
def run(duration: float) -> dict[str, Any]:
|
||||
if not PROGRAM.is_file():
|
||||
return {"schema": SCHEMA, "status": "blocked", "code": "native_program_missing", "program": PROGRAM.as_posix()}
|
||||
cleanup = cleanup_linuxcnc_services()
|
||||
environment = os.environ.copy()
|
||||
environment["DISPLAY"] = environment.get("DISPLAY", ":1")
|
||||
command = ["./run-5axis-rtcp.sh", "xyzac-trt.ini"]
|
||||
process = subprocess.Popen(command, cwd=ROOT, env=environment, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, start_new_session=True)
|
||||
output_lines: list[str] = []
|
||||
rows: list[dict[str, Any]] = []
|
||||
errors: list[str] = []
|
||||
stat: Any = None
|
||||
control: Any = None
|
||||
connected = False
|
||||
window: dict[str, Any] | None = None
|
||||
try:
|
||||
deadline = time.monotonic() + 20
|
||||
while time.monotonic() < deadline and process.poll() is None:
|
||||
window = native_axis_window(environment)
|
||||
if not window:
|
||||
time.sleep(0.1)
|
||||
continue
|
||||
try:
|
||||
stat = linuxcnc.stat()
|
||||
control = linuxcnc.command()
|
||||
stat.poll()
|
||||
if getattr(stat, "task_state", None) in (linuxcnc.STATE_ESTOP, linuxcnc.STATE_ESTOP_RESET, linuxcnc.STATE_OFF, linuxcnc.STATE_ON):
|
||||
connected = True
|
||||
break
|
||||
except Exception:
|
||||
time.sleep(0.1)
|
||||
if stat is None or control is None or not connected:
|
||||
raise RuntimeError("native_linuxcnc_stat_unavailable")
|
||||
|
||||
control.state(linuxcnc.STATE_ESTOP_RESET)
|
||||
control.wait_complete(5)
|
||||
wait_stat(stat, lambda item: item.task_state != linuxcnc.STATE_ESTOP, 5)
|
||||
control.state(linuxcnc.STATE_ON)
|
||||
control.wait_complete(5)
|
||||
wait_stat(stat, lambda item: item.task_state == linuxcnc.STATE_ON, 5)
|
||||
control.mode(linuxcnc.MODE_MANUAL)
|
||||
control.wait_complete(5)
|
||||
for joint in range(5):
|
||||
control.home(joint)
|
||||
control.wait_complete(5)
|
||||
wait_stat(stat, lambda item: all(bool(value) for value in list(getattr(item, "homed", []))[:5]), 10)
|
||||
control.program_open(str(PROGRAM))
|
||||
control.wait_complete(5)
|
||||
control.mode(linuxcnc.MODE_AUTO)
|
||||
control.wait_complete(5)
|
||||
control.auto(linuxcnc.AUTO_RUN, 0)
|
||||
|
||||
start = time.monotonic()
|
||||
sample_index = 0
|
||||
previous_taskbeat: int | None = None
|
||||
last_change = start
|
||||
while time.monotonic() - start < duration:
|
||||
row = stat_row(stat, sample_index, previous_taskbeat)
|
||||
current_taskbeat = row["taskbeat"]
|
||||
if row["taskbeat_changed"] or not rows:
|
||||
row["native_elapsed_ms"] = round((time.monotonic() - start) * 1000.0, 3)
|
||||
rows.append(row)
|
||||
previous_taskbeat = current_taskbeat if isinstance(current_taskbeat, int) else previous_taskbeat
|
||||
last_change = time.monotonic()
|
||||
if row["task_state"] == linuxcnc.STATE_OFF or row["interp_state"] == linuxcnc.INTERP_IDLE and row["queue_depth"] == 0 and len(rows) > 4:
|
||||
break
|
||||
sample_index += 1
|
||||
time.sleep(0.005)
|
||||
final = stat_row(stat, sample_index + 1, previous_taskbeat)
|
||||
final["native_elapsed_ms"] = round((time.monotonic() - start) * 1000.0, 3)
|
||||
rows.append(final)
|
||||
except Exception as error:
|
||||
errors.append(f"{type(error).__name__}:{error}")
|
||||
finally:
|
||||
terminate_process(process)
|
||||
try:
|
||||
output, _ = process.communicate(timeout=2)
|
||||
output_lines.extend(output.splitlines())
|
||||
except (subprocess.TimeoutExpired, ValueError):
|
||||
pass
|
||||
cleanup_linuxcnc_services()
|
||||
|
||||
source_bytes = PROGRAM.read_bytes()
|
||||
trace_hash = sha256_bytes(canonical(rows).encode())
|
||||
status = "verified" if rows and not errors else "blocked"
|
||||
return {
|
||||
"schema": SCHEMA,
|
||||
"status": status,
|
||||
"case": {"path": CASE, "scope": "xyzac_only"},
|
||||
"launcher": command,
|
||||
"native_axis_window": window,
|
||||
"program": {"path": PROGRAM.relative_to(ROOT).as_posix(), "sha256": sha256_bytes(source_bytes)},
|
||||
"native_linuxcnc": {"python_module": getattr(linuxcnc, "__file__", None), "display": environment["DISPLAY"]},
|
||||
"tick_source": "linuxcnc.stat.taskbeat",
|
||||
"rows": rows,
|
||||
"row_count": len(rows),
|
||||
"trace_sha256": trace_hash,
|
||||
"errors": errors,
|
||||
"launcher_output_tail": "\n".join(output_lines[-80:]),
|
||||
"launcher_output_sha256": sha256_bytes("\n".join(output_lines).encode()),
|
||||
"cleanup": cleanup,
|
||||
"known_boundaries": [
|
||||
"native stat exposes motion_id but not full TP segment geometry; active_segment_geometry is explicit null",
|
||||
"taskbeat is the public native task-cycle counter; wall-clock is recorded only as diagnostic elapsed_ms",
|
||||
"this report is an oracle input and does not claim native/WASM parity by itself",
|
||||
],
|
||||
"release_status": "STOP_RELEASE",
|
||||
"report_sha256": None,
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--duration", type=float, default=8.0)
|
||||
parser.add_argument("--output", type=Path, default=Path("coverage/xyzac-native-tick-trace.report.json"))
|
||||
args = parser.parse_args()
|
||||
report = run(max(0.1, min(args.duration, 300.0)))
|
||||
unsigned = {key: value for key, value in report.items() if key != "report_sha256"}
|
||||
report["report_sha256"] = sha256_bytes(canonical(unsigned).encode())
|
||||
destination = args.output if args.output.is_absolute() else ROOT / args.output
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
destination.write_text(json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n", encoding="utf-8")
|
||||
print(json.dumps({"schema": SCHEMA, "status": report["status"], "row_count": report["row_count"], "trace_sha256": report["trace_sha256"], "errors": report["errors"], "report_sha256": report["report_sha256"]}, ensure_ascii=False))
|
||||
return 0 if report["status"] == "verified" else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user