Run XYZAC through upstream LinuxCNC TP
This commit is contained in:
@@ -17,6 +17,7 @@ import re
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
@@ -62,16 +63,20 @@ def scalar(value: Any) -> float | int | bool | str | 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]:
|
||||
def stat_row(stat: Any, sample_index: int, previous_tick: int | None) -> dict[str, Any]:
|
||||
stat.poll()
|
||||
taskbeat = scalar(getattr(stat, "task_heartbeat", None))
|
||||
motionbeat = scalar(getattr(stat, "motion_heartbeat", None))
|
||||
tick = motionbeat if isinstance(motionbeat, int) else taskbeat
|
||||
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,
|
||||
"motion_heartbeat": motionbeat,
|
||||
"tick": tick,
|
||||
"taskbeat_changed": previous_tick is None or tick != previous_tick,
|
||||
"task_state": scalar(getattr(stat, "task_state", None)),
|
||||
"task_mode": scalar(getattr(stat, "task_mode", None)),
|
||||
"interp_state": scalar(getattr(stat, "interp_state", None)),
|
||||
@@ -79,6 +84,8 @@ def stat_row(stat: Any, sample_index: int, previous_taskbeat: int | None) -> dic
|
||||
"current_line": scalar(getattr(stat, "current_line", None)),
|
||||
"motion_line": scalar(getattr(stat, "motion_line", None)),
|
||||
"read_line": scalar(getattr(stat, "read_line", None)),
|
||||
"command": getattr(stat, "command", None),
|
||||
"file": getattr(stat, "file", None),
|
||||
"actual_pose": finite_list(getattr(stat, "actual_position", None)),
|
||||
"commanded_pose": commanded_pose,
|
||||
"joint_actual": finite_list(getattr(stat, "joint_actual_position", None)),
|
||||
@@ -154,6 +161,9 @@ def run(duration: float) -> dict[str, Any]:
|
||||
cleanup = cleanup_linuxcnc_services()
|
||||
environment = os.environ.copy()
|
||||
environment["DISPLAY"] = environment.get("DISPLAY", ":1")
|
||||
trace_directory = tempfile.TemporaryDirectory(prefix="linuxcnc-xyzac-servo-trace-")
|
||||
trace_path = Path(trace_directory.name) / "motion.jsonl"
|
||||
environment["LINUXCNC_XYZAC_TRACE_FILE"] = str(trace_path)
|
||||
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] = []
|
||||
@@ -164,6 +174,7 @@ def run(duration: float) -> dict[str, Any]:
|
||||
control: Any = None
|
||||
connected = False
|
||||
window: dict[str, Any] | None = None
|
||||
run_start_motion_heartbeat: int | None = None
|
||||
try:
|
||||
deadline = time.monotonic() + 20
|
||||
while time.monotonic() < deadline and process.poll() is None:
|
||||
@@ -222,6 +233,8 @@ def run(duration: float) -> dict[str, Any]:
|
||||
control.wait_complete(5)
|
||||
stage = "program_run"
|
||||
control.auto(linuxcnc.AUTO_RUN, 0)
|
||||
stat.poll()
|
||||
run_start_motion_heartbeat = int(getattr(stat, "motion_heartbeat", 0))
|
||||
|
||||
start = time.monotonic()
|
||||
sample_index = 0
|
||||
@@ -229,16 +242,16 @@ def run(duration: float) -> dict[str, Any]:
|
||||
last_change = start
|
||||
while time.monotonic() - start < duration:
|
||||
row = stat_row(stat, sample_index, previous_taskbeat)
|
||||
current_taskbeat = row["taskbeat"]
|
||||
current_tick = row["tick"]
|
||||
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
|
||||
previous_taskbeat = current_tick if isinstance(current_tick, 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)
|
||||
time.sleep(0.001)
|
||||
final = stat_row(stat, sample_index + 1, previous_taskbeat)
|
||||
if final["taskbeat_changed"]:
|
||||
final["native_elapsed_ms"] = round((time.monotonic() - start) * 1000.0, 3)
|
||||
@@ -254,10 +267,75 @@ def run(duration: float) -> dict[str, Any]:
|
||||
pass
|
||||
cleanup_linuxcnc_services()
|
||||
|
||||
servo_rows: list[dict[str, Any]] = []
|
||||
if trace_path.is_file():
|
||||
for raw_line in trace_path.read_text(encoding="utf-8", errors="replace").splitlines():
|
||||
try:
|
||||
value = json.loads(raw_line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if isinstance(value, dict) and isinstance(value.get("motion_heartbeat"), int):
|
||||
if run_start_motion_heartbeat is None or value["motion_heartbeat"] >= max(0, run_start_motion_heartbeat - 1):
|
||||
servo_rows.append(value)
|
||||
trace_directory.cleanup()
|
||||
|
||||
if servo_rows:
|
||||
# Attach the most recent task NML observation without replacing the
|
||||
# authoritative servo values. Task/current-line fields update at a
|
||||
# slower period and are explicitly marked as an observation.
|
||||
task_observations = sorted(rows, key=lambda item: int(item.get("motion_heartbeat") or -1))
|
||||
first_wall_ns = int(servo_rows[0].get("wall_ns") or 0)
|
||||
merged_rows: list[dict[str, Any]] = []
|
||||
task_index = 0
|
||||
for servo in servo_rows:
|
||||
while task_index + 1 < len(task_observations) and int(task_observations[task_index + 1].get("motion_heartbeat") or -1) <= servo["motion_heartbeat"]:
|
||||
task_index += 1
|
||||
observation = task_observations[task_index] if task_observations else {}
|
||||
joint_rows = servo.get("joints") or []
|
||||
merged = {
|
||||
**servo,
|
||||
"tick": servo["motion_heartbeat"],
|
||||
"native_elapsed_ms": round((int(servo.get("wall_ns") or first_wall_ns) - first_wall_ns) / 1000000.0, 3),
|
||||
"task_observation": {key: observation.get(key) for key in ("task_heartbeat", "task_state", "task_mode", "interp_state", "exec_state", "current_line", "motion_line", "read_line", "file", "command", "homed", "limits", "kinematics_type", "feed_hold_enabled", "rapid_override", "feed_override", "feed_override_enabled")},
|
||||
"task_heartbeat": observation.get("task_heartbeat"),
|
||||
"task_state": observation.get("task_state"),
|
||||
"task_mode": observation.get("task_mode"),
|
||||
"interp_state": observation.get("interp_state"),
|
||||
"exec_state": observation.get("exec_state"),
|
||||
"current_line": observation.get("current_line"),
|
||||
"motion_line": observation.get("motion_line"),
|
||||
"read_line": observation.get("read_line"),
|
||||
"homed": observation.get("homed"),
|
||||
"limits": observation.get("limits"),
|
||||
"kinematics_type": observation.get("kinematics_type"),
|
||||
"feed_override": observation.get("feed_override"),
|
||||
"rapid_override": observation.get("rapid_override"),
|
||||
"feed_override_enabled": observation.get("feed_override_enabled"),
|
||||
"file": observation.get("file"),
|
||||
"command": observation.get("command"),
|
||||
"actual_pose": servo.get("actual_position"),
|
||||
"commanded_pose": servo.get("position"),
|
||||
"joint_actual": [item.get("fb") for item in joint_rows],
|
||||
"joint_commanded": [item.get("cmd") for item in joint_rows],
|
||||
"acceleration": servo.get("acceleration"),
|
||||
"active_queue": servo.get("active_depth"),
|
||||
"queue_depth": servo.get("queue_depth"),
|
||||
"queue_full": servo.get("queue_full"),
|
||||
"motion_id": servo.get("motion_id"),
|
||||
"active_segment_id": servo.get("motion_id"),
|
||||
"motion_type": servo.get("motion_type"),
|
||||
"position": servo.get("position"),
|
||||
"paused": bool(servo.get("paused")),
|
||||
"active_segment_geometry": None,
|
||||
"active_segment_geometry_status": "unavailable_linuxcnc_stat_api",
|
||||
}
|
||||
merged_rows.append(merged)
|
||||
rows = merged_rows
|
||||
|
||||
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]
|
||||
heartbeat_values = [row.get("tick") 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())
|
||||
@@ -282,7 +360,8 @@ def run(duration: float) -> dict[str, Any]:
|
||||
"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_source": "linuxcnc.motion_controller_trace.motion_heartbeat" if servo_rows else "linuxcnc.stat.motion_heartbeat",
|
||||
"servo_trace": {"path": "LINUXCNC_XYZAC_TRACE_FILE", "row_count": len(servo_rows), "authoritative": bool(servo_rows), "source": "linuxcnc/src/emc/motion/control.c", "start_motion_heartbeat": run_start_motion_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),
|
||||
|
||||
Reference in New Issue
Block a user