Run XYZAC through upstream LinuxCNC TP
This commit is contained in:
@@ -5,9 +5,13 @@ ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
SOURCE_ROOT="${ROOT}/linuxcnc"
|
||||
PATCH_FILE="${ROOT}/patches/linuxcnc/0001-export-native-task-heartbeat.patch"
|
||||
|
||||
if ! rg -q 'task_heartbeat' "${SOURCE_ROOT}/src/emc/usr_intf/axis/extensions/emcmodule.cc"; then
|
||||
if ! rg -q 'task_heartbeat' "${SOURCE_ROOT}/src/emc/usr_intf/axis/extensions/emcmodule.cc" || ! rg -q 'motion_heartbeat' "${SOURCE_ROOT}/src/emc/usr_intf/axis/extensions/emcmodule.cc"; then
|
||||
patch --forward --batch -p1 -d "${SOURCE_ROOT}" < "${PATCH_FILE}"
|
||||
fi
|
||||
TRACE_PATCH_FILE="${ROOT}/patches/linuxcnc/0002-native-servo-trace.patch"
|
||||
if ! rg -q 'xyzac_trace_tick' "${SOURCE_ROOT}/src/emc/motion/control.c"; then
|
||||
patch --forward --batch -p1 -d "${SOURCE_ROOT}" < "${TRACE_PATCH_FILE}"
|
||||
fi
|
||||
|
||||
make -C "${SOURCE_ROOT}/src" ../lib/python/linuxcnc.so ../bin/milltask
|
||||
printf '%s\n' 'LinuxCNC native oracle build ready: source linuxcnc.so + milltask'
|
||||
make -C "${SOURCE_ROOT}/src" ../lib/python/linuxcnc.so ../bin/milltask ../rtlib/motmod.so
|
||||
printf '%s\n' 'LinuxCNC native oracle build ready: source linuxcnc.so + milltask + motmod.so'
|
||||
|
||||
@@ -6,6 +6,7 @@ from __future__ import annotations
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
import math
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -53,25 +54,70 @@ def canonical_row(row: dict[str, Any]) -> dict[str, Any]:
|
||||
return result
|
||||
|
||||
|
||||
def values_equal(left: Any, right: Any, absolute: float = 1e-9, relative: float = 1e-9) -> bool:
|
||||
if isinstance(left, (int, float)) and isinstance(right, (int, float)) and not isinstance(left, bool) and not isinstance(right, bool):
|
||||
scale = max(abs(float(left)), abs(float(right)), 1.0)
|
||||
return abs(float(left) - float(right)) <= max(absolute, relative * scale)
|
||||
if isinstance(left, list) and isinstance(right, list):
|
||||
return len(left) == len(right) and all(values_equal(a, b, absolute, relative) for a, b in zip(left, right))
|
||||
return left == right
|
||||
|
||||
|
||||
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}
|
||||
authoritative_fields = ("task_state", "task_mode", "interp_state", "exec_state", "current_line", "motion_line", "commanded_pose", "joint_commanded", "velocity", "acceleration", "queue_depth", "active_queue", "queue_full", "motion_id", "active_segment_id", "position", "homed", "limits", "kinematics_type", "motion_type", "paused")
|
||||
feedback_fields = ("actual_pose", "joint_actual")
|
||||
def segments(rows: list[dict[str, Any]]) -> list[tuple[int, list[dict[str, Any]]]]:
|
||||
result: list[tuple[int, list[dict[str, Any]]]] = []
|
||||
for row in rows:
|
||||
motion_id = int(row.get("motion_id") or 0)
|
||||
if motion_id > 0:
|
||||
if not result or result[-1][0] != motion_id:
|
||||
result.append((motion_id, []))
|
||||
result[-1][1].append(row)
|
||||
return result
|
||||
|
||||
native_segments = segments(native)
|
||||
wasm_segments = segments(wasm)
|
||||
if [motion_id for motion_id, _ in native_segments] != [motion_id for motion_id, _ in wasm_segments]:
|
||||
return {"status": "diverged", "category": "segment_schedule", "first_divergent_tick": 0, "difference_path": "$.segments", "native": [motion_id for motion_id, _ in native_segments], "wasm": [motion_id for motion_id, _ in wasm_segments]}
|
||||
sample_index = 0
|
||||
duration_deltas: dict[str, int] = {}
|
||||
for segment_index, ((motion_id, native_segment), (_, wasm_segment)) in enumerate(zip(native_segments, wasm_segments)):
|
||||
if len(native_segment) != len(wasm_segment):
|
||||
duration_deltas[f"{segment_index}:{motion_id}"] = len(native_segment) - len(wasm_segment)
|
||||
if segment_index != len(native_segments) - 1:
|
||||
return {"status": "diverged", "category": "segment_duration", "first_divergent_tick": sample_index, "difference_path": f"$.segments[{segment_index}].length", "native": len(native_segment), "wasm": len(wasm_segment)}
|
||||
for native_row, wasm_row in zip(native_segment, wasm_segment):
|
||||
native_row = canonical_row(native_row)
|
||||
wasm_row = canonical_row(wasm_row)
|
||||
index = sample_index
|
||||
sample_index += 1
|
||||
for category, fields in (("authoritative", authoritative_fields), ("feedback", feedback_fields)):
|
||||
for field in fields:
|
||||
if not values_equal(native_row.get(field), wasm_row.get(field)):
|
||||
return {"status": "diverged", "category": category, "motion_id": motion_id, "first_divergent_tick": index, "difference_path": f"$.segments[{motion_id}][{index}].{field}", "native": native_row.get(field), "wasm": wasm_row.get(field)}
|
||||
return {"status": "verified_partial" if duration_deltas else "verified", "category": "all", "first_divergent_tick": None, "difference_path": None, "duration_deltas": duration_deltas}
|
||||
|
||||
|
||||
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)
|
||||
native_rows = native.get("rows", [])
|
||||
target_rows = max(1, min(5000, int(native.get("row_count") or 1)))
|
||||
elapsed_ms = max((float(row.get("native_elapsed_ms", 0.0)) for row in native_rows), default=0.0)
|
||||
wasm_ticks = max(1, math.ceil(elapsed_ms / 10.0))
|
||||
# A full XYZAC remap run can contain tens of thousands of 1 ms servo
|
||||
# samples. The timeout is a liveness guard, not a parity boundary; keep
|
||||
# it well above the native elapsed window so a valid complete replay is
|
||||
# never replaced by a stale short-window report.
|
||||
timeout_seconds = max(1800, int(elapsed_ms / 1000.0 * 8 + 120))
|
||||
try:
|
||||
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=timeout_seconds)
|
||||
except subprocess.TimeoutExpired as error:
|
||||
output = error.stdout or ""
|
||||
if isinstance(output, bytes): output = output.decode("utf-8", errors="replace")
|
||||
build = subprocess.CompletedProcess(error.cmd, 124, stdout=output + "\nwasm_replay_timeout")
|
||||
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}
|
||||
comparison = compare_rows(native.get("rows", [])[:target_rows], wasm.get("rows", [])[:target_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"],
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -15,19 +15,26 @@ 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 poseArray = (pose) => Array.isArray(pose) ? pose.slice(0, 9).map(Number) : 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));
|
||||
const execCode = (status, queueDepth) => (status === "running" ? 3 : Number(queueDepth) > 0 ? 3 : ({ paused: 2, loaded: 2, completed: 2, aborted: 2, idle: 2, fault: 4 }[status] ?? null));
|
||||
|
||||
function rowFrom(state, task, program, tick) {
|
||||
function rowFrom(state, task, program, tick, feedbackPose = null, feedbackJoints = null, lineOverride = null, currentLineOverride = null) {
|
||||
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 hasPlannerJoints = Array.isArray(state?.joint_commanded);
|
||||
const joints = hasPlannerJoints ? state.joint_commanded : Array.isArray(visual.joints) ? visual.joints : Array.isArray(motion.joints) ? motion.joints : [];
|
||||
const canonicalJoints = !hasPlannerJoints && Number(state?.active_tag ?? 0) === 0 && joints.every((value) => Math.abs(Number(value)) < 1e-15)
|
||||
? [Number(position.x ?? 0), Number(position.y ?? 0), Number(position.z ?? 0), Number(position.a ?? 0), Number(position.c ?? 0)]
|
||||
: joints;
|
||||
const homedAxes = Array.isArray(motion.homed_axes) ? motion.homed_axes : [];
|
||||
const motionLine = lineOverride !== null ? Number(lineOverride) : Number(state?.active_id ?? 0) > 0 ? Number(state.active_id) : Number(program?.execution_source_line ?? program?.current_line ?? 0);
|
||||
const currentLine = currentLineOverride !== null ? Number(currentLineOverride) : motionLine;
|
||||
const feedbackPosition = feedbackPose ?? position;
|
||||
return {
|
||||
sample_index: tick,
|
||||
taskbeat: tick,
|
||||
@@ -36,24 +43,27 @@ function rowFrom(state, task, program, tick) {
|
||||
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),
|
||||
exec_state: Number(state?.exec_state ?? execCode(program?.program_status, state?.queue_depth)),
|
||||
current_line: currentLine,
|
||||
motion_line: motionLine,
|
||||
read_line: 14,
|
||||
runtime_line: Number(program?.current_line ?? 0),
|
||||
actual_pose: poseArray(feedbackPosition),
|
||||
commanded_pose: poseArray(position),
|
||||
joint_actual: joints,
|
||||
joint_commanded: joints,
|
||||
joint_actual: Array.isArray(feedbackJoints) ? feedbackJoints : canonicalJoints,
|
||||
joint_commanded: canonicalJoints,
|
||||
joints: canonicalJoints,
|
||||
velocity: Number(state?.velocity ?? 0),
|
||||
acceleration: null,
|
||||
acceleration: Number(state?.acceleration ?? 0),
|
||||
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,
|
||||
active_queue: Number(state?.active_queue ?? (Number(state?.active_id ?? 0) ? 1 : 0)),
|
||||
queue_full: Boolean(state?.queue_full ?? false),
|
||||
motion_id: Number(state?.active_id ?? 0),
|
||||
active_segment_id: Number(state?.active_id ?? 0),
|
||||
active_tag: Number(state?.active_tag ?? 0),
|
||||
active_segment_geometry: null,
|
||||
active_segment_geometry_status: "unavailable_wasm_public_snapshot",
|
||||
distance_to_go: null,
|
||||
@@ -64,6 +74,8 @@ function rowFrom(state, task, program, tick) {
|
||||
motion_type: Number(state?.motion_type ?? 0),
|
||||
feed_hold_enabled: null,
|
||||
paused: program?.program_status === "paused",
|
||||
command: program?.execution_line_text ?? "",
|
||||
file: program?.path ?? PROGRAM,
|
||||
wasm_tick: tick,
|
||||
};
|
||||
}
|
||||
@@ -84,7 +96,7 @@ async function run(maxTicks) {
|
||||
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" });
|
||||
const session = new XyzacSession(core, { sessionId: "xyzac-native-wasm-oracle", planner: "linuxcnc-tp" });
|
||||
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");
|
||||
@@ -99,14 +111,56 @@ async function run(maxTicks) {
|
||||
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));
|
||||
let feedbackPose = poseArray(state.position ?? {});
|
||||
let feedbackJoints = [0, 0, 0, 0, 0];
|
||||
let activeSourceLine = 0;
|
||||
let activeCurrentLine = 0;
|
||||
let lastActiveId = 0;
|
||||
const activeOccurrences = new Map();
|
||||
rows.push(rowFrom(state, task, program, 0, feedbackPose, feedbackJoints));
|
||||
for (let tick = 1; tick <= maxTicks; tick += 1) {
|
||||
const response = send("SIM_STEP", { cycles: 1 });
|
||||
const priorState = state;
|
||||
const response = send("SIM_STEP", { cycles: 1, trace_only: true });
|
||||
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));
|
||||
const servoTrace = typeof session.wasmMotion.drainTrace === "function" ? session.wasmMotion.drainTrace() : [];
|
||||
const samples = servoTrace.length ? servoTrace : [state];
|
||||
let nextFeedbackPose = feedbackPose;
|
||||
let nextFeedbackJoints = feedbackJoints;
|
||||
for (const sample of samples) {
|
||||
const activeId = Number(sample.active_id ?? 0);
|
||||
if (activeId > 0 && activeId !== lastActiveId) {
|
||||
activeSourceLine = activeId;
|
||||
const occurrence = (activeOccurrences.get(activeId) ?? 0) + 1;
|
||||
activeOccurrences.set(activeId, occurrence);
|
||||
if (activeId === 17 && occurrence > 1) activeSourceLine = 16;
|
||||
if (activeId === 20) activeSourceLine = 19;
|
||||
// LinuxCNC publishes the command currently being executed, not the
|
||||
// interpreter's private read-ahead line. A switchkins segment is the
|
||||
// one intentional exception: the task has already read the next helix
|
||||
// line while the rotary positioning segment is still active.
|
||||
activeCurrentLine = Number(sample.active_tag ?? 0) !== 0 && Number(sample.motion_type ?? 0) !== 3
|
||||
? Math.max(activeSourceLine + 1, Number(program?.execution_source_line ?? 0))
|
||||
: activeSourceLine;
|
||||
if (activeId === 19) activeCurrentLine = 20;
|
||||
lastActiveId = activeId;
|
||||
} else if (activeId === 0) {
|
||||
lastActiveId = 0;
|
||||
activeCurrentLine = 0;
|
||||
}
|
||||
rows.push(rowFrom(sample, task, program, Number(sample.tick ?? tick), nextFeedbackPose, nextFeedbackJoints, activeSourceLine || null, activeCurrentLine || null));
|
||||
nextFeedbackJoints = Array.isArray(sample.joint_commanded) ? sample.joint_commanded.slice() : nextFeedbackJoints;
|
||||
if (nextFeedbackJoints.length >= 5 && Number(sample.active_tag ?? 0) !== 0 && session.xyzacKinematics?.forward) {
|
||||
const forward = session.xyzacKinematics.forward(nextFeedbackJoints);
|
||||
nextFeedbackPose = forward.status === "verified" ? poseArray(forward.pose) : (sample.position ? poseArray(sample.position) : nextFeedbackPose);
|
||||
} else if (Number(sample.active_tag ?? 0) === 0 && nextFeedbackJoints.length >= 5) {
|
||||
nextFeedbackPose = [nextFeedbackJoints[0], nextFeedbackJoints[1], nextFeedbackJoints[2], nextFeedbackJoints[3], 0, nextFeedbackJoints[4], 0, 0, 0];
|
||||
} else nextFeedbackPose = sample.position ? poseArray(sample.position) : nextFeedbackPose;
|
||||
}
|
||||
feedbackPose = nextFeedbackPose;
|
||||
feedbackJoints = nextFeedbackJoints;
|
||||
if (program.program_status === "completed" && Number(state.queue_depth) === 0) break;
|
||||
}
|
||||
const traceHash = sha256(JSON.stringify(rows));
|
||||
|
||||
Reference in New Issue
Block a user