接入 task HAL Web 仿真运行时

This commit is contained in:
2026-06-22 06:11:55 +08:00
parent 3771b9eafe
commit bd11a5f8d6
42 changed files with 5574 additions and 50 deletions

View File

@@ -42,6 +42,10 @@ families, and drift report.
| RTAPI compatibility headers | `src/rtapi/rtapi_*.h` in the manifest | Copy unchanged plus standalone shim include path | `runtime/core/shims/rtapi.h` supplies the minimal standalone RTAPI surface needed by vendored code | Vendor byte sync, compile coverage through dependent source probes | | RTAPI compatibility headers | `src/rtapi/rtapi_*.h` in the manifest | Copy unchanged plus standalone shim include path | `runtime/core/shims/rtapi.h` supplies the minimal standalone RTAPI surface needed by vendored code | Vendor byte sync, compile coverage through dependent source probes |
| Canon/NML-facing interpreter types | `src/emc/nml_intf/canon*.hh`, `emctool.h`, `interp_return.hh`, `motion_types.h`, `emcpose.*`, `emcpos.h`, `debugflags.h`, `src/emc/linuxcnc.h` | Copy unchanged plus narrow standalone status shim | NML transport is not ported; `runtime/core/shims/nml_intf/emc.hh` exposes only the `emcStatus` machine-units status edge currently needed by vendored interpreter conversion and initialization code | Vendor byte sync, dependent source probes, `linuxcnc_emc_status_probe`, `linuxcnc_tp_api_probe`, interpreter harnesses, `tests/wasm/node/verify_interp_wasm.sh` and `tests/browser/verify_interp_browser.sh` `Interp::init()` machine-unit assertions | | Canon/NML-facing interpreter types | `src/emc/nml_intf/canon*.hh`, `emctool.h`, `interp_return.hh`, `motion_types.h`, `emcpose.*`, `emcpos.h`, `debugflags.h`, `src/emc/linuxcnc.h` | Copy unchanged plus narrow standalone status shim | NML transport is not ported; `runtime/core/shims/nml_intf/emc.hh` exposes only the `emcStatus` machine-units status edge currently needed by vendored interpreter conversion and initialization code | Vendor byte sync, dependent source probes, `linuxcnc_emc_status_probe`, `linuxcnc_tp_api_probe`, interpreter harnesses, `tests/wasm/node/verify_interp_wasm.sh` and `tests/browser/verify_interp_browser.sh` `Interp::init()` machine-unit assertions |
| Motion state headers | `src/emc/motion/state_tag.h`, `emcmotcfg.h`, `simple_tp.h`, `motion.h`, `mot_priv.h`, `axis.h` | Copy unchanged | Realtime motion process is not ported; standalone probes seed the small motion status/config state required by TP calls | Vendor byte sync, `linuxcnc_tp_api_probe`, `tests/wasm/node/verify_tp_wasm.sh` | | Motion state headers | `src/emc/motion/state_tag.h`, `emcmotcfg.h`, `simple_tp.h`, `motion.h`, `mot_priv.h`, `axis.h` | Copy unchanged | Realtime motion process is not ported; standalone probes seed the small motion status/config state required by TP calls | Vendor byte sync, `linuxcnc_tp_api_probe`, `tests/wasm/node/verify_tp_wasm.sh` |
| Native task / motion / HAL sync phase 0 references | `tools/task-hal-source-manifest.txt` lists `src/emc/task/*`, selected `src/emc/motion/*`, `src/hal/*`, `src/emc/nml_intf/emc.hh`, and `src/libnml/posemath/*` | Reference proof for task/motion/HAL Web simulation runtime | `tools/verify_task_hal_source_manifest.sh` compares the local LinuxCNC reference tree and vendored tree where present, emits task/HAL/motion/NML/libnml counts. `tests/native/probe_trt_task_hal_runtime.sh` is opt-in for exclusive host LinuxCNC runtime probing and does not promote by default; Web simulation promotion is handled by task/HAL WASM and browser gates. | `tests/native/verify_task_hal_phase0.sh` |
| HAL runtime phase 2 minimal boundary | `src/hal/hal_lib.c`, `src/hal/hal_priv.h`, `src/hal/components/threads.c`, `src/hal/utils/halcmd_commands.cc` as source references; `runtime/core/shims/hal.h` type boundary | Runtime-edge adapter, not full native HAL promotion | `runtime/core/linuxcnc_wrap/linuxcnc_hal_runtime.cpp` owns C/C++ HAL pin, signal, param, net, and thread scheduler state behind LinuxCNC-style `hal_*` APIs and `lchal_*` C ABI; `loadusr` is blocked evidence; task runtime and native HAL comparison remain future work | `tests/wasm/node/verify_hal_runtime.sh` WASM Node smoke; native fallback only when `emcc` is unavailable |
| Motion/HAL sync phase 3 minimal boundary | `src/emc/motion/motion.h`, `src/emc/motion/command.c`, `src/emc/motion/control.c`, `src/emc/motion/mot_priv.h`, `src/emc/task/taskintf.cc` as source references | Runtime-edge adapter, not full native motion promotion | `runtime/core/linuxcnc_wrap/linuxcnc_motion_runtime.c` exposes the planned `lcmot_*` C ABI, accepts LinuxCNC-style motion command JSON, advances deterministic servo cycles, and synchronizes `motion.*`, `axis.*`, and `joint.*` HAL pins through the phase 2 HAL runtime; full LinuxCNC `emcmotController()` and task/NML queue integration remain future work | `tests/wasm/node/verify_motion_hal_sync.sh` WASM Node smoke |
| Task/motion/HAL simulation runtime | `src/emc/task/task.hh`, `src/emc/task/emctask.cc`, `src/emc/task/emctaskmain.cc`, `src/emc/task/taskintf.cc`, `src/emc/task/emccanon.cc`, and `src/emc/nml_intf/emc.hh` as source references | Runtime-edge adapter promoted for Web simulation boundary only | `runtime/core/linuxcnc_wrap/linuxcnc_task_hal_wasm.cpp` exposes the planned `lctask_*` C ABI, stages files, opens a program, tracks task state/mode/interp/exec status, and forwards RUN/PAUSE/RESUME/ABORT/MDI/JOG into the phase 3 motion command queue. `runtime/sdk/src/linuxcnc-task-hal.js`, `app/src/runtime/linuxcnc-task-hal-runtime.js`, Worker/client files, store action mapping, gmoccapy diagnostics, and full boundary gates now validate `nativeTaskReady=true` plus `nativeHalSyncReady=true` for deterministic Web simulation. Hardware drive, host realtime kernel, external user-M process, and tool DB process remain false. | `tests/wasm/node/verify_task_hal_wasm.sh`; `tests/wasm/node/verify_task_hal_sdk.sh`; `web-rtcp-5axis-sim-plan/tests/node/verify_linuxcnc_task_hal_runtime.mjs`; browser smoke |
| Identity/trivial kinematics | `src/emc/kinematics/kinematics.h`, `cubic.h`, `kins_util.c`, `trivkins.c` | Copy unchanged | HAL component lifecycle and RTAPI module metadata are replaced by standalone shims; forward/inverse mapping behavior remains LinuxCNC source | Vendor byte sync, per-file source probes, `linuxcnc_kinematics_probe` | | Identity/trivial kinematics | `src/emc/kinematics/kinematics.h`, `cubic.h`, `kins_util.c`, `trivkins.c` | Copy unchanged | HAL component lifecycle and RTAPI module metadata are replaced by standalone shims; forward/inverse mapping behavior remains LinuxCNC source | Vendor byte sync, per-file source probes, `linuxcnc_kinematics_probe` |
| Switchable 5-axis bridge kinematics | `src/emc/kinematics/5axiskins.c`, `switchkins.c`, `switchkins.h`, `userkfuncs.c`, plus `src/rtapi/rtapi_ctype.h` | Copy unchanged | HAL pin allocation, HAL component lifecycle, and RTAPI module metadata are standalone runtime edges; switchable 5-axis forward/inverse behavior remains LinuxCNC source | Vendor byte sync, per-file source probes, `linuxcnc_5axis_kinematics_probe` | | Switchable 5-axis bridge kinematics | `src/emc/kinematics/5axiskins.c`, `switchkins.c`, `switchkins.h`, `userkfuncs.c`, plus `src/rtapi/rtapi_ctype.h` | Copy unchanged | HAL pin allocation, HAL component lifecycle, and RTAPI module metadata are standalone runtime edges; switchable 5-axis forward/inverse behavior remains LinuxCNC source | Vendor byte sync, per-file source probes, `linuxcnc_5axis_kinematics_probe` |
| TRT table-rotary kinematics | `src/emc/kinematics/trtfuncs.c`, `xyzac-trt-kins.c`, `xyzbc-trt-kins.c` | Copy unchanged | HAL pin allocation and switchkins lifecycle stay runtime boundaries; XYZAC/XYZBC TRT forward/inverse behavior remains LinuxCNC source | Vendor byte sync, per-file source probes, `linuxcnc_xyzac_trt_kinematics_probe`, `linuxcnc_xyzbc_trt_kinematics_probe` | | TRT table-rotary kinematics | `src/emc/kinematics/trtfuncs.c`, `xyzac-trt-kins.c`, `xyzbc-trt-kins.c` | Copy unchanged | HAL pin allocation and switchkins lifecycle stay runtime boundaries; XYZAC/XYZBC TRT forward/inverse behavior remains LinuxCNC source | Vendor byte sync, per-file source probes, `linuxcnc_xyzac_trt_kinematics_probe`, `linuxcnc_xyzbc_trt_kinematics_probe` |
@@ -69,7 +73,7 @@ families, and drift report.
| Native file IO | `inifile.cc`, `rs274ngc_pre.cc`, `tooldata_common.cc`, parameter file paths, tool table paths | Allowed in native probes; the interpreter WASM C ABI validates direct parameter-file restore/save by calling vendored `Interp::restore_parameters()` and `Interp::save_parameters()`, validates startup G92 parameter persistence by passing staged `[RS274NGC]PARAMETER_FILE` paths through vendored `ini_load()`/`Interp::init()`/`restore_parameters()`, including missing required numeric parameter defaulting and `DISABLE_G92_PERSISTENCE`, and validates tool-table load/save by calling vendored `tooldata_load()` and `tooldata_save()` against Emscripten filesystem paths; browser OPFS remains a host-side adapter under `runtime/opfs/`, with path ownership in `runtime/opfs/path-model.js`, generic snapshot persistence in `runtime/opfs/snapshot-store.js`, pure-text machine-file persistence in `runtime/opfs/machine-file-store.js`, OPFS-to-WASM parameter-file copying in `runtime/opfs/linuxcnc-parameter-bridge.js`, OPFS-to-WASM tool-table copying in `runtime/opfs/linuxcnc-tool-table-bridge.js`, and grouped INI/parameter/tool-table session loading in `runtime/opfs/linuxcnc-machine-session-bridge.js`, including INI-derived `[RS274NGC]PARAMETER_FILE` and `[EMCIO]TOOL_TABLE` OPFS filename selection through the LinuxCNC-backed INI SDK; explicit host session file-name options take precedence over INI-derived names, missing INI file-name values fall back to host default `linuxcnc.var` and `tool.tbl` paths, and OPFS path validation rejects traversal and nested segments before host storage access | | Native file IO | `inifile.cc`, `rs274ngc_pre.cc`, `tooldata_common.cc`, parameter file paths, tool table paths | Allowed in native probes; the interpreter WASM C ABI validates direct parameter-file restore/save by calling vendored `Interp::restore_parameters()` and `Interp::save_parameters()`, validates startup G92 parameter persistence by passing staged `[RS274NGC]PARAMETER_FILE` paths through vendored `ini_load()`/`Interp::init()`/`restore_parameters()`, including missing required numeric parameter defaulting and `DISABLE_G92_PERSISTENCE`, and validates tool-table load/save by calling vendored `tooldata_load()` and `tooldata_save()` against Emscripten filesystem paths; browser OPFS remains a host-side adapter under `runtime/opfs/`, with path ownership in `runtime/opfs/path-model.js`, generic snapshot persistence in `runtime/opfs/snapshot-store.js`, pure-text machine-file persistence in `runtime/opfs/machine-file-store.js`, OPFS-to-WASM parameter-file copying in `runtime/opfs/linuxcnc-parameter-bridge.js`, OPFS-to-WASM tool-table copying in `runtime/opfs/linuxcnc-tool-table-bridge.js`, and grouped INI/parameter/tool-table session loading in `runtime/opfs/linuxcnc-machine-session-bridge.js`, including INI-derived `[RS274NGC]PARAMETER_FILE` and `[EMCIO]TOOL_TABLE` OPFS filename selection through the LinuxCNC-backed INI SDK; explicit host session file-name options take precedence over INI-derived names, missing INI file-name values fall back to host default `linuxcnc.var` and `tool.tbl` paths, and OPFS path validation rejects traversal and nested segments before host storage access |
| RTAPI | `rtapi_*.h`, TP, posemath, motion headers | Minimal standalone shim in `runtime/core/shims/rtapi.h` | | RTAPI | `rtapi_*.h`, TP, posemath, motion headers | Minimal standalone shim in `runtime/core/shims/rtapi.h` |
| NML transport | `emc.hh`, motion/NML type headers | Transport is not ported; only the status/type edges needed by vendored compute code are exposed through standalone shims and probes | | NML transport | `emc.hh`, motion/NML type headers | Transport is not ported; only the status/type edges needed by vendored compute code are exposed through standalone shims and probes |
| HAL runtime | named parameter lookup, kinematics component lifecycle, and runtime status edges | Standalone HAL adapter under `runtime/core/linuxcnc_wrap/` | | HAL runtime | named parameter lookup, kinematics component lifecycle, and runtime status edges | Existing kinematics/interpreter HAL adapter remains under `runtime/core/linuxcnc_wrap/linuxcnc_hal_adapter.cpp`; phase 2 task-HAL work adds `linuxcnc_hal_runtime.cpp` for owned pin/signal/param/net/thread scheduler state, still unpromoted for native HAL sync |
| User M-code process execution | `emctask.cc`, `interp_convert.cc`, `interp_queue.cc` | Search and registration are mirrored at the standalone machine-config boundary; native/WASM tests record deterministic `USER_M_COMMAND` events and do not spawn host processes | | User M-code process execution | `emctask.cc`, `interp_convert.cc`, `interp_queue.cc` | Search and registration are mirrored at the standalone machine-config boundary; native/WASM tests record deterministic `USER_M_COMMAND` events and do not spawn host processes |
| Python/remap | `rs274ngc_pre.cc`, `interp_o_word.cc`, `interp_remap.cc`, remap hooks, selected LinuxCNC `configs/sim/axis/vismach/5axis/*/remap_subs/*.ngc` files, `tests/remap/duplicate-o-word/*`, `tests/remap/fail/args.0/*`, `tests/remap/fail/args.1/*`, `tests/remap/fail/args.2/*`, `tests/remap/fail/body-ngc/*`, `tests/remap/m30-interaction/*`, `tests/remap/nested-remaps-oword/*`, `tests/remap/posargs.0/*`, `tests/remap/sequencing/*`, and NGC-only `tests/remap/remap-io/test-ngc.ini` plus `io_*.ngc` | Python calls remain stubbed at the runtime boundary today; five-axis M428/M429/M430 source assets and the upstream NGC remap regression files are vendored unchanged. NGC remap descriptor paths parse through vendored LinuxCNC code, native/WASM/browser validation executes the NGC remap files through vendored LinuxCNC O-word and file execution paths, and the remap-IO NGC-only branch feeds the upstream MDI sequence into vendored LinuxCNC `Interp::execute()` while external M66 input values and `_hal[...]` synchronization remain standalone runtime adapter boundaries | | Python/remap | `rs274ngc_pre.cc`, `interp_o_word.cc`, `interp_remap.cc`, remap hooks, selected LinuxCNC `configs/sim/axis/vismach/5axis/*/remap_subs/*.ngc` files, `tests/remap/duplicate-o-word/*`, `tests/remap/fail/args.0/*`, `tests/remap/fail/args.1/*`, `tests/remap/fail/args.2/*`, `tests/remap/fail/body-ngc/*`, `tests/remap/m30-interaction/*`, `tests/remap/nested-remaps-oword/*`, `tests/remap/posargs.0/*`, `tests/remap/sequencing/*`, and NGC-only `tests/remap/remap-io/test-ngc.ini` plus `io_*.ngc` | Python calls remain stubbed at the runtime boundary today; five-axis M428/M429/M430 source assets and the upstream NGC remap regression files are vendored unchanged. NGC remap descriptor paths parse through vendored LinuxCNC code, native/WASM/browser validation executes the NGC remap files through vendored LinuxCNC O-word and file execution paths, and the remap-IO NGC-only branch feeds the upstream MDI sequence into vendored LinuxCNC `Interp::execute()` while external M66 input values and `_hal[...]` synchronization remain standalone runtime adapter boundaries |
| Canonical machine actions | `interp_convert.cc`, `interp_execute.cc`, `interp_queue.cc`, selected `tests/interp/*` regression assets | Captured by standalone canonical event sink functions for regression fixtures; vendored upstream interpreter test assets remain unchanged and are executed through LinuxCNC file execution | | Canonical machine actions | `interp_convert.cc`, `interp_execute.cc`, `interp_queue.cc`, selected `tests/interp/*` regression assets | Captured by standalone canonical event sink functions for regression fixtures; vendored upstream interpreter test assets remain unchanged and are executed through LinuxCNC file execution |
@@ -128,3 +132,9 @@ families, and drift report.
`runtime/opfs/path-model.js`. Full machine-state restoration remains future `runtime/opfs/path-model.js`. Full machine-state restoration remains future
work. work.
- Native LinuxCNC GUI code remains out of scope for implementation. - Native LinuxCNC GUI code remains out of scope for implementation.
- Native task/motion/HAL sync is complete for deterministic Web simulation:
phase 0 source/probe gates, phase 2 HAL registry, phase 3 motion/HAL
servo-cycle C ABI, phase 4 task shim plus SDK wrapper, machine-file session
handoff, Worker/client files, store action mapping, diagnostics, and full
boundary promotion gates are present. This does not imply host realtime
kernel, hardware IO, external user-M process, or full tool DB process support.

View File

@@ -0,0 +1,968 @@
#include "linuxcnc_hal_runtime.hh"
#include <algorithm>
#include <cstdarg>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <map>
#include <sstream>
#include <string>
#include <utility>
#include <vector>
namespace {
struct HalEntry {
std::string name;
hal_type_t type = HAL_TYPE_UNINITIALIZED;
hal_pin_dir_t dir = HAL_DIR_UNSPECIFIED;
hal_data_u value{};
bool connected = false;
std::string signal;
};
struct HalThreadFunction {
std::string name;
double position = 0.0;
int uses_fp = 0;
};
struct HalThread {
std::string name;
unsigned long period_ns = 0;
int uses_fp = 0;
std::vector<HalThreadFunction> functions;
};
struct HalRuntime {
int next_comp_id = 1;
bool ready = false;
bool threads_running = false;
long long cycle = 0;
std::map<std::string, HalEntry> pins;
std::map<std::string, HalEntry> signals;
std::map<std::string, HalEntry> params;
std::map<std::string, HalThread> threads;
std::vector<void *> allocations;
std::vector<std::string> events;
};
HalRuntime &runtime()
{
static HalRuntime state;
return state;
}
const char *type_name(hal_type_t type)
{
switch (type) {
case HAL_BIT:
return "bit";
case HAL_FLOAT:
return "float";
case HAL_S32:
return "s32";
case HAL_U32:
return "u32";
case HAL_S64:
return "s64";
case HAL_U64:
return "u64";
case HAL_TYPE_UNINITIALIZED:
default:
return "uninitialized";
}
}
std::string json_escape(const std::string &value)
{
std::ostringstream out;
for (const char ch : value) {
switch (ch) {
case '\\':
out << "\\\\";
break;
case '"':
out << "\\\"";
break;
case '\n':
out << "\\n";
break;
case '\r':
out << "\\r";
break;
case '\t':
out << "\\t";
break;
default:
out << ch;
break;
}
}
return out.str();
}
std::string value_json(const HalEntry &entry)
{
std::ostringstream out;
switch (entry.type) {
case HAL_BIT:
out << (entry.value.b ? "true" : "false");
break;
case HAL_FLOAT:
out.precision(17);
out << entry.value.f;
break;
case HAL_S32:
out << entry.value.s;
break;
case HAL_U32:
out << entry.value.u;
break;
case HAL_S64:
out << entry.value.ls;
break;
case HAL_U64:
out << entry.value.lu;
break;
case HAL_TYPE_UNINITIALIZED:
default:
out << "null";
break;
}
return out.str();
}
std::string entry_json(const HalEntry &entry)
{
std::ostringstream out;
out << "{\"name\":\"" << json_escape(entry.name) << "\"";
out << ",\"type\":\"" << type_name(entry.type) << "\"";
out << ",\"dir\":" << static_cast<int>(entry.dir);
out << ",\"connected\":" << (entry.connected ? "true" : "false");
if (!entry.signal.empty()) {
out << ",\"signal\":\"" << json_escape(entry.signal) << "\"";
}
out << ",\"value\":" << value_json(entry) << "}";
return out.str();
}
int write_output(const std::string &value, char *out, int out_len)
{
if (!out || out_len <= 0) {
return -1;
}
const int required = static_cast<int>(value.size()) + 1;
if (out_len < required) {
if (out_len > 0) {
out[0] = '\0';
}
return required;
}
std::memcpy(out, value.c_str(), static_cast<std::size_t>(required));
return 0;
}
std::vector<std::string> split_words(const std::string &line)
{
std::istringstream in(line);
std::vector<std::string> words;
std::string word;
while (in >> word) {
if (!word.empty() && word[0] == '#') {
break;
}
words.push_back(word);
}
return words;
}
HalEntry *lookup_entry(const char *name)
{
if (!name) {
return nullptr;
}
auto &state = runtime();
auto pin = state.pins.find(name);
if (pin != state.pins.end()) {
return &pin->second;
}
auto param = state.params.find(name);
if (param != state.params.end()) {
return &param->second;
}
auto signal = state.signals.find(name);
if (signal != state.signals.end()) {
return &signal->second;
}
return nullptr;
}
hal_data_u *value_ptr(HalEntry &entry)
{
return &entry.value;
}
template <typename PointerT>
int create_pin(const char *name, hal_pin_dir_t dir, PointerT **data_ptr_addr, int, hal_type_t type)
{
if (!name || !data_ptr_addr) {
return -1;
}
auto &state = runtime();
auto [it, inserted] = state.pins.emplace(name, HalEntry{});
HalEntry &entry = it->second;
entry.name = name;
entry.type = type;
entry.dir = dir;
entry.connected = false;
switch (type) {
case HAL_BIT:
*data_ptr_addr = reinterpret_cast<PointerT *>(&entry.value.b);
break;
case HAL_FLOAT:
*data_ptr_addr = reinterpret_cast<PointerT *>(&entry.value.f);
break;
case HAL_S32:
*data_ptr_addr = reinterpret_cast<PointerT *>(&entry.value.s);
break;
case HAL_U32:
*data_ptr_addr = reinterpret_cast<PointerT *>(&entry.value.u);
break;
case HAL_S64:
*data_ptr_addr = reinterpret_cast<PointerT *>(&entry.value.ls);
break;
case HAL_U64:
*data_ptr_addr = reinterpret_cast<PointerT *>(&entry.value.lu);
break;
case HAL_TYPE_UNINITIALIZED:
default:
return -1;
}
state.events.push_back(std::string(inserted ? "pin_create:" : "pin_reuse:") + name);
return 0;
}
template <typename PointerT, int (*Create)(const char *, hal_pin_dir_t, PointerT **, int)>
int create_pin_newf(hal_pin_dir_t dir, PointerT **data_ptr_addr, int comp_id, const char *fmt,
va_list ap)
{
if (!fmt) {
return -1;
}
char name[HAL_NAME_LEN * 2]{};
std::vsnprintf(name, sizeof(name), fmt, ap);
return Create(name, dir, data_ptr_addr, comp_id);
}
void copy_value_from_addr(HalEntry &entry, const void *data_addr)
{
switch (entry.type) {
case HAL_BIT:
entry.value.b = *static_cast<const hal_bit_t *>(data_addr);
break;
case HAL_FLOAT:
entry.value.f = *static_cast<const hal_float_t *>(data_addr);
break;
case HAL_S32:
entry.value.s = *static_cast<const hal_s32_t *>(data_addr);
break;
case HAL_U32:
entry.value.u = *static_cast<const hal_u32_t *>(data_addr);
break;
case HAL_S64:
entry.value.ls = *static_cast<const hal_s64_t *>(data_addr);
break;
case HAL_U64:
entry.value.lu = *static_cast<const hal_u64_t *>(data_addr);
break;
case HAL_TYPE_UNINITIALIZED:
default:
break;
}
}
int create_param(const char *name, hal_param_dir_t dir, void *data_addr, int, hal_type_t type)
{
if (!name || !data_addr) {
return -1;
}
auto &entry = runtime().params[name];
entry.name = name;
entry.type = type;
entry.dir = static_cast<hal_pin_dir_t>(dir);
entry.connected = false;
copy_value_from_addr(entry, data_addr);
runtime().events.push_back(std::string("param_create:") + name);
return 0;
}
template <typename ValueT,
int (*Create)(const char *, hal_param_dir_t, ValueT *, int)>
int create_param_newf(hal_param_dir_t dir, ValueT *data_addr, int comp_id, const char *fmt,
va_list ap)
{
if (!fmt) {
return -1;
}
char name[HAL_NAME_LEN * 2]{};
std::vsnprintf(name, sizeof(name), fmt, ap);
return Create(name, dir, data_addr, comp_id);
}
bool assign_from_text(HalEntry &entry, const char *text)
{
if (!text) {
return false;
}
switch (entry.type) {
case HAL_BIT:
entry.value.b = std::strcmp(text, "1") == 0 || std::strcmp(text, "true") == 0 ||
std::strcmp(text, "TRUE") == 0;
return true;
case HAL_FLOAT:
entry.value.f = std::strtod(text, nullptr);
return true;
case HAL_S32:
entry.value.s = static_cast<int>(std::strtol(text, nullptr, 0));
return true;
case HAL_U32:
entry.value.u = static_cast<unsigned int>(std::strtoul(text, nullptr, 0));
return true;
case HAL_S64:
entry.value.ls = std::strtoll(text, nullptr, 0);
return true;
case HAL_U64:
entry.value.lu = std::strtoull(text, nullptr, 0);
return true;
case HAL_TYPE_UNINITIALIZED:
default:
return false;
}
}
void propagate_signal(const std::string &signal_name)
{
auto &state = runtime();
auto signal_it = state.signals.find(signal_name);
if (signal_it == state.signals.end()) {
return;
}
for (auto &[pin_name, pin] : state.pins) {
if (pin.signal == signal_name) {
pin.value = signal_it->second.value;
}
}
}
int link_pin_to_signal(const std::string &pin_name, const std::string &signal_name)
{
auto &state = runtime();
auto pin_it = state.pins.find(pin_name);
if (pin_it == state.pins.end()) {
return -1;
}
auto [signal_it, inserted] = state.signals.emplace(signal_name, HalEntry{});
HalEntry &signal = signal_it->second;
signal.name = signal_name;
if (inserted || signal.type == HAL_TYPE_UNINITIALIZED) {
signal.type = pin_it->second.type;
signal.value = pin_it->second.value;
}
if (signal.type != pin_it->second.type) {
return -1;
}
signal.connected = true;
pin_it->second.connected = true;
pin_it->second.signal = signal_name;
pin_it->second.value = signal.value;
state.events.push_back("net:" + signal_name + ":" + pin_name);
return 0;
}
int set_entry_value(const char *name, const char *text)
{
HalEntry *entry = lookup_entry(name);
if (!entry || !assign_from_text(*entry, text)) {
return -1;
}
auto &state = runtime();
if (state.signals.count(entry->name) > 0) {
propagate_signal(entry->name);
} else if (!entry->signal.empty()) {
auto signal_it = state.signals.find(entry->signal);
if (signal_it != state.signals.end()) {
signal_it->second.value = entry->value;
propagate_signal(entry->signal);
}
}
state.events.push_back(std::string("setp:") + name + "=" + text);
return 0;
}
std::string snapshot_json()
{
auto &state = runtime();
std::ostringstream out;
out << "{\"semanticBoundary\":\"linuxcnc_hal_runtime_phase2_minimal\"";
out << ",\"halRuntimeReady\":true";
out << ",\"halSyncReady\":false";
out << ",\"nativeHalSyncReady\":false";
out << ",\"cycle\":" << state.cycle;
out << ",\"ready\":" << (state.ready ? "true" : "false");
out << ",\"threadsRunning\":" << (state.threads_running ? "true" : "false");
out << ",\"pins\":{";
bool first = true;
for (const auto &[name, entry] : state.pins) {
if (!first) {
out << ",";
}
first = false;
out << "\"" << json_escape(name) << "\":" << entry_json(entry);
}
out << "},\"signals\":{";
first = true;
for (const auto &[name, entry] : state.signals) {
if (!first) {
out << ",";
}
first = false;
out << "\"" << json_escape(name) << "\":" << entry_json(entry);
}
out << "},\"params\":{";
first = true;
for (const auto &[name, entry] : state.params) {
if (!first) {
out << ",";
}
first = false;
out << "\"" << json_escape(name) << "\":" << entry_json(entry);
}
out << "},\"threads\":{";
first = true;
for (const auto &[name, thread] : state.threads) {
if (!first) {
out << ",";
}
first = false;
out << "\"" << json_escape(name) << "\":{\"periodNs\":" << thread.period_ns;
out << ",\"functions\":[";
for (std::size_t i = 0; i < thread.functions.size(); ++i) {
if (i > 0) {
out << ",";
}
out << "{\"name\":\"" << json_escape(thread.functions[i].name) << "\"";
out << ",\"position\":" << thread.functions[i].position << "}";
}
out << "]}";
}
out << "},\"events\":[";
for (std::size_t i = 0; i < state.events.size(); ++i) {
if (i > 0) {
out << ",";
}
out << "\"" << json_escape(state.events[i]) << "\"";
}
out << "]}";
return out.str();
}
void clear_runtime()
{
auto &state = runtime();
for (void *ptr : state.allocations) {
::operator delete(ptr);
}
state = HalRuntime{};
}
int load_hal_line(const std::vector<std::string> &words)
{
if (words.empty()) {
return 0;
}
if (words[0] == "loadusr") {
runtime().events.push_back("blocked:loadusr");
return -2;
}
if (words[0] == "loadrt") {
runtime().events.push_back(words.size() > 1 ? "loadrt:" + words[1] : "loadrt");
return 0;
}
if (words[0] == "addf" && words.size() >= 3) {
return hal_add_funct_to_thread(words[1].c_str(), words[2].c_str(), 0.0, 0);
}
if (words[0] == "net" && words.size() >= 3) {
const std::string signal = words[1];
for (std::size_t i = 2; i < words.size(); ++i) {
if (link_pin_to_signal(words[i], signal) != 0) {
return -1;
}
}
return 0;
}
if (words[0] == "setp" && words.size() >= 3) {
return hal_set_p(words[1].c_str(), words[2].c_str());
}
if (words[0] == "gets") {
runtime().events.push_back("gets");
return 0;
}
runtime().events.push_back("ignored:" + words[0]);
return 0;
}
} // namespace
extern "C" {
int hal_init(const char *)
{
return runtime().next_comp_id++;
}
int hal_ready(int)
{
runtime().ready = true;
runtime().events.push_back("ready");
return 0;
}
int hal_exit(int)
{
runtime().ready = false;
runtime().events.push_back("exit");
return 0;
}
void *hal_malloc(long int size)
{
if (size <= 0) {
return nullptr;
}
void *ptr = ::operator new(static_cast<std::size_t>(size), std::nothrow);
if (ptr) {
runtime().allocations.push_back(ptr);
}
return ptr;
}
int hal_pin_bit_new(const char *name, hal_pin_dir_t dir, hal_bit_t **data_ptr_addr, int comp_id)
{
return create_pin(name, dir, data_ptr_addr, comp_id, HAL_BIT);
}
int hal_pin_float_new(const char *name, hal_pin_dir_t dir, hal_float_t **data_ptr_addr,
int comp_id)
{
return create_pin(name, dir, data_ptr_addr, comp_id, HAL_FLOAT);
}
int hal_pin_u32_new(const char *name, hal_pin_dir_t dir, hal_u32_t **data_ptr_addr, int comp_id)
{
return create_pin(name, dir, data_ptr_addr, comp_id, HAL_U32);
}
int hal_pin_s32_new(const char *name, hal_pin_dir_t dir, hal_s32_t **data_ptr_addr, int comp_id)
{
return create_pin(name, dir, data_ptr_addr, comp_id, HAL_S32);
}
int hal_pin_u64_new(const char *name, hal_pin_dir_t dir, hal_u64_t **data_ptr_addr, int comp_id)
{
return create_pin(name, dir, data_ptr_addr, comp_id, HAL_U64);
}
int hal_pin_s64_new(const char *name, hal_pin_dir_t dir, hal_s64_t **data_ptr_addr, int comp_id)
{
return create_pin(name, dir, data_ptr_addr, comp_id, HAL_S64);
}
int hal_pin_bit_newf(hal_pin_dir_t dir, hal_bit_t **data_ptr_addr, int comp_id,
const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
const int rc = create_pin_newf<hal_bit_t, hal_pin_bit_new>(dir, data_ptr_addr, comp_id, fmt, ap);
va_end(ap);
return rc;
}
int hal_pin_float_newf(hal_pin_dir_t dir, hal_float_t **data_ptr_addr, int comp_id,
const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
const int rc =
create_pin_newf<hal_float_t, hal_pin_float_new>(dir, data_ptr_addr, comp_id, fmt, ap);
va_end(ap);
return rc;
}
int hal_pin_u32_newf(hal_pin_dir_t dir, hal_u32_t **data_ptr_addr, int comp_id,
const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
const int rc = create_pin_newf<hal_u32_t, hal_pin_u32_new>(dir, data_ptr_addr, comp_id, fmt, ap);
va_end(ap);
return rc;
}
int hal_pin_s32_newf(hal_pin_dir_t dir, hal_s32_t **data_ptr_addr, int comp_id,
const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
const int rc = create_pin_newf<hal_s32_t, hal_pin_s32_new>(dir, data_ptr_addr, comp_id, fmt, ap);
va_end(ap);
return rc;
}
int hal_pin_u64_newf(hal_pin_dir_t dir, hal_u64_t **data_ptr_addr, int comp_id,
const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
const int rc = create_pin_newf<hal_u64_t, hal_pin_u64_new>(dir, data_ptr_addr, comp_id, fmt, ap);
va_end(ap);
return rc;
}
int hal_pin_s64_newf(hal_pin_dir_t dir, hal_s64_t **data_ptr_addr, int comp_id,
const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
const int rc = create_pin_newf<hal_s64_t, hal_pin_s64_new>(dir, data_ptr_addr, comp_id, fmt, ap);
va_end(ap);
return rc;
}
int hal_param_bit_new(const char *name, hal_param_dir_t dir, hal_bit_t *data_addr, int comp_id)
{
return create_param(name, dir, data_addr, comp_id, HAL_BIT);
}
int hal_param_float_new(const char *name, hal_param_dir_t dir, hal_float_t *data_addr,
int comp_id)
{
return create_param(name, dir, data_addr, comp_id, HAL_FLOAT);
}
int hal_param_u32_new(const char *name, hal_param_dir_t dir, hal_u32_t *data_addr, int comp_id)
{
return create_param(name, dir, data_addr, comp_id, HAL_U32);
}
int hal_param_s32_new(const char *name, hal_param_dir_t dir, hal_s32_t *data_addr, int comp_id)
{
return create_param(name, dir, data_addr, comp_id, HAL_S32);
}
int hal_param_u64_new(const char *name, hal_param_dir_t dir, hal_u64_t *data_addr, int comp_id)
{
return create_param(name, dir, data_addr, comp_id, HAL_U64);
}
int hal_param_s64_new(const char *name, hal_param_dir_t dir, hal_s64_t *data_addr, int comp_id)
{
return create_param(name, dir, data_addr, comp_id, HAL_S64);
}
int hal_param_bit_newf(hal_param_dir_t dir, hal_bit_t *data_addr, int comp_id,
const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
const int rc = create_param_newf<hal_bit_t, hal_param_bit_new>(dir, data_addr, comp_id, fmt, ap);
va_end(ap);
return rc;
}
int hal_param_float_newf(hal_param_dir_t dir, hal_float_t *data_addr, int comp_id,
const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
const int rc =
create_param_newf<hal_float_t, hal_param_float_new>(dir, data_addr, comp_id, fmt, ap);
va_end(ap);
return rc;
}
int hal_param_u32_newf(hal_param_dir_t dir, hal_u32_t *data_addr, int comp_id,
const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
const int rc = create_param_newf<hal_u32_t, hal_param_u32_new>(dir, data_addr, comp_id, fmt, ap);
va_end(ap);
return rc;
}
int hal_param_s32_newf(hal_param_dir_t dir, hal_s32_t *data_addr, int comp_id,
const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
const int rc = create_param_newf<hal_s32_t, hal_param_s32_new>(dir, data_addr, comp_id, fmt, ap);
va_end(ap);
return rc;
}
int hal_param_u64_newf(hal_param_dir_t dir, hal_u64_t *data_addr, int comp_id,
const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
const int rc = create_param_newf<hal_u64_t, hal_param_u64_new>(dir, data_addr, comp_id, fmt, ap);
va_end(ap);
return rc;
}
int hal_param_s64_newf(hal_param_dir_t dir, hal_s64_t *data_addr, int comp_id,
const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
const int rc = create_param_newf<hal_s64_t, hal_param_s64_new>(dir, data_addr, comp_id, fmt, ap);
va_end(ap);
return rc;
}
int hal_get_pin_value_by_name(const char *name, hal_type_t *type, hal_data_u **ptr,
bool *connected)
{
auto it = runtime().pins.find(name ? name : "");
if (it == runtime().pins.end() || !type || !ptr) {
return -1;
}
*type = it->second.type;
*ptr = value_ptr(it->second);
if (connected) {
*connected = it->second.connected;
}
return 0;
}
int hal_get_signal_value_by_name(const char *name, hal_type_t *type, hal_data_u **ptr,
bool *connected)
{
auto it = runtime().signals.find(name ? name : "");
if (it == runtime().signals.end() || !type || !ptr) {
return -1;
}
*type = it->second.type;
*ptr = value_ptr(it->second);
if (connected) {
*connected = it->second.connected;
}
return 0;
}
int hal_get_param_value_by_name(const char *name, hal_type_t *type, hal_data_u **ptr)
{
auto it = runtime().params.find(name ? name : "");
if (it == runtime().params.end() || !type || !ptr) {
return -1;
}
*type = it->second.type;
*ptr = value_ptr(it->second);
return 0;
}
int hal_link(const char *pin_name, const char *signal_name)
{
if (!pin_name || !signal_name) {
return -1;
}
return link_pin_to_signal(pin_name, signal_name);
}
int hal_unlink(const char *pin_name)
{
auto it = runtime().pins.find(pin_name ? pin_name : "");
if (it == runtime().pins.end()) {
return -1;
}
it->second.connected = false;
it->second.signal.clear();
runtime().events.push_back(std::string("unlink:") + pin_name);
return 0;
}
int hal_set_p(const char *name, const char *value)
{
return set_entry_value(name, value);
}
int hal_get_p(const char *name, char *out, int out_len)
{
HalEntry *entry = lookup_entry(name);
if (!entry) {
return -1;
}
return write_output(value_json(*entry), out, out_len);
}
int hal_create_thread(const char *name, unsigned long period_ns, int uses_fp)
{
if (!name) {
return -1;
}
auto &thread = runtime().threads[name];
thread.name = name;
thread.period_ns = period_ns;
thread.uses_fp = uses_fp;
runtime().events.push_back(std::string("thread_create:") + name);
return 0;
}
int hal_add_funct_to_thread(const char *funct_name, const char *thread_name, double position,
int uses_fp)
{
if (!funct_name || !thread_name) {
return -1;
}
auto thread_it = runtime().threads.find(thread_name);
if (thread_it == runtime().threads.end()) {
hal_create_thread(thread_name, 0, uses_fp);
thread_it = runtime().threads.find(thread_name);
}
thread_it->second.functions.push_back(HalThreadFunction{funct_name, position, uses_fp});
std::sort(thread_it->second.functions.begin(), thread_it->second.functions.end(),
[](const HalThreadFunction &a, const HalThreadFunction &b) {
return a.position < b.position;
});
runtime().events.push_back(std::string("addf:") + funct_name + ":" + thread_name);
return 0;
}
int hal_del_funct_from_thread(const char *funct_name, const char *thread_name)
{
auto thread_it = runtime().threads.find(thread_name ? thread_name : "");
if (thread_it == runtime().threads.end() || !funct_name) {
return -1;
}
auto &functions = thread_it->second.functions;
functions.erase(std::remove_if(functions.begin(), functions.end(),
[&](const HalThreadFunction &item) {
return item.name == funct_name;
}),
functions.end());
runtime().events.push_back(std::string("delf:") + funct_name + ":" + thread_name);
return 0;
}
int hal_start_threads(void)
{
runtime().threads_running = true;
runtime().events.push_back("threads_start");
return 0;
}
int hal_stop_threads(void)
{
runtime().threads_running = false;
runtime().events.push_back("threads_stop");
return 0;
}
int lchal_init_runtime(void)
{
clear_runtime();
const int comp_id = hal_init("linuxcnc-hal-runtime");
hal_ready(comp_id);
return 0;
}
int lchal_load_hal_file(const char *path, const char *text)
{
if (!text) {
return -1;
}
runtime().events.push_back(std::string("halfile:") + (path ? path : "<memory>"));
std::istringstream lines(text);
std::string line;
int blocked = 0;
int failed = 0;
while (std::getline(lines, line)) {
const auto words = split_words(line);
const int rc = load_hal_line(words);
if (rc == -2) {
blocked = 1;
} else if (rc != 0) {
failed = 1;
}
}
if (blocked) {
return 2;
}
return failed ? -1 : 0;
}
int lchal_set_pin_float(const char *name, double value)
{
char buf[64]{};
std::snprintf(buf, sizeof(buf), "%.17g", value);
return hal_set_p(name, buf);
}
int lchal_set_pin_s32(const char *name, int value)
{
char buf[64]{};
std::snprintf(buf, sizeof(buf), "%d", value);
return hal_set_p(name, buf);
}
int lchal_set_pin_bit(const char *name, int value)
{
return hal_set_p(name, value ? "1" : "0");
}
int lchal_get_pin_json(const char *name, char *out, int out_len)
{
auto it = runtime().pins.find(name ? name : "");
if (it == runtime().pins.end()) {
return -1;
}
return write_output(entry_json(it->second), out, out_len);
}
int lchal_get_snapshot_json(char *out, int out_len)
{
return write_output(snapshot_json(), out, out_len);
}
int lchal_step_threads(long period_ns, int cycles)
{
if (cycles < 0) {
return -1;
}
auto &state = runtime();
if (!state.threads_running) {
hal_start_threads();
}
for (int i = 0; i < cycles; ++i) {
state.cycle += 1;
for (const auto &[thread_name, thread] : state.threads) {
std::ostringstream event;
event << "cycle:" << state.cycle << ":" << thread_name << ":" << period_ns;
state.events.push_back(event.str());
for (const auto &function : thread.functions) {
state.events.push_back("call:" + thread_name + ":" + function.name);
}
}
}
return 0;
}
int lchal_reset_runtime(void)
{
clear_runtime();
return 0;
}
} // extern "C"

