Plan full LinuxCNC TP numerical parity

This commit is contained in:
2026-08-31 09:30:27 +08:00
parent 6033dcd47a
commit b564792452
5 changed files with 618 additions and 0 deletions

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

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

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

View 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 数值闭环。

View 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())