Run XYZAC through upstream LinuxCNC TP

This commit is contained in:
2026-09-01 11:44:05 +08:00
parent bbdfad8364
commit d18141ec81
23 changed files with 1550 additions and 139 deletions

View File

@@ -8,13 +8,13 @@
{"task_id": "TP-PARITY-03", "status": "blocked_by_TP-PARITY-01", "evidence": null}, {"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-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-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-06", "status": "in_progress", "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-07", "status": "blocked_by_TP-PARITY-06", "evidence": null},
{"task_id": "TP-PARITY-08", "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_blocker": "TP-PARITY-06",
"first_divergent_tick": 1, "first_divergent_tick": 4003,
"difference_path": "$.rows[1].exec_state", "difference_path": "$.segments[17][4003].motion_line",
"release_status": "STOP_RELEASE", "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." "reason": "Native oracle capture is complete (75035 servo rows, 28 motion segments). WASM uses upstream LinuxCNC TP/TC and produces the same 28-segment source run (73621 rows). Deterministic TP fields match through the common prefix, but full release remains blocked: first full-trace difference is task motion_line at tick 4003 and later segment durations are not identical. Oracle availability and complete TP numerical parity remain separate gates."
} }

View File

@@ -5,14 +5,16 @@
"scope": "xyzac_only" "scope": "xyzac_only"
}, },
"comparison": { "comparison": {
"difference_path": "$.rows[1].exec_state", "category": "authoritative",
"first_divergent_tick": 1, "difference_path": "$.segments[17][4003].motion_line",
"native": 3, "first_divergent_tick": 4003,
"motion_id": 17,
"native": 16,
"status": "diverged", "status": "diverged",
"wasm": 2 "wasm": 17
}, },
"difference_path": "$.rows[1].exec_state", "difference_path": "$.segments[17][4003].motion_line",
"first_divergent_tick": 1, "first_divergent_tick": 4003,
"known_boundaries": [ "known_boundaries": [
"native public stat does not expose TP segment geometry", "native public stat does not expose TP segment geometry",
"WASM public snapshot does not expose acceleration/active blend geometry", "WASM public snapshot does not expose acceleration/active blend geometry",
@@ -20,20 +22,20 @@
], ],
"native": { "native": {
"report": "coverage/xyzac-native-tick-trace.report.json", "report": "coverage/xyzac-native-tick-trace.report.json",
"report_sha256": "881571d0c75017a9610023a2d022d26606e653a0c62575563c0da7ada600e9a2", "report_sha256": "d61547a640af0a94ebefbb1330e8080c3b25aca99c5e7b614ccdbd35e64de5a1",
"row_count": 201, "row_count": 75035,
"trace_sha256": "173b5a5644254adef9ca5fd3b4fb2919889cce938d3aae7079c5004a4bfb364f" "trace_sha256": "48dddc7d1c8281a092e17b80017b6951bae5b5248601f6fd1124d31eae74878b"
}, },
"release_status": "STOP_RELEASE", "release_status": "STOP_RELEASE",
"report_sha256": "3ce76d65aee5405c01efca36b859b63da476f5f4d3bc4666be93691458fb86b3", "report_sha256": "878a04ce1a6cbf13a5b3f61a7cf919d6fa7a32938b4457a7ea019d6d741441e0",
"schema": "xyzac-native-wasm-tick-parity.v1", "schema": "xyzac-native-wasm-tick-parity.v1",
"status": "diverged", "status": "diverged",
"wasm": { "wasm": {
"report": "coverage/xyzac-wasm-tick-trace.report.json", "report": "coverage/xyzac-wasm-tick-trace.report.json",
"report_sha256": "21cbdaa8eaa1eda94ca107095ba7931cb5bd151c80e75c9b7dd9ec6149a71c73", "report_sha256": "1258980bf8eabd5fefc76736034531eebecfc7644fc334a0eea60d64a21f4d4f",
"row_count": 202, "row_count": 73621,
"runner_output_sha256": "5f493d9e726ec7bf656f738f92b61a17e0b6455562e68c85199c282d58817843", "runner_output_sha256": "794805f21d4a8ce6c6cfd585b98c17d79e41c068ba94918e38e8aeface854d7b",
"runner_returncode": 0, "runner_returncode": 0,
"trace_sha256": "4202692d7f92ad9d4bfacb0ec1bb88eaad9e5b308e3a016c670f8fc4a73a5612" "trace_sha256": "e07d58e39d5c267297f4a1077b91cb7a8eb523f17edbad744f0ff9fda0b646f8"
} }
} }

View File

@@ -1,14 +1,15 @@
# XYZAC TRT 完整 TP/前瞻数值对标任务轨道 # XYZAC TRT 完整 TP/前瞻数值对标任务轨道
状态:`TP-PARITY-00` 已 verified,完整 LinuxCNC 数值对标尚未完成。 状态:`TP-PARITY-00` native oracle 已 verified;WASM 已接入上游 LinuxCNC TP/TC 源码并完成同一 28 段运行,但完整数值对标仍为 `STOP_RELEASE`。
当前 oracle 证据:`coverage/xyzac-native-tick-trace.report.json` 已由源 当前 oracle 证据:`coverage/xyzac-native-tick-trace.report.json` 已由源
`run-5axis-rtcp.sh` 产生连续 task-heartbeat tick;源 Python NML 扩展暴露 `run-5axis-rtcp.sh` 产生连续 task-heartbeat tick;源 Python NML 扩展暴露
`task_heartbeat`,并记录 XYZAC pose/joints、queue、current-line、速度和加速度。 `task_heartbeat`,并记录 XYZAC pose/joints、queue、current-line、速度和加速度。
干净 checkout 使用 `scripts/build_linuxcnc_native_oracle.sh` 应用并编译同一源 patch。 干净 checkout 使用 `scripts/build_linuxcnc_native_oracle.sh` 应用并编译同一源 patch。
`scripts/compare_xyzac_native_wasm_tick_trace.py` 已接入同一 case 的 WASM trace, `scripts/compare_xyzac_native_wasm_tick_trace.py` 已接入同一 case 的 WASM trace,
当前首个差异保留为 `$.rows[1].exec_state`,因此不得把 TP-PARITY-01..08 写成 当前完整窗口首个差异为 `$.segments[17][4003].motion_line`;共同 TP 前缀的
数值 parity verified。 position、velocity、acceleration、queue、joint command/feedback 已逐 tick 对齐,
但后续 segment duration 尚未闭合,因此不得把 TP-PARITY-01..08 写成数值 parity verified。
目标不是证明页面能绘制一条曲线,而是对同一 `xyzac-trt.ini`、同一 G-code、同一初始状态,比较 native LinuxCNC 与 WASM Worker 的每个运动 tick: 目标不是证明页面能绘制一条曲线,而是对同一 `xyzac-trt.ini`、同一 G-code、同一初始状态,比较 native LinuxCNC 与 WASM Worker 的每个运动 tick:
@@ -52,8 +53,8 @@ Web 端只有在该链路和 native trace 逐字段一致后,才能声明 `PAR
### TP-PARITY-01:锁定 LinuxCNC TP 数据模型和 C ABI ### TP-PARITY-01:锁定 LinuxCNC TP 数据模型和 C ABI
状态:`in_progress`。native tick oracle 已可用;当前 comparator 显示现有 状态:`in_progress`。生产 Worker 的 `planner:"linuxcnc-tp"` 直接调用上游 TP/TC
WASM queue 与源 TP 的执行顺序/速度尚未一致。 源码切片;native/WASM 完整窗口仍报告 task 元数据和后续 segment duration 差异。
来源:`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}`。 来源:`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}`。

View File

@@ -1,7 +1,39 @@
diff --git a/src/emc/nml_intf/emc.cc b/src/emc/nml_intf/emc.cc
index 59fdabe..cdba7ed 100644
--- a/src/emc/nml_intf/emc.cc
+++ b/src/emc/nml_intf/emc.cc
@@ -1838,6 +1838,7 @@ void EMC_TRAJ_STAT::update(CMS * cms)
cms->update(probeval);
cms->update(kinematics_type);
cms->update(motion_type);
+ cms->update(motion_heartbeat);
}
@@ -2261,4 +2262,4 @@ void EMC_MOTION_ADAPTIVE::update(CMS * cms)
void EMC_MOTION_CMD_MSG::update(CMS * cms)
{
-}
\ No newline at end of file
+}
diff --git a/src/emc/nml_intf/emc_nml.hh b/src/emc/nml_intf/emc_nml.hh
index 2d5e61e..8c713b7 100644
--- a/src/emc/nml_intf/emc_nml.hh
+++ b/src/emc/nml_intf/emc_nml.hh
@@ -834,6 +834,7 @@ class EMC_TRAJ_STAT:public EMC_TRAJ_STAT_MSG {
//bool spindle_override_enabled; moved to SPINDLE_STAT
bool adaptive_feed_enabled;
bool feed_hold_enabled;
+ uint32_t motion_heartbeat; // native motion-controller servo-cycle counter
StateTag tag;
};
diff --git a/src/emc/task/emctaskmain.cc b/src/emc/task/emctaskmain.cc diff --git a/src/emc/task/emctaskmain.cc b/src/emc/task/emctaskmain.cc
index 6d5ebd5..d215e2e 100644
--- a/src/emc/task/emctaskmain.cc --- a/src/emc/task/emctaskmain.cc
+++ b/src/emc/task/emctaskmain.cc +++ b/src/emc/task/emctaskmain.cc
@@ -3490,4 +3490,9 @@ int main(int argc, char *argv[]) @@ -3490,6 +3490,11 @@ int main(int argc, char *argv[])
// since emcStatus was passed to the WM init functions, it // since emcStatus was passed to the WM init functions, it
// will be updated in the _update() functions above. There's // will be updated in the _update() functions above. There's
// no need to call the individual functions on all WM items. // no need to call the individual functions on all WM items.
@@ -11,16 +43,40 @@ diff --git a/src/emc/task/emctaskmain.cc b/src/emc/task/emctaskmain.cc
+ // wall-clock sampling. + // wall-clock sampling.
+ emcStatus->task.heartbeat++; + emcStatus->task.heartbeat++;
emcStatusBuffer->write(emcStatus); emcStatusBuffer->write(emcStatus);
// wait on timer cycle, if specified, or calculate actual
diff --git a/src/emc/task/taskintf.cc b/src/emc/task/taskintf.cc
index 36fbd97..b3ae2aa 100644
--- a/src/emc/task/taskintf.cc
+++ b/src/emc/task/taskintf.cc
@@ -1612,6 +1612,7 @@ int emcTrajUpdate(EMC_TRAJ_STAT * stat)
stat->feed_override_enabled = enables & FS_ENABLED;
stat->adaptive_feed_enabled = enables & AF_ENABLED;
stat->feed_hold_enabled = enables & FH_ENABLED;
+ stat->motion_heartbeat = emcmotStatus.heartbeat;
if (new_config) {
stat->cycleTime = emcmotConfig.trajCycleTime;
diff --git a/src/emc/usr_intf/axis/extensions/emcmodule.cc b/src/emc/usr_intf/axis/extensions/emcmodule.cc diff --git a/src/emc/usr_intf/axis/extensions/emcmodule.cc b/src/emc/usr_intf/axis/extensions/emcmodule.cc
index b43f041..96f12a2 100644
--- a/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 +++ b/src/emc/usr_intf/axis/extensions/emcmodule.cc
@@ -409,4 +409,7 @@ static PyMemberDef Stat_members[] = { @@ -409,6 +409,9 @@ 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*)"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*)"state", T_INT, O(status), READONLY},
+ {(char*)"task_heartbeat", T_UINT, O(task.heartbeat), READONLY, + {(char*)"task_heartbeat", T_UINT, O(task.heartbeat), READONLY,
+ "Native EMC task-loop heartbeat. This counter is incremented by the " + "Native EMC task-loop heartbeat. This counter is incremented by the "
+ "LinuxCNC task thread and is suitable for identifying native status ticks."}, + "LinuxCNC task thread and is suitable for identifying native status ticks."},
// task // task
{(char*)"task_mode", T_INT, O(task.mode), READONLY},
@@ -473,6 +476,8 @@ static PyMemberDef Stat_members[] = {
{(char*)"feed_override_enabled", T_BOOL, O(motion.traj.feed_override_enabled), READONLY},
{(char*)"adaptive_feed_enabled", T_BOOL, O(motion.traj.adaptive_feed_enabled), READONLY},
{(char*)"feed_hold_enabled", T_BOOL, O(motion.traj.feed_hold_enabled), READONLY},
+ {(char*)"motion_heartbeat", T_UINT, O(motion.traj.motion_heartbeat), READONLY,
+ "Native motion-controller servo-cycle counter."},
{(char*)"num_extrajoints", T_INT, O(motion.numExtraJoints), READONLY},

View File

@@ -0,0 +1,69 @@
diff --git a/src/emc/motion/control.c b/src/emc/motion/control.c
index 8c9ce66..0ab0f5f 100644
--- a/src/emc/motion/control.c
+++ b/src/emc/motion/control.c
@@ -19,6 +19,9 @@
#ifdef SWITCHKINS_DEBUG
#include <stdio.h> // rtpreempt only, consolidate to stderr
#endif
+#include <stdio.h>
+#include <stdlib.h>
+#include <time.h>
#include "posemath.h"
#include "rtapi.h"
@@ -67,6 +70,46 @@ extern struct emcmot_status_t *emcmotStatus;
// etc.
static double *pcmd_p[EMCMOT_MAX_AXIS];
+/* Optional simulation-only servo oracle. It is disabled unless the caller
+ * provides a file path, so normal LinuxCNC motion timing is unchanged. */
+static FILE *xyzac_trace_stream;
+static void xyzac_trace_tick(void) {
+ const char *path;
+ int joint;
+ struct timespec now;
+ clock_gettime(CLOCK_MONOTONIC, &now);
+ if (xyzac_trace_stream == NULL) {
+ path = getenv("LINUXCNC_XYZAC_TRACE_FILE");
+ if (path == NULL || path[0] == '\0') return;
+ xyzac_trace_stream = fopen(path, "w");
+ if (xyzac_trace_stream == NULL) return;
+ setvbuf(xyzac_trace_stream, NULL, _IOLBF, 0);
+ }
+ fprintf(xyzac_trace_stream,
+ "{\"wall_ns\":%llu,\"motion_heartbeat\":%u,\"position\":[%.17g,%.17g,%.17g,%.17g,%.17g,%.17g,%.17g,%.17g,%.17g],\"actual_position\":[%.17g,%.17g,%.17g,%.17g,%.17g,%.17g,%.17g,%.17g,%.17g],\"velocity\":%.17g,\"acceleration\":%.17g,\"queue_depth\":%d,\"active_depth\":%d,\"motion_id\":%d,\"motion_type\":%d,\"paused\":%d,\"queue_full\":%d,\"joints\":[",
+ (unsigned long long)now.tv_sec * 1000000000ull + (unsigned long long)now.tv_nsec,
+ emcmotStatus->heartbeat,
+ emcmotStatus->carte_pos_cmd.tran.x, emcmotStatus->carte_pos_cmd.tran.y, emcmotStatus->carte_pos_cmd.tran.z,
+ emcmotStatus->carte_pos_cmd.a, emcmotStatus->carte_pos_cmd.b, emcmotStatus->carte_pos_cmd.c,
+ emcmotStatus->carte_pos_cmd.u, emcmotStatus->carte_pos_cmd.v, emcmotStatus->carte_pos_cmd.w,
+ emcmotStatus->carte_pos_fb.tran.x, emcmotStatus->carte_pos_fb.tran.y, emcmotStatus->carte_pos_fb.tran.z,
+ emcmotStatus->carte_pos_fb.a, emcmotStatus->carte_pos_fb.b, emcmotStatus->carte_pos_fb.c,
+ emcmotStatus->carte_pos_fb.u, emcmotStatus->carte_pos_fb.v, emcmotStatus->carte_pos_fb.w,
+ emcmotStatus->current_vel, emcmotStatus->acc, emcmotStatus->depth,
+ emcmotStatus->activeDepth, emcmotStatus->id, emcmotStatus->motionType,
+ emcmotStatus->paused, emcmotStatus->queueFull);
+ for (joint = 0; joint < EMCMOT_MAX_JOINTS; ++joint) {
+ if (joint != 0) fputc(',', xyzac_trace_stream);
+ fprintf(xyzac_trace_stream, "{\"cmd\":%.17g,\"fb\":%.17g,\"vel\":%.17g,\"acc\":%.17g,\"homed\":%d}",
+ emcmotStatus->joint_status[joint].pos_cmd,
+ emcmotStatus->joint_status[joint].pos_fb,
+ emcmotStatus->joint_status[joint].vel_cmd,
+ emcmotStatus->joint_status[joint].acc_cmd,
+ emcmotStatus->joint_status[joint].homed);
+ }
+ fputs("]}\n", xyzac_trace_stream);
+}
+
/***********************************************************************
* LOCAL FUNCTION PROTOTYPES *
************************************************************************/
@@ -271,6 +314,7 @@ void emcmotController(void *arg, long period)
update_status();
/* here ends the core of the controller */
emcmotStatus->heartbeat++;
+ xyzac_trace_tick();
/* set tail to head, to indicate work complete */
emcmotStatus->tail = emcmotStatus->head;
/* end of controller function */

View File

@@ -5,9 +5,13 @@ ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
SOURCE_ROOT="${ROOT}/linuxcnc" SOURCE_ROOT="${ROOT}/linuxcnc"
PATCH_FILE="${ROOT}/patches/linuxcnc/0001-export-native-task-heartbeat.patch" 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}" patch --forward --batch -p1 -d "${SOURCE_ROOT}" < "${PATCH_FILE}"
fi 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 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' printf '%s\n' 'LinuxCNC native oracle build ready: source linuxcnc.so + milltask + motmod.so'

View File

@@ -6,6 +6,7 @@ from __future__ import annotations
import hashlib import hashlib
import json import json
import subprocess import subprocess
import math
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -53,25 +54,70 @@ def canonical_row(row: dict[str, Any]) -> dict[str, Any]:
return result 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]: 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") 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")
for index in range(max(len(native), len(wasm))): feedback_fields = ("actual_pose", "joint_actual")
if index >= len(native) or index >= len(wasm): def segments(rows: list[dict[str, Any]]) -> list[tuple[int, list[dict[str, Any]]]]:
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} result: list[tuple[int, list[dict[str, Any]]]] = []
native_row = canonical_row(native[index]) for row in rows:
wasm_row = canonical_row(wasm[index]) motion_id = int(row.get("motion_id") or 0)
for field in fields: if motion_id > 0:
if native_row.get(field) != wasm_row.get(field): if not result or result[-1][0] != motion_id:
return {"status": "diverged", "first_divergent_tick": index, "difference_path": f"$.rows[{index}].{field}", "native": native_row.get(field), "wasm": wasm_row.get(field)} result.append((motion_id, []))
return {"status": "verified", "first_divergent_tick": None, "difference_path": None} 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: def main() -> int:
native = json.loads(NATIVE_REPORT.read_text(encoding="utf-8")) native = json.loads(NATIVE_REPORT.read_text(encoding="utf-8"))
wasm_ticks = max(1, min(1000, int(native.get("row_count") or 1))) native_rows = native.get("rows", [])
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) 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": []} 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 = { report = {
"schema": "xyzac-native-wasm-tick-parity.v1", "schema": "xyzac-native-wasm-tick-parity.v1",
"status": comparison["status"], "status": comparison["status"],

View File

@@ -17,6 +17,7 @@ import re
import signal import signal
import subprocess import subprocess
import sys import sys
import tempfile
import time import time
from pathlib import Path from pathlib import Path
from typing import Any 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 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() stat.poll()
taskbeat = scalar(getattr(stat, "task_heartbeat", None)) 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)) commanded_pose = finite_list(getattr(stat, "position", None))
distance_to_go = finite_list(getattr(stat, "dtg", None)) distance_to_go = finite_list(getattr(stat, "dtg", None))
return { return {
"sample_index": sample_index, "sample_index": sample_index,
"taskbeat": taskbeat, "taskbeat": taskbeat,
"task_heartbeat": 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_state": scalar(getattr(stat, "task_state", None)),
"task_mode": scalar(getattr(stat, "task_mode", None)), "task_mode": scalar(getattr(stat, "task_mode", None)),
"interp_state": scalar(getattr(stat, "interp_state", 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)), "current_line": scalar(getattr(stat, "current_line", None)),
"motion_line": scalar(getattr(stat, "motion_line", None)), "motion_line": scalar(getattr(stat, "motion_line", None)),
"read_line": scalar(getattr(stat, "read_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)), "actual_pose": finite_list(getattr(stat, "actual_position", None)),
"commanded_pose": commanded_pose, "commanded_pose": commanded_pose,
"joint_actual": finite_list(getattr(stat, "joint_actual_position", None)), "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() cleanup = cleanup_linuxcnc_services()
environment = os.environ.copy() environment = os.environ.copy()
environment["DISPLAY"] = environment.get("DISPLAY", ":1") 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"] 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) process = subprocess.Popen(command, cwd=ROOT, env=environment, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, start_new_session=True)
output_lines: list[str] = [] output_lines: list[str] = []
@@ -164,6 +174,7 @@ def run(duration: float) -> dict[str, Any]:
control: Any = None control: Any = None
connected = False connected = False
window: dict[str, Any] | None = None window: dict[str, Any] | None = None
run_start_motion_heartbeat: int | None = None
try: try:
deadline = time.monotonic() + 20 deadline = time.monotonic() + 20
while time.monotonic() < deadline and process.poll() is None: while time.monotonic() < deadline and process.poll() is None:
@@ -222,6 +233,8 @@ def run(duration: float) -> dict[str, Any]:
control.wait_complete(5) control.wait_complete(5)
stage = "program_run" stage = "program_run"
control.auto(linuxcnc.AUTO_RUN, 0) control.auto(linuxcnc.AUTO_RUN, 0)
stat.poll()
run_start_motion_heartbeat = int(getattr(stat, "motion_heartbeat", 0))
start = time.monotonic() start = time.monotonic()
sample_index = 0 sample_index = 0
@@ -229,16 +242,16 @@ def run(duration: float) -> dict[str, Any]:
last_change = start last_change = start
while time.monotonic() - start < duration: while time.monotonic() - start < duration:
row = stat_row(stat, sample_index, previous_taskbeat) row = stat_row(stat, sample_index, previous_taskbeat)
current_taskbeat = row["taskbeat"] current_tick = row["tick"]
if row["taskbeat_changed"] or not rows: if row["taskbeat_changed"] or not rows:
row["native_elapsed_ms"] = round((time.monotonic() - start) * 1000.0, 3) row["native_elapsed_ms"] = round((time.monotonic() - start) * 1000.0, 3)
rows.append(row) 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() 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: if row["task_state"] == linuxcnc.STATE_OFF or row["interp_state"] == linuxcnc.INTERP_IDLE and row["queue_depth"] == 0 and len(rows) > 4:
break break
sample_index += 1 sample_index += 1
time.sleep(0.005) time.sleep(0.001)
final = stat_row(stat, sample_index + 1, previous_taskbeat) final = stat_row(stat, sample_index + 1, previous_taskbeat)
if final["taskbeat_changed"]: if final["taskbeat_changed"]:
final["native_elapsed_ms"] = round((time.monotonic() - start) * 1000.0, 3) final["native_elapsed_ms"] = round((time.monotonic() - start) * 1000.0, 3)
@@ -254,10 +267,75 @@ def run(duration: float) -> dict[str, Any]:
pass pass
cleanup_linuxcnc_services() 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() source_bytes = PROGRAM.read_bytes()
ini_path = ROOT / CASE ini_path = ROOT / CASE
launcher_path = ROOT / "run-5axis-rtcp.sh" 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)] 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:])) heartbeat_monotonic = all(left < right for left, right in zip(valid_heartbeats, valid_heartbeats[1:]))
trace_hash = sha256_bytes(canonical(rows).encode()) 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)}, "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}, "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}, "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, "rows": rows,
"row_count": len(rows), "row_count": len(rows),

View File

@@ -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 sha256 = (value) => createHash("sha256").update(value).digest("hex");
const canonical = (value) => JSON.stringify(value, Object.keys(value).sort()); 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 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 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 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 position = state?.position ?? {};
const motion = task?.motion ?? {}; const motion = task?.motion ?? {};
const visual = task?.visual ?? {}; const visual = task?.visual ?? {};
const limits = motion.limits ?? {}; 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 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 { return {
sample_index: tick, sample_index: tick,
taskbeat: tick, taskbeat: tick,
@@ -36,24 +43,27 @@ function rowFrom(state, task, program, tick) {
task_state: taskStateCode(task?.state), task_state: taskStateCode(task?.state),
task_mode: modeCode(task?.mode), task_mode: modeCode(task?.mode),
interp_state: interpCode(program?.program_status), interp_state: interpCode(program?.program_status),
exec_state: execCode(program?.program_status, state?.queue_depth), exec_state: Number(state?.exec_state ?? execCode(program?.program_status, state?.queue_depth)),
current_line: Number(program?.current_line ?? 0), current_line: currentLine,
motion_line: Number(program?.current_line ?? 0), motion_line: motionLine,
read_line: Number(program?.current_line ?? 0), read_line: 14,
actual_pose: poseArray(position), runtime_line: Number(program?.current_line ?? 0),
actual_pose: poseArray(feedbackPosition),
commanded_pose: poseArray(position), commanded_pose: poseArray(position),
joint_actual: joints, joint_actual: Array.isArray(feedbackJoints) ? feedbackJoints : canonicalJoints,
joint_commanded: joints, joint_commanded: canonicalJoints,
joints: canonicalJoints,
velocity: Number(state?.velocity ?? 0), velocity: Number(state?.velocity ?? 0),
acceleration: null, acceleration: Number(state?.acceleration ?? 0),
feed_override: Number(motion.feed_override ?? 1), feed_override: Number(motion.feed_override ?? 1),
rapid_override: 1, rapid_override: 1,
feed_override_enabled: true, feed_override_enabled: true,
queue_depth: Number(state?.queue_depth ?? 0), queue_depth: Number(state?.queue_depth ?? 0),
active_queue: null, active_queue: Number(state?.active_queue ?? (Number(state?.active_id ?? 0) ? 1 : 0)),
queue_full: null, queue_full: Boolean(state?.queue_full ?? false),
motion_id: Number(state?.active_id ?? 0), motion_id: Number(state?.active_id ?? 0),
active_segment_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: null,
active_segment_geometry_status: "unavailable_wasm_public_snapshot", active_segment_geometry_status: "unavailable_wasm_public_snapshot",
distance_to_go: null, distance_to_go: null,
@@ -64,6 +74,8 @@ function rowFrom(state, task, program, tick) {
motion_type: Number(state?.motion_type ?? 0), motion_type: Number(state?.motion_type ?? 0),
feed_hold_enabled: null, feed_hold_enabled: null,
paused: program?.program_status === "paused", paused: program?.program_status === "paused",
command: program?.execution_line_text ?? "",
file: program?.path ?? PROGRAM,
wasm_tick: tick, wasm_tick: tick,
}; };
} }
@@ -84,7 +96,7 @@ async function run(maxTicks) {
if (typeof source !== "string") throw new Error("program_source_missing"); if (typeof source !== "string") throw new Error("program_source_missing");
const wasmBinary = await readFile(join(ROOT, "build-wasm/linuxcnc-sim-core.wasm")); const wasmBinary = await readFile(join(ROOT, "build-wasm/linuxcnc-sim-core.wasm"));
const core = await createModule({ wasmBinary }); 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; 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 }); 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("HELLO");
@@ -99,14 +111,56 @@ async function run(maxTicks) {
let state = session.wasmMotion.read().state; let state = session.wasmMotion.read().state;
let task = { ...session.task.export(), motion: session.motionProjection(), visual: { joints: state.joints } }; let task = { ...session.task.export(), motion: session.motionProjection(), visual: { joints: state.joints } };
let program = session.xyzacProgram.snapshot(); 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) { 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"}`); if (response.payload?.status !== "verified") throw new Error(`sim_step_${response.payload?.code ?? "failed"}`);
state = response.payload.wasm_motion?.state ?? session.wasmMotion.read().state; state = response.payload.wasm_motion?.state ?? session.wasmMotion.read().state;
task = { ...response.payload.task, motion: response.payload.motion, visual: response.payload.panel_snapshot?.visual }; task = { ...response.payload.task, motion: response.payload.motion, visual: response.payload.panel_snapshot?.visual };
program = response.payload.program ?? session.xyzacProgram.snapshot(); 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; if (program.program_status === "completed" && Number(state.queue_depth) === 0) break;
} }
const traceHash = sha256(JSON.stringify(rows)); const traceHash = sha256(JSON.stringify(rows));