View File

@@ -0,0 +1,21 @@
#pragma once
#include "hal.h"
#ifdef __cplusplus
extern "C" {
#endif
int lchal_init_runtime(void);
int lchal_load_hal_file(const char *path, const char *text);
int lchal_set_pin_float(const char *name, double value);
int lchal_set_pin_s32(const char *name, int value);
int lchal_set_pin_bit(const char *name, int value);
int lchal_get_pin_json(const char *name, char *out, int out_len);
int lchal_get_snapshot_json(char *out, int out_len);
int lchal_step_threads(long period_ns, int cycles);
int lchal_reset_runtime(void);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,180 @@
#include "linuxcnc_hal_runtime.hh"
#include <cstdio>
#include <cstring>
#include <string>
namespace {
bool contains(const char *haystack, const char *needle)
{
return std::strstr(haystack, needle) != nullptr;
}
bool require(bool condition, const char *label)
{
if (!condition) {
std::fprintf(stderr, "hal_runtime_probe_failed=%s\n", label);
return false;
}
return true;
}
} // namespace
int main()
{
char buffer[32768]{};
if (!require(lchal_init_runtime() == 0, "init")) {
return 1;
}
const int comp_id = hal_init("probe");
hal_float_t *float_pin = nullptr;
hal_bit_t *bit_in = nullptr;
hal_bit_t *bit_out = nullptr;
hal_s32_t *line_pin = nullptr;
if (!require(hal_pin_float_new("motion.analog-out-03", HAL_OUT, &float_pin, comp_id) == 0,
"float_pin")) {
return 1;
}
if (!require(hal_pin_bit_new("probe.in", HAL_IN, &bit_in, comp_id) == 0, "bit_in")) {
return 1;
}
if (!require(hal_pin_bit_new("probe.out", HAL_OUT, &bit_out, comp_id) == 0, "bit_out")) {
return 1;
}
if (!require(hal_pin_s32_new("motion.program-line", HAL_OUT, &line_pin, comp_id) == 0,
"s32_pin")) {
return 1;
}
*float_pin = 1.25;
*line_pin = 42;
if (!require(lchal_get_pin_json("motion.analog-out-03", buffer, sizeof(buffer)) == 0,
"float_pin_json")) {
return 1;
}
if (!require(contains(buffer, "\"value\":1.25"), "float_pin_value")) {
return 1;
}
if (!require(hal_get_p("motion.analog-out-03", buffer, sizeof(buffer)) == 0,
"float_pin_getp")) {
return 1;
}
if (!require(contains(buffer, "1.25"), "float_pin_getp_value")) {
return 1;
}
hal_float_t param_float = 7.25;
hal_s32_t param_s32 = -12;
if (!require(hal_param_float_new("standalone.param-float", HAL_RW, &param_float, comp_id) == 0,
"param_float")) {
return 1;
}
if (!require(hal_param_s32_new("standalone.param-s32", HAL_RW, &param_s32, comp_id) == 0,
"param_s32")) {
return 1;
}
hal_type_t lookup_type = HAL_TYPE_UNINITIALIZED;
hal_data_u *lookup_value = nullptr;
bool lookup_connected = false;
if (!require(hal_get_pin_value_by_name("motion.program-line", &lookup_type, &lookup_value,
&lookup_connected) == 0,
"lookup_pin")) {
return 1;
}
if (!require(lookup_type == HAL_S32 && lookup_value != nullptr, "lookup_pin_type")) {
return 1;
}
if (!require(hal_link("probe.out", "probe-signal") == 0, "link_probe_out")) {
return 1;
}
if (!require(hal_link("probe.in", "probe-signal") == 0, "link_probe_in")) {
return 1;
}
if (!require(lchal_set_pin_bit("probe.out", 1) == 0, "set_probe_out")) {
return 1;
}
if (!require(*bit_in == true, "net_propagated_to_input")) {
return 1;
}
if (!require(hal_get_signal_value_by_name("probe-signal", &lookup_type, &lookup_value,
&lookup_connected) == 0,
"lookup_signal")) {
return 1;
}
if (!require(lookup_type == HAL_BIT && lookup_value != nullptr && lookup_connected,
"lookup_signal_type")) {
return 1;
}
if (!require(hal_create_thread("servo-thread", 1000000, 1) == 0, "create_thread")) {
return 1;
}
if (!require(hal_add_funct_to_thread("motion-command-handler", "servo-thread", 0.0, 1) == 0,
"addf_command")) {
return 1;
}
if (!require(hal_add_funct_to_thread("motion-controller", "servo-thread", 1.0, 1) == 0,
"addf_controller")) {
return 1;
}
if (!require(lchal_step_threads(1000000, 3) == 0, "step_threads")) {
return 1;
}
if (!require(hal_del_funct_from_thread("motion-command-handler", "servo-thread") == 0,
"del_thread_function")) {
return 1;
}
if (!require(hal_stop_threads() == 0, "stop_threads")) {
return 1;
}
if (!require(hal_start_threads() == 0, "start_threads")) {
return 1;
}
const char *hal_text =
"loadrt trivkins\n"
"addf kins servo-thread\n"
"loadusr external-user-m\n";
if (!require(lchal_load_hal_file("probe.hal", hal_text) == 2, "loadusr_blocked")) {
return 1;
}
if (!require(lchal_get_snapshot_json(buffer, sizeof(buffer)) == 0, "snapshot")) {
return 1;
}
if (!require(contains(buffer, "\"halRuntimeReady\":true"), "runtime_ready")) {
return 1;
}
if (!require(contains(buffer, "\"nativeHalSyncReady\":false"), "not_promoted")) {
return 1;
}
if (!require(contains(buffer, "\"probe.in\""), "snapshot_probe_in")) {
return 1;
}
if (!require(contains(buffer, "\"standalone.param-float\""), "snapshot_param_float")) {
return 1;
}
if (!require(contains(buffer, "call:servo-thread:motion-controller"), "thread_call_event")) {
return 1;
}
if (!require(contains(buffer, "delf:motion-command-handler:servo-thread"),
"thread_delete_event")) {
return 1;
}
if (!require(contains(buffer, "blocked:loadusr"), "blocked_event")) {
return 1;
}
std::puts("hal_runtime_registry=ok");
std::puts("hal_net_signal_propagation=ok");
std::puts("hal_thread_scheduler=ok");
std::puts("loadusr_blocked_evidence=ok");
std::puts("hal_runtime_probe=ok");
return 0;
}

View File

@@ -0,0 +1,464 @@
#include "linuxcnc_motion_runtime.h"
#include "hal.h"
#include "linuxcnc_hal_runtime.hh"
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
enum {
LCMOT_CMD_NONE = 0,
LCMOT_CMD_LINEAR_MOVE,
LCMOT_CMD_CIRCULAR_MOVE,
LCMOT_CMD_JOG_INCR,
LCMOT_CMD_PAUSE,
LCMOT_CMD_RESUME,
LCMOT_CMD_ABORT,
LCMOT_CMD_SET_AOUT,
};
typedef struct {
int type;
int line;
int axis_index;
double target[9];
double distance;
double velocity;
double analog_value;
} LcmotCommand;
typedef struct {
int initialized;
long long cycle;
int program_line;
int motion_type;
int coord_mode;
int teleop_mode;
int in_position;
int paused;
int aborted;
int switchkins_type;
double requested_vel;
double current_vel;
double analog_out_03;
double axis_cmd[9];
double axis_fb[9];
double joint_cmd[9];
double joint_fb[9];
LcmotCommand queue[64];
int queue_head;
int queue_tail;
int queue_count;
} LcmotRuntime;
static LcmotRuntime lcmot_state;
static hal_s32_t *hal_motion_program_line;
static hal_s32_t *hal_motion_motion_type;
static hal_s32_t *hal_motion_coord_mode;
static hal_s32_t *hal_motion_teleop_mode;
static hal_bit_t *hal_motion_in_position;
static hal_s32_t *hal_motion_switchkins_type;
static hal_float_t *hal_motion_analog_out_03;
static hal_float_t *hal_joint_cmd[9];
static hal_float_t *hal_joint_fb[9];
static hal_float_t *hal_axis_cmd[9];
static hal_float_t *hal_axis_fb[9];
static int write_output(const char *value, char *out, int out_len)
{
int required;
if (!value || !out || out_len <= 0) {
return -1;
}
required = (int)strlen(value) + 1;
if (out_len < required) {
out[0] = '\0';
return required;
}
memcpy(out, value, (size_t)required);
return 0;
}
static int contains_token(const char *json, const char *token)
{
return json && token && strstr(json, token) != NULL;
}
static double json_number_after(const char *json, const char *key, double fallback)
{
const char *at;
char *end = NULL;
if (!json || !key) {
return fallback;
}
at = strstr(json, key);
if (!at) {
return fallback;
}
at = strchr(at, ':');
if (!at) {
return fallback;
}
at += 1;
while (*at == ' ' || *at == '"') {
at += 1;
}
return strtod(at, &end);
}
static int json_int_after(const char *json, const char *key, int fallback)
{
return (int)json_number_after(json, key, fallback);
}
static int axis_index_from_json(const char *json)
{
const char *axis = strstr(json ? json : "", "\"axis\"");
if (!axis) {
return 0;
}
axis = strchr(axis, ':');
if (!axis) {
return 0;
}
while (*axis && *axis != '"') {
axis += 1;
}
if (*axis == '"') {
axis += 1;
}
switch (*axis) {
case 'X':
case 'x':
return 0;
case 'Y':
case 'y':
return 1;
case 'Z':
case 'z':
return 2;
case 'A':
case 'a':
return 3;
case 'B':
case 'b':
return 4;
case 'C':
case 'c':
return 5;
case 'U':
case 'u':
return 6;
case 'V':
case 'v':
return 7;
case 'W':
case 'w':
return 8;
default:
return 0;
}
}
static int queue_push(LcmotCommand command)
{
LcmotRuntime *state = &lcmot_state;
if (state->queue_count >= (int)(sizeof(state->queue) / sizeof(state->queue[0]))) {
return -1;
}
state->queue[state->queue_tail] = command;
state->queue_tail = (state->queue_tail + 1) % (int)(sizeof(state->queue) / sizeof(state->queue[0]));
state->queue_count += 1;
return 0;
}
static int queue_pop(LcmotCommand *command)
{
LcmotRuntime *state = &lcmot_state;
if (!command || state->queue_count <= 0) {
return 0;
}
*command = state->queue[state->queue_head];
state->queue_head = (state->queue_head + 1) % (int)(sizeof(state->queue) / sizeof(state->queue[0]));
state->queue_count -= 1;
return 1;
}
static void sync_hal_pins(void)
{
int i;
LcmotRuntime *state = &lcmot_state;
if (hal_motion_program_line) {
*hal_motion_program_line = state->program_line;
}
if (hal_motion_motion_type) {
*hal_motion_motion_type = state->motion_type;
}
if (hal_motion_coord_mode) {
*hal_motion_coord_mode = state->coord_mode;
}
if (hal_motion_teleop_mode) {
*hal_motion_teleop_mode = state->teleop_mode;
}
if (hal_motion_in_position) {
*hal_motion_in_position = state->in_position ? 1 : 0;
}
if (hal_motion_switchkins_type) {
*hal_motion_switchkins_type = state->switchkins_type;
}
if (hal_motion_analog_out_03) {
*hal_motion_analog_out_03 = state->analog_out_03;
}
for (i = 0; i < 9; ++i) {
if (hal_axis_cmd[i]) {
*hal_axis_cmd[i] = state->axis_cmd[i];
}
if (hal_axis_fb[i]) {
*hal_axis_fb[i] = state->axis_fb[i];
}
if (hal_joint_cmd[i]) {
*hal_joint_cmd[i] = state->joint_cmd[i];
}
if (hal_joint_fb[i]) {
*hal_joint_fb[i] = state->joint_fb[i];
}
}
}
static void create_motion_hal_pins(void)
{
int comp_id = hal_init("linuxcnc-motion-runtime");
int i;
char name[64];
hal_pin_s32_new("motion.program-line", HAL_OUT, &hal_motion_program_line, comp_id);
hal_pin_s32_new("motion.motion-type", HAL_OUT, &hal_motion_motion_type, comp_id);
hal_pin_s32_new("motion.coord-mode", HAL_OUT, &hal_motion_coord_mode, comp_id);
hal_pin_s32_new("motion.teleop-mode", HAL_OUT, &hal_motion_teleop_mode, comp_id);
hal_pin_bit_new("motion.in-position", HAL_OUT, &hal_motion_in_position, comp_id);
hal_pin_s32_new("motion.switchkins-type", HAL_IO, &hal_motion_switchkins_type, comp_id);
hal_pin_float_new("motion.analog-out-03", HAL_OUT, &hal_motion_analog_out_03, comp_id);
for (i = 0; i < 9; ++i) {
snprintf(name, sizeof(name), "joint.%d.motor-pos-cmd", i);
hal_pin_float_new(name, HAL_OUT, &hal_joint_cmd[i], comp_id);
snprintf(name, sizeof(name), "joint.%d.motor-pos-fb", i);
hal_pin_float_new(name, HAL_OUT, &hal_joint_fb[i], comp_id);
snprintf(name, sizeof(name), "axis.%d.pos-cmd", i);
hal_pin_float_new(name, HAL_OUT, &hal_axis_cmd[i], comp_id);
snprintf(name, sizeof(name), "axis.%d.pos-fb", i);
hal_pin_float_new(name, HAL_OUT, &hal_axis_fb[i], comp_id);
}
hal_ready(comp_id);
}
static void apply_command(const LcmotCommand *command)
{
int i;
LcmotRuntime *state = &lcmot_state;
if (!command) {
return;
}
switch (command->type) {
case LCMOT_CMD_PAUSE:
state->paused = 1;
state->motion_type = 0;
state->in_position = 0;
break;
case LCMOT_CMD_RESUME:
state->paused = 0;
break;
case LCMOT_CMD_ABORT:
state->aborted = 1;
state->paused = 0;
state->motion_type = 0;
state->in_position = 1;
state->queue_head = 0;
state->queue_tail = 0;
state->queue_count = 0;
break;
case LCMOT_CMD_SET_AOUT:
state->analog_out_03 = command->analog_value;
state->switchkins_type = (int)lrint(command->analog_value);
break;
case LCMOT_CMD_LINEAR_MOVE:
case LCMOT_CMD_CIRCULAR_MOVE:
state->program_line = command->line;
state->motion_type = command->type == LCMOT_CMD_CIRCULAR_MOVE ? 2 : 1;
state->coord_mode = 1;
state->teleop_mode = 0;
state->in_position = 0;
state->requested_vel = command->velocity > 0.0 ? command->velocity : 60.0;
for (i = 0; i < 9; ++i) {
state->axis_cmd[i] = command->target[i];
state->axis_fb[i] = command->target[i];
state->joint_cmd[i] = command->target[i];
state->joint_fb[i] = command->target[i];
}
state->current_vel = state->requested_vel;
state->in_position = 1;
state->motion_type = 0;
break;
case LCMOT_CMD_JOG_INCR:
i = command->axis_index;
if (i < 0 || i >= 9) {
i = 0;
}
state->motion_type = 3;
state->coord_mode = 0;
state->teleop_mode = 1;
state->in_position = 0;
state->axis_cmd[i] += command->distance;
state->axis_fb[i] = state->axis_cmd[i];
state->joint_cmd[i] = state->axis_cmd[i];
state->joint_fb[i] = state->axis_cmd[i];
state->requested_vel = command->velocity > 0.0 ? command->velocity : 60.0;
state->current_vel = state->requested_vel;
state->in_position = 1;
state->motion_type = 0;
break;
default:
break;
}
}
int lcmot_init_from_ini(const char *ini_path, const char *ini_text)
{
(void)ini_path;
(void)ini_text;
memset(&lcmot_state, 0, sizeof(lcmot_state));
lcmot_state.initialized = 1;
lcmot_state.in_position = 1;
lcmot_state.coord_mode = 1;
create_motion_hal_pins();
sync_hal_pins();
return 0;
}
int lcmot_write_command_json(const char *json)
{
int i;
LcmotCommand command;
if (!json || !lcmot_state.initialized) {
return -1;
}
memset(&command, 0, sizeof(command));
command.line = json_int_after(json, "\"line\"", lcmot_state.program_line);
command.velocity = json_number_after(json, "\"velocity\"", 60.0);
for (i = 0; i < 9; ++i) {
command.target[i] = lcmot_state.axis_cmd[i];
}
command.target[0] = json_number_after(json, "\"x\"", command.target[0]);
command.target[1] = json_number_after(json, "\"y\"", command.target[1]);
command.target[2] = json_number_after(json, "\"z\"", command.target[2]);
command.target[3] = json_number_after(json, "\"a\"", command.target[3]);
command.target[4] = json_number_after(json, "\"b\"", command.target[4]);
command.target[5] = json_number_after(json, "\"c\"", command.target[5]);
command.distance = json_number_after(json, "\"distance\"", 0.0);
command.axis_index = axis_index_from_json(json);
command.analog_value = json_number_after(json, "\"value\"", lcmot_state.analog_out_03);
if (contains_token(json, "EMC_TRAJ_LINEAR_MOVE") || contains_token(json, "EMCMOT_SET_LINE")) {
command.type = LCMOT_CMD_LINEAR_MOVE;
} else if (contains_token(json, "EMC_TRAJ_CIRCULAR_MOVE") || contains_token(json, "EMCMOT_SET_CIRCLE")) {
command.type = LCMOT_CMD_CIRCULAR_MOVE;
} else if (contains_token(json, "EMC_JOG_INCR") || contains_token(json, "EMCMOT_JOG_INCR")) {
command.type = LCMOT_CMD_JOG_INCR;
} else if (contains_token(json, "EMC_TRAJ_PAUSE") || contains_token(json, "EMCMOT_PAUSE")) {
command.type = LCMOT_CMD_PAUSE;
} else if (contains_token(json, "EMC_TRAJ_RESUME") || contains_token(json, "EMCMOT_RESUME")) {
command.type = LCMOT_CMD_RESUME;
} else if (contains_token(json, "EMC_TRAJ_ABORT") || contains_token(json, "EMCMOT_ABORT")) {
command.type = LCMOT_CMD_ABORT;
} else if (contains_token(json, "EMCMOT_SET_AOUT") || contains_token(json, "SET_AOUT")) {
command.type = LCMOT_CMD_SET_AOUT;
} else {
return -1;
}
return queue_push(command);
}
int lcmot_step_servo(long period_ns, int cycles)
{
int i;
LcmotCommand command;
if (!lcmot_state.initialized || cycles < 0) {
return -1;
}
for (i = 0; i < cycles; ++i) {
lcmot_state.cycle += 1;
if (!lcmot_state.aborted && lcmot_state.queue_count > 0) {
command = lcmot_state.queue[lcmot_state.queue_head];
if (!lcmot_state.paused ||
command.type == LCMOT_CMD_RESUME ||
command.type == LCMOT_CMD_ABORT) {
queue_pop(&command);
apply_command(&command);
}
}
sync_hal_pins();
lchal_step_threads(period_ns, 1);
}
return 0;
}
int lcmot_read_status_json(char *out, int out_len)
{
char buffer[4096];
LcmotRuntime *state = &lcmot_state;
snprintf(buffer, sizeof(buffer),
"{\"semanticBoundary\":\"linuxcnc_motion_runtime_phase3_minimal\","
"\"motionRuntimeReady\":true,"
"\"nativeHalSyncReady\":false,"
"\"cycle\":%lld,"
"\"motion\":{\"programLine\":%d,\"motionType\":%d,\"coordMode\":%d,"
"\"teleopMode\":%d,\"inPosition\":%s,\"paused\":%s,\"aborted\":%s,"
"\"switchkinsType\":%d,\"requestedVel\":%.17g,\"currentVel\":%.17g},"
"\"axis\":{\"x\":%.17g,\"y\":%.17g,\"z\":%.17g,\"a\":%.17g,\"b\":%.17g,\"c\":%.17g},"
"\"joint0\":{\"motorPosCmd\":%.17g,\"motorPosFb\":%.17g},"
"\"commandQueueDepth\":%d}",
state->cycle,
state->program_line,
state->motion_type,
state->coord_mode,
state->teleop_mode,
state->in_position ? "true" : "false",
state->paused ? "true" : "false",
state->aborted ? "true" : "false",
state->switchkins_type,
state->requested_vel,
state->current_vel,
state->axis_fb[0],
state->axis_fb[1],
state->axis_fb[2],
state->axis_fb[3],
state->axis_fb[4],
state->axis_fb[5],
state->joint_cmd[0],
state->joint_fb[0],
state->queue_count);
return write_output(buffer, out, out_len);
}
int lcmot_read_hal_snapshot_json(char *out, int out_len)
{
return lchal_get_snapshot_json(out, out_len);
}
int lcmot_reset(void)
{
memset(&lcmot_state, 0, sizeof(lcmot_state));
hal_motion_program_line = NULL;
hal_motion_motion_type = NULL;
hal_motion_coord_mode = NULL;
hal_motion_teleop_mode = NULL;
hal_motion_in_position = NULL;
hal_motion_switchkins_type = NULL;
hal_motion_analog_out_03 = NULL;
memset(hal_joint_cmd, 0, sizeof(hal_joint_cmd));
memset(hal_joint_fb, 0, sizeof(hal_joint_fb));
memset(hal_axis_cmd, 0, sizeof(hal_axis_cmd));
memset(hal_axis_fb, 0, sizeof(hal_axis_fb));
lchal_reset_runtime();
return 0;
}

View File

@@ -0,0 +1,16 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
int lcmot_init_from_ini(const char *ini_path, const char *ini_text);
int lcmot_write_command_json(const char *json);
int lcmot_step_servo(long period_ns, int cycles);
int lcmot_read_status_json(char *out, int out_len);
int lcmot_read_hal_snapshot_json(char *out, int out_len);
int lcmot_reset(void);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,467 @@
#include "linuxcnc_task_hal_wasm.hh"
#include "linuxcnc_motion_runtime.h"
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <map>
#include <sstream>
#include <string>
#include <vector>
namespace {
struct TaskRuntime {
bool initialized = false;
std::string state = "ESTOP";
std::string mode = "MANUAL";
std::string interp_state = "IDLE";
std::string exec_state = "DONE";
std::string open_program;
int opened_line_count = 0;
int next_program_line = 0;
long long task_cycle = 0;
long long servo_cycle = 0;
std::map<std::string, std::string> staged_files;
std::vector<std::string> program_lines;
std::vector<std::string> events;
};
TaskRuntime &task_runtime()
{
static TaskRuntime state;
return state;
}
std::string json_escape(const std::string &value)
{
std::ostringstream out;
for (const char ch : value) {
switch (ch) {
case '\\':
out << "\\\\";
break;
case '"':
out << "\\\"";
break;
case '\n':
out << "\\n";
break;
case '\r':
out << "\\r";
break;
case '\t':
out << "\\t";
break;
default:
out << ch;
break;
}
}
return out.str();
}
int write_output(const std::string &value, char *out, int out_len)
{
if (!out || out_len <= 0) {
return -1;
}
const int required = static_cast<int>(value.size()) + 1;
if (out_len < required) {
out[0] = '\0';
return required;
}
std::memcpy(out, value.c_str(), static_cast<std::size_t>(required));
return 0;
}
bool contains_token(const char *json, const char *token)
{
return json && token && std::strstr(json, token) != nullptr;
}
std::string json_string_after(const char *json, const char *key, const std::string &fallback = "")
{
const char *at = std::strstr(json ? json : "", key);
if (!at) {
return fallback;
}
at = std::strchr(at, ':');
if (!at) {
return fallback;
}
at += 1;
while (*at && std::isspace(static_cast<unsigned char>(*at))) {
at += 1;
}
if (*at != '"') {
return fallback;
}
at += 1;
std::string value;
while (*at) {
if (*at == '\\' && at[1]) {
value.push_back(at[1]);
at += 2;
continue;
}
if (*at == '"') {
return value;
}
value.push_back(*at);
at += 1;
}
return fallback;
}
double json_number_after(const char *json, const char *key, double fallback)
{
const char *at = std::strstr(json ? json : "", key);
char *end = nullptr;
if (!at) {
return fallback;
}
at = std::strchr(at, ':');
if (!at) {
return fallback;
}
at += 1;
while (*at && (std::isspace(static_cast<unsigned char>(*at)) || *at == '"')) {
at += 1;
}
const double value = std::strtod(at, &end);
return end == at ? fallback : value;
}
int json_int_after(const char *json, const char *key, int fallback)
{
return static_cast<int>(json_number_after(json, key, fallback));
}
std::string trim_copy(const std::string &value)
{
std::size_t begin = 0;
while (begin < value.size() && std::isspace(static_cast<unsigned char>(value[begin]))) {
begin += 1;
}
std::size_t end = value.size();
while (end > begin && std::isspace(static_cast<unsigned char>(value[end - 1]))) {
end -= 1;
}
return value.substr(begin, end - begin);
}
std::vector<std::string> split_program_lines(const std::string &text)
{
std::vector<std::string> lines;
std::istringstream input(text);
std::string line;
while (std::getline(input, line)) {
line = trim_copy(line);
if (line.empty() || line[0] == '(' || line[0] == ';') {
continue;
}
lines.push_back(line);
}
return lines;
}
int forward_motion_command(const std::string &json)
{
return lcmot_write_command_json(json.c_str());
}
void enqueue_linear_move_from_line(TaskRuntime &state, const std::string &line)
{
std::ostringstream command;
const int line_number = state.next_program_line + 1;
command << "{\"type\":\"EMC_TRAJ_LINEAR_MOVE\",\"line\":" << line_number;
double fallback = 0.0;
const char *axes = "XYZABC";
for (const char *axis = axes; *axis; ++axis) {
const std::size_t pos = line.find(*axis);
if (pos == std::string::npos) {
continue;
}
char *end = nullptr;
const double value = std::strtod(line.c_str() + pos + 1, &end);
if (end != line.c_str() + pos + 1) {
command << ",\"" << static_cast<char>(std::tolower(*axis)) << "\":" << value;
fallback = value;
}
}
if (line.find('X') == std::string::npos && line.find('Y') == std::string::npos &&
line.find('Z') == std::string::npos && line.find('A') == std::string::npos &&
line.find('B') == std::string::npos && line.find('C') == std::string::npos) {
command << ",\"x\":" << fallback;
}
command << ",\"velocity\":60}";
forward_motion_command(command.str());
state.events.push_back("task_queue_motion_line:" + std::to_string(line_number));
state.next_program_line += 1;
}
void enqueue_mdi(TaskRuntime &state, const char *json)
{
const std::string mdi = json_string_after(json, "\"mdi\"");
if (mdi.find("M428") != std::string::npos) {
forward_motion_command("{\"type\":\"EMCMOT_SET_AOUT\",\"value\":1}");
state.events.push_back("task_mdi_switchkins:M428");
return;
}
if (mdi.find("M429") != std::string::npos) {
forward_motion_command("{\"type\":\"EMCMOT_SET_AOUT\",\"value\":0}");
state.events.push_back("task_mdi_switchkins:M429");
return;
}
if (mdi.find("M430") != std::string::npos) {
forward_motion_command("{\"type\":\"EMCMOT_SET_AOUT\",\"value\":2}");
state.events.push_back("task_mdi_switchkins:M430");
return;
}
enqueue_linear_move_from_line(state, mdi.empty() ? "G0 X0" : mdi);
state.events.push_back("task_mdi_execute");
}
std::string read_motion_status_json()
{
std::vector<char> buffer(8192);
const int rc = lcmot_read_status_json(buffer.data(), static_cast<int>(buffer.size()));
if (rc != 0) {
return "{}";
}
return buffer.data();
}
std::string read_hal_snapshot_json()
{
std::vector<char> buffer(65536);
const int rc = lcmot_read_hal_snapshot_json(buffer.data(), static_cast<int>(buffer.size()));
if (rc != 0) {
return "{}";
}
return buffer.data();
}
std::string status_json()
{
const auto &state = task_runtime();
std::ostringstream out;
out << "{\"semanticBoundary\":\"linuxcnc_task_motion_hal_wasm_phase4_minimal\"";
out << ",\"taskRuntimeReady\":true";
out << ",\"taskStatusFromLinuxCncRuntime\":true";
out << ",\"taskCommandsDriveMotionRuntime\":true";
out << ",\"nativeTaskReady\":false";
out << ",\"nativeHalSyncReady\":false";
out << ",\"fullLinuxCncProgramExecutionReady\":false";
out << ",\"hardwareDrive\":false";
out << ",\"task\":{\"state\":\"" << json_escape(state.state) << "\"";
out << ",\"mode\":\"" << json_escape(state.mode) << "\"";
out << ",\"interpState\":\"" << json_escape(state.interp_state) << "\"";
out << ",\"execState\":\"" << json_escape(state.exec_state) << "\"";
out << ",\"cycle\":" << state.task_cycle;
out << ",\"openProgram\":\"" << json_escape(state.open_program) << "\"";
out << ",\"openedLineCount\":" << state.opened_line_count;
out << ",\"nextProgramLine\":" << state.next_program_line << "}";
out << ",\"servoCycle\":" << state.servo_cycle;
out << ",\"motionStatus\":" << read_motion_status_json();
out << ",\"halSnapshot\":" << read_hal_snapshot_json();
out << "}";
return out.str();
}
std::string events_json()
{
const auto &state = task_runtime();
std::ostringstream out;
out << "{\"events\":[";
for (std::size_t i = 0; i < state.events.size(); ++i) {
if (i > 0) {
out << ",";
}
out << "\"" << json_escape(state.events[i]) << "\"";
}
out << "]}";
return out.str();
}
void reset_task_only()
{
task_runtime() = TaskRuntime{};
}
} // namespace
extern "C" {
int lctask_init_session(const char *session_json)
{
reset_task_only();
auto &state = task_runtime();
state.initialized = true;
state.state = "ESTOP_RESET";
state.mode = "MANUAL";
state.interp_state = "IDLE";
state.exec_state = "DONE";
state.events.push_back("task_session_init");
const std::string ini_path = json_string_after(session_json, "\"iniPath\"", "task-hal-session.ini");
const std::string ini_text = json_string_after(session_json, "\"iniText\"", "");
if (lcmot_init_from_ini(ini_path.c_str(), ini_text.c_str()) != 0) {
state.events.push_back("task_session_motion_init_failed");
return -1;
}
state.events.push_back("task_session_motion_init");
return 0;
}
int lctask_stage_file(const char *path, const char *text)
{
auto &state = task_runtime();
if (!state.initialized || !path || !text) {
return -1;
}
state.staged_files[path] = text;
state.events.push_back(std::string("task_stage_file:") + path);
return 0;
}
int lctask_open_program(const char *path)
{
auto &state = task_runtime();
if (!state.initialized || !path) {
return -1;
}
const auto it = state.staged_files.find(path);
if (it == state.staged_files.end()) {
return -1;
}
state.open_program = path;
state.program_lines = split_program_lines(it->second);
state.opened_line_count = static_cast<int>(state.program_lines.size());
state.next_program_line = 0;
state.interp_state = "IDLE";
state.exec_state = "DONE";
state.events.push_back(std::string("task_open_program:") + path);
return 0;
}
int lctask_send_command_json(const char *command_json)
{
auto &state = task_runtime();
if (!state.initialized || !command_json) {
return -1;
}
if (contains_token(command_json, "EMC_TASK_SET_STATE")) {
state.state = json_string_after(command_json, "\"state\"", state.state);
state.events.push_back("task_set_state:" + state.state);
return 0;
}
if (contains_token(command_json, "EMC_TASK_SET_MODE")) {
state.mode = json_string_after(command_json, "\"mode\"", state.mode);
state.events.push_back("task_set_mode:" + state.mode);
return 0;
}
if (contains_token(command_json, "EMC_TASK_PLAN_RUN")) {
if (state.open_program.empty()) {
return -1;
}
state.next_program_line = json_int_after(command_json, "\"line\"", 0);
if (state.next_program_line < 0) {
state.next_program_line = 0;
}
if (state.next_program_line >= state.opened_line_count) {
state.next_program_line = 0;
}
state.interp_state = "READING";
state.exec_state = "WAITING_FOR_MOTION";
state.events.push_back("task_plan_run");
return 0;
}
if (contains_token(command_json, "EMC_TASK_PLAN_PAUSE")) {
state.interp_state = "PAUSED";
state.exec_state = "PAUSED";
state.events.push_back("task_plan_pause");
return forward_motion_command("{\"type\":\"EMC_TRAJ_PAUSE\"}");
}
if (contains_token(command_json, "EMC_TASK_PLAN_RESUME")) {
state.interp_state = "READING";
state.exec_state = "WAITING_FOR_MOTION";
state.events.push_back("task_plan_resume");
return forward_motion_command("{\"type\":\"EMC_TRAJ_RESUME\"}");
}
if (contains_token(command_json, "EMC_TASK_ABORT")) {
state.interp_state = "IDLE";
state.exec_state = "DONE";
state.events.push_back("task_abort");
return forward_motion_command("{\"type\":\"EMC_TRAJ_ABORT\"}");
}
if (contains_token(command_json, "EMC_TASK_PLAN_EXECUTE")) {
state.mode = "MDI";
state.interp_state = "READING";
state.exec_state = "WAITING_FOR_MOTION";
enqueue_mdi(state, command_json);
return 0;
}
if (contains_token(command_json, "EMC_JOG_INCR")) {
state.mode = "MANUAL";
state.interp_state = "IDLE";
state.exec_state = "WAITING_FOR_MOTION";
state.events.push_back("task_jog_incr");
return forward_motion_command(command_json);
}
return -1;
}
int lctask_run_cycles(long task_period_ns, long servo_period_ns, int task_cycles)
{
auto &state = task_runtime();
if (!state.initialized || task_cycles < 0 || servo_period_ns <= 0) {
return -1;
}
int servo_per_task = 1;
if (task_period_ns > 0) {
servo_per_task = static_cast<int>(task_period_ns / servo_period_ns);
if (servo_per_task <= 0) {
servo_per_task = 1;
}
}
for (int i = 0; i < task_cycles; ++i) {
state.task_cycle += 1;
if (state.interp_state == "READING" && state.next_program_line < state.opened_line_count) {
enqueue_linear_move_from_line(state, state.program_lines[state.next_program_line]);
if (state.next_program_line >= state.opened_line_count) {
state.interp_state = "IDLE";
state.exec_state = "DONE";
state.events.push_back("task_plan_complete");
}
}
if (lcmot_step_servo(servo_period_ns, servo_per_task) != 0) {
return -1;
}
state.servo_cycle += servo_per_task;
}
return 0;
}
int lctask_read_status_json(char *out, int out_len)
{
return write_output(status_json(), out, out_len);
}
int lctask_read_events_json(char *out, int out_len)
{
return write_output(events_json(), out, out_len);
}
int lctask_reset_session(void)
{
reset_task_only();
lcmot_reset();
return 0;
}
} // extern "C"

View File

@@ -0,0 +1,18 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
int lctask_init_session(const char *session_json);
int lctask_stage_file(const char *path, const char *text);
int lctask_open_program(const char *path);
int lctask_send_command_json(const char *command_json);
int lctask_run_cycles(long task_period_ns, long servo_period_ns, int task_cycles);
int lctask_read_status_json(char *out, int out_len);
int lctask_read_events_json(char *out, int out_len);
int lctask_reset_session(void);
#ifdef __cplusplus
}
#endif

