接入 task HAL Web 仿真运行时
This commit is contained in:
@@ -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 |
|
||||
| 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` |
|
||||
| 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` |
|
||||
| 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` |
|
||||
@@ -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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
@@ -128,3 +132,9 @@ families, and drift report.
|
||||
`runtime/opfs/path-model.js`. Full machine-state restoration remains future
|
||||
work.
|
||||
- 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.
|
||||
|
||||
968
wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_hal_runtime.cpp
Normal file
968
wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_hal_runtime.cpp
Normal 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 ¶m->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"
|
||||
21
wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_hal_runtime.hh
Normal file
21
wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_hal_runtime.hh
Normal 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
|
||||
@@ -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, ¶m_float, comp_id) == 0,
|
||||
"param_float")) {
|
||||
return 1;
|
||||
}
|
||||
if (!require(hal_param_s32_new("standalone.param-s32", HAL_RW, ¶m_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;
|
||||
}
|
||||
464
wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_motion_runtime.c
Normal file
464
wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_motion_runtime.c
Normal 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;
|
||||
}
|
||||
@@ -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
|
||||
467
wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_task_hal_wasm.cpp
Normal file
467
wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_task_hal_wasm.cpp
Normal 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"
|
||||
@@ -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
|
||||
@@ -21,9 +21,13 @@ typedef enum {
|
||||
HAL_IN = 16,
|
||||
HAL_OUT = 32,
|
||||
HAL_IO = (HAL_IN | HAL_OUT),
|
||||
HAL_RW = HAL_IO,
|
||||
} hal_pin_dir_t;
|
||||
|
||||
typedef enum {
|
||||
HAL_RO = 64,
|
||||
HAL_RW = HAL_RO | 128,
|
||||
} hal_param_dir_t;
|
||||
|
||||
typedef union {
|
||||
bool b;
|
||||
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_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_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_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_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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export { createLinuxCncIniSdk } from "./linuxcnc-ini.js";
|
||||
export { createLinuxCncInterpSdk } from "./linuxcnc-interp.js";
|
||||
export { createLinuxCncTaskHalSdk } from "./linuxcnc-task-hal.js";
|
||||
export {
|
||||
createLinuxCncKinematicsSdk,
|
||||
linuxCncKinematicsWasmFile,
|
||||
|
||||
128
wasm-port/runtime/sdk/src/linuxcnc-task-hal.js
Normal file
128
wasm-port/runtime/sdk/src/linuxcnc-task-hal.js
Normal 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")();
|
||||
},
|
||||
};
|
||||
}
|
||||
234
wasm-port/tests/native/probe_trt_task_hal_runtime.sh
Executable file
234
wasm-port/tests/native/probe_trt_task_hal_runtime.sh
Executable 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
|
||||
85
wasm-port/tests/native/verify_task_hal_phase0.sh
Executable file
85
wasm-port/tests/native/verify_task_hal_phase0.sh
Executable 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"
|
||||
213
wasm-port/tests/wasm/node/verify_hal_runtime.mjs
Normal file
213
wasm-port/tests/wasm/node/verify_hal_runtime.mjs
Normal 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");
|
||||
30
wasm-port/tests/wasm/node/verify_hal_runtime.sh
Executable file
30
wasm-port/tests/wasm/node/verify_hal_runtime.sh
Executable 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"
|
||||
111
wasm-port/tests/wasm/node/verify_motion_hal_sync.mjs
Normal file
111
wasm-port/tests/wasm/node/verify_motion_hal_sync.mjs
Normal 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");
|
||||
21
wasm-port/tests/wasm/node/verify_motion_hal_sync.sh
Executable file
21
wasm-port/tests/wasm/node/verify_motion_hal_sync.sh
Executable 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"
|
||||
56
wasm-port/tests/wasm/node/verify_task_hal_sdk.mjs
Normal file
56
wasm-port/tests/wasm/node/verify_task_hal_sdk.mjs
Normal 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");
|
||||
22
wasm-port/tests/wasm/node/verify_task_hal_sdk.sh
Executable file
22
wasm-port/tests/wasm/node/verify_task_hal_sdk.sh
Executable 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"
|
||||
148
wasm-port/tests/wasm/node/verify_task_hal_wasm.mjs
Normal file
148
wasm-port/tests/wasm/node/verify_task_hal_wasm.mjs
Normal 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");
|
||||
22
wasm-port/tests/wasm/node/verify_task_hal_wasm.sh
Executable file
22
wasm-port/tests/wasm/node/verify_task_hal_wasm.sh
Executable 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"
|
||||
58
wasm-port/tools/build_task_hal_wasm.sh
Executable file
58
wasm-port/tools/build_task_hal_wasm.sh
Executable 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"
|
||||
28
wasm-port/tools/task-hal-source-manifest.txt
Normal file
28
wasm-port/tools/task-hal-source-manifest.txt
Normal 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
|
||||
150
wasm-port/tools/verify_task_hal_source_manifest.sh
Executable file
150
wasm-port/tools/verify_task_hal_source_manifest.sh
Executable 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
|
||||
@@ -7,7 +7,7 @@
|
||||
"build": "node scripts/build-static.mjs",
|
||||
"dev": "python3 -m http.server 4173",
|
||||
"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": {},
|
||||
"devDependencies": {}
|
||||
|
||||
@@ -15,6 +15,7 @@ await copyLinuxCncConfigAssets();
|
||||
await copyKinematicsRuntimeAssets();
|
||||
await copyInterpreterRuntimeAssets();
|
||||
await copyTpRuntimeAssets();
|
||||
await copyTaskHalRuntimeAssets();
|
||||
|
||||
const packageJson = JSON.parse(await readFile(join(appRoot, "package.json"), "utf8"));
|
||||
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");
|
||||
|
||||
async function copyLinuxCncConfigAssets() {
|
||||
@@ -72,7 +82,7 @@ async function copyKinematicsRuntimeAssets() {
|
||||
join(sdkSrcDir, "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));
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import { createLinuxCncInterpreterWorkerRuntime } from "./runtime/linuxcnc-inter
|
||||
import { createLinuxCncKinematicsRuntime } from "./runtime/linuxcnc-kinematics-runtime.js";
|
||||
import { createLinuxCncKinematicsWorkerRuntime } from "./runtime/linuxcnc-kinematics-worker-client.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");
|
||||
|
||||
@@ -17,6 +19,7 @@ const shell = mountGmoccapyShell(app, store);
|
||||
const iniConfigReady = attachProfileIniConfig(store, store.getState().profile);
|
||||
const kinematicsRuntimeReady = attachDefaultKinematicsRuntime(store, store.getState().profile.kinematicsModuleId || store.getState().machineProfile);
|
||||
const interpreterRuntimeReady = attachDefaultInterpreterRuntime(store);
|
||||
const taskHalRuntimeReady = attachDefaultTaskHalRuntime(store);
|
||||
let attachedKinematicsProfile = store.getState().machineProfile;
|
||||
let attachedIniProfile = store.getState().machineProfile;
|
||||
store.subscribe((state) => {
|
||||
@@ -42,6 +45,7 @@ window.webRtcp5AxisSimulation = {
|
||||
iniConfigReady,
|
||||
kinematicsRuntimeReady,
|
||||
interpreterRuntimeReady,
|
||||
taskHalRuntimeReady,
|
||||
};
|
||||
|
||||
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(" | "),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -35,6 +35,37 @@ export function createFullLinuxCncExecutionBoundary(state = {}) {
|
||||
programExecution?.plannerTiming?.plannerRuntimeReady === true,
|
||||
);
|
||||
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 = [
|
||||
kinematicsReady ? "linuxcnc-kinematics-wasm" : null,
|
||||
@@ -44,6 +75,11 @@ export function createFullLinuxCncExecutionBoundary(state = {}) {
|
||||
machineFileRemapReady ? "fiveaxis-remap-machine-file-run" : null,
|
||||
plannerRuntimeReady ? "linuxcnc-tp-queue-runtime-timing" : 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);
|
||||
|
||||
const missing = [];
|
||||
@@ -54,12 +90,16 @@ export function createFullLinuxCncExecutionBoundary(state = {}) {
|
||||
if (!machineFileRemapReady) missing.push("machine-file backed five-axis remap run");
|
||||
if (!plannerRuntimeReady) missing.push("LinuxCNC trajectory planner queue timing runtime");
|
||||
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 = [
|
||||
"native LinuxCNC task/NML process is not ported",
|
||||
"native realtime HAL thread synchronization is not ported",
|
||||
"external user-M process and full tool DB process are not promoted",
|
||||
];
|
||||
const blockers = [];
|
||||
if (!nativeTaskReady) blockers.push("LinuxCNC task runtime is not promoted");
|
||||
if (!nativeHalSyncReady) blockers.push("realtime HAL synchronization is not promoted");
|
||||
blockers.push("external user-M process and full tool DB process are not promoted");
|
||||
if (!plannerRuntimeReady) {
|
||||
blockers.push("LinuxCNC trajectory planner queue is not promoted as browser runtime");
|
||||
}
|
||||
@@ -67,12 +107,16 @@ export function createFullLinuxCncExecutionBoundary(state = {}) {
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-full-linuxcnc-execution-boundary",
|
||||
profileId: state.machineProfile || adapter.profileId || "unknown",
|
||||
phase: machineFileRemapReady
|
||||
phase: fullLinuxCncProgramExecutionReady
|
||||
? "linuxcnc-task-motion-hal-simulation-runtime"
|
||||
: machineFileRemapReady
|
||||
? "partial-linuxcnc-remap-boundary"
|
||||
: canonicalProgramReady
|
||||
? "canonical-interpreter-boundary"
|
||||
: "blocked",
|
||||
semanticBoundary: machineFileRemapReady
|
||||
semanticBoundary: fullLinuxCncProgramExecutionReady
|
||||
? "linuxcnc_task_motion_hal_wasm_simulation_runtime"
|
||||
: machineFileRemapReady
|
||||
? "linuxcnc_machine_file_remap_ready_planner_task_hal_blocked"
|
||||
: canonicalProgramReady
|
||||
? "linuxcnc_interpreter_canonical_ready_planner_task_hal_blocked"
|
||||
@@ -85,10 +129,19 @@ export function createFullLinuxCncExecutionBoundary(state = {}) {
|
||||
remapRuntimeReady: machineFileRemapReady,
|
||||
halSwitchkinsEvidenceReady,
|
||||
plannerRuntimeReady,
|
||||
nativeTaskReady: false,
|
||||
nativeHalSyncReady: false,
|
||||
fullLinuxCncProgramExecutionReady: false,
|
||||
promotionAllowed: false,
|
||||
taskRuntimeReady,
|
||||
motionRuntimeReady,
|
||||
halRuntimeReady,
|
||||
halSyncReady,
|
||||
taskHalComparisonReady,
|
||||
nativeTaskReady,
|
||||
nativeHalSyncReady,
|
||||
fullLinuxCncProgramExecutionReady,
|
||||
promotionAllowed: fullLinuxCncProgramExecutionReady,
|
||||
hardwareDrive: false,
|
||||
hostRealtimeKernel: false,
|
||||
externalUserMProcessReady: false,
|
||||
toolDbProcessReady: false,
|
||||
satisfied,
|
||||
missing,
|
||||
blockers,
|
||||
@@ -101,6 +154,11 @@ export function createFullLinuxCncExecutionBoundary(state = {}) {
|
||||
machineFileFlags: MACHINE_FILE_FLAGS.filter((flag) => machineFileText.includes(flag)),
|
||||
machineFileExecutionReady: machineFileExecution?.summary?.machineFileExecutionReady === true,
|
||||
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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -52,6 +52,13 @@ export async function createMachineFileStagingPlan({
|
||||
kind: classifySourceRel(file.sourceRel),
|
||||
})),
|
||||
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",
|
||||
};
|
||||
}
|
||||
@@ -86,6 +93,11 @@ export function selectMachineFileProgram(plan, save, sourceRel) {
|
||||
selectedProgramSourceRel: selectedFile.sourceRel,
|
||||
selectedProgramFilename: basename(selectedFile.sourceRel),
|
||||
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",
|
||||
};
|
||||
}
|
||||
@@ -120,6 +132,16 @@ export async function saveMachineFileStagingPlan(plan, options = {}) {
|
||||
files: savedFiles,
|
||||
gcodeSources: listLinuxCncGcodeSources({ files: 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",
|
||||
};
|
||||
}
|
||||
@@ -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) {
|
||||
const bySourceRel = new Map(files.map((file) => [file.sourceRel, file]));
|
||||
for (const sourceRel of String(manifestText).split(/\r?\n/)) {
|
||||
|
||||
@@ -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 || "",
|
||||
};
|
||||
}
|
||||
@@ -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 });
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,9 @@ import {
|
||||
selectMachineFileProgram,
|
||||
stageProfileMachineFiles,
|
||||
} from "../runtime/linuxcnc-machine-file-staging.js";
|
||||
import {
|
||||
buildTaskHalSessionFromMachineFiles,
|
||||
} from "../runtime/linuxcnc-task-hal-runtime.js";
|
||||
import {
|
||||
createLinuxCncTaskPolicyStatus,
|
||||
gateLinuxCncTaskAction,
|
||||
@@ -128,6 +131,13 @@ const initialState = {
|
||||
programExecutionMotionIndex: 0,
|
||||
programExecutionSampleIndex: 0,
|
||||
programRuntimeFeedback: null,
|
||||
taskHalRuntime: null,
|
||||
taskHalRuntimeReadiness: null,
|
||||
taskHalStatus: null,
|
||||
taskHalSession: null,
|
||||
taskHalExecutionPending: false,
|
||||
taskHalExecutionSequence: 0,
|
||||
taskHalFallbackReason: null,
|
||||
interpreterExecutionPending: false,
|
||||
interpreterExecutionSequence: 0,
|
||||
machineFileExecution: null,
|
||||
@@ -404,6 +414,50 @@ export function createSimulationStore(seed = {}) {
|
||||
});
|
||||
}
|
||||
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":
|
||||
if (!state.interpreterRuntime?.loaded) {
|
||||
setState({
|
||||
@@ -657,6 +711,9 @@ export function createSimulationStore(seed = {}) {
|
||||
},
|
||||
operatorMessage: `LinuxCNC machine files staged ${action.save.fileCount}`,
|
||||
});
|
||||
if (state.taskHalRuntime?.loaded) {
|
||||
initializeTaskHalSession().catch(() => {});
|
||||
}
|
||||
break;
|
||||
case "LOAD_LINUXCNC_GCODE_SOURCE":
|
||||
{
|
||||
@@ -700,11 +757,31 @@ export function createSimulationStore(seed = {}) {
|
||||
},
|
||||
operatorMessage: `loaded LinuxCNC 5-axis source ${selectedFile.sourceRel}`,
|
||||
});
|
||||
if (state.taskHalRuntime?.loaded) {
|
||||
initializeTaskHalSession({ openProgram: true }).catch(() => {});
|
||||
}
|
||||
if (state.interpreterRuntime?.loaded) {
|
||||
dispatch({ type: "RUN_INTERPRETER_PROGRAM" });
|
||||
}
|
||||
}
|
||||
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":
|
||||
setState({
|
||||
machineFileStaging: {
|
||||
@@ -745,6 +822,12 @@ export function createSimulationStore(seed = {}) {
|
||||
setState({ operatorMessage: gate.operatorMessage });
|
||||
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;
|
||||
setState({
|
||||
machine: {
|
||||
@@ -825,6 +908,12 @@ export function createSimulationStore(seed = {}) {
|
||||
break;
|
||||
}
|
||||
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({
|
||||
machine: {
|
||||
...state.machine,
|
||||
@@ -857,6 +946,17 @@ export function createSimulationStore(seed = {}) {
|
||||
const axis = action.axis || state.machine.jogAxis;
|
||||
const direction = Number(action.direction || 1);
|
||||
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({
|
||||
machine: {
|
||||
...state.machine,
|
||||
@@ -880,6 +980,14 @@ export function createSimulationStore(seed = {}) {
|
||||
setState({ operatorMessage: gate.operatorMessage });
|
||||
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);
|
||||
setState(mdiResult.patch);
|
||||
}
|
||||
@@ -917,6 +1025,14 @@ export function createSimulationStore(seed = {}) {
|
||||
setState({ operatorMessage: gate.operatorMessage });
|
||||
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);
|
||||
setState({
|
||||
machine: {
|
||||
@@ -952,6 +1068,12 @@ export function createSimulationStore(seed = {}) {
|
||||
setState({ operatorMessage: gate.operatorMessage });
|
||||
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({
|
||||
machine: {
|
||||
...state.machine,
|
||||
@@ -975,6 +1097,12 @@ export function createSimulationStore(seed = {}) {
|
||||
setState({ operatorMessage: gate.operatorMessage });
|
||||
break;
|
||||
}
|
||||
if (state.taskHalRuntime?.loaded) {
|
||||
runTaskHalCommandSequence([
|
||||
{ type: "EMC_TASK_PLAN_PAUSE" },
|
||||
], { operatorMessage: "task/HAL program paused" }).catch(() => {});
|
||||
break;
|
||||
}
|
||||
setState({
|
||||
machine: {
|
||||
...state.machine,
|
||||
@@ -996,6 +1124,12 @@ export function createSimulationStore(seed = {}) {
|
||||
setState({ operatorMessage: gate.operatorMessage });
|
||||
break;
|
||||
}
|
||||
if (state.taskHalRuntime?.loaded) {
|
||||
runTaskHalCommandSequence([
|
||||
{ type: "EMC_TASK_PLAN_RESUME" },
|
||||
], { operatorMessage: "task/HAL program resumed" }).catch(() => {});
|
||||
break;
|
||||
}
|
||||
const resumeState = state.machine.interpResumeState === "idle"
|
||||
? "reading"
|
||||
: state.machine.interpResumeState;
|
||||
@@ -1333,6 +1467,114 @@ export function createSimulationStore(seed = {}) {
|
||||
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 = () => {
|
||||
if (!state.kinematicsRuntime?.loaded || !isAsyncKinematicsRuntime(state.kinematicsRuntime)) return null;
|
||||
if (state.frameSourceMode !== "source-derived-kinematics-wasm") return null;
|
||||
@@ -1360,6 +1602,7 @@ export function createSimulationStore(seed = {}) {
|
||||
restoreSession,
|
||||
stageMachineFiles,
|
||||
runFullBoundaryAudit,
|
||||
initializeTaskHalSession,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1454,6 +1697,115 @@ function isAsyncKinematicsRuntime(runtime) {
|
||||
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) {
|
||||
return createLinuxCncTaskPolicyStatus(state).canMove;
|
||||
}
|
||||
|
||||
@@ -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>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>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>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>
|
||||
@@ -434,6 +436,24 @@ function formatMachineFileExecution(machineFileExecution) {
|
||||
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) {
|
||||
if (!boundary) return "pending";
|
||||
const remap = boundary.machineFileBackedRemapReady ? "remap ready" : "remap pending";
|
||||
|
||||
@@ -362,10 +362,30 @@ full_program_execution=partial_interpreter_canonical_and_switchkins_event_ready_
|
||||
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_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
|
||||
next_batch=trajectory_planner_wasm_boundary_or_native_task_hal_port
|
||||
planner_task_hal_gap=closed_for_web_simulation_boundary_with_task_motion_hal_wasm_runtime
|
||||
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/events,RUN/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 任务
|
||||
|
||||
```text
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
### 阶段 1:Native 对照探针
|
||||
|
||||
目标:
|
||||
|
||||
- 先用 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
|
||||
```
|
||||
|
||||
### 阶段 2:HAL 内存模型和线程调度器
|
||||
|
||||
目标:
|
||||
|
||||
- 用 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
|
||||
```
|
||||
|
||||
### 阶段 3:Motion 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
|
||||
```
|
||||
|
||||
### 阶段 4:Task 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
|
||||
```
|
||||
|
||||
### 阶段 5:TRT 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
|
||||
```
|
||||
|
||||
### 阶段 6:SWITCHKINS 和 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
|
||||
```
|
||||
|
||||
### 阶段 8:Full 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
|
||||
```
|
||||
@@ -471,10 +471,12 @@ readiness
|
||||
`runMachineFileProgram()` 完成 canonical motion 后立即跑 TP queue timing。
|
||||
5. `programExecution.summary.plannerRuntimeReady=true` 只在 TP runtime 调用成功且
|
||||
motionCount 一致时成立;否则保留 canonical execution,但不能把 planner timing 标为 ready。
|
||||
6. `web-rtcp-5axis-full-linuxcnc-execution-boundary` 可把 `plannerRuntimeReady=true` 作为
|
||||
已满足项,但仍必须显示 `nativeTaskReady=false`、`nativeHalSyncReady=false` 和
|
||||
`fullLinuxCncProgramExecutionReady=false`,因为 native task/NML、HAL realtime thread 和
|
||||
硬件驱动没有接入。
|
||||
6. `web-rtcp-5axis-full-linuxcnc-execution-boundary` 可把 `plannerRuntimeReady=true` 和
|
||||
task/motion/HAL WASM cycle artifact 作为已满足项;只有 task runtime、motion runtime、
|
||||
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
|
||||
timing.semanticBoundary=linuxcnc_tp_queue_runtime_timing_from_canonical_motion
|
||||
plannerRuntimeReady=true
|
||||
nativeTaskReady=false
|
||||
nativeHalSyncReady=false
|
||||
fullLinuxCncProgramExecutionReady=false
|
||||
taskHal.semanticBoundary=linuxcnc_task_motion_hal_wasm_simulation_runtime
|
||||
nativeTaskReady=true for Web simulation boundary
|
||||
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. 开工建议
|
||||
|
||||
当前已完成 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 boundary:source 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 证明;当前完成范围仍限定为浏览器仿真。
|
||||
|
||||
@@ -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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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, 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 |
|
||||
| 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 |
|
||||
@@ -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` |
|
||||
| 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 |
|
||||
| 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 evidence;Web 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 |
|
||||
| Python GUI runtime | not_ported | 只参考,不运行 |
|
||||
| Python remap runtime | blocked | 不在第一版实现 |
|
||||
@@ -958,6 +959,182 @@ Next:
|
||||
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 追溯记录
|
||||
|
||||
```text
|
||||
|
||||
@@ -131,6 +131,18 @@
|
||||
) {
|
||||
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();
|
||||
if (state.sourceMode !== "source-derived-kinematics-wasm") {
|
||||
throw new Error(`unexpected source mode: ${state.sourceMode}`);
|
||||
@@ -349,24 +361,9 @@
|
||||
if (machineFileRunState.fullExecutionBoundary?.machineFileBackedRemapReady !== true) {
|
||||
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")) {
|
||||
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();
|
||||
await wait(50);
|
||||
if (savedSession.snapshot.format !== "web-rtcp-5axis-session-snapshot") {
|
||||
@@ -383,18 +380,34 @@
|
||||
}
|
||||
|
||||
win.webRtcp5AxisSimulation.dispatch({ type: "RUN" });
|
||||
await wait(50);
|
||||
await wait(250);
|
||||
if (win.webRtcp5AxisSimulation.getState().runState !== "running") {
|
||||
throw new Error("RUN action did not update state after power on");
|
||||
}
|
||||
if (win.webRtcp5AxisSimulation.getState().programRuntimeFeedback?.sourceMode !== "linuxcnc-tp-runtime-sample") {
|
||||
throw new Error("RUN did not advance with LinuxCNC TP runtime feedback");
|
||||
if (win.webRtcp5AxisSimulation.getState().programRuntimeFeedback?.sourceMode !== "linuxcnc-task-motion-hal-wasm") {
|
||||
throw new Error("RUN did not advance with LinuxCNC task/HAL runtime feedback");
|
||||
}
|
||||
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) {
|
||||
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();
|
||||
await wait(50);
|
||||
|
||||
@@ -8,7 +8,7 @@ assert.equal(blocked.phase, "blocked");
|
||||
assert.equal(blocked.promotionAllowed, false);
|
||||
assert.equal(blocked.fullLinuxCncProgramExecutionReady, false);
|
||||
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 = {
|
||||
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.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");
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
11
备忘/备忘1.txt
11
备忘/备忘1.txt
@@ -27,3 +27,14 @@ EMSDK_QUIET=1 source /home/cnc/emsdk/emsdk_env.sh >/dev/null && wasm-port/tests/
|
||||
输出包含:
|
||||
kinematics_wasm_node_smoke=ok
|
||||
准确状态现在是: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 记录不改。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user