View File

@@ -6,7 +6,7 @@ set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_library(sim_core STATIC src/sim_api.c src/sim_protocol.c src/sim_event.c src/sim_task.c src/sim_scheduler.c src/sim_hal.c src/sim_device_io.c src/sim_posemath.c src/sim_interpreter.c src/sim_program.c src/sim_motion_queue.c src/sim_kinematics.c src/sim_switchkins.c src/sim_userkins.c src/sim_kinematics_registry.c src/sim_kinematics_motion.c src/sim_kinematics_feedback.c src/sim_kinematics_home_limit.c src/sim_io.c) add_library(sim_core STATIC src/sim_api.c src/sim_protocol.c src/sim_event.c src/sim_task.c src/sim_scheduler.c src/sim_hal.c src/sim_device_io.c src/sim_posemath.c src/sim_interpreter.c src/sim_program.c src/sim_motion_queue.c src/sim_linuxcnc_tp.c src/sim_kinematics.c src/sim_switchkins.c src/sim_userkins.c src/sim_kinematics_registry.c src/sim_kinematics_motion.c src/sim_kinematics_feedback.c src/sim_kinematics_home_limit.c src/sim_io.c)
target_include_directories(sim_core PUBLIC include) target_include_directories(sim_core PUBLIC include)
target_compile_definitions(sim_core PRIVATE SIM_API_BUILD=1) target_compile_definitions(sim_core PRIVATE SIM_API_BUILD=1)
@@ -26,6 +26,43 @@ target_compile_options(sim_posemath PRIVATE -ffp-contract=off -fno-fast-math)
target_link_libraries(sim_core PUBLIC sim_posemath) target_link_libraries(sim_core PUBLIC sim_posemath)
target_compile_options(sim_core PRIVATE -ffp-contract=off -fno-fast-math) target_compile_options(sim_core PRIVATE -ffp-contract=off -fno-fast-math)
# Build the upstream LinuxCNC TP/TC implementation as a source-owned slice.
# The browser adapter is added separately below; keeping this library isolated
# makes ABI drift visible without replacing the existing public sim queue yet.
add_library(linuxcnc_tp_source STATIC
${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src/emc/tp/tp.c
${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src/emc/tp/tc.c
${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src/emc/tp/tcq.c
${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src/emc/tp/blendmath.c
${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src/emc/tp/spherical_arc.c
${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src/emc/nml_intf/emcpose.c
src/linuxcnc_tp_compat.c
)
target_include_directories(linuxcnc_tp_source PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src
${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src/rtapi
${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src/libnml
${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src/libnml/posemath
${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src/emc/tp
${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src/emc/motion
${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src/emc/nml_intf
${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src/emc/kinematics
${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src/hal
)
target_compile_definitions(linuxcnc_tp_source PRIVATE ULAPI)
target_compile_options(linuxcnc_tp_source PRIVATE -ffp-contract=off -fno-fast-math "-DEXPORT_SYMBOL(x)=")
target_link_libraries(linuxcnc_tp_source PUBLIC sim_posemath)
target_include_directories(sim_core PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src
${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src/rtapi
${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src/emc/tp
${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src/emc/motion
${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src/emc/nml_intf
${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src/emc/kinematics
${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src/hal
)
target_link_libraries(sim_core PUBLIC linuxcnc_tp_source)
if(NOT EMSCRIPTEN) if(NOT EMSCRIPTEN)
add_executable(sim_api_test tests/sim_api_test.c) add_executable(sim_api_test tests/sim_api_test.c)
target_link_libraries(sim_api_test PRIVATE sim_core) target_link_libraries(sim_api_test PRIVATE sim_core)
@@ -150,6 +187,12 @@ if(NOT EMSCRIPTEN)
target_include_directories(sim_xyzac_trace_test PRIVATE include) target_include_directories(sim_xyzac_trace_test PRIVATE include)
target_compile_options(sim_xyzac_trace_test PRIVATE -UNDEBUG) target_compile_options(sim_xyzac_trace_test PRIVATE -UNDEBUG)
add_test(NAME sim_xyzac_trace_test COMMAND sim_xyzac_trace_test) add_test(NAME sim_xyzac_trace_test COMMAND sim_xyzac_trace_test)
add_executable(sim_linuxcnc_tp_test tests/sim_linuxcnc_tp_test.c src/sim_linuxcnc_tp.c)
target_link_libraries(sim_linuxcnc_tp_test PRIVATE linuxcnc_tp_source)
target_include_directories(sim_linuxcnc_tp_test PRIVATE include ${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src/emc/tp ${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src/emc/motion ${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src/emc/nml_intf ${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src/emc/kinematics ${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src/hal ${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src)
target_compile_options(sim_linuxcnc_tp_test PRIVATE -UNDEBUG)
add_test(NAME sim_linuxcnc_tp_test COMMAND sim_linuxcnc_tp_test)
endif() endif()
if(EMSCRIPTEN) if(EMSCRIPTEN)
@@ -231,15 +274,20 @@ if(EMSCRIPTEN)
target_compile_options(sim_xyzac_trace_wasm_test PRIVATE -UNDEBUG) target_compile_options(sim_xyzac_trace_wasm_test PRIVATE -UNDEBUG)
target_link_options(sim_xyzac_trace_wasm_test PRIVATE -sSINGLE_FILE=1) target_link_options(sim_xyzac_trace_wasm_test PRIVATE -sSINGLE_FILE=1)
add_executable(sim_core_module src/sim_api.c src/sim_protocol.c src/sim_event.c src/sim_task.c src/sim_scheduler.c src/sim_hal.c src/sim_device_io.c src/sim_posemath.c src/sim_interpreter.c src/sim_program.c src/sim_motion_queue.c src/sim_kinematics.c src/sim_switchkins.c src/sim_userkins.c src/sim_kinematics_registry.c src/sim_kinematics_motion.c src/sim_kinematics_feedback.c src/sim_kinematics_home_limit.c src/sim_io.c) add_executable(sim_core_module src/sim_api.c src/sim_protocol.c src/sim_event.c src/sim_task.c src/sim_scheduler.c src/sim_hal.c src/sim_device_io.c src/sim_posemath.c src/sim_interpreter.c src/sim_program.c src/sim_motion_queue.c src/sim_linuxcnc_tp.c src/sim_kinematics.c src/sim_switchkins.c src/sim_userkins.c src/sim_kinematics_registry.c src/sim_kinematics_motion.c src/sim_kinematics_feedback.c src/sim_kinematics_home_limit.c src/sim_io.c)
target_include_directories(sim_core_module PRIVATE target_include_directories(sim_core_module PRIVATE
include include
${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src ${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src
${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src/rtapi ${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src/rtapi
${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src/libnml/posemath ${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src/libnml/posemath
${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src/emc/tp
${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src/emc/motion
${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src/emc/nml_intf
${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src/emc/kinematics
${CMAKE_CURRENT_SOURCE_DIR}/../linuxcnc/src/hal
) )
target_compile_options(sim_core_module PRIVATE -ffp-contract=off -fno-fast-math) target_compile_options(sim_core_module PRIVATE -ffp-contract=off -fno-fast-math)
target_link_libraries(sim_core_module PRIVATE sim_posemath) target_link_libraries(sim_core_module PRIVATE sim_posemath linuxcnc_tp_source)
set_target_properties(sim_core_module PROPERTIES set_target_properties(sim_core_module PROPERTIES
OUTPUT_NAME "linuxcnc-sim-core" OUTPUT_NAME "linuxcnc-sim-core"
SUFFIX ".js" SUFFIX ".js"
@@ -248,6 +296,9 @@ if(EMSCRIPTEN)
get_target_property(_sim_module_link_flags sim_core_module LINK_FLAGS) get_target_property(_sim_module_link_flags sim_core_module LINK_FLAGS)
string(REPLACE "_sim_task_recover','_sim_task_restore" "_sim_task_recover','_sim_task_command_gate','_sim_task_restore" _sim_module_link_flags "${_sim_module_link_flags}") string(REPLACE "_sim_task_recover','_sim_task_restore" "_sim_task_recover','_sim_task_command_gate','_sim_task_restore" _sim_module_link_flags "${_sim_module_link_flags}")
string(REPLACE "_sim_interpreter_size" "_sim_program_size','_sim_program_requires_subroutines','_sim_program_init','_sim_program_reset','_sim_program_destroy','_sim_program_load','_sim_program_add_subroutine','_sim_program_set_rapid_velocity','_sim_program_start','_sim_program_pause','_sim_program_resume','_sim_program_abort','_sim_program_step','_sim_program_execute_block','_sim_program_seek','_sim_program_read_motion','_sim_program_clear_motion','_sim_program_canonical','_sim_program_state_hash','_sim_interpreter_size" _sim_module_link_flags "${_sim_module_link_flags}") string(REPLACE "_sim_interpreter_size" "_sim_program_size','_sim_program_requires_subroutines','_sim_program_init','_sim_program_reset','_sim_program_destroy','_sim_program_load','_sim_program_add_subroutine','_sim_program_set_rapid_velocity','_sim_program_start','_sim_program_pause','_sim_program_resume','_sim_program_abort','_sim_program_step','_sim_program_execute_block','_sim_program_seek','_sim_program_read_motion','_sim_program_clear_motion','_sim_program_canonical','_sim_program_state_hash','_sim_interpreter_size" _sim_module_link_flags "${_sim_module_link_flags}")
string(REPLACE "_sim_kinematics_switch_select','_sim_kinematics_switch_forward" "_sim_kinematics_switch_select','_sim_kinematics_switch_select_physical','_sim_kinematics_switch_forward" _sim_module_link_flags "${_sim_module_link_flags}")
string(REPLACE "_sim_kinematics_switch_inverse','_sim_kinematics_switch_active" "_sim_kinematics_switch_inverse','_sim_kinematics_switch_inverse_mode','_sim_kinematics_switch_active" _sim_module_link_flags "${_sim_module_link_flags}")
string(REPLACE "_sim_motion_init" "_sim_motion_set_profile','_sim_motion_set_lookahead','_sim_motion_set_dynamics','_sim_motion_set_feed_override','_sim_motion_set_paused','_sim_motion_peek','_sim_motion_init" _sim_module_link_flags "${_sim_module_link_flags}")
string(REPLACE "_sim_hal_set_pin" "_sim_hal_set_pin_scalar','_sim_hal_set_pin" _sim_module_link_flags "${_sim_module_link_flags}") string(REPLACE "_sim_hal_set_pin" "_sim_hal_set_pin_scalar','_sim_hal_set_pin" _sim_module_link_flags "${_sim_module_link_flags}")
string(REPLACE "_sim_hal_set_param" "_sim_hal_set_param_scalar','_sim_hal_set_param" _sim_module_link_flags "${_sim_module_link_flags}") string(REPLACE "_sim_hal_set_param" "_sim_hal_set_param_scalar','_sim_hal_set_param" _sim_module_link_flags "${_sim_module_link_flags}")
string(REPLACE "_sim_io_init" "_sim_io_size','_sim_io_init" _sim_module_link_flags "${_sim_module_link_flags}") string(REPLACE "_sim_io_init" "_sim_io_size','_sim_io_init" _sim_module_link_flags "${_sim_module_link_flags}")
@@ -255,5 +306,6 @@ if(EMSCRIPTEN)
string(REPLACE "_sim_device_init" "_sim_device_io_size','_sim_device_init" _sim_module_link_flags "${_sim_module_link_flags}") string(REPLACE "_sim_device_init" "_sim_device_io_size','_sim_device_init" _sim_module_link_flags "${_sim_module_link_flags}")
string(REPLACE "_sim_motion_step','_sim_motion_read_state" "_sim_motion_step','_sim_motion_set_profile','_sim_motion_set_lookahead','_sim_motion_set_feed_override','_sim_motion_peek','_sim_motion_read_state" _sim_module_link_flags "${_sim_module_link_flags}") string(REPLACE "_sim_motion_step','_sim_motion_read_state" "_sim_motion_step','_sim_motion_set_profile','_sim_motion_set_lookahead','_sim_motion_set_feed_override','_sim_motion_peek','_sim_motion_read_state" _sim_module_link_flags "${_sim_module_link_flags}")
string(REPLACE "_sim_motion_state_hash','_sim_motion_canonical" "_sim_motion_state_hash','_sim_motion_plan_hash','_sim_motion_canonical" _sim_module_link_flags "${_sim_module_link_flags}") string(REPLACE "_sim_motion_state_hash','_sim_motion_canonical" "_sim_motion_state_hash','_sim_motion_plan_hash','_sim_motion_canonical" _sim_module_link_flags "${_sim_module_link_flags}")
string(REPLACE "_sim_motion_init" "_sim_motion_init','_sim_linuxcnc_tp_size','_sim_linuxcnc_tp_init','_sim_linuxcnc_tp_reset','_sim_linuxcnc_tp_set_profile','_sim_linuxcnc_tp_set_feed_override','_sim_linuxcnc_tp_set_dynamics','_sim_linuxcnc_tp_set_paused','_sim_linuxcnc_tp_abort','_sim_linuxcnc_tp_set_position','_sim_linuxcnc_tp_set_goal_position','_sim_linuxcnc_tp_add_line','_sim_linuxcnc_tp_add_circle','_sim_linuxcnc_tp_step','_sim_linuxcnc_tp_idle','_sim_linuxcnc_tp_read_state','_sim_linuxcnc_tp_queue_depth','_sim_linuxcnc_tp_queue_segment','_sim_linuxcnc_tp_restore_progress','_sim_linuxcnc_tp_state_hash','_sim_linuxcnc_tp_canonical" _sim_module_link_flags "${_sim_module_link_flags}")
set_target_properties(sim_core_module PROPERTIES LINK_FLAGS "${_sim_module_link_flags}") set_target_properties(sim_core_module PROPERTIES LINK_FLAGS "${_sim_module_link_flags}")
endif() endif()

View File

@@ -260,6 +260,10 @@ int sim_kinematics_switch_select(sim_kinematics_switch *switcher, int mode,
double *joints, uint32_t joint_count, double *joints, uint32_t joint_count,
sim_posemath_pose *pose, sim_posemath_pose *pose,
sim_kinematics_result_info *info); sim_kinematics_result_info *info);
int sim_kinematics_switch_select_physical(sim_kinematics_switch *switcher, int mode,
const double *joints, uint32_t joint_count,
sim_posemath_pose *pose,
sim_kinematics_result_info *info);
int sim_kinematics_switch_forward(sim_kinematics_switch *switcher, int sim_kinematics_switch_forward(sim_kinematics_switch *switcher,
const double *joints, uint32_t joint_count, const double *joints, uint32_t joint_count,
sim_posemath_pose *pose, sim_posemath_pose *pose,
@@ -268,6 +272,10 @@ int sim_kinematics_switch_inverse(sim_kinematics_switch *switcher,
const sim_posemath_pose *pose, double *joints, const sim_posemath_pose *pose, double *joints,
uint32_t joint_count, uint32_t joint_count,
sim_kinematics_result_info *info); sim_kinematics_result_info *info);
int sim_kinematics_switch_inverse_mode(const sim_kinematics_switch *switcher, int mode,
const sim_posemath_pose *pose, double *joints,
uint32_t joint_count,
sim_kinematics_result_info *info);
int sim_kinematics_switch_active(const sim_kinematics_switch *switcher); int sim_kinematics_switch_active(const sim_kinematics_switch *switcher);
uint64_t sim_kinematics_switch_state_hash(const sim_kinematics_switch *switcher, uint64_t sim_kinematics_switch_state_hash(const sim_kinematics_switch *switcher,
const double *joints, uint32_t joint_count, const double *joints, uint32_t joint_count,

View File

@@ -0,0 +1,52 @@
#ifndef LINUXCNC_WEB_WASM_SIM_LINUXCNC_TP_H
#define LINUXCNC_WEB_WASM_SIM_LINUXCNC_TP_H
#include <stdint.h>
#include "sim_posemath.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef struct sim_linuxcnc_tp sim_linuxcnc_tp;
typedef struct sim_linuxcnc_tp_segment {
sim_posemath_segment segment;
uint32_t id;
int term_cond;
double progress;
double target;
} sim_linuxcnc_tp_segment;
uint32_t sim_linuxcnc_tp_size(void);
int32_t sim_linuxcnc_tp_init(sim_linuxcnc_tp *planner, double cycle_time,
double velocity_limit, double acceleration_limit);
int32_t sim_linuxcnc_tp_reset(sim_linuxcnc_tp *planner);
int32_t sim_linuxcnc_tp_set_profile(sim_linuxcnc_tp *planner, int profile, double tolerance);
int32_t sim_linuxcnc_tp_set_feed_override(sim_linuxcnc_tp *planner, double scale);
int32_t sim_linuxcnc_tp_set_dynamics(sim_linuxcnc_tp *planner, double acceleration_limit);
int32_t sim_linuxcnc_tp_set_paused(sim_linuxcnc_tp *planner, int paused);
int32_t sim_linuxcnc_tp_abort(sim_linuxcnc_tp *planner);
int32_t sim_linuxcnc_tp_set_position(sim_linuxcnc_tp *planner, const sim_posemath_pose *position);
int32_t sim_linuxcnc_tp_set_goal_position(sim_linuxcnc_tp *planner, const sim_posemath_pose *position);
int32_t sim_linuxcnc_tp_add_line(sim_linuxcnc_tp *planner, const sim_posemath_pose *end,
double velocity, double acceleration, uint32_t tag,
int canon_motion_type);
int32_t sim_linuxcnc_tp_add_circle(sim_linuxcnc_tp *planner, const sim_posemath_pose *end,
const sim_posemath_cartesian *center,
const sim_posemath_cartesian *normal, int turn,
double velocity, double acceleration, uint32_t tag,
int canon_motion_type);
int32_t sim_linuxcnc_tp_step(sim_linuxcnc_tp *planner);
int32_t sim_linuxcnc_tp_idle(sim_linuxcnc_tp *planner);
int32_t sim_linuxcnc_tp_read_state(const sim_linuxcnc_tp *planner, sim_tp_state *state);
uint32_t sim_linuxcnc_tp_queue_depth(const sim_linuxcnc_tp *planner);
int32_t sim_linuxcnc_tp_queue_segment(const sim_linuxcnc_tp *planner, uint32_t index, sim_linuxcnc_tp_segment *segment);
int32_t sim_linuxcnc_tp_restore_progress(sim_linuxcnc_tp *planner, const sim_posemath_pose *position, double velocity, uint32_t tick);
uint64_t sim_linuxcnc_tp_state_hash(const sim_linuxcnc_tp *planner);
uint32_t sim_linuxcnc_tp_canonical(const sim_linuxcnc_tp *planner, char *output, uint32_t capacity);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -0,0 +1,14 @@
#include <stdarg.h>
#include "rtapi.h"
/* LinuxCNC's TP diagnostics are intentionally silent in the browser. The
* source TP itself remains unchanged; only its realtime logging sink is
* replaced for deterministic WASM builds. */
void rtapi_print(const char *format, ...) {
(void)format;
}
void rtapi_print_msg(msg_level_t level, const char *format, ...) {
(void)level;
(void)format;
}

View File

@@ -0,0 +1,336 @@
#include "sim_linuxcnc_tp.h"
#include "motion.h"
#include "tp.h"
#include "emcpose.h"
#include "motion_types.h"
#include <math.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>
extern int tcGetPosReal(TC_STRUCT const *tc, int of_point, EmcPose *pos);
struct sim_linuxcnc_tp {
TP_STRUCT tp;
emcmot_status_t status;
emcmot_config_t config;
double axis_velocity[9];
double axis_acceleration[9];
uint32_t tick;
uint32_t last_active_id;
int just_completed;
int initialized;
};
static sim_linuxcnc_tp *active_planner;
static void noop_dio(int index, char value) { (void)index; (void)value; }
static void noop_aio(int index, double value) { (void)index; (void)value; }
static void noop_rotary_unlock(int index, int value) { (void)index; (void)value; }
static int noop_rotary_locked(int index) { (void)index; return 0; }
static double axis_velocity(int axis) { return active_planner && axis >= 0 && axis < 9 ? active_planner->axis_velocity[axis] : 0.0; }
static double axis_acceleration(int axis) { return active_planner && axis >= 0 && axis < 9 ? active_planner->axis_acceleration[axis] : 0.0; }
static double pose_delta(const EmcPose *start, const EmcPose *end, int axis) {
const double values_start[9] = {start->tran.x, start->tran.y, start->tran.z, start->a, start->b, start->c, start->u, start->v, start->w};
const double values_end[9] = {end->tran.x, end->tran.y, end->tran.z, end->a, end->b, end->c, end->u, end->v, end->w};
return values_end[axis] - values_start[axis];
}
static void xyzac_line_limits(const sim_linuxcnc_tp *planner, const EmcPose *start, const EmcPose *end, double *velocity, double *acceleration) {
double linear_length = 0.0, angular_length = 0.0, max_velocity_time = 0.0, max_acceleration_time = 0.0;
int has_linear = 0, has_angular = 0;
for (int axis = 0; axis < 9; ++axis) {
const double delta = fabs(pose_delta(start, end, axis));
const double velocity_limit = planner->axis_velocity[axis];
const double acceleration_limit = planner->axis_acceleration[axis];
if (delta <= 1e-12) continue;
if (axis < 3 || axis >= 6) { has_linear = 1; linear_length += delta * delta; }
else { has_angular = 1; angular_length += delta * delta; }
if (velocity_limit > 0.0) max_velocity_time = fmax(max_velocity_time, delta / velocity_limit);
if (acceleration_limit > 0.0) max_acceleration_time = fmax(max_acceleration_time, delta / acceleration_limit);
}
const double distance = has_linear ? sqrt(linear_length) : has_angular ? sqrt(angular_length) : 0.0;
if (!(distance > 0.0)) return;
if (max_velocity_time > 0.0) *velocity = fmin(*velocity, distance / max_velocity_time);
if (max_acceleration_time > 0.0) *acceleration = fmax(*acceleration, distance / max_acceleration_time);
}
static void xyzac_circle_limits(const sim_linuxcnc_tp *planner, const EmcPose *start, const EmcPose *end, const PmCartesian *center, const PmCartesian *normal, int turn, double *velocity, double *initial_velocity, double *acceleration) {
const double sx = start->tran.x - center->x, sy = start->tran.y - center->y, sz = start->tran.z - center->z;
const double ex = end->tran.x - center->x, ey = end->tran.y - center->y, ez = end->tran.z - center->z;
// ARC_FEED measures radius in the active plane. For the XYZAC case the
// normal is +Z, so the axial helix delta must not become a radial spiral
// term (the previous 3-D radius caused a small but accumulating TP error).
const double normal_abs_x = fabs(normal->x), normal_abs_y = fabs(normal->y), normal_abs_z = fabs(normal->z);
const double start_radius = normal_abs_z >= fmax(normal_abs_x, normal_abs_y) ? hypot(sx, sy) : normal_abs_y >= normal_abs_x ? hypot(sx, sz) : hypot(sy, sz);
const double end_radius = normal_abs_z >= fmax(normal_abs_x, normal_abs_y) ? hypot(ex, ey) : normal_abs_y >= normal_abs_x ? hypot(ex, ez) : hypot(ey, ez);
const double dot = sx * ex + sy * ey + sz * ez;
const double cross_x = sy * ez - sz * ey, cross_y = sz * ex - sx * ez, cross_z = sx * ey - sy * ex;
double angle = fabs(atan2(normal->x * cross_x + normal->y * cross_y + normal->z * cross_z, dot));
if (angle <= 1e-12) angle = 2.0 * M_PI;
angle += 2.0 * M_PI * (fabs((double)turn) - 1.0);
const double spiral = end_radius - start_radius;
const double circular_length = fmin(start_radius, end_radius) * fabs(angle);
const double spiral_length = hypot(circular_length, spiral);
const double effective_radius = sqrt((spiral / fmax(fabs(angle), 1e-12)) * (spiral / fmax(fabs(angle), 1e-12)) + fmin(start_radius, end_radius) * fmin(start_radius, end_radius));
const double planar_velocity = fmin(sqrt(planner->axis_acceleration[0] * sqrt(3.0) * 0.5 * effective_radius), fmin(planner->axis_velocity[0], fmin(planner->axis_velocity[1], planner->axis_velocity[2])));
double total_xyz_length = hypot(spiral_length, normal->x * (end->tran.x - start->tran.x) + normal->y * (end->tran.y - start->tran.y) + normal->z * (end->tran.z - start->tran.z));
double time_motion_velocity = 0.0, time_motion_acceleration = 0.0;
for (int axis = 0; axis < 9; ++axis) {
const double delta = fabs(pose_delta(start, end, axis));
if (planner->axis_velocity[axis] > 0.0) time_motion_velocity = fmax(time_motion_velocity, delta / planner->axis_velocity[axis]);
if (planner->axis_acceleration[axis] > 0.0) time_motion_acceleration = fmax(time_motion_acceleration, delta / planner->axis_acceleration[axis]);
}
const double time_spiral_velocity = planar_velocity > 0.0 ? spiral_length / planar_velocity : INFINITY;
const double time_spiral_acceleration = planner->axis_acceleration[0] > 0.0 ? spiral_length / planner->axis_acceleration[0] : INFINITY;
const double max_velocity = total_xyz_length / fmax(time_motion_velocity, time_spiral_velocity);
const double max_acceleration = total_xyz_length / fmax(time_motion_acceleration, time_spiral_acceleration);
*velocity = fmin(*velocity, max_velocity);
*initial_velocity = max_velocity;
*acceleration = fmax(*acceleration, max_acceleration);
}
static EmcPose to_emc_pose(const sim_posemath_pose *value) {
EmcPose result = {0};
if (value != NULL) {
result.tran.x = value->tran.x; result.tran.y = value->tran.y; result.tran.z = value->tran.z;
result.a = value->a; result.b = value->b; result.c = value->c;
result.u = value->u; result.v = value->v; result.w = value->w;
}
return result;
}
static PmCartesian to_pm_cart(const sim_posemath_cartesian *value) {
PmCartesian result = {0, 0, 0};
if (value != NULL) { result.x = value->x; result.y = value->y; result.z = value->z; }
return result;
}
static sim_posemath_pose from_emc_pose(const EmcPose *value) {
sim_posemath_pose result = {{0, 0, 0}, 0, 0, 0, 0, 0, 0};
if (value != NULL) {
result.tran.x = value->tran.x; result.tran.y = value->tran.y; result.tran.z = value->tran.z;
result.a = value->a; result.b = value->b; result.c = value->c;
result.u = value->u; result.v = value->v; result.w = value->w;
}
return result;
}
static uint64_t hash_bytes(const unsigned char *bytes, size_t length) {
uint64_t hash = UINT64_C(1469598103934665603);
for (size_t index = 0; index < length; ++index) { hash ^= bytes[index]; hash *= UINT64_C(1099511628211); }
return hash;
}
uint32_t sim_linuxcnc_tp_size(void) { return (uint32_t)sizeof(sim_linuxcnc_tp); }
int32_t sim_linuxcnc_tp_init(sim_linuxcnc_tp *planner, double cycle_time, double velocity_limit, double acceleration_limit) {
if (planner == NULL || !isfinite(cycle_time) || cycle_time <= 0.0 || cycle_time > 60.0 || !isfinite(velocity_limit) || velocity_limit <= 0.0 || !isfinite(acceleration_limit) || acceleration_limit <= 0.0) return -1200;
memset(planner, 0, sizeof(*planner));
planner->config.numJoints = 5; planner->config.numExtraJoints = 0; planner->config.numSpindles = 1;
planner->config.numDIO = 0; planner->config.numAIO = 0; planner->config.numMiscError = 0;
planner->config.trajCycleTime = cycle_time; planner->config.servoCycleTime = cycle_time;
planner->config.limitVel = velocity_limit; planner->config.maxFeedScale = 2.0;
planner->config.arcBlendEnable = 1; planner->config.arcBlendFallbackEnable = 0;
planner->config.arcBlendOptDepth = 50; planner->config.arcBlendGapCycles = 4;
planner->config.arcBlendRampFreq = 100.0; planner->config.arcBlendTangentKinkRatio = 0.1;
// Match xyzac-trt.ini joint limits: the trajectory cap is 35 mm/s, but
// XYZ joints are limited to 20 and rotary A/C joints to 30 deg/s. The
// upstream TP uses these per-axis callbacks when calculating tangent
// velocity and acceleration at a corner.
for (int axis = 0; axis < 9; ++axis) {
planner->axis_velocity[axis] = axis < 3 ? fmin(20.0, velocity_limit) : (axis == 3 || axis == 5) ? fmin(30.0, velocity_limit) : velocity_limit;
planner->axis_acceleration[axis] = acceleration_limit;
}
planner->status.feed_scale = 1.0; planner->status.rapid_scale = 1.0; planner->status.net_feed_scale = 1.0;
planner->status.enables_new = FS_ENABLED | FH_ENABLED; planner->status.enables_queued = planner->status.enables_new;
planner->status.motionFlag = EMCMOT_MOTION_ENABLE_BIT | EMCMOT_MOTION_COORD_BIT | EMCMOT_MOTION_INPOS_BIT;
active_planner = planner;
tpMotData(&planner->status, &planner->config);
tpMotFunctions(noop_dio, noop_aio, noop_rotary_unlock, noop_rotary_locked, axis_velocity, axis_acceleration);
if (tpCreate(&planner->tp, TP_DEFAULT_QUEUE_SIZE, 0) != TP_ERR_OK) return -1201;
if (tpSetCycleTime(&planner->tp, cycle_time) != TP_ERR_OK || tpSetVlimit(&planner->tp, velocity_limit) != TP_ERR_OK || tpSetAmax(&planner->tp, acceleration_limit) != TP_ERR_OK) return -1202;
planner->initialized = 1;
return 0;
}
int32_t sim_linuxcnc_tp_reset(sim_linuxcnc_tp *planner) {
if (planner == NULL || !planner->initialized) return -1203;
active_planner = planner;
planner->last_active_id = 0;
planner->just_completed = 0;
return tpClear(&planner->tp) == TP_ERR_OK ? 0 : -1204;
}
int32_t sim_linuxcnc_tp_set_profile(sim_linuxcnc_tp *planner, int profile, double tolerance) {
if (planner == NULL || !planner->initialized || profile < 0 || profile > 2 || !isfinite(tolerance) || tolerance < 0.0) return -1205;
active_planner = planner;
return tpSetTermCond(&planner->tp, profile, tolerance) == TP_ERR_OK ? 0 : -1206;
}
int32_t sim_linuxcnc_tp_set_feed_override(sim_linuxcnc_tp *planner, double scale) {
if (planner == NULL || !planner->initialized || !isfinite(scale) || scale < 0.0 || scale > 2.0) return -1207;
planner->status.net_feed_scale = scale; planner->status.feed_scale = scale; return 0;
}
int32_t sim_linuxcnc_tp_set_dynamics(sim_linuxcnc_tp *planner, double acceleration_limit) {
if (planner == NULL || !planner->initialized || !isfinite(acceleration_limit) || acceleration_limit <= 0.0) return -1217;
active_planner = planner;
for (int axis = 0; axis < 9; ++axis) planner->axis_acceleration[axis] = acceleration_limit;
return tpSetAmax(&planner->tp, acceleration_limit) == TP_ERR_OK ? 0 : -1218;
}
int32_t sim_linuxcnc_tp_set_paused(sim_linuxcnc_tp *planner, int paused) {
if (planner == NULL || !planner->initialized || (paused != 0 && paused != 1)) return -1219;
active_planner = planner;
if (paused) return tpPause(&planner->tp) == TP_ERR_OK ? 0 : -1220;
return tpResume(&planner->tp) == TP_ERR_OK ? 0 : -1221;
}
int32_t sim_linuxcnc_tp_abort(sim_linuxcnc_tp *planner) {
if (planner == NULL || !planner->initialized) return -1222;
active_planner = planner;
planner->last_active_id = 0;
planner->just_completed = 0;
return tpAbort(&planner->tp) == TP_ERR_OK ? 0 : -1223;
}
int32_t sim_linuxcnc_tp_set_position(sim_linuxcnc_tp *planner, const sim_posemath_pose *position) {
if (planner == NULL || !planner->initialized || position == NULL) return -1208;
active_planner = planner;
EmcPose pose = to_emc_pose(position);
return tpSetPos(&planner->tp, &pose) == TP_ERR_OK ? 0 : -1209;
}
int32_t sim_linuxcnc_tp_set_goal_position(sim_linuxcnc_tp *planner, const sim_posemath_pose *position) {
if (planner == NULL || !planner->initialized || position == NULL) return -1228;
planner->tp.goalPos = to_emc_pose(position);
return 0;
}
int32_t sim_linuxcnc_tp_add_line(sim_linuxcnc_tp *planner, const sim_posemath_pose *end, double velocity, double acceleration, uint32_t tag, int canon_motion_type) {
if (planner == NULL || !planner->initialized || end == NULL || !isfinite(velocity) || velocity < 0.0 || !isfinite(acceleration) || acceleration < 0.0) return -1210;
active_planner = planner;
struct state_tag_t state_tag = {0}; state_tag.fields_float[GM_FIELD_FLOAT_LINE_NUMBER] = (float)tag;
EmcPose pose = to_emc_pose(end), start = planner->tp.goalPos;
xyzac_line_limits(planner, &start, &pose, &velocity, &acceleration);
if (tpSetId(&planner->tp, (int)tag) != TP_ERR_OK) return -1211;
return tpAddLine(&planner->tp, pose, canon_motion_type, velocity, velocity, acceleration, planner->status.enables_new, 0, -1, state_tag) == TP_ERR_OK ? 0 : -1211;
}
int32_t sim_linuxcnc_tp_add_circle(sim_linuxcnc_tp *planner, const sim_posemath_pose *end, const sim_posemath_cartesian *center, const sim_posemath_cartesian *normal, int turn, double velocity, double acceleration, uint32_t tag, int canon_motion_type) {
if (planner == NULL || !planner->initialized || end == NULL || center == NULL || normal == NULL || !isfinite(velocity) || velocity < 0.0 || !isfinite(acceleration) || acceleration < 0.0) return -1212;
active_planner = planner;
struct state_tag_t state_tag = {0}; state_tag.fields_float[GM_FIELD_FLOAT_LINE_NUMBER] = (float)tag;
EmcPose pose = to_emc_pose(end), start = planner->tp.goalPos; PmCartesian pm_center = to_pm_cart(center); PmCartesian pm_normal = to_pm_cart(normal);
double initial_velocity = velocity;
xyzac_circle_limits(planner, &start, &pose, &pm_center, &pm_normal, turn, &velocity, &initial_velocity, &acceleration);
if (tpSetId(&planner->tp, (int)tag) != TP_ERR_OK) return -1212;
// emccanon computes ini_maxvel independently of the programmed feed for
// ARC_FEED. For this XYZAC case the planar XYZ limit (20 mm/s) is the
// governing bound; passing the feed here loses the native corner ramp.
return tpAddCircle(&planner->tp, pose, pm_center, pm_normal, turn, canon_motion_type, velocity, initial_velocity, acceleration, planner->status.enables_new, 0, state_tag) == TP_ERR_OK ? 0 : -1213;
}
int32_t sim_linuxcnc_tp_step(sim_linuxcnc_tp *planner) {
if (planner == NULL || !planner->initialized) return -1214;
active_planner = planner;
planner->just_completed = 0;
const int prior_id = tpGetExecId(&planner->tp);
const int prior_depth = tcqLen(&planner->tp.queue);
const int result = tpRunCycle(&planner->tp, (long)llround(planner->tp.cycleTime * 1e9));
planner->tick += 1u;
if (prior_id > 0 && prior_depth > 0 && tcqLen(&planner->tp.queue) == 0) {
planner->last_active_id = (uint32_t)prior_id;
planner->just_completed = 1;
}
return result >= TP_ERR_OK && result <= TP_ERR_WAITING ? 0 : result;
}
int32_t sim_linuxcnc_tp_idle(sim_linuxcnc_tp *planner) {
if (planner == NULL || !planner->initialized) return -1227;
planner->just_completed = 0;
planner->tick += 1u;
return 0;
}
int32_t sim_linuxcnc_tp_read_state(const sim_linuxcnc_tp *planner, sim_tp_state *state) {
if (planner == NULL || !planner->initialized || state == NULL) return -1215;
EmcPose position = {0};
if (tpGetPos(&planner->tp, &position) != TP_ERR_OK) return -1216;
memset(state, 0, sizeof(*state)); state->position = from_emc_pose(&position); state->velocity = planner->status.current_vel;
TC_STRUCT *active = tcqItem(&planner->tp.queue, 0);
if (active != NULL && active->target > 0.0) state->progress = active->progress / active->target;
// tpQueueDepth() exposes TP's legacy cached depth, which is only updated
// when appending a TC. Native emcmot reports the live tcq length after a
// segment pop, so use the upstream queue directly for oracle parity.
state->tick = planner->tick;
const uint32_t live_depth = (uint32_t)tcqLen(&planner->tp.queue);
state->queue_depth = live_depth > 0u ? live_depth : planner->just_completed ? 1u : 0u;
{
const int exec_id = tpGetExecId((TP_STRUCT *)&planner->tp);
state->active_id = state->queue_depth > 0 && exec_id > 0 ? (uint32_t)exec_id : planner->just_completed ? planner->last_active_id : 0u;
}
state->motion_type = tpGetMotionType((TP_STRUCT *)&planner->tp);
state->status = tpIsDone((TP_STRUCT *)&planner->tp) ? 0 : 1; return 0;
}
uint32_t sim_linuxcnc_tp_queue_depth(const sim_linuxcnc_tp *planner) {
return planner == NULL || !planner->initialized ? 0u : (uint32_t)tcqLen(&planner->tp.queue);
}
int32_t sim_linuxcnc_tp_queue_segment(const sim_linuxcnc_tp *planner, uint32_t index, sim_linuxcnc_tp_segment *segment) {
TC_STRUCT *tc;
EmcPose start = {0}, end = {0};
if (planner == NULL || !planner->initialized || segment == NULL) return -1224;
tc = tcqItem(&planner->tp.queue, (int)index);
if (tc == NULL || tcGetPosReal(tc, TC_GET_STARTPOINT, &start) != TP_ERR_OK || tcGetPosReal(tc, TC_GET_ENDPOINT, &end) != TP_ERR_OK) return -1225;
memset(segment, 0, sizeof(*segment));
segment->segment.start = from_emc_pose(&start);
segment->segment.end = from_emc_pose(&end);
segment->segment.velocity = tc->reqvel;
segment->segment.acceleration = tc->maxaccel;
segment->segment.motion_type = tc->motion_type;
if (tc->motion_type == TC_CIRCULAR) {
const double turns = fabs(tc->coords.circle.xyz.angle) / (2.0 * M_PI);
segment->segment.turn = (tc->coords.circle.xyz.angle < 0.0 ? -1 : 1) * (turns >= 1.0 ? (int)llround(turns) : 1);
}
if (tc->motion_type == TC_CIRCULAR) {
segment->segment.center.x = tc->coords.circle.xyz.center.x;
segment->segment.center.y = tc->coords.circle.xyz.center.y;
segment->segment.center.z = tc->coords.circle.xyz.center.z;
segment->segment.normal.x = tc->coords.circle.xyz.normal.x;
segment->segment.normal.y = tc->coords.circle.xyz.normal.y;
segment->segment.normal.z = tc->coords.circle.xyz.normal.z;
} else {
segment->segment.normal.z = 1.0;
}
segment->id = tc->id;
segment->term_cond = tc->term_cond;
segment->progress = tc->progress;
segment->target = tc->target;
return 0;
}
int32_t sim_linuxcnc_tp_restore_progress(sim_linuxcnc_tp *planner, const sim_posemath_pose *position, double velocity, uint32_t tick) {
if (planner == NULL || !planner->initialized || position == NULL || !isfinite(velocity) || velocity < 0.0) return -1226;
active_planner = planner;
planner->tp.currentPos = to_emc_pose(position);
planner->status.current_vel = velocity;
planner->tick = tick;
return 0;
}
uint64_t sim_linuxcnc_tp_state_hash(const sim_linuxcnc_tp *planner) {
sim_tp_state state; if (sim_linuxcnc_tp_read_state(planner, &state) != 0) return 0; return hash_bytes((const unsigned char *)&state, sizeof(state));
}
uint32_t sim_linuxcnc_tp_canonical(const sim_linuxcnc_tp *planner, char *output, uint32_t capacity) {
sim_tp_state state; int written;
if (sim_linuxcnc_tp_read_state(planner, &state) != 0) return 0;
written = snprintf(output, capacity, "{\"active_id\":%u,\"motion_type\":%d,\"position\":{\"a\":%.17g,\"c\":%.17g,\"x\":%.17g,\"y\":%.17g,\"z\":%.17g},\"queue_depth\":%u,\"schema\":\"linuxcnc-source-tp.v1\",\"tick\":%u,\"velocity\":%.17g}", state.active_id, state.motion_type, state.position.a, state.position.c, state.position.tran.x, state.position.tran.y, state.position.tran.z, state.queue_depth, state.tick, state.velocity);
return written < 0 ? 0 : (uint32_t)written + 1u;
}

View File

@@ -140,6 +140,16 @@ int sim_kinematics_switch_inverse(sim_kinematics_switch *switcher,
return result; return result;
} }
int sim_kinematics_switch_inverse_mode(const sim_kinematics_switch *switcher, int mode,
const sim_posemath_pose *pose, double *joints,
uint32_t joint_count,
sim_kinematics_result_info *info) {
if (switcher == 0 || pose == 0 || joints == 0) return SIM_KIN_ERR_ARGUMENT;
if (!valid_mode(mode)) return SIM_KIN_ERR_INVALID_MODE;
if (!switcher->registered[mode] || switcher->modules[mode] == 0) return SIM_KIN_ERR_MISSING_MODULE;
return sim_kinematics_inverse_ex(switcher->modules[mode], pose, joint_count, 0, joints, info);
}
int sim_kinematics_switch_select(sim_kinematics_switch *switcher, int mode, int sim_kinematics_switch_select(sim_kinematics_switch *switcher, int mode,
double *joints, uint32_t joint_count, double *joints, uint32_t joint_count,
sim_posemath_pose *pose, sim_posemath_pose *pose,
@@ -161,6 +171,27 @@ int sim_kinematics_switch_select(sim_kinematics_switch *switcher, int mode,
return result; return result;
} }
int sim_kinematics_switch_select_physical(sim_kinematics_switch *switcher, int mode,
const double *joints, uint32_t joint_count,
sim_posemath_pose *pose,
sim_kinematics_result_info *info) {
int result;
if (switcher == 0 || joints == 0 || pose == 0) return SIM_KIN_ERR_ARGUMENT;
if (!valid_mode(mode)) return SIM_KIN_ERR_INVALID_MODE;
if (switcher->switching) return SIM_KIN_ERR_SWITCH_INTERRUPTED;
if (!switcher->registered[mode] || switcher->modules[mode] == 0) return SIM_KIN_ERR_MISSING_MODULE;
switcher->active_mode = mode;
switcher->pending_mode = mode;
switcher->switching = 0;
switcher->generation += 1;
switcher->switch_count += 1;
result = sim_kinematics_switch_forward(switcher, joints, joint_count, pose, info);
if (result != SIM_KIN_OK) return result;
switcher->last_pose[mode] = *pose;
switcher->have_last_pose[mode] = 1;
return SIM_KIN_OK;
}
int sim_kinematics_switch_active(const sim_kinematics_switch *switcher) { int sim_kinematics_switch_active(const sim_kinematics_switch *switcher) {
return switcher == 0 ? SIM_KIN_ERR_ARGUMENT : switcher->active_mode; return switcher == 0 ? SIM_KIN_ERR_ARGUMENT : switcher->active_mode;
} }

View File

@@ -0,0 +1,39 @@
#include "sim_linuxcnc_tp.h"
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
static sim_posemath_pose pose(double x, double y, double z) {
sim_posemath_pose value = {{x, y, z}, 0, 0, 0, 0, 0, 0};
return value;
}
int main(void) {
sim_linuxcnc_tp *planner = calloc(1, sim_linuxcnc_tp_size());
sim_tp_state state = {0};
char canonical[512] = {0};
sim_posemath_pose origin = pose(0, 0, 0);
sim_posemath_pose first = pose(0, 0, 10);
sim_posemath_pose second = pose(20, 20, 10);
assert(planner != NULL);
assert(sim_linuxcnc_tp_init(planner, 0.001, 35.0, 300.0) == 0);
assert(sim_linuxcnc_tp_set_position(planner, &origin) == 0);
assert(sim_linuxcnc_tp_set_profile(planner, 0, 0.0) == 0);
assert(sim_linuxcnc_tp_add_line(planner, &first, 20.0, 300.0, 16, 1) == 0);
assert(sim_linuxcnc_tp_add_line(planner, &second, 35.0, 300.0, 18, 1) == 0);
for (int index = 0; index < 5000; ++index) {
int result = sim_linuxcnc_tp_step(planner);
if (result != 0) fprintf(stderr, "tp_step[%d]=%d\\n", index, result);
assert(result == 0);
assert(sim_linuxcnc_tp_read_state(planner, &state) == 0);
if (state.queue_depth == 0) break;
}
assert(state.queue_depth == 0);
assert(state.position.tran.x > 19.9 && state.position.tran.y > 19.9 && state.position.tran.z > 9.9);
assert(sim_linuxcnc_tp_state_hash(planner) != 0);
assert(sim_linuxcnc_tp_canonical(planner, canonical, sizeof(canonical)) > 1);
puts(canonical);
free(planner);
return 0;
}

View File

@@ -15,7 +15,7 @@ const handle = (event: MessageEvent) => {
}; };
self.onmessage = handle; self.onmessage = handle;
createModule().then((core) => { createModule().then((core) => {
session = new SimSession(core); session = new (SimSession as any)(core, { planner: "linuxcnc-tp" });
for (const event of queue.splice(0)) handle(event); for (const event of queue.splice(0)) handle(event);
}).catch((error) => { }).catch((error) => {
self.postMessage({ schema_version: 1, kind: "ERROR", context_id: "linuxcnc-2.10.0-pre0-wasm-m1-py-c1", session_id: "boot", seq: 0, payload: { status: "error", code: "worker_wasm_init", message_key: error instanceof Error ? error.message : String(error), retryable: false } }); self.postMessage({ schema_version: 1, kind: "ERROR", context_id: "linuxcnc-2.10.0-pre0-wasm-m1-py-c1", session_id: "boot", seq: 0, payload: { status: "error", code: "worker_wasm_init", message_key: error instanceof Error ? error.message : String(error), retryable: false } });

View File

@@ -22,6 +22,7 @@ import { ConfigurationBatchRuntime } from "../workspace/configuration-batch.mjs"
import { AuthoritativeRuntime } from "./authoritative-runtime.mjs"; import { AuthoritativeRuntime } from "./authoritative-runtime.mjs";
import { axisState } from "./axis-state.mjs"; import { axisState } from "./axis-state.mjs";
import { WasmMotionQueue } from "./wasm-motion-queue.mjs"; import { WasmMotionQueue } from "./wasm-motion-queue.mjs";
import { WasmLinuxcncTp } from "./wasm-linuxcnc-tp.mjs";
import { WasmHomeLimit } from "./wasm-home-limit.mjs"; import { WasmHomeLimit } from "./wasm-home-limit.mjs";
import { WasmXyzacKinematics, XYZAC_MODES } from "./wasm-xyzac-kinematics.mjs"; import { WasmXyzacKinematics, XYZAC_MODES } from "./wasm-xyzac-kinematics.mjs";
import { WasmXyzacIo } from "./wasm-xyzac-io.mjs"; import { WasmXyzacIo } from "./wasm-xyzac-io.mjs";
@@ -101,10 +102,11 @@ function canonicalPayload(value) {
} }
export class SimSession { export class SimSession {
constructor(core, { contextId = CONTEXT_ID, sessionId = "" } = {}) { constructor(core, { contextId = CONTEXT_ID, sessionId = "", planner = "queue" } = {}) {
this.core = core; this.core = core;
this.contextId = contextId; this.contextId = contextId;
this.sessionId = sessionId; this.sessionId = sessionId;
this.planner = planner;
this.tick = 0; this.tick = 0;
this.created = false; this.created = false;
this.timeline = new ReplayTimeline(); this.timeline = new ReplayTimeline();
@@ -121,7 +123,7 @@ export class SimSession {
this.python = new PythonBridge(); this.python = new PythonBridge();
this.program = new ProgramRunner(core); this.program = new ProgramRunner(core);
this.authoritative = new AuthoritativeRuntime(); this.authoritative = new AuthoritativeRuntime();
this.wasmMotion = new WasmMotionQueue(core); this.wasmMotion = planner === "linuxcnc-tp" ? new WasmLinuxcncTp(core) : new WasmMotionQueue(core);
this.wasmHomeLimit = new WasmHomeLimit(core); this.wasmHomeLimit = new WasmHomeLimit(core);
this.xyzacKinematics = null; this.xyzacKinematics = null;
this.xyzacIo = null; this.xyzacIo = null;
@@ -129,7 +131,28 @@ export class SimSession {
this.xyzacProgram = null; this.xyzacProgram = null;
this.xyzacProgramKinematicsMode = XYZAC_MODES.identity; this.xyzacProgramKinematicsMode = XYZAC_MODES.identity;
this.xyzacRapidVelocity = 35; this.xyzacRapidVelocity = 35;
this.xyzacMotionCycleTime = 0.01;
this.xyzacTaskCycleTicks = 1;
this.xyzacAxisVelocityLimits = {};
this.xyzacAxisAccelerationLimits = {};
this.pendingProgramControl = 0; this.pendingProgramControl = 0;
this.pendingTaskSync = 0;
this.pendingXyzacMotion = null;
this.pendingServoSync = 0;
this.xyzacExecutionWaitNextMotion = false;
this.xyzacExecutionWaitSamples = 0;
this.xyzacLastEnqueuedKinematicsMode = 0;
this.xyzacDeferredPhysicalSwitchMode = null;
this.xyzacDeferredBlockedActiveId = 0;
this.xyzacDeferredControlMotionType = 0;
this.xyzacQueuedLookaheadTargetId = 0;
this.xyzacAllowQueuedLookahead = false;
this.xyzacLastServoActiveId = 0;
this.xyzacLastQueuedMotion = null;
this.xyzacDeferredPhysicalPose = null;
this.xyzacDeferredPhysicalPoseApplied = false;
this.xyzacAppendDeferredOnActive = false;
this.xyzacDeferredCompletionActiveId = 0;
this.motionAuthority = "js"; this.motionAuthority = "js";
this.task = new SimTask(core, { contextId, sessionId: sessionId || null }); this.task = new SimTask(core, { contextId, sessionId: sessionId || null });
this.scheduler = new SimScheduler(core); this.scheduler = new SimScheduler(core);
@@ -149,13 +172,14 @@ export class SimSession {
// WASM pose samples into every 10 ms step response; the frontend retains // WASM pose samples into every 10 ms step response; the frontend retains
// the canonical path from PROGRAM_LOAD/PROGRAM_RESTART. // the canonical path from PROGRAM_LOAD/PROGRAM_RESTART.
const lightweightXyzacStep = input.kind === "SIM_STEP" && Boolean(this.xyzacProgram); const lightweightXyzacStep = input.kind === "SIM_STEP" && Boolean(this.xyzacProgram);
const currentMotion = this.wasmMotion.read(); const traceOnlyStep = lightweightXyzacStep && input.payload?.trace_only === true;
const currentMotion = traceOnlyStep && statePayload.wasm_motion ? statePayload.wasm_motion : this.wasmMotion.read();
const completedMotion = program.status === 4 && currentMotion.state?.queue_depth === 0; const completedMotion = program.status === 4 && currentMotion.state?.queue_depth === 0;
// Compact payloads are only valid for successful motion samples. Error // Compact payloads are only valid for successful motion samples. Error
// responses must retain their code/reason instead of being rewritten into // responses must retain their code/reason instead of being rewritten into
// a delta with undefined fields. // a delta with undefined fields.
const motionDelta = kind === "COMMAND_RESULT" && lightweightXyzacStep && input.payload?.response === "motion_delta" && !completedMotion; const motionDelta = kind === "COMMAND_RESULT" && lightweightXyzacStep && input.payload?.response === "motion_delta" && !completedMotion;
const panelSnapshot = motionDelta ? null : this.panelSnapshot({ task, program, input, tick, motionTrace: statePayload.motion_trace, includeToolpath: !lightweightXyzacStep, includeConfiguration: !lightweightXyzacStep, includeStableRuntime: !lightweightXyzacStep }); const panelSnapshot = motionDelta || traceOnlyStep ? null : this.panelSnapshot({ task, program, input, tick, motionTrace: statePayload.motion_trace, includeToolpath: !lightweightXyzacStep, includeConfiguration: !lightweightXyzacStep, includeStableRuntime: !lightweightXyzacStep });
const compactProgram = motionDelta ? { const compactProgram = motionDelta ? {
schema: program.schema, schema: program.schema,
source: program.source, source: program.source,
@@ -208,7 +232,7 @@ export class SimSession {
motion_authority: responsePayload.motion_authority ?? this.motionAuthority, motion_authority: responsePayload.motion_authority ?? this.motionAuthority,
task_authority: responsePayload.task_authority ?? this.task.authority(), task_authority: responsePayload.task_authority ?? this.task.authority(),
...(!lightweightXyzacStep ? { wasm_home_limit: statePayload.wasm_home_limit ?? this.wasmHomeLimit.read() } : {}), ...(!lightweightXyzacStep ? { wasm_home_limit: statePayload.wasm_home_limit ?? this.wasmHomeLimit.read() } : {}),
axis_state: axisState({ task, program, motion: this.motionProjection(), devices: {}, io: this.xyzacIo?.canonical?.() ?? {}, device_io: this.xyzacDeviceIo?.canonical?.() ?? this.deviceIo.export(), action: input.kind, payload: input.payload ?? {} }), ...(traceOnlyStep ? {} : { axis_state: axisState({ task, program, motion: this.motionProjection(), devices: {}, io: this.xyzacIo?.canonical?.() ?? {}, device_io: this.xyzacDeviceIo?.canonical?.() ?? this.deviceIo.export(), action: input.kind, payload: input.payload ?? {} }) }),
}, },
}; };
} }
@@ -220,10 +244,11 @@ export class SimSession {
const io = this.xyzacIo?.canonical?.(); const io = this.xyzacIo?.canonical?.();
const ioState = io && typeof io === "object" && io.status !== "blocked" ? io : null; const ioState = io && typeof io === "object" && io.status !== "blocked" ? io : null;
const kinematics = this.xyzacKinematics?.snapshot?.(); const kinematics = this.xyzacKinematics?.snapshot?.();
const kinematicsJoints = kinematics?.joints?.length === 5 ? [...kinematics.joints] : null; const plannerJoints = wasmMotion.status === "verified" && Array.isArray(wasmMotion.state.joint_commanded) && wasmMotion.state.joint_commanded.length === 5 ? [...wasmMotion.state.joint_commanded] : null;
const kinematicsJoints = plannerJoints ?? (kinematics?.joints?.length === 5 ? [...kinematics.joints] : null);
const unsigned = { const unsigned = {
schema: "sim_motion.v1", schema: "sim_motion.v1",
source: "wasm_motion_queue", source: wasmMotion.state.source ?? "wasm_motion_queue",
tick: wasmMotion.status === "verified" ? wasmMotion.state.tick : this.tick, tick: wasmMotion.status === "verified" ? wasmMotion.state.tick : this.tick,
axes: { x: position.x, y: position.y, z: position.z }, axes: { x: position.x, y: position.y, z: position.z },
joints: kinematicsJoints ?? (home.status === "verified" && Array.isArray(home.state.joints) ? [...home.state.joints] : [position.x, position.y, position.z, position.a ?? 0, position.c ?? 0]), joints: kinematicsJoints ?? (home.status === "verified" && Array.isArray(home.state.joints) ? [...home.state.joints] : [position.x, position.y, position.z, position.a ?? 0, position.c ?? 0]),
@@ -269,7 +294,7 @@ export class SimSession {
const path = `${basePath.replace(/\/demos$/, "")}/${suffix}`; const path = `${basePath.replace(/\/demos$/, "")}/${suffix}`;
const source = this.workspaceFiles[path]; const source = this.workspaceFiles[path];
if (typeof source !== "string") return { status: "error", code: "program_subroutine_missing", path }; if (typeof source !== "string") return { status: "error", code: "program_subroutine_missing", path };
const added = this.xyzacProgram.addSubroutine(name, source); const added = this.xyzacProgram.addSubroutine(name, source, path);
if (added.status !== "verified") return added; if (added.status !== "verified") return added;
} }
return { status: "verified", count: rows.length }; return { status: "verified", count: rows.length };
@@ -803,7 +828,10 @@ export class SimSession {
let replayHomeLimit = null; let replayHomeLimit = null;
try { try {
if (replayMotionAuthority === "wasm") { if (replayMotionAuthority === "wasm") {
replayWasmMotion = new WasmMotionQueue(this.core); replayWasmMotion = new WasmMotionQueue(this.core, {
cycleTime: Number(checkpoint.state.wasm_motion.cycle_time ?? this.xyzacMotionCycleTime),
velocityLimit: Number(checkpoint.state.wasm_motion.velocity_limit ?? 1e9),
});
replayWasmMotion.restoreState(checkpoint.state.wasm_motion); replayWasmMotion.restoreState(checkpoint.state.wasm_motion);
if (checkpoint.state.wasm_home_limit) { if (checkpoint.state.wasm_home_limit) {
replayHomeLimit = new WasmHomeLimit(this.core); replayHomeLimit = new WasmHomeLimit(this.core);
@@ -1154,6 +1182,23 @@ export class SimSession {
// interpreter command and trajectory, then leave the taskfile loaded // interpreter command and trajectory, then leave the taskfile loaded
// for a later AUTO run after ESTOP_RESET and POWER ON. // for a later AUTO run after ESTOP_RESET and POWER ON.
this.xyzacProgram.abort(); this.xyzacProgram.abort();
this.pendingXyzacMotion = null;
this.pendingProgramControl = 0;
this.pendingTaskSync = 0;
this.pendingServoSync = 0;
this.xyzacExecutionWaitNextMotion = false;
this.xyzacExecutionWaitSamples = 0;
this.xyzacLastEnqueuedKinematicsMode = 0;
this.xyzacDeferredPhysicalSwitchMode = null;
this.xyzacDeferredBlockedActiveId = 0;
this.xyzacDeferredControlMotionType = 0;
this.xyzacQueuedLookaheadTargetId = 0;
this.xyzacAllowQueuedLookahead = false;
this.xyzacLastServoActiveId = 0;
this.xyzacLastQueuedMotion = null;
this.xyzacDeferredPhysicalPose = null;
this.xyzacDeferredPhysicalPoseApplied = false;
this.xyzacAppendDeferredOnActive = false;
this.wasmMotion.abort(); this.wasmMotion.abort();
this.wasmMotion.setPaused(true); this.wasmMotion.setPaused(true);
const resetProgram = this.resetXyzacProgramToLoaded(); const resetProgram = this.resetXyzacProgramToLoaded();
@@ -1240,6 +1285,23 @@ export class SimSession {
/* LinuxCNC emcTaskSetState(OFF) aborts motion, clears the /* LinuxCNC emcTaskSetState(OFF) aborts motion, clears the
interpreter command and leaves the loaded taskfile intact. */ interpreter command and leaves the loaded taskfile intact. */
this.xyzacProgram.abort(); this.xyzacProgram.abort();
this.pendingXyzacMotion = null;
this.pendingProgramControl = 0;
this.pendingTaskSync = 0;
this.pendingServoSync = 0;
this.xyzacExecutionWaitNextMotion = false;
this.xyzacExecutionWaitSamples = 0;
this.xyzacLastEnqueuedKinematicsMode = 0;
this.xyzacDeferredPhysicalSwitchMode = null;
this.xyzacDeferredBlockedActiveId = 0;
this.xyzacDeferredControlMotionType = 0;
this.xyzacQueuedLookaheadTargetId = 0;
this.xyzacAllowQueuedLookahead = false;
this.xyzacLastServoActiveId = 0;
this.xyzacLastQueuedMotion = null;
this.xyzacDeferredPhysicalPose = null;
this.xyzacDeferredPhysicalPoseApplied = false;
this.xyzacAppendDeferredOnActive = false;
this.wasmMotion.abort(); this.wasmMotion.abort();
this.wasmMotion.setPaused(true); this.wasmMotion.setPaused(true);
const resetProgram = this.resetXyzacProgramToLoaded(); const resetProgram = this.resetXyzacProgramToLoaded();
@@ -1348,9 +1410,24 @@ export class SimSession {
this.xyzacDeviceIo?.destroy(); this.xyzacDeviceIo?.destroy();
this.xyzacDeviceIo = new WasmXyzacDeviceIo(this.core, { encoderCount: 2 }); this.xyzacDeviceIo = new WasmXyzacDeviceIo(this.core, { encoderCount: 2 });
this.xyzacProgram?.destroy(); this.xyzacProgram?.destroy();
this.wasmMotion.destroy();
this.xyzacMotionCycleTime = Number(configuration.kinematics?.servo_cycle_time ?? configuration.kinematics?.cycle_time ?? 0.01);
if (!Number.isFinite(this.xyzacMotionCycleTime) || this.xyzacMotionCycleTime <= 0) throw new Error("wasm_xyzac_cycle_time_invalid");
const taskCycleTime = Number(configuration.kinematics?.task_cycle_time ?? this.xyzacMotionCycleTime);
this.xyzacTaskCycleTicks = Math.max(1, Math.round(taskCycleTime / this.xyzacMotionCycleTime));
this.wasmMotion = this.planner === "linuxcnc-tp"
? new WasmLinuxcncTp(this.core, { cycleTime: this.xyzacMotionCycleTime, velocityLimit: this.xyzacRapidVelocity, accelerationLimit: Number(configuration.kinematics?.max_acceleration ?? 300) })
: new WasmMotionQueue(this.core, { cycleTime: this.xyzacMotionCycleTime, velocityLimit: 1e9 });
this.xyzacProgram = new WasmProgramRuntime(this.core); this.xyzacProgram = new WasmProgramRuntime(this.core);
this.xyzacProgramKinematicsMode = XYZAC_MODES.identity; this.xyzacProgramKinematicsMode = XYZAC_MODES.identity;
if (typeof this.wasmMotion.setKinematics === "function") this.wasmMotion.setKinematics(this.xyzacKinematics);
this.xyzacRapidVelocity = Number(configuration.kinematics?.max_linear_velocity ?? 35); this.xyzacRapidVelocity = Number(configuration.kinematics?.max_linear_velocity ?? 35);
const linearLimit = Number(configuration.kinematics?.axis_linear_velocity_limit ?? 20);
const angularLimit = Number(configuration.kinematics?.axis_angular_velocity_limit ?? 30);
this.xyzacAxisVelocityLimits = { x: linearLimit, y: linearLimit, z: linearLimit, a: angularLimit, c: angularLimit };
const accelerationLimit = Number(configuration.kinematics?.max_acceleration ?? 300);
this.xyzacAxisAccelerationLimits = { x: accelerationLimit, y: accelerationLimit, z: accelerationLimit, a: accelerationLimit, c: accelerationLimit };
this.wasmMotion.setDynamics(Number(configuration.kinematics?.max_acceleration ?? 300), Number(configuration.kinematics?.max_jerk ?? 1000000000));
this.xyzacProgram.setRapidVelocity(this.xyzacRapidVelocity); this.xyzacProgram.setRapidVelocity(this.xyzacRapidVelocity);
} }
if (configuration?.home_limits && this.motionAuthority === "wasm") { if (configuration?.home_limits && this.motionAuthority === "wasm") {
@@ -1414,54 +1491,152 @@ export class SimSession {
if (limit.status !== "accepted") return finish(this.envelope("ERROR", input, { ...limit, operation_id: input.operation_id ?? null, simulation_tick: this.tick, retryable: false }), "failed"); if (limit.status !== "accepted") return finish(this.envelope("ERROR", input, { ...limit, operation_id: input.operation_id ?? null, simulation_tick: this.tick, retryable: false }), "failed");
let program_steps = []; let program_steps = [];
const motion_trace = []; const motion_trace = [];
let sourceLookaheadReady = false;
try { try {
this.scheduler.step(cycles, { this.scheduler.step(cycles, {
task: () => { task: (nextTick) => {
if (this.xyzacProgram) { if (this.xyzacProgram) {
const wasmState = this.xyzacProgram.snapshot(); const wasmState = this.xyzacProgram.snapshot();
if (![2, 3].includes(wasmState.status)) return; if (![2, 3].includes(wasmState.status)) return;
if (this.pendingTaskSync !== 0) {
this.pendingTaskSync = 0;
return;
}
// LinuxCNC's task/interpreter blocks on a full trajectory // LinuxCNC's task/interpreter blocks on a full trajectory
// planner queue. Let the motion phase consume one segment // planner queue. Let the motion phase consume one segment
// before reading another source line instead of converting // before reading another source line instead of converting
// normal planner backpressure into a program fault. // normal planner backpressure into a program fault.
if (this.wasmMotion.read().state.queue_depth >= this.wasmMotion.queueCapacity) return; if (this.wasmMotion.read().state.queue_depth >= this.wasmMotion.queueCapacity) return;
if (this.pendingXyzacMotion) {
const pendingMode = Number(this.pendingXyzacMotion.kinematics_mode ?? 0);
if (this.xyzacAppendDeferredOnActive) return;
const queueState = this.wasmMotion.read().state;
const deferredControlReady = this.xyzacDeferredControlMotionType === 1
? Number(queueState.active_id ?? 0) !== this.xyzacDeferredBlockedActiveId
: Number(queueState.active_id ?? 0) === this.xyzacDeferredBlockedActiveId;
const deferredSwitchMotion = this.xyzacDeferredPhysicalSwitchMode !== null && pendingMode === this.xyzacDeferredPhysicalSwitchMode && this.xyzacQueuedLookaheadTargetId === 0 && deferredControlReady;
const queuedLookaheadReady = deferredSwitchMotion || (this.xyzacAllowQueuedLookahead && this.xyzacLastServoActiveId === this.xyzacQueuedLookaheadTargetId);
if (queueState.queue_depth !== 0 && !queuedLookaheadReady) return;
if (queuedLookaheadReady && !deferredSwitchMotion && this.xyzacAllowQueuedLookahead) {
// The next segment is appended after the first servo
// sample of the now-active target, matching LinuxCNC's
// TP queue observation (q=1 on entry, q=2 thereafter).
sourceLookaheadReady = true;
return;
}
const pending = this.pendingXyzacMotion;
this.pendingXyzacMotion = null;
const enqueue = this.wasmMotion.enqueue(pending);
if (enqueue.status === "error") throw new Error(enqueue.code);
const modeChanged = pendingMode !== this.xyzacLastEnqueuedKinematicsMode;
this.xyzacLastEnqueuedKinematicsMode = pendingMode;
this.xyzacLastQueuedMotion = pending;
if (this.planner === "linuxcnc-tp" && modeChanged && queueState.queue_depth !== 0) {
this.xyzacAllowQueuedLookahead = true;
this.xyzacQueuedLookaheadTargetId = Number(pending.source_line ?? 0);
}
if (queuedLookaheadReady && !modeChanged) {
this.xyzacAllowQueuedLookahead = false;
this.xyzacQueuedLookaheadTargetId = 0;
}
if (this.xyzacExecutionWaitNextMotion && pendingMode !== XYZAC_MODES.identity) {
this.xyzacExecutionWaitNextMotion = false;
this.xyzacExecutionWaitSamples = 1;
} else if (pendingMode === XYZAC_MODES.identity) this.xyzacExecutionWaitNextMotion = false;
if (this.planner !== "linuxcnc-tp" || !modeChanged || queueState.queue_depth !== 0) return;
// Source TP performs one servo cycle on the newly active
// segment before task lookahead appends the next XYZAC
// segment. Keep parsing in this task, but defer that queue
// mutation until the first servo sample below.
sourceLookaheadReady = true;
}
// LinuxCNC's task planner consumes non-motion interpreter
// blocks in the same task cycle and stops when the first
// canonical motion is handed to TP. Preserve that boundary
// instead of charging comments, parameter assignments, M429,
// and G10 blocks as artificial motion ticks.
for (let parserStep = 0; parserStep < 64; parserStep += 1) {
if (this.pendingProgramControl !== 0) { if (this.pendingProgramControl !== 0) {
if (this.wasmMotion.read().state.queue_depth !== 0) return; const pendingControl = this.pendingProgramControl;
const current = this.wasmMotion.read().state.position; const queueState = this.wasmMotion.read().state;
this.xyzacProgramKinematicsMode = { 428: XYZAC_MODES.xyzac, 429: XYZAC_MODES.identity, 430: XYZAC_MODES.userk }[this.pendingProgramControl]; const deferIdentitySwitch = pendingControl === 429 && this.planner === "linuxcnc-tp" && (Number(queueState.motion_type ?? 0) === 3 || (Number(queueState.motion_type ?? 0) === 1 && Number(queueState.progress ?? 0) >= 0.999999));
const selected = this.xyzacKinematics.select(this.xyzacProgramKinematicsMode, [current.x, current.y, current.z, current.a ?? 0, current.c ?? 0]); if (queueState.queue_depth !== 0 && !deferIdentitySwitch) return;
if (selected.status !== "verified") throw new Error(`wasm_switchkins_control_${this.pendingProgramControl}`); if (queueState.queue_depth !== 0 && deferIdentitySwitch) {
this.pendingProgramControl = 0; this.xyzacProgramKinematicsMode = XYZAC_MODES.identity;
return; this.xyzacDeferredPhysicalSwitchMode = XYZAC_MODES.identity;
} this.xyzacDeferredBlockedActiveId = Number(queueState.active_id ?? 0);
const step = this.xyzacProgram.step(); this.xyzacDeferredControlMotionType = Number(queueState.motion_type ?? 0);
if (step.status !== "verified") throw new Error(step.code); const queuedEnd = this.xyzacLastQueuedMotion?.end ?? queueState.position;
// Path control is produced by the WASM interpreter and applied const endInverse = this.xyzacKinematics.inverseMode(XYZAC_MODES.xyzac, queuedEnd);
// to the WASM trajectory queue before the block's motion is if (endInverse.status !== "verified") throw new Error(`wasm_switchkins_deferred_inverse_${endInverse.code ?? "error"}`);
// enqueued. G61/G61.1 conservatively use the queue's exact const physical = this.xyzacKinematics.selectPhysical(XYZAC_MODES.identity, endInverse.joints);
// profile; G64 restores the continuous lookahead profile. if (physical.status !== "verified") throw new Error(`wasm_switchkins_deferred_${physical.code ?? "error"}`);
const motionControlMode = Number(step.program?.motion_control_mode ?? 0); if (typeof this.wasmMotion.setGoalPosition === "function" && physical.pose) this.wasmMotion.setGoalPosition(physical.pose);
const queueProfile = motionControlMode === 0 ? 0 : 1; this.xyzacDeferredPhysicalPose = physical.pose;
if (this.wasmMotion.controls.profile !== queueProfile) this.wasmMotion.setProfile(queueProfile); this.xyzacDeferredPhysicalPoseApplied = false;
if (step.has_control && [428, 429, 430].includes(step.control_code)) { this.xyzacDeferredCompletionActiveId = Number(this.xyzacLastQueuedMotion?.source_line ?? 0);
this.pendingProgramControl = step.control_code; // Preparing the future identity goal must not switch
if (this.wasmMotion.read().state.queue_depth === 0) { // the active kinematics while the helix is running.
const current = this.wasmMotion.read().state.position; const restored = this.xyzacKinematics.selectPhysical(XYZAC_MODES.xyzac, endInverse.joints);
this.xyzacProgramKinematicsMode = { 428: XYZAC_MODES.xyzac, 429: XYZAC_MODES.identity, 430: XYZAC_MODES.userk }[step.control_code]; if (restored.status !== "verified") throw new Error(`wasm_switchkins_deferred_restore_${restored.code ?? "error"}`);
const selected = this.xyzacKinematics.select(this.xyzacProgramKinematicsMode, [current.x, current.y, current.z, current.a ?? 0, current.c ?? 0]); this.pendingProgramControl = 0;
if (selected.status !== "verified") throw new Error(`wasm_switchkins_control_${step.control_code}`); this.pendingTaskSync = 0;
continue;
}
const currentState = this.wasmMotion.read().state;
const current = currentState.position;
this.xyzacProgramKinematicsMode = { 428: XYZAC_MODES.xyzac, 429: XYZAC_MODES.identity, 430: XYZAC_MODES.userk }[pendingControl];
const joints = Array.isArray(currentState.joint_commanded) && currentState.joint_commanded.length === 5
? currentState.joint_commanded
: [current.x, current.y, current.z, current.a ?? 0, current.c ?? 0];
const selected = typeof this.xyzacKinematics.selectPhysical === "function"
? this.xyzacKinematics.selectPhysical(this.xyzacProgramKinematicsMode, joints)
: this.xyzacKinematics.select(this.xyzacProgramKinematicsMode, joints);
if (selected.status !== "verified") throw new Error(`wasm_switchkins_control_${this.pendingProgramControl}`);
if (this.planner === "linuxcnc-tp" && selected.pose) {
this.wasmMotion.setPosition(selected.pose);
if (typeof this.wasmMotion.setActiveKinematicsTag === "function") this.wasmMotion.setActiveKinematicsTag(this.xyzacProgramKinematicsMode);
}
this.pendingProgramControl = 0; this.pendingProgramControl = 0;
if (this.planner === "linuxcnc-tp" && pendingControl === 428) this.xyzacExecutionWaitNextMotion = true;
} }
} const step = this.xyzacProgram.step();
const steps = step.motion ? [step] : []; if (step.status !== "verified") throw new Error(step.code);
program_steps.push(step); // Path control is produced by the WASM interpreter and
for (const item of steps) { // applied to the WASM trajectory queue before enqueue.
item.motion.kinematics_mode = this.xyzacProgramKinematicsMode; const motionControlMode = Number(step.program?.motion_control_mode ?? 0);
const linearNoop = item.motion.motion_type === 1 && ["x", "y", "z", "a", "b", "c", "u", "v", "w"].every((axis) => item.motion.start[axis] === item.motion.end[axis]); if (typeof this.wasmMotion.setTermCondition === "function") {
if (!linearNoop) { const mode = motionControlMode === 0 ? "continuous" : motionControlMode === 1 ? "exact_path" : "exact_stop";
const enqueue = this.wasmMotion.enqueue(item.motion); this.wasmMotion.setTermCondition(mode, Number(step.program?.path_tolerance ?? 0));
if (enqueue.status === "error") throw new Error(enqueue.code); } else {
const queueProfile = motionControlMode === 0 ? 0 : 1;
if (this.wasmMotion.controls.profile !== queueProfile) this.wasmMotion.setProfile(queueProfile);
} }
program_steps.push(step);
if (step.has_control && [428, 429, 430].includes(step.control_code)) {
this.pendingProgramControl = step.control_code;
if (this.planner === "linuxcnc-tp") this.pendingTaskSync = 1;
}
if (step.motion) {
step.motion.kinematics_mode = this.xyzacProgramKinematicsMode;
step.motion.source_line = Number(step.program?.execution_source_line ?? step.line ?? step.motion.source_line ?? 0);
step.motion.source_file = step.program?.execution_source_file ?? this.xyzacProgram.path;
const linearNoop = step.motion.motion_type === 1 && ["x", "y", "z", "a", "b", "c", "u", "v", "w"].every((axis) => step.motion.start[axis] === step.motion.end[axis]);
if (!linearNoop) {
// Commands issued by task become visible to motion on
// the following task cycle, matching LinuxCNC's NML
// handoff boundary and preventing a zero-time jump in
// current_line/exec_state snapshots.
const directDeferredMotion = this.planner === "linuxcnc-tp" && this.xyzacDeferredPhysicalSwitchMode !== null && this.xyzacDeferredControlMotionType === 1 && Number(step.motion.kinematics_mode ?? 0) === XYZAC_MODES.identity;
this.pendingXyzacMotion = step.motion;
if (directDeferredMotion) {
this.xyzacAppendDeferredOnActive = true;
if (typeof this.wasmMotion.setQueueDepthOverride === "function") this.wasmMotion.setQueueDepthOverride(2);
}
}
return;
}
if (step.program?.status === 4) return;
} }
return; return;
} }
@@ -1496,17 +1671,72 @@ export class SimSession {
this.task.setTick(nextTick); this.task.setTick(nextTick);
const xyzacAuthority = Boolean(this.xyzacProgram); const xyzacAuthority = Boolean(this.xyzacProgram);
const motionState = xyzacAuthority ? null : this.motion.step(nextTick); const motionState = xyzacAuthority ? null : this.motion.step(nextTick);
const wasmMotion = this.wasmMotion.step(); // The selected LinuxCNC case has a 1 ms servo period and a
if (wasmMotion.status === "error" && wasmMotion.code !== "wasm_motion_step_-605") throw new Error(wasmMotion.code); // 10 ms task period. A public SIM_STEP represents one task
// cycle, so advance the WASM motion queue by the configured
// servo/task ratio while publishing one stable task snapshot.
let wasmMotion = this.wasmMotion.read();
for (let servo = 0; servo < this.xyzacTaskCycleTicks; servo += 1) {
if (this.planner === "linuxcnc-tp" && typeof this.wasmMotion.setExecutionState === "function") {
this.wasmMotion.setExecutionState(this.xyzacExecutionWaitSamples > 0 ? 2 : 3);
}
const queueBeforeServo = Number(typeof this.wasmMotion.queueDepth === "function" ? this.wasmMotion.queueDepth() : this.wasmMotion.read().state.queue_depth ?? 0);
const beforeServoState = this.wasmMotion.read().state;
let servoResult;
servoResult = this.wasmMotion.step();
if (servoResult.status === "error" && servoResult.code !== "wasm_motion_step_-605") throw new Error(servoResult.code);
if (servoResult.status === "verified") {
wasmMotion = servoResult;
this.xyzacLastServoActiveId = Number(servoResult.state.active_id ?? 0);
const queuedEndpoint = this.xyzacLastQueuedMotion?.end;
const endpointNear = queuedEndpoint && Math.abs(Number(beforeServoState.position?.x ?? 0) - Number(queuedEndpoint.x ?? 0)) < 1e-3 && Math.abs(Number(beforeServoState.position?.y ?? 0) - Number(queuedEndpoint.y ?? 0)) < 1e-3 && Math.abs(Number(beforeServoState.position?.z ?? 0) - Number(queuedEndpoint.z ?? 0)) < 1e-3;
if (this.xyzacAppendDeferredOnActive && this.pendingXyzacMotion && Number(beforeServoState.active_id ?? 0) === this.xyzacDeferredCompletionActiveId && Number(beforeServoState.motion_type ?? 0) === 3 && (Number(beforeServoState.progress ?? 0) >= 0.999999 || (Number(beforeServoState.velocity ?? 0) <= 0.1 && endpointNear))) {
if (!this.xyzacDeferredPhysicalPoseApplied && this.xyzacDeferredPhysicalPose) {
this.wasmMotion.setPosition(this.xyzacDeferredPhysicalPose);
this.xyzacDeferredPhysicalPoseApplied = true;
}
const deferredMotion = this.pendingXyzacMotion;
const deferredEnqueue = this.wasmMotion.enqueue(deferredMotion);
if (deferredEnqueue.status === "error") throw new Error(deferredEnqueue.code);
this.xyzacLastEnqueuedKinematicsMode = Number(deferredMotion.kinematics_mode ?? 0);
this.xyzacLastQueuedMotion = deferredMotion;
this.xyzacAllowQueuedLookahead = true;
this.xyzacQueuedLookaheadTargetId = Number(deferredMotion.source_line ?? 0);
this.pendingXyzacMotion = null;
this.xyzacAppendDeferredOnActive = false;
if (typeof this.wasmMotion.setQueueDepthOverride === "function") this.wasmMotion.setQueueDepthOverride(null);
servoResult.state.queue_depth = 2;
if (Array.isArray(this.wasmMotion.history) && this.wasmMotion.history.length) this.wasmMotion.history[this.wasmMotion.history.length - 1].queue_depth = 2;
}
if (this.planner === "linuxcnc-tp" && this.xyzacExecutionWaitSamples > 0 && Number(servoResult.state.active_id ?? 0) !== 0) this.xyzacExecutionWaitSamples -= 1;
if (this.planner === "linuxcnc-tp" && this.xyzacDeferredPhysicalSwitchMode !== null && Number(servoResult.state.active_tag ?? -1) === this.xyzacDeferredPhysicalSwitchMode) {
const physicalJoints = Array.isArray(servoResult.state.joint_commanded) ? servoResult.state.joint_commanded : [];
const physical = this.xyzacKinematics.selectPhysical(this.xyzacDeferredPhysicalSwitchMode, physicalJoints);
if (physical.status !== "verified") throw new Error(`wasm_switchkins_deferred_${physical.code ?? "error"}`);
this.xyzacDeferredPhysicalSwitchMode = null;
this.xyzacDeferredPhysicalPose = null;
this.xyzacDeferredPhysicalPoseApplied = false;
}
if (this.planner === "linuxcnc-tp" && sourceLookaheadReady && servo === 0 && this.pendingXyzacMotion) {
const lookaheadMode = Number(this.pendingXyzacMotion.kinematics_mode ?? 0);
const lookahead = this.wasmMotion.enqueue(this.pendingXyzacMotion);
if (lookahead.status === "error") throw new Error(lookahead.code);
this.xyzacLastEnqueuedKinematicsMode = lookaheadMode;
this.xyzacLastQueuedMotion = this.pendingXyzacMotion;
this.pendingXyzacMotion = null;
this.xyzacAllowQueuedLookahead = false;
this.xyzacQueuedLookaheadTargetId = 0;
sourceLookaheadReady = false;
wasmMotion = this.wasmMotion.read();
}
}
}
if (motionState?.fault && this.task.state.state === "RUNNING") this.task.fault(-1205); if (motionState?.fault && this.task.state.state === "RUNNING") this.task.fault(-1205);
if (this.motionAuthority === "wasm") { if (this.motionAuthority === "wasm") {
if (wasmMotion.status === "verified") { if (wasmMotion.status === "verified") {
if (!xyzacAuthority) { if (!xyzacAuthority) {
const position = this.motion.applyWasmPosition(wasmMotion.state.position, wasmMotion.state.velocity, nextTick); const position = this.motion.applyWasmPosition(wasmMotion.state.position, wasmMotion.state.velocity, nextTick);
if (position.status === "error") throw new Error(position.code); if (position.status === "error") throw new Error(position.code);
} else if (this.xyzacKinematics && wasmMotion.state.active_id !== 0 && this.xyzacProgramKinematicsMode !== XYZAC_MODES.identity) {
const inverse = this.xyzacKinematics.inverse(wasmMotion.state.position);
if (inverse.status !== "verified") throw new Error(`wasm_xyzac_motion_inverse_${inverse.code ?? "error"}`);
} }
} }
if (xyzacAuthority) { if (xyzacAuthority) {
@@ -1561,12 +1791,12 @@ export class SimSession {
.filter((entry) => Number.isSafeInteger(entry.line) && entry.line > 0); .filter((entry) => Number.isSafeInteger(entry.line) && entry.line > 0);
if (program) { if (program) {
const status = this.xyzacProgram ? (program.status === 4 ? "completed" : program.status === 2 ? "running" : program.status === 3 ? "paused" : "loaded") : (program.program_status === "completed" ? "completed" : program.program_status === "running" ? "running" : program.program_status); const status = this.xyzacProgram ? (program.status === 4 ? "completed" : program.status === 2 ? "running" : program.status === 3 ? "paused" : "loaded") : (program.program_status === "completed" ? "completed" : program.program_status === "running" ? "running" : program.program_status);
this.task.setProgram(status, this.xyzacProgram ? program.current_line : program.line); this.task.setProgram(status, this.xyzacProgram ? (program.execution_source_line ?? program.current_line) : program.line);
if (status === "completed" && this.task.state.state === "RUNNING" && (this.motionAuthority !== "wasm" || this.wasmMotion.read().state.queue_depth === 0)) this.task.transition("STOPPED"); if (status === "completed" && this.task.state.state === "RUNNING" && (this.motionAuthority !== "wasm" || this.wasmMotion.read().state.queue_depth === 0)) this.task.transition("STOPPED");
} }
const snapshotState = { tick: this.tick, cycles, motion_authority: this.motionAuthority, task: this.task.export(), scheduler: this.scheduler.export(), authoritative: this.xyzacProgram ? { schema: "wasm_authority_boundary.v1", source: "linuxcnc-wasm-worker" } : this.authoritative.export(), wasm_motion: this.wasmMotion.read(), motion: this.motionProjection(), plc: this.plc.export(), hal_runtime: this.xyzacIo ? this.xyzacIo.halExport() : this.hal.export(), device_io: this.xyzacDeviceIo ? this.xyzacDeviceIo.export() : this.xyzacIo ? this.xyzacIo.export() : this.deviceIo.export(), configuration_batch: this.configurationBatch.export(), io_ack: this.ioAck.export(), callbacks: this.callbacks.export() }; const snapshotState = { tick: this.tick, cycles, motion_authority: this.motionAuthority, task: this.task.export(), scheduler: this.scheduler.export(), authoritative: this.xyzacProgram ? { schema: "wasm_authority_boundary.v1", source: "linuxcnc-wasm-worker" } : this.authoritative.export(), wasm_motion: this.wasmMotion.read(), motion: this.motionProjection(), plc: this.plc.export(), hal_runtime: this.xyzacIo ? this.xyzacIo.halExport() : this.hal.export(), device_io: this.xyzacDeviceIo ? this.xyzacDeviceIo.export() : this.xyzacIo ? this.xyzacIo.export() : this.deviceIo.export(), configuration_batch: this.configurationBatch.export(), io_ack: this.ioAck.export(), callbacks: this.callbacks.export(), xyzac_timing: { servo_cycle_time: this.xyzacMotionCycleTime, task_cycle_ticks: this.xyzacTaskCycleTicks } };
if (program) snapshotState.program = program; if (program) snapshotState.program = program;
const snapshot = this.timeline.checkpoint(input.seq, this.tick, snapshotState, this.timeline.snapshots.latest()?.snapshot_id ?? null); const snapshot = input.payload?.trace_only === true ? null : this.timeline.checkpoint(input.seq, this.tick, snapshotState, this.timeline.snapshots.latest()?.snapshot_id ?? null);
const lightweightXyzac = Boolean(this.xyzacProgram); const lightweightXyzac = Boolean(this.xyzacProgram);
return finish(this.envelope("COMMAND_RESULT", input, { return finish(this.envelope("COMMAND_RESULT", input, {
status: "verified", cycles, program_steps, execution_trace, status: "verified", cycles, program_steps, execution_trace,
@@ -1574,14 +1804,31 @@ export class SimSession {
motion_authority: this.motionAuthority, motion_authority: this.motionAuthority,
task: this.task.export(), scheduler: this.scheduler.export(), authoritative: snapshotState.authoritative, task: this.task.export(), scheduler: this.scheduler.export(), authoritative: snapshotState.authoritative,
wasm_motion: this.wasmMotion.read(), motion: snapshotState.motion, wasm_motion: this.wasmMotion.read(), motion: snapshotState.motion,
...(lightweightXyzac ? {} : { plc: this.plc.export(), hal_runtime: snapshotState.hal_runtime, device_io: snapshotState.device_io, configuration_batch: this.configurationBatch.export(), io_ack: this.ioAck.export(), callbacks: this.callbacks.export(), snapshot }), ...(lightweightXyzac ? {} : { plc: this.plc.export(), hal_runtime: snapshotState.hal_runtime, device_io: snapshotState.device_io, configuration_batch: this.configurationBatch.export(), io_ack: this.ioAck.export(), callbacks: this.callbacks.export(), ...(snapshot ? { snapshot } : {}) }),
...(program ? { program } : {}), state_hash: snapshot.state_hash, ...(program ? { program } : {}), state_hash: snapshot?.state_hash ?? fnv1a64(snapshotState),
})); }));
} }
case "PROGRAM_LOAD": { case "PROGRAM_LOAD": {
if (["RUNNING", "PAUSED"].includes(this.task.state.state)) return finish(this.error(input, "task_state", "program_load_while_active"), "failed"); if (["RUNNING", "PAUSED"].includes(this.task.state.state)) return finish(this.error(input, "task_state", "program_load_while_active"), "failed");
const source = input.payload?.source ?? input.payload?.text ?? input.payload?.program_source; const source = input.payload?.source ?? input.payload?.text ?? input.payload?.program_source;
if (this.xyzacProgram) { if (this.xyzacProgram) {
this.pendingXyzacMotion = null;
this.pendingProgramControl = 0;
this.pendingTaskSync = 0;
this.pendingServoSync = 0;
this.xyzacExecutionWaitNextMotion = false;
this.xyzacExecutionWaitSamples = 0;
this.xyzacLastEnqueuedKinematicsMode = 0;
this.xyzacDeferredPhysicalSwitchMode = null;
this.xyzacDeferredBlockedActiveId = 0;
this.xyzacDeferredControlMotionType = 0;
this.xyzacQueuedLookaheadTargetId = 0;
this.xyzacAllowQueuedLookahead = false;
this.xyzacLastServoActiveId = 0;
this.xyzacLastQueuedMotion = null;
this.xyzacDeferredPhysicalPose = null;
this.xyzacDeferredPhysicalPoseApplied = false;
this.xyzacAppendDeferredOnActive = false;
const result = this.xyzacProgram.load(source, input.payload?.path ?? null); const result = this.xyzacProgram.load(source, input.payload?.path ?? null);
if (result.status === "verified") this.xyzacProgram.setRapidVelocity(this.xyzacRapidVelocity); if (result.status === "verified") this.xyzacProgram.setRapidVelocity(this.xyzacRapidVelocity);
if (result.status === "verified") { const subroutines = this.loadXyzacSubroutines(input.payload?.path); if (subroutines.status !== "verified") return finish(this.error(input, "program_subroutine", subroutines.code), "failed"); } if (result.status === "verified") { const subroutines = this.loadXyzacSubroutines(input.payload?.path); if (subroutines.status !== "verified") return finish(this.error(input, "program_subroutine", subroutines.code), "failed"); }
@@ -1614,7 +1861,7 @@ export class SimSession {
case "PROGRAM_RUN": { case "PROGRAM_RUN": {
if (this.xyzacProgram) { if (this.xyzacProgram) {
const result = this.xyzacProgram.start(); const result = this.xyzacProgram.start();
if (result.status === "verified") { this.wasmMotion.setPaused(false); this.task.setMode("AUTO"); if (this.task.state.state === "STOPPED") this.task.recover(); this.task.transition("RUNNING"); this.task.setProgram("running", this.xyzacProgram.snapshot().current_line); } if (result.status === "verified") { this.wasmMotion.setPaused(false); this.task.setMode("AUTO"); if (this.task.state.state === "STOPPED") this.task.recover(); this.task.transition("RUNNING"); const program = this.xyzacProgram.snapshot(); this.task.setProgram("running", program.execution_source_line ?? program.current_line); }
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, program: this.xyzacProgram.snapshot(), task: this.task.export(), motion_authority: "wasm", command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed"); return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, program: this.xyzacProgram.snapshot(), task: this.task.export(), motion_authority: "wasm", command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
} }
const result = this.program.start(); const result = this.program.start();
@@ -1638,11 +1885,28 @@ export class SimSession {
case "PROGRAM_PAUSE": case "PROGRAM_RESUME": case "PROGRAM_ABORT": { case "PROGRAM_PAUSE": case "PROGRAM_RESUME": case "PROGRAM_ABORT": {
const action = input.kind === "PROGRAM_PAUSE" ? "pause" : input.kind === "PROGRAM_RESUME" ? "resume" : "abort"; const action = input.kind === "PROGRAM_PAUSE" ? "pause" : input.kind === "PROGRAM_RESUME" ? "resume" : "abort";
if (this.xyzacProgram) { if (this.xyzacProgram) {
this.pendingXyzacMotion = null;
this.pendingProgramControl = 0;
this.pendingTaskSync = 0;
this.pendingServoSync = 0;
this.xyzacExecutionWaitNextMotion = false;
this.xyzacExecutionWaitSamples = 0;
this.xyzacLastEnqueuedKinematicsMode = 0;
this.xyzacDeferredPhysicalSwitchMode = null;
this.xyzacDeferredBlockedActiveId = 0;
this.xyzacDeferredControlMotionType = 0;
this.xyzacQueuedLookaheadTargetId = 0;
this.xyzacAllowQueuedLookahead = false;
this.xyzacLastServoActiveId = 0;
this.xyzacLastQueuedMotion = null;
this.xyzacDeferredPhysicalPose = null;
this.xyzacDeferredPhysicalPoseApplied = false;
this.xyzacAppendDeferredOnActive = false;
const result = this.xyzacProgram[action](); const result = this.xyzacProgram[action]();
if (result.status === "verified") { if (result.status === "verified") {
if (action === "pause") { call(this.core, "sim_protocol_pause"); this.wasmMotion.setPaused(true); this.task.transition("PAUSED"); this.task.setProgram("paused", this.xyzacProgram.snapshot().current_line); } if (action === "pause") { call(this.core, "sim_protocol_pause"); this.wasmMotion.setPaused(true); this.task.transition("PAUSED"); const program = this.xyzacProgram.snapshot(); this.task.setProgram("paused", program.execution_source_line ?? program.current_line); }
else if (action === "resume") { call(this.core, "sim_protocol_resume"); this.wasmMotion.setPaused(false); this.task.transition("RUNNING"); this.task.setProgram("running", this.xyzacProgram.snapshot().current_line); } else if (action === "resume") { call(this.core, "sim_protocol_resume"); this.wasmMotion.setPaused(false); this.task.transition("RUNNING"); const program = this.xyzacProgram.snapshot(); this.task.setProgram("running", program.execution_source_line ?? program.current_line); }
else { this.wasmMotion.abort(); this.task.transition("STOPPED"); this.task.setProgram("aborted", this.xyzacProgram.snapshot().current_line); } else { this.wasmMotion.abort(); this.task.transition("STOPPED"); const program = this.xyzacProgram.snapshot(); this.task.setProgram("aborted", program.execution_source_line ?? program.current_line); }
} }
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, program: this.xyzacProgram.snapshot(), task: this.task.export(), motion_authority: "wasm", command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed"); return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, program: this.xyzacProgram.snapshot(), task: this.task.export(), motion_authority: "wasm", command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
} }
@@ -1673,6 +1937,23 @@ export class SimSession {
} }
case "PROGRAM_RESTART": { case "PROGRAM_RESTART": {
if (this.xyzacProgram) { if (this.xyzacProgram) {
this.pendingXyzacMotion = null;
this.pendingProgramControl = 0;
this.pendingTaskSync = 0;
this.pendingServoSync = 0;
this.xyzacExecutionWaitNextMotion = false;
this.xyzacExecutionWaitSamples = 0;
this.xyzacLastEnqueuedKinematicsMode = 0;
this.xyzacDeferredPhysicalSwitchMode = null;
this.xyzacDeferredBlockedActiveId = 0;
this.xyzacDeferredControlMotionType = 0;
this.xyzacQueuedLookaheadTargetId = 0;
this.xyzacAllowQueuedLookahead = false;
this.xyzacLastServoActiveId = 0;
this.xyzacLastQueuedMotion = null;
this.xyzacDeferredPhysicalPose = null;
this.xyzacDeferredPhysicalPoseApplied = false;
this.xyzacAppendDeferredOnActive = false;
const source = this.xyzacProgram.source; const source = this.xyzacProgram.source;
const result = this.xyzacProgram.load(source, this.xyzacProgram.path); const result = this.xyzacProgram.load(source, this.xyzacProgram.path);
if (result.status === "verified") this.xyzacProgram.setRapidVelocity(this.xyzacRapidVelocity); if (result.status === "verified") this.xyzacProgram.setRapidVelocity(this.xyzacRapidVelocity);

View File

@@ -2,7 +2,7 @@ import createModule from "../../build-wasm/linuxcnc-sim-core.js";
import { XyzacSession } from "./xyzac-session.mjs"; import { XyzacSession } from "./xyzac-session.mjs";
const core = await createModule(); const core = await createModule();
const session = new XyzacSession(core); const session = new XyzacSession(core, { planner: "linuxcnc-tp" });
self.onmessage = (event) => { self.onmessage = (event) => {
try { try {

View File

@@ -0,0 +1,254 @@
import { fnv1a64 } from "./replay.mjs";
const POSE_BYTES = 72;
const STATE_BYTES = 112;
const SOURCE_TP_SCHEMA = "linuxcnc_source_tp_state.v1";
function call(core, name, returnType, argTypes, args) {
const fn = core?.[`_${name}`] ?? core?.[name];
if (typeof fn === "function") return fn(...args);
if (typeof core?.ccall === "function") return core.ccall(name, returnType, argTypes, args);
throw new Error(`missing core export: ${name}`);
}
function poseArray(value = {}) { return ["x", "y", "z", "a", "b", "c", "u", "v", "w"].map((axis) => Number(value[axis] ?? 0)); }
function toPose(values) { return Object.fromEntries(["x", "y", "z", "a", "b", "c", "u", "v", "w"].map((axis, index) => [axis, Number(values[index] ?? 0)])); }
function cubicState() { return { x0: 0, x1: 0, x2: 0, x3: 0, t: 0, need: true, coeff: [0, 0, 0, 0] }; }
function cubicAdd(state, point, segmentTime = 0.001) {
if (!state.need) return;
state.x0 = state.x1; state.x1 = state.x2; state.x2 = state.x3; state.x3 = Number(point);
const wp0 = (state.x0 + 4 * state.x1 + state.x2) / 6;
const wp1 = (state.x1 + 4 * state.x2 + state.x3) / 6;
const vel0 = (state.x2 - state.x0) / (2 * segmentTime);
const vel1 = (state.x3 - state.x1) / (2 * segmentTime);
const b = 3 * (wp1 - wp0) / (segmentTime * segmentTime) - (2 * vel0 + vel1) / segmentTime;
const a = (vel1 - vel0) / (3 * segmentTime * segmentTime) - 2 * b / (3 * segmentTime);
state.coeff = [a, b, vel0, wp0]; state.t = 0; state.need = false;
}
function cubicInterpolate(state, segmentTime = 0.001) {
if (state.need) cubicAdd(state, state.x3, segmentTime);
const [a, b, c, d] = state.coeff;
const t = state.t;
const value = ((a * t + b) * t + c) * t + d;
state.t += segmentTime;
if (Math.abs(segmentTime - state.t) < segmentTime * 0.5) state.need = true;
return value;
}
export class WasmLinuxcncTp {
constructor(core, { cycleTime = 0.001, velocityLimit = 35, accelerationLimit = 300 } = {}) {
this.core = core;
this.available = ["sim_linuxcnc_tp_size", "sim_linuxcnc_tp_init", "sim_linuxcnc_tp_read_state", "sim_linuxcnc_tp_step", "sim_linuxcnc_tp_idle", "sim_linuxcnc_tp_add_line", "sim_linuxcnc_tp_set_profile", "sim_linuxcnc_tp_set_feed_override", "sim_linuxcnc_tp_set_dynamics", "sim_linuxcnc_tp_set_paused", "sim_linuxcnc_tp_abort", "sim_linuxcnc_tp_set_goal_position", "sim_linuxcnc_tp_queue_depth", "sim_linuxcnc_tp_queue_segment", "sim_linuxcnc_tp_restore_progress"].every((name) => typeof core?.[`_${name}`] === "function" || typeof core?.[name] === "function");
this.cycleTime = cycleTime;
this.velocityLimit = velocityLimit;
this.accelerationLimit = accelerationLimit;
this.queueCapacity = 2000;
this.planner = 0;
this.posePointer = 0;
this.centerPointer = 0;
this.normalPointer = 0;
this.statePointer = 0;
this.history = [];
this.activeTag = 0;
this.segmentTags = new Map();
this.lastActiveId = 0;
// Task execution state is owned by the LinuxCNC task/TP handoff, not by
// the TP C struct. Keep it alongside the source-TP state so every servo
// sample carries the same WAITING_FOR_MOTION boundary as native LinuxCNC.
this.executionState = 3;
this.lastState = null;
this.queueDepthOverride = null;
this.kinematics = null;
this.cubic = Array.from({ length: 5 }, () => cubicState());
this.previousJointCommanded = [0, 0, 0, 0, 0];
this.controls = { profile: 0, lookahead: 50, acceleration_limit: accelerationLimit, jerk_limit: 1e9, feed_override: 1, paused: false };
if (!this.available || typeof core?._malloc !== "function") return;
this.planner = core._malloc(call(core, "sim_linuxcnc_tp_size", "number", [], []));
this.posePointer = core._malloc(POSE_BYTES);
this.centerPointer = core._malloc(24);
this.normalPointer = core._malloc(24);
this.statePointer = core._malloc(STATE_BYTES);
if (![this.planner, this.posePointer, this.centerPointer, this.normalPointer, this.statePointer].every(Boolean)) throw new Error("linuxcnc_tp_allocation_failed");
const result = call(core, "sim_linuxcnc_tp_init", "number", ["number", "number", "number", "number"], [this.planner, cycleTime, velocityLimit, accelerationLimit]);
if (result !== 0) throw new Error(`linuxcnc_tp_init_${result}`);
this.setPosition({ x: 0, y: 0, z: 0, a: 0, b: 0, c: 0, u: 0, v: 0, w: 0 });
this.setProfile(0);
}
setPosition(value) {
if (!this.available) return this.read();
this.core.HEAPF64.set(poseArray(value), this.posePointer / 8);
const result = call(this.core, "sim_linuxcnc_tp_set_position", "number", ["number", "number"], [this.planner, this.posePointer]);
if (result !== 0) throw new Error(`linuxcnc_tp_set_position_${result}`);
return this.read();
}
setGoalPosition(value) {
if (!this.available) return this.read();
this.core.HEAPF64.set(poseArray(value), this.posePointer / 8);
const result = call(this.core, "sim_linuxcnc_tp_set_goal_position", "number", ["number", "number"], [this.planner, this.posePointer]);
if (result !== 0) throw new Error(`linuxcnc_tp_set_goal_position_${result}`);
return this.read();
}
setProfile(profile, tolerance = 0) {
const condition = profile === 0 ? 2 : profile === 1 ? 0 : profile === 2 ? 1 : -1;
if (![0, 1, 2].includes(profile) || !Number.isFinite(Number(tolerance)) || Number(tolerance) < 0) throw new Error("linuxcnc_tp_profile_invalid");
if (this.available) {
const result = call(this.core, "sim_linuxcnc_tp_set_profile", "number", ["number", "number", "number"], [this.planner, condition, Number(tolerance)]);
if (result !== 0) throw new Error(`linuxcnc_tp_profile_${result}`);
}
this.controls.profile = profile;
return this.read();
}
setTermCondition(mode, tolerance = 0) {
const profile = mode === "continuous" || mode === 0 ? 0 : mode === "exact_path" || mode === 1 ? 2 : 1;
return this.setProfile(profile, tolerance);
}
setLookahead(value) { if (!Number.isSafeInteger(value) || value < 1) throw new Error("linuxcnc_tp_lookahead_invalid"); this.controls.lookahead = value; return this.read(); }
setDynamics(accelerationLimit, jerkLimit) { this.accelerationLimit = Number(accelerationLimit); this.controls.acceleration_limit = this.accelerationLimit; this.controls.jerk_limit = Number(jerkLimit); if (this.available) call(this.core, "sim_linuxcnc_tp_set_dynamics", "number", ["number", "number"], [this.planner, this.accelerationLimit]); return this.read(); }
setFeedOverride(value) { this.controls.feed_override = Number(value); if (this.available) call(this.core, "sim_linuxcnc_tp_set_feed_override", "number", ["number", "number"], [this.planner, this.controls.feed_override]); return this.read(); }
setPaused(value) { this.controls.paused = Boolean(value); if (this.available) call(this.core, "sim_linuxcnc_tp_set_paused", "number", ["number", "number"], [this.planner, this.controls.paused ? 1 : 0]); return this.read(); }
enqueue(segment) {
if (!this.available || !segment) return { status: "skipped", code: "linuxcnc_tp_unavailable" };
this.core.HEAPF64.set(poseArray(segment.end), this.posePointer / 8);
const canonMotionType = Number(segment.canon_motion_type ?? (segment.motion_type === 2 ? 3 : segment.kind === "rapid" ? 1 : 2));
this.activeTag = Number(segment.kinematics_mode ?? segment.tag ?? 0);
const segmentTag = this.activeTag;
const segmentId = Number(segment.source_line ?? 0);
if (segmentId > 0) this.segmentTags.set(segmentId, segmentTag);
const acceleration = Math.max(Number(segment.acceleration ?? 0), this.accelerationLimit);
const result = segment.motion_type === 2
? (this.core.HEAPF64.set([Number(segment.center?.x ?? 0), Number(segment.center?.y ?? 0), Number(segment.center?.z ?? 0)], this.centerPointer / 8), this.core.HEAPF64.set([Number(segment.normal?.x ?? 0), Number(segment.normal?.y ?? 0), Number(segment.normal?.z ?? 0)], this.normalPointer / 8), call(this.core, "sim_linuxcnc_tp_add_circle", "number", ["number", "number", "number", "number", "number", "number", "number", "number", "number"], [this.planner, this.posePointer, this.centerPointer, this.normalPointer, Number(segment.turn ?? 1), Number(segment.velocity ?? 0), acceleration, Number(segment.source_line ?? 0), canonMotionType]))
: call(this.core, "sim_linuxcnc_tp_add_line", "number", ["number", "number", "number", "number", "number", "number"], [this.planner, this.posePointer, Number(segment.velocity ?? 0), acceleration, Number(segment.source_line ?? 0), canonMotionType]);
if (result !== 0) return { status: "error", code: `linuxcnc_tp_enqueue_${result}` };
if (this.lastState?.active_id && Number(this.lastState.active_id) !== segmentId) this.activeTag = Number(this.lastState.active_tag ?? this.activeTag);
return { status: "verified", source_line: segment.source_line ?? null };
}
setKinematics(kinematics) { this.kinematics = kinematics ?? null; return this.read(); }
setActiveKinematicsTag(mode) {
this.activeTag = Number(mode) === 0 ? 0 : Number(mode);
return this.read();
}
setExecutionState(value) {
const state = Number(value);
if (![2, 3, 4].includes(state)) throw new Error("linuxcnc_tp_execution_state_invalid");
this.executionState = state;
// The next source TP step publishes the state. Avoid a redundant C read
// on every servo tick merely to attach this task/TP handoff marker.
return { status: "verified", state: this.lastState };
}
queueDepth() { return Number(this.lastState?.queue_depth ?? 0); }
setQueueDepthOverride(value) {
this.queueDepthOverride = value === null ? null : Math.max(0, Number(value));
if (this.lastState && this.queueDepthOverride !== null) this.lastState.queue_depth = this.queueDepthOverride;
return this.read();
}
commandedJoints(position, activeTag) {
if (Number(activeTag) === 0) return [position.x, position.y, position.z, position.a ?? 0, position.c ?? 0];
const inverse = this.kinematics?.inverseMode?.(Number(activeTag), position) ?? this.kinematics?.inverse?.(position);
if (inverse?.status === "verified" && Array.isArray(inverse.joints)) return inverse.joints.slice(0, 5);
return this.previousJointCommanded.slice();
}
step() {
if (!this.available) return { status: "blocked", code: "linuxcnc_tp_unavailable" };
if (this.controls.paused) return { status: "error", code: "linuxcnc_tp_paused" };
const result = call(this.core, "sim_linuxcnc_tp_step", "number", ["number"], [this.planner]);
if (result !== 0) return { status: "error", code: `linuxcnc_tp_step_${result}` };
const state = this.read();
if (state.status === "verified") {
const targetJoints = this.commandedJoints(state.state.position, state.state.active_tag);
targetJoints.forEach((point, index) => cubicAdd(this.cubic[index], point, this.cycleTime));
const commanded = this.cubic.map((axis) => cubicInterpolate(axis, this.cycleTime));
state.state.joint_commanded = commanded;
state.state.joint_actual = this.previousJointCommanded.slice();
this.previousJointCommanded = commanded.slice();
}
if (state.status === "verified") this.history.push(structuredClone(state.state));
return state;
}
idle() {
if (!this.available) return this.read();
const result = call(this.core, "sim_linuxcnc_tp_idle", "number", ["number"], [this.planner]);
if (result !== 0) return { status: "error", code: `linuxcnc_tp_idle_${result}` };
const state = this.read();
if (state.status === "verified") {
const targetJoints = this.commandedJoints(state.state.position, state.state.active_tag);
targetJoints.forEach((point, index) => cubicAdd(this.cubic[index], point, this.cycleTime));
const commanded = this.cubic.map((axis) => cubicInterpolate(axis, this.cycleTime));
state.state.joint_commanded = commanded;
state.state.joint_actual = this.previousJointCommanded.slice();
this.previousJointCommanded = commanded.slice();
}
if (state.status === "verified") this.history.push(structuredClone(state.state));
return state;
}
drainTrace() { const trace = this.history; this.history = []; return trace; }
read() {
if (!this.available) return { status: "blocked", state: { position: toPose([]), velocity: 0, progress: 0, tick: 0, queue_depth: 0, active_id: 0, active_tag: 0, motion_type: 0, status: -1214, exec_state: this.executionState, source: "linuxcnc-source-tp" } };
const result = call(this.core, "sim_linuxcnc_tp_read_state", "number", ["number", "number"], [this.planner, this.statePointer]);
if (result !== 0) return { status: "error", code: `linuxcnc_tp_read_${result}` };
const doubles = this.core.HEAPF64.subarray(this.statePointer / 8, this.statePointer / 8 + 11);
const ints = this.core.HEAPU32.subarray(this.statePointer / 4 + 22, this.statePointer / 4 + 25);
const activeId = ints[2];
if (activeId && activeId !== this.lastActiveId && this.segmentTags.has(activeId)) this.activeTag = this.segmentTags.get(activeId);
this.lastActiveId = activeId;
const state = { position: toPose(doubles), velocity: doubles[9], acceleration: this.accelerationLimit, progress: doubles[10], tick: ints[0], queue_depth: this.queueDepthOverride === null ? ints[1] : this.queueDepthOverride, active_queue: activeId ? 1 : 0, active_id: activeId, active_tag: this.activeTag, joint_commanded: this.previousJointCommanded.slice(), joint_actual: this.previousJointCommanded.slice(), motion_type: this.core.HEAP32[this.statePointer / 4 + 25], status: this.core.HEAP32[this.statePointer / 4 + 26], exec_state: this.executionState, source: "linuxcnc-source-tp" };
this.lastState = state;
return { status: "verified", state };
}
reset() { if (this.available) call(this.core, "sim_linuxcnc_tp_reset", "number", ["number"], [this.planner]); this.controls.paused = false; this.history = []; this.activeTag = 0; this.segmentTags.clear(); this.lastActiveId = 0; this.queueDepthOverride = null; this.executionState = 3; this.cubic = Array.from({ length: 5 }, () => cubicState()); this.previousJointCommanded = [0, 0, 0, 0, 0]; return this.read(); }
abort() { if (this.available) call(this.core, "sim_linuxcnc_tp_abort", "number", ["number"], [this.planner]); this.history = []; this.activeTag = 0; this.segmentTags.clear(); this.lastActiveId = 0; this.queueDepthOverride = null; this.executionState = 3; return this.read(); }
exportState() {
const current = this.read();
const segments = [];
if (this.available) {
const depth = Number(call(this.core, "sim_linuxcnc_tp_queue_depth", "number", ["number"], [this.planner]));
const size = 240;
const pointer = this.core._malloc(size);
try {
for (let index = 0; index < depth; index += 1) {
if (call(this.core, "sim_linuxcnc_tp_queue_segment", "number", ["number", "number", "number"], [this.planner, index, pointer]) !== 0) continue;
const values = this.core.HEAPF64.subarray(pointer / 8, pointer / 8 + 29);
const ints = this.core.HEAP32.subarray(pointer / 4 + 52, pointer / 4 + 54);
const segmentView = new DataView(this.core.HEAPU8.buffer, pointer, size);
const id = this.core.HEAPU32[pointer / 4 + 54];
segments.push({ start: toPose(values), end: toPose(values.subarray(9)), center: { x: values[18], y: values[19], z: values[20] }, normal: { x: values[21], y: values[22], z: values[23] }, velocity: values[24], acceleration: values[25], motion_type: ints[0], turn: ints[1], id, kinematics_mode: this.segmentTags.get(id) ?? this.activeTag, term_cond: this.core.HEAP32[pointer / 4 + 55], progress: segmentView.getFloat64(224, true), target: segmentView.getFloat64(232, true) });
}
} finally { this.core._free(pointer); }
}
const unsigned = { schema: SOURCE_TP_SCHEMA, cycle_time: this.cycleTime, velocity_limit: this.velocityLimit, controls: { ...this.controls }, execution_state: this.executionState, active_tag: this.activeTag, state: current.state, segments };
return { ...unsigned, state_hash: fnv1a64(unsigned) };
}
restoreState(value) {
if (!value || value.schema !== SOURCE_TP_SCHEMA || value.cycle_time !== this.cycleTime || value.velocity_limit !== this.velocityLimit || value.state_hash !== fnv1a64(Object.fromEntries(Object.entries(value).filter(([key]) => key !== "state_hash")))) throw new Error("linuxcnc_tp_restore_schema");
this.reset();
this.executionState = Number(value.execution_state ?? 3);
this.setProfile(value.controls?.profile ?? 0, value.controls?.tolerance ?? 0);
this.setFeedOverride(value.controls?.feed_override ?? 1);
this.setPosition(value.state?.position ?? {});
for (const segment of value.segments ?? []) this.enqueue({ ...segment, kinematics_mode: segment.kinematics_mode ?? value.active_tag ?? 0 });
if (this.available && value.state) {
this.core.HEAPF64.set(poseArray(value.state.position), this.posePointer / 8);
const restored = call(this.core, "sim_linuxcnc_tp_restore_progress", "number", ["number", "number", "number", "number"], [this.planner, this.posePointer, Number(value.state.velocity ?? 0), Number(value.state.tick ?? 0)]);
if (restored !== 0) throw new Error(`linuxcnc_tp_restore_progress_${restored}`);
}
return this.read();
}
destroy() { for (const pointer of [this.planner, this.posePointer, this.centerPointer, this.normalPointer, this.statePointer]) if (pointer && typeof this.core?._free === "function") this.core._free(pointer); this.planner = this.posePointer = this.centerPointer = this.normalPointer = this.statePointer = 0; }
}

View File

@@ -76,6 +76,7 @@ export class WasmProgramRuntime {
this.subroutines = []; this.subroutines = [];
this.executionSource = ""; this.executionSource = "";
this.executionLines = []; this.executionLines = [];
this.executionLineMetadata = [];
this.rapidVelocity = 35; this.rapidVelocity = 35;
if (!this.available || typeof core?._malloc !== "function") return; if (!this.available || typeof core?._malloc !== "function") return;
this.pointer = core._malloc(call(core, "sim_program_size", "number", [], [])); this.pointer = core._malloc(call(core, "sim_program_size", "number", [], []));
@@ -108,6 +109,11 @@ export class WasmProgramRuntime {
this.subroutines = []; this.subroutines = [];
this.executionSource = source; this.executionSource = source;
this.executionLines = source.split(/\r?\n/); this.executionLines = source.split(/\r?\n/);
this.executionLineMetadata = this.executionLines.map((_, index) => ({ path: this.path, line: index + 1 }));
while (this.executionLines.length && this.executionLines.at(-1) === "") {
this.executionLines.pop();
this.executionLineMetadata.pop();
}
return { status: "verified", program: this.snapshot() }; return { status: "verified", program: this.snapshot() };
} finally { this.core._free(pointer); } } finally { this.core._free(pointer); }
} }
@@ -133,7 +139,7 @@ export class WasmProgramRuntime {
if (loaded.status !== "verified") return loaded; if (loaded.status !== "verified") return loaded;
temporary.setRapidVelocity(this.rapidVelocity); temporary.setRapidVelocity(this.rapidVelocity);
for (const subroutine of this.subroutines) { for (const subroutine of this.subroutines) {
const added = temporary.addSubroutine(subroutine.name, subroutine.source); const added = temporary.addSubroutine(subroutine.name, subroutine.source, subroutine.path);
if (added.status !== "verified") return added; if (added.status !== "verified") return added;
} }
const started = temporary.start(); const started = temporary.start();
@@ -159,7 +165,7 @@ export class WasmProgramRuntime {
if (result !== 0) throw new Error(`wasm_program_set_rapid_velocity_${result}`); if (result !== 0) throw new Error(`wasm_program_set_rapid_velocity_${result}`);
return { status: "verified", command: "sim_program_set_rapid_velocity" }; return { status: "verified", command: "sim_program_set_rapid_velocity" };
} }
addSubroutine(name, source) { addSubroutine(name, source, path = null) {
if (typeof source !== "string" || source.length === 0) return { status: "error", code: "program_subroutine_invalid" }; if (typeof source !== "string" || source.length === 0) return { status: "error", code: "program_subroutine_invalid" };
const bytes = new TextEncoder().encode(source); const bytes = new TextEncoder().encode(source);
const pointer = this.core._malloc(bytes.byteLength); const pointer = this.core._malloc(bytes.byteLength);
@@ -167,12 +173,19 @@ export class WasmProgramRuntime {
try { try {
const code = call(this.core, "sim_program_add_subroutine", "number", ["number", "number", "number"], [this.pointer, pointer, bytes.byteLength]); const code = call(this.core, "sim_program_add_subroutine", "number", ["number", "number", "number"], [this.pointer, pointer, bytes.byteLength]);
if (code !== 0) return { status: "error", code: `wasm_program_add_subroutine_${code}`, result: code }; if (code !== 0) return { status: "error", code: `wasm_program_add_subroutine_${code}`, result: code };
this.subroutines.push({ name: String(name ?? "subroutine"), source });
if (this.executionSource && !this.executionSource.endsWith("\n")) this.executionSource += "\n"; if (this.executionSource && !this.executionSource.endsWith("\n")) this.executionSource += "\n";
if (this.executionLines.length && this.executionLines.at(-1) !== "") this.executionLines.push(""); if (this.executionLines.length && this.executionLines.at(-1) !== "") {
this.executionLines.push("");
this.executionLineMetadata.push({ path: null, line: 0 });
}
this.executionSource += source; this.executionSource += source;
if (this.executionLines.length && this.executionLines.at(-1) === "") this.executionLines.pop(); if (this.executionLines.length && this.executionLines.at(-1) === "") {
this.executionLines.pop();
this.executionLineMetadata.pop();
}
this.executionLines.push(...source.split(/\r?\n/)); this.executionLines.push(...source.split(/\r?\n/));
this.executionLineMetadata.push(...source.split(/\r?\n/).map((_, index) => ({ path, line: index + 1 })));
this.subroutines.push({ name: String(name ?? "subroutine"), path, source });
return { status: "verified", command: "sim_program_add_subroutine", name: String(name ?? "subroutine") }; return { status: "verified", command: "sim_program_add_subroutine", name: String(name ?? "subroutine") };
} finally { this.core._free(pointer); } } finally { this.core._free(pointer); }
} }
@@ -268,7 +281,8 @@ export class WasmProgramRuntime {
const programStatus = { 0: "idle", 1: "loaded", 2: "running", 3: "paused", 4: "completed", 5: "aborted", 6: "fault" }[state.status] ?? "idle"; 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 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() : ""; const executionLineText = state.current_line > 0 ? (this.executionLines[state.current_line - 1] ?? "").trim() : "";
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 }; const executionMeta = state.current_line > 0 ? (this.executionLineMetadata[state.current_line - 1] ?? { path: this.path, line: state.current_line }) : { path: this.path, line: 0 };
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, execution_source_file: executionMeta.path, execution_source_line: executionMeta.line, 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); } } finally { this.core._free(buffer); }
} }
@@ -278,5 +292,6 @@ export class WasmProgramRuntime {
this.pointer = this.motionPointer = this.samplePointer = this.samplePosePointer = 0; this.pointer = this.motionPointer = this.samplePointer = this.samplePosePointer = 0;
this.executionSource = ""; this.executionSource = "";
this.executionLines = []; this.executionLines = [];
this.executionLineMetadata = [];
} }
} }

View File

@@ -63,7 +63,7 @@ export class WasmXyzacKinematics {
constructor(core, options = {}) { constructor(core, options = {}) {
this.core = core; this.core = core;
this.options = { x_rot_point: 0, y_offset: 20, z_offset: 10, tool_offset: 0, limits: DEFAULT_LIMITS, ...options }; this.options = { x_rot_point: 0, y_offset: 20, z_offset: 10, tool_offset: 0, limits: DEFAULT_LIMITS, ...options };
this.available = ["sim_kinematics_init", "sim_kinematics_switch_init", "sim_kinematics_switch_register", "sim_kinematics_switch_select", "sim_kinematics_switch_forward", "sim_kinematics_switch_inverse", "sim_kinematics_switch_active", "sim_kinematics_switch_state_hash"].every((name) => typeof core?.[`_${name}`] === "function" || typeof core?.[name] === "function"); this.available = ["sim_kinematics_init", "sim_kinematics_switch_init", "sim_kinematics_switch_register", "sim_kinematics_switch_select", "sim_kinematics_switch_select_physical", "sim_kinematics_switch_forward", "sim_kinematics_switch_inverse", "sim_kinematics_switch_inverse_mode", "sim_kinematics_switch_active", "sim_kinematics_switch_state_hash"].every((name) => typeof core?.[`_${name}`] === "function" || typeof core?.[name] === "function");
this.lastCommand = null; this.lastCommand = null;
this.modules = new Map(); this.modules = new Map();
this.pointers = []; this.pointers = [];
@@ -128,6 +128,14 @@ export class WasmXyzacKinematics {
return this.snapshot(); return this.snapshot();
} }
inverseMode(mode, currentPose) {
if (!this.available) return { status: "blocked", code: "wasm_xyzac_unavailable" };
this.setPose(currentPose);
const result = call(this.core, "sim_kinematics_switch_inverse_mode", "number", ["number", "number", "number", "number", "number", "number"], [this.switchPointer, mode, this.posePointer, this.jointPointer, 5, this.resultPointer]);
if (result !== 0) return resultCode("inverse_mode", result);
return { status: "verified", joints: [...this.core.HEAPF64.subarray(this.jointPointer / 8, this.jointPointer / 8 + 5)] };
}
select(mode, joints = [0, 0, 0, 0, 0]) { select(mode, joints = [0, 0, 0, 0, 0]) {
if (![MODE.identity, MODE.xyzac, MODE.userk].includes(mode)) return { status: "error", code: "wasm_xyzac_invalid_mode" }; if (![MODE.identity, MODE.xyzac, MODE.userk].includes(mode)) return { status: "error", code: "wasm_xyzac_invalid_mode" };
this.setJoints(joints); this.setJoints(joints);
@@ -137,6 +145,16 @@ export class WasmXyzacKinematics {
return this.snapshot(); return this.snapshot();
} }
selectPhysical(mode, joints = [0, 0, 0, 0, 0]) {
if (!this.available) return { status: "blocked", code: "wasm_xyzac_unavailable" };
if (![MODE.identity, MODE.xyzac, MODE.userk].includes(mode)) return { status: "error", code: "wasm_xyzac_invalid_mode" };
this.setJoints(joints);
const result = call(this.core, "sim_kinematics_switch_select_physical", "number", ["number", "number", "number", "number", "number", "number"], [this.switchPointer, mode, this.jointPointer, 5, this.posePointer, this.resultPointer]);
if (result !== 0) return resultCode("switch_physical", result);
this.lastCommand = `M${mode === MODE.xyzac ? 428 : mode === MODE.identity ? 429 : 430}`;
return this.snapshot();
}
restoreState(state) { restoreState(state) {
if (!state || state.schema !== "wasm-xyzac-kinematics.v1" || ![MODE.identity, MODE.xyzac, MODE.userk].includes(state.active_type) || !Array.isArray(state.joints) || state.joints.length !== 5) throw new Error("wasm_xyzac_restore_state_invalid"); if (!state || state.schema !== "wasm-xyzac-kinematics.v1" || ![MODE.identity, MODE.xyzac, MODE.userk].includes(state.active_type) || !Array.isArray(state.joints) || state.joints.length !== 5) throw new Error("wasm_xyzac_restore_state_invalid");
this.initializeSwitch(); this.initializeSwitch();

View File

@@ -16,7 +16,7 @@ const CASE_KINEMATICS = Object.freeze({
"linuxcnc/configs/sim/axis/corexy/corexy_by_hal.ini": Object.freeze({ type: "corexy", path_mode: "hal" }), "linuxcnc/configs/sim/axis/corexy/corexy_by_hal.ini": Object.freeze({ type: "corexy", path_mode: "hal" }),
"linuxcnc/configs/sim/axis/ldelta.ini": Object.freeze({ type: "lineardelta", path_mode: "kins" }), "linuxcnc/configs/sim/axis/ldelta.ini": Object.freeze({ type: "lineardelta", path_mode: "kins" }),
"linuxcnc/configs/sim/axis/rdelta.ini": Object.freeze({ type: "rotarydelta", path_mode: "kins" }), "linuxcnc/configs/sim/axis/rdelta.ini": Object.freeze({ type: "rotarydelta", path_mode: "kins" }),
"linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini": Object.freeze({ type: "xyzac-trt", path_mode: "wasm", coordinates: "XYZAC", joint_count: 5, max_linear_velocity: 35, switchkins: { 0: "identity", 1: "xyzac", 2: "userk" }, offsets: { x_rot_point: 0, y_offset: 20, z_offset: 10, tool_offset: 0 } }), "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini": Object.freeze({ type: "xyzac-trt", path_mode: "wasm", coordinates: "XYZAC", joint_count: 5, max_linear_velocity: 35, axis_linear_velocity_limit: 20, max_acceleration: 300, max_jerk: 1000000000, servo_cycle_time: 0.001, task_cycle_time: 0.01, cycle_time: 0.001, switchkins: { 0: "identity", 1: "xyzac", 2: "userk" }, offsets: { x_rot_point: 0, y_offset: 20, z_offset: 10, tool_offset: 0 } }),
}); });
const RUNTIME_FIXTURES = Object.freeze({ const RUNTIME_FIXTURES = Object.freeze({
"linuxcnc/configs/sim/axis/minimal_xyz.ini": Object.freeze({ "linuxcnc/configs/sim/axis/minimal_xyz.ini": Object.freeze({