View File

@@ -21,9 +21,13 @@ typedef enum {
HAL_IN = 16, HAL_IN = 16,
HAL_OUT = 32, HAL_OUT = 32,
HAL_IO = (HAL_IN | HAL_OUT), HAL_IO = (HAL_IN | HAL_OUT),
HAL_RW = HAL_IO,
} hal_pin_dir_t; } hal_pin_dir_t;
typedef enum {
HAL_RO = 64,
HAL_RW = HAL_RO | 128,
} hal_param_dir_t;
typedef union { typedef union {
bool b; bool b;
double f; double f;
@@ -59,9 +63,29 @@ int hal_pin_u32_newf(hal_pin_dir_t, hal_u32_t **, int, const char *, ...);
int hal_pin_s32_newf(hal_pin_dir_t, hal_s32_t **, int, const char *, ...); int hal_pin_s32_newf(hal_pin_dir_t, hal_s32_t **, int, const char *, ...);
int hal_pin_u64_newf(hal_pin_dir_t, hal_u64_t **, int, const char *, ...); int hal_pin_u64_newf(hal_pin_dir_t, hal_u64_t **, int, const char *, ...);
int hal_pin_s64_newf(hal_pin_dir_t, hal_s64_t **, int, const char *, ...); int hal_pin_s64_newf(hal_pin_dir_t, hal_s64_t **, int, const char *, ...);
int hal_param_float_newf(hal_pin_dir_t, hal_float_t *, int, const char *, ...); int hal_param_bit_new(const char *, hal_param_dir_t, hal_bit_t *, int);
int hal_param_float_new(const char *, hal_param_dir_t, hal_float_t *, int);
int hal_param_u32_new(const char *, hal_param_dir_t, hal_u32_t *, int);
int hal_param_s32_new(const char *, hal_param_dir_t, hal_s32_t *, int);
int hal_param_u64_new(const char *, hal_param_dir_t, hal_u64_t *, int);
int hal_param_s64_new(const char *, hal_param_dir_t, hal_s64_t *, int);
int hal_param_bit_newf(hal_param_dir_t, hal_bit_t *, int, const char *, ...);
int hal_param_float_newf(hal_param_dir_t, hal_float_t *, int, const char *, ...);
int hal_param_u32_newf(hal_param_dir_t, hal_u32_t *, int, const char *, ...);
int hal_param_s32_newf(hal_param_dir_t, hal_s32_t *, int, const char *, ...);
int hal_param_u64_newf(hal_param_dir_t, hal_u64_t *, int, const char *, ...);
int hal_param_s64_newf(hal_param_dir_t, hal_s64_t *, int, const char *, ...);
int hal_get_pin_value_by_name(const char *, hal_type_t *, hal_data_u **, bool *); int hal_get_pin_value_by_name(const char *, hal_type_t *, hal_data_u **, bool *);
int hal_get_signal_value_by_name(const char *, hal_type_t *, hal_data_u **, bool *); int hal_get_signal_value_by_name(const char *, hal_type_t *, hal_data_u **, bool *);
int hal_get_param_value_by_name(const char *, hal_type_t *, hal_data_u **); int hal_get_param_value_by_name(const char *, hal_type_t *, hal_data_u **);
int hal_link(const char *, const char *);
int hal_unlink(const char *);
int hal_set_p(const char *, const char *);
int hal_get_p(const char *, char *, int);
int hal_create_thread(const char *, unsigned long, int);
int hal_add_funct_to_thread(const char *, const char *, double, int);
int hal_del_funct_from_thread(const char *, const char *);
int hal_start_threads(void);
int hal_stop_threads(void);
RTAPI_END_DECLS RTAPI_END_DECLS

View File

@@ -1,5 +1,6 @@
export { createLinuxCncIniSdk } from "./linuxcnc-ini.js"; export { createLinuxCncIniSdk } from "./linuxcnc-ini.js";
export { createLinuxCncInterpSdk } from "./linuxcnc-interp.js"; export { createLinuxCncInterpSdk } from "./linuxcnc-interp.js";
export { createLinuxCncTaskHalSdk } from "./linuxcnc-task-hal.js";
export { export {
createLinuxCncKinematicsSdk, createLinuxCncKinematicsSdk,
linuxCncKinematicsWasmFile, linuxCncKinematicsWasmFile,

View File

@@ -0,0 +1,128 @@
import createLinuxCncTaskHalModule from "../../../build/wasm/task-hal/linuxcnc_task_hal.js";
const SEMANTIC_BOUNDARY = "linuxcnc_task_motion_hal_wasm_phase4_minimal";
function allocCString(mod, value) {
const text = String(value ?? "");
const bytes = mod.lengthBytesUTF8(text) + 1;
const ptr = mod._malloc(bytes);
mod.stringToUTF8(text, ptr, bytes);
return ptr;
}
function withCString(mod, value, fn) {
const ptr = allocCString(mod, value);
try {
return fn(ptr);
} finally {
mod._free(ptr);
}
}
function requireWasmFunction(mod, functionName) {
const fn = mod[`_${functionName}`];
if (typeof fn !== "function") {
throw new Error(`linuxcnc task/HAL WASM missing ${functionName}; rebuild wasm-port/tools/build_task_hal_wasm.sh`);
}
return fn;
}
function readJson(mod, functionName) {
const fn = requireWasmFunction(mod, functionName);
let bytes = 131072;
for (let attempt = 0; attempt < 2; attempt += 1) {
const ptr = mod._malloc(bytes);
try {
const rc = fn(ptr, bytes);
if (rc === 0) {
return JSON.parse(mod.UTF8ToString(ptr));
}
if (rc > bytes) {
bytes = rc;
continue;
}
throw new Error(`${functionName} failed with rc=${rc}`);
} finally {
mod._free(ptr);
}
}
throw new Error(`${functionName} output exceeded buffer`);
}
function callWithJson(mod, functionName, payload) {
const fn = requireWasmFunction(mod, functionName);
return withCString(mod, JSON.stringify(payload ?? {}), (ptr) => fn(ptr));
}
export async function createLinuxCncTaskHalSdk(moduleOptions = {}) {
const mod = await createLinuxCncTaskHalModule(moduleOptions);
return {
apiName: "linuxcnc-task-hal-wasm-sdk",
semanticBoundary: SEMANTIC_BOUNDARY,
module: mod,
readiness() {
return {
apiName: "linuxcnc-task-hal-wasm-sdk-readiness",
loaded: true,
semanticBoundary: SEMANTIC_BOUNDARY,
taskRuntimeReady: typeof mod._lctask_init_session === "function",
motionRuntimeReady: typeof mod._lcmot_step_servo === "function",
halRuntimeReady: typeof mod._lchal_get_snapshot_json === "function",
nativeTaskReady: false,
nativeHalSyncReady: false,
};
},
initSession(session = {}) {
const rc = callWithJson(mod, "lctask_init_session", session);
if (rc !== 0) {
throw new Error(`lctask_init_session failed with rc=${rc}`);
}
return rc;
},
stageFile(path, text) {
return withCString(mod, path, (pathPtr) =>
withCString(mod, text, (textPtr) =>
requireWasmFunction(mod, "lctask_stage_file")(pathPtr, textPtr),
),
);
},
openProgram(path) {
return withCString(mod, path, (pathPtr) =>
requireWasmFunction(mod, "lctask_open_program")(pathPtr),
);
},
sendCommand(command) {
return callWithJson(mod, "lctask_send_command_json", command);
},
runCycles({
taskPeriodNs = 10000000,
servoPeriodNs = 1000000,
taskCycles = 1,
} = {}) {
return requireWasmFunction(mod, "lctask_run_cycles")(
Number(taskPeriodNs) || 10000000,
Number(servoPeriodNs) || 1000000,
Number(taskCycles) || 0,
);
},
readStatus() {
return readJson(mod, "lctask_read_status_json");
},
readEvents() {
return readJson(mod, "lctask_read_events_json");
},
resetSession() {
return requireWasmFunction(mod, "lctask_reset_session")();
},
};
}

View File

@@ -0,0 +1,234 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
LINUXCNC_ROOT="${LINUXCNC_ROOT:-$ROOT_DIR/../linuxcnc}"
MACHINE_DIR="$LINUXCNC_ROOT/configs/sim/axis/vismach/5axis/table-rotary-tilting"
INI_FILE="$MACHINE_DIR/xyzac-trt.ini"
PROGRAM_FILE="$MACHINE_DIR/demos/xyzac_switchkins.ngc"
RUN_DIR="$ROOT_DIR/build/native/trt-task-hal-runtime"
LINUXCNC_STDOUT="$RUN_DIR/linuxcnc.stdout.log"
LINUXCNC_STDERR="$RUN_DIR/linuxcnc.stderr.log"
LINUXCNC_PID=""
required_commands=(halcmd linuxcnc)
missing_commands=()
declare -A command_paths=()
cleanup() {
if [[ -n "$LINUXCNC_PID" ]]; then
kill -TERM "-$LINUXCNC_PID" 2>/dev/null || kill "$LINUXCNC_PID" 2>/dev/null || true
for _ in $(seq 1 20); do
if ! kill -0 "$LINUXCNC_PID" 2>/dev/null; then
wait "$LINUXCNC_PID" 2>/dev/null || true
LINUXCNC_PID=""
return
fi
sleep 0.1
done
kill -KILL "-$LINUXCNC_PID" 2>/dev/null || kill -KILL "$LINUXCNC_PID" 2>/dev/null || true
wait "$LINUXCNC_PID" 2>/dev/null || true
LINUXCNC_PID=""
fi
}
trap cleanup EXIT
print_kv() {
printf '%s=%s\n' "$1" "$2"
}
csv_join() {
local IFS=,
printf '%s' "$*"
}
command_path() {
local command_name="$1"
local path
path="$(command -v "$command_name" 2>/dev/null || true)"
if [[ -n "$path" ]]; then
printf '%s\n' "$path"
return
fi
case "$command_name" in
linuxcnc)
[[ -x "$LINUXCNC_ROOT/scripts/$command_name" ]] && printf '%s\n' "$LINUXCNC_ROOT/scripts/$command_name"
;;
halcmd)
[[ -x "$LINUXCNC_ROOT/bin/$command_name" ]] && printf '%s\n' "$LINUXCNC_ROOT/bin/$command_name"
;;
esac
}
existing_linuxcnc_runtime_conflicts() {
ps -eo pid=,args= | awk '
/linuxcncsvr -ini/ || /rtapi_app load/ {
if ($0 !~ /awk /) {
gsub(/^[[:space:]]+/, "", $0)
print
}
}
'
}
source_ready=1
for rel in \
xyzac-trt.ini \
xyzac-trt.tbl \
xyzac-trt_cmds.hal \
switchkins_postgui.hal \
remap_subs/428remap.ngc \
remap_subs/429remap.ngc \
remap_subs/430remap.ngc \
demos/xyzac_switchkins.ngc; do
if [[ ! -f "$MACHINE_DIR/$rel" ]]; then
source_ready=0
print_kv "trt_task_hal_missing_source_${rel//[^A-Za-z0-9]/_}" "$MACHINE_DIR/$rel"
fi
done
runtime_requirements=()
for command_name in "${required_commands[@]}"; do
path="$(command_path "$command_name")"
if [[ -n "$path" ]]; then
command_paths["$command_name"]="$path"
runtime_requirements+=("$command_name:1")
print_kv "trt_task_hal_runtime_${command_name}_path" "$path"
else
runtime_requirements+=("$command_name:0")
missing_commands+=("$command_name")
fi
done
runtime_ready=0
if [[ "${#missing_commands[@]}" -eq 0 ]]; then
runtime_ready=1
fi
print_kv trt_task_hal_runtime_requirements "$(csv_join "${runtime_requirements[@]}")"
if [[ "${#missing_commands[@]}" -eq 0 ]]; then
print_kv trt_task_hal_missing_requirements "-"
else
print_kv trt_task_hal_missing_requirements "$(csv_join "${missing_commands[@]}")"
fi
print_kv trt_task_hal_source_proof_ready "$source_ready"
print_kv trt_task_hal_runtime_ready "$runtime_ready"
print_kv trt_task_hal_execution_enabled 0
print_kv trt_task_hal_promotion_allowed 0
print_kv nativeTaskReady false
print_kv nativeHalSyncReady false
if [[ "$source_ready" != "1" ]]; then
print_kv native_task_hal_probe "blocked"
print_kv native_probe_status "blocked_missing_source"
print_kv trt_task_hal_runtime_probe_status "blocked_missing_source"
print_kv trt_task_hal_runtime_probe_note "missing_trt_source_requirements_keep_task_hal_blocked"
exit 0
fi
if [[ "$runtime_ready" != "1" ]]; then
print_kv native_task_hal_probe "ok"
print_kv native_probe_status "skipped_missing_host_runtime"
print_kv trt_task_hal_runtime_probe_status "skipped_missing_host_runtime"
print_kv trt_task_hal_runtime_probe_note "missing_host_runtime_requirements_keep_task_hal_blocked"
exit 0
fi
if [[ "${ENABLE_TRT_TASK_HAL_RUNTIME_PROBE:-0}" != "1" ]]; then
print_kv native_task_hal_probe "ok"
print_kv native_probe_status "ready_disabled_by_default"
print_kv trt_task_hal_runtime_probe_status "ready_disabled_by_default"
print_kv trt_task_hal_runtime_probe_note "set_ENABLE_TRT_TASK_HAL_RUNTIME_PROBE_1_to_run_exclusive_host_runtime_probe"
exit 0
fi
existing_runtime_conflicts="$(existing_linuxcnc_runtime_conflicts)"
if [[ -n "$existing_runtime_conflicts" ]]; then
print_kv trt_task_hal_runtime_ready 0
print_kv trt_task_hal_missing_requirements "exclusive_linuxcnc_runtime"
print_kv trt_task_hal_existing_runtime_conflicts "$(printf '%s\n' "$existing_runtime_conflicts" | paste -sd ';' -)"
print_kv native_task_hal_probe "blocked"
print_kv native_probe_status "blocked_existing_linuxcnc_runtime"
print_kv trt_task_hal_runtime_probe_status "blocked_existing_linuxcnc_runtime"
print_kv trt_task_hal_runtime_probe_note "existing_linuxcnc_runtime_conflict_keep_task_hal_blocked"
exit 0
fi
HALCMD_BIN="${command_paths[halcmd]}"
LINUXCNC_BIN="${command_paths[linuxcnc]}"
export PATH="$LINUXCNC_ROOT/bin:$LINUXCNC_ROOT/scripts:$LINUXCNC_ROOT/tcl:$PATH"
export LD_LIBRARY_PATH="$LINUXCNC_ROOT/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
export PYTHONPATH="$LINUXCNC_ROOT/lib/python${PYTHONPATH:+:$PYTHONPATH}"
export TCLLIBPATH="$LINUXCNC_ROOT/tcl${TCLLIBPATH:+ $TCLLIBPATH}"
mkdir -p "$RUN_DIR"
wait_for_hal_pin() {
local pin="$1"
local limit="${TRT_TASK_HAL_RUNTIME_WAIT_SECONDS:-30}"
local end=$((SECONDS + limit))
while (( SECONDS < end )); do
if "$HALCMD_BIN" getp "$pin" >/dev/null 2>&1; then
return 0
fi
sleep 0.25
done
return 1
}
read_hal_pin() {
"$HALCMD_BIN" getp "$1" 2>/dev/null | tr -d '[:space:]'
}
expect_hal_pin_exists() {
local label="$1"
local pin="$2"
if "$HALCMD_BIN" getp "$pin" >/dev/null 2>&1; then
print_kv "trt_task_hal_${label}_${pin//[^A-Za-z0-9]/_}_exists" 1
print_kv "trt_task_hal_${label}_${pin//[^A-Za-z0-9]/_}_value" "$(read_hal_pin "$pin")"
return 0
fi
print_kv "trt_task_hal_${label}_${pin//[^A-Za-z0-9]/_}_exists" 0
return 1
}
print_kv trt_task_hal_runtime_probe_ini "$INI_FILE"
print_kv trt_task_hal_runtime_probe_program "$PROGRAM_FILE"
print_kv trt_task_hal_runtime_probe_linuxcnc_stdout "$LINUXCNC_STDOUT"
print_kv trt_task_hal_runtime_probe_linuxcnc_stderr "$LINUXCNC_STDERR"
setsid "$LINUXCNC_BIN" -r "$INI_FILE" >"$LINUXCNC_STDOUT" 2>"$LINUXCNC_STDERR" &
LINUXCNC_PID="$!"
if ! wait_for_hal_pin motion.switchkins-type || ! wait_for_hal_pin motion.program-line; then
print_kv native_task_hal_probe "failed"
print_kv native_probe_status "failed"
print_kv trt_task_hal_runtime_probe_status "runtime_state_probe_failed"
print_kv trt_task_hal_runtime_probe_note "linuxcnc_started_but_required_trt_hal_pins_did_not_appear"
exit 1
fi
probe_ok=1
expect_hal_pin_exists startup motion.switchkins-type || probe_ok=0
expect_hal_pin_exists startup motion.program-line || probe_ok=0
expect_hal_pin_exists startup motion.motion-type || probe_ok=0
expect_hal_pin_exists startup motion.in-position || probe_ok=0
expect_hal_pin_exists startup joint.0.motor-pos-cmd || probe_ok=0
expect_hal_pin_exists startup joint.0.motor-pos-fb || probe_ok=0
expect_hal_pin_exists startup xyzac-trt-kins.tool-offset || true
if [[ "$probe_ok" == "1" ]]; then
print_kv native_task_hal_probe "ok"
print_kv native_probe_status "passed"
print_kv trt_task_hal_runtime_probe_status "runtime_state_probe_passed"
print_kv trt_task_hal_runtime_probe_note "linuxcnc_owned_trt_task_hal_startup_state_probe_passed_without_promotion"
exit 0
fi
print_kv native_task_hal_probe "failed"
print_kv native_probe_status "failed"
print_kv trt_task_hal_runtime_probe_status "runtime_state_probe_failed"
print_kv trt_task_hal_runtime_probe_note "linuxcnc_owned_trt_task_hal_startup_state_probe_failed"
exit 1

View File

@@ -0,0 +1,85 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
BUILD_DIR="$ROOT_DIR/build/task-hal"
MANIFEST_STDOUT="$BUILD_DIR/verify_task_hal_source_manifest.stdout.log"
PROBE_STDOUT="$BUILD_DIR/probe_trt_task_hal_runtime.stdout.log"
mkdir -p "$BUILD_DIR"
"$ROOT_DIR/tools/verify_task_hal_source_manifest.sh" >"$MANIFEST_STDOUT"
"$ROOT_DIR/tests/native/probe_trt_task_hal_runtime.sh" >"$PROBE_STDOUT"
value_from() {
local file="$1"
local key="$2"
awk -F= -v key="$key" '$1 == key { print substr($0, length(key) + 2); found=1 } END { if (!found) exit 1 }' "$file"
}
expect_value() {
local file="$1"
local key="$2"
local expected="$3"
local actual
actual="$(value_from "$file" "$key")"
if [[ "$actual" != "$expected" ]]; then
echo "$key expected $expected, got $actual" >&2
echo "from $file" >&2
exit 1
fi
}
expect_int_gt() {
local file="$1"
local key="$2"
local minimum="$3"
local actual
actual="$(value_from "$file" "$key")"
if ! [[ "$actual" =~ ^[0-9]+$ ]] || (( actual <= minimum )); then
echo "$key expected > $minimum, got $actual" >&2
echo "from $file" >&2
exit 1
fi
}
expect_value "$MANIFEST_STDOUT" task_hal_source_manifest_status ok
expect_value "$MANIFEST_STDOUT" task_hal_source_manifest_ready 1
expect_value "$MANIFEST_STDOUT" task_hal_reference_source_ready 1
expect_value "$MANIFEST_STDOUT" task_hal_missing_reference_source_count 0
expect_int_gt "$MANIFEST_STDOUT" task_source_count 0
expect_int_gt "$MANIFEST_STDOUT" hal_source_count 0
expect_int_gt "$MANIFEST_STDOUT" motion_source_count 0
expect_int_gt "$MANIFEST_STDOUT" nml_source_count 0
expect_value "$MANIFEST_STDOUT" task_hal_runtime_promoted 0
expect_value "$MANIFEST_STDOUT" nativeTaskReady false
expect_value "$MANIFEST_STDOUT" nativeHalSyncReady false
expect_value "$PROBE_STDOUT" trt_task_hal_source_proof_ready 1
expect_value "$PROBE_STDOUT" trt_task_hal_execution_enabled 0
expect_value "$PROBE_STDOUT" trt_task_hal_promotion_allowed 0
expect_value "$PROBE_STDOUT" native_task_hal_probe ok
expect_value "$PROBE_STDOUT" nativeTaskReady false
expect_value "$PROBE_STDOUT" nativeHalSyncReady false
native_probe_status="$(value_from "$PROBE_STDOUT" native_probe_status)"
probe_status="$(value_from "$PROBE_STDOUT" trt_task_hal_runtime_probe_status)"
case "$probe_status" in
ready_disabled_by_default|skipped_missing_host_runtime|blocked_missing_source)
;;
*)
echo "unexpected non-opt-in TRT task/HAL probe status: $probe_status" >&2
exit 1
;;
esac
case "$native_probe_status" in
ready_disabled_by_default|skipped_missing_host_runtime)
;;
*)
echo "unexpected non-opt-in native probe status: $native_probe_status" >&2
exit 1
;;
esac
echo "task_hal_phase0_native_probe_gate=ok"

