Unblock native XYZAC TP oracle
This commit is contained in:
@@ -2,30 +2,34 @@
|
||||
"schema": "task-handoff.v1",
|
||||
"task_id": "TP-PARITY-00",
|
||||
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
|
||||
"status": "blocked",
|
||||
"status": "verified",
|
||||
"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", "sha256": "86d13e1a715c20b2a7bfc243301b46780a9f2942bd96d928c5ae24a53dbcca71"},
|
||||
"result": "run-5axis-rtcp.sh launches the source LinuxCNC XYZAC case and the source Python NML extension captures a strictly monotonic native task-heartbeat trace with pose, joints, queue, current-line and dynamics fields",
|
||||
"report": {"path": "coverage/xyzac-native-tick-trace.report.json", "status": "verified", "sha256": "881571d0c75017a9610023a2d022d26606e653a0c62575563c0da7ada600e9a2"},
|
||||
"parity_report": {"path": "coverage/xyzac-native-wasm-tick-parity.report.json", "status": "diverged", "sha256": "3ce76d65aee5405c01efca36b859b63da476f5f4d3bc4666be93691458fb86b3"},
|
||||
"wasm_report": {"path": "coverage/xyzac-wasm-tick-trace.report.json", "status": "verified", "sha256": "21cbdaa8eaa1eda94ca107095ba7931cb5bd151c80e75c9b7dd9ec6149a71c73"},
|
||||
"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"}
|
||||
{"path": "scripts/run_xyzac_native_tick_trace.py", "status": "verified"},
|
||||
{"path": "scripts/run_xyzac_wasm_tick_trace.mjs", "status": "verified"},
|
||||
{"path": "scripts/compare_xyzac_native_wasm_tick_trace.py", "status": "verified"},
|
||||
{"path": "doc-rtcp/xyzac-trt-full-tp-parity-plan.md", "status": "updated"},
|
||||
{"path": "coverage/task-packets/TP-PARITY-00.json", "status": "verified"}
|
||||
],
|
||||
"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",
|
||||
"python_module": "blocked: installed 2.10.0~pre1 extension differs from source 2.10.0~pre0 launcher",
|
||||
"native_trace": "blocked",
|
||||
"stat_attach": "verified: source extension attaches after AXIS/NML startup",
|
||||
"python_module": "verified: source extension matches run-in-place launcher",
|
||||
"native_trace": "verified: 201 native heartbeat rows",
|
||||
"wasm_trace": "verified: same XYZAC source/config loaded through linuxcnc-wasm worker",
|
||||
"native_wasm_compare": "diverged: first difference $.rows[1].exec_state; release remains STOP_RELEASE",
|
||||
"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"},
|
||||
{"code": "NATIVE_PYTHON_MODULE_MISMATCH", "detail": "collector imported /usr/lib/python3/dist-packages/linuxcnc.cpython-312-x86_64-linux-gnu.so while run-5axis-rtcp.sh uses the source 2.10.0~pre0 launcher"}
|
||||
{"code": "ACTIVE_SEGMENT_GEOMETRY_UNAVAILABLE", "detail": "public linuxcnc.stat exposes motion_id but not TP segment geometry; collector retains explicit null instead of fabricating geometry"}
|
||||
],
|
||||
"next_task": "TP-PARITY-01",
|
||||
"next_task_status": "blocked_by_TP-PARITY-00",
|
||||
"next_task_status": "in_progress",
|
||||
"release_status": "STOP_RELEASE"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"schema": "task-packet.v1",
|
||||
"task_id": "TP-PARITY-00",
|
||||
"status": "blocked",
|
||||
"status": "verified",
|
||||
"title": "Native LinuxCNC XYZAC per-tick oracle",
|
||||
"milestone": "M7-TP-NUMERICAL-PARITY",
|
||||
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
|
||||
@@ -22,14 +22,21 @@
|
||||
],
|
||||
"required_outputs": [
|
||||
"scripts/run_xyzac_native_tick_trace.py",
|
||||
"scripts/run_xyzac_wasm_tick_trace.mjs",
|
||||
"scripts/compare_xyzac_native_wasm_tick_trace.py",
|
||||
"coverage/xyzac-native-tick-trace.report.json",
|
||||
"coverage/xyzac-wasm-tick-trace.report.json",
|
||||
"coverage/xyzac-native-wasm-tick-parity.report.json",
|
||||
"coverage/task-packets/TP-PARITY-00.handoff.json"
|
||||
],
|
||||
"observed": {
|
||||
"report": "coverage/xyzac-native-tick-trace.report.json",
|
||||
"report_sha256": "86d13e1a715c20b2a7bfc243301b46780a9f2942bd96d928c5ae24a53dbcca71",
|
||||
"row_count": 0,
|
||||
"first_blocker": "NATIVE_STAT_BUFFER_INVALID"
|
||||
"report_sha256": "881571d0c75017a9610023a2d022d26606e653a0c62575563c0da7ada600e9a2",
|
||||
"row_count": 201,
|
||||
"first_blocker": null,
|
||||
"tick_source": "linuxcnc.stat.task_heartbeat",
|
||||
"tick_integrity": "strictly_monotonic",
|
||||
"wasm_comparator": {"status": "diverged", "first_divergent_tick": 1, "difference_path": "$.rows[1].exec_state", "report": "coverage/xyzac-native-wasm-tick-parity.report.json"}
|
||||
},
|
||||
"acceptance": [
|
||||
"run-5axis-rtcp.sh launches native LinuxCNC",
|
||||
@@ -53,7 +60,8 @@
|
||||
"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"
|
||||
"full LinuxCNC TP source integration may require ABI-compatible C port rather than simplified queue extension",
|
||||
"native/WASM comparator currently diverges at current-line scheduling before TP numeric parity can be promoted"
|
||||
],
|
||||
"release_status": "STOP_RELEASE"
|
||||
}
|
||||
|
||||
20
coverage/tp-parity-release-state.report.json
Normal file
20
coverage/tp-parity-release-state.report.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"schema": "tp-parity-release-state.v1",
|
||||
"scope": "xyzac_only",
|
||||
"tasks": [
|
||||
{"task_id": "TP-PARITY-00", "status": "verified", "evidence": "coverage/xyzac-native-tick-trace.report.json"},
|
||||
{"task_id": "TP-PARITY-01", "status": "in_progress", "evidence": "coverage/xyzac-native-wasm-tick-parity.report.json"},
|
||||
{"task_id": "TP-PARITY-02", "status": "in_progress", "evidence": "wasm-core/src/sim_program.c"},
|
||||
{"task_id": "TP-PARITY-03", "status": "blocked_by_TP-PARITY-01", "evidence": null},
|
||||
{"task_id": "TP-PARITY-04", "status": "blocked_by_TP-PARITY-01", "evidence": null},
|
||||
{"task_id": "TP-PARITY-05", "status": "blocked_by_TP-PARITY-01", "evidence": null},
|
||||
{"task_id": "TP-PARITY-06", "status": "diverged", "evidence": "coverage/xyzac-native-wasm-tick-parity.report.json"},
|
||||
{"task_id": "TP-PARITY-07", "status": "blocked_by_TP-PARITY-06", "evidence": null},
|
||||
{"task_id": "TP-PARITY-08", "status": "blocked_by_TP-PARITY-06", "evidence": null}
|
||||
],
|
||||
"first_blocker": null,
|
||||
"first_divergent_tick": 1,
|
||||
"difference_path": "$.rows[1].exec_state",
|
||||
"release_status": "STOP_RELEASE",
|
||||
"reason": "The native oracle is available, but the existing WASM planner diverges from source LinuxCNC before full TP field parity is established."
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
39
coverage/xyzac-native-wasm-tick-parity.report.json
Normal file
39
coverage/xyzac-native-wasm-tick-parity.report.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"candidate_eligible": false,
|
||||
"case": {
|
||||
"path": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",
|
||||
"scope": "xyzac_only"
|
||||
},
|
||||
"comparison": {
|
||||
"difference_path": "$.rows[1].exec_state",
|
||||
"first_divergent_tick": 1,
|
||||
"native": 3,
|
||||
"status": "diverged",
|
||||
"wasm": 2
|
||||
},
|
||||
"difference_path": "$.rows[1].exec_state",
|
||||
"first_divergent_tick": 1,
|
||||
"known_boundaries": [
|
||||
"native public stat does not expose TP segment geometry",
|
||||
"WASM public snapshot does not expose acceleration/active blend geometry",
|
||||
"verified oracle availability is not a full TP numerical parity claim"
|
||||
],
|
||||
"native": {
|
||||
"report": "coverage/xyzac-native-tick-trace.report.json",
|
||||
"report_sha256": "881571d0c75017a9610023a2d022d26606e653a0c62575563c0da7ada600e9a2",
|
||||
"row_count": 201,
|
||||
"trace_sha256": "173b5a5644254adef9ca5fd3b4fb2919889cce938d3aae7079c5004a4bfb364f"
|
||||
},
|
||||
"release_status": "STOP_RELEASE",
|
||||
"report_sha256": "3ce76d65aee5405c01efca36b859b63da476f5f4d3bc4666be93691458fb86b3",
|
||||
"schema": "xyzac-native-wasm-tick-parity.v1",
|
||||
"status": "diverged",
|
||||
"wasm": {
|
||||
"report": "coverage/xyzac-wasm-tick-trace.report.json",
|
||||
"report_sha256": "21cbdaa8eaa1eda94ca107095ba7931cb5bd151c80e75c9b7dd9ec6149a71c73",
|
||||
"row_count": 202,
|
||||
"runner_output_sha256": "5f493d9e726ec7bf656f738f92b61a17e0b6455562e68c85199c282d58817843",
|
||||
"runner_returncode": 0,
|
||||
"trace_sha256": "4202692d7f92ad9d4bfacb0ec1bb88eaad9e5b308e3a016c670f8fc4a73a5612"
|
||||
}
|
||||
}
|
||||
19424
coverage/xyzac-wasm-tick-trace.report.json
Normal file
19424
coverage/xyzac-wasm-tick-trace.report.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,13 @@
|
||||
# XYZAC TRT 完整 TP/前瞻数值对标任务轨道
|
||||
|
||||
状态:`TP-PARITY-00` 已建立,完整 LinuxCNC 数值对标尚未完成。
|
||||
状态:`TP-PARITY-00` 已 verified,完整 LinuxCNC 数值对标尚未完成。
|
||||
|
||||
当前 oracle 证据:`coverage/xyzac-native-tick-trace.report.json` 已由源
|
||||
`run-5axis-rtcp.sh` 产生连续 task-heartbeat tick;源 Python NML 扩展暴露
|
||||
`task_heartbeat`,并记录 XYZAC pose/joints、queue、current-line、速度和加速度。
|
||||
`scripts/compare_xyzac_native_wasm_tick_trace.py` 已接入同一 case 的 WASM trace,
|
||||
当前首个差异保留为 `$.rows[1].exec_state`,因此不得把 TP-PARITY-01..08 写成
|
||||
数值 parity verified。
|
||||
|
||||
目标不是证明页面能绘制一条曲线,而是对同一 `xyzac-trt.ini`、同一 G-code、同一初始状态,比较 native LinuxCNC 与 WASM Worker 的每个运动 tick:
|
||||
|
||||
@@ -44,6 +51,9 @@ Web 端只有在该链路和 native trace 逐字段一致后,才能声明 `PAR
|
||||
|
||||
### TP-PARITY-01:锁定 LinuxCNC TP 数据模型和 C ABI
|
||||
|
||||
状态:`in_progress`。native tick oracle 已可用;当前 comparator 显示现有
|
||||
WASM queue 与源 TP 的执行顺序/速度尚未一致。
|
||||
|
||||
来源:`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}`。
|
||||
|
||||
工作:
|
||||
@@ -56,6 +66,9 @@ Web 端只有在该链路和 native trace 逐字段一致后,才能声明 `PAR
|
||||
|
||||
### TP-PARITY-02:G64/G61/G61.1 完整路径控制
|
||||
|
||||
状态:`in_progress`,WASM interpreter 侧已闭合指令模式和容差状态,numeric
|
||||
blend 仍待 TP port 和 comparator 通过。
|
||||
|
||||
来源:
|
||||
|
||||
- `linuxcnc/src/emc/rs274ngc/interp_convert.cc` 的 G64/G61 处理
|
||||
@@ -68,6 +81,11 @@ Web 端只有在该链路和 native trace 逐字段一致后,才能声明 `PAR
|
||||
- 保存 modal 状态、每个 canonical motion 的 termination condition 和 tolerance。
|
||||
- 增加直线-直线、直线-圆弧、圆弧-直线、圆弧-圆弧、急转角、短段、零容差和非法 P/Q 样本。
|
||||
|
||||
当前 WASM C interpreter 已输出三种模式及 P/Q 容差到 `sim_program.v1`,Worker
|
||||
在入队前应用对应 queue profile;LinuxCNC exact-path 与 exact-stop 的细微
|
||||
差别、P/Q 驱动的 corner blend 仍由 TP-PARITY-03/06 的 native tick comparator
|
||||
验证,尚未标记完成。
|
||||
|
||||
退出条件:native/WASM 每条 TC 的 termination mode、tolerance、blend point、速度和完成 tick 一致。
|
||||
|
||||
### TP-PARITY-03:Lookahead、corner blending、acceleration/jerk
|
||||
@@ -144,6 +162,12 @@ state_hash
|
||||
- 浮点使用固定绝对/相对容差,并记录原始值和差异值。
|
||||
- 列表长度、字段缺失、状态顺序不允许静默忽略。
|
||||
- 保存 `first_divergent_tick`、`difference_path`、native/WASM 两侧样本。
|
||||
|
||||
已提供可重复入口:
|
||||
|
||||
- `scripts/run_xyzac_wasm_tick_trace.mjs`
|
||||
- `scripts/compare_xyzac_native_wasm_tick_trace.py`
|
||||
- `coverage/xyzac-native-wasm-tick-parity.report.json`
|
||||
- 最终坐标相同但中间 tick 不同必须判定为 `diverged`。
|
||||
|
||||
### TP-PARITY-07:预览和实际执行一致性
|
||||
|
||||
26
patches/linuxcnc/0001-export-native-task-heartbeat.patch
Normal file
26
patches/linuxcnc/0001-export-native-task-heartbeat.patch
Normal file
@@ -0,0 +1,26 @@
|
||||
diff --git a/src/emc/task/emctaskmain.cc b/src/emc/task/emctaskmain.cc
|
||||
--- a/src/emc/task/emctaskmain.cc
|
||||
+++ b/src/emc/task/emctaskmain.cc
|
||||
@@ -3490,4 +3490,9 @@ int main(int argc, char *argv[])
|
||||
// since emcStatus was passed to the WM init functions, it
|
||||
// will be updated in the _update() functions above. There's
|
||||
// no need to call the individual functions on all WM items.
|
||||
+ // EMC_TASK_STAT_MSG::heartbeat is the native task-cycle oracle used by
|
||||
+ // external clients. Keep it monotonic and publish it with this cycle's
|
||||
+ // status image so a reader can identify every task tick without relying on
|
||||
+ // wall-clock sampling.
|
||||
+ emcStatus->task.heartbeat++;
|
||||
emcStatusBuffer->write(emcStatus);
|
||||
|
||||
diff --git a/src/emc/usr_intf/axis/extensions/emcmodule.cc b/src/emc/usr_intf/axis/extensions/emcmodule.cc
|
||||
--- a/src/emc/usr_intf/axis/extensions/emcmodule.cc
|
||||
+++ b/src/emc/usr_intf/axis/extensions/emcmodule.cc
|
||||
@@ -409,4 +409,7 @@ static PyMemberDef Stat_members[] = {
|
||||
{(char*)"echo_serial_number", T_INT, O(echo_serial_number), READONLY},
|
||||
{(char*)"echo_serial_number", T_INT, O(echo_serial_number), READONLY},
|
||||
{(char*)"state", T_INT, O(status), READONLY},
|
||||
+ {(char*)"task_heartbeat", T_UINT, O(task.heartbeat), READONLY,
|
||||
+ "Native EMC task-loop heartbeat. This counter is incremented by the "
|
||||
+ "LinuxCNC task thread and is suitable for identifying native status ticks."},
|
||||
|
||||
// task
|
||||
95
scripts/compare_xyzac_native_wasm_tick_trace.py
Normal file
95
scripts/compare_xyzac_native_wasm_tick_trace.py
Normal file
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compare the source LinuxCNC XYZAC oracle with the WASM worker trace."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
NATIVE_REPORT = ROOT / "coverage/xyzac-native-tick-trace.report.json"
|
||||
WASM_REPORT = ROOT / "coverage/xyzac-wasm-tick-trace.report.json"
|
||||
REPORT = ROOT / "coverage/xyzac-native-wasm-tick-parity.report.json"
|
||||
|
||||
|
||||
def canonical(value: Any) -> str:
|
||||
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
||||
|
||||
|
||||
def digest(value: Any) -> str:
|
||||
return hashlib.sha256((value if isinstance(value, bytes) else canonical(value).encode())).hexdigest()
|
||||
|
||||
|
||||
def first_difference(left: Any, right: Any, path: str = "$") -> str | None:
|
||||
if type(left) is not type(right):
|
||||
return path
|
||||
if isinstance(left, dict):
|
||||
for key in sorted(set(left) | set(right)):
|
||||
if key not in left or key not in right:
|
||||
return f"{path}.{key}"
|
||||
difference = first_difference(left[key], right[key], f"{path}.{key}")
|
||||
if difference:
|
||||
return difference
|
||||
return None
|
||||
if isinstance(left, list):
|
||||
for index in range(max(len(left), len(right))):
|
||||
if index >= len(left) or index >= len(right):
|
||||
return f"{path}[{index}]"
|
||||
difference = first_difference(left[index], right[index], f"{path}[{index}]")
|
||||
if difference:
|
||||
return difference
|
||||
return None
|
||||
return None if left == right else path
|
||||
|
||||
|
||||
def canonical_row(row: dict[str, Any]) -> dict[str, Any]:
|
||||
result = dict(row)
|
||||
for key in ("joint_actual", "joint_commanded", "homed", "limits"):
|
||||
if isinstance(result.get(key), list):
|
||||
result[key] = result[key][:5]
|
||||
return result
|
||||
|
||||
|
||||
def compare_rows(native: list[dict[str, Any]], wasm: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
fields = ("task_state", "task_mode", "interp_state", "exec_state", "current_line", "motion_line", "actual_pose", "commanded_pose", "joint_actual", "joint_commanded", "velocity", "queue_depth", "motion_id", "active_segment_id", "position", "homed", "limits", "kinematics_type", "motion_type", "paused")
|
||||
for index in range(max(len(native), len(wasm))):
|
||||
if index >= len(native) or index >= len(wasm):
|
||||
return {"status": "diverged", "first_divergent_tick": index, "difference_path": f"$.rows[{index}]", "native": native[index] if index < len(native) else None, "wasm": wasm[index] if index < len(wasm) else None}
|
||||
native_row = canonical_row(native[index])
|
||||
wasm_row = canonical_row(wasm[index])
|
||||
for field in fields:
|
||||
if native_row.get(field) != wasm_row.get(field):
|
||||
return {"status": "diverged", "first_divergent_tick": index, "difference_path": f"$.rows[{index}].{field}", "native": native_row.get(field), "wasm": wasm_row.get(field)}
|
||||
return {"status": "verified", "first_divergent_tick": None, "difference_path": None}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
native = json.loads(NATIVE_REPORT.read_text(encoding="utf-8"))
|
||||
wasm_ticks = max(1, min(1000, int(native.get("row_count") or 1)))
|
||||
build = subprocess.run(["node", "scripts/run_xyzac_wasm_tick_trace.mjs", "--ticks", str(wasm_ticks), "--output", str(WASM_REPORT)], cwd=ROOT, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=False, timeout=120)
|
||||
wasm = json.loads(WASM_REPORT.read_text(encoding="utf-8")) if WASM_REPORT.is_file() else {"status": "blocked", "rows": []}
|
||||
comparison = compare_rows(native.get("rows", []), wasm.get("rows", [])) if native.get("status") == "verified" and wasm.get("status") == "verified" else {"status": "blocked", "first_divergent_tick": None, "difference_path": None}
|
||||
report = {
|
||||
"schema": "xyzac-native-wasm-tick-parity.v1",
|
||||
"status": comparison["status"],
|
||||
"candidate_eligible": comparison["status"] == "verified",
|
||||
"case": native.get("case"),
|
||||
"native": {"report": str(NATIVE_REPORT.relative_to(ROOT)), "report_sha256": native.get("report_sha256"), "trace_sha256": native.get("trace_sha256"), "row_count": native.get("row_count")},
|
||||
"wasm": {"report": str(WASM_REPORT.relative_to(ROOT)), "report_sha256": wasm.get("report_sha256"), "trace_sha256": wasm.get("trace_sha256"), "row_count": wasm.get("row_count"), "runner_returncode": build.returncode, "runner_output_sha256": digest(build.stdout)},
|
||||
"comparison": comparison,
|
||||
"first_divergent_tick": comparison.get("first_divergent_tick"),
|
||||
"difference_path": comparison.get("difference_path"),
|
||||
"known_boundaries": ["native public stat does not expose TP segment geometry", "WASM public snapshot does not expose acceleration/active blend geometry", "verified oracle availability is not a full TP numerical parity claim"],
|
||||
"release_status": "STOP_RELEASE",
|
||||
}
|
||||
report["report_sha256"] = digest({key: value for key, value in report.items() if key != "report_sha256"})
|
||||
REPORT.write_text(json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n", encoding="utf-8")
|
||||
print(json.dumps({"schema": report["schema"], "status": report["status"], "first_divergent_tick": report["first_divergent_tick"], "difference_path": report["difference_path"], "report_sha256": report["report_sha256"]}, ensure_ascii=False))
|
||||
return 0 if report["status"] == "verified" else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -100,10 +100,20 @@ def cleanup_linuxcnc_services() -> dict[str, object]:
|
||||
pass
|
||||
lock = Path("/tmp/linuxcnc.lock")
|
||||
lock_removed = False
|
||||
lock_backup = None
|
||||
if lock.exists() and not any(service_pattern.search(command) for _, command in process_rows()):
|
||||
lock.unlink()
|
||||
# Preserve stale lock evidence for post-mortem inspection. A lock is
|
||||
# runtime state, not a repository artifact, so moving it is sufficient
|
||||
# and recoverable while allowing the next source launcher to start.
|
||||
lock_backup_path = Path(f"/tmp/linuxcnc.lock.stale-{time.strftime('%Y%m%d-%H%M%S')}")
|
||||
suffix = 1
|
||||
while lock_backup_path.exists():
|
||||
lock_backup_path = Path(f"/tmp/linuxcnc.lock.stale-{time.strftime('%Y%m%d-%H%M%S')}-{suffix}")
|
||||
suffix += 1
|
||||
lock.rename(lock_backup_path)
|
||||
lock_removed = True
|
||||
return {"commands": commands, "killed_pids": killed, "lock_path": lock.as_posix(), "lock_removed": lock_removed, "remaining_services": [command for _, command in process_rows() if service_pattern.search(command)]}
|
||||
lock_backup = lock_backup_path.as_posix()
|
||||
return {"commands": commands, "killed_pids": killed, "lock_path": lock.as_posix(), "lock_removed": lock_removed, "lock_backup": lock_backup, "remaining_services": [command for _, command in process_rows() if service_pattern.search(command)]}
|
||||
|
||||
|
||||
def run(screenshot_path: Path | None = None) -> dict[str, object]:
|
||||
|
||||
@@ -16,16 +16,22 @@ import os
|
||||
import re
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
# The oracle must bind to the same source build that run-5axis-rtcp.sh starts.
|
||||
# In particular, importing the distro linuxcnc extension would silently bind to
|
||||
# a different NML ABI/version and produce plausible but unusable status values.
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SOURCE_PYTHON_DIR = ROOT / "linuxcnc/lib/python"
|
||||
sys.path.insert(0, str(SOURCE_PYTHON_DIR))
|
||||
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"
|
||||
@@ -58,10 +64,13 @@ def scalar(value: Any) -> float | int | bool | str | None:
|
||||
|
||||
def stat_row(stat: Any, sample_index: int, previous_taskbeat: int | None) -> dict[str, Any]:
|
||||
stat.poll()
|
||||
taskbeat = scalar(getattr(stat, "taskbeat", None))
|
||||
taskbeat = scalar(getattr(stat, "task_heartbeat", None))
|
||||
commanded_pose = finite_list(getattr(stat, "position", None))
|
||||
distance_to_go = finite_list(getattr(stat, "dtg", None))
|
||||
return {
|
||||
"sample_index": sample_index,
|
||||
"taskbeat": taskbeat,
|
||||
"task_heartbeat": 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)),
|
||||
@@ -71,7 +80,7 @@ def stat_row(stat: Any, sample_index: int, previous_taskbeat: int | None) -> dic
|
||||
"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)),
|
||||
"commanded_pose": commanded_pose,
|
||||
"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)),
|
||||
@@ -86,7 +95,7 @@ def stat_row(stat: Any, sample_index: int, previous_taskbeat: int | None) -> dic
|
||||
"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)),
|
||||
"distance_to_go": distance_to_go,
|
||||
"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,
|
||||
@@ -150,6 +159,7 @@ def run(duration: float) -> dict[str, Any]:
|
||||
output_lines: list[str] = []
|
||||
rows: list[dict[str, Any]] = []
|
||||
errors: list[str] = []
|
||||
stage = "launch"
|
||||
stat: Any = None
|
||||
control: Any = None
|
||||
connected = False
|
||||
@@ -161,34 +171,56 @@ def run(duration: float) -> dict[str, Any]:
|
||||
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):
|
||||
# The AXIS toplevel is created only after linuxcncsvr/milltask have
|
||||
# opened the NML buffers. Creating linuxcnc.stat before that point
|
||||
# permanently binds an invalid channel, so wait for the real
|
||||
# window first and create fresh channels afterwards.
|
||||
time.sleep(0.1)
|
||||
if stat is None or control is None or not connected:
|
||||
if not window:
|
||||
raise RuntimeError("native_linuxcnc_window_unavailable")
|
||||
stat = None
|
||||
control = None
|
||||
attach_deadline = time.monotonic() + 10
|
||||
while time.monotonic() < attach_deadline and process.poll() is None:
|
||||
try:
|
||||
candidate_stat = linuxcnc.stat()
|
||||
candidate_stat.poll()
|
||||
stat = candidate_stat
|
||||
control = linuxcnc.command()
|
||||
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")
|
||||
except BaseException:
|
||||
stat = None
|
||||
control = None
|
||||
time.sleep(0.1)
|
||||
if stat is None or control is None or not connected:
|
||||
raise RuntimeError("native_linuxcnc_stat_unavailable")
|
||||
|
||||
stage = "state_estop_reset"
|
||||
control.state(linuxcnc.STATE_ESTOP_RESET)
|
||||
control.wait_complete(5)
|
||||
wait_stat(stat, lambda item: item.task_state != linuxcnc.STATE_ESTOP, 5)
|
||||
stage = "state_on"
|
||||
control.state(linuxcnc.STATE_ON)
|
||||
control.wait_complete(5)
|
||||
wait_stat(stat, lambda item: item.task_state == linuxcnc.STATE_ON, 5)
|
||||
stage = "mode_manual"
|
||||
control.mode(linuxcnc.MODE_MANUAL)
|
||||
control.wait_complete(5)
|
||||
for joint in range(5):
|
||||
stage = f"home_joint_{joint}"
|
||||
control.home(joint)
|
||||
control.wait_complete(5)
|
||||
stage = "wait_home_all"
|
||||
wait_stat(stat, lambda item: all(bool(value) for value in list(getattr(item, "homed", []))[:5]), 10)
|
||||
stage = "program_open"
|
||||
control.program_open(str(PROGRAM))
|
||||
control.wait_complete(5)
|
||||
stage = "mode_auto"
|
||||
control.mode(linuxcnc.MODE_AUTO)
|
||||
control.wait_complete(5)
|
||||
stage = "program_run"
|
||||
control.auto(linuxcnc.AUTO_RUN, 0)
|
||||
|
||||
start = time.monotonic()
|
||||
@@ -208,10 +240,11 @@ def run(duration: float) -> dict[str, Any]:
|
||||
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)
|
||||
if final["taskbeat_changed"]:
|
||||
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}")
|
||||
errors.append(f"{stage}:{type(error).__name__}:{error}")
|
||||
finally:
|
||||
terminate_process(process)
|
||||
try:
|
||||
@@ -222,12 +255,19 @@ def run(duration: float) -> dict[str, Any]:
|
||||
cleanup_linuxcnc_services()
|
||||
|
||||
source_bytes = PROGRAM.read_bytes()
|
||||
ini_path = ROOT / CASE
|
||||
launcher_path = ROOT / "run-5axis-rtcp.sh"
|
||||
heartbeat_values = [row.get("task_heartbeat") for row in rows]
|
||||
valid_heartbeats = [value for value in heartbeat_values if isinstance(value, int)]
|
||||
heartbeat_monotonic = all(left < right for left, right in zip(valid_heartbeats, valid_heartbeats[1:]))
|
||||
trace_hash = sha256_bytes(canonical(rows).encode())
|
||||
python_module = getattr(linuxcnc, "__file__", None)
|
||||
source_python_module = str(ROOT / "linuxcnc/lib/python/linuxcnc.so")
|
||||
if python_module != source_python_module:
|
||||
errors.append("NATIVE_PYTHON_MODULE_MISMATCH")
|
||||
status = "verified" if rows and not errors else "blocked"
|
||||
status = "verified" if rows and not errors and heartbeat_monotonic and len(valid_heartbeats) == len(set(valid_heartbeats)) else "blocked"
|
||||
if rows and not heartbeat_monotonic:
|
||||
errors.append("NATIVE_TASK_HEARTBEAT_NON_MONOTONIC")
|
||||
return {
|
||||
"schema": SCHEMA,
|
||||
"status": status,
|
||||
@@ -235,8 +275,15 @@ def run(duration: float) -> dict[str, Any]:
|
||||
"launcher": command,
|
||||
"native_axis_window": window,
|
||||
"program": {"path": PROGRAM.relative_to(ROOT).as_posix(), "sha256": sha256_bytes(source_bytes)},
|
||||
"native_linuxcnc": {"python_module": python_module, "expected_source_python_module": source_python_module, "display": environment["DISPLAY"]},
|
||||
"tick_source": "linuxcnc.stat.taskbeat",
|
||||
"native_linuxcnc": {"python_module": python_module, "expected_source_python_module": source_python_module, "display": environment["DISPLAY"], "python_module_sha256": sha256_bytes(Path(python_module).read_bytes()) if python_module and Path(python_module).is_file() else None},
|
||||
"inputs": {
|
||||
"case_ini": {"path": CASE, "sha256": sha256_bytes(ini_path.read_bytes())},
|
||||
"launcher": {"path": "run-5axis-rtcp.sh", "sha256": sha256_bytes(launcher_path.read_bytes())},
|
||||
"program": {"path": PROGRAM.relative_to(ROOT).as_posix(), "sha256": sha256_bytes(source_bytes)},
|
||||
"source_python_module": {"path": source_python_module, "sha256": sha256_bytes(Path(source_python_module).read_bytes()) if Path(source_python_module).is_file() else None},
|
||||
},
|
||||
"tick_source": "linuxcnc.stat.task_heartbeat",
|
||||
"tick_integrity": {"first": valid_heartbeats[0] if valid_heartbeats else None, "last": valid_heartbeats[-1] if valid_heartbeats else None, "row_count": len(valid_heartbeats), "unique_count": len(set(valid_heartbeats)), "strictly_monotonic": heartbeat_monotonic},
|
||||
"rows": rows,
|
||||
"row_count": len(rows),
|
||||
"trace_sha256": trace_hash,
|
||||
|
||||
145
scripts/run_xyzac_wasm_tick_trace.mjs
Normal file
145
scripts/run_xyzac_wasm_tick_trace.mjs
Normal file
@@ -0,0 +1,145 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { createHash } from "node:crypto";
|
||||
import { join, resolve } from "node:path";
|
||||
import createModule from "../build-wasm/linuxcnc-sim-core.js";
|
||||
import { loadSimulationCase, toWorkerConfiguration } from "../web/workspace/sim-case-loader.mjs";
|
||||
import { XyzacSession, CONTEXT_ID } from "../web/worker/xyzac-session.mjs";
|
||||
|
||||
const ROOT = resolve(process.cwd());
|
||||
const CASE = "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini";
|
||||
const PROGRAM = "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins.ngc";
|
||||
const SCHEMA = "xyzac-wasm-linuxcnc-tick-trace.v1";
|
||||
const AXES = ["x", "y", "z", "a", "b", "c", "u", "v", "w"];
|
||||
|
||||
const sha256 = (value) => createHash("sha256").update(value).digest("hex");
|
||||
const canonical = (value) => JSON.stringify(value, Object.keys(value).sort());
|
||||
const poseArray = (pose) => AXES.map((axis) => Number(pose?.[axis] ?? 0));
|
||||
const modeCode = (mode) => ({ MANUAL: 1, AUTO: 2, MDI: 3 }[mode] ?? null);
|
||||
const taskStateCode = (state) => ({ ESTOP: 1, ESTOP_RESET: 2, OFF: 3, READY: 4, RUNNING: 4, PAUSED: 4, STOPPED: 4 }[state] ?? null);
|
||||
const interpCode = (status) => ({ idle: 1, loaded: 1, running: 2, paused: 3, completed: 1, aborted: 1, fault: 1 }[status] ?? null);
|
||||
const execCode = (status, queueDepth) => (Number(queueDepth) > 0 ? 3 : ({ running: 2, paused: 2, loaded: 2, completed: 2, aborted: 2, idle: 2, fault: 4 }[status] ?? null));
|
||||
|
||||
function rowFrom(state, task, program, tick) {
|
||||
const position = state?.position ?? {};
|
||||
const motion = task?.motion ?? {};
|
||||
const visual = task?.visual ?? {};
|
||||
const limits = motion.limits ?? {};
|
||||
const joints = Array.isArray(visual.joints) ? visual.joints : Array.isArray(motion.joints) ? motion.joints : [];
|
||||
const homedAxes = Array.isArray(motion.homed_axes) ? motion.homed_axes : [];
|
||||
return {
|
||||
sample_index: tick,
|
||||
taskbeat: tick,
|
||||
task_heartbeat: tick,
|
||||
taskbeat_changed: true,
|
||||
task_state: taskStateCode(task?.state),
|
||||
task_mode: modeCode(task?.mode),
|
||||
interp_state: interpCode(program?.program_status),
|
||||
exec_state: execCode(program?.program_status, state?.queue_depth),
|
||||
current_line: Number(program?.current_line ?? 0),
|
||||
motion_line: Number(program?.current_line ?? 0),
|
||||
read_line: Number(program?.current_line ?? 0),
|
||||
actual_pose: poseArray(position),
|
||||
commanded_pose: poseArray(position),
|
||||
joint_actual: joints,
|
||||
joint_commanded: joints,
|
||||
velocity: Number(state?.velocity ?? 0),
|
||||
acceleration: null,
|
||||
feed_override: Number(motion.feed_override ?? 1),
|
||||
rapid_override: 1,
|
||||
feed_override_enabled: true,
|
||||
queue_depth: Number(state?.queue_depth ?? 0),
|
||||
active_queue: null,
|
||||
queue_full: null,
|
||||
motion_id: Number(state?.active_id ?? 0),
|
||||
active_segment_id: Number(state?.active_id ?? 0),
|
||||
active_segment_geometry: null,
|
||||
active_segment_geometry_status: "unavailable_wasm_public_snapshot",
|
||||
distance_to_go: null,
|
||||
position: poseArray(position),
|
||||
homed: homedAxes.map((value) => (value ? 1 : 0)),
|
||||
limits: ["x", "y", "z", "a", "b", "c", "u", "v", "w"].map((axis) => limits[axis] ? 1 : 0),
|
||||
kinematics_type: 4,
|
||||
motion_type: Number(state?.motion_type ?? 0),
|
||||
feed_hold_enabled: null,
|
||||
paused: program?.program_status === "paused",
|
||||
wasm_tick: tick,
|
||||
};
|
||||
}
|
||||
|
||||
const fetchImpl = async (url) => {
|
||||
try {
|
||||
const body = await readFile(join(ROOT, String(url).replace(/^\//, "")));
|
||||
return { ok: true, text: async () => body.toString("utf8") };
|
||||
} catch {
|
||||
return { ok: false, text: async () => "" };
|
||||
}
|
||||
};
|
||||
|
||||
async function run(maxTicks) {
|
||||
const bundle = await loadSimulationCase({ path: CASE, fetchImpl });
|
||||
if (bundle.status !== "ready") throw new Error(`case_load_${bundle.code ?? bundle.status}`);
|
||||
const source = bundle.files?.[PROGRAM];
|
||||
if (typeof source !== "string") throw new Error("program_source_missing");
|
||||
const wasmBinary = await readFile(join(ROOT, "build-wasm/linuxcnc-sim-core.wasm"));
|
||||
const core = await createModule({ wasmBinary });
|
||||
const session = new XyzacSession(core, { sessionId: "xyzac-native-wasm-oracle" });
|
||||
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: `oracle-${seq}`, payload });
|
||||
send("HELLO");
|
||||
send("SESSION_ATTACH");
|
||||
const loadedWorkspace = send("LOAD_WORKSPACE", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) });
|
||||
if (loadedWorkspace.payload?.status !== "verified") throw new Error("workspace_load_failed");
|
||||
for (const [kind, payload] of [["POWER", { active: true }], ["HOME", { all: true }], ["PROGRAM_LOAD", { source, path: PROGRAM }], ["PROGRAM_RUN"]]) {
|
||||
const result = send(kind, payload);
|
||||
if (result.payload?.status !== "verified") throw new Error(`${kind.toLowerCase()}_${result.payload?.code ?? "failed"}`);
|
||||
}
|
||||
const rows = [];
|
||||
let state = session.wasmMotion.read().state;
|
||||
let task = { ...session.task.export(), motion: session.motionProjection(), visual: { joints: state.joints } };
|
||||
let program = session.xyzacProgram.snapshot();
|
||||
rows.push(rowFrom(state, task, program, 0));
|
||||
for (let tick = 1; tick <= maxTicks; tick += 1) {
|
||||
const response = send("SIM_STEP", { cycles: 1 });
|
||||
if (response.payload?.status !== "verified") throw new Error(`sim_step_${response.payload?.code ?? "failed"}`);
|
||||
state = response.payload.wasm_motion?.state ?? session.wasmMotion.read().state;
|
||||
task = { ...response.payload.task, motion: response.payload.motion, visual: response.payload.panel_snapshot?.visual };
|
||||
program = response.payload.program ?? session.xyzacProgram.snapshot();
|
||||
rows.push(rowFrom(state, task, program, tick));
|
||||
if (program.program_status === "completed" && Number(state.queue_depth) === 0) break;
|
||||
}
|
||||
const traceHash = sha256(JSON.stringify(rows));
|
||||
const sourceHash = sha256(source);
|
||||
return {
|
||||
schema: SCHEMA,
|
||||
status: "verified",
|
||||
case: { path: CASE, scope: "xyzac_only" },
|
||||
native_equivalent_inputs: { case_ini: sha256(await readFile(join(ROOT, CASE))), program: sourceHash },
|
||||
wasm: { module: "build-wasm/linuxcnc-sim-core.wasm", module_sha256: sha256(wasmBinary), authority: "linuxcnc-wasm-worker" },
|
||||
tick_source: "wasm_simulation_tick",
|
||||
tick_integrity: { first: rows[0]?.wasm_tick ?? null, last: rows.at(-1)?.wasm_tick ?? null, row_count: rows.length, unique_count: new Set(rows.map((row) => row.wasm_tick)).size, strictly_monotonic: rows.every((row, index) => index === 0 || row.wasm_tick > rows[index - 1].wasm_tick) },
|
||||
rows,
|
||||
row_count: rows.length,
|
||||
trace_sha256: traceHash,
|
||||
errors: [],
|
||||
release_status: "STOP_RELEASE",
|
||||
};
|
||||
}
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const ticksIndex = args.indexOf("--ticks");
|
||||
const outputIndex = args.indexOf("--output");
|
||||
const maxTicks = Math.max(1, Math.min(100000, Number(ticksIndex >= 0 ? args[ticksIndex + 1] : 3000)));
|
||||
const output = outputIndex >= 0 ? resolve(args[outputIndex + 1]) : join(ROOT, "coverage/xyzac-wasm-tick-trace.report.json");
|
||||
try {
|
||||
const report = await run(maxTicks);
|
||||
const unsigned = { ...report, report_sha256: undefined };
|
||||
delete unsigned.report_sha256;
|
||||
report.report_sha256 = sha256(JSON.stringify(unsigned));
|
||||
await writeFile(output, `${JSON.stringify(report, null, 2)}\n`);
|
||||
console.log(JSON.stringify({ schema: report.schema, status: report.status, row_count: report.row_count, trace_sha256: report.trace_sha256, report_sha256: report.report_sha256 }));
|
||||
} catch (error) {
|
||||
console.error(JSON.stringify({ schema: SCHEMA, status: "blocked", error: String(error?.message ?? error) }));
|
||||
process.exitCode = 1;
|
||||
}
|
||||
@@ -37,6 +37,12 @@ enum sim_program_status {
|
||||
SIM_PROGRAM_ERROR = 6,
|
||||
};
|
||||
|
||||
enum sim_program_motion_control {
|
||||
SIM_PROGRAM_MOTION_CONTINUOUS = 0,
|
||||
SIM_PROGRAM_MOTION_EXACT_PATH = 1,
|
||||
SIM_PROGRAM_MOTION_EXACT_STOP = 2,
|
||||
};
|
||||
|
||||
typedef struct sim_program_subroutine {
|
||||
char name[SIM_PROGRAM_SUBROUTINE_NAME_CAPACITY];
|
||||
uint32_t body_line;
|
||||
@@ -70,6 +76,9 @@ typedef struct sim_program {
|
||||
uint32_t has_control;
|
||||
int32_t control_code;
|
||||
double rapid_velocity;
|
||||
int motion_control_mode;
|
||||
double path_tolerance;
|
||||
double naive_cam_tolerance;
|
||||
} sim_program;
|
||||
|
||||
uint32_t sim_program_size(void);
|
||||
|
||||
@@ -85,6 +85,10 @@ static int code_is(double value, int integer_code) {
|
||||
return isfinite(value) && fabs(value - (double)integer_code) <= 1e-12;
|
||||
}
|
||||
|
||||
static int code_is_value(double value, double expected) {
|
||||
return isfinite(value) && fabs(value - expected) <= 1e-12;
|
||||
}
|
||||
|
||||
static int axis_index(char letter) {
|
||||
switch (letter) {
|
||||
case 'X': return 0; case 'Y': return 1; case 'Z': return 2;
|
||||
@@ -346,7 +350,7 @@ static int apply_g(sim_interpreter_state *state, interpreter_meta *meta, double
|
||||
if (code_is(code, 40) || code_is(code, 41) || code_is(code, 42)) { if (++groups[4] > 1) return SIM_INTERP_ERR_STATE; if (meta) meta->cutter_comp = (int)code; return SIM_INTERP_OK; }
|
||||
if (code_is(code, 43) || code_is(code, 49)) { if (++groups[5] > 1) return SIM_INTERP_ERR_STATE; if (meta && code_is(code, 49)) meta->tool_length = 0.0; return SIM_INTERP_OK; }
|
||||
if (code_is(code, 80) || (code >= 81.0 && code <= 89.0 && floor(code) == code)) { if (meta) meta->cycle_mode = (int)code; return SIM_INTERP_OK; }
|
||||
if (code_is(code, 64)) return SIM_INTERP_OK;
|
||||
if (code_is(code, 61) || code_is_value(code, 61.1) || code_is(code, 64)) { if (++groups[7] > 1) return SIM_INTERP_ERR_STATE; return SIM_INTERP_OK; }
|
||||
if (code_is(code, 54) || code_is(code, 55) || code_is(code, 56) || code_is(code, 57) || code_is(code, 58) || code_is(code, 59)) { if (meta) meta->coordinate_system = (int)code - 53; return SIM_INTERP_OK; }
|
||||
if (code_is(code, 53)) return SIM_INTERP_OK;
|
||||
if (code_is(code, 10)) return SIM_INTERP_OK;
|
||||
@@ -444,7 +448,7 @@ int sim_interpreter_init(sim_interpreter *interpreter) { return sim_interpreter_
|
||||
|
||||
int sim_interpreter_execute(sim_interpreter *interpreter, const char *block, uint32_t length) {
|
||||
sim_interpreter_state previous; sim_interpreter_state next; interpreter_meta *meta; expression_parser parser; const char *end;
|
||||
int groups[7] = {0}, seen_axis[9] = {0}, seen_feed = 0, seen_speed = 0, seen_line = 0, seen_p = 0, seen_r = 0, seen_l = 0, seen_g10 = 0, seen_g53 = 0, result = SIM_INTERP_OK;
|
||||
int groups[8] = {0}, seen_axis[9] = {0}, seen_feed = 0, seen_speed = 0, seen_line = 0, seen_p = 0, seen_r = 0, seen_l = 0, seen_g10 = 0, seen_g53 = 0, result = SIM_INTERP_OK;
|
||||
int seen_center[3] = {0};
|
||||
double axis_values[9] = {0}, center_values[3] = {0}, feed_value = 0.0, speed_value = 0.0, p_value = 0.0, r_value = 0.0, l_value = 0.0;
|
||||
if (interpreter == NULL || block == NULL || length == 0) return SIM_INTERP_ERR_ARGUMENT;
|
||||
|
||||
@@ -253,6 +253,45 @@ static int switch_mcode(const char *block, uint32_t length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_motion_control(sim_program *program, const char *block, uint32_t length) {
|
||||
const char *cursor = block;
|
||||
const char *end = block + length;
|
||||
int active = 0;
|
||||
double path = 0.0;
|
||||
double naive = 0.0;
|
||||
while (cursor < end) {
|
||||
char letter;
|
||||
char *number_end;
|
||||
double value;
|
||||
while (cursor < end && isspace((unsigned char)*cursor)) ++cursor;
|
||||
if (cursor >= end || *cursor == ';') break;
|
||||
if (*cursor == '(') { while (cursor < end && *cursor++ != ')') {} continue; }
|
||||
if (!isalpha((unsigned char)*cursor)) { ++cursor; continue; }
|
||||
letter = (char)toupper((unsigned char)*cursor++);
|
||||
value = strtod(cursor, &number_end);
|
||||
if (number_end == cursor) continue;
|
||||
cursor = number_end;
|
||||
if (letter == 'G') {
|
||||
if (fabs(value - 61.0) <= 1e-12) active = SIM_PROGRAM_MOTION_EXACT_PATH;
|
||||
else if (fabs(value - 61.1) <= 1e-12) active = SIM_PROGRAM_MOTION_EXACT_STOP;
|
||||
else if (fabs(value - 64.0) <= 1e-12) active = SIM_PROGRAM_MOTION_CONTINUOUS;
|
||||
else active = 0;
|
||||
if (active != 0 || fabs(value - 64.0) <= 1e-12) {
|
||||
program->motion_control_mode = active;
|
||||
path = 0.0;
|
||||
naive = 0.0;
|
||||
}
|
||||
} else if (active == SIM_PROGRAM_MOTION_CONTINUOUS && (letter == 'P' || letter == 'Q')) {
|
||||
if (!isfinite(value) || value < 0.0) return SIM_PROGRAM_ERR_RANGE;
|
||||
if (letter == 'P') path = value;
|
||||
else naive = value;
|
||||
program->path_tolerance = path;
|
||||
program->naive_cam_tolerance = naive;
|
||||
}
|
||||
}
|
||||
return SIM_PROGRAM_OK;
|
||||
}
|
||||
|
||||
static void reset_runtime(sim_program *program) {
|
||||
sim_interpreter_reset(&program->interpreter);
|
||||
program->source_length = 0;
|
||||
@@ -268,6 +307,9 @@ static void reset_runtime(sim_program *program) {
|
||||
program->has_control = 0;
|
||||
program->control_code = 0;
|
||||
program->rapid_velocity = 35.0;
|
||||
program->motion_control_mode = SIM_PROGRAM_MOTION_CONTINUOUS;
|
||||
program->path_tolerance = 0.0;
|
||||
program->naive_cam_tolerance = 0.0;
|
||||
memset(&program->motion, 0, sizeof(program->motion));
|
||||
memset(program->subroutines, 0, sizeof(program->subroutines));
|
||||
memset(program->call_stack, 0, sizeof(program->call_stack));
|
||||
@@ -356,6 +398,8 @@ int32_t sim_program_execute_block(sim_program *program, const char *block, uint3
|
||||
int result;
|
||||
if (program == NULL || block == NULL || length == 0) return SIM_PROGRAM_ERR_ARGUMENT;
|
||||
if (control_line(block)) return SIM_PROGRAM_ERR_CONTROL;
|
||||
result = parse_motion_control(program, block, length);
|
||||
if (result != SIM_PROGRAM_OK) return result;
|
||||
program->has_control = 0;
|
||||
program->control_code = switch_mcode(block, length);
|
||||
if (program->control_code != 0) program->has_control = 1;
|
||||
@@ -449,8 +493,8 @@ uint32_t sim_program_canonical(const sim_program *program, char *output, uint32_
|
||||
int written;
|
||||
if (program == NULL) return 0;
|
||||
written = snprintf(destination, destination_capacity,
|
||||
"{\"call_depth\":%u,\"control_code\":%d,\"current_line\":%u,\"error_line\":%u,\"feed\":%.17g,\"has_control\":%u,\"has_motion\":%u,\"last_error\":%d,\"line_count\":%u,\"main_line_count\":%u,\"motion_mode\":%d,\"plane\":%d,\"rapid_velocity\":%.17g,\"schema\":\"sim_program.v1\",\"speed\":%.17g,\"spindle_mode\":%d,\"status\":%u,\"subroutine_count\":%u,\"source_length\":%u,\"units\":%d,\"distance_mode\":%d,\"interpreter_state_hash\":\"%llu\"}",
|
||||
program->call_depth, program->control_code, program->current_line, program->error_line, program->interpreter.state.feed, program->has_control, program->has_motion, program->last_error, program->line_count, program->main_line_count, program->interpreter.state.motion_mode, program->interpreter.state.plane, program->rapid_velocity, program->interpreter.state.speed, program->interpreter.state.spindle_mode, program->status, program->subroutine_count, program->source_length, program->interpreter.state.units, program->interpreter.state.distance_mode,
|
||||
"{\"call_depth\":%u,\"control_code\":%d,\"current_line\":%u,\"error_line\":%u,\"feed\":%.17g,\"has_control\":%u,\"has_motion\":%u,\"last_error\":%d,\"line_count\":%u,\"main_line_count\":%u,\"motion_control_mode\":%d,\"path_tolerance\":%.17g,\"naive_cam_tolerance\":%.17g,\"motion_mode\":%d,\"plane\":%d,\"rapid_velocity\":%.17g,\"schema\":\"sim_program.v1\",\"speed\":%.17g,\"spindle_mode\":%d,\"status\":%u,\"subroutine_count\":%u,\"source_length\":%u,\"units\":%d,\"distance_mode\":%d,\"interpreter_state_hash\":\"%llu\"}",
|
||||
program->call_depth, program->control_code, program->current_line, program->error_line, program->interpreter.state.feed, program->has_control, program->has_motion, program->last_error, program->line_count, program->main_line_count, program->motion_control_mode, program->path_tolerance, program->naive_cam_tolerance, program->interpreter.state.motion_mode, program->interpreter.state.plane, program->rapid_velocity, program->interpreter.state.speed, program->interpreter.state.spindle_mode, program->status, program->subroutine_count, program->source_length, program->interpreter.state.units, program->interpreter.state.distance_mode,
|
||||
(unsigned long long)sim_interpreter_state_hash(&program->interpreter));
|
||||
return written < 0 ? 0 : (uint32_t)written + 1u;
|
||||
}
|
||||
|
||||
@@ -1436,6 +1436,13 @@ export class SimSession {
|
||||
}
|
||||
const step = this.xyzacProgram.step();
|
||||
if (step.status !== "verified") throw new Error(step.code);
|
||||
// Path control is produced by the WASM interpreter and applied
|
||||
// to the WASM trajectory queue before the block's motion is
|
||||
// enqueued. G61/G61.1 conservatively use the queue's exact
|
||||
// profile; G64 restores the continuous lookahead profile.
|
||||
const motionControlMode = Number(step.program?.motion_control_mode ?? 0);
|
||||
const queueProfile = motionControlMode === 0 ? 0 : 1;
|
||||
if (this.wasmMotion.controls.profile !== queueProfile) this.wasmMotion.setProfile(queueProfile);
|
||||
if (step.has_control && [428, 429, 430].includes(step.control_code)) {
|
||||
this.pendingProgramControl = step.control_code;
|
||||
if (this.wasmMotion.read().state.queue_depth === 0) {
|
||||
|
||||
@@ -266,8 +266,9 @@ export class WasmProgramRuntime {
|
||||
const state = JSON.parse(readString(this.core, buffer, required));
|
||||
const hash = call(this.core, "sim_program_state_hash", "number", ["number"], [this.pointer]);
|
||||
const programStatus = { 0: "idle", 1: "loaded", 2: "running", 3: "paused", 4: "completed", 5: "aborted", 6: "fault" }[state.status] ?? "idle";
|
||||
const motionControl = { 0: "continuous", 1: "exact_path", 2: "exact_stop" }[state.motion_control_mode] ?? "continuous";
|
||||
const executionLineText = state.current_line > 0 ? (this.executionLines[state.current_line - 1] ?? "").trim() : "";
|
||||
return { schema: SCHEMA, source: "linuxcnc-wasm-sim_program", ...state, active_gcodes: activeGcodes(state), loaded: state.status !== 0, line: state.current_line, currentLine: state.current_line, execution_line: state.current_line, execution_line_text: executionLineText, program_status: programStatus, path: this.path, subroutines: this.subroutines.map((subroutine) => subroutine.name), state_hash: `fnv1a64:${BigInt.asUintN(64, BigInt(hash >>> 0)).toString(16).padStart(16, "0")}`, source_hash: this.source ? fnv1a64(this.source) : null };
|
||||
return { schema: SCHEMA, source: "linuxcnc-wasm-sim_program", ...state, motion_control: { mode: motionControl, path_tolerance: Number(state.path_tolerance ?? 0), naive_cam_tolerance: Number(state.naive_cam_tolerance ?? 0) }, active_gcodes: activeGcodes(state), loaded: state.status !== 0, line: state.current_line, currentLine: state.current_line, execution_line: state.current_line, execution_line_text: executionLineText, program_status: programStatus, path: this.path, subroutines: this.subroutines.map((subroutine) => subroutine.name), state_hash: `fnv1a64:${BigInt.asUintN(64, BigInt(hash >>> 0)).toString(16).padStart(16, "0")}`, source_hash: this.source ? fnv1a64(this.source) : null };
|
||||
} finally { this.core._free(buffer); }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user