diff --git a/wasm-port/docs/source-reuse-map.md b/wasm-port/docs/source-reuse-map.md index 57bbb0f..5692197 100644 --- a/wasm-port/docs/source-reuse-map.md +++ b/wasm-port/docs/source-reuse-map.md @@ -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. diff --git a/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_hal_runtime.cpp b/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_hal_runtime.cpp new file mode 100644 index 0000000..520d193 --- /dev/null +++ b/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_hal_runtime.cpp @@ -0,0 +1,968 @@ +#include "linuxcnc_hal_runtime.hh" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 functions; +}; + +struct HalRuntime { + int next_comp_id = 1; + bool ready = false; + bool threads_running = false; + long long cycle = 0; + std::map pins; + std::map signals; + std::map params; + std::map threads; + std::vector allocations; + std::vector 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(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(value.size()) + 1; + if (out_len < required) { + if (out_len > 0) { + out[0] = '\0'; + } + return required; + } + std::memcpy(out, value.c_str(), static_cast(required)); + return 0; +} + +std::vector split_words(const std::string &line) +{ + std::istringstream in(line); + std::vector 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 +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(&entry.value.b); + break; + case HAL_FLOAT: + *data_ptr_addr = reinterpret_cast(&entry.value.f); + break; + case HAL_S32: + *data_ptr_addr = reinterpret_cast(&entry.value.s); + break; + case HAL_U32: + *data_ptr_addr = reinterpret_cast(&entry.value.u); + break; + case HAL_S64: + *data_ptr_addr = reinterpret_cast(&entry.value.ls); + break; + case HAL_U64: + *data_ptr_addr = reinterpret_cast(&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 +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(data_addr); + break; + case HAL_FLOAT: + entry.value.f = *static_cast(data_addr); + break; + case HAL_S32: + entry.value.s = *static_cast(data_addr); + break; + case HAL_U32: + entry.value.u = *static_cast(data_addr); + break; + case HAL_S64: + entry.value.ls = *static_cast(data_addr); + break; + case HAL_U64: + entry.value.lu = *static_cast(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(dir); + entry.connected = false; + copy_value_from_addr(entry, data_addr); + runtime().events.push_back(std::string("param_create:") + name); + return 0; +} + +template +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(std::strtol(text, nullptr, 0)); + return true; + case HAL_U32: + entry.value.u = static_cast(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 &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(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(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(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(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(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(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(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(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(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(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(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(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(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 : "")); + 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" diff --git a/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_hal_runtime.hh b/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_hal_runtime.hh new file mode 100644 index 0000000..ead791e --- /dev/null +++ b/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_hal_runtime.hh @@ -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 diff --git a/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_hal_runtime_probe.cpp b/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_hal_runtime_probe.cpp new file mode 100644 index 0000000..93f41b0 --- /dev/null +++ b/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_hal_runtime_probe.cpp @@ -0,0 +1,180 @@ +#include "linuxcnc_hal_runtime.hh" + +#include +#include +#include + +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; +} diff --git a/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_motion_runtime.c b/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_motion_runtime.c new file mode 100644 index 0000000..a5ac6db --- /dev/null +++ b/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_motion_runtime.c @@ -0,0 +1,464 @@ +#include "linuxcnc_motion_runtime.h" + +#include "hal.h" +#include "linuxcnc_hal_runtime.hh" + +#include +#include +#include +#include + +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; +} diff --git a/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_motion_runtime.h b/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_motion_runtime.h new file mode 100644 index 0000000..c427144 --- /dev/null +++ b/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_motion_runtime.h @@ -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 diff --git a/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_task_hal_wasm.cpp b/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_task_hal_wasm.cpp new file mode 100644 index 0000000..9f05cb0 --- /dev/null +++ b/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_task_hal_wasm.cpp @@ -0,0 +1,467 @@ +#include "linuxcnc_task_hal_wasm.hh" + +#include "linuxcnc_motion_runtime.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 staged_files; + std::vector program_lines; + std::vector 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(value.size()) + 1; + if (out_len < required) { + out[0] = '\0'; + return required; + } + std::memcpy(out, value.c_str(), static_cast(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(*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(*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(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(value[begin]))) { + begin += 1; + } + std::size_t end = value.size(); + while (end > begin && std::isspace(static_cast(value[end - 1]))) { + end -= 1; + } + return value.substr(begin, end - begin); +} + +std::vector split_program_lines(const std::string &text) +{ + std::vector 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(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 buffer(8192); + const int rc = lcmot_read_status_json(buffer.data(), static_cast(buffer.size())); + if (rc != 0) { + return "{}"; + } + return buffer.data(); +} + +std::string read_hal_snapshot_json() +{ + std::vector buffer(65536); + const int rc = lcmot_read_hal_snapshot_json(buffer.data(), static_cast(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(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(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" diff --git a/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_task_hal_wasm.hh b/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_task_hal_wasm.hh new file mode 100644 index 0000000..0643ff2 --- /dev/null +++ b/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_task_hal_wasm.hh @@ -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 diff --git a/wasm-port/runtime/core/shims/hal.h b/wasm-port/runtime/core/shims/hal.h index e7a5fb3..09fa91a 100644 --- a/wasm-port/runtime/core/shims/hal.h +++ b/wasm-port/runtime/core/shims/hal.h @@ -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 diff --git a/wasm-port/runtime/sdk/src/index.js b/wasm-port/runtime/sdk/src/index.js index e883f08..41d09cc 100644 --- a/wasm-port/runtime/sdk/src/index.js +++ b/wasm-port/runtime/sdk/src/index.js @@ -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, diff --git a/wasm-port/runtime/sdk/src/linuxcnc-task-hal.js b/wasm-port/runtime/sdk/src/linuxcnc-task-hal.js new file mode 100644 index 0000000..4f69e81 --- /dev/null +++ b/wasm-port/runtime/sdk/src/linuxcnc-task-hal.js @@ -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")(); + }, + }; +} diff --git a/wasm-port/tests/native/probe_trt_task_hal_runtime.sh b/wasm-port/tests/native/probe_trt_task_hal_runtime.sh new file mode 100755 index 0000000..32ef564 --- /dev/null +++ b/wasm-port/tests/native/probe_trt_task_hal_runtime.sh @@ -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 diff --git a/wasm-port/tests/native/verify_task_hal_phase0.sh b/wasm-port/tests/native/verify_task_hal_phase0.sh new file mode 100755 index 0000000..c418f91 --- /dev/null +++ b/wasm-port/tests/native/verify_task_hal_phase0.sh @@ -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" diff --git a/wasm-port/tests/wasm/node/verify_hal_runtime.mjs b/wasm-port/tests/wasm/node/verify_hal_runtime.mjs new file mode 100644 index 0000000..e81bd88 --- /dev/null +++ b/wasm-port/tests/wasm/node/verify_hal_runtime.mjs @@ -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"); diff --git a/wasm-port/tests/wasm/node/verify_hal_runtime.sh b/wasm-port/tests/wasm/node/verify_hal_runtime.sh new file mode 100755 index 0000000..2342385 --- /dev/null +++ b/wasm-port/tests/wasm/node/verify_hal_runtime.sh @@ -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" diff --git a/wasm-port/tests/wasm/node/verify_motion_hal_sync.mjs b/wasm-port/tests/wasm/node/verify_motion_hal_sync.mjs new file mode 100644 index 0000000..2edf27b --- /dev/null +++ b/wasm-port/tests/wasm/node/verify_motion_hal_sync.mjs @@ -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"); diff --git a/wasm-port/tests/wasm/node/verify_motion_hal_sync.sh b/wasm-port/tests/wasm/node/verify_motion_hal_sync.sh new file mode 100755 index 0000000..105d443 --- /dev/null +++ b/wasm-port/tests/wasm/node/verify_motion_hal_sync.sh @@ -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" diff --git a/wasm-port/tests/wasm/node/verify_task_hal_sdk.mjs b/wasm-port/tests/wasm/node/verify_task_hal_sdk.mjs new file mode 100644 index 0000000..44c7267 --- /dev/null +++ b/wasm-port/tests/wasm/node/verify_task_hal_sdk.mjs @@ -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"); diff --git a/wasm-port/tests/wasm/node/verify_task_hal_sdk.sh b/wasm-port/tests/wasm/node/verify_task_hal_sdk.sh new file mode 100755 index 0000000..3849687 --- /dev/null +++ b/wasm-port/tests/wasm/node/verify_task_hal_sdk.sh @@ -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" diff --git a/wasm-port/tests/wasm/node/verify_task_hal_wasm.mjs b/wasm-port/tests/wasm/node/verify_task_hal_wasm.mjs new file mode 100644 index 0000000..1ddcd59 --- /dev/null +++ b/wasm-port/tests/wasm/node/verify_task_hal_wasm.mjs @@ -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"); diff --git a/wasm-port/tests/wasm/node/verify_task_hal_wasm.sh b/wasm-port/tests/wasm/node/verify_task_hal_wasm.sh new file mode 100755 index 0000000..2ca5d69 --- /dev/null +++ b/wasm-port/tests/wasm/node/verify_task_hal_wasm.sh @@ -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" diff --git a/wasm-port/tools/build_task_hal_wasm.sh b/wasm-port/tools/build_task_hal_wasm.sh new file mode 100755 index 0000000..a86e14e --- /dev/null +++ b/wasm-port/tools/build_task_hal_wasm.sh @@ -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" diff --git a/wasm-port/tools/task-hal-source-manifest.txt b/wasm-port/tools/task-hal-source-manifest.txt new file mode 100644 index 0000000..a5ddb4d --- /dev/null +++ b/wasm-port/tools/task-hal-source-manifest.txt @@ -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 diff --git a/wasm-port/tools/verify_task_hal_source_manifest.sh b/wasm-port/tools/verify_task_hal_source_manifest.sh new file mode 100755 index 0000000..5eadf60 --- /dev/null +++ b/wasm-port/tools/verify_task_hal_source_manifest.sh @@ -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 diff --git a/web-rtcp-5axis-sim-plan/app/package.json b/web-rtcp-5axis-sim-plan/app/package.json index 02d6318..837e80c 100644 --- a/web-rtcp-5axis-sim-plan/app/package.json +++ b/web-rtcp-5axis-sim-plan/app/package.json @@ -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": {} diff --git a/web-rtcp-5axis-sim-plan/app/scripts/build-static.mjs b/web-rtcp-5axis-sim-plan/app/scripts/build-static.mjs index 96af09d..e2374ce 100644 --- a/web-rtcp-5axis-sim-plan/app/scripts/build-static.mjs +++ b/web-rtcp-5axis-sim-plan/app/scripts/build-static.mjs @@ -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)); } diff --git a/web-rtcp-5axis-sim-plan/app/src/main.js b/web-rtcp-5axis-sim-plan/app/src/main.js index 08d775c..7d61af8 100644 --- a/web-rtcp-5axis-sim-plan/app/src/main.js +++ b/web-rtcp-5axis-sim-plan/app/src/main.js @@ -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(" | "), + }; +} diff --git a/web-rtcp-5axis-sim-plan/app/src/runtime/full-execution-boundary.js b/web-rtcp-5axis-sim-plan/app/src/runtime/full-execution-boundary.js index d8bf606..e5227ee 100644 --- a/web-rtcp-5axis-sim-plan/app/src/runtime/full-execution-boundary.js +++ b/web-rtcp-5axis-sim-plan/app/src/runtime/full-execution-boundary.js @@ -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, }, }; } diff --git a/web-rtcp-5axis-sim-plan/app/src/runtime/linuxcnc-machine-file-staging.js b/web-rtcp-5axis-sim-plan/app/src/runtime/linuxcnc-machine-file-staging.js index d56dd3e..ed11d51 100644 --- a/web-rtcp-5axis-sim-plan/app/src/runtime/linuxcnc-machine-file-staging.js +++ b/web-rtcp-5axis-sim-plan/app/src/runtime/linuxcnc-machine-file-staging.js @@ -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/)) { diff --git a/web-rtcp-5axis-sim-plan/app/src/runtime/linuxcnc-task-hal-runtime.js b/web-rtcp-5axis-sim-plan/app/src/runtime/linuxcnc-task-hal-runtime.js new file mode 100644 index 0000000..88c5cd1 --- /dev/null +++ b/web-rtcp-5axis-sim-plan/app/src/runtime/linuxcnc-task-hal-runtime.js @@ -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 || "", + }; +} diff --git a/web-rtcp-5axis-sim-plan/app/src/runtime/linuxcnc-task-hal-worker-client.js b/web-rtcp-5axis-sim-plan/app/src/runtime/linuxcnc-task-hal-worker-client.js new file mode 100644 index 0000000..e0ca9f9 --- /dev/null +++ b/web-rtcp-5axis-sim-plan/app/src/runtime/linuxcnc-task-hal-worker-client.js @@ -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 }); + }); + }, + }; +} diff --git a/web-rtcp-5axis-sim-plan/app/src/runtime/linuxcnc-task-hal-worker.js b/web-rtcp-5axis-sim-plan/app/src/runtime/linuxcnc-task-hal-worker.js new file mode 100644 index 0000000..522995a --- /dev/null +++ b/web-rtcp-5axis-sim-plan/app/src/runtime/linuxcnc-task-hal-worker.js @@ -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"); + } +} diff --git a/web-rtcp-5axis-sim-plan/app/src/state/store.js b/web-rtcp-5axis-sim-plan/app/src/state/store.js index 04e38f5..89d210b 100644 --- a/web-rtcp-5axis-sim-plan/app/src/state/store.js +++ b/web-rtcp-5axis-sim-plan/app/src/state/store.js @@ -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; } diff --git a/web-rtcp-5axis-sim-plan/app/src/ui/gmoccapy-shell.js b/web-rtcp-5axis-sim-plan/app/src/ui/gmoccapy-shell.js index 5eb24db..8cc83e5 100644 --- a/web-rtcp-5axis-sim-plan/app/src/ui/gmoccapy-shell.js +++ b/web-rtcp-5axis-sim-plan/app/src/ui/gmoccapy-shell.js @@ -343,6 +343,8 @@ function renderInfoTabs(element, state) {
INI joints:
${state.iniConfigReadiness.jointCount ?? 0} joints / ${state.iniConfigReadiness.axisCount ?? 0} axes
Machine files:
${formatMachineFileStaging(state.machineFileStaging)}
LinuxCNC G-code:
${state.machineFileStaging.selectedGcodeSourceRel || state.programSourceRel || "-"}
+
Task/HAL:
${formatTaskHalReadiness(state)}
+
Task/HAL cycles:
${formatTaskHalCycles(state)}
Full boundary:
${formatFullExecutionBoundary(state.fullExecutionBoundary)}
Planner/task:
${formatFullExecutionBlockers(state.fullExecutionBoundary)}
Boundary evidence:
${formatFullExecutionEvidence(state.fullExecutionBoundary)}
@@ -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"; diff --git a/web-rtcp-5axis-sim-plan/docs/development-continuation.md b/web-rtcp-5axis-sim-plan/docs/development-continuation.md index 93bd439..f558484 100644 --- a/web-rtcp-5axis-sim-plan/docs/development-continuation.md +++ b/web-rtcp-5axis-sim-plan/docs/development-continuation.md @@ -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 diff --git a/web-rtcp-5axis-sim-plan/docs/native-task-hal-sync-implementation-steps.md b/web-rtcp-5axis-sim-plan/docs/native-task-hal-sync-implementation-steps.md new file mode 100644 index 0000000..6119502 --- /dev/null +++ b/web-rtcp-5axis-sim-plan/docs/native-task-hal-sync-implementation-steps.md @@ -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 +``` diff --git a/web-rtcp-5axis-sim-plan/docs/program-implementation-guide.md b/web-rtcp-5axis-sim-plan/docs/program-implementation-guide.md index 92173cc..0238001 100644 --- a/web-rtcp-5axis-sim-plan/docs/program-implementation-guide.md +++ b/web-rtcp-5axis-sim-plan/docs/program-implementation-guide.md @@ -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 证明;当前完成范围仍限定为浏览器仿真。 diff --git a/web-rtcp-5axis-sim-plan/docs/traceability-matrix.md b/web-rtcp-5axis-sim-plan/docs/traceability-matrix.md index 55e4d2f..3528bd3 100644 --- a/web-rtcp-5axis-sim-plan/docs/traceability-matrix.md +++ b/web-rtcp-5axis-sim-plan/docs/traceability-matrix.md @@ -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 diff --git a/web-rtcp-5axis-sim-plan/tests/browser/gmoccapy_shell_smoke.html b/web-rtcp-5axis-sim-plan/tests/browser/gmoccapy_shell_smoke.html index fdf99df..1172908 100644 --- a/web-rtcp-5axis-sim-plan/tests/browser/gmoccapy_shell_smoke.html +++ b/web-rtcp-5axis-sim-plan/tests/browser/gmoccapy_shell_smoke.html @@ -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); diff --git a/web-rtcp-5axis-sim-plan/tests/node/verify_full_execution_boundary.mjs b/web-rtcp-5axis-sim-plan/tests/node/verify_full_execution_boundary.mjs index 749d4e3..771ca7e 100644 --- a/web-rtcp-5axis-sim-plan/tests/node/verify_full_execution_boundary.mjs +++ b/web-rtcp-5axis-sim-plan/tests/node/verify_full_execution_boundary.mjs @@ -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"); diff --git a/web-rtcp-5axis-sim-plan/tests/node/verify_linuxcnc_task_hal_runtime.mjs b/web-rtcp-5axis-sim-plan/tests/node/verify_linuxcnc_task_hal_runtime.mjs new file mode 100644 index 0000000..e57f04e --- /dev/null +++ b/web-rtcp-5axis-sim-plan/tests/node/verify_linuxcnc_task_hal_runtime.mjs @@ -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"); +} diff --git a/备忘/备忘1.txt b/备忘/备忘1.txt index 1036f99..e80b5aa 100644 --- a/备忘/备忘1.txt +++ b/备忘/备忘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 记录不改。 + + + + +