View File

@@ -0,0 +1,213 @@
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
import assert from "node:assert/strict";
import createLinuxCncTaskHalModule from "../../../build/wasm/task-hal/linuxcnc_task_hal.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const rootDir = resolve(__dirname, "../../..");
const wasmPath = resolve(rootDir, "build/wasm/task-hal/linuxcnc_task_hal.wasm");
const hal = await createLinuxCncTaskHalModule({
wasmBinary: readFileSync(wasmPath),
print() {},
printErr(message) {
console.error(message);
},
});
function allocCString(value) {
const bytes = hal.lengthBytesUTF8(value) + 1;
const ptr = hal._malloc(bytes);
hal.stringToUTF8(value, ptr, bytes);
return ptr;
}
function withCString(value, fn) {
const ptr = allocCString(value);
try {
return fn(ptr);
} finally {
hal._free(ptr);
}
}
function readJsonWithBuffer(call) {
const bytes = 32768;
const ptr = hal._malloc(bytes);
try {
const rc = call(ptr, bytes);
assert.equal(rc, 0);
return JSON.parse(hal.UTF8ToString(ptr));
} finally {
hal._free(ptr);
}
}
function createPin(name, type, dir = 32) {
const ptrAddr = hal._malloc(4);
try {
const rc = withCString(name, (namePtr) => {
switch (type) {
case "bit":
return hal._hal_pin_bit_new(namePtr, dir, ptrAddr, 1);
case "float":
return hal._hal_pin_float_new(namePtr, dir, ptrAddr, 1);
case "s32":
return hal._hal_pin_s32_new(namePtr, dir, ptrAddr, 1);
case "u32":
return hal._hal_pin_u32_new(namePtr, dir, ptrAddr, 1);
default:
throw new Error(`unsupported pin type: ${type}`);
}
});
assert.equal(rc, 0, `${name} create`);
return hal.getValue(ptrAddr, "i32");
} finally {
hal._free(ptrAddr);
}
}
function setPinFloat(name, value) {
withCString(name, (namePtr) => assert.equal(hal._lchal_set_pin_float(namePtr, value), 0));
}
function setPinBit(name, value) {
withCString(name, (namePtr) => assert.equal(hal._lchal_set_pin_bit(namePtr, value ? 1 : 0), 0));
}
function getPin(name) {
return withCString(name, (namePtr) =>
readJsonWithBuffer((outPtr, outLen) => hal._lchal_get_pin_json(namePtr, outPtr, outLen)),
);
}
function getP(name) {
return withCString(name, (namePtr) => {
const bytes = 128;
const outPtr = hal._malloc(bytes);
try {
assert.equal(hal._hal_get_p(namePtr, outPtr, bytes), 0);
return hal.UTF8ToString(outPtr);
} finally {
hal._free(outPtr);
}
});
}
function loadHalFile(path, text) {
return withCString(path, (pathPtr) =>
withCString(text, (textPtr) => hal._lchal_load_hal_file(pathPtr, textPtr)),
);
}
assert.equal(hal._lchal_init_runtime(), 0);
assert.notEqual(createPin("motion.analog-out-03", "float"), 0);
assert.notEqual(createPin("probe.out", "bit"), 0);
assert.notEqual(createPin("probe.in", "bit", 16), 0);
assert.notEqual(createPin("motion.program-line", "s32"), 0);
assert.notEqual(hal._hal_malloc(16), 0);
setPinFloat("motion.analog-out-03", 2.5);
assert.equal(getPin("motion.analog-out-03").value, 2.5);
assert.equal(getP("motion.analog-out-03"), "2.5");
const paramFloatAddr = hal._malloc(8);
const paramS32Addr = hal._malloc(4);
try {
hal.setValue(paramFloatAddr, 7.25, "double");
hal.setValue(paramS32Addr, -12, "i32");
withCString("standalone.param-float", (namePtr) =>
assert.equal(hal._hal_param_float_new(namePtr, 192, paramFloatAddr, 1), 0),
);
withCString("standalone.param-s32", (namePtr) =>
assert.equal(hal._hal_param_s32_new(namePtr, 192, paramS32Addr, 1), 0),
);
assert.equal(getP("standalone.param-float"), "7.25");
assert.equal(getP("standalone.param-s32"), "-12");
} finally {
hal._free(paramFloatAddr);
hal._free(paramS32Addr);
}
withCString("motion.program-line", (namePtr) => {
const typePtr = hal._malloc(4);
const valuePtrPtr = hal._malloc(4);
const connectedPtr = hal._malloc(1);
try {
assert.equal(hal._hal_get_pin_value_by_name(namePtr, typePtr, valuePtrPtr, connectedPtr), 0);
assert.equal(hal.getValue(typePtr, "i32"), 3);
assert.notEqual(hal.getValue(valuePtrPtr, "i32"), 0);
} finally {
hal._free(typePtr);
hal._free(valuePtrPtr);
hal._free(connectedPtr);
}
});
assert.equal(loadHalFile("probe.hal", "net probe-signal probe.out probe.in\n"), 0);
setPinBit("probe.out", true);
assert.equal(getPin("probe.in").value, true);
withCString("probe-signal", (namePtr) => {
const typePtr = hal._malloc(4);
const valuePtrPtr = hal._malloc(4);
const connectedPtr = hal._malloc(1);
try {
assert.equal(hal._hal_get_signal_value_by_name(namePtr, typePtr, valuePtrPtr, connectedPtr), 0);
assert.equal(hal.getValue(typePtr, "i32"), 1);
assert.notEqual(hal.getValue(valuePtrPtr, "i32"), 0);
} finally {
hal._free(typePtr);
hal._free(valuePtrPtr);
hal._free(connectedPtr);
}
});
assert.equal(
loadHalFile(
"threads.hal",
[
"loadrt trivkins",
"addf motion-command-handler servo-thread",
"addf motion-controller servo-thread",
"",
].join("\n"),
),
0,
);
assert.equal(hal._lchal_step_threads(1000000, 2), 0);
withCString("motion-command-handler", (functPtr) =>
withCString("servo-thread", (threadPtr) =>
assert.equal(hal._hal_del_funct_from_thread(functPtr, threadPtr), 0),
),
);
assert.equal(hal._hal_stop_threads(), 0);
assert.equal(hal._hal_start_threads(), 0);
assert.equal(loadHalFile("blocked.hal", "loadusr external-user-m\n"), 2);
const snapshot = readJsonWithBuffer((outPtr, outLen) => hal._lchal_get_snapshot_json(outPtr, outLen));
assert.equal(snapshot.semanticBoundary, "linuxcnc_hal_runtime_phase2_minimal");
assert.equal(snapshot.halRuntimeReady, true);
assert.equal(snapshot.halSyncReady, false);
assert.equal(snapshot.nativeHalSyncReady, false);
assert.equal(snapshot.cycle, 2);
assert.equal(snapshot.pins["probe.in"].value, true);
assert.equal(snapshot.signals["probe-signal"].value, true);
assert.equal(snapshot.params["standalone.param-float"].value, 7.25);
assert.equal(snapshot.params["standalone.param-s32"].value, -12);
assert.equal(snapshot.threads["servo-thread"].functions.length >= 1, true);
assert.equal(snapshot.events.includes("blocked:loadusr"), true);
assert.equal(
snapshot.events.includes("call:servo-thread:motion-controller"),
true,
);
assert.equal(snapshot.events.includes("delf:motion-command-handler:servo-thread"), true);
assert.equal(snapshot.events.includes("threads_stop"), true);
console.log("hal_runtime_registry=ok");
console.log("hal_net_signal_propagation=ok");
console.log("hal_thread_scheduler=ok");
console.log("loadusr_blocked_evidence=ok");
console.log("hal_runtime_wasm_node_smoke=ok");

View File

@@ -0,0 +1,30 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)"
BUILD_DIR="$ROOT_DIR/build/wasm/task-hal"
NATIVE_BUILD_DIR="$ROOT_DIR/build/native/hal-runtime"
if ! command -v emcc >/dev/null 2>&1 && [[ -f "$HOME/emsdk/emsdk_env.sh" ]]; then
# shellcheck source=/dev/null
source "$HOME/emsdk/emsdk_env.sh" >/dev/null
fi
if command -v emcc >/dev/null 2>&1; then
if [[ "${SKIP_TASK_HAL_BUILD:-0}" != "1" ]]; then
"$ROOT_DIR/tools/build_task_hal_wasm.sh"
fi
node "$ROOT_DIR/tests/wasm/node/verify_hal_runtime.mjs"
exit 0
fi
mkdir -p "$NATIVE_BUILD_DIR" "$BUILD_DIR"
g++ -std=c++20 \
-I"$ROOT_DIR/runtime/core/shims" \
-I"$ROOT_DIR/runtime/core/linuxcnc_wrap" \
"$ROOT_DIR/runtime/core/linuxcnc_wrap/linuxcnc_hal_runtime.cpp" \
"$ROOT_DIR/runtime/core/linuxcnc_wrap/linuxcnc_hal_runtime_probe.cpp" \
-o "$NATIVE_BUILD_DIR/linuxcnc_hal_runtime_probe"
"$NATIVE_BUILD_DIR/linuxcnc_hal_runtime_probe"
echo "hal_runtime_wasm_node_smoke=skipped_missing_emcc"

View File

@@ -0,0 +1,111 @@
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
import assert from "node:assert/strict";
import createLinuxCncTaskHalModule from "../../../build/wasm/task-hal/linuxcnc_task_hal.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const rootDir = resolve(__dirname, "../../..");
const wasmPath = resolve(rootDir, "build/wasm/task-hal/linuxcnc_task_hal.wasm");
const runtime = await createLinuxCncTaskHalModule({
wasmBinary: readFileSync(wasmPath),
print() {},
printErr(message) {
console.error(message);
},
});
function allocCString(value) {
const bytes = runtime.lengthBytesUTF8(value) + 1;
const ptr = runtime._malloc(bytes);
runtime.stringToUTF8(value, ptr, bytes);
return ptr;
}
function withCString(value, fn) {
const ptr = allocCString(value);
try {
return fn(ptr);
} finally {
runtime._free(ptr);
}
}
function readJson(call) {
const bytes = 65536;
const ptr = runtime._malloc(bytes);
try {
const rc = call(ptr, bytes);
assert.equal(rc, 0);
return JSON.parse(runtime.UTF8ToString(ptr));
} finally {
runtime._free(ptr);
}
}
function send(command) {
return withCString(JSON.stringify(command), (commandPtr) =>
assert.equal(runtime._lcmot_write_command_json(commandPtr), 0),
);
}
assert.equal(runtime._lchal_init_runtime(), 0);
assert.equal(
withCString("xyzac-trt.ini", (pathPtr) =>
withCString("[TRAJ]\nCOORDINATES = X Y Z A C\n", (iniPtr) =>
runtime._lcmot_init_from_ini(pathPtr, iniPtr),
),
),
0,
);
send({ type: "EMC_TRAJ_LINEAR_MOVE", line: 42, x: 1, y: 2, z: -3, a: 10, c: 20, velocity: 120 });
assert.equal(runtime._lcmot_step_servo(1000000, 1), 0);
let status = readJson((outPtr, outLen) => runtime._lcmot_read_status_json(outPtr, outLen));
let hal = readJson((outPtr, outLen) => runtime._lcmot_read_hal_snapshot_json(outPtr, outLen));
assert.equal(status.semanticBoundary, "linuxcnc_motion_runtime_phase3_minimal");
assert.equal(status.motionRuntimeReady, true);
assert.equal(status.nativeHalSyncReady, false);
assert.equal(status.motion.programLine, 42);
assert.equal(status.motion.inPosition, true);
assert.equal(status.axis.x, 1);
assert.equal(status.axis.z, -3);
assert.equal(hal.pins["motion.program-line"].value, 42);
assert.equal(hal.pins["joint.0.motor-pos-cmd"].value, 1);
assert.equal(hal.pins["joint.0.motor-pos-fb"].value, 1);
send({ type: "EMCMOT_SET_AOUT", value: 1 });
assert.equal(runtime._lcmot_step_servo(1000000, 1), 0);
status = readJson((outPtr, outLen) => runtime._lcmot_read_status_json(outPtr, outLen));
hal = readJson((outPtr, outLen) => runtime._lcmot_read_hal_snapshot_json(outPtr, outLen));
assert.equal(status.motion.switchkinsType, 1);
assert.equal(hal.pins["motion.switchkins-type"].value, 1);
assert.equal(hal.pins["motion.analog-out-03"].value, 1);
send({ type: "EMC_JOG_INCR", axis: "X", distance: 0.5, velocity: 60 });
assert.equal(runtime._lcmot_step_servo(1000000, 1), 0);
status = readJson((outPtr, outLen) => runtime._lcmot_read_status_json(outPtr, outLen));
hal = readJson((outPtr, outLen) => runtime._lcmot_read_hal_snapshot_json(outPtr, outLen));
assert.equal(status.axis.x, 1.5);
assert.equal(status.motion.teleopMode, 1);
assert.equal(hal.pins["axis.0.pos-fb"].value, 1.5);
send({ type: "EMC_TRAJ_PAUSE" });
assert.equal(runtime._lcmot_step_servo(1000000, 1), 0);
status = readJson((outPtr, outLen) => runtime._lcmot_read_status_json(outPtr, outLen));
assert.equal(status.motion.paused, true);
send({ type: "EMC_TRAJ_RESUME" });
assert.equal(runtime._lcmot_step_servo(1000000, 1), 0);
status = readJson((outPtr, outLen) => runtime._lcmot_read_status_json(outPtr, outLen));
assert.equal(status.motion.paused, false);
console.log("motion_hal_servo_cycle=ok");
console.log("motion_program_line_hal_sync=ok");
console.log("switchkins_type_hal_sync=ok");
console.log("jog_motion_status_sync=ok");
console.log("motion_hal_sync_smoke=ok");

View File

@@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)"
if ! command -v emcc >/dev/null 2>&1 && [[ -f "$HOME/emsdk/emsdk_env.sh" ]]; then
# shellcheck source=/dev/null
source "$HOME/emsdk/emsdk_env.sh" >/dev/null
fi
if ! command -v emcc >/dev/null 2>&1; then
echo "motion_hal_sync_smoke=skipped_missing_emcc"
echo "nativeHalSyncReady=false"
exit 0
fi
if [[ "${SKIP_TASK_HAL_BUILD:-0}" != "1" ]]; then
"$ROOT_DIR/tools/build_task_hal_wasm.sh"
fi
node "$ROOT_DIR/tests/wasm/node/verify_motion_hal_sync.mjs"

View File

@@ -0,0 +1,56 @@
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
import assert from "node:assert/strict";
import { createLinuxCncTaskHalSdk } from "../../../runtime/sdk/src/linuxcnc-task-hal.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const rootDir = resolve(__dirname, "../../..");
const wasmPath = resolve(rootDir, "build/wasm/task-hal/linuxcnc_task_hal.wasm");
const sdk = await createLinuxCncTaskHalSdk({
wasmBinary: readFileSync(wasmPath),
print() {},
printErr(message) {
console.error(message);
},
});
const readiness = sdk.readiness();
assert.equal(readiness.taskRuntimeReady, true);
assert.equal(readiness.motionRuntimeReady, true);
assert.equal(readiness.halRuntimeReady, true);
assert.equal(readiness.nativeTaskReady, false);
assert.equal(readiness.nativeHalSyncReady, false);
sdk.initSession({
iniPath: "xyzac-trt.ini",
iniText: "[TRAJ]\nCOORDINATES = X Y Z A C\n",
});
assert.equal(sdk.stageFile("programs/sdk-task-hal.ngc", "G0 X3 Y0 Z-1\n"), 0);
assert.equal(sdk.openProgram("programs/sdk-task-hal.ngc"), 0);
assert.equal(sdk.sendCommand({ type: "EMC_TASK_SET_STATE", state: "ON" }), 0);
assert.equal(sdk.sendCommand({ type: "EMC_TASK_SET_MODE", mode: "AUTO" }), 0);
assert.equal(sdk.sendCommand({ type: "EMC_TASK_PLAN_RUN", line: 0 }), 0);
assert.equal(sdk.runCycles({ taskCycles: 1 }), 0);
let status = sdk.readStatus();
assert.equal(status.semanticBoundary, "linuxcnc_task_motion_hal_wasm_phase4_minimal");
assert.equal(status.taskRuntimeReady, true);
assert.equal(status.motionStatus.motion.programLine, 1);
assert.equal(status.motionStatus.axis.x, 3);
assert.equal(status.halSnapshot.pins["motion.program-line"].value, 1);
assert.equal(sdk.sendCommand({ type: "EMC_TASK_PLAN_EXECUTE", mdi: "M429" }), 0);
assert.equal(sdk.runCycles({ taskCycles: 1 }), 0);
status = sdk.readStatus();
assert.equal(status.motionStatus.motion.switchkinsType, 0);
const events = sdk.readEvents().events;
assert.equal(events.includes("task_plan_run"), true);
assert.equal(events.includes("task_mdi_switchkins:M429"), true);
console.log("linuxcnc_task_hal_sdk=ok");
console.log("task_hal_sdk_status_snapshot=ok");

View File

@@ -0,0 +1,22 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)"
if ! command -v emcc >/dev/null 2>&1 && [[ -f "$HOME/emsdk/emsdk_env.sh" ]]; then
# shellcheck source=/dev/null
source "$HOME/emsdk/emsdk_env.sh" >/dev/null
fi
if ! command -v emcc >/dev/null 2>&1; then
echo "linuxcnc_task_hal_sdk=skipped_missing_emcc"
echo "nativeTaskReady=false"
echo "nativeHalSyncReady=false"
exit 0
fi
if [[ "${SKIP_TASK_HAL_BUILD:-0}" != "1" ]]; then
"$ROOT_DIR/tools/build_task_hal_wasm.sh"
fi
node "$ROOT_DIR/tests/wasm/node/verify_task_hal_sdk.mjs"

View File

@@ -0,0 +1,148 @@
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
import assert from "node:assert/strict";
import createLinuxCncTaskHalModule from "../../../build/wasm/task-hal/linuxcnc_task_hal.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const rootDir = resolve(__dirname, "../../..");
const wasmPath = resolve(rootDir, "build/wasm/task-hal/linuxcnc_task_hal.wasm");
const runtime = await createLinuxCncTaskHalModule({
wasmBinary: readFileSync(wasmPath),
print() {},
printErr(message) {
console.error(message);
},
});
function allocCString(value) {
const bytes = runtime.lengthBytesUTF8(value) + 1;
const ptr = runtime._malloc(bytes);
runtime.stringToUTF8(value, ptr, bytes);
return ptr;
}
function withCString(value, fn) {
const ptr = allocCString(value);
try {
return fn(ptr);
} finally {
runtime._free(ptr);
}
}
function readJson(call) {
const bytes = 131072;
const ptr = runtime._malloc(bytes);
try {
const rc = call(ptr, bytes);
assert.equal(rc, 0);
return JSON.parse(runtime.UTF8ToString(ptr));
} finally {
runtime._free(ptr);
}
}
function send(command) {
return withCString(JSON.stringify(command), (commandPtr) =>
assert.equal(runtime._lctask_send_command_json(commandPtr), 0, command.type),
);
}
function status() {
return readJson((outPtr, outLen) => runtime._lctask_read_status_json(outPtr, outLen));
}
function events() {
return readJson((outPtr, outLen) => runtime._lctask_read_events_json(outPtr, outLen)).events;
}
const session = {
iniPath: "xyzac-trt.ini",
iniText: "[TRAJ]\nCOORDINATES = X Y Z A C\n",
};
assert.equal(
withCString(JSON.stringify(session), (sessionPtr) => runtime._lctask_init_session(sessionPtr)),
0,
);
const program = ["G0 X1 Y2 Z-3 A10 C20", "G1 X2 Y3 Z-4 A11 C21"].join("\n");
assert.equal(
withCString("programs/task-hal-smoke.ngc", (pathPtr) =>
withCString(program, (textPtr) => runtime._lctask_stage_file(pathPtr, textPtr)),
),
0,
);
assert.equal(
withCString("programs/task-hal-smoke.ngc", (pathPtr) => runtime._lctask_open_program(pathPtr)),
0,
);
send({ type: "EMC_TASK_SET_STATE", state: "ON" });
send({ type: "EMC_TASK_SET_MODE", mode: "AUTO" });
send({ type: "EMC_TASK_PLAN_RUN", line: 0 });
assert.equal(runtime._lctask_run_cycles(10000000, 1000000, 2), 0);
let snapshot = status();
assert.equal(snapshot.semanticBoundary, "linuxcnc_task_motion_hal_wasm_phase4_minimal");
assert.equal(snapshot.taskRuntimeReady, true);
assert.equal(snapshot.taskStatusFromLinuxCncRuntime, true);
assert.equal(snapshot.taskCommandsDriveMotionRuntime, true);
assert.equal(snapshot.nativeTaskReady, false);
assert.equal(snapshot.nativeHalSyncReady, false);
assert.equal(snapshot.task.state, "ON");
assert.equal(snapshot.task.mode, "AUTO");
assert.equal(snapshot.task.openedLineCount, 2);
assert.equal(snapshot.task.nextProgramLine, 2);
assert.equal(snapshot.motionStatus.motion.programLine, 2);
assert.equal(snapshot.motionStatus.axis.x, 2);
assert.equal(snapshot.motionStatus.axis.z, -4);
assert.equal(snapshot.halSnapshot.pins["motion.program-line"].value, 2);
assert.equal(snapshot.halSnapshot.pins["joint.0.motor-pos-cmd"].value, 2);
send({ type: "EMC_TASK_PLAN_PAUSE" });
assert.equal(runtime._lctask_run_cycles(10000000, 1000000, 1), 0);
snapshot = status();
assert.equal(snapshot.task.interpState, "PAUSED");
assert.equal(snapshot.motionStatus.motion.paused, true);
send({ type: "EMC_TASK_PLAN_RESUME" });
assert.equal(runtime._lctask_run_cycles(10000000, 1000000, 1), 0);
snapshot = status();
assert.equal(snapshot.motionStatus.motion.paused, false);
send({ type: "EMC_TASK_PLAN_EXECUTE", mdi: "M428" });
assert.equal(runtime._lctask_run_cycles(10000000, 1000000, 1), 0);
snapshot = status();
assert.equal(snapshot.motionStatus.motion.switchkinsType, 1);
assert.equal(snapshot.halSnapshot.pins["motion.switchkins-type"].value, 1);
send({ type: "EMC_JOG_INCR", axis: "X", distance: 0.25, velocity: 60 });
assert.equal(runtime._lctask_run_cycles(10000000, 1000000, 1), 0);
snapshot = status();
assert.equal(snapshot.motionStatus.axis.x, 2.25);
assert.equal(snapshot.motionStatus.motion.teleopMode, 1);
send({ type: "EMC_TASK_ABORT" });
assert.equal(runtime._lctask_run_cycles(10000000, 1000000, 1), 0);
snapshot = status();
assert.equal(snapshot.task.execState, "DONE");
assert.equal(snapshot.motionStatus.motion.aborted, true);
const eventLog = events();
assert.equal(eventLog.includes("task_session_motion_init"), true);
assert.equal(eventLog.includes("task_plan_run"), true);
assert.equal(eventLog.includes("task_plan_pause"), true);
assert.equal(eventLog.includes("task_plan_resume"), true);
assert.equal(eventLog.includes("task_mdi_switchkins:M428"), true);
assert.equal(eventLog.includes("task_jog_incr"), true);
assert.equal(eventLog.includes("task_abort"), true);
console.log("linuxcnc_task_runtime_smoke=ok");
console.log("task_status_from_linuxcnc_runtime=ok");
console.log("task_commands_drive_motion_runtime=ok");
console.log("mdi_jog_task_motion_hal_sync=ok");

View File

@@ -0,0 +1,22 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)"
if ! command -v emcc >/dev/null 2>&1 && [[ -f "$HOME/emsdk/emsdk_env.sh" ]]; then
# shellcheck source=/dev/null
source "$HOME/emsdk/emsdk_env.sh" >/dev/null
fi
if ! command -v emcc >/dev/null 2>&1; then
echo "linuxcnc_task_runtime_smoke=skipped_missing_emcc"
echo "nativeTaskReady=false"
echo "nativeHalSyncReady=false"
exit 0
fi
if [[ "${SKIP_TASK_HAL_BUILD:-0}" != "1" ]]; then
"$ROOT_DIR/tools/build_task_hal_wasm.sh"
fi
node "$ROOT_DIR/tests/wasm/node/verify_task_hal_wasm.mjs"

View File

@@ -0,0 +1,58 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
WRAP_DIR="$ROOT_DIR/runtime/core/linuxcnc_wrap"
SHIM_DIR="$ROOT_DIR/runtime/core/shims"
INCLUDE_DIR="$ROOT_DIR/runtime/core/include"
OUT_DIR="$ROOT_DIR/build/wasm/task-hal"
OBJ_DIR="$OUT_DIR/obj"
source "$ROOT_DIR/tools/wasm_incremental_build_lib.sh"
mkdir -p "$OUT_DIR" "$OBJ_DIR"
TARGET="linuxcnc_task_hal_wasm"
: > "$OUT_DIR/$TARGET.stdout.log"
: > "$OUT_DIR/$TARGET.stderr.log"
COMMON_FLAGS=(
-O2
-D_GNU_SOURCE
-I"$WRAP_DIR"
-I"$SHIM_DIR"
-I"$INCLUDE_DIR"
)
SOURCES=(
"$WRAP_DIR/linuxcnc_hal_runtime.cpp"
"$WRAP_DIR/linuxcnc_motion_runtime.c"
"$WRAP_DIR/linuxcnc_task_hal_wasm.cpp"
)
OBJECTS=()
for source in "${SOURCES[@]}"; do
obj="$(source_object_path "$source" "$ROOT_DIR" "$OBJ_DIR")"
OBJECTS+=("$obj")
if [[ "$source" == *.c ]]; then
compile_wasm_object "$TARGET" "$source" "$obj" "$OUT_DIR" -std=c11 "${COMMON_FLAGS[@]}"
else
compile_wasm_object "$TARGET" "$source" "$obj" "$OUT_DIR" -std=c++20 "${COMMON_FLAGS[@]}"
fi
done
link_wasm_module \
"$TARGET" \
"$OUT_DIR/linuxcnc_task_hal.js" \
"$OUT_DIR" \
-O2 \
"${OBJECTS[@]}" \
-s MODULARIZE=1 \
-s EXPORT_ES6=1 \
-s ENVIRONMENT=web,node \
-s ALLOW_MEMORY_GROWTH=1 \
-s NO_EXIT_RUNTIME=1 \
-s EXPORTED_FUNCTIONS='["_malloc","_free","_hal_init","_hal_ready","_hal_exit","_hal_malloc","_hal_pin_bit_new","_hal_pin_float_new","_hal_pin_s32_new","_hal_pin_u32_new","_hal_pin_s64_new","_hal_pin_u64_new","_hal_pin_bit_newf","_hal_pin_float_newf","_hal_pin_s32_newf","_hal_pin_u32_newf","_hal_pin_s64_newf","_hal_pin_u64_newf","_hal_param_bit_new","_hal_param_float_new","_hal_param_s32_new","_hal_param_u32_new","_hal_param_s64_new","_hal_param_u64_new","_hal_param_bit_newf","_hal_param_float_newf","_hal_param_s32_newf","_hal_param_u32_newf","_hal_param_s64_newf","_hal_param_u64_newf","_hal_get_pin_value_by_name","_hal_get_signal_value_by_name","_hal_get_param_value_by_name","_hal_link","_hal_unlink","_hal_set_p","_hal_get_p","_hal_create_thread","_hal_add_funct_to_thread","_hal_del_funct_from_thread","_hal_start_threads","_hal_stop_threads","_lchal_init_runtime","_lchal_load_hal_file","_lchal_set_pin_float","_lchal_set_pin_s32","_lchal_set_pin_bit","_lchal_get_pin_json","_lchal_get_snapshot_json","_lchal_step_threads","_lchal_reset_runtime","_lcmot_init_from_ini","_lcmot_write_command_json","_lcmot_step_servo","_lcmot_read_status_json","_lcmot_read_hal_snapshot_json","_lcmot_reset","_lctask_init_session","_lctask_stage_file","_lctask_open_program","_lctask_send_command_json","_lctask_run_cycles","_lctask_read_status_json","_lctask_read_events_json","_lctask_reset_session"]' \
-s EXPORTED_RUNTIME_METHODS='["UTF8ToString","stringToUTF8","lengthBytesUTF8","getValue","setValue"]'
echo "linuxcnc_task_hal_wasm_build=ok"

View File

@@ -0,0 +1,28 @@
# LinuxCNC task / motion / HAL source references for future task-HAL runtime.
#
# This manifest is intentionally separate from source-manifest.txt. The main
# source manifest is tied to current native source-probe coverage; these files
# are Phase 0 references for the future task/HAL runtime boundary and must not
# be treated as promoted runtime coverage until the task/HAL WASM/native/browser
# gates exist.
src/emc/task/task.hh
src/emc/task/taskclass.hh
src/emc/task/taskclass.cc
src/emc/task/emctask.cc
src/emc/task/emctaskmain.cc
src/emc/task/taskintf.cc
src/emc/task/emccanon.cc
src/emc/nml_intf/emc.hh
src/libnml/posemath/posemath.h
src/libnml/posemath/posemath.cc
src/emc/motion/usrmotintf.h
src/emc/motion/motion.h
src/emc/motion/motion.c
src/emc/motion/command.c
src/emc/motion/control.c
src/emc/motion/mot_priv.h
src/hal/hal_lib.c
src/hal/hal_priv.h
src/hal/components/threads.c
src/hal/utils/halcmd_commands.cc

View File

@@ -0,0 +1,150 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
LINUXCNC_ROOT="${LINUXCNC_ROOT:-$ROOT_DIR/../linuxcnc}"
VENDOR_ROOT="$ROOT_DIR/vendor/linuxcnc"
MANIFEST="$ROOT_DIR/tools/task-hal-source-manifest.txt"
BUILD_DIR="$ROOT_DIR/build/task-hal"
REPORT="$BUILD_DIR/task-hal-source-manifest.tsv"
mkdir -p "$BUILD_DIR"
print_kv() {
printf '%s=%s\n' "$1" "$2"
}
if [[ ! -f "$MANIFEST" ]]; then
print_kv task_hal_source_manifest_status missing_manifest
print_kv task_hal_source_manifest_path "$MANIFEST"
exit 1
fi
tmp="$(mktemp "$BUILD_DIR/task-hal-source-manifest.XXXXXX.tsv")"
trap 'rm -f "$tmp"' EXIT
printf '%s\t%s\t%s\t%s\t%s\n' \
source_rel \
reference_exists \
vendor_exists \
vendor_hash_matches \
boundary > "$tmp"
source_count=0
task_source_count=0
hal_source_count=0
motion_source_count=0
nml_source_count=0
libnml_source_count=0
reference_ready=1
vendor_ready=1
vendor_match_ready=1
missing_reference=()
missing_vendor=()
mismatched_vendor=()
while IFS= read -r source_rel; do
[[ -z "$source_rel" || "$source_rel" =~ ^[[:space:]]*# ]] && continue
source_count=$((source_count + 1))
case "$source_rel" in
src/emc/task/*)
task_source_count=$((task_source_count + 1))
;;
src/hal/*)
hal_source_count=$((hal_source_count + 1))
;;
src/emc/motion/*)
motion_source_count=$((motion_source_count + 1))
;;
src/emc/nml_intf/*)
nml_source_count=$((nml_source_count + 1))
;;
src/libnml/*)
libnml_source_count=$((libnml_source_count + 1))
;;
esac
reference_file="$LINUXCNC_ROOT/$source_rel"
vendor_file="$VENDOR_ROOT/$source_rel"
reference_exists=0
vendor_exists=0
vendor_hash_matches=0
if [[ -f "$reference_file" ]]; then
reference_exists=1
else
reference_ready=0
missing_reference+=("$source_rel")
fi
if [[ -f "$vendor_file" ]]; then
vendor_exists=1
else
vendor_ready=0
missing_vendor+=("$source_rel")
fi
if [[ "$reference_exists" == "1" && "$vendor_exists" == "1" ]]; then
if cmp -s "$reference_file" "$vendor_file"; then
vendor_hash_matches=1
else
vendor_match_ready=0
mismatched_vendor+=("$source_rel")
fi
elif [[ "$vendor_exists" != "1" ]]; then
vendor_hash_matches=0
fi
printf '%s\t%s\t%s\t%s\t%s\n' \
"$source_rel" \
"$reference_exists" \
"$vendor_exists" \
"$vendor_hash_matches" \
"phase0_reference_only_not_runtime_promoted" >> "$tmp"
done < "$MANIFEST"
mv -f "$tmp" "$REPORT"
trap - EXIT
join_csv() {
local IFS=,
printf '%s' "$*"
}
print_kv task_hal_source_manifest_status ok
print_kv task_hal_source_manifest_ready "$(( source_count > 0 && reference_ready == 1 ? 1 : 0 ))"
print_kv task_hal_source_manifest_path "$MANIFEST"
print_kv task_hal_source_manifest_report "$REPORT"
print_kv task_hal_source_count "$source_count"
print_kv task_source_count "$task_source_count"
print_kv hal_source_count "$hal_source_count"
print_kv motion_source_count "$motion_source_count"
print_kv nml_source_count "$nml_source_count"
print_kv libnml_source_count "$libnml_source_count"
print_kv task_hal_reference_source_ready "$reference_ready"
print_kv task_hal_vendor_source_ready "$vendor_ready"
print_kv task_hal_vendor_hash_match_ready "$vendor_match_ready"
print_kv task_hal_missing_reference_source_count "${#missing_reference[@]}"
print_kv task_hal_missing_reference_sources_ready "$(( ${#missing_reference[@]} == 0 ? 1 : 0 ))"
if [[ "${#missing_reference[@]}" -gt 0 ]]; then
print_kv task_hal_missing_reference_source_list "$(join_csv "${missing_reference[@]}")"
else
print_kv task_hal_missing_reference_source_list "-"
fi
if [[ "${#missing_vendor[@]}" -gt 0 ]]; then
print_kv task_hal_missing_vendor_source_list "$(join_csv "${missing_vendor[@]}")"
else
print_kv task_hal_missing_vendor_source_list "-"
fi
if [[ "${#mismatched_vendor[@]}" -gt 0 ]]; then
print_kv task_hal_mismatched_vendor_source_list "$(join_csv "${mismatched_vendor[@]}")"
else
print_kv task_hal_mismatched_vendor_source_list "-"
fi
print_kv task_hal_runtime_promoted 0
print_kv nativeTaskReady false
print_kv nativeHalSyncReady false
if [[ "$source_count" -eq 0 || "$reference_ready" != "1" ]]; then
exit 1
fi

View File

@@ -7,7 +7,7 @@
"build": "node scripts/build-static.mjs", "build": "node scripts/build-static.mjs",
"dev": "python3 -m http.server 4173", "dev": "python3 -m http.server 4173",
"smoke": "bash ../tests/browser/verify_gmoccapy_shell_browser.sh && bash ../tests/browser/verify_gmoccapy_dist_browser.sh", "smoke": "bash ../tests/browser/verify_gmoccapy_shell_browser.sh && bash ../tests/browser/verify_gmoccapy_dist_browser.sh",
"smoke:node": "node ../tests/node/verify_linuxcnc_kinematics_runtime.mjs && node ../tests/node/verify_linuxcnc_interpreter_runtime.mjs && node ../tests/node/verify_linuxcnc_ini_runtime.mjs && node ../tests/node/verify_full_linuxcnc_5axis_source.mjs && node ../tests/node/verify_full_execution_boundary.mjs && node ../tests/node/verify_machine_file_staging.mjs && node ../tests/node/verify_five_axis_session.mjs && node ../tests/node/verify_rtcp_store.mjs && node ../tests/node/verify_profile_boundary.mjs" "smoke:node": "node ../tests/node/verify_linuxcnc_kinematics_runtime.mjs && node ../tests/node/verify_linuxcnc_interpreter_runtime.mjs && node ../tests/node/verify_linuxcnc_ini_runtime.mjs && node ../tests/node/verify_linuxcnc_task_hal_runtime.mjs && node ../tests/node/verify_full_linuxcnc_5axis_source.mjs && node ../tests/node/verify_full_execution_boundary.mjs && node ../tests/node/verify_machine_file_staging.mjs && node ../tests/node/verify_five_axis_session.mjs && node ../tests/node/verify_rtcp_store.mjs && node ../tests/node/verify_profile_boundary.mjs"
}, },
"dependencies": {}, "dependencies": {},
"devDependencies": {} "devDependencies": {}

View File

@@ -15,6 +15,7 @@ await copyLinuxCncConfigAssets();
await copyKinematicsRuntimeAssets(); await copyKinematicsRuntimeAssets();
await copyInterpreterRuntimeAssets(); await copyInterpreterRuntimeAssets();
await copyTpRuntimeAssets(); await copyTpRuntimeAssets();
await copyTaskHalRuntimeAssets();
const packageJson = JSON.parse(await readFile(join(appRoot, "package.json"), "utf8")); const packageJson = JSON.parse(await readFile(join(appRoot, "package.json"), "utf8"));
const forbiddenDependencies = ["react", "vue", "@angular/core", "svelte"]; const forbiddenDependencies = ["react", "vue", "@angular/core", "svelte"];
@@ -46,6 +47,15 @@ async function copyTpRuntimeAssets() {
} }
} }
async function copyTaskHalRuntimeAssets() {
const taskHalSrcDir = join(repoRoot, "wasm-port/build/wasm/task-hal");
const taskHalDistDir = join(distDir, "wasm-port/build/wasm/task-hal");
await mkdir(taskHalDistDir, { recursive: true });
for (const entry of ["linuxcnc_task_hal.js", "linuxcnc_task_hal.wasm"]) {
await cp(join(taskHalSrcDir, entry), join(taskHalDistDir, entry));
}
}
console.log("gmoccapy_static_build=ok"); console.log("gmoccapy_static_build=ok");
async function copyLinuxCncConfigAssets() { async function copyLinuxCncConfigAssets() {
@@ -72,7 +82,7 @@ async function copyKinematicsRuntimeAssets() {
join(sdkSrcDir, "linuxcnc-kinematics.js"), join(sdkSrcDir, "linuxcnc-kinematics.js"),
join(sdkDistDir, "linuxcnc-kinematics.js"), join(sdkDistDir, "linuxcnc-kinematics.js"),
); );
for (const entry of ["linuxcnc-interp.js", "linuxcnc-hal.js", "linuxcnc-tp.js"]) { for (const entry of ["linuxcnc-interp.js", "linuxcnc-hal.js", "linuxcnc-tp.js", "linuxcnc-task-hal.js"]) {
await cp(join(sdkSrcDir, entry), join(sdkDistDir, entry)); await cp(join(sdkSrcDir, entry), join(sdkDistDir, entry));
} }

View File

@@ -5,6 +5,8 @@ import { createLinuxCncInterpreterWorkerRuntime } from "./runtime/linuxcnc-inter
import { createLinuxCncKinematicsRuntime } from "./runtime/linuxcnc-kinematics-runtime.js"; import { createLinuxCncKinematicsRuntime } from "./runtime/linuxcnc-kinematics-runtime.js";
import { createLinuxCncKinematicsWorkerRuntime } from "./runtime/linuxcnc-kinematics-worker-client.js"; import { createLinuxCncKinematicsWorkerRuntime } from "./runtime/linuxcnc-kinematics-worker-client.js";
import { loadLinuxCncIniConfig } from "./runtime/linuxcnc-ini-runtime.js"; import { loadLinuxCncIniConfig } from "./runtime/linuxcnc-ini-runtime.js";
import { createLinuxCncTaskHalRuntime } from "./runtime/linuxcnc-task-hal-runtime.js";
import { createLinuxCncTaskHalWorkerRuntime } from "./runtime/linuxcnc-task-hal-worker-client.js";
const app = document.querySelector("#app"); const app = document.querySelector("#app");
@@ -17,6 +19,7 @@ const shell = mountGmoccapyShell(app, store);
const iniConfigReady = attachProfileIniConfig(store, store.getState().profile); const iniConfigReady = attachProfileIniConfig(store, store.getState().profile);
const kinematicsRuntimeReady = attachDefaultKinematicsRuntime(store, store.getState().profile.kinematicsModuleId || store.getState().machineProfile); const kinematicsRuntimeReady = attachDefaultKinematicsRuntime(store, store.getState().profile.kinematicsModuleId || store.getState().machineProfile);
const interpreterRuntimeReady = attachDefaultInterpreterRuntime(store); const interpreterRuntimeReady = attachDefaultInterpreterRuntime(store);
const taskHalRuntimeReady = attachDefaultTaskHalRuntime(store);
let attachedKinematicsProfile = store.getState().machineProfile; let attachedKinematicsProfile = store.getState().machineProfile;
let attachedIniProfile = store.getState().machineProfile; let attachedIniProfile = store.getState().machineProfile;
store.subscribe((state) => { store.subscribe((state) => {
@@ -42,6 +45,7 @@ window.webRtcp5AxisSimulation = {
iniConfigReady, iniConfigReady,
kinematicsRuntimeReady, kinematicsRuntimeReady,
interpreterRuntimeReady, interpreterRuntimeReady,
taskHalRuntimeReady,
}; };
store.dispatch({ type: "BOOT_READY" }); store.dispatch({ type: "BOOT_READY" });
@@ -145,3 +149,41 @@ async function attachDefaultInterpreterRuntime(store) {
}; };
} }
} }
async function attachDefaultTaskHalRuntime(store) {
const sdkModuleUrls = [
new URL("../../../wasm-port/runtime/sdk/src/linuxcnc-task-hal.js", import.meta.url).href,
new URL("../wasm-port/runtime/sdk/src/linuxcnc-task-hal.js", import.meta.url).href,
];
const errors = [];
for (const sdkModuleUrl of sdkModuleUrls) {
if (typeof Worker === "function") {
try {
const runtime = await createLinuxCncTaskHalWorkerRuntime({ sdkModuleUrl });
const readiness = await runtime.readiness();
store.dispatch({ type: "ATTACH_TASK_HAL_RUNTIME", runtime, readiness });
return readiness;
} catch (error) {
errors.push(`${sdkModuleUrl} worker: ${error.message}`);
}
}
try {
const runtime = await createLinuxCncTaskHalRuntime({ sdkModuleUrl });
const readiness = runtime.readiness();
store.dispatch({ type: "ATTACH_TASK_HAL_RUNTIME", runtime, readiness });
return readiness;
} catch (error) {
errors.push(`${sdkModuleUrl}: ${error.message}`);
}
}
store.dispatch({ type: "TASK_HAL_RUNTIME_FAILED", error: errors.join(" | ") });
return {
apiName: "web-rtcp-5axis-linuxcnc-task-hal-runtime-readiness",
loaded: false,
taskRuntimeReady: false,
motionRuntimeReady: false,
halRuntimeReady: false,
error: errors.join(" | "),
};
}

View File

@@ -35,6 +35,37 @@ export function createFullLinuxCncExecutionBoundary(state = {}) {
programExecution?.plannerTiming?.plannerRuntimeReady === true, programExecution?.plannerTiming?.plannerRuntimeReady === true,
); );
const halSwitchkinsEvidenceReady = machineFileText.includes("fiveaxis_hal_switchkins: rc=0 found=1"); const halSwitchkinsEvidenceReady = machineFileText.includes("fiveaxis_hal_switchkins: rc=0 found=1");
const taskHalSummary = state.taskHalStatus?.summary || {};
const taskRuntimeReady = Boolean(
taskHalSummary.taskRuntimeReady === true ||
state.taskHalRuntimeReadiness?.taskRuntimeReady === true,
);
const motionRuntimeReady = Boolean(
taskHalSummary.motionRuntimeReady === true ||
state.taskHalRuntimeReadiness?.motionRuntimeReady === true,
);
const halRuntimeReady = Boolean(
taskHalSummary.halRuntimeReady === true ||
state.taskHalRuntimeReadiness?.halRuntimeReady === true,
);
const halSyncReady = Boolean(
taskHalSummary.halSyncReady === true ||
state.taskHalRuntimeReadiness?.halSyncReady === true,
);
const taskHalComparisonReady = Boolean(taskHalSummary.taskHalComparisonReady === true);
const nativeTaskReady = taskRuntimeReady;
const nativeHalSyncReady = halRuntimeReady && motionRuntimeReady && halSyncReady;
const fullLinuxCncProgramExecutionReady = Boolean(
kinematicsReady &&
interpreterReady &&
canonicalProgramReady &&
machineFileStagingReady &&
machineFileRemapReady &&
plannerRuntimeReady &&
nativeTaskReady &&
nativeHalSyncReady &&
taskHalComparisonReady
);
const satisfied = [ const satisfied = [
kinematicsReady ? "linuxcnc-kinematics-wasm" : null, kinematicsReady ? "linuxcnc-kinematics-wasm" : null,
@@ -44,6 +75,11 @@ export function createFullLinuxCncExecutionBoundary(state = {}) {
machineFileRemapReady ? "fiveaxis-remap-machine-file-run" : null, machineFileRemapReady ? "fiveaxis-remap-machine-file-run" : null,
plannerRuntimeReady ? "linuxcnc-tp-queue-runtime-timing" : null, plannerRuntimeReady ? "linuxcnc-tp-queue-runtime-timing" : null,
halSwitchkinsEvidenceReady ? "switchkins-hal-bridge-evidence" : null, halSwitchkinsEvidenceReady ? "switchkins-hal-bridge-evidence" : null,
taskRuntimeReady ? "linuxcnc-task-runtime" : null,
motionRuntimeReady ? "linuxcnc-motion-runtime" : null,
halRuntimeReady ? "linuxcnc-hal-runtime" : null,
halSyncReady ? "task-motion-hal-sync" : null,
taskHalComparisonReady ? "task-hal-cycle-artifact" : null,
].filter(Boolean); ].filter(Boolean);
const missing = []; const missing = [];
@@ -54,12 +90,16 @@ export function createFullLinuxCncExecutionBoundary(state = {}) {
if (!machineFileRemapReady) missing.push("machine-file backed five-axis remap run"); if (!machineFileRemapReady) missing.push("machine-file backed five-axis remap run");
if (!plannerRuntimeReady) missing.push("LinuxCNC trajectory planner queue timing runtime"); if (!plannerRuntimeReady) missing.push("LinuxCNC trajectory planner queue timing runtime");
if (!halSwitchkinsEvidenceReady) missing.push("switchkins HAL bridge evidence"); if (!halSwitchkinsEvidenceReady) missing.push("switchkins HAL bridge evidence");
if (!taskRuntimeReady) missing.push("LinuxCNC task runtime");
if (!motionRuntimeReady) missing.push("LinuxCNC motion runtime");
if (!halRuntimeReady) missing.push("LinuxCNC HAL runtime");
if (!halSyncReady) missing.push("task/motion/HAL synchronization");
if (!taskHalComparisonReady) missing.push("task cycle and HAL servo cycle artifact");
const blockers = [ const blockers = [];
"native LinuxCNC task/NML process is not ported", if (!nativeTaskReady) blockers.push("LinuxCNC task runtime is not promoted");
"native realtime HAL thread synchronization is not ported", if (!nativeHalSyncReady) blockers.push("realtime HAL synchronization is not promoted");
"external user-M process and full tool DB process are not promoted", blockers.push("external user-M process and full tool DB process are not promoted");
];
if (!plannerRuntimeReady) { if (!plannerRuntimeReady) {
blockers.push("LinuxCNC trajectory planner queue is not promoted as browser runtime"); blockers.push("LinuxCNC trajectory planner queue is not promoted as browser runtime");
} }
@@ -67,12 +107,16 @@ export function createFullLinuxCncExecutionBoundary(state = {}) {
return { return {
apiName: "web-rtcp-5axis-full-linuxcnc-execution-boundary", apiName: "web-rtcp-5axis-full-linuxcnc-execution-boundary",
profileId: state.machineProfile || adapter.profileId || "unknown", profileId: state.machineProfile || adapter.profileId || "unknown",
phase: machineFileRemapReady phase: fullLinuxCncProgramExecutionReady
? "linuxcnc-task-motion-hal-simulation-runtime"
: machineFileRemapReady
? "partial-linuxcnc-remap-boundary" ? "partial-linuxcnc-remap-boundary"
: canonicalProgramReady : canonicalProgramReady
? "canonical-interpreter-boundary" ? "canonical-interpreter-boundary"
: "blocked", : "blocked",
semanticBoundary: machineFileRemapReady semanticBoundary: fullLinuxCncProgramExecutionReady
? "linuxcnc_task_motion_hal_wasm_simulation_runtime"
: machineFileRemapReady
? "linuxcnc_machine_file_remap_ready_planner_task_hal_blocked" ? "linuxcnc_machine_file_remap_ready_planner_task_hal_blocked"
: canonicalProgramReady : canonicalProgramReady
? "linuxcnc_interpreter_canonical_ready_planner_task_hal_blocked" ? "linuxcnc_interpreter_canonical_ready_planner_task_hal_blocked"
@@ -85,10 +129,19 @@ export function createFullLinuxCncExecutionBoundary(state = {}) {
remapRuntimeReady: machineFileRemapReady, remapRuntimeReady: machineFileRemapReady,
halSwitchkinsEvidenceReady, halSwitchkinsEvidenceReady,
plannerRuntimeReady, plannerRuntimeReady,
nativeTaskReady: false, taskRuntimeReady,
nativeHalSyncReady: false, motionRuntimeReady,
fullLinuxCncProgramExecutionReady: false, halRuntimeReady,
promotionAllowed: false, halSyncReady,
taskHalComparisonReady,
nativeTaskReady,
nativeHalSyncReady,
fullLinuxCncProgramExecutionReady,
promotionAllowed: fullLinuxCncProgramExecutionReady,
hardwareDrive: false,
hostRealtimeKernel: false,
externalUserMProcessReady: false,
toolDbProcessReady: false,
satisfied, satisfied,
missing, missing,
blockers, blockers,
@@ -101,6 +154,11 @@ export function createFullLinuxCncExecutionBoundary(state = {}) {
machineFileFlags: MACHINE_FILE_FLAGS.filter((flag) => machineFileText.includes(flag)), machineFileFlags: MACHINE_FILE_FLAGS.filter((flag) => machineFileText.includes(flag)),
machineFileExecutionReady: machineFileExecution?.summary?.machineFileExecutionReady === true, machineFileExecutionReady: machineFileExecution?.summary?.machineFileExecutionReady === true,
stagedFileCount: state.machineFileStaging?.fileCount || 0, stagedFileCount: state.machineFileStaging?.fileCount || 0,
taskHal: taskHalSummary,
taskCycle: state.taskHalStatus?.ui?.taskCycle || 0,
servoCycle: state.taskHalStatus?.ui?.servoCycle || 0,
motionQueueDepth: state.taskHalStatus?.ui?.motionQueueDepth || 0,
halChangedPinCount: state.taskHalStatus?.ui?.halChangedPinCount || 0,
}, },
}; };
} }

View File

@@ -52,6 +52,13 @@ export async function createMachineFileStagingPlan({
kind: classifySourceRel(file.sourceRel), kind: classifySourceRel(file.sourceRel),
})), })),
summary: summarizePlan(files), summary: summarizePlan(files),
taskHalSession: createTaskHalSession({
profileId: profile.id,
wasmDir: plan.wasmDir,
iniPath: plan.iniPath,
programPath: plan.programPath,
files,
}),
semanticBoundary: "linuxcnc_sim_config_file_staging_plan_only", semanticBoundary: "linuxcnc_sim_config_file_staging_plan_only",
}; };
} }
@@ -86,6 +93,11 @@ export function selectMachineFileProgram(plan, save, sourceRel) {
selectedProgramSourceRel: selectedFile.sourceRel, selectedProgramSourceRel: selectedFile.sourceRel,
selectedProgramFilename: basename(selectedFile.sourceRel), selectedProgramFilename: basename(selectedFile.sourceRel),
selectedProgramBytes: selectedFile.bytes, selectedProgramBytes: selectedFile.bytes,
taskHalSession: {
...(plan.taskHalSession || {}),
programPath: selectedFile.wasmPath || selectedFile.path,
programSourceRel: selectedFile.sourceRel,
},
semanticBoundary: "linuxcnc_sim_config_file_staging_plan_with_selected_gcode_source", semanticBoundary: "linuxcnc_sim_config_file_staging_plan_with_selected_gcode_source",
}; };
} }
@@ -120,6 +132,16 @@ export async function saveMachineFileStagingPlan(plan, options = {}) {
files: savedFiles, files: savedFiles,
gcodeSources: listLinuxCncGcodeSources({ files: savedFiles }), gcodeSources: listLinuxCncGcodeSources({ files: savedFiles }),
summary: summarizeSavedFiles(savedFiles), summary: summarizeSavedFiles(savedFiles),
taskHalSession: {
...(plan.taskHalSession || {}),
files: savedFiles.map((file) => ({
sourceRel: file.sourceRel,
wasmPath: file.wasmPath,
path: file.wasmPath,
kind: file.kind,
bytes: file.bytes,
})),
},
semanticBoundary: "opfs_machine_file_text_staging_only", semanticBoundary: "opfs_machine_file_text_staging_only",
}; };
} }
@@ -150,6 +172,20 @@ function summarizePlan(files) {
}; };
} }
function createTaskHalSession({ profileId, wasmDir, iniPath, programPath, files }) {
return {
apiName: "web-rtcp-5axis-task-hal-session-plan",
semanticBoundary: "linuxcnc_machine_file_session_for_task_hal_wasm",
profileId,
wasmDir,
iniPath,
programPath,
halFiles: files.filter((file) => classifySourceRel(file.sourceRel) === "hal").map((file) => file.wasmPath),
remapFiles: files.filter((file) => classifySourceRel(file.sourceRel) === "remap").map((file) => file.wasmPath),
toolTableFiles: files.filter((file) => classifySourceRel(file.sourceRel) === "toolTable").map((file) => file.wasmPath),
};
}
function addVendoredDemoSources(files, manifestText, wasmDir) { function addVendoredDemoSources(files, manifestText, wasmDir) {
const bySourceRel = new Map(files.map((file) => [file.sourceRel, file])); const bySourceRel = new Map(files.map((file) => [file.sourceRel, file]));
for (const sourceRel of String(manifestText).split(/\r?\n/)) { for (const sourceRel of String(manifestText).split(/\r?\n/)) {

View File

@@ -0,0 +1,210 @@
const DEFAULT_SDK_MODULE_URLS = [
new URL("../../../../wasm-port/runtime/sdk/src/linuxcnc-task-hal.js", import.meta.url).href,
new URL("../../wasm-port/runtime/sdk/src/linuxcnc-task-hal.js", import.meta.url).href,
];
const SEMANTIC_BOUNDARY = "linuxcnc_task_motion_hal_wasm_simulation_runtime";
export async function createLinuxCncTaskHalRuntime({
sdkModuleUrl = null,
moduleOptions = {},
} = {}) {
const errors = [];
const candidateUrls = sdkModuleUrl ? [sdkModuleUrl] : DEFAULT_SDK_MODULE_URLS;
for (const url of candidateUrls) {
try {
const { createLinuxCncTaskHalSdk } = await import(url);
const sdk = await createLinuxCncTaskHalSdk(moduleOptions);
return wrapTaskHalSdk(sdk, {
sdkModuleUrl: url,
executionContext: "direct",
});
} catch (error) {
errors.push(`${url}: ${error instanceof Error ? error.message : String(error)}`);
}
}
throw new Error(`LinuxCNC task/HAL runtime unavailable: ${errors.join(" | ")}`);
}
export function wrapTaskHalSdk(sdk, {
sdkModuleUrl = null,
executionContext = "direct",
workerUrl = null,
} = {}) {
if (!sdk || typeof sdk.readiness !== "function") {
throw new Error("wrapTaskHalSdk requires a task/HAL SDK");
}
return {
apiName: "web-rtcp-5axis-linuxcnc-task-hal-runtime",
semanticBoundary: SEMANTIC_BOUNDARY,
executionContext,
sdkModuleUrl,
workerUrl,
loaded: true,
readiness() {
const readiness = sdk.readiness();
const taskRuntimeReady = readiness.taskRuntimeReady === true;
const motionRuntimeReady = readiness.motionRuntimeReady === true;
const halRuntimeReady = readiness.halRuntimeReady === true;
return {
apiName: "web-rtcp-5axis-linuxcnc-task-hal-runtime-readiness",
loaded: true,
semanticBoundary: SEMANTIC_BOUNDARY,
sdkSemanticBoundary: readiness.semanticBoundary,
executionContext,
workerUrl,
taskRuntimeReady,
motionRuntimeReady,
halRuntimeReady,
halSyncReady: taskRuntimeReady && motionRuntimeReady && halRuntimeReady,
nativeTaskReady: taskRuntimeReady,
nativeHalSyncReady: taskRuntimeReady && motionRuntimeReady && halRuntimeReady,
hardwareDrive: false,
hostRealtimeKernel: false,
externalUserMProcessReady: false,
};
},
initSession(session = {}) {
return sdk.initSession(session);
},
stageFiles(files = []) {
let staged = 0;
for (const file of files) {
const path = file.wasmPath || file.path;
if (!path) continue;
const rc = sdk.stageFile(path, file.text || "");
if (rc !== 0) {
throw new Error(`lctask_stage_file failed for ${path} rc=${rc}`);
}
staged += 1;
}
return staged;
},
openProgram(path) {
const rc = sdk.openProgram(path);
if (rc !== 0) {
throw new Error(`lctask_open_program failed for ${path} rc=${rc}`);
}
return rc;
},
sendCommand(command) {
const rc = sdk.sendCommand(command);
if (rc !== 0) {
throw new Error(`lctask_send_command_json failed for ${command?.type || "unknown"} rc=${rc}`);
}
return rc;
},
runCycles(options = {}) {
const rc = sdk.runCycles(options);
if (rc !== 0) {
throw new Error(`lctask_run_cycles failed rc=${rc}`);
}
return rc;
},
readStatus() {
return normalizeTaskHalStatus(sdk.readStatus());
},
readEvents() {
return sdk.readEvents();
},
resetSession() {
return sdk.resetSession();
},
};
}
export function buildTaskHalSessionFromMachineFiles({ profile, plan, save, selectedProgramRel = null } = {}) {
const files = save?.files || [];
const iniFile = files.find((file) => file.kind === "ini")
|| files.find((file) => file.sourceRel === profile?.iniPath)
|| null;
const selectedFile = selectedProgramRel
? files.find((file) => file.sourceRel === selectedProgramRel)
: null;
const programFile = selectedFile
|| files.find((file) => file.wasmPath === plan?.wasmProgramPath)
|| files.find((file) => file.kind === "demo")
|| null;
return {
apiName: "web-rtcp-5axis-task-hal-session",
semanticBoundary: "linuxcnc_machine_files_for_task_hal_wasm_runtime",
profileId: profile?.id || plan?.profileId || save?.profileId || "unknown",
iniPath: iniFile?.wasmPath || plan?.wasmIniPath || plan?.iniPath || profile?.iniPath || null,
iniText: iniFile?.text || "",
programPath: programFile?.wasmPath || plan?.wasmProgramPath || null,
programSourceRel: programFile?.sourceRel || selectedProgramRel || null,
halFiles: files.filter((file) => file.kind === "hal").map(sessionFileDescriptor),
toolTableFiles: files.filter((file) => file.kind === "toolTable").map(sessionFileDescriptor),
remapFiles: files.filter((file) => file.kind === "remap").map(sessionFileDescriptor),
files: files.map(sessionFileDescriptor),
fileCount: files.length,
};
}
export function normalizeTaskHalStatus(status = {}) {
const motion = status.motionStatus?.motion || {};
const axis = status.motionStatus?.axis || {};
const halPins = status.halSnapshot?.pins || {};
return {
...status,
semanticBoundary: SEMANTIC_BOUNDARY,
summary: {
taskRuntimeReady: status.taskRuntimeReady === true,
motionRuntimeReady: status.motionStatus?.motionHalSyncReady === true || status.taskCommandsDriveMotionRuntime === true,
halRuntimeReady: Boolean(status.halSnapshot?.halRuntimeReady ?? status.halSnapshot?.ready ?? true),
halSyncReady: status.taskCommandsDriveMotionRuntime === true && Boolean(halPins["motion.program-line"]),
taskHalComparisonReady: status.taskRuntimeReady === true && status.taskCommandsDriveMotionRuntime === true,
switchkinsRemapHalSync: Boolean(halPins["motion.switchkins-type"]),
nativeTaskReady: status.taskRuntimeReady === true,
nativeHalSyncReady: status.taskCommandsDriveMotionRuntime === true && Boolean(halPins["motion.program-line"]),
fullLinuxCncProgramExecutionReady: false,
hardwareDrive: false,
hostRealtimeKernel: false,
},
ui: {
taskState: String(status.task?.state || "ESTOP").toLowerCase(),
taskMode: String(status.task?.mode || "MANUAL").toLowerCase(),
interpState: String(status.task?.interpState || "IDLE").toLowerCase(),
execState: String(status.task?.execState || "DONE").toLowerCase(),
taskCycle: Number(status.task?.taskCycle ?? status.taskCycle ?? 0),
servoCycle: Number(status.motionStatus?.cycle ?? status.task?.servoCycle ?? 0),
motionQueueDepth: Number(status.motionStatus?.queueDepth ?? motion.queueDepth ?? 0),
halChangedPinCount: Array.isArray(status.halSnapshot?.changedPins)
? status.halSnapshot.changedPins.length
: Number(status.halSnapshot?.changedPinCount || 0),
activeLine: Number(motion.programLine || halPins["motion.program-line"]?.value || 1),
switchkinsType: Number(motion.switchkinsType ?? halPins["motion.switchkins-type"]?.value ?? 0),
axisPose: {
x: Number(axis.x ?? halPins["axis.0.pos-cmd"]?.value ?? 0),
y: Number(axis.y ?? halPins["axis.1.pos-cmd"]?.value ?? 0),
z: Number(axis.z ?? halPins["axis.2.pos-cmd"]?.value ?? 0),
a: Number(axis.a ?? halPins["axis.3.pos-cmd"]?.value ?? 0),
b: Number(axis.b ?? halPins["axis.4.pos-cmd"]?.value ?? 0),
c: Number(axis.c ?? halPins["axis.5.pos-cmd"]?.value ?? 0),
},
currentVelocity: Number(motion.currentVel || motion.currentVelocity || 0) * 60,
},
};
}
function sessionFileDescriptor(file) {
return {
sourceRel: file.sourceRel,
wasmPath: file.wasmPath || file.path,
path: file.wasmPath || file.path,
kind: file.kind,
bytes: Number(file.bytes || String(file.text || "").length),
text: file.text || "",
};
}

View File

@@ -0,0 +1,70 @@
import { wrapTaskHalSdk } from "./linuxcnc-task-hal-runtime.js";
export async function createLinuxCncTaskHalWorkerRuntime({
workerUrl = new URL("./linuxcnc-task-hal-worker.js", import.meta.url).href,
sdkModuleUrl = null,
moduleOptions = {},
} = {}) {
if (typeof Worker !== "function") {
throw new Error("Worker is not available");
}
const worker = new Worker(workerUrl, { type: "module" });
const client = createWorkerClient(worker);
await client.call("init", { sdkModuleUrl, moduleOptions });
return {
apiName: "web-rtcp-5axis-linuxcnc-task-hal-worker-runtime",
semanticBoundary: "linuxcnc_task_motion_hal_wasm_simulation_runtime",
executionContext: "worker",
workerUrl,
loaded: true,
readiness: () => client.call("readiness"),
initSession: (session) => client.call("initSession", { session }),
stageFiles: (files) => client.call("stageFiles", { files }),
openProgram: (path) => client.call("openProgram", { path }),
sendCommand: (command) => client.call("command", { command }),
runCycles: (options) => client.call("runCycles", { options }),
readStatus: () => client.call("readStatus"),
readEvents: () => client.call("readEvents"),
resetSession: () => client.call("reset"),
terminate: () => worker.terminate(),
};
}
export function createDirectTaskHalRuntimeFromSdk(sdk) {
return wrapTaskHalSdk(sdk, { executionContext: "direct" });
}
function createWorkerClient(worker) {
let nextId = 1;
const pending = new Map();
worker.addEventListener("message", (event) => {
const { id, ok, result, error } = event.data || {};
const request = pending.get(id);
if (!request) return;
pending.delete(id);
if (ok) {
request.resolve(result);
} else {
request.reject(new Error(error || "LinuxCNC task/HAL worker failed"));
}
});
worker.addEventListener("error", (event) => {
for (const request of pending.values()) {
request.reject(new Error(event.message || "LinuxCNC task/HAL worker error"));
}
pending.clear();
});
return {
call(type, payload = {}) {
const id = nextId;
nextId += 1;
return new Promise((resolve, reject) => {
pending.set(id, { resolve, reject });
worker.postMessage({ id, type, payload });
});
},
};
}

View File

@@ -0,0 +1,60 @@
import { createLinuxCncTaskHalRuntime } from "./linuxcnc-task-hal-runtime.js";
let runtime = null;
self.addEventListener("message", async (event) => {
const { id, type, payload = {} } = event.data || {};
try {
const result = await handleMessage(type, payload);
self.postMessage({ id, ok: true, result });
} catch (error) {
self.postMessage({
id,
ok: false,
error: error instanceof Error ? error.message : String(error),
});
}
});
async function handleMessage(type, payload) {
switch (type) {
case "init":
runtime = await createLinuxCncTaskHalRuntime(payload);
return runtime.readiness();
case "readiness":
assertRuntime();
return runtime.readiness();
case "stageFiles":
assertRuntime();
return runtime.stageFiles(payload.files || []);
case "initSession":
assertRuntime();
return runtime.initSession(payload.session || {});
case "openProgram":
assertRuntime();
return runtime.openProgram(payload.path);
case "command":
assertRuntime();
return runtime.sendCommand(payload.command);
case "runCycles":
assertRuntime();
return runtime.runCycles(payload.options || {});
case "readStatus":
assertRuntime();
return runtime.readStatus();
case "readEvents":
assertRuntime();
return runtime.readEvents();
case "reset":
assertRuntime();
return runtime.resetSession();
default:
throw new Error(`Unknown task/HAL worker message: ${type}`);
}
}
function assertRuntime() {
if (!runtime) {
throw new Error("LinuxCNC task/HAL worker runtime is not initialized");
}
}

View File

@@ -16,6 +16,9 @@ import {
selectMachineFileProgram, selectMachineFileProgram,
stageProfileMachineFiles, stageProfileMachineFiles,
} from "../runtime/linuxcnc-machine-file-staging.js"; } from "../runtime/linuxcnc-machine-file-staging.js";
import {
buildTaskHalSessionFromMachineFiles,
} from "../runtime/linuxcnc-task-hal-runtime.js";
import { import {
createLinuxCncTaskPolicyStatus, createLinuxCncTaskPolicyStatus,
gateLinuxCncTaskAction, gateLinuxCncTaskAction,
@@ -128,6 +131,13 @@ const initialState = {
programExecutionMotionIndex: 0, programExecutionMotionIndex: 0,
programExecutionSampleIndex: 0, programExecutionSampleIndex: 0,
programRuntimeFeedback: null, programRuntimeFeedback: null,
taskHalRuntime: null,
taskHalRuntimeReadiness: null,
taskHalStatus: null,
taskHalSession: null,
taskHalExecutionPending: false,
taskHalExecutionSequence: 0,
taskHalFallbackReason: null,
interpreterExecutionPending: false, interpreterExecutionPending: false,
interpreterExecutionSequence: 0, interpreterExecutionSequence: 0,
machineFileExecution: null, machineFileExecution: null,
@@ -404,6 +414,50 @@ export function createSimulationStore(seed = {}) {
}); });
} }
break; break;
case "ATTACH_TASK_HAL_RUNTIME":
{
const runtime = action.runtime || null;
const maybeReadiness = action.readiness || (runtime?.readiness ? runtime.readiness() : null);
const readiness = typeof maybeReadiness?.then === "function"
? {
apiName: "web-rtcp-5axis-linuxcnc-task-hal-runtime-readiness",
loaded: Boolean(runtime?.loaded),
taskRuntimeReady: false,
motionRuntimeReady: false,
halRuntimeReady: false,
pending: true,
}
: maybeReadiness;
setState({
taskHalRuntime: runtime,
taskHalRuntimeReadiness: readiness,
taskHalFallbackReason: runtime?.loaded ? null : "LinuxCNC task/HAL runtime missing",
operatorMessage: runtime?.loaded
? "LinuxCNC task/HAL runtime ready"
: "LinuxCNC task/HAL runtime missing",
});
if (runtime?.loaded && state.machineFileStaging?.status === "staged") {
initializeTaskHalSession().catch(() => {});
}
}
break;
case "TASK_HAL_RUNTIME_FAILED":
setState({
taskHalRuntime: null,
taskHalRuntimeReadiness: {
apiName: "web-rtcp-5axis-linuxcnc-task-hal-runtime-readiness",
loaded: false,
taskRuntimeReady: false,
motionRuntimeReady: false,
halRuntimeReady: false,
nativeTaskReady: false,
nativeHalSyncReady: false,
error: action.error,
},
taskHalFallbackReason: action.error,
operatorMessage: `LinuxCNC task/HAL runtime blocked: ${action.error}`,
});
break;
case "RUN_INTERPRETER_PROGRAM": case "RUN_INTERPRETER_PROGRAM":
if (!state.interpreterRuntime?.loaded) { if (!state.interpreterRuntime?.loaded) {
setState({ setState({
@@ -657,6 +711,9 @@ export function createSimulationStore(seed = {}) {
}, },
operatorMessage: `LinuxCNC machine files staged ${action.save.fileCount}`, operatorMessage: `LinuxCNC machine files staged ${action.save.fileCount}`,
}); });
if (state.taskHalRuntime?.loaded) {
initializeTaskHalSession().catch(() => {});
}
break; break;
case "LOAD_LINUXCNC_GCODE_SOURCE": case "LOAD_LINUXCNC_GCODE_SOURCE":
{ {
@@ -700,11 +757,31 @@ export function createSimulationStore(seed = {}) {
}, },
operatorMessage: `loaded LinuxCNC 5-axis source ${selectedFile.sourceRel}`, operatorMessage: `loaded LinuxCNC 5-axis source ${selectedFile.sourceRel}`,
}); });
if (state.taskHalRuntime?.loaded) {
initializeTaskHalSession({ openProgram: true }).catch(() => {});
}
if (state.interpreterRuntime?.loaded) { if (state.interpreterRuntime?.loaded) {
dispatch({ type: "RUN_INTERPRETER_PROGRAM" }); dispatch({ type: "RUN_INTERPRETER_PROGRAM" });
} }
} }
break; break;
case "TASK_HAL_SESSION_READY":
setState({
taskHalSession: action.session,
taskHalFallbackReason: null,
operatorMessage: `LinuxCNC task/HAL session ready ${action.session.programPath || "-"}`,
});
break;
case "TASK_HAL_STATUS_APPLIED":
setState(applyTaskHalStatusPatch(state, action.status, action.operatorMessage));
break;
case "TASK_HAL_COMMAND_FAILED":
setState({
taskHalFallbackReason: action.error,
taskHalExecutionPending: false,
operatorMessage: `task/HAL fallback: ${action.error}`,
});
break;
case "MACHINE_FILE_STAGING_FAILED": case "MACHINE_FILE_STAGING_FAILED":
setState({ setState({
machineFileStaging: { machineFileStaging: {
@@ -745,6 +822,12 @@ export function createSimulationStore(seed = {}) {
setState({ operatorMessage: gate.operatorMessage }); setState({ operatorMessage: gate.operatorMessage });
break; break;
} }
if (state.taskHalRuntime?.loaded) {
runTaskHalCommandSequence([
{ type: "EMC_TASK_SET_STATE", state: state.machine.powerOn ? "ESTOP_RESET" : "ON" },
], { operatorMessage: state.machine.powerOn ? "task/HAL machine power off" : "task/HAL machine power on" }).catch(() => {});
break;
}
const turningOff = state.machine.taskState === "on" || state.machine.powerOn; const turningOff = state.machine.taskState === "on" || state.machine.powerOn;
setState({ setState({
machine: { machine: {
@@ -825,6 +908,12 @@ export function createSimulationStore(seed = {}) {
break; break;
} }
const mode = normalizeLinuxCncTaskMode(action.mode); const mode = normalizeLinuxCncTaskMode(action.mode);
if (state.taskHalRuntime?.loaded) {
runTaskHalCommandSequence([
{ type: "EMC_TASK_SET_MODE", mode: mode.toUpperCase() },
], { operatorMessage: `task/HAL mode ${mode}` }).catch(() => {});
break;
}
setState({ setState({
machine: { machine: {
...state.machine, ...state.machine,
@@ -857,6 +946,17 @@ export function createSimulationStore(seed = {}) {
const axis = action.axis || state.machine.jogAxis; const axis = action.axis || state.machine.jogAxis;
const direction = Number(action.direction || 1); const direction = Number(action.direction || 1);
const increment = Number(action.increment || state.machine.jogIncrement); const increment = Number(action.increment || state.machine.jogIncrement);
if (state.taskHalRuntime?.loaded) {
runTaskHalCommandSequence([
{
type: "EMC_JOG_INCR",
axis: axis.toUpperCase(),
distance: direction * increment,
velocity: Number(action.velocity || 60),
},
], { operatorMessage: `task/HAL jog ${axis.toUpperCase()} ${direction > 0 ? "+" : "-"}${increment}` }).catch(() => {});
break;
}
setState({ setState({
machine: { machine: {
...state.machine, ...state.machine,
@@ -880,6 +980,14 @@ export function createSimulationStore(seed = {}) {
setState({ operatorMessage: gate.operatorMessage }); setState({ operatorMessage: gate.operatorMessage });
break; break;
} }
if (state.taskHalRuntime?.loaded) {
const command = normalizeMdiCommand(action.command ?? state.machine.mdiCommand);
runTaskHalCommandSequence([
{ type: "EMC_TASK_SET_MODE", mode: "MDI" },
{ type: "EMC_TASK_PLAN_EXECUTE", mdi: command },
], { operatorMessage: `task/HAL MDI ${command}` }).catch(() => {});
break;
}
const mdiResult = executeMdiCommand(state, action.command ?? state.machine.mdiCommand); const mdiResult = executeMdiCommand(state, action.command ?? state.machine.mdiCommand);
setState(mdiResult.patch); setState(mdiResult.patch);
} }
@@ -917,6 +1025,14 @@ export function createSimulationStore(seed = {}) {
setState({ operatorMessage: gate.operatorMessage }); setState({ operatorMessage: gate.operatorMessage });
break; break;
} }
if (state.taskHalRuntime?.loaded) {
runTaskHalCommandSequence([
{ type: "EMC_TASK_SET_STATE", state: "ON" },
{ type: "EMC_TASK_SET_MODE", mode: "AUTO" },
{ type: "EMC_TASK_PLAN_RUN", line: Math.max(Number(state.activeLine || 1) - 1, 0) },
], { taskCycles: 5, operatorMessage: "task/HAL program run" }).catch(() => {});
break;
}
const playback = nextProgramRuntimeSamplePlayback(state, 5); const playback = nextProgramRuntimeSamplePlayback(state, 5);
setState({ setState({
machine: { machine: {
@@ -952,6 +1068,12 @@ export function createSimulationStore(seed = {}) {
setState({ operatorMessage: gate.operatorMessage }); setState({ operatorMessage: gate.operatorMessage });
break; break;
} }
if (state.taskHalRuntime?.loaded) {
runTaskHalCommandSequence([
{ type: "EMC_TASK_ABORT" },
], { operatorMessage: action.type === "ABORT" ? "task/HAL abort complete" : "task/HAL program stopped" }).catch(() => {});
break;
}
setState({ setState({
machine: { machine: {
...state.machine, ...state.machine,
@@ -975,6 +1097,12 @@ export function createSimulationStore(seed = {}) {
setState({ operatorMessage: gate.operatorMessage }); setState({ operatorMessage: gate.operatorMessage });
break; break;
} }
if (state.taskHalRuntime?.loaded) {
runTaskHalCommandSequence([
{ type: "EMC_TASK_PLAN_PAUSE" },
], { operatorMessage: "task/HAL program paused" }).catch(() => {});
break;
}
setState({ setState({
machine: { machine: {
...state.machine, ...state.machine,
@@ -996,6 +1124,12 @@ export function createSimulationStore(seed = {}) {
setState({ operatorMessage: gate.operatorMessage }); setState({ operatorMessage: gate.operatorMessage });
break; break;
} }
if (state.taskHalRuntime?.loaded) {
runTaskHalCommandSequence([
{ type: "EMC_TASK_PLAN_RESUME" },
], { operatorMessage: "task/HAL program resumed" }).catch(() => {});
break;
}
const resumeState = state.machine.interpResumeState === "idle" const resumeState = state.machine.interpResumeState === "idle"
? "reading" ? "reading"
: state.machine.interpResumeState; : state.machine.interpResumeState;
@@ -1333,6 +1467,114 @@ export function createSimulationStore(seed = {}) {
dispatch({ type: "RUN_MACHINE_FILE_PROGRAM" }); dispatch({ type: "RUN_MACHINE_FILE_PROGRAM" });
}; };
const initializeTaskHalSession = async ({ openProgram = true } = {}) => {
if (!state.taskHalRuntime?.loaded || !state.machineFileStaging?.save?.files?.length) {
return null;
}
const selectedPlan = selectMachineFileProgramForState(state);
const session = buildTaskHalSessionFromMachineFiles({
profile: state.profile,
plan: selectedPlan,
save: state.machineFileStaging.save,
selectedProgramRel: state.machineFileStaging.selectedGcodeSourceRel,
});
await state.taskHalRuntime.resetSession?.();
await state.taskHalRuntime.initSession({
profileId: session.profileId,
iniPath: session.iniPath,
iniText: session.iniText,
programPath: session.programPath,
semanticBoundary: session.semanticBoundary,
});
await state.taskHalRuntime.stageFiles(session.files);
if (openProgram && session.programPath) {
await state.taskHalRuntime.openProgram(session.programPath);
}
dispatch({ type: "TASK_HAL_SESSION_READY", session });
const status = await state.taskHalRuntime.readStatus();
dispatch({
type: "TASK_HAL_STATUS_APPLIED",
status,
operatorMessage: `LinuxCNC task/HAL session ready ${session.programPath || "-"}`,
});
return session;
};
const runTaskHalCommandSequence = async (commands, {
taskCycles = 1,
taskPeriodNs = 10000000,
servoPeriodNs = 1000000,
operatorMessage = "task/HAL command complete",
} = {}) => {
if (!state.taskHalRuntime?.loaded) {
throw new Error("LinuxCNC task/HAL runtime not attached");
}
const sequence = state.taskHalExecutionSequence + 1;
setState({
taskHalExecutionPending: true,
taskHalExecutionSequence: sequence,
operatorMessage: "LinuxCNC task/HAL command running",
});
try {
if (!state.taskHalSession && state.machineFileStaging?.save?.files?.length) {
await initializeTaskHalSession({ openProgram: true });
} else if (!state.taskHalSession && state.programLines?.length) {
await initializeFixtureTaskHalSessionForState();
}
for (const command of commands) {
await state.taskHalRuntime.sendCommand(command);
}
await state.taskHalRuntime.runCycles({ taskPeriodNs, servoPeriodNs, taskCycles });
const status = await state.taskHalRuntime.readStatus();
if (state.taskHalExecutionSequence !== sequence) {
return status;
}
dispatch({ type: "TASK_HAL_STATUS_APPLIED", status, operatorMessage });
return status;
} catch (error) {
dispatch({
type: "TASK_HAL_COMMAND_FAILED",
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
};
const initializeFixtureTaskHalSessionForState = async () => {
if (!state.taskHalRuntime?.loaded) return null;
const programPath = "web-ui/current-program.ngc";
const iniText = state.linuxCncIniConfig?.sourceText || `[TRAJ]\nCOORDINATES = ${state.profile.traj.coordinates.split("").join(" ")}\n`;
await state.taskHalRuntime.resetSession?.();
await state.taskHalRuntime.initSession({
profileId: state.machineProfile,
iniPath: state.profile.iniPath,
iniText,
programPath,
semanticBoundary: "linuxcnc_task_hal_fixture_program_session",
});
await state.taskHalRuntime.stageFiles([
{
sourceRel: state.programSourceRel || state.activeProgram,
wasmPath: programPath,
path: programPath,
kind: "demo",
text: state.programLines.join("\n"),
bytes: state.programLines.join("\n").length,
},
]);
await state.taskHalRuntime.openProgram(programPath);
const session = {
apiName: "web-rtcp-5axis-task-hal-fixture-session",
semanticBoundary: "linuxcnc_task_hal_fixture_program_session",
profileId: state.machineProfile,
iniPath: state.profile.iniPath,
programPath,
fileCount: 1,
};
dispatch({ type: "TASK_HAL_SESSION_READY", session });
return session;
};
const scheduleAsyncKinematicsRefresh = () => { const scheduleAsyncKinematicsRefresh = () => {
if (!state.kinematicsRuntime?.loaded || !isAsyncKinematicsRuntime(state.kinematicsRuntime)) return null; if (!state.kinematicsRuntime?.loaded || !isAsyncKinematicsRuntime(state.kinematicsRuntime)) return null;
if (state.frameSourceMode !== "source-derived-kinematics-wasm") return null; if (state.frameSourceMode !== "source-derived-kinematics-wasm") return null;
@@ -1360,6 +1602,7 @@ export function createSimulationStore(seed = {}) {
restoreSession, restoreSession,
stageMachineFiles, stageMachineFiles,
runFullBoundaryAudit, runFullBoundaryAudit,
initializeTaskHalSession,
}; };
} }
@@ -1454,6 +1697,115 @@ function isAsyncKinematicsRuntime(runtime) {
return runtime?.executionContext === "worker"; return runtime?.executionContext === "worker";
} }
function applyTaskHalStatusPatch(state, status, operatorMessage) {
const ui = status?.ui || {};
const task = status?.task || {};
const motion = status?.motionStatus?.motion || {};
const taskState = normalizeTaskHalTaskState(ui.taskState || task.state);
const taskMode = normalizeLinuxCncTaskMode(ui.taskMode || task.mode || state.machine.mode);
const interpState = normalizeTaskHalInterpState(ui.interpState || task.interpState);
const activeLine = state.programStartLine + Math.max(Number(ui.activeLine || 1) - 1, 0);
const kinsType = kinsTypeFromSwitchkinsTypeValue(state, ui.switchkinsType);
const axisPose = clampAxisPoseToProfile({
...state.axisPose,
...ui.axisPose,
}, state.profile);
const currentVelocity = Number.isFinite(ui.currentVelocity) && ui.currentVelocity > 0
? ui.currentVelocity
: state.feed.currentVelocity;
const paused = interpState === "paused" || motion.paused === true;
const aborted = motion.aborted === true;
const programComplete = interpState === "idle" && Number(task.nextProgramLine || 0) >= Number(task.openedLineCount || 1);
const runState = aborted
? "stopped"
: paused
? "paused"
: taskMode === "mdi"
? "mdi"
: interpState === "reading"
? "running"
: programComplete
? "complete"
: state.runState === "jogging"
? "jogging"
: "idle";
return {
taskHalStatus: status,
taskHalExecutionPending: false,
taskHalFallbackReason: null,
activeLine,
axisPose,
kinsType,
rtcpState: rtcpStateFromKinsType(kinsType),
programExecutionSourceMode: "linuxcnc-task-motion-hal-wasm",
machine: {
...state.machine,
powerOn: taskState === "on",
estopActive: taskState === "estop",
taskState,
mode: taskMode,
interpState,
interpResumeState: paused ? state.machine.interpResumeState || "reading" : interpState,
taskPaused: paused,
},
runState,
feed: {
...state.feed,
currentVelocity,
},
programRuntimeFeedback: createTaskHalRuntimeFeedback(state, status, axisPose, activeLine),
operatorMessage,
};
}
function createTaskHalRuntimeFeedback(state, status, axisPose, activeLine) {
const ui = status?.ui || {};
const motion = status?.motionStatus?.motion || {};
return {
apiName: "web-rtcp-5axis-program-runtime-feedback",
sourceMode: "linuxcnc-task-motion-hal-wasm",
semanticBoundary: status?.semanticBoundary || "linuxcnc_task_motion_hal_wasm_simulation_runtime",
sampleIndex: Number(ui.servoCycle || 0),
motionIndex: Math.max(Number(ui.activeLine || 1) - 1, 0),
line: activeLine,
type: Number(motion.motionType || 0) === 3 ? "JOG" : "TASK_MOTION",
timeSeconds: Number(ui.taskCycle || 0) * 0.01,
axisPose,
currentVelocityMmPerMin: Number(ui.currentVelocity || 0),
requestedVelocityMmPerMin: Number(motion.requestedVel || 0) * 60,
distanceToGo: motion.inPosition === true ? 0 : 1,
dtg: { x: 0, y: 0, z: 0 },
queueDepth: Number(ui.motionQueueDepth || status?.motionStatus?.commandQueueDepth || 0),
activeDepth: motion.inPosition === true ? 0 : 1,
cycle: Number(ui.servoCycle || status?.servoCycle || 0),
taskCycle: Number(ui.taskCycle || status?.task?.cycle || 0),
halChangedPinCount: Number(ui.halChangedPinCount || 0),
};
}
function normalizeTaskHalTaskState(value) {
const state = String(value || "").toLowerCase().replaceAll("_", "-");
if (state === "on") return "on";
if (state === "estop") return "estop";
if (state === "off") return "off";
return "estop-reset";
}
function normalizeTaskHalInterpState(value) {
const state = String(value || "").toLowerCase();
if (state === "paused") return "paused";
if (state === "reading") return "reading";
return "idle";
}
function kinsTypeFromSwitchkinsTypeValue(state, value) {
const numeric = Number(value);
if (!Number.isFinite(numeric) || numeric === 0) return "identity";
return state.profile.kinematicsParameters.switchkinsTypes
.find((type) => Number(type.value) === numeric)?.webKinsType || state.kinsType;
}
function canMoveMachine(state) { function canMoveMachine(state) {
return createLinuxCncTaskPolicyStatus(state).canMove; return createLinuxCncTaskPolicyStatus(state).canMove;
} }

View File

@@ -343,6 +343,8 @@ function renderInfoTabs(element, state) {
<dt>INI joints:</dt><dd data-linuxcnc-ini="joints">${state.iniConfigReadiness.jointCount ?? 0} joints / ${state.iniConfigReadiness.axisCount ?? 0} axes</dd> <dt>INI joints:</dt><dd data-linuxcnc-ini="joints">${state.iniConfigReadiness.jointCount ?? 0} joints / ${state.iniConfigReadiness.axisCount ?? 0} axes</dd>
<dt>Machine files:</dt><dd data-machine-file-staging="status">${formatMachineFileStaging(state.machineFileStaging)}</dd> <dt>Machine files:</dt><dd data-machine-file-staging="status">${formatMachineFileStaging(state.machineFileStaging)}</dd>
<dt>LinuxCNC G-code:</dt><dd data-linuxcnc-gcode-source="selected">${state.machineFileStaging.selectedGcodeSourceRel || state.programSourceRel || "-"}</dd> <dt>LinuxCNC G-code:</dt><dd data-linuxcnc-gcode-source="selected">${state.machineFileStaging.selectedGcodeSourceRel || state.programSourceRel || "-"}</dd>
<dt>Task/HAL:</dt><dd data-task-hal-runtime="readiness">${formatTaskHalReadiness(state)}</dd>
<dt>Task/HAL cycles:</dt><dd data-task-hal-runtime="cycles">${formatTaskHalCycles(state)}</dd>
<dt>Full boundary:</dt><dd data-full-execution-boundary="status">${formatFullExecutionBoundary(state.fullExecutionBoundary)}</dd> <dt>Full boundary:</dt><dd data-full-execution-boundary="status">${formatFullExecutionBoundary(state.fullExecutionBoundary)}</dd>
<dt>Planner/task:</dt><dd data-full-execution-boundary="blockers">${formatFullExecutionBlockers(state.fullExecutionBoundary)}</dd> <dt>Planner/task:</dt><dd data-full-execution-boundary="blockers">${formatFullExecutionBlockers(state.fullExecutionBoundary)}</dd>
<dt>Boundary evidence:</dt><dd data-full-execution-boundary="evidence">${formatFullExecutionEvidence(state.fullExecutionBoundary)}</dd> <dt>Boundary evidence:</dt><dd data-full-execution-boundary="evidence">${formatFullExecutionEvidence(state.fullExecutionBoundary)}</dd>
@@ -434,6 +436,24 @@ function formatMachineFileExecution(machineFileExecution) {
return `${summary.machineFileExecutionReady ? "ready" : "ran"} ${summary.motionEventCount || 0} motion ${machineFileExecution.machineFilePlan.profileId}`; return `${summary.machineFileExecutionReady ? "ready" : "ran"} ${summary.motionEventCount || 0} motion ${machineFileExecution.machineFilePlan.profileId}`;
} }
function formatTaskHalReadiness(state) {
const readiness = state.taskHalRuntimeReadiness;
if (!readiness?.loaded && !state.taskHalStatus) return "pending";
return [
readiness?.taskRuntimeReady ? "task" : "task pending",
readiness?.motionRuntimeReady ? "motion" : "motion pending",
readiness?.halRuntimeReady ? "hal" : "hal pending",
state.taskHalStatus?.summary?.halSyncReady ? "sync" : "sync pending",
state.taskHalFallbackReason ? `fallback ${state.taskHalFallbackReason}` : null,
].filter(Boolean).join(" / ");
}
function formatTaskHalCycles(state) {
const status = state.taskHalStatus;
if (!status?.ui) return "pending";
return `task ${status.ui.taskCycle} / servo ${status.ui.servoCycle} / queue ${status.ui.motionQueueDepth} / HAL changed ${status.ui.halChangedPinCount}`;
}
function formatFullExecutionBoundary(boundary) { function formatFullExecutionBoundary(boundary) {
if (!boundary) return "pending"; if (!boundary) return "pending";
const remap = boundary.machineFileBackedRemapReady ? "remap ready" : "remap pending"; const remap = boundary.machineFileBackedRemapReady ? "remap ready" : "remap pending";

View File

@@ -362,10 +362,30 @@ full_program_execution=partial_interpreter_canonical_and_switchkins_event_ready_
session_persistence=implemented_opfs_save_restore_for_5axis_session session_persistence=implemented_opfs_save_restore_for_5axis_session
machine_file_staging=implemented_linuxcnc_trt_ini_hal_tool_table_remap_demo_opfs_staging machine_file_staging=implemented_linuxcnc_trt_ini_hal_tool_table_remap_demo_opfs_staging
machine_file_backed_run=implemented_linuxcnc_fiveaxis_remap_wasm_machine_file_execution machine_file_backed_run=implemented_linuxcnc_fiveaxis_remap_wasm_machine_file_execution
planner_task_hal_gap=audited_as_blocked_without_native_task_nml_realtime_hal_and_planner_queue_runtime planner_task_hal_gap=closed_for_web_simulation_boundary_with_task_motion_hal_wasm_runtime
next_batch=trajectory_planner_wasm_boundary_or_native_task_hal_port native_task_hal_sync_plan=docs_native_task_hal_sync_implementation_steps_ready
native_task_hal_phase0_8=source_manifest_native_probe_hal_runtime_motion_hal_sync_task_shim_sdk_machine_file_store_ui_full_boundary_smokes_ready
next_batch=hardware_realtime_external_processes_if_required
``` ```
当前 native task/HAL 接续状态:
- Phase 0 source manifest gate 已输出 `task_hal_source_manifest_ready=1`
- Phase 1 TRT native probe 已按 opt-in 模式就绪,默认 `ready_disabled_by_default`,不提升;
- Phase 2 HAL runtime 已覆盖 pin/signal/param/net/thread scheduler 和 `loadusr` blocked evidence
- Phase 3 minimal motion/HAL C ABI 已覆盖 servo cycle、program-line、switchkins、jog/status HAL 同步;
- Phase 4 minimal task/HAL C ABI 已覆盖 `lctask_*` session/stage/open/command/cycle/status/eventsRUN/MDI/JOG/PAUSE/RESUME/ABORT 能从 task shim 驱动 motion/HAL runtime并已补 SDK wrapper
- Phase 5-8 已把 machine-file session、task/HAL runtime adapter、Worker/client 文件、store action mapping、gmoccapy diagnostics 和 full boundary gate 接上;
- `nativeTaskReady=true``nativeHalSyncReady=true` 当前只表示 Web simulation boundary 内的 task/motion/HAL WASM runtime proof仍保持 `hardwareDrive=false``hostRealtimeKernel=false``externalUserMProcessReady=false``toolDbProcessReady=false`
补充实施文档:
```text
docs/native-task-hal-sync-implementation-steps.md
```
该文档给出完成 `nativeTaskReady``nativeHalSyncReady` 的分阶段实现步骤、程序编写顺序、LinuxCNC 源程序参考清单和验收 gate。当前已完成 Web simulation boundary不表示真实硬件、host realtime kernel 或外部进程 runtime 已完成。
## 10. M12 任务 ## 10. M12 任务
```text ```text

View File

@@ -0,0 +1,803 @@
# Native Task 与实时 HAL 同步实现步骤
生成时间2026-06-21 CST
## 1. 目标和边界
本文用于指导后续把当前保留的两个 blocker 推进到可验证完成状态:
```text
nativeTaskReady=false
nativeHalSyncReady=false
```
这里的“完成”不是把浏览器变成真实机床控制器,也不是接入真实硬件 IO目标是建立一个 LinuxCNC 源码拥有语义的 task/HAL 同步 runtime 边界,使 Web 仿真能按 LinuxCNC task、motion、HAL 的顺序运行程序、执行 MDI/JOG、同步 HAL pin并把状态反馈到 gmoccapy Web UI。
完成后的目标状态:
```text
nativeTaskReady=true
nativeHalSyncReady=true
plannerRuntimeReady=true
fullLinuxCncProgramExecutionReady=true for simulated machine-file runtime
hardwareDrive=false
promotionAllowed=true for Web simulation boundary only
```
仍不属于本文范围:
- 真实硬件 IO、Mesa/并口/伺服驱动;
- Linux 内核实时线程等同物;
- 任意外部 user-M 进程;
- Python GUI、GTK、Tk、QtVCP runtime
- 未经过 LinuxCNC 源码边界验证的 JavaScript CNC 语义。
## 2. LinuxCNC 源程序参考清单
后续实现必须以这些源文件为主参考,不能用 Web 侧自定义状态机替代 CNC 语义:
```text
linuxcnc/src/emc/task/task.hh
linuxcnc/src/emc/task/emctask.cc
linuxcnc/src/emc/task/emctaskmain.cc
linuxcnc/src/emc/task/taskintf.cc
linuxcnc/src/emc/task/emccanon.cc
linuxcnc/src/emc/nml_intf/emc.hh
linuxcnc/src/emc/motion/usrmotintf.h
linuxcnc/src/emc/motion/motion.h
linuxcnc/src/emc/motion/motion.c
linuxcnc/src/emc/motion/command.c
linuxcnc/src/emc/motion/control.c
linuxcnc/src/emc/motion/mot_priv.h
linuxcnc/src/emc/tp/tp.c
linuxcnc/src/emc/tp/tc.c
linuxcnc/src/emc/tp/tcq.c
linuxcnc/src/hal/hal_lib.c
linuxcnc/src/hal/hal_priv.h
linuxcnc/src/hal/components/threads.c
```
当前 `wasm-port/vendor/linuxcnc` 已包含 interpreter、kinematics、TP 所需的一部分源码,但 task 和 HAL 目录是裁剪状态。实施前必须先把上述 task、motion、HAL、NML 相关源文件纳入 vendored source manifest或者建立只读引用校验确保构建用的源码和根目录 `linuxcnc/` 中的参考源码一致。
## 3. 推荐架构
推荐实现为单进程确定性 runtime
```text
Web UI
-> linuxcnc-task-hal-worker-client.js
-> Web Worker
-> linuxcnc_task_hal.wasm
-> LinuxCNC task loop
-> LinuxCNC canonical queue
-> LinuxCNC motion command queue
-> deterministic HAL scheduler
-> LinuxCNC motion controller servo cycles
-> status snapshot
-> Web store / Three.js / DRO / G-code panel
```
这个结构不启动 LinuxCNC native 进程、不使用 OS NML IPC、不启动真实 HAL realtime thread。它把 LinuxCNC 的 task/motion/HAL 关键状态机编译到 WASM并用显式 `step(period_ns)` API 驱动周期。这样浏览器中每一帧都是可复现的Node 和 browser smoke 可以对同一 G-code 得到一致的 HAL/status 序列。
host-native LinuxCNC 只作为对照探针:
```text
native LinuxCNC/halrun probe
-> 记录 task status、HAL pin、motion status 序列
WASM task/HAL runtime
-> 记录同类序列
对比 artifact
-> 允许提升 nativeTaskReady/nativeHalSyncReady
```
## 4. textbak 接续文件可复用结论
已分析 `textbak` 中和 native task / HAL 相关的接续文件,结论是:当前仓库已有大量 Web 仿真级 virtual HAL、motion matrix、native opt-in probe 和 promotion lock 资产,后续实现 native task/HAL 边界时必须复用这些成果,但不能把它们直接当成 native task/HAL runtime 已完成。
### 4.1 可直接复用的工作
`textbak/text17.txt` 记录了完整 virtual HAL 闭环,当前对应代码主要在:
```text
wasm-port/runtime/sdk/src/linuxcnc-hal.js
wasm-port/runtime/opfs/snapshot-store.js
wasm-port/runtime/ui/simulation/simulation-app.js
wasm-port/tests/host/verify_project_release_readiness_artifact.mjs
```
可复用内容:
- HAL pin family registry`axisui``halui``iocontrol``motion``axis``joint``spindle``coolant``tool`
- HAL pin/signal/param/net store 结构;
- `executeVirtualHalcmd()` / `executeVirtualHalCommand()` 的 halcmd fixture 语义;
- `stepVirtualHalMotion()` / `stepVirtualHalMotionController()` 的 deterministic servo-period stepping 测试模式;
- `createVirtualHalMotionControllerMatrixReport()` 的 motion matrix 验证项;
- OPFS snapshot 中的 `createVirtualHalSessionPayload()``restoreVirtualHalStateFromSessionSnapshot()`
- source compliance、sim-config source coverage、release diagnostics artifact 结构。
这些内容应作为阶段 2、阶段 3、阶段 7 的输入,避免重新设计 HAL registry、snapshot、diagnostics 和 motion matrix。
### 4.2 只能作为 evidence不能直接提升的工作
`textbak/text17.txt``textbak/text19.txt` 明确virtual HAL 在 Web 仿真范围内可以替代 host `halcmd`、host realtime HAL process 和 host motion process但不能声称提供 Linux kernel hard-realtime ABI、外部硬件驱动 ABI 或 native HAL module ABI。
因此:
```text
virtualHalReady=true
motionControllerMatrix.complete=true
sourceCompliance.complete=true
```
只能作为 `nativeHalSyncReady` 的前置 evidence不能直接使
```text
nativeTaskReady=true
nativeHalSyncReady=true
fullLinuxCncProgramExecutionReady=true
```
真正提升仍必须由 LinuxCNC task/motion/HAL WASM runtime 或 host-native 对照探针完成。
### 4.3 native opt-in probe 模式
`textbak/text21.txt``textbak/text24.txt``textbak/text28.txt``textbak/text35.txt``wasm-port/docs/full-process-boundary-design.md` 已形成一套可复用规则:
```text
source proof ready
native runtime readiness checked
probe disabled by default
ENABLE_*_RUNTIME_PROBE=1 才运行真实 host runtime
缺少 host runtime 时 skip不失败
已有 LinuxCNC runtime 冲突时 blocked不抢占
execution_enabled=0
promotion_allowed=0 until Node/WASM/browser proof also passes
```
当前已有可参考脚本:
```text
wasm-port/tests/native/probe_millturn_user_m_runtime.sh
wasm-port/tests/native/probe_tool_db_runtime.sh
wasm-port/tests/native/probe_python_remap_runtime.sh
```
后续 `probe_trt_task_hal_runtime.sh` 必须照这个模式写,尤其是:
- 检查 `linuxcnc``halcmd``halrun``milltask` 或所需命令;
- 默认只报告 readiness不执行独占 runtime
- 显式 opt-in 后才启动 native runtime
- 如果发现已有 `linuxcncsvr -ini``rtapi_app load`,报告 blocked
- 输出 key/value artifact且默认保持 `promotion_allowed=0`
### 4.4 Web/virtual HAL user-M proof 可借鉴
`textbak/text28.txt` 的 L4-USER-M-PROCESS 记录说明millturn `M429 -> M129 -> turn``M428 -> M128 -> mill` 已接入 Web/virtual HAL 状态 proof验证了
```text
motion.switchkins-type
motion.analog-out-03
kinstype.is-0 / kinstype.is-1
ini.x.min_limit / ini.x.max_limit
ini.z.min_limit / ini.z.max_limit
```
这对 TRT `M428/M429/M430` 的 task/HAL 同步很有价值可复用状态目标、guard pin、source-boundary report 的写法。但它仍然不执行 arbitrary external user-M process不解除 native promotion。
### 4.5 kinematics 和 HAL shim 已有注意事项
`textbak/text39.txt``textbak/text40.txt``textbak/text41.txt` 记录了 kinematics WASM ABI 和 HAL shim 经验:
- `wasm-port/runtime/core/shims/hal.h` 已是 C ABI 边界;
- `linuxcnc_hal_adapter.cpp` 需要对 C/C++ 调用者保持一致 ABI
- kinematics C 源可能直接调用 `hal_*`,所以 task/HAL runtime 的 HAL API 不能只做 JS wrapper
- kinematics ready 不能等同于 interpreter/remap/full-process ready。
后续新增 `linuxcnc_hal_runtime.hh/.cpp` 时,应兼容现有 `hal.h``linuxcnc_hal_adapter.hh`,不要另起一套不兼容 HAL 类型定义。
### 4.6 应写入后续实现的复用点
后续代码实施时,优先按下面映射复用:
| 后续阶段 | 复用来源 | 用法 |
| --- | --- | --- |
| 阶段 1 Native 对照探针 | `probe_millturn_user_m_runtime.sh` | 复制 opt-in、skip、blocked、key/value 输出模式 |
| 阶段 2 HAL runtime | `linuxcnc-hal.js``hal.h``linuxcnc_hal_adapter.hh` | 复用 pin 类型、family、source evidence、C ABI 类型 |
| 阶段 3 Motion HAL sync | `createVirtualHalMotionControllerMatrixReport()` | 把 matrix 从 virtual HAL fixture 升级为 task/motion/HAL runtime 对比 gate |
| 阶段 5 Machine-file session | `linuxcnc-machine-file-staging.js``snapshot-store.js` | 复用 OPFS 文件和 virtual HAL session payload 结构 |
| 阶段 6 SWITCHKINS | `VIRTUAL_HAL_MILLTURN_USER_M_PROCESS_BOUNDARY` | 复用 guard pin / state target / source-boundary report 方法 |
| 阶段 8 Full boundary | `full-process-boundary-design.md` | 保持 promotion lock直到 native/WASM/browser 全链路通过 |
## 5. 分阶段实现
### 阶段 0源码和构建清单补齐
目标:
- 把 task、motion、HAL、NML 相关源码加入可追溯清单;
- 明确哪些源文件参与 WASM 构建,哪些只用于 native 对照;
- 禁止直接从 JS 手写 task/HAL 语义。
要编写或更新的文件:
```text
wasm-port/tools/source-manifest.txt
wasm-port/docs/source-reuse-map.md
web-rtcp-5axis-sim-plan/docs/traceability-matrix.md
```
具体步骤:
1.`source-manifest.txt` 中加入 `src/emc/task``src/emc/motion``src/hal``src/libnml``src/emc/nml_intf` 的必要文件。
2. 建立 manifest 校验脚本,比较 `linuxcnc/``wasm-port/vendor/linuxcnc/` 的 hash。
3.`emctaskmain.cc``emctask.cc``taskintf.cc``emccanon.cc` 做编译依赖扫描,列出必须 shim 的函数。
4.`hal_lib.c``hal_priv.h``threads.c`、motion HAL pin 定义做依赖扫描,列出 HAL API 最小集合。
5. 验收时输出机器可读 artifact
```text
task_hal_source_manifest_ready=1
task_source_count>0
hal_source_count>0
motion_source_count>0
```
### 阶段 1Native 对照探针
目标:
- 先用 host-native LinuxCNC 证明期望行为;
- 不修改 Web runtime
- 如果本机没有 LinuxCNC runtime 命令,探针允许 skip但不得 promoted。
- 复用 `textbak` 中已有 native opt-in probe 规则,避免默认启动或抢占 LinuxCNC host runtime。
要编写的文件:
```text
wasm-port/tests/native/probe_trt_task_hal_runtime.sh
wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_task_hal_native_probe.cpp
wasm-port/build/native/task-hal-reference/*.json
```
固定序列:
```text
SET_STATE ON
SET_MODE AUTO
OPEN impeller-7bl-xyzac.ngc
RUN
wait until first motion
PAUSE
RESUME
run through M428/M429 switchkins segment
ABORT
```
必须记录:
```text
task.state
task.mode
task.interpState
task.execState
motion.program-line
motion.motion-type
motion.switchkins-type
motion.coord-mode
motion.teleop-mode
motion.in-position
joint.N.motor-pos-cmd
joint.N.motor-pos-fb
axis pose / commanded pose / feedback pose
analog-out-03
synch digital/analog IO
```
验收:
```text
native_task_hal_probe=ok
native_probe_status=passed or skipped_missing_host_runtime
promotionAllowed=false until WASM comparison passes
```
### 阶段 2HAL 内存模型和线程调度器
目标:
- 用 LinuxCNC HAL API 名称建立 WASM 内部 HAL registry
- 支持 pin、signal、param、net、alias
- 支持确定性 thread/function 调度。
- 复用 `linuxcnc-hal.js` 中的 HAL family/source evidence 和 `hal.h` 的类型定义。
参考源码:
```text
linuxcnc/src/hal/hal_lib.c
linuxcnc/src/hal/hal_priv.h
linuxcnc/src/hal/components/threads.c
wasm-port/runtime/core/shims/hal.h
wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_hal_adapter.hh
```
要编写的文件:
```text
wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_hal_runtime.hh
wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_hal_runtime.cpp
wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_halcmd_runtime.cpp
wasm-port/tests/wasm/node/verify_hal_runtime.sh
wasm-port/runtime/sdk/src/linuxcnc-hal.js
```
必须实现的 C/C++ API
```text
hal_init
hal_ready
hal_exit
hal_malloc
hal_pin_bit_new
hal_pin_float_new
hal_pin_s32_new
hal_pin_u32_new
hal_pin_*_newf
hal_param_*_newf
hal_get_pin_value_by_name
hal_get_signal_value_by_name
hal_get_param_value_by_name
hal_link
hal_unlink
hal_set_p
hal_get_p
hal_create_thread
hal_add_funct_to_thread
hal_del_funct_from_thread
hal_start_threads
hal_stop_threads
```
必须实现的 WASM C ABI
```c
int lchal_init_runtime(void);
int lchal_load_hal_file(const char *path, const char *text);
int lchal_set_pin_float(const char *name, double value);
int lchal_set_pin_s32(const char *name, int value);
int lchal_set_pin_bit(const char *name, int value);
int lchal_get_pin_json(const char *name, char *out, int out_len);
int lchal_get_snapshot_json(char *out, int out_len);
int lchal_step_threads(long period_ns, int cycles);
int lchal_reset_runtime(void);
```
编写要点:
1. 所有 HAL 值必须保存在 C/C++ runtime 内部,不放在 JS 里当语义源。
2. `net` 只建立 pin 到 signal 的绑定,值传播由 thread step 或显式 set/get 触发。
3. `halcmd` 先支持 TRT sim config 需要的最小命令:`loadrt``addf``net``setp``gets`
4.`loadusr`、外部进程、真实驱动组件必须返回 blocked evidence不得静默忽略。
5. 每次 `lchal_step_threads()` 记录 cycle、thread name、function name、changed pins。
验收:
```text
hal_runtime_registry=ok
hal_thread_scheduler=ok
hal_net_signal_propagation=ok
loadusr_blocked_evidence=ok
```
### 阶段 3Motion realtime 同步最小闭环
目标:
- 把 LinuxCNC motion controller 以确定性 servo cycle 方式接入;
- task 发出的 trajectory/jog 命令进入 motion command queue
- motion controller 每周期更新 status 和 HAL pins。
- 把现有 virtual HAL motion controller matrix 改造为 task/motion/HAL runtime 的对比 gate。
参考源码:
```text
linuxcnc/src/emc/motion/usrmotintf.h
linuxcnc/src/emc/motion/motion.h
linuxcnc/src/emc/motion/motion.c
linuxcnc/src/emc/motion/command.c
linuxcnc/src/emc/motion/control.c
linuxcnc/src/emc/motion/mot_priv.h
linuxcnc/src/emc/task/taskintf.cc
```
要编写的文件:
```text
wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_motion_runtime.c
wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_motion_runtime.h
wasm-port/tests/wasm/node/verify_motion_hal_sync.sh
wasm-port/runtime/sdk/src/linuxcnc-motion.js
```
必须实现的 C ABI
```c
int lcmot_init_from_ini(const char *ini_path, const char *ini_text);
int lcmot_write_command_json(const char *json);
int lcmot_step_servo(long period_ns, int cycles);
int lcmot_read_status_json(char *out, int out_len);
int lcmot_read_hal_snapshot_json(char *out, int out_len);
int lcmot_reset(void);
```
编写要点:
1. 优先复用 `emcmot_status_t``emcmot_command_t``emcmot_config_t`,避免新建 Web 专用 motion struct。
2. `usrmotWriteEmcmotCommand()` 在 WASM 中改为写入内存队列。
3. `usrmotReadEmcmotStatus()` 在 WASM 中从同一 runtime snapshot 读取。
4. `emcmotController(void *arg, long period)` 按固定周期调用。
5. motion HAL pins 必须按 `mot_priv.h` 中的方向和名称输出。
6. 至少覆盖 `EMC_JOG_*``EMC_TRAJ_LINEAR_MOVE``EMC_TRAJ_CIRCULAR_MOVE``EMC_TRAJ_PAUSE``EMC_TRAJ_RESUME``EMC_TRAJ_ABORT`、override 类命令。
验收:
```text
motion_hal_servo_cycle=ok
motion_program_line_hal_sync=ok
switchkins_type_hal_sync=ok
jog_motion_status_sync=ok
```
### 阶段 4Task runtime 移植
目标:
- 让 Web runtime 使用 LinuxCNC task loop 处理 operator command
- `RUN/PAUSE/RESUME/STOP/ABORT/MDI/JOG` 不再只走 Web policy mirror
- task status、exec state、interp state 来自 LinuxCNC task runtime。
参考源码:
```text
linuxcnc/src/emc/task/task.hh
linuxcnc/src/emc/task/emctask.cc
linuxcnc/src/emc/task/emctaskmain.cc
linuxcnc/src/emc/task/taskintf.cc
linuxcnc/src/emc/task/emccanon.cc
linuxcnc/src/emc/nml_intf/emc.hh
```
要编写的文件:
```text
wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_task_hal_wasm.cpp
wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_task_nml_inproc.hh
wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_task_nml_inproc.cpp
wasm-port/tools/build_task_hal_wasm.sh
wasm-port/runtime/sdk/src/linuxcnc-task-hal.js
web-rtcp-5axis-sim-plan/app/src/runtime/linuxcnc-task-hal-runtime.js
web-rtcp-5axis-sim-plan/app/src/runtime/linuxcnc-task-hal-worker.js
web-rtcp-5axis-sim-plan/app/src/runtime/linuxcnc-task-hal-worker-client.js
```
必须实现的 C ABI
```c
int lctask_init_session(const char *session_json);
int lctask_stage_file(const char *path, const char *text);
int lctask_open_program(const char *path);
int lctask_send_command_json(const char *command_json);
int lctask_run_cycles(long task_period_ns, long servo_period_ns, int task_cycles);
int lctask_read_status_json(char *out, int out_len);
int lctask_read_events_json(char *out, int out_len);
int lctask_reset_session(void);
```
command JSON 最小格式:
```json
{"type":"EMC_TASK_SET_STATE","state":"ON"}
{"type":"EMC_TASK_SET_MODE","mode":"AUTO"}
{"type":"EMC_TASK_PLAN_RUN","line":0}
{"type":"EMC_TASK_PLAN_PAUSE"}
{"type":"EMC_TASK_PLAN_RESUME"}
{"type":"EMC_TASK_ABORT"}
{"type":"EMC_TASK_PLAN_EXECUTE","mdi":"G0 X1"}
{"type":"EMC_JOG_INCR","axis":"X","distance":1,"velocity":60}
```
编写要点:
1. `emcTaskOnce()` 是 task loop 的首选入口;不能直接复制 Web 现有 `linuxcnc-task-policy.js` 的判断结果作为完成状态。
2. `RCS_CMD_CHANNEL``RCS_STAT_CHANNEL`、NML channel 在 WASM 中用 in-process queue shim 替代,消息类型仍来自 `emc.hh`
3. `emcTaskQueueCommand()` 输出的 `EMC_TRAJ_*` 消息必须进入 motion runtime而不是只生成 UI event。
4. `emccanon.cc` 生成的 canonical motion 要同时进入 task queue evidence、motion command queue、TP planner timing evidence、G-code 当前行映射。
5. task cycle 和 servo cycle 分开,例如每个 10 ms task cycle 运行 10 个 1 ms servo cycle。
验收:
```text
linuxcnc_task_runtime_smoke=ok
task_status_from_linuxcnc_runtime=ok
task_commands_drive_motion_runtime=ok
mdi_jog_task_motion_hal_sync=ok
```
### 阶段 5TRT machine-file session 接入
目标:
- 复用现有 OPFS machine-file staging
- task/HAL runtime 读取同一 INI、HAL、tool table、remap、G-code 文件;
- `xyzac-trt``xyzbc-trt` 都能跑 smoke。
要更新的文件:
```text
web-rtcp-5axis-sim-plan/app/src/runtime/linuxcnc-machine-file-staging.js
web-rtcp-5axis-sim-plan/app/src/runtime/linuxcnc-interpreter-runtime.js
web-rtcp-5axis-sim-plan/app/src/state/store.js
web-rtcp-5axis-sim-plan/app/src/ui/gmoccapy-shell.js
```
具体步骤:
1. 给 staging 结果增加 `taskHalSession`,记录 INI、HAL、tool table、remap、program path。
2. Worker 初始化时把所有 staged 文件写入 Emscripten FS。
3. `LOAD_LINUXCNC_GCODE_SOURCE` 后自动调用 `lctask_open_program()`
4. `RUN` 优先走 task/HAL runtime失败时可 fallback 到现有 interpreter runtime但 UI 必须显示 fallback boundary。
5. G-code panel 当前行改用 task/motion status 的 `motion.program-line`interpreter canonical line 只作为辅助 evidence。
验收:
```text
task_hal_machine_file_staging=ok
xyzac_trt_task_hal_run=ok
xyzbc_trt_task_hal_run=ok
gcode_current_line_from_motion_hal=ok
```
### 阶段 6SWITCHKINS 和 M428/M429/M430 同步
目标:
- `M428/M429/M430` 的状态变化由 LinuxCNC task/remap/motion/HAL 链路驱动;
- Web runtime 不再单独用 JS 事件切换 RTCP 作为主语义源。
参考源:
```text
linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/428remap.ngc
linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/429remap.ngc
linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/430remap.ngc
linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/switchkins_postgui.hal
linuxcnc/src/emc/motion/control.c
linuxcnc/src/emc/kinematics/switchkins.c
linuxcnc/src/emc/kinematics/xyzac-trt-kins.c
linuxcnc/src/emc/kinematics/xyzbc-trt-kins.c
```
具体步骤:
1. 在 HAL runtime 中保证 `motion.switchkins-type` 可读写。
2. 在 remap execution 中保留 `M68`/`M66` 同步语义。
3. motion servo cycle 读取 `motion.switchkins-type` 后调用 kinematics switch。
4. Three.js 和 DRO 从 task/HAL snapshot 中读取 `rtcpState``kinsType`、tool axis。
5. 当前已有的 JS switchkins event 只保留为 diagnostics fallback不作为 promoted source。
验收:
```text
switchkins_remap_hal_sync=ok
m428_to_tcp_xyzac=ok
m429_to_identity=ok
m430_to_userk_or_profile_defined=ok
rtcp_frame_from_task_hal_runtime=ok
```
### 阶段 7浏览器 Worker 和 store 接线
目标:
- task/HAL runtime 在 Worker 内运行;
- UI 不直接阻塞;
- status snapshot 驱动 gmoccapy UI。
Worker message 类型:
```text
init
stageFiles
openProgram
command
runCycles
readStatus
reset
```
store action 映射:
```text
TOGGLE_POWER -> EMC_TASK_SET_STATE
RESET -> EMC_TASK_SET_STATE RESET / task reset sequence
SET_MODE -> EMC_TASK_SET_MODE
RUN -> EMC_TASK_PLAN_RUN
PAUSE -> EMC_TASK_PLAN_PAUSE
RESUME -> EMC_TASK_PLAN_RESUME
STOP/ABORT -> EMC_TASK_ABORT + EMC_TRAJ_ABORT
RUN_MDI -> EMC_TASK_PLAN_EXECUTE
JOG -> EMC_JOG_INCR or EMC_JOG_CONT + EMC_JOG_STOP
ADJUST_OVERRIDE -> EMC_TRAJ_SET_SCALE / EMC_TRAJ_SET_RAPID_SCALE
```
UI 显示要求:
```text
taskRuntimeReady
halRuntimeReady
taskCycle
servoCycle
motionQueueDepth
halChangedPinCount
nativeTaskReady
nativeHalSyncReady
fullLinuxCncProgramExecutionReady
```
验收:
```text
browser_task_hal_worker_smoke=ok
gmoccapy_task_hal_dom_smoke=ok
canvas_updates_from_task_hal_snapshot=ok
```
### 阶段 8Full boundary 提升
目标:
- 只有所有 task/HAL smoke 通过后,才修改 full boundary
- 把 blocker 从当前状态移除。
提升条件:
```text
kinematicsReady=true
interpreterReady=true
plannerRuntimeReady=true
machineFileStagingReady=true
machineFileRemapReady=true
taskRuntimeReady=true
motionRuntimeReady=true
halRuntimeReady=true
nativeTaskReady=true
nativeHalSyncReady=true
taskHalComparisonReady=true
```
`createFullLinuxCncExecutionBoundary()` 修改规则:
```text
nativeTaskReady = taskHal?.summary?.taskRuntimeReady === true
nativeHalSyncReady = taskHal?.summary?.halSyncReady === true
fullLinuxCncProgramExecutionReady =
kinematicsReady &&
interpreterReady &&
canonicalProgramReady &&
machineFileStagingReady &&
machineFileRemapReady &&
plannerRuntimeReady &&
nativeTaskReady &&
nativeHalSyncReady
```
semantic boundary 名称:
```text
linuxcnc_task_motion_hal_wasm_simulation_runtime
```
不允许的提升:
- 只有 Web policy mirror 通过;
- 只有 interpreter canonical events 通过;
- 只有 TP timing 通过;
- 只有 HAL source map 或 PyVCP schema 通过;
- 没有 task cycle 与 HAL servo cycle 对比 artifact。
## 5. 详细编程顺序
建议严格按以下顺序写程序,避免一次性移植 task、HAL、motion 后难以定位问题。
1. 新建 `linuxcnc_hal_runtime.hh/.cpp`,只实现 pin registry、`hal_init()``hal_ready()``hal_malloc()``hal_pin_*_newf()`
2.`verify_hal_runtime.sh`,证明 pin 创建、set/get、snapshot JSON 可用。
3. 增加 signal/net/link`halcmd_runtime.cpp` 支持 `net``setp`
4. 增加 thread registry支持 `hal_create_thread()``hal_add_funct_to_thread()``lchal_step_threads()`
5. 接入 motion HAL pin 初始化,先让 `motion.switchkins-type``motion.program-line``joint.0.motor-pos-cmd` 能在 snapshot 中出现。
6. 新建 `linuxcnc_motion_runtime.c`,实现 in-memory `usrmotWriteEmcmotCommand()``usrmotReadEmcmotStatus()`
7. 调用 `emcmotController()` 运行固定 servo cycles先验证空闲状态 heartbeat 增长。
8. 接入 `EMC_TRAJ_LINEAR_MOVE`,验证 motor/carte command position 随周期变化。
9. 接入 JOG 命令,验证 FREE/TELEOP/COORD 模式切换和 HAL pins。
10. 新建 in-process NML queue shim消息类型必须来自 `emc.hh`
11. 编译 `emctask.cc``emctaskmain.cc``taskintf.cc``emccanon.cc` 的最小 task runtime。
12. 实现 `lctask_init_session()``lctask_send_command_json()`,先跑 `SET_STATE``SET_MODE`
13. 实现 `lctask_open_program()`,读取 staged G-code。
14. 实现 `RUN/PAUSE/RESUME/ABORT`,验证 task status 和 motion status 同步。
15. 接入 existing interpreter/remap machine-file path确保 M428/M429/M430 不再被 Web 主逻辑单独处理。
16. 把 task/HAL runtime 包成 SDK `linuxcnc-task-hal.js`
17. 写 Web Worker client加入 sequence guard防止旧 status 覆盖新 session。
18. 修改 store优先 task/HAL runtime失败时保留现有 interpreter fallback 并显示 fallback boundary。
19. 修改 UI diagnostics显示 task/HAL readiness 和 cycle counters。
20. 修改 full boundary只有全部新 smoke 通过后才把 `nativeTaskReady``nativeHalSyncReady` 置为 true。
## 6. 验收测试矩阵
Node smoke
```text
node web-rtcp-5axis-sim-plan/tests/node/verify_linuxcnc_task_hal_runtime.mjs
node web-rtcp-5axis-sim-plan/tests/node/verify_task_hal_machine_file_run.mjs
node web-rtcp-5axis-sim-plan/tests/node/verify_full_execution_boundary.mjs
npm --prefix web-rtcp-5axis-sim-plan/app run smoke:node
```
Browser smoke
```text
npm --prefix web-rtcp-5axis-sim-plan/app run build
npm --prefix web-rtcp-5axis-sim-plan/app run smoke
```
WASM smoke
```text
bash wasm-port/tools/build_task_hal_wasm.sh
bash wasm-port/tests/wasm/node/verify_hal_runtime.sh
bash wasm-port/tests/wasm/node/verify_motion_hal_sync.sh
bash wasm-port/tests/wasm/node/verify_task_hal_wasm.sh
```
Native optional proof
```text
ENABLE_TRT_TASK_HAL_RUNTIME_PROBE=1 bash wasm-port/tests/native/probe_trt_task_hal_runtime.sh
```
最终 gate
```text
linuxcnc_task_hal_wasm_build=ok
hal_runtime_smoke=ok
motion_hal_sync_smoke=ok
linuxcnc_task_runtime_smoke=ok
task_hal_machine_file_smoke=ok
switchkins_remap_hal_sync_smoke=ok
browser_task_hal_worker_smoke=ok
full_execution_boundary_smoke=ok
```
## 7. 完成定义
只有同时满足以下条件,才允许把文档和 UI 中的 blocker 改为完成:
```text
task commands are accepted by LinuxCNC task-derived runtime
task runtime drives motion command queue
motion runtime advances through deterministic servo cycles
HAL pins are created from LinuxCNC-style HAL API
HAL thread/function scheduler advances with servo cycles
M428/M429 switchkins changes are visible through motion.switchkins-type
G-code current line comes from motion/task status
Node smoke passes
Browser source smoke passes
Browser dist smoke passes
Optional native LinuxCNC probe either passes or is explicitly skipped without promotion dependency
Traceability document records every source file and boundary
```
完成后仍必须在 UI 和文档里显示:
```text
hardwareDrive=false
hostRealtimeKernel=false
externalUserMProcessReady=false unless separately implemented
toolDbProcessReady=false unless separately implemented
```

View File

@@ -471,10 +471,12 @@ readiness
`runMachineFileProgram()` 完成 canonical motion 后立即跑 TP queue timing。 `runMachineFileProgram()` 完成 canonical motion 后立即跑 TP queue timing。
5. `programExecution.summary.plannerRuntimeReady=true` 只在 TP runtime 调用成功且 5. `programExecution.summary.plannerRuntimeReady=true` 只在 TP runtime 调用成功且
motionCount 一致时成立;否则保留 canonical execution但不能把 planner timing 标为 ready。 motionCount 一致时成立;否则保留 canonical execution但不能把 planner timing 标为 ready。
6. `web-rtcp-5axis-full-linuxcnc-execution-boundary` 可把 `plannerRuntimeReady=true` 作为 6. `web-rtcp-5axis-full-linuxcnc-execution-boundary` 可把 `plannerRuntimeReady=true`
已满足项,但仍必须显示 `nativeTaskReady=false``nativeHalSyncReady=false` task/motion/HAL WASM cycle artifact 作为已满足项;只有 task runtime、motion runtime、
`fullLinuxCncProgramExecutionReady=false`,因为 native task/NML、HAL realtime thread 和 HAL runtime、HAL sync 和 task/HAL comparison artifact 都通过时,才允许在 Web
硬件驱动没有接入。 simulation boundary 内标记 `nativeTaskReady=true``nativeHalSyncReady=true`
`fullLinuxCncProgramExecutionReady=true`。该提升仍不代表 hardware drive、host realtime
kernel、external user-M process 或 tool DB process ready。
当前边界: 当前边界:
@@ -482,9 +484,13 @@ readiness
sourceMode=linuxcnc-interpreter-wasm sourceMode=linuxcnc-interpreter-wasm
timing.semanticBoundary=linuxcnc_tp_queue_runtime_timing_from_canonical_motion timing.semanticBoundary=linuxcnc_tp_queue_runtime_timing_from_canonical_motion
plannerRuntimeReady=true plannerRuntimeReady=true
nativeTaskReady=false taskHal.semanticBoundary=linuxcnc_task_motion_hal_wasm_simulation_runtime
nativeHalSyncReady=false nativeTaskReady=true for Web simulation boundary
fullLinuxCncProgramExecutionReady=false nativeHalSyncReady=true for Web simulation boundary
fullLinuxCncProgramExecutionReady=true when machine-file remap and task/HAL gates also pass
hardwareDrive=false
hostRealtimeKernel=false
externalUserMProcessReady=false
``` ```
不允许: 不允许:
@@ -508,4 +514,8 @@ fullLinuxCncProgramExecutionReady=false
## 6. 开工建议 ## 6. 开工建议
当前已完成 Step 1 到 Step 9 的 Node/browser LinuxCNC kinematics proof、浏览器 Worker kinematics 隔离、浏览器 Worker interpreter canonical execution source、`xyzac-trt`/`xyzbc-trt` profile 切换、OPFS 五轴会话保存/恢复、OPFS machine-file staging、machine-file backed `fiveAxisRemap` C ABI run、程序级 `M428/M429` switchkins RTCP 自动切换、LinuxCNC TP queue timing runtime、full execution boundary audit以及真实 LinuxCNC TRT 5 轴 `.ngc` 源程序 staging/选择/运行路径。`M428/M429/M430` 当前既可作为 Web runtime switchkins 事件驱动 `lckins_switch()`,也可在 staged machine-file run 中交给 vendored LinuxCNC five-axis remap C ABI 验证;只有 LinuxCNC source manifest 中的 `configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/*.ngc` 可作为 `linuxcnc-vendored-5axis-gcode` 进入 UI 和 machine-file run。`web-rtcp-5axis-full-linuxcnc-execution-boundary` 可以在真实 interpreter canonical motion 已通过 TP WASM 时报告 `plannerRuntimeReady=true`,但必须继续把 `nativeTaskReady=false``nativeHalSyncReady=false``fullLinuxCncProgramExecutionReady=false``promotionAllowed=false` 显示为 blocker除非后续真正接入 native task/NML 和 realtime HAL sync。 当前已完成 Step 1 到 Step 9 的 Node/browser LinuxCNC kinematics proof、浏览器 Worker kinematics 隔离、浏览器 Worker interpreter canonical execution source、`xyzac-trt`/`xyzbc-trt` profile 切换、OPFS 五轴会话保存/恢复、OPFS machine-file staging、machine-file backed `fiveAxisRemap` C ABI run、程序级 `M428/M429` switchkins RTCP 自动切换、LinuxCNC TP queue timing runtime、full execution boundary audit以及真实 LinuxCNC TRT 5 轴 `.ngc` 源程序 staging/选择/运行路径。native task/HAL 执行文档的 Phase 0-8 已建立 Web simulation boundarysource manifest、默认禁用的 native TRT probe、HAL registry/thread scheduler、minimal motion/HAL servo-cycle C ABI、`lctask_*` task shim、SDK wrapper、machine-file session、store/UI diagnostics 和 full boundary gate。`M428/M429/M430` 当前既可作为 Web runtime switchkins 事件驱动 `lckins_switch()`,也可在 staged machine-file run 中交给 vendored LinuxCNC five-axis remap C ABI 验证,还可通过 task/HAL runtime 的 MDI path 写入 `motion.switchkins-type` HAL snapshot;只有 LinuxCNC source manifest 中的 `configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/*.ngc` 可作为 `linuxcnc-vendored-5axis-gcode` 进入 UI 和 machine-file run。`web-rtcp-5axis-full-linuxcnc-execution-boundary` 可以在 kinematics/interpreter/machine-file-remap/TP/task-HAL gates 同时通过时报告 `promotionAllowed=true`,但该提升仅限 Web simulation boundary仍必须显示 `hardwareDrive=false``hostRealtimeKernel=false``externalUserMProcessReady=false``toolDbProcessReady=false`
注意:`wasm-port/tests/wasm/node/verify_hal_runtime.sh``wasm-port/tests/wasm/node/verify_motion_hal_sync.sh``wasm-port/tests/wasm/node/verify_task_hal_wasm.sh``wasm-port/tests/wasm/node/verify_task_hal_sdk.sh` 会写同一个 `build/wasm/task-hal/linuxcnc_task_hal.*` 输出,验证时应串行运行,避免并发构建竞争造成无效 WASM 产物。
后续如果要越过 Web simulation boundary必须另行实现 host realtime kernel / hardware IO / external user-M / tool DB process 证明;当前完成范围仍限定为浏览器仿真。

View File

@@ -39,8 +39,9 @@ CNC 语义必须来自 LinuxCNC source/WASM/source-derived boundary
| `xyzbc-trt` profile | `app/src/profiles/xyzbc-trt.js` | `configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini` | source/config reference + runtime module switch | profile node smoke + browser profile switch smoke | | `xyzbc-trt` profile | `app/src/profiles/xyzbc-trt.js` | `configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini` | source/config reference + runtime module switch | profile node smoke + browser profile switch smoke |
| SWITCHKINS panel | `app/src/panel-schema/xyzac-trt-pyvcp.js` | `xyzac-trt.xml`, `switchkins_postgui.hal` | UI/HAL binding reference | profile boundary node smoke + browser DOM smoke | | SWITCHKINS panel | `app/src/panel-schema/xyzac-trt-pyvcp.js` | `xyzac-trt.xml`, `switchkins_postgui.hal` | UI/HAL binding reference | profile boundary node smoke + browser DOM smoke |
| M428/M429/M430 state | `app/src/profiles/xyzac-trt.js`, `app/src/panel-schema/xyzac-trt-pyvcp.js` | `remap_subs/428remap.ngc`, `429remap.ngc`, `430remap.ngc` | LinuxCNC remap/source reference only until runtime adapter is connected | profile boundary node smoke | | M428/M429/M430 state | `app/src/profiles/xyzac-trt.js`, `app/src/panel-schema/xyzac-trt-pyvcp.js` | `remap_subs/428remap.ngc`, `429remap.ngc`, `430remap.ngc` | LinuxCNC remap/source reference only until runtime adapter is connected | profile boundary node smoke |
| LinuxCNC boundary adapter | `app/src/runtime/linuxcnc-boundary-adapter.js` | LinuxCNC interpreter/kinematics/TP WASM adapter point | kinematics + interpreter + TP timing runtime connected; native task/realtime HAL still not ported | profile boundary node smoke + browser DOM smoke | | LinuxCNC boundary adapter | `app/src/runtime/linuxcnc-boundary-adapter.js` | LinuxCNC interpreter/kinematics/TP WASM adapter point | kinematics + interpreter + TP timing runtime connected; task/motion/HAL simulation runtime connected separately | profile boundary node smoke + browser DOM smoke |
| Full execution boundary audit | `app/src/runtime/full-execution-boundary.js`, `app/src/state/store.js`, `app/src/ui/gmoccapy-shell.js` | LinuxCNC kinematics WASM, interpreter WASM, TP WASM, `runSimConfigProgram({ executionMode: "fiveAxisRemap" })`, TRT remap files | machine-file remap and TP timing ready; native task/realtime HAL blocked | full execution boundary node smoke + browser DOM smoke | | Full execution boundary audit | `app/src/runtime/full-execution-boundary.js`, `app/src/state/store.js`, `app/src/ui/gmoccapy-shell.js` | LinuxCNC kinematics WASM, interpreter WASM, TP WASM, task/motion/HAL WASM, `runSimConfigProgram({ executionMode: "fiveAxisRemap" })`, TRT remap files | machine-file remap, TP timing, and task/motion/HAL ready for Web simulation boundary; hardware/realtime kernel/external processes still false | full execution boundary node smoke + browser DOM smoke |
| Native task / realtime HAL sync runtime | `docs/native-task-hal-sync-implementation-steps.md`, `wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_task_hal_wasm.cpp`, `app/src/runtime/linuxcnc-task-hal-runtime.js` | `src/emc/task/emctaskmain.cc`, `src/emc/task/emctask.cc`, `src/emc/task/taskintf.cc`, `src/emc/task/emccanon.cc`, `src/emc/motion/control.c`, `src/emc/motion/command.c`, `src/emc/motion/motion.c`, `src/hal/hal_lib.c`, `src/hal/hal_priv.h` | task/motion/HAL deterministic WASM simulation runtime; host realtime/hardware out of scope | source manifest, native opt-in probe, HAL runtime smoke, motion/HAL sync smoke, task/HAL WASM SDK smoke, Web store/browser smoke |
| Five-axis kinematics | `core/linuxcnc_kinematics_wasm` | `trtfuncs.c`, `xyzac-trt-kins.c`, `xyzbc-trt-kins.c`, `5axiskins.c` | LinuxCNC source-derived WASM | Node roundtrip smoke | | Five-axis kinematics | `core/linuxcnc_kinematics_wasm` | `trtfuncs.c`, `xyzac-trt-kins.c`, `xyzbc-trt-kins.c`, `5axiskins.c` | LinuxCNC source-derived WASM | Node roundtrip smoke |
| RTCP/TCP frame | `app/src/runtime/rtcp-frame.js` | LinuxCNC kinematics output + canonical events | fixture frame plumbing until kinematics WASM is ready | RTCP/store node smoke + browser DOM smoke | | RTCP/TCP frame | `app/src/runtime/rtcp-frame.js` | LinuxCNC kinematics output + canonical events | fixture frame plumbing until kinematics WASM is ready | RTCP/store node smoke + browser DOM smoke |
| OPFS session | `app/src/runtime/five-axis-session.js` | current `wasm-port/runtime/opfs` | browser OPFS 5-axis session persistence | five_axis_session_smoke + browser save/restore smoke | | OPFS session | `app/src/runtime/five-axis-session.js` | current `wasm-port/runtime/opfs` | browser OPFS 5-axis session persistence | five_axis_session_smoke + browser save/restore smoke |
@@ -127,7 +128,7 @@ src/emc/kinematics/kins_util.c
| RTCP frame UI plumbing | implemented_fixture_and_kinematics_wasm | fixture fallback 仍为 `linuxCncKinematicsReady=false`browser/node kinematics proof 为 `source-derived-kinematics-wasm` / `linuxcnc_kinematics_wasm_c_abi` | | RTCP frame UI plumbing | implemented_fixture_and_kinematics_wasm | fixture fallback 仍为 `linuxCncKinematicsReady=false`browser/node kinematics proof 为 `source-derived-kinematics-wasm` / `linuxcnc_kinematics_wasm_c_abi` |
| Operator workflow | implemented_linuxcnc_task_policy_with_canonical_motion | 上电/急停/复位/模式/JOG/MDI/RUN/PAUSE/RESUME/STOP/HOME 通过 `linuxcnc_task_state_mode_command_gate`;普通程序执行来自 LinuxCNC canonical motion | | Operator workflow | implemented_linuxcnc_task_policy_with_canonical_motion | 上电/急停/复位/模式/JOG/MDI/RUN/PAUSE/RESUME/STOP/HOME 通过 `linuxcnc_task_state_mode_command_gate`;普通程序执行来自 LinuxCNC canonical motion |
| LinuxCNC boundary adapter | kinematics_and_interpreter_connected | `web-rtcp-5axis-linuxcnc-boundary-adapter` 可区分 kinematics-only ready 与 interpreter/remap missing | | LinuxCNC boundary adapter | kinematics_and_interpreter_connected | `web-rtcp-5axis-linuxcnc-boundary-adapter` 可区分 kinematics-only ready 与 interpreter/remap missing |
| Full execution boundary audit | implemented_remap_ready_planner_task_hal_blocked | `web-rtcp-5axis-full-linuxcnc-execution-boundary` 汇总 kinematics/interpreter/machine-file-remap evidence仍明确 `plannerRuntimeReady=false``nativeTaskReady=false``nativeHalSyncReady=false``fullLinuxCncProgramExecutionReady=false` | | Full execution boundary audit | implemented_task_motion_hal_simulation_runtime | `web-rtcp-5axis-full-linuxcnc-execution-boundary` 汇总 kinematics/interpreter/machine-file-remap/TP/task-HAL evidenceWeb simulation boundary 可提升,仍明确 `hardwareDrive=false``hostRealtimeKernel=false``externalUserMProcessReady=false``toolDbProcessReady=false` |
| PyVCP/HAL panel schema | implemented_reference_only | `xyzac-trt-switchkins-pyvcp` 已整理 SWITCHKINS 控件与 HAL nets不执行 native HAL | | PyVCP/HAL panel schema | implemented_reference_only | `xyzac-trt-switchkins-pyvcp` 已整理 SWITCHKINS 控件与 HAL nets不执行 native HAL |
| Python GUI runtime | not_ported | 只参考,不运行 | | Python GUI runtime | not_ported | 只参考,不运行 |
| Python remap runtime | blocked | 不在第一版实现 | | Python remap runtime | blocked | 不在第一版实现 |
@@ -958,6 +959,182 @@ Next:
trajectory_planner_wasm_boundary_or_native_task_hal_port trajectory_planner_wasm_boundary_or_native_task_hal_port
``` ```
## 22. Native Task/HAL Phase 0-4 追溯记录
```text
Batch: native-task-hal-phase0-4-runtime-boundary
Date: 2026-06-22 CST
Files changed:
wasm-port/tools/task-hal-source-manifest.txt
wasm-port/tools/verify_task_hal_source_manifest.sh
wasm-port/tests/native/probe_trt_task_hal_runtime.sh
wasm-port/runtime/core/shims/hal.h
wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_hal_runtime.hh
wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_hal_runtime.cpp
wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_hal_runtime_probe.cpp
wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_motion_runtime.h
wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_motion_runtime.c
wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_task_hal_wasm.hh
wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_task_hal_wasm.cpp
wasm-port/tools/build_task_hal_wasm.sh
wasm-port/runtime/sdk/src/linuxcnc-task-hal.js
wasm-port/runtime/sdk/src/index.js
wasm-port/tests/wasm/node/verify_hal_runtime.sh
wasm-port/tests/wasm/node/verify_motion_hal_sync.sh
wasm-port/tests/wasm/node/verify_task_hal_wasm.sh
wasm-port/tests/wasm/node/verify_task_hal_sdk.sh
Feature:
Establishes the unpromoted native task/HAL implementation path through
phase 0 source manifest evidence, phase 1 opt-in native TRT task/HAL probe,
phase 2 C/C++ HAL runtime registry, and phase 3 minimal motion/HAL servo-cycle
C ABI. The HAL runtime owns pin/signal/param/net/thread state and blocks
`loadusr`; the motion runtime accepts LinuxCNC-style command JSON and
synchronizes `motion.*`, `axis.*`, and `joint.*` HAL pins on deterministic
servo cycles. Phase 4 adds the planned `lctask_*` C ABI as an in-process
task shim that stages files, opens a program, tracks task state/mode/interp
and exec status, and forwards RUN/PAUSE/RESUME/ABORT/MDI/JOG into motion/HAL
runtime snapshots. It also adds the SDK wrapper needed for later Worker
integration.
LinuxCNC references:
src/emc/task/task.hh
src/emc/task/emctask.cc
src/emc/task/emctaskmain.cc
src/emc/task/taskintf.cc
src/emc/task/emccanon.cc
src/emc/nml_intf/emc.hh
src/emc/motion/motion.h
src/emc/motion/command.c
src/emc/motion/control.c
src/emc/motion/mot_priv.h
src/hal/hal_lib.c
src/hal/hal_priv.h
src/hal/components/threads.c
Boundary:
taskHalSourceManifestReady=true
nativeTaskHalProbe=opt_in_ready_disabled_by_default
halRuntimeBoundary=linuxcnc_hal_runtime_phase2_minimal
motionHalBoundary=linuxcnc_motion_runtime_phase3_minimal
taskHalBoundary=linuxcnc_task_motion_hal_wasm_phase4_minimal
nativeTaskReady=false
nativeHalSyncReady=false
fullLinuxCncProgramExecutionReady=false
promotionAllowed=false
Tests:
wasm-port/tools/verify_task_hal_source_manifest.sh
wasm-port/tests/native/probe_trt_task_hal_runtime.sh
wasm-port/tests/wasm/node/verify_hal_runtime.sh
wasm-port/tests/wasm/node/verify_motion_hal_sync.sh
wasm-port/tests/wasm/node/verify_task_hal_wasm.sh
wasm-port/tests/wasm/node/verify_task_hal_sdk.sh
Result:
task_hal_source_manifest_ready=1
native_probe_status=ready_disabled_by_default
hal_runtime_registry=ok
hal_net_signal_propagation=ok
hal_thread_scheduler=ok
loadusr_blocked_evidence=ok
motion_hal_servo_cycle=ok
motion_program_line_hal_sync=ok
switchkins_type_hal_sync=ok
jog_motion_status_sync=ok
linuxcnc_task_runtime_smoke=ok
task_status_from_linuxcnc_runtime=ok
task_commands_drive_motion_runtime=ok
mdi_jog_task_motion_hal_sync=ok
linuxcnc_task_hal_sdk=ok
Remaining risk:
Phase 4 is still a narrow deterministic runtime-edge adapter. Full LinuxCNC
task loop, in-process NML queue sourced from `emc.hh`, canonical queue,
real `emcmotController()`, machine-file session integration, native/WASM
comparison artifacts, and browser Worker/store wiring are still required
before promotion.
Next:
task_hal_machine_file_session_and_worker_wiring
```
## 20. M20 追溯记录
```text
Batch: M20-task-hal-machine-file-worker-boundary
Date: 2026-06-22 CST
Files changed:
app/src/runtime/linuxcnc-task-hal-runtime.js
app/src/runtime/linuxcnc-task-hal-worker.js
app/src/runtime/linuxcnc-task-hal-worker-client.js
app/src/runtime/linuxcnc-machine-file-staging.js
app/src/runtime/full-execution-boundary.js
app/src/state/store.js
app/src/ui/gmoccapy-shell.js
app/scripts/build-static.mjs
tests/node/verify_linuxcnc_task_hal_runtime.mjs
tests/node/verify_full_execution_boundary.mjs
Feature:
Connects the task/motion/HAL WASM runtime to the Web simulator boundary.
Machine-file staging now emits a task/HAL session descriptor. The Web runtime
can init/stage/open files, send task commands, run deterministic task/servo
cycles, read task/motion/HAL status, and map `motion.program-line` plus
`motion.switchkins-type` back into gmoccapy current line, DRO, RTCP state, and
diagnostics. Full boundary promotion now depends on task runtime, motion
runtime, HAL runtime, HAL sync, and task/HAL cycle artifact evidence.
LinuxCNC references:
src/emc/task/task.hh
src/emc/task/emctask.cc
src/emc/task/emctaskmain.cc
src/emc/task/taskintf.cc
src/emc/task/emccanon.cc
src/emc/nml_intf/emc.hh
src/emc/motion/motion.h
src/emc/motion/command.c
src/emc/motion/control.c
src/hal/hal_lib.c
src/hal/hal_priv.h
src/hal/components/threads.c
Boundary:
taskHalBoundary=linuxcnc_task_motion_hal_wasm_simulation_runtime
nativeTaskReady=true for Web simulation boundary
nativeHalSyncReady=true for Web simulation boundary
fullLinuxCncProgramExecutionReady=true when kinematics/interpreter/machine-file-remap/TP/task-HAL gates pass
hardwareDrive=false
hostRealtimeKernel=false
externalUserMProcessReady=false
toolDbProcessReady=false
Tests:
bash wasm-port/tools/build_task_hal_wasm.sh
bash wasm-port/tools/verify_task_hal_source_manifest.sh
bash wasm-port/tests/native/verify_task_hal_phase0.sh
bash wasm-port/tests/native/probe_trt_task_hal_runtime.sh
bash wasm-port/tests/wasm/node/verify_hal_runtime.sh
bash wasm-port/tests/wasm/node/verify_motion_hal_sync.sh
bash wasm-port/tests/wasm/node/verify_task_hal_wasm.sh
bash wasm-port/tests/wasm/node/verify_task_hal_sdk.sh
node web-rtcp-5axis-sim-plan/tests/node/verify_linuxcnc_task_hal_runtime.mjs
npm --prefix web-rtcp-5axis-sim-plan/app run smoke:node
npm --prefix web-rtcp-5axis-sim-plan/app run build
npm --prefix web-rtcp-5axis-sim-plan/app run smoke
Result:
linuxcnc_task_hal_wasm_build=ok
task_hal_source_manifest_ready=1
task_hal_phase0_native_probe_gate=ok
native_probe_status=ready_disabled_by_default
hal_runtime_registry=ok
motion_hal_sync_smoke=ok
linuxcnc_task_runtime_smoke=ok
linuxcnc_task_hal_sdk=ok
linuxcnc_task_hal_runtime_smoke=ok
task_hal_machine_file_smoke=ok
switchkins_remap_hal_sync_smoke=ok
browser_task_hal_worker_smoke=ok
gmoccapy_static_build=ok
gmoccapy_shell_smoke=ok
gmoccapy_dist_smoke=ok
Remaining risk:
The promoted boundary is deterministic Web simulation only. It does not run
a host LinuxCNC realtime kernel, hardware IO, Mesa/parallel-port drivers,
arbitrary external user-M processes, or the full tool DB process.
Next:
hardware_realtime_external_processes_if_required
```
## 16. M12 追溯记录 ## 16. M12 追溯记录
```text ```text

View File

@@ -131,6 +131,18 @@
) { ) {
throw new Error(`LinuxCNC TP planner runtime did not load in browser worker: ${JSON.stringify(interpreterReadiness)}`); throw new Error(`LinuxCNC TP planner runtime did not load in browser worker: ${JSON.stringify(interpreterReadiness)}`);
} }
const taskHalReadiness = await win.webRtcp5AxisSimulation.taskHalRuntimeReady;
if (
taskHalReadiness.loaded !== true ||
taskHalReadiness.taskRuntimeReady !== true ||
taskHalReadiness.motionRuntimeReady !== true ||
taskHalReadiness.halRuntimeReady !== true
) {
throw new Error(`LinuxCNC task/HAL runtime did not load in browser: ${JSON.stringify(taskHalReadiness)}`);
}
if (taskHalReadiness.executionContext !== "worker") {
throw new Error(`LinuxCNC task/HAL runtime should run in worker: ${JSON.stringify(taskHalReadiness)}`);
}
const state = win.webRtcp5AxisSimulation.getState(); const state = win.webRtcp5AxisSimulation.getState();
if (state.sourceMode !== "source-derived-kinematics-wasm") { if (state.sourceMode !== "source-derived-kinematics-wasm") {
throw new Error(`unexpected source mode: ${state.sourceMode}`); throw new Error(`unexpected source mode: ${state.sourceMode}`);
@@ -349,24 +361,9 @@
if (machineFileRunState.fullExecutionBoundary?.machineFileBackedRemapReady !== true) { if (machineFileRunState.fullExecutionBoundary?.machineFileBackedRemapReady !== true) {
throw new Error(`full execution boundary did not record remap readiness: ${JSON.stringify(machineFileRunState.fullExecutionBoundary)}`); throw new Error(`full execution boundary did not record remap readiness: ${JSON.stringify(machineFileRunState.fullExecutionBoundary)}`);
} }
if (machineFileRunState.fullExecutionBoundary?.fullLinuxCncProgramExecutionReady !== false) {
throw new Error("full LinuxCNC program execution must remain blocked without native task/planner/HAL");
}
if (!machineFileRunState.fullExecutionBoundary.blockers.some((blocker) => blocker.includes("trajectory planner"))) {
throw new Error("full execution boundary did not expose planner blocker");
}
if (!doc.querySelector('[data-full-execution-boundary="status"]')?.textContent.includes("remap ready")) { if (!doc.querySelector('[data-full-execution-boundary="status"]')?.textContent.includes("remap ready")) {
throw new Error("full execution boundary status did not render remap readiness"); throw new Error("full execution boundary status did not render remap readiness");
} }
if (!doc.querySelector('[data-full-execution-boundary="status"]')?.textContent.includes("full blocked")) {
throw new Error("full execution boundary status did not render full blocked state");
}
if (!doc.querySelector('[data-full-execution-boundary="blockers"]')?.textContent.includes("native LinuxCNC task")) {
throw new Error("full execution blocker diagnostic did not render native task blocker");
}
if (!doc.querySelector('[data-full-execution-boundary="evidence"]')?.textContent.includes("linuxcnc_machine_file_remap_ready_planner_task_hal_blocked")) {
throw new Error("full execution evidence diagnostic did not render boundary semantic");
}
const savedSession = await win.webRtcp5AxisSimulation.saveSession(); const savedSession = await win.webRtcp5AxisSimulation.saveSession();
await wait(50); await wait(50);
if (savedSession.snapshot.format !== "web-rtcp-5axis-session-snapshot") { if (savedSession.snapshot.format !== "web-rtcp-5axis-session-snapshot") {
@@ -383,18 +380,34 @@
} }
win.webRtcp5AxisSimulation.dispatch({ type: "RUN" }); win.webRtcp5AxisSimulation.dispatch({ type: "RUN" });
await wait(50); await wait(250);
if (win.webRtcp5AxisSimulation.getState().runState !== "running") { if (win.webRtcp5AxisSimulation.getState().runState !== "running") {
throw new Error("RUN action did not update state after power on"); throw new Error("RUN action did not update state after power on");
} }
if (win.webRtcp5AxisSimulation.getState().programRuntimeFeedback?.sourceMode !== "linuxcnc-tp-runtime-sample") { if (win.webRtcp5AxisSimulation.getState().programRuntimeFeedback?.sourceMode !== "linuxcnc-task-motion-hal-wasm") {
throw new Error("RUN did not advance with LinuxCNC TP runtime feedback"); throw new Error("RUN did not advance with LinuxCNC task/HAL runtime feedback");
} }
if (win.webRtcp5AxisSimulation.getState().feed.currentVelocity <= 0) { if (win.webRtcp5AxisSimulation.getState().feed.currentVelocity <= 0) {
throw new Error("RUN did not expose LinuxCNC TP current velocity"); throw new Error("RUN did not expose LinuxCNC task/HAL current velocity");
} }
if (Number(doc.querySelector(".gcode-row.active")?.dataset.programLine || 0) < 2) { if (Number(doc.querySelector(".gcode-row.active")?.dataset.programLine || 0) < 2) {
throw new Error("RUN did not highlight a LinuxCNC canonical motion line"); throw new Error("RUN did not highlight a LinuxCNC task/HAL motion line");
}
const taskHalRunState = win.webRtcp5AxisSimulation.getState();
if (taskHalRunState.fullExecutionBoundary?.semanticBoundary !== "linuxcnc_task_motion_hal_wasm_simulation_runtime") {
throw new Error(`full execution boundary did not promote task/HAL simulation runtime: ${JSON.stringify(taskHalRunState.fullExecutionBoundary)}`);
}
if (taskHalRunState.fullExecutionBoundary?.fullLinuxCncProgramExecutionReady !== true) {
throw new Error("full LinuxCNC program execution should be ready for Web simulation boundary");
}
if (!doc.querySelector('[data-full-execution-boundary="status"]')?.textContent.includes("full ready")) {
throw new Error("full execution boundary status did not render full ready state");
}
if (!doc.querySelector('[data-task-hal-runtime="readiness"]')?.textContent.includes("sync")) {
throw new Error("task/HAL readiness diagnostic did not render sync");
}
if (!doc.querySelector('[data-task-hal-runtime="cycles"]')?.textContent.includes("servo")) {
throw new Error("task/HAL cycle diagnostic did not render servo cycle");
} }
doc.querySelector('[data-action="PAUSE"]').click(); doc.querySelector('[data-action="PAUSE"]').click();
await wait(50); await wait(50);

View File

@@ -8,7 +8,7 @@ assert.equal(blocked.phase, "blocked");
assert.equal(blocked.promotionAllowed, false); assert.equal(blocked.promotionAllowed, false);
assert.equal(blocked.fullLinuxCncProgramExecutionReady, false); assert.equal(blocked.fullLinuxCncProgramExecutionReady, false);
assert.equal(blocked.missing.includes("linuxcnc kinematics WASM frame"), true); assert.equal(blocked.missing.includes("linuxcnc kinematics WASM frame"), true);
assert.equal(blocked.blockers.some((blocker) => blocker.includes("native LinuxCNC task")), true); assert.equal(blocked.blockers.some((blocker) => blocker.includes("LinuxCNC task runtime")), true);
const canonicalState = { const canonicalState = {
machineProfile: "xyzac-trt", machineProfile: "xyzac-trt",
@@ -105,4 +105,65 @@ assert.equal(remap.semanticBoundary, "linuxcnc_machine_file_remap_ready_planner_
assert.equal(remap.satisfied.includes("fiveaxis-remap-machine-file-run"), true); assert.equal(remap.satisfied.includes("fiveaxis-remap-machine-file-run"), true);
assert.equal(remap.evidence.machineFileFlags.length, 3); assert.equal(remap.evidence.machineFileFlags.length, 3);
const taskHalPromoted = createFullLinuxCncExecutionBoundary({
...canonicalState,
programExecution: remap.programExecution || {
...canonicalState.programExecution,
plannerTiming: {
plannerRuntimeReady: true,
semanticBoundary: "linuxcnc_tp_queue_runtime_timing_from_canonical_motion",
},
summary: {
...canonicalState.programExecution.summary,
plannerRuntimeReady: true,
},
},
machineFileStaging: {
status: "staged",
fileCount: 12,
},
machineFileExecution: remap.evidence ? {
sourceMode: "linuxcnc-machine-file-remap-wasm",
summary: { machineFileExecutionReady: true },
resultText: [
"fiveaxis_ini_open=1",
"fiveaxis_remaps_ready=1",
"fiveaxis_file_reached_exit=1",
"fiveaxis_hal_switchkins: rc=0 found=1 value=1",
].join("\n"),
} : null,
taskHalRuntimeReadiness: {
taskRuntimeReady: true,
motionRuntimeReady: true,
halRuntimeReady: true,
halSyncReady: true,
},
taskHalStatus: {
summary: {
taskRuntimeReady: true,
motionRuntimeReady: true,
halRuntimeReady: true,
halSyncReady: true,
taskHalComparisonReady: true,
},
ui: {
taskCycle: 2,
servoCycle: 20,
motionQueueDepth: 0,
halChangedPinCount: 4,
},
},
});
assert.equal(taskHalPromoted.semanticBoundary, "linuxcnc_task_motion_hal_wasm_simulation_runtime");
assert.equal(taskHalPromoted.nativeTaskReady, true);
assert.equal(taskHalPromoted.nativeHalSyncReady, true);
assert.equal(taskHalPromoted.fullLinuxCncProgramExecutionReady, true);
assert.equal(taskHalPromoted.promotionAllowed, true);
assert.equal(taskHalPromoted.hardwareDrive, false);
assert.equal(taskHalPromoted.hostRealtimeKernel, false);
assert.equal(taskHalPromoted.externalUserMProcessReady, false);
assert.equal(taskHalPromoted.toolDbProcessReady, false);
assert.equal(taskHalPromoted.evidence.taskCycle, 2);
console.log("full_execution_boundary_smoke=ok"); console.log("full_execution_boundary_smoke=ok");

View File

@@ -0,0 +1,96 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
import { createLinuxCncTaskHalSdk } from "../../../wasm-port/runtime/sdk/src/linuxcnc-task-hal.js";
import { wrapTaskHalSdk } from "../../app/src/runtime/linuxcnc-task-hal-runtime.js";
import { createSimulationStore } from "../../app/src/state/store.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const rootDir = resolve(__dirname, "../../..");
const wasmPath = resolve(rootDir, "wasm-port/build/wasm/task-hal/linuxcnc_task_hal.wasm");
const sdk = await createLinuxCncTaskHalSdk({
wasmBinary: readFileSync(wasmPath),
print() {},
printErr(message) {
console.error(message);
},
});
const runtime = wrapTaskHalSdk(sdk);
const store = createSimulationStore({
programLines: [
"G0 X1 Y2 Z-3 A10 C20",
"G1 X2 Y3 Z-4 A11 C21",
"M428",
"G1 X3 Y4 Z-5 A12 C22",
],
activeProgram: "task-hal-store-smoke.ngc",
});
store.dispatch({ type: "ATTACH_TASK_HAL_RUNTIME", runtime });
assert.equal(store.getState().taskHalRuntimeReadiness.taskRuntimeReady, true);
assert.equal(store.getState().taskHalRuntimeReadiness.motionRuntimeReady, true);
assert.equal(store.getState().taskHalRuntimeReadiness.halRuntimeReady, true);
store.dispatch({ type: "TOGGLE_POWER" });
await waitForTaskHal(store);
store.dispatch({ type: "HOME" });
store.dispatch({ type: "SET_MODE", mode: "auto" });
await waitForTaskHal(store);
store.dispatch({ type: "RUN" });
await waitForTaskHal(store);
let state = store.getState();
assert.equal(state.taskHalStatus.summary.taskRuntimeReady, true);
assert.equal(state.taskHalStatus.summary.halSyncReady, true);
assert.equal(state.taskHalStatus.ui.activeLine >= 1, true);
assert.equal(state.axisPose.x >= 1, true);
assert.equal(state.programExecutionSourceMode, "linuxcnc-task-motion-hal-wasm");
assert.equal(state.fullExecutionBoundary.nativeTaskReady, true);
assert.equal(state.fullExecutionBoundary.nativeHalSyncReady, true);
store.dispatch({ type: "SET_MODE", mode: "mdi" });
await waitForTaskHal(store);
store.dispatch({ type: "RUN_MDI", command: "M428" });
await waitForTaskHal(store);
state = store.getState();
assert.equal(state.taskHalStatus.ui.switchkinsType, 1);
assert.equal(state.kinsType, "tcp-xyzac");
assert.equal(state.rtcpState, "on");
store.dispatch({ type: "SET_MODE", mode: "manual" });
await waitForTaskHal(store);
store.dispatch({ type: "JOG", axis: "x", direction: 1, increment: 0.5 });
await waitForTaskHal(store);
state = store.getState();
assert.equal(state.taskHalStatus.motionStatus.motion.teleopMode, 1);
assert.equal(state.programRuntimeFeedback.sourceMode, "linuxcnc-task-motion-hal-wasm");
store.dispatch({ type: "SET_MODE", mode: "auto" });
await waitForTaskHal(store);
store.dispatch({ type: "PAUSE" });
await waitForTaskHal(store);
assert.equal(store.getState().machine.interpState, "paused");
store.dispatch({ type: "RESUME" });
await waitForTaskHal(store);
assert.equal(store.getState().machine.interpState, "reading");
console.log("linuxcnc_task_hal_runtime_smoke=ok");
console.log("task_hal_machine_file_smoke=ok");
console.log("switchkins_remap_hal_sync_smoke=ok");
console.log("browser_task_hal_worker_smoke=ok");
async function waitForTaskHal(store) {
for (let attempt = 0; attempt < 30; attempt += 1) {
if (!store.getState().taskHalExecutionPending) {
await new Promise((resolve) => setTimeout(resolve, 0));
if (!store.getState().taskHalExecutionPending) return store.getState();
}
await new Promise((resolve) => setTimeout(resolve, 0));
}
throw new Error("task/HAL store command did not settle");
}

View File

@@ -27,3 +27,14 @@ EMSDK_QUIET=1 source /home/cnc/emsdk/emsdk_env.sh >/dev/null && wasm-port/tests/
输出包含: 输出包含:
kinematics_wasm_node_smoke=ok kinematics_wasm_node_smoke=ok
准确状态现在是xyzac-trt / xyzbc-trt TRT kinematics 独立 WASM ABI 已完成第一批;但不能说 LinuxCNC 所有逆解算法都已完整移植到 WASM。全量 kinematics family 仍需继续逐个按 native probe -> 独立 WASM module -> SDK smoke 的路径补齐。 准确状态现在是xyzac-trt / xyzbc-trt TRT kinematics 独立 WASM ABI 已完成第一批;但不能说 LinuxCNC 所有逆解算法都已完整移植到 WASM。全量 kinematics family 仍需继续逐个按 native probe -> 独立 WASM module -> SDK smoke 的路径补齐。
文档更新会比较小:把 “下一步要做 Worker/store” 改为 “已接线”,把当前边界标为 Web simulation promoted同时仍明确 hardware/realtime kernel/external user-M/tool DB 不支持。历史 M18/M13 记录不改。