From 55d9fff9f0768cd1b2a1089a0810534216801166 Mon Sep 17 00:00:00 2001 From: CNC Local Date: Fri, 22 May 2026 04:43:21 +0800 Subject: [PATCH] Initial wasm simulator checkpoint --- .gitignore | 10 + README.zh-CN.md | 163 ++++ build-wasm.sh | 16 + core/CMakeLists.txt | 174 +++++ core/include/cnc_sim_api.h | 88 +++ core/src/canon_event_sink.cpp | 242 ++++++ core/src/canon_event_sink.h | 64 ++ core/src/cnc_sim_api.cpp | 235 ++++++ core/src/gcode_backend.cpp | 44 ++ core/src/gcode_backend.h | 20 + core/src/linuxcnc_canon_bridge.cpp | 703 ++++++++++++++++++ core/src/linuxcnc_canon_bridge.h | 7 + core/src/linuxcnc_rs274_backend.cpp | 179 +++++ core/src/linuxcnc_rs274_backend.h | 12 + core/src/rtcp_kinematics.cpp | 72 ++ core/src/rtcp_kinematics.h | 17 + core/src/simulator_gcode_controls.cpp | 145 ++++ core/src/simulator_gcode_controls.h | 24 + core/src/smoke_gcode_parser.cpp | 399 ++++++++++ core/src/smoke_gcode_parser.h | 18 + core/tests/canon_event_sink_smoke.cpp | 61 ++ .../cnc_sim_api_linuxcnc_rs274_smoke.cpp | 162 ++++ core/tests/cnc_sim_api_smoke.cpp | 141 ++++ core/tests/linuxcnc_canon_bridge_smoke.cpp | 96 +++ core/tests/rtcp_kinematics_smoke.cpp | 67 ++ core/tests/simulator_gcode_controls_smoke.cpp | 59 ++ core/tools/cnc_sim_dump.cpp | 120 +++ core/tools/linuxcnc_rs274_dump.cpp | 250 +++++++ docs/architecture.md | 80 ++ docs/linuxcnc-porting.md | 194 +++++ docs/linuxcnc-rs274-source-map.md | 125 ++++ linuxcnc-rs274-source-files.txt | 37 + rs274ngc.var | 119 +++ test-all-native.sh | 14 + test-linuxcnc-api-native.sh | 42 ++ test-linuxcnc-bridge-native.sh | 28 + test-linuxcnc-rs274-native.sh | 108 +++ test-linuxcnc-source-link.sh | 157 ++++ test-linuxcnc-source-objects.sh | 49 ++ test-linuxcnc-source-syntax.sh | 48 ++ test-native.sh | 57 ++ tests/gcode/basic_mill.ngc | 13 + tests/gcode/incremental_and_r_arc.ngc | 9 + tests/gcode/linuxcnc_basic_motion.ngc | 12 + tests/gcode/linuxcnc_canned_cycle.ngc | 5 + tests/gcode/linuxcnc_coordinate_offsets.ngc | 5 + tests/gcode/linuxcnc_rtcp_controls.ngc | 5 + web/index.html | 91 +++ web/package.json | 19 + web/src/app.js | 221 ++++++ web/src/index.ts | 62 ++ web/src/wasm-core.d.ts | 8 + web/src/wasm-core.js | 148 ++++ web/styles.css | 279 +++++++ 54 files changed, 5523 insertions(+) create mode 100644 .gitignore create mode 100644 README.zh-CN.md create mode 100755 build-wasm.sh create mode 100644 core/CMakeLists.txt create mode 100644 core/include/cnc_sim_api.h create mode 100644 core/src/canon_event_sink.cpp create mode 100644 core/src/canon_event_sink.h create mode 100644 core/src/cnc_sim_api.cpp create mode 100644 core/src/gcode_backend.cpp create mode 100644 core/src/gcode_backend.h create mode 100644 core/src/linuxcnc_canon_bridge.cpp create mode 100644 core/src/linuxcnc_canon_bridge.h create mode 100644 core/src/linuxcnc_rs274_backend.cpp create mode 100644 core/src/linuxcnc_rs274_backend.h create mode 100644 core/src/rtcp_kinematics.cpp create mode 100644 core/src/rtcp_kinematics.h create mode 100644 core/src/simulator_gcode_controls.cpp create mode 100644 core/src/simulator_gcode_controls.h create mode 100644 core/src/smoke_gcode_parser.cpp create mode 100644 core/src/smoke_gcode_parser.h create mode 100644 core/tests/canon_event_sink_smoke.cpp create mode 100644 core/tests/cnc_sim_api_linuxcnc_rs274_smoke.cpp create mode 100644 core/tests/cnc_sim_api_smoke.cpp create mode 100644 core/tests/linuxcnc_canon_bridge_smoke.cpp create mode 100644 core/tests/rtcp_kinematics_smoke.cpp create mode 100644 core/tests/simulator_gcode_controls_smoke.cpp create mode 100644 core/tools/cnc_sim_dump.cpp create mode 100644 core/tools/linuxcnc_rs274_dump.cpp create mode 100644 docs/architecture.md create mode 100644 docs/linuxcnc-porting.md create mode 100644 docs/linuxcnc-rs274-source-map.md create mode 100644 linuxcnc-rs274-source-files.txt create mode 100644 rs274ngc.var create mode 100755 test-all-native.sh create mode 100755 test-linuxcnc-api-native.sh create mode 100755 test-linuxcnc-bridge-native.sh create mode 100755 test-linuxcnc-rs274-native.sh create mode 100755 test-linuxcnc-source-link.sh create mode 100755 test-linuxcnc-source-objects.sh create mode 100755 test-linuxcnc-source-syntax.sh create mode 100755 test-native.sh create mode 100644 tests/gcode/basic_mill.ngc create mode 100644 tests/gcode/incremental_and_r_arc.ngc create mode 100644 tests/gcode/linuxcnc_basic_motion.ngc create mode 100644 tests/gcode/linuxcnc_canned_cycle.ngc create mode 100644 tests/gcode/linuxcnc_coordinate_offsets.ngc create mode 100644 tests/gcode/linuxcnc_rtcp_controls.ngc create mode 100644 web/index.html create mode 100644 web/package.json create mode 100644 web/src/app.js create mode 100644 web/src/index.ts create mode 100644 web/src/wasm-core.d.ts create mode 100644 web/src/wasm-core.js create mode 100644 web/styles.css diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ad432cc --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +build/ +dist/ +node_modules/ +*.o +*.a +*.so +*.wasm +*.js.map +*.log +.DS_Store diff --git a/README.zh-CN.md b/README.zh-CN.md new file mode 100644 index 0000000..c7aa65b --- /dev/null +++ b/README.zh-CN.md @@ -0,0 +1,163 @@ +# LinuxCNC Web/WASM CNC Simulator + +本目录是把 LinuxCNC 面向浏览器仿真的改写入口。目标不是把完整 LinuxCNC 直接搬进浏览器,而是复用可移植的解释器、运动学和刀路算法,替换实时 HAL/NML/GUI 层,输出适合 Web Worker、Three.js/WebGPU 和后续碰撞/去料仿真的事件流。 + +## 关键判断 + +- 不能按“整套 LinuxCNC 编译为 wasm”做。LinuxCNC 包含实时线程、HAL、NML、动态模块、Python/Boost.Python remap、桌面 GUI 和 POSIX 依赖,这些都不是浏览器仿真核心。 +- 可以先复用 `src/emc/rs274ngc` 解释器,接管 `canon.hh` 中的 Canonical Machining Functions,把 `STRAIGHT_FEED`、`ARC_FEED`、`DWELL`、`CHANGE_TOOL` 等调用记录为仿真事件。 +- Fanuc、Siemens 等主流系统应作为“方言层”实现:先把宏变量、固定循环、坐标旋转、RTCP/TRAORI/CYCLE800 等控制器特性归一化为内部 IR,再交给同一个仿真内核。 +- 浏览器端不做实时控制,只做可重复、可暂停、可回放的离线仿真,所以速度规划、前瞻、碰撞和去料可以使用非实时算法。 + +## 分层 + +1. `dialect` + - LinuxCNC RS274/NGC + - Fanuc Macro B / common milling and turning extensions + - Siemens 840D-style cycles and transforms + +2. `interp` + - LinuxCNC interpreter compiled to wasm where possible + - Canon callback adapter records normalized motion/tool/state events + +3. `sim-core` + - Units, modal state, work offsets, tool table + - Arc/NURBS segmentation + - 3/4/5-axis kinematics and RTCP + - Feed/time estimation and non-realtime lookahead + +4. `verification` + - Golden G-code corpus + - Controller dialect compatibility matrix + - Numeric tolerance checks against LinuxCNC native output + +5. `web` + - Web Worker wrapper around wasm + - Toolpath display + - Machine model, fixtures, stock, collision and material removal + +## First milestone + +The first useful target is: + +- compile a small wasm module exposing `cnc_sim_*` C ABI; +- connect LinuxCNC RS274 interpreter to a Canon event sink; +- return a JSON event stream to TypeScript; +- render rapid/feed/arc events in the browser; +- compare the event stream against native LinuxCNC for a small test corpus. + +## 当前进度 + +- 已有稳定 C ABI 和 Web 调用封装。 +- 已有数控系统风格网页面板。 +- 已有临时 smoke parser,用于在 LinuxCNC 后端接入前测试 UI 和 wasm 回调链路。 +- 已有 native smoke test 和 JSON dump 工具。 +- 已有 LinuxCNC `canon.hh` bridge,并已通过独立 smoke test。 +- 已有 native `librs274` runner,可以让 LinuxCNC 解释器输出 `CncSimEvent`。 +- 已有最小 LinuxCNC tooldata 初始化,native runner 和 API 后端都能覆盖 `T... M6` 换刀路径。 +- 已有 LinuxCNC RS274 源码级链接 smoke:本项目直接编译 23 个解释器 core 源文件,不再通过 `librs274` 取得解释器主体。 +- 已有五轴 RTCP 第一版几何内核,可根据编程刀尖点、A/B/C 姿态和刀长计算枢轴/主轴点补偿位置。 +- 已有坐标系事件回归语料,覆盖 `G10 L2` 的 G5X/XY 旋转事件和 `G92.1` 清零事件。 +- 下一步是继续替换源码级链接中残留的 Python、INI/HAL、文件系统和动态加载依赖,并把 RTCP 内核接入 canon 事件流和机型配置。 + +## 本地测试 + +```bash +./test-all-native.sh +``` + +当前 native 测试覆盖: + +- smoke parser +- Canon event sink +- LinuxCNC Canon bridge +- 直接链接 LinuxCNC `librs274` 的 runner +- 通过 `cnc_sim_api` 选择 `linuxcnc-rs274` 后端 +- LinuxCNC RS274 源文件语法探针和对象编译探针 +- LinuxCNC RS274 源码级链接 smoke +- 五轴 RTCP 几何内核 smoke +- `G10 L2` / `G92.1` 坐标系事件回归 + +## 后端选择 + +公共 API 使用同一个入口解析程序,后端通过 JSON 配置选择: + +```json +{"backend":"smoke"} +``` + +`smoke` 是默认后端,用于 wasm 和 UI 联调。 + +native 构建启用 LinuxCNC 后端后,可以使用: + +```json +{"backend":"linuxcnc-rs274"} +``` + +网页面板右上角的 `BACKEND` 下拉框会把该配置传给 wasm/core。当前默认 wasm 构建不包含 LinuxCNC 后端,选择 `LINUXCNC` 会返回明确错误;native API 测试已验证该后端可用。 + +## RTCP 配置 + +RTCP 默认关闭。开启后,原有 `rapid`、`linear-feed`、`arc-feed` 事件仍表示编程刀尖轨迹;core 会额外发出 `rtcp-pivot` 事件,`start`/`end` 是按刀长和 A/B/C 姿态补偿后的枢轴/主轴点轨迹,`center` 的 `x/y/z` 暂存当前刀具向量。 + +```json +{ + "backend": "linuxcnc-rs274", + "rtcp": { + "enabled": true, + "toolLength": 100, + "toolLengths": { + "7": 125 + } + } +} +``` + +当前 RTCP 内核是第一版头头/摆头类几何模型:编程 XYZ 视为刀尖点,刀长沿本地 `-Z`,A/B/C 按 `Rz(C) * Ry(B) * Rx(A)` 旋转。后续会继续加入机型拓扑、旋转中心偏置、刀表刀长和不同五轴构型。 + +RTCP G 码控制已开始标准化: + +- `G43.4 H...`:开启 RTCP,`H` 号进入事件,实际刀长优先从 JSON 配置中的 `toolLengths` 按 H 号读取,未配置时回退到 `toolLength`。 +- `G43.5 H...`:同样作为 RTCP 开启处理。 +- `G49`:关闭 RTCP。 + +解析后会发出 `rtcp-state` 事件。事件中的 `feed` 表示 RTCP 状态:`0=关闭`、`1=开启`;`tool` 表示 H 号;`dwell_seconds` 暂存当前配置刀长。 + +## 运动学切换 M 码 + +core 已对原型中的运动学切换 M 码做标准化解析: + +- `M429`:切换到三轴恒等运动学 `IDENTITY`,关闭 RTCP。 +- `M428`:恢复到原始五轴运动学,开启 RTCP。 +- `M430`:切换到 `FIVEAXIS_BC`,开启 RTCP。 + +解析后会发出 `kinematics-switch` 事件。事件中的 `reserved` 字段表示运动学类型:`0=IDENTITY`、`1=ORIGINAL`、`2=FIVEAXIS_BC`;`feed` 字段临时表示 RTCP 状态:`0=关闭`、`1=开启`。同一行中的 `M428 M429 M430` 会按出现顺序产生三个切换事件。 + +`linuxcnc-rs274` native API backend 会在送入 LinuxCNC 解释器前拦截只包含这些切换 M 码的行,避免 LinuxCNC 去查找外部用户 M-code 文件。 + +## 坐标系事件 + +Canon bridge 已不再丢弃坐标系相关回调,会输出: + +- `set-g5x-offset`:来自 `SET_G5X_OFFSET`,`tool` 字段存 G5X index,`start` 存 XYZABCUVW offset。 +- `set-g92-offset`:来自 `SET_G92_OFFSET`,`start` 存 XYZABCUVW offset。 +- `set-xy-rotation`:来自 `SET_XY_ROTATION`,`feed` 暂存旋转角度。 + +这些事件先保证 LinuxCNC Canon 层信息完整进入 Web/core。具体 G-code(如 `G10`、`G92`、坐标旋转)是否触发这些回调,还要继续按 LinuxCNC 解释器参数路径逐项补回归语料。 +目前已有第一组回归语料 `tests/gcode/linuxcnc_coordinate_offsets.ngc`,覆盖 `G10 L2 P1 X... Y... Z... R...` 触发 `set-g5x-offset` / `set-xy-rotation`,以及 `G92.1` 触发 `set-g92-offset` 清零。后续继续扩展到 `G10 L20`、多坐标系 P 号、持久参数文件和旋转叠加运动路径。 + +## CMake native LinuxCNC 后端 + +手写测试脚本仍是当前主验证路径。如果使用 CMake,启用 native LinuxCNC 后端的配置为: + +```bash +cmake -S core -B build/native-linuxcnc \ + -DCNC_SIM_ENABLE_LINUXCNC_RS274_BACKEND=ON \ + -DCNC_SIM_LINUXCNC_ROOT=/home/cnc/桌面/cnc/linuxcnc +``` + +该选项是 native-only,会链接 LinuxCNC 已构建的 `librs274` 和 `libtooldata`。 + +## Legal note + +LinuxCNC is GPL licensed. If this wasm module links LinuxCNC code, the combined simulator core must be distributed under GPL-compatible terms unless you replace that part with an independently written interpreter. diff --git a/build-wasm.sh b/build-wasm.sh new file mode 100755 index 0000000..4e56ffd --- /dev/null +++ b/build-wasm.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")" + +if ! command -v emcmake >/dev/null 2>&1; then + echo "Emscripten is required. Install/activate emsdk so emcmake and emcc are in PATH." >&2 + exit 1 +fi + +emcmake cmake -S core -B build/wasm -DCMAKE_BUILD_TYPE=Release +cmake --build build/wasm + +mkdir -p web/public +cp build/wasm/cnc_sim.js web/public/ +cp build/wasm/cnc_sim.wasm web/public/ diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt new file mode 100644 index 0000000..d3a1f91 --- /dev/null +++ b/core/CMakeLists.txt @@ -0,0 +1,174 @@ +cmake_minimum_required(VERSION 3.20) +project(cnc_sim_wasm_core LANGUAGES CXX) + +option(CNC_SIM_ENABLE_LINUXCNC_BRIDGE "Build the experimental LinuxCNC canon bridge" OFF) +option(CNC_SIM_ENABLE_LINUXCNC_RS274_BACKEND "Build the native LinuxCNC librs274 backend" OFF) +set(CNC_SIM_LINUXCNC_ROOT "" CACHE PATH "LinuxCNC source root for the experimental bridge") + +set(cnc_sim_core_sources + src/canon_event_sink.cpp + src/cnc_sim_api.cpp + src/gcode_backend.cpp + src/rtcp_kinematics.cpp + src/simulator_gcode_controls.cpp + src/smoke_gcode_parser.cpp +) + +if(CNC_SIM_ENABLE_LINUXCNC_RS274_BACKEND) + if(EMSCRIPTEN) + message(FATAL_ERROR "CNC_SIM_ENABLE_LINUXCNC_RS274_BACKEND is native-only; it links LinuxCNC librs274") + endif() + if(NOT CNC_SIM_LINUXCNC_ROOT) + message(FATAL_ERROR "Set CNC_SIM_LINUXCNC_ROOT to the LinuxCNC source root") + endif() + list(APPEND cnc_sim_core_sources + src/linuxcnc_canon_bridge.cpp + src/linuxcnc_rs274_backend.cpp + ) +endif() + +add_library(cnc_sim_objects OBJECT + ${cnc_sim_core_sources} +) + +target_include_directories(cnc_sim_objects + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/include + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src +) + +target_compile_features(cnc_sim_objects PUBLIC cxx_std_17) + +if(CNC_SIM_ENABLE_LINUXCNC_RS274_BACKEND) + find_package(Python3 REQUIRED COMPONENTS Development) + target_compile_definitions(cnc_sim_objects + PRIVATE + CNC_SIM_ENABLE_LINUXCNC_RS274_BACKEND + ) + target_include_directories(cnc_sim_objects + PRIVATE + ${Python3_INCLUDE_DIRS} + ${CNC_SIM_LINUXCNC_ROOT}/src + ${CNC_SIM_LINUXCNC_ROOT}/src/emc + ${CNC_SIM_LINUXCNC_ROOT}/src/emc/nml_intf + ${CNC_SIM_LINUXCNC_ROOT}/src/emc/rs274ngc + ${CNC_SIM_LINUXCNC_ROOT}/src/emc/motion + ${CNC_SIM_LINUXCNC_ROOT}/include + ) +endif() + +add_library(cnc_sim_core STATIC + $ +) + +target_include_directories(cnc_sim_core + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/include + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src +) + +if(CNC_SIM_ENABLE_LINUXCNC_RS274_BACKEND) + target_link_directories(cnc_sim_core + PUBLIC + ${CNC_SIM_LINUXCNC_ROOT}/lib + ) + target_link_libraries(cnc_sim_core + PUBLIC + rs274 + tooldata + Python3::Python + ) + target_link_options(cnc_sim_core + PUBLIC + "-Wl,-rpath,${CNC_SIM_LINUXCNC_ROOT}/lib" + ) +endif() + +if(NOT EMSCRIPTEN) + add_executable(cnc_sim_api_smoke + tests/cnc_sim_api_smoke.cpp + ) + target_link_libraries(cnc_sim_api_smoke PRIVATE cnc_sim_core) + target_include_directories(cnc_sim_api_smoke PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) + + add_executable(canon_event_sink_smoke + tests/canon_event_sink_smoke.cpp + ) + target_link_libraries(canon_event_sink_smoke PRIVATE cnc_sim_core) + target_include_directories(canon_event_sink_smoke PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) + + add_executable(rtcp_kinematics_smoke + tests/rtcp_kinematics_smoke.cpp + ) + target_link_libraries(rtcp_kinematics_smoke PRIVATE cnc_sim_core) + target_include_directories(rtcp_kinematics_smoke PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) + + add_executable(simulator_gcode_controls_smoke + tests/simulator_gcode_controls_smoke.cpp + ) + target_link_libraries(simulator_gcode_controls_smoke PRIVATE cnc_sim_core) + target_include_directories(simulator_gcode_controls_smoke PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) + + add_executable(cnc_sim_dump + tools/cnc_sim_dump.cpp + ) + target_link_libraries(cnc_sim_dump PRIVATE cnc_sim_core) + target_include_directories(cnc_sim_dump PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) + + if(CNC_SIM_ENABLE_LINUXCNC_RS274_BACKEND) + add_executable(cnc_sim_api_linuxcnc_rs274_smoke + tests/cnc_sim_api_linuxcnc_rs274_smoke.cpp + ) + target_link_libraries(cnc_sim_api_linuxcnc_rs274_smoke PRIVATE cnc_sim_core) + target_include_directories(cnc_sim_api_linuxcnc_rs274_smoke PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) + endif() +endif() + +if(CNC_SIM_ENABLE_LINUXCNC_BRIDGE) + if(NOT CNC_SIM_LINUXCNC_ROOT) + message(FATAL_ERROR "Set CNC_SIM_LINUXCNC_ROOT to the LinuxCNC source root") + endif() + add_library(cnc_sim_linuxcnc_canon_bridge STATIC + src/linuxcnc_canon_bridge.cpp + ) + target_include_directories(cnc_sim_linuxcnc_canon_bridge + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/include + ${CMAKE_CURRENT_SOURCE_DIR}/src + ${CNC_SIM_LINUXCNC_ROOT}/src/emc + ${CNC_SIM_LINUXCNC_ROOT}/src/emc/nml_intf + ${CNC_SIM_LINUXCNC_ROOT}/src/emc/rs274ngc + ${CNC_SIM_LINUXCNC_ROOT}/src + ${CNC_SIM_LINUXCNC_ROOT}/include + ) + target_link_libraries(cnc_sim_linuxcnc_canon_bridge PRIVATE cnc_sim_core) +endif() + +if(EMSCRIPTEN) + add_executable(cnc_sim_wasm + $ + ) + + target_include_directories(cnc_sim_wasm + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/include + ${CMAKE_CURRENT_SOURCE_DIR}/src + ) + + set_target_properties(cnc_sim_wasm PROPERTIES + OUTPUT_NAME "cnc_sim" + SUFFIX ".js" + ) + + target_link_options(cnc_sim_wasm PRIVATE + "--no-entry" + "-sMODULARIZE=1" + "-sEXPORT_NAME=createCncSimModule" + "-sALLOW_MEMORY_GROWTH=1" + "-sALLOW_TABLE_GROWTH=1" + "-sEXPORTED_FUNCTIONS=['_malloc','_free','_cnc_sim_create','_cnc_sim_destroy','_cnc_sim_reset','_cnc_sim_set_dialect','_cnc_sim_set_event_callback','_cnc_sim_load_config_json','_cnc_sim_parse_program','_cnc_sim_last_error']" + "-sEXPORTED_RUNTIME_METHODS=['ccall','cwrap','UTF8ToString','stringToUTF8','lengthBytesUTF8','addFunction','removeFunction']" + ) +endif() diff --git a/core/include/cnc_sim_api.h b/core/include/cnc_sim_api.h new file mode 100644 index 0000000..30386b7 --- /dev/null +++ b/core/include/cnc_sim_api.h @@ -0,0 +1,88 @@ +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#if defined(_WIN32) +#define CNC_SIM_EXPORT __declspec(dllexport) +#else +#define CNC_SIM_EXPORT __attribute__((visibility("default"))) +#endif + +typedef struct CncSimHandle CncSimHandle; + +typedef enum CncSimDialect { + CNC_SIM_DIALECT_LINUXCNC = 0, + CNC_SIM_DIALECT_FANUC = 1, + CNC_SIM_DIALECT_SIEMENS = 2 +} CncSimDialect; + +typedef enum CncSimEventType { + CNC_SIM_EVENT_NONE = 0, + CNC_SIM_EVENT_ERROR = 1, + CNC_SIM_EVENT_COMMENT = 2, + CNC_SIM_EVENT_SET_UNITS = 3, + CNC_SIM_EVENT_SET_PLANE = 4, + CNC_SIM_EVENT_SET_FEED = 5, + CNC_SIM_EVENT_SET_SPINDLE = 6, + CNC_SIM_EVENT_TOOL_CHANGE = 7, + CNC_SIM_EVENT_DWELL = 8, + CNC_SIM_EVENT_RAPID = 9, + CNC_SIM_EVENT_LINEAR_FEED = 10, + CNC_SIM_EVENT_ARC_FEED = 11, + CNC_SIM_EVENT_PROBE = 12, + CNC_SIM_EVENT_PROGRAM_END = 13, + CNC_SIM_EVENT_RTCP_PIVOT = 14, + CNC_SIM_EVENT_KINEMATICS_SWITCH = 15, + CNC_SIM_EVENT_RTCP_STATE = 16, + CNC_SIM_EVENT_SET_G5X_OFFSET = 17, + CNC_SIM_EVENT_SET_G92_OFFSET = 18, + CNC_SIM_EVENT_SET_XY_ROTATION = 19 +} CncSimEventType; + +typedef struct CncSimPose { + double x; + double y; + double z; + double a; + double b; + double c; + double u; + double v; + double w; +} CncSimPose; + +typedef struct CncSimEvent { + uint32_t version; + CncSimEventType type; + int32_t line; + int32_t plane; + int32_t tool; + double feed; + double spindle; + double dwell_seconds; + CncSimPose start; + CncSimPose end; + CncSimPose center; + int32_t arc_turns; + int32_t reserved; +} CncSimEvent; + +typedef int (*CncSimEventCallback)(const CncSimEvent *event, void *user_data); + +CNC_SIM_EXPORT CncSimHandle *cnc_sim_create(void); +CNC_SIM_EXPORT void cnc_sim_destroy(CncSimHandle *handle); +CNC_SIM_EXPORT void cnc_sim_reset(CncSimHandle *handle); +CNC_SIM_EXPORT int cnc_sim_set_dialect(CncSimHandle *handle, CncSimDialect dialect); +CNC_SIM_EXPORT int cnc_sim_set_event_callback(CncSimHandle *handle, CncSimEventCallback callback, void *user_data); +CNC_SIM_EXPORT int cnc_sim_load_config_json(CncSimHandle *handle, const char *json, size_t json_len); +CNC_SIM_EXPORT int cnc_sim_parse_program(CncSimHandle *handle, const char *program, size_t program_len); +CNC_SIM_EXPORT const char *cnc_sim_last_error(CncSimHandle *handle); + +#ifdef __cplusplus +} +#endif diff --git a/core/src/canon_event_sink.cpp b/core/src/canon_event_sink.cpp new file mode 100644 index 0000000..1da1f04 --- /dev/null +++ b/core/src/canon_event_sink.cpp @@ -0,0 +1,242 @@ +#include "canon_event_sink.h" + +#include "rtcp_kinematics.h" + +void CanonEventSink::reset() { + position_ = {}; + plane_ = 17; + unit_scale_ = 1.0; + feed_ = 0.0; + spindle_ = 0.0; + selected_tool_ = 0; + callback_status_ = 0; + kinematics_type_ = 1; +} + +void CanonEventSink::set_callback(CncSimEventCallback callback, void *user_data) { + callback_ = callback; + user_data_ = user_data; + callback_status_ = 0; +} + +void CanonEventSink::clear_callback_status() { + callback_status_ = 0; +} + +bool CanonEventSink::callback_aborted() const { + return callback_status_ != 0; +} + +void CanonEventSink::configure_rtcp(bool enabled, double tool_length) { + rtcp_enabled_ = enabled; + default_rtcp_tool_length_ = tool_length; + rtcp_tool_length_ = tool_length; +} + +void CanonEventSink::set_tool_length(int h_code, double tool_length) { + if (h_code <= 0) { + default_rtcp_tool_length_ = tool_length; + if (rtcp_h_code_ == 0) { + rtcp_tool_length_ = tool_length; + } + return; + } + tool_lengths_[h_code] = tool_length; + if (rtcp_h_code_ == h_code) { + rtcp_tool_length_ = tool_length; + } +} + +void CanonEventSink::set_rtcp_state(bool enabled, int h_code, int line) { + rtcp_enabled_ = enabled; + rtcp_h_code_ = enabled ? h_code : 0; + if (enabled) { + const auto tool = tool_lengths_.find(h_code); + rtcp_tool_length_ = tool != tool_lengths_.end() ? tool->second : default_rtcp_tool_length_; + } + CncSimEvent event = base_event(CNC_SIM_EVENT_RTCP_STATE, line); + event.feed = rtcp_enabled_ ? 1.0 : 0.0; + event.tool = rtcp_h_code_; + event.dwell_seconds = rtcp_tool_length_; + emit(event); +} + +void CanonEventSink::switch_kinematics(int kinematics_type, bool rtcp_enabled, int line) { + kinematics_type_ = kinematics_type; + rtcp_enabled_ = rtcp_enabled; + CncSimEvent event = base_event(CNC_SIM_EVENT_KINEMATICS_SWITCH, line); + event.reserved = kinematics_type_; + event.feed = rtcp_enabled_ ? 1.0 : 0.0; + emit(event); +} + +void CanonEventSink::set_g5x_offset(int index, const CncSimPose &offset, int line) { + CncSimEvent event = base_event(CNC_SIM_EVENT_SET_G5X_OFFSET, line); + event.tool = index; + event.start = offset; + emit(event); +} + +void CanonEventSink::set_g92_offset(const CncSimPose &offset, int line) { + CncSimEvent event = base_event(CNC_SIM_EVENT_SET_G92_OFFSET, line); + event.start = offset; + emit(event); +} + +void CanonEventSink::set_xy_rotation(double angle_degrees, int line) { + CncSimEvent event = base_event(CNC_SIM_EVENT_SET_XY_ROTATION, line); + event.feed = angle_degrees; + emit(event); +} + +void CanonEventSink::use_length_units(double scale, int line) { + unit_scale_ = scale; + CncSimEvent event = base_event(CNC_SIM_EVENT_SET_UNITS, line); + event.feed = scale; + emit(event); +} + +void CanonEventSink::select_plane(int plane, int line) { + plane_ = plane; + CncSimEvent event = base_event(CNC_SIM_EVENT_SET_PLANE, line); + event.plane = plane_; + emit(event); +} + +void CanonEventSink::set_feed_rate(double feed, int line) { + feed_ = feed; + CncSimEvent event = base_event(CNC_SIM_EVENT_SET_FEED, line); + event.feed = feed_; + emit(event); +} + +void CanonEventSink::set_spindle_speed(double spindle, int line) { + spindle_ = spindle; + CncSimEvent event = base_event(CNC_SIM_EVENT_SET_SPINDLE, line); + event.spindle = spindle_; + emit(event); +} + +void CanonEventSink::select_tool(int tool) { + selected_tool_ = tool; +} + +void CanonEventSink::change_tool(int line) { + CncSimEvent event = base_event(CNC_SIM_EVENT_TOOL_CHANGE, line); + event.tool = selected_tool_; + emit(event); +} + +void CanonEventSink::straight_traverse(int line, const CncSimPose &end) { + CncSimEvent event = base_event(CNC_SIM_EVENT_RAPID, line); + event.start = position_; + event.end = end; + emit(event); + emit_rtcp_pivot(line, position_, end); + position_ = end; +} + +void CanonEventSink::straight_feed(int line, const CncSimPose &end) { + CncSimEvent event = base_event(CNC_SIM_EVENT_LINEAR_FEED, line); + event.start = position_; + event.end = end; + emit(event); + emit_rtcp_pivot(line, position_, end); + position_ = end; +} + +void CanonEventSink::arc_feed(int line, const CncSimPose &end, const CncSimPose ¢er, int turns) { + CncSimEvent event = base_event(CNC_SIM_EVENT_ARC_FEED, line); + event.start = position_; + event.end = end; + event.center = center; + event.arc_turns = turns; + emit(event); + emit_rtcp_pivot(line, position_, end); + position_ = end; +} + +void CanonEventSink::dwell(double seconds, int line) { + CncSimEvent event = base_event(CNC_SIM_EVENT_DWELL, line); + event.dwell_seconds = seconds; + emit(event); +} + +void CanonEventSink::program_end(int line) { + emit(base_event(CNC_SIM_EVENT_PROGRAM_END, line)); +} + +void CanonEventSink::emit_raw(CncSimEvent event) { + if (event.version == 0) { + event.version = 1; + } + emit(event); +} + +const CncSimPose &CanonEventSink::position() const { + return position_; +} + +int CanonEventSink::plane() const { + return plane_; +} + +double CanonEventSink::unit_scale() const { + return unit_scale_; +} + +double CanonEventSink::feed_rate() const { + return feed_; +} + +double CanonEventSink::spindle_speed() const { + return spindle_; +} + +int CanonEventSink::selected_tool() const { + return selected_tool_; +} + +int CanonEventSink::kinematics_type() const { + return kinematics_type_; +} + +bool CanonEventSink::rtcp_enabled() const { + return rtcp_enabled_; +} + +double CanonEventSink::rtcp_tool_length() const { + return rtcp_tool_length_; +} + +void CanonEventSink::emit(CncSimEvent event) { + if (callback_ && callback_status_ == 0) { + callback_status_ = callback_(&event, user_data_); + } +} + +void CanonEventSink::emit_rtcp_pivot(int line, const CncSimPose &start, const CncSimPose &end) { + if (!rtcp_enabled_ || rtcp_tool_length_ == 0.0 || callback_status_ != 0) { + return; + } + CncSimEvent event = base_event(CNC_SIM_EVENT_RTCP_PIVOT, line); + event.start = rtcp_pivot_from_tool_tip(start, rtcp_tool_length_); + event.end = rtcp_pivot_from_tool_tip(end, rtcp_tool_length_); + const RtcpVector tool = rtcp_tool_vector_from_pose(end, rtcp_tool_length_); + event.center.x = tool.x; + event.center.y = tool.y; + event.center.z = tool.z; + emit(event); +} + +CncSimEvent CanonEventSink::base_event(CncSimEventType type, int line) const { + CncSimEvent event{}; + event.version = 1; + event.type = type; + event.line = line; + event.plane = plane_; + event.feed = feed_; + event.spindle = spindle_; + event.tool = selected_tool_; + return event; +} diff --git a/core/src/canon_event_sink.h b/core/src/canon_event_sink.h new file mode 100644 index 0000000..ef205c9 --- /dev/null +++ b/core/src/canon_event_sink.h @@ -0,0 +1,64 @@ +#pragma once + +#include "cnc_sim_api.h" + +#include + +class CanonEventSink { +public: + void reset(); + void set_callback(CncSimEventCallback callback, void *user_data); + void clear_callback_status(); + bool callback_aborted() const; + void configure_rtcp(bool enabled, double tool_length); + void set_tool_length(int h_code, double tool_length); + void set_rtcp_state(bool enabled, int h_code, int line); + void switch_kinematics(int kinematics_type, bool rtcp_enabled, int line); + void set_g5x_offset(int index, const CncSimPose &offset, int line); + void set_g92_offset(const CncSimPose &offset, int line); + void set_xy_rotation(double angle_degrees, int line); + + void use_length_units(double scale, int line); + void select_plane(int plane, int line); + void set_feed_rate(double feed, int line); + void set_spindle_speed(double spindle, int line); + void select_tool(int tool); + void change_tool(int line); + void straight_traverse(int line, const CncSimPose &end); + void straight_feed(int line, const CncSimPose &end); + void arc_feed(int line, const CncSimPose &end, const CncSimPose ¢er, int turns); + void dwell(double seconds, int line); + void program_end(int line); + void emit_raw(CncSimEvent event); + + const CncSimPose &position() const; + int plane() const; + double unit_scale() const; + double feed_rate() const; + double spindle_speed() const; + int selected_tool() const; + int kinematics_type() const; + bool rtcp_enabled() const; + double rtcp_tool_length() const; + +private: + void emit(CncSimEvent event); + void emit_rtcp_pivot(int line, const CncSimPose &start, const CncSimPose &end); + CncSimEvent base_event(CncSimEventType type, int line) const; + + CncSimEventCallback callback_ = nullptr; + void *user_data_ = nullptr; + int callback_status_ = 0; + CncSimPose position_{}; + int plane_ = 17; + double unit_scale_ = 1.0; + double feed_ = 0.0; + double spindle_ = 0.0; + int selected_tool_ = 0; + bool rtcp_enabled_ = false; + double default_rtcp_tool_length_ = 0.0; + double rtcp_tool_length_ = 0.0; + int rtcp_h_code_ = 0; + int kinematics_type_ = 1; + std::unordered_map tool_lengths_; +}; diff --git a/core/src/cnc_sim_api.cpp b/core/src/cnc_sim_api.cpp new file mode 100644 index 0000000..a9c9371 --- /dev/null +++ b/core/src/cnc_sim_api.cpp @@ -0,0 +1,235 @@ +#include "cnc_sim_api.h" +#include "canon_event_sink.h" +#include "gcode_backend.h" + +#include +#include +#include +#include + +struct CncSimHandle { + CncSimDialect dialect = CNC_SIM_DIALECT_LINUXCNC; + GcodeBackendKind backend = GcodeBackendKind::Smoke; + std::string last_error; + CanonEventSink sink; +}; + +namespace { + +void set_error(CncSimHandle *handle, const std::string &message) { + if (handle) { + handle->last_error = message; + } +} + +std::string config_text(const char *json, size_t json_len) { + if (!json || json_len == 0) { + return {}; + } + return std::string(json, json + json_len); +} + +std::string lower_ascii(std::string value) { + std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) { + return static_cast(std::tolower(ch)); + }); + return value; +} + +bool contains_backend_value(const std::string &json, const char *value) { + std::string compact; + compact.reserve(json.size()); + for (char ch : json) { + if (!std::isspace(static_cast(ch))) { + compact.push_back(static_cast(std::tolower(static_cast(ch)))); + } + } + const std::string needle = std::string("\"backend\":\"") + value + "\""; + return compact.find(needle) != std::string::npos; +} + +std::string compact_lower_json(const std::string &json) { + std::string compact; + compact.reserve(json.size()); + for (char ch : json) { + if (!std::isspace(static_cast(ch))) { + compact.push_back(static_cast(std::tolower(static_cast(ch)))); + } + } + return compact; +} + +bool contains_bool_value(const std::string &compact, const char *key, bool *value) { + const std::string true_needle = std::string("\"") + key + "\":true"; + if (compact.find(true_needle) != std::string::npos) { + *value = true; + return true; + } + const std::string false_needle = std::string("\"") + key + "\":false"; + if (compact.find(false_needle) != std::string::npos) { + *value = false; + return true; + } + return false; +} + +bool find_number_value(const std::string &compact, const char *key, double *value) { + const std::string needle = std::string("\"") + key + "\":"; + const size_t pos = compact.find(needle); + if (pos == std::string::npos) { + return false; + } + const char *start = compact.c_str() + pos + needle.size(); + char *end = nullptr; + const double parsed = std::strtod(start, &end); + if (end == start) { + return false; + } + *value = parsed; + return true; +} + +void apply_tool_length_table(CanonEventSink &sink, const std::string &compact) { + const std::string needle = "\"toollengths\":{"; + size_t pos = compact.find(needle); + if (pos == std::string::npos) { + return; + } + + pos += needle.size(); + while (pos < compact.size() && compact[pos] != '}') { + if (compact[pos] == ',') { + ++pos; + continue; + } + if (compact[pos] != '"') { + break; + } + ++pos; + char *h_end = nullptr; + const long h_code = std::strtol(compact.c_str() + pos, &h_end, 10); + if (h_end == compact.c_str() + pos || *h_end != '"') { + break; + } + pos = static_cast(h_end - compact.c_str()) + 1; + if (pos >= compact.size() || compact[pos] != ':') { + break; + } + ++pos; + char *length_end = nullptr; + const double tool_length = std::strtod(compact.c_str() + pos, &length_end); + if (length_end == compact.c_str() + pos) { + break; + } + if (h_code > 0) { + sink.set_tool_length(static_cast(h_code), tool_length); + } + pos = static_cast(length_end - compact.c_str()); + } +} + +int apply_config(CncSimHandle *handle, const std::string &json) { + if (json.empty()) { + return 0; + } + const std::string compact = compact_lower_json(json); + + if (contains_backend_value(json, "smoke")) { + handle->backend = GcodeBackendKind::Smoke; + } else if (contains_backend_value(json, "linuxcnc-rs274")) { + handle->backend = GcodeBackendKind::LinuxCncRs274; + } else if (compact.find("\"backend\"") != std::string::npos) { + set_error(handle, "unsupported backend in config"); + return -1; + } + + bool rtcp_enabled = false; + double tool_length = 0.0; + const bool has_rtcp_enabled = contains_bool_value(compact, "rtcp", &rtcp_enabled) || + contains_bool_value(compact, "enabled", &rtcp_enabled); + const bool has_tool_length = find_number_value(compact, "toollength", &tool_length) || + find_number_value(compact, "tool_length", &tool_length); + if (has_rtcp_enabled || has_tool_length) { + handle->sink.configure_rtcp(rtcp_enabled, tool_length); + } + apply_tool_length_table(handle->sink, compact); + return 0; +} + +} // namespace + +extern "C" { + +CncSimHandle *cnc_sim_create(void) { + return new CncSimHandle(); +} + +void cnc_sim_destroy(CncSimHandle *handle) { + delete handle; +} + +void cnc_sim_reset(CncSimHandle *handle) { + if (!handle) { + return; + } + handle->last_error.clear(); + handle->sink.reset(); +} + +int cnc_sim_set_dialect(CncSimHandle *handle, CncSimDialect dialect) { + if (!handle) { + return -1; + } + switch (dialect) { + case CNC_SIM_DIALECT_LINUXCNC: + case CNC_SIM_DIALECT_FANUC: + case CNC_SIM_DIALECT_SIEMENS: + handle->dialect = dialect; + return 0; + default: + set_error(handle, "unsupported dialect"); + return -1; + } +} + +int cnc_sim_set_event_callback(CncSimHandle *handle, CncSimEventCallback callback, void *user_data) { + if (!handle) { + return -1; + } + handle->sink.set_callback(callback, user_data); + return 0; +} + +int cnc_sim_load_config_json(CncSimHandle *handle, const char *json, size_t json_len) { + if (!handle) { + return -1; + } + if (!json && json_len != 0) { + set_error(handle, "null config buffer"); + return -1; + } + handle->last_error.clear(); + return apply_config(handle, config_text(json, json_len)); +} + +int cnc_sim_parse_program(CncSimHandle *handle, const char *program, size_t program_len) { + if (!handle) { + return -1; + } + + handle->last_error.clear(); + const int rc = parse_gcode_with_backend(handle->backend, handle->sink, program, program_len, &handle->last_error); + if (rc != 0 && handle->last_error.empty()) { + handle->last_error = "program parse failed"; + } + return rc; +} + +const char *cnc_sim_last_error(CncSimHandle *handle) { + if (!handle) { + return "null simulator handle"; + } + return handle->last_error.c_str(); +} + +} // extern "C" diff --git a/core/src/gcode_backend.cpp b/core/src/gcode_backend.cpp new file mode 100644 index 0000000..7c4fc44 --- /dev/null +++ b/core/src/gcode_backend.cpp @@ -0,0 +1,44 @@ +#include "gcode_backend.h" + +#ifdef CNC_SIM_ENABLE_LINUXCNC_RS274_BACKEND +#include "linuxcnc_rs274_backend.h" +#endif +#include "smoke_gcode_parser.h" + +const char *gcode_backend_name(GcodeBackendKind backend) { + switch (backend) { + case GcodeBackendKind::Smoke: + return "smoke"; + case GcodeBackendKind::LinuxCncRs274: + return "linuxcnc-rs274"; + default: + return "unknown"; + } +} + +int parse_gcode_with_backend(GcodeBackendKind backend, + CanonEventSink &sink, + const char *program, + size_t program_len, + std::string *error) { + switch (backend) { + case GcodeBackendKind::Smoke: { + SmokeGcodeParser parser(sink); + return parser.parse(program, program_len, error); + } + case GcodeBackendKind::LinuxCncRs274: +#ifdef CNC_SIM_ENABLE_LINUXCNC_RS274_BACKEND + return parse_linuxcnc_rs274_backend(sink, program, program_len, error); +#else + if (error) { + *error = "linuxcnc-rs274 backend is not compiled into this build"; + } + return -1; +#endif + default: + if (error) { + *error = "unknown G-code backend"; + } + return -1; + } +} diff --git a/core/src/gcode_backend.h b/core/src/gcode_backend.h new file mode 100644 index 0000000..09d2dd9 --- /dev/null +++ b/core/src/gcode_backend.h @@ -0,0 +1,20 @@ +#pragma once + +#include "canon_event_sink.h" + +#include +#include + +enum class GcodeBackendKind { + Smoke, + LinuxCncRs274, +}; + +const char *gcode_backend_name(GcodeBackendKind backend); + +int parse_gcode_with_backend(GcodeBackendKind backend, + CanonEventSink &sink, + const char *program, + size_t program_len, + std::string *error); + diff --git a/core/src/linuxcnc_canon_bridge.cpp b/core/src/linuxcnc_canon_bridge.cpp new file mode 100644 index 0000000..f5369c3 --- /dev/null +++ b/core/src/linuxcnc_canon_bridge.cpp @@ -0,0 +1,703 @@ +#include "linuxcnc_canon_bridge.h" + +#include "canon_event_sink.h" + +#include "canon.hh" + +#include +#include +#include +#include +#include +#include + +namespace { + +CanonEventSink *active_sink = nullptr; +std::string parameter_file_name = "rs274ngc.var"; + +CncSimPose make_pose(double x, double y, double z, + double a, double b, double c, + double u, double v, double w) { + CncSimPose pose{}; + pose.x = x; + pose.y = y; + pose.z = z; + pose.a = a; + pose.b = b; + pose.c = c; + pose.u = u; + pose.v = v; + pose.w = w; + return pose; +} + +int plane_to_g_code(CANON_PLANE plane) { + switch (plane) { + case CANON_PLANE::XY: + return 17; + case CANON_PLANE::XZ: + return 18; + case CANON_PLANE::YZ: + return 19; + default: + return 17; + } +} + +double units_to_scale(CANON_UNITS units) { + switch (units) { + case CANON_UNITS_INCHES: + return 25.4; + case CANON_UNITS_CM: + return 10.0; + case CANON_UNITS_MM: + default: + return 1.0; + } +} + +CANON_PLANE g_code_to_plane(int plane) { + switch (plane) { + case 18: + return CANON_PLANE::XZ; + case 19: + return CANON_PLANE::YZ; + case 17: + default: + return CANON_PLANE::XY; + } +} + +const CncSimPose ¤t_position() { + static CncSimPose zero{}; + return active_sink ? active_sink->position() : zero; +} + +void trace_call(const char *name) { + if (std::getenv("CNC_SIM_TRACE_CANON")) { + std::fprintf(stderr, "canon:%s\n", name); + std::fflush(stderr); + } +} + +} // namespace + +void cnc_sim_linuxcnc_set_canon_sink(CanonEventSink *sink) { + trace_call("cnc_sim_linuxcnc_set_canon_sink"); + active_sink = sink; +} + +CanonEventSink *cnc_sim_linuxcnc_get_canon_sink() { + return active_sink; +} + +void INIT_CANON() { + trace_call("INIT_CANON"); + if (active_sink) { + active_sink->reset(); + } +} + +void USE_LENGTH_UNITS(CANON_UNITS units) { + trace_call("USE_LENGTH_UNITS"); + if (active_sink) { + active_sink->use_length_units(units_to_scale(units), 0); + } +} + +void SELECT_PLANE(CANON_PLANE plane) { + trace_call("SELECT_PLANE"); + if (active_sink) { + active_sink->select_plane(plane_to_g_code(plane), 0); + } +} + +void SET_FEED_RATE(double rate) { + trace_call("SET_FEED_RATE"); + if (active_sink) { + active_sink->set_feed_rate(rate, 0); + } +} + +void SET_SPINDLE_SPEED(int, double speed) { + trace_call("SET_SPINDLE_SPEED"); + if (active_sink) { + active_sink->set_spindle_speed(speed, 0); + } +} + +void SELECT_TOOL(int tool) { + trace_call("SELECT_TOOL"); + if (active_sink) { + active_sink->select_tool(tool); + } +} + +void CHANGE_TOOL() { + trace_call("CHANGE_TOOL"); + if (active_sink) { + active_sink->change_tool(0); + } +} + +void STRAIGHT_TRAVERSE(int lineno, + double x, double y, double z, + double a, double b, double c, + double u, double v, double w) { + trace_call("STRAIGHT_TRAVERSE"); + if (active_sink) { + active_sink->straight_traverse(lineno, make_pose(x, y, z, a, b, c, u, v, w)); + } +} + +void STRAIGHT_FEED(int lineno, + double x, double y, double z, + double a, double b, double c, + double u, double v, double w) { + trace_call("STRAIGHT_FEED"); + if (active_sink) { + active_sink->straight_feed(lineno, make_pose(x, y, z, a, b, c, u, v, w)); + } +} + +void ARC_FEED(int lineno, + double first_end, double second_end, + double first_axis, double second_axis, int rotation, + double axis_end_point, + double a, double b, double c, + double u, double v, double w) { + trace_call("ARC_FEED"); + if (!active_sink) { + return; + } + + CncSimPose end = active_sink->position(); + CncSimPose center = active_sink->position(); + switch (active_sink->plane()) { + case 17: + end.x = first_end; + end.y = second_end; + end.z = axis_end_point; + center.x = first_axis; + center.y = second_axis; + break; + case 18: + end.z = first_end; + end.x = second_end; + end.y = axis_end_point; + center.z = first_axis; + center.x = second_axis; + break; + case 19: + end.y = first_end; + end.z = second_end; + end.x = axis_end_point; + center.y = first_axis; + center.z = second_axis; + break; + default: + break; + } + end.a = a; + end.b = b; + end.c = c; + end.u = u; + end.v = v; + end.w = w; + active_sink->arc_feed(lineno, end, center, rotation); +} + +void DWELL(double seconds) { + trace_call("DWELL"); + if (active_sink) { + active_sink->dwell(seconds, 0); + } +} + +void PROGRAM_END() { + trace_call("PROGRAM_END"); + if (active_sink) { + active_sink->program_end(0); + } +} + +void FINISH(void) { + trace_call("FINISH"); +} + +void SET_G5X_OFFSET(int index, + double x, double y, double z, + double a, double b, double c, + double u, double v, double w) { + trace_call("SET_G5X_OFFSET"); + if (active_sink) { + active_sink->set_g5x_offset(index, make_pose(x, y, z, a, b, c, u, v, w), 0); + } +} + +void SET_G92_OFFSET(double x, double y, double z, + double a, double b, double c, + double u, double v, double w) { + trace_call("SET_G92_OFFSET"); + if (active_sink) { + active_sink->set_g92_offset(make_pose(x, y, z, a, b, c, u, v, w), 0); + } +} + +void SET_XY_ROTATION(double angle) { + trace_call("SET_XY_ROTATION"); + if (active_sink) { + active_sink->set_xy_rotation(angle, 0); + } +} + +void CANON_UPDATE_END_POINT(double x, double y, double z, + double a, double b, double c, + double u, double v, double w) { + trace_call("CANON_UPDATE_END_POINT"); + if (active_sink) { + active_sink->straight_traverse(0, make_pose(x, y, z, a, b, c, u, v, w)); + } +} + +void SET_TRAVERSE_RATE(double) { +} + +void SET_FEED_REFERENCE(CANON_FEED_REFERENCE) { +} + +void SET_FEED_MODE(int, int) { +} + +void SET_MOTION_CONTROL_MODE(CANON_MOTION_MODE, double) { +} + +void SET_NAIVECAM_TOLERANCE(double) { +} + +void SET_CUTTER_RADIUS_COMPENSATION(double) { +} + +void START_CUTTER_RADIUS_COMPENSATION(int) { +} + +void STOP_CUTTER_RADIUS_COMPENSATION() { +} + +void START_SPEED_FEED_SYNCH(int, double, bool) { +} + +void STOP_SPEED_FEED_SYNCH() { +} + +void NURBS_G5_FEED(int lineno, const std::vector &points, unsigned int, CANON_PLANE) { + if (!active_sink) { + return; + } + for (const auto &point : points) { + CncSimPose end = active_sink->position(); + end.x = point.NURBS_X; + end.y = point.NURBS_Y; + active_sink->straight_feed(lineno, end); + } +} + +void NURBS_G6_FEED(int lineno, const std::vector &points, unsigned int, double, int, CANON_PLANE) { + if (!active_sink) { + return; + } + for (const auto &point : points) { + CncSimPose end = active_sink->position(); + end.x = point.NURBS_X; + end.y = point.NURBS_Y; + active_sink->straight_feed(lineno, end); + } +} + +void RIGID_TAP(int lineno, double x, double y, double z, double) { + if (active_sink) { + CncSimPose end = active_sink->position(); + end.x = x; + end.y = y; + end.z = z; + active_sink->straight_feed(lineno, end); + } +} + +void STRAIGHT_PROBE(int lineno, + double x, double y, double z, + double a, double b, double c, + double u, double v, double w, + unsigned char) { + if (active_sink) { + active_sink->straight_feed(lineno, make_pose(x, y, z, a, b, c, u, v, w)); + } +} + +void STOP() { +} + +void SET_SPINDLE_MODE(int, double) { +} + +void SPINDLE_RETRACT_TRAVERSE() { +} + +void START_SPINDLE_CLOCKWISE(int, int) { +} + +void START_SPINDLE_COUNTERCLOCKWISE(int, int) { +} + +void STOP_SPINDLE_TURNING(int) { + if (active_sink) { + active_sink->set_spindle_speed(0.0, 0); + } +} + +void SPINDLE_RETRACT() { +} + +void ORIENT_SPINDLE(int, double, int) { +} + +void WAIT_SPINDLE_ORIENT_COMPLETE(int, double) { +} + +void LOCK_SPINDLE_Z() { +} + +void USE_SPINDLE_FORCE() { +} + +void USE_NO_SPINDLE_FORCE() { +} + +void SET_TOOL_TABLE_ENTRY(int, int, const EmcPose &, double, double, double, int) { +} + +void USE_TOOL_LENGTH_OFFSET(const EmcPose &) { +} + +void CHANGE_TOOL_NUMBER(int number) { + if (active_sink) { + active_sink->select_tool(number); + active_sink->change_tool(0); + } +} + +void RELOAD_TOOLDATA(void) { +} + +void CLAMP_AXIS(CANON_AXIS) { +} + +void COMMENT(const char *) { + if (active_sink) { + CncSimEvent event{}; + event.version = 1; + event.type = CNC_SIM_EVENT_COMMENT; + active_sink->emit_raw(event); + } +} + +void DISABLE_ADAPTIVE_FEED() { +} + +void ENABLE_ADAPTIVE_FEED() { +} + +void DISABLE_FEED_OVERRIDE() { +} + +void ENABLE_FEED_OVERRIDE() { +} + +void DISABLE_SPEED_OVERRIDE(int) { +} + +void ENABLE_SPEED_OVERRIDE(int) { +} + +void DISABLE_FEED_HOLD() { +} + +void ENABLE_FEED_HOLD() { +} + +void FLOOD_OFF() { +} + +void FLOOD_ON() { +} + +void MESSAGE(char *) { +} + +void LOG(char *) { +} + +void LOGOPEN(char *) { +} + +void LOGAPPEND(char *) { +} + +void LOGCLOSE() { +} + +void MIST_OFF() { +} + +void MIST_ON() { +} + +void PALLET_SHUTTLE() { +} + +void TURN_PROBE_OFF() { +} + +void TURN_PROBE_ON() { +} + +void UNCLAMP_AXIS(CANON_AXIS) { +} + +void NURB_KNOT_VECTOR() { +} + +void NURB_CONTROL_POINT(int, double, double, double, double) { +} + +void NURB_FEED(double, double) { +} + +void SET_BLOCK_DELETE(bool) { +} + +bool GET_BLOCK_DELETE(void) { + return false; +} + +void OPTIONAL_PROGRAM_STOP() { +} + +void SET_OPTIONAL_PROGRAM_STOP(bool) { +} + +bool GET_OPTIONAL_PROGRAM_STOP() { + return false; +} + +void PROGRAM_STOP() { +} + +void SET_MOTION_OUTPUT_BIT(int) { +} + +void CLEAR_MOTION_OUTPUT_BIT(int) { +} + +void SET_AUX_OUTPUT_BIT(int) { +} + +void CLEAR_AUX_OUTPUT_BIT(int) { +} + +void SET_MOTION_OUTPUT_VALUE(int, double) { +} + +void SET_AUX_OUTPUT_VALUE(int, double) { +} + +int WAIT(int, int, int wait_type, double) { + return wait_type; +} + +int UNLOCK_ROTARY(int, int) { + return 0; +} + +int LOCK_ROTARY(int, int) { + return 0; +} + +double GET_EXTERNAL_FEED_RATE() { + return active_sink ? active_sink->feed_rate() : 0.0; +} + +int GET_EXTERNAL_FLOOD() { + return 0; +} + +CANON_UNITS GET_EXTERNAL_LENGTH_UNIT_TYPE() { + return active_sink && active_sink->unit_scale() == 25.4 ? CANON_UNITS_INCHES : CANON_UNITS_MM; +} + +double GET_EXTERNAL_LENGTH_UNITS() { + return active_sink ? active_sink->unit_scale() : 1.0; +} + +double GET_EXTERNAL_ANGLE_UNITS() { + return 1.0; +} + +int GET_EXTERNAL_MIST() { + return 0; +} + +CANON_MOTION_MODE GET_EXTERNAL_MOTION_CONTROL_MODE() { + return CANON_EXACT_STOP; +} + +double GET_EXTERNAL_MOTION_CONTROL_TOLERANCE() { + return 0.0; +} + +double GET_EXTERNAL_MOTION_CONTROL_NAIVECAM_TOLERANCE() { + return 0.0; +} + +void GET_EXTERNAL_PARAMETER_FILE_NAME(char *filename, int max_size) { + trace_call("GET_EXTERNAL_PARAMETER_FILE_NAME"); + std::snprintf(filename, max_size, "%s", parameter_file_name.c_str()); +} + +void SET_PARAMETER_FILE_NAME(const char *filename) { + trace_call("SET_PARAMETER_FILE_NAME"); + parameter_file_name = filename ? filename : "rs274ngc.var"; +} + +CANON_PLANE GET_EXTERNAL_PLANE() { + return active_sink ? g_code_to_plane(active_sink->plane()) : CANON_PLANE::XY; +} + +double GET_EXTERNAL_POSITION_A() { return current_position().a; } +double GET_EXTERNAL_POSITION_B() { return current_position().b; } +double GET_EXTERNAL_POSITION_C() { return current_position().c; } +double GET_EXTERNAL_POSITION_X() { return current_position().x; } +double GET_EXTERNAL_POSITION_Y() { return current_position().y; } +double GET_EXTERNAL_POSITION_Z() { return current_position().z; } +double GET_EXTERNAL_POSITION_U() { return current_position().u; } +double GET_EXTERNAL_POSITION_V() { return current_position().v; } +double GET_EXTERNAL_POSITION_W() { return current_position().w; } + +double GET_EXTERNAL_PROBE_POSITION_A() { return current_position().a; } +double GET_EXTERNAL_PROBE_POSITION_B() { return current_position().b; } +double GET_EXTERNAL_PROBE_POSITION_C() { return current_position().c; } +double GET_EXTERNAL_PROBE_POSITION_X() { return current_position().x; } +double GET_EXTERNAL_PROBE_POSITION_Y() { return current_position().y; } +double GET_EXTERNAL_PROBE_POSITION_Z() { return current_position().z; } +double GET_EXTERNAL_PROBE_POSITION_U() { return current_position().u; } +double GET_EXTERNAL_PROBE_POSITION_V() { return current_position().v; } +double GET_EXTERNAL_PROBE_POSITION_W() { return current_position().w; } + +double GET_EXTERNAL_PROBE_VALUE() { + return 0.0; +} + +int GET_EXTERNAL_PROBE_TRIPPED_VALUE() { + return 0; +} + +int GET_EXTERNAL_QUEUE_EMPTY() { + return 1; +} + +double GET_EXTERNAL_SPEED(int) { + return active_sink ? active_sink->spindle_speed() : 0.0; +} + +CANON_DIRECTION GET_EXTERNAL_SPINDLE(int) { + return active_sink && active_sink->spindle_speed() != 0.0 ? CANON_CLOCKWISE : CANON_STOPPED; +} + +double GET_EXTERNAL_TOOL_LENGTH_XOFFSET() { return 0.0; } +double GET_EXTERNAL_TOOL_LENGTH_YOFFSET() { return 0.0; } +double GET_EXTERNAL_TOOL_LENGTH_ZOFFSET() { return 0.0; } +double GET_EXTERNAL_TOOL_LENGTH_AOFFSET() { return 0.0; } +double GET_EXTERNAL_TOOL_LENGTH_BOFFSET() { return 0.0; } +double GET_EXTERNAL_TOOL_LENGTH_COFFSET() { return 0.0; } +double GET_EXTERNAL_TOOL_LENGTH_UOFFSET() { return 0.0; } +double GET_EXTERNAL_TOOL_LENGTH_VOFFSET() { return 0.0; } +double GET_EXTERNAL_TOOL_LENGTH_WOFFSET() { return 0.0; } + +int GET_EXTERNAL_TOOL_SLOT() { + return active_sink ? active_sink->selected_tool() : 0; +} + +int GET_EXTERNAL_SELECTED_TOOL_SLOT() { + return active_sink ? active_sink->selected_tool() : -1; +} + +CANON_TOOL_TABLE GET_EXTERNAL_TOOL_TABLE(int) { + CANON_TOOL_TABLE tool{}; + std::memset(&tool, 0, sizeof(tool)); + return tool; +} + +int GET_EXTERNAL_TC_FAULT() { + return 0; +} + +int GET_EXTERNAL_TC_REASON() { + return 0; +} + +double GET_EXTERNAL_TRAVERSE_RATE() { + return 0.0; +} + +int GET_EXTERNAL_FEED_OVERRIDE_ENABLE() { + return 1; +} + +int GET_EXTERNAL_SPINDLE_OVERRIDE_ENABLE(int) { + return 1; +} + +int GET_EXTERNAL_ADAPTIVE_FEED_ENABLE() { + return 1; +} + +int GET_EXTERNAL_FEED_HOLD_ENABLE() { + return 1; +} + +int GET_EXTERNAL_DIGITAL_INPUT(int, int def) { + return def; +} + +double GET_EXTERNAL_ANALOG_INPUT(int, double def) { + return def; +} + +int GET_EXTERNAL_AXIS_MASK() { + return 0x1ff; +} + +void ON_RESET(void) { + if (active_sink) { + active_sink->reset(); + } +} + +void CANON_ERROR(const char *, ...) { +} + +void UPDATE_TAG(const StateTag &) { +} + +USER_DEFINED_FUNCTION_TYPE USER_DEFINED_FUNCTION[USER_DEFINED_FUNCTION_NUM]; + +int GET_EXTERNAL_OFFSET_APPLIED() { + return 0; +} + +EmcPose GET_EXTERNAL_OFFSETS() { + EmcPose pose; + ZERO_EMC_POSE(pose); + return pose; +} diff --git a/core/src/linuxcnc_canon_bridge.h b/core/src/linuxcnc_canon_bridge.h new file mode 100644 index 0000000..d02ca63 --- /dev/null +++ b/core/src/linuxcnc_canon_bridge.h @@ -0,0 +1,7 @@ +#pragma once + +class CanonEventSink; + +void cnc_sim_linuxcnc_set_canon_sink(CanonEventSink *sink); +CanonEventSink *cnc_sim_linuxcnc_get_canon_sink(); + diff --git a/core/src/linuxcnc_rs274_backend.cpp b/core/src/linuxcnc_rs274_backend.cpp new file mode 100644 index 0000000..da1ac27 --- /dev/null +++ b/core/src/linuxcnc_rs274_backend.cpp @@ -0,0 +1,179 @@ +#include "linuxcnc_rs274_backend.h" + +#include + +#include "linuxcnc_canon_bridge.h" +#include "simulator_gcode_controls.h" + +#include "linuxcnc.h" +#include "nml_intf/canon.hh" +#include "nml_intf/interp_return.hh" +#include "rs274ngc/interp_base.hh" +#include "emc/tooldata/tooldata.hh" + +#include +#include +#include +#include + +int _task = 0; +char _parameter_file_name[LINELEN]; + +extern "C" PyObject *PyInit_interpreter(void); +extern "C" PyObject *PyInit_emccanon(void); +extern "C" struct _inittab builtin_modules[]; +struct _inittab builtin_modules[] = { + {"interpreter", PyInit_interpreter}, + {"emccanon", PyInit_emccanon}, + {nullptr, nullptr}, +}; + +namespace { + +void init_minimal_tooldata_once() { + static bool created = false; + if (!created) { + tool_mmap_creator(nullptr, 0); + created = true; + } + + tooldata_reset(); + + CANON_TOOL_TABLE spindle = tooldata_entry_init(); + spindle.toolno = 0; + spindle.pocketno = 0; + tooldata_put(spindle, 0); + + CANON_TOOL_TABLE tool = tooldata_entry_init(); + tool.toolno = 1; + tool.pocketno = 1; + tool.diameter = 6.0; + tooldata_put(tool, 1); +} + +bool normal_read_status(int status) { + return status == INTERP_OK || + status == INTERP_EXECUTE_FINISH || + status == INTERP_ENDFILE || + status == INTERP_EXIT; +} + +bool normal_execute_status(int status, bool *program_done) { + if (status == INTERP_EXIT || status == INTERP_ENDFILE) { + *program_done = true; + return true; + } + return status == INTERP_OK || status == INTERP_EXECUTE_FINISH; +} + +bool stop_if_callback_aborted(CanonEventSink &sink, std::string *error) { + if (!sink.callback_aborted()) { + return false; + } + if (error) { + *error = "event callback aborted parsing"; + } + return true; +} + +std::string interp_error(InterpBase *interp, int status, const char *stage) { + char message[1024]{}; + interp->error_text(status, message, sizeof(message)); + std::string result = stage; + result += " failed"; + if (message[0]) { + result += ": "; + result += message; + } + return result; +} + +} // namespace + +int parse_linuxcnc_rs274_backend(CanonEventSink &sink, + const char *program, + size_t program_len, + std::string *error) { + if (!program && program_len != 0) { + if (error) { + *error = "null program buffer"; + } + return -1; + } + + if (const char *parameter_file = std::getenv("CNC_SIM_RS274_VAR")) { + SET_PARAMETER_FILE_NAME(parameter_file); + } + init_minimal_tooldata_once(); + sink.clear_callback_status(); + cnc_sim_linuxcnc_set_canon_sink(&sink); + + InterpBase *interp = makeInterp(); + int status = interp->init(); + if (status != INTERP_OK) { + if (error) { + *error = interp_error(interp, status, "init"); + } + delete interp; + cnc_sim_linuxcnc_set_canon_sink(nullptr); + return -1; + } + + std::string source(program, program + program_len); + std::istringstream input(source); + std::string line; + bool program_done = false; + int line_number = 0; + while (!program_done && std::getline(input, line)) { + ++line_number; + std::vector control_actions; + if (parse_simulator_gcode_control_line(line, &control_actions)) { + for (const auto &action : control_actions) { + emit_simulator_gcode_control_action(sink, action, line_number); + if (stop_if_callback_aborted(sink, error)) { + interp->exit(); + delete interp; + cnc_sim_linuxcnc_set_canon_sink(nullptr); + return -1; + } + } + continue; + } + + status = interp->read(line.c_str()); + if (!normal_read_status(status)) { + if (error) { + *error = interp_error(interp, status, "read"); + } + interp->exit(); + delete interp; + cnc_sim_linuxcnc_set_canon_sink(nullptr); + return -1; + } + if (status == INTERP_EXIT || status == INTERP_ENDFILE) { + break; + } + + status = interp->execute(); + if (!normal_execute_status(status, &program_done)) { + if (error) { + *error = interp_error(interp, status, "execute"); + } + interp->exit(); + delete interp; + cnc_sim_linuxcnc_set_canon_sink(nullptr); + return -1; + } + if (stop_if_callback_aborted(sink, error)) { + interp->exit(); + delete interp; + cnc_sim_linuxcnc_set_canon_sink(nullptr); + return -1; + } + } + + interp->exit(); + delete interp; + cnc_sim_linuxcnc_set_canon_sink(nullptr); + return 0; +} diff --git a/core/src/linuxcnc_rs274_backend.h b/core/src/linuxcnc_rs274_backend.h new file mode 100644 index 0000000..2951dd3 --- /dev/null +++ b/core/src/linuxcnc_rs274_backend.h @@ -0,0 +1,12 @@ +#pragma once + +#include "canon_event_sink.h" + +#include +#include + +int parse_linuxcnc_rs274_backend(CanonEventSink &sink, + const char *program, + size_t program_len, + std::string *error); + diff --git a/core/src/rtcp_kinematics.cpp b/core/src/rtcp_kinematics.cpp new file mode 100644 index 0000000..0d4a272 --- /dev/null +++ b/core/src/rtcp_kinematics.cpp @@ -0,0 +1,72 @@ +#include "rtcp_kinematics.h" + +#include + +namespace { + +constexpr double kPi = 3.141592653589793238462643383279502884; + +double radians(double degrees) { + return degrees * kPi / 180.0; +} + +RtcpVector rotate_x(RtcpVector vector, double angle) { + const double c = std::cos(angle); + const double s = std::sin(angle); + return { + vector.x, + vector.y * c - vector.z * s, + vector.y * s + vector.z * c, + }; +} + +RtcpVector rotate_y(RtcpVector vector, double angle) { + const double c = std::cos(angle); + const double s = std::sin(angle); + return { + vector.x * c + vector.z * s, + vector.y, + -vector.x * s + vector.z * c, + }; +} + +RtcpVector rotate_z(RtcpVector vector, double angle) { + const double c = std::cos(angle); + const double s = std::sin(angle); + return { + vector.x * c - vector.y * s, + vector.x * s + vector.y * c, + vector.z, + }; +} + +} // namespace + +RtcpVector rtcp_rotate_abc_degrees(RtcpVector vector, double a_deg, double b_deg, double c_deg) { + vector = rotate_x(vector, radians(a_deg)); + vector = rotate_y(vector, radians(b_deg)); + vector = rotate_z(vector, radians(c_deg)); + return vector; +} + +RtcpVector rtcp_tool_vector_from_pose(const CncSimPose &pose, double tool_length) { + return rtcp_rotate_abc_degrees({0.0, 0.0, -tool_length}, pose.a, pose.b, pose.c); +} + +CncSimPose rtcp_pivot_from_tool_tip(const CncSimPose &tool_tip, double tool_length) { + const RtcpVector tool = rtcp_tool_vector_from_pose(tool_tip, tool_length); + CncSimPose pivot = tool_tip; + pivot.x = tool_tip.x - tool.x; + pivot.y = tool_tip.y - tool.y; + pivot.z = tool_tip.z - tool.z; + return pivot; +} + +CncSimPose rtcp_tool_tip_from_pivot(const CncSimPose &pivot, double tool_length) { + const RtcpVector tool = rtcp_tool_vector_from_pose(pivot, tool_length); + CncSimPose tool_tip = pivot; + tool_tip.x = pivot.x + tool.x; + tool_tip.y = pivot.y + tool.y; + tool_tip.z = pivot.z + tool.z; + return tool_tip; +} diff --git a/core/src/rtcp_kinematics.h b/core/src/rtcp_kinematics.h new file mode 100644 index 0000000..ebfa405 --- /dev/null +++ b/core/src/rtcp_kinematics.h @@ -0,0 +1,17 @@ +#pragma once + +#include "cnc_sim_api.h" + +struct RtcpVector { + double x; + double y; + double z; +}; + +// Rotation order is intrinsic tool orientation A then B then C, represented as +// Rz(C) * Ry(B) * Rx(A) applied to a local tool vector. +RtcpVector rtcp_rotate_abc_degrees(RtcpVector vector, double a_deg, double b_deg, double c_deg); + +RtcpVector rtcp_tool_vector_from_pose(const CncSimPose &pose, double tool_length); +CncSimPose rtcp_pivot_from_tool_tip(const CncSimPose &tool_tip, double tool_length); +CncSimPose rtcp_tool_tip_from_pivot(const CncSimPose &pivot, double tool_length); diff --git a/core/src/simulator_gcode_controls.cpp b/core/src/simulator_gcode_controls.cpp new file mode 100644 index 0000000..950386e --- /dev/null +++ b/core/src/simulator_gcode_controls.cpp @@ -0,0 +1,145 @@ +#include "simulator_gcode_controls.h" + +#include +#include +#include + +namespace { + +std::string strip_line_comment(const std::string &line) { + std::string out; + bool in_paren = false; + for (char raw : line) { + const char ch = static_cast(std::toupper(static_cast(raw))); + if (in_paren) { + if (ch == ')') { + in_paren = false; + } + continue; + } + if (ch == '(') { + in_paren = true; + continue; + } + if (ch == ';') { + break; + } + out.push_back(ch); + } + return out; +} + +bool g_code_is(double actual, double expected) { + return std::fabs(actual - expected) < 0.0001; +} + +bool parse_m_control_line(const std::string &stripped, + std::vector *actions) { + const char *text = stripped.c_str(); + bool saw_code = false; + for (size_t i = 0; text[i] != '\0';) { + if (std::isspace(static_cast(text[i]))) { + ++i; + continue; + } + if (text[i] != 'M') { + return false; + } + ++i; + char *end = nullptr; + const long value = std::strtol(text + i, &end, 10); + if (end == text + i) { + return false; + } + + SimulatorGcodeControlAction action{}; + action.kind = SimulatorGcodeControlKind::KinematicsSwitch; + if (value == 428) { + action.value = 1; + action.rtcp_enabled = true; + } else if (value == 429) { + action.value = 0; + action.rtcp_enabled = false; + } else if (value == 430) { + action.value = 2; + action.rtcp_enabled = true; + } else { + return false; + } + actions->push_back(action); + saw_code = true; + i = static_cast(end - text); + } + return saw_code; +} + +bool parse_rtcp_control_line(const std::string &stripped, + std::vector *actions) { + const char *text = stripped.c_str(); + std::vector parsed; + int h_code = 0; + bool saw_rtcp_code = false; + + for (size_t i = 0; text[i] != '\0';) { + if (std::isspace(static_cast(text[i]))) { + ++i; + continue; + } + const char letter = text[i]; + if (letter != 'G' && letter != 'H') { + return false; + } + ++i; + char *end = nullptr; + const double value = std::strtod(text + i, &end); + if (end == text + i) { + return false; + } + if (letter == 'H') { + h_code = static_cast(std::lround(value)); + } else if (g_code_is(value, 43.4) || g_code_is(value, 43.5) || g_code_is(value, 49.0)) { + SimulatorGcodeControlAction action{}; + action.kind = SimulatorGcodeControlKind::RtcpState; + action.value = static_cast(std::lround(value * 10.0)); + action.rtcp_enabled = g_code_is(value, 43.4) || g_code_is(value, 43.5); + parsed.push_back(action); + saw_rtcp_code = true; + } else { + return false; + } + i = static_cast(end - text); + } + + for (auto &action : parsed) { + action.h_code = action.rtcp_enabled ? h_code : 0; + actions->push_back(action); + } + return saw_rtcp_code; +} + +} // namespace + +bool parse_simulator_gcode_control_line(const std::string &line, + std::vector *actions) { + actions->clear(); + const std::string stripped = strip_line_comment(line); + if (parse_m_control_line(stripped, actions)) { + return true; + } + actions->clear(); + if (parse_rtcp_control_line(stripped, actions)) { + return true; + } + actions->clear(); + return false; +} + +void emit_simulator_gcode_control_action(CanonEventSink &sink, + const SimulatorGcodeControlAction &action, + int line) { + if (action.kind == SimulatorGcodeControlKind::KinematicsSwitch) { + sink.switch_kinematics(action.value, action.rtcp_enabled, line); + } else if (action.kind == SimulatorGcodeControlKind::RtcpState) { + sink.set_rtcp_state(action.rtcp_enabled, action.h_code, line); + } +} diff --git a/core/src/simulator_gcode_controls.h b/core/src/simulator_gcode_controls.h new file mode 100644 index 0000000..6572fcb --- /dev/null +++ b/core/src/simulator_gcode_controls.h @@ -0,0 +1,24 @@ +#pragma once + +#include "canon_event_sink.h" + +#include +#include + +enum class SimulatorGcodeControlKind { + KinematicsSwitch, + RtcpState, +}; + +struct SimulatorGcodeControlAction { + SimulatorGcodeControlKind kind; + int value; + bool rtcp_enabled; + int h_code; +}; + +bool parse_simulator_gcode_control_line(const std::string &line, + std::vector *actions); +void emit_simulator_gcode_control_action(CanonEventSink &sink, + const SimulatorGcodeControlAction &action, + int line); diff --git a/core/src/smoke_gcode_parser.cpp b/core/src/smoke_gcode_parser.cpp new file mode 100644 index 0000000..791f208 --- /dev/null +++ b/core/src/smoke_gcode_parser.cpp @@ -0,0 +1,399 @@ +#include "smoke_gcode_parser.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct Word { + char letter; + double value; +}; + +std::string trim(const std::string &value) { + auto first = std::find_if_not(value.begin(), value.end(), [](unsigned char ch) { return std::isspace(ch); }); + auto last = std::find_if_not(value.rbegin(), value.rend(), [](unsigned char ch) { return std::isspace(ch); }).base(); + if (first >= last) { + return {}; + } + return std::string(first, last); +} + +std::string strip_comments(const std::string &line, std::string *comment) { + std::string out; + bool in_paren = false; + std::string paren; + + for (char raw : line) { + char ch = static_cast(std::toupper(static_cast(raw))); + if (in_paren) { + if (ch == ')') { + in_paren = false; + if (comment && !paren.empty()) { + if (!comment->empty()) { + *comment += " "; + } + *comment += trim(paren); + } + paren.clear(); + } else { + paren.push_back(raw); + } + continue; + } + if (ch == '(') { + in_paren = true; + continue; + } + if (ch == ';') { + break; + } + out.push_back(ch); + } + return out; +} + +std::vector parse_word_list(const std::string &line) { + std::vector words; + const char *text = line.c_str(); + char *end = nullptr; + for (size_t i = 0; text[i] != '\0';) { + if (std::isspace(static_cast(text[i]))) { + ++i; + continue; + } + char letter = text[i]; + if (!std::isalpha(static_cast(letter))) { + ++i; + continue; + } + ++i; + double value = std::strtod(text + i, &end); + if (end == text + i) { + continue; + } + words.push_back({letter, value}); + i = static_cast(end - text); + } + return words; +} + +std::unordered_map last_words_by_letter(const std::vector &word_list) { + std::unordered_map words; + for (const Word &word : word_list) { + words[word.letter] = word.value; + } + return words; +} + +bool has_axis_word(const std::unordered_map &words) { + static constexpr char axes[] = {'X', 'Y', 'Z', 'A', 'B', 'C', 'U', 'V', 'W'}; + for (char axis : axes) { + if (words.find(axis) != words.end()) { + return true; + } + } + return false; +} + +void apply_axis(CncSimPose *pose, const std::unordered_map &words, char axis, double CncSimPose::*member, bool absolute, double scale) { + auto it = words.find(axis); + if (it == words.end()) { + return; + } + const double value = it->second * scale; + if (absolute) { + pose->*member = value; + } else { + pose->*member += value; + } +} + +void apply_axes(CncSimPose *pose, const std::unordered_map &words, bool absolute, double scale) { + apply_axis(pose, words, 'X', &CncSimPose::x, absolute, scale); + apply_axis(pose, words, 'Y', &CncSimPose::y, absolute, scale); + apply_axis(pose, words, 'Z', &CncSimPose::z, absolute, scale); + apply_axis(pose, words, 'A', &CncSimPose::a, absolute, 1.0); + apply_axis(pose, words, 'B', &CncSimPose::b, absolute, 1.0); + apply_axis(pose, words, 'C', &CncSimPose::c, absolute, 1.0); + apply_axis(pose, words, 'U', &CncSimPose::u, absolute, scale); + apply_axis(pose, words, 'V', &CncSimPose::v, absolute, scale); + apply_axis(pose, words, 'W', &CncSimPose::w, absolute, scale); +} + +void set_arc_center(CncSimEvent *event, const std::unordered_map &words, int plane, double scale) { + event->center = event->start; + if (words.count('R')) { + const double radius = words.at('R') * scale; + double start_first = 0.0; + double start_second = 0.0; + double end_first = 0.0; + double end_second = 0.0; + + if (plane == 17) { + start_first = event->start.x; + start_second = event->start.y; + end_first = event->end.x; + end_second = event->end.y; + } else if (plane == 18) { + start_first = event->start.x; + start_second = event->start.z; + end_first = event->end.x; + end_second = event->end.z; + } else if (plane == 19) { + start_first = event->start.y; + start_second = event->start.z; + end_first = event->end.y; + end_second = event->end.z; + } + + const double dx = end_first - start_first; + const double dy = end_second - start_second; + const double chord = std::hypot(dx, dy); + if (chord > 0.0 && std::fabs(radius) >= chord * 0.5) { + const double mid_first = (start_first + end_first) * 0.5; + const double mid_second = (start_second + end_second) * 0.5; + double h = std::sqrt(std::max(0.0, radius * radius - chord * chord * 0.25)); + if (radius < 0.0) { + h = -h; + } + const double direction = event->arc_turns >= 0 ? 1.0 : -1.0; + const double center_first = mid_first + (-dy / chord) * h * direction; + const double center_second = mid_second + (dx / chord) * h * direction; + + if (plane == 17) { + event->center.x = center_first; + event->center.y = center_second; + } else if (plane == 18) { + event->center.x = center_first; + event->center.z = center_second; + } else if (plane == 19) { + event->center.y = center_first; + event->center.z = center_second; + } + return; + } + } + + const double i = words.count('I') ? words.at('I') * scale : 0.0; + const double j = words.count('J') ? words.at('J') * scale : 0.0; + const double k = words.count('K') ? words.at('K') * scale : 0.0; + if (plane == 17) { + event->center.x = event->start.x + i; + event->center.y = event->start.y + j; + } else if (plane == 18) { + event->center.x = event->start.x + i; + event->center.z = event->start.z + k; + } else if (plane == 19) { + event->center.y = event->start.y + j; + event->center.z = event->start.z + k; + } +} + +int rounded_word(const std::unordered_map &words, char letter, int fallback) { + auto it = words.find(letter); + if (it == words.end()) { + return fallback; + } + return static_cast(std::lround(it->second)); +} + +int rounded_value(double value) { + return static_cast(std::lround(value)); +} + +bool g_code_is(double actual, double expected) { + return std::fabs(actual - expected) < 0.0001; +} + +} // namespace + +SmokeGcodeParser::SmokeGcodeParser(CanonEventSink &sink) + : sink_(sink) { +} + +namespace { + +bool stop_if_callback_aborted(CanonEventSink &sink, std::string *error) { + if (!sink.callback_aborted()) { + return false; + } + if (error) { + *error = "event callback aborted parsing"; + } + return true; +} + +} // namespace + +int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string *error) { + if (!program && program_len != 0) { + if (error) { + *error = "null program buffer"; + } + return -1; + } + + sink_.reset(); + sink_.clear_callback_status(); + absolute_ = true; + + std::string source(program, program + program_len); + std::istringstream input(source); + std::string raw_line; + int line_number = 0; + int modal_motion = -1; + + while (std::getline(input, raw_line)) { + ++line_number; + std::string comment; + std::string line = strip_comments(raw_line, &comment); + if (!comment.empty()) { + CncSimEvent event{}; + event.version = 1; + event.type = CNC_SIM_EVENT_COMMENT; + event.line = line_number; + sink_.emit_raw(event); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + } + + auto word_list = parse_word_list(line); + if (word_list.empty()) { + continue; + } + + auto words = last_words_by_letter(word_list); + std::vector m_codes; + + for (const Word &word : word_list) { + if (word.letter == 'G') { + const int g = rounded_value(word.value); + if (g_code_is(word.value, 43.4) || g_code_is(word.value, 43.5)) { + sink_.set_rtcp_state(true, rounded_word(words, 'H', 0), line_number); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + } else if (g == 49) { + sink_.set_rtcp_state(false, 0, line_number); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + } else if (g == 17 || g == 18 || g == 19) { + sink_.select_plane(g, line_number); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + } else if (g == 20 || g == 70) { + sink_.use_length_units(25.4, line_number); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + } else if (g == 21 || g == 71) { + sink_.use_length_units(1.0, line_number); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + } else if (g == 90) { + absolute_ = true; + } else if (g == 91) { + absolute_ = false; + } else if (g == 0 || g == 1 || g == 2 || g == 3) { + modal_motion = g; + } else if (g == 4) { + sink_.dwell(words.count('P') ? words['P'] : 0.0, line_number); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + } + } else if (word.letter == 'M') { + m_codes.push_back(rounded_value(word.value)); + } + } + + if (words.count('F')) { + sink_.set_feed_rate(words['F'] * sink_.unit_scale(), line_number); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + } + if (words.count('S')) { + sink_.set_spindle_speed(words['S'], line_number); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + } + if (words.count('T')) { + sink_.select_tool(rounded_word(words, 'T', sink_.selected_tool())); + } + + bool program_end = false; + for (int m : m_codes) { + if (m == 3 || m == 4 || m == 5) { + sink_.set_spindle_speed(m == 5 ? 0.0 : sink_.spindle_speed(), line_number); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + } else if (m == 6) { + sink_.change_tool(line_number); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + } else if (m == 428) { + sink_.switch_kinematics(1, true, line_number); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + } else if (m == 429) { + sink_.switch_kinematics(0, false, line_number); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + } else if (m == 430) { + sink_.switch_kinematics(2, true, line_number); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + } else if (m == 2 || m == 30) { + sink_.program_end(line_number); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + program_end = true; + } + } + if (program_end) { + break; + } + + if (has_axis_word(words) && (modal_motion == 0 || modal_motion == 1 || modal_motion == 2 || modal_motion == 3)) { + CncSimEvent event{}; + event.start = sink_.position(); + event.end = sink_.position(); + apply_axes(&event.end, words, absolute_, sink_.unit_scale()); + if (modal_motion == 2 || modal_motion == 3) { + event.arc_turns = modal_motion == 2 ? -1 : 1; + set_arc_center(&event, words, sink_.plane(), sink_.unit_scale()); + } + if (modal_motion == 0) { + sink_.straight_traverse(line_number, event.end); + } else if (modal_motion == 1) { + sink_.straight_feed(line_number, event.end); + } else { + sink_.arc_feed(line_number, event.end, event.center, event.arc_turns); + } + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + } + } + + return 0; +} diff --git a/core/src/smoke_gcode_parser.h b/core/src/smoke_gcode_parser.h new file mode 100644 index 0000000..3470c06 --- /dev/null +++ b/core/src/smoke_gcode_parser.h @@ -0,0 +1,18 @@ +#pragma once + +#include "canon_event_sink.h" + +#include +#include + +class SmokeGcodeParser { +public: + explicit SmokeGcodeParser(CanonEventSink &sink); + + int parse(const char *program, size_t program_len, std::string *error); + +private: + CanonEventSink &sink_; + bool absolute_ = true; +}; + diff --git a/core/tests/canon_event_sink_smoke.cpp b/core/tests/canon_event_sink_smoke.cpp new file mode 100644 index 0000000..8827996 --- /dev/null +++ b/core/tests/canon_event_sink_smoke.cpp @@ -0,0 +1,61 @@ +#include "canon_event_sink.h" + +#include +#include + +namespace { + +int collect_event(const CncSimEvent *event, void *user_data) { + auto *events = static_cast *>(user_data); + events->push_back(*event); + return 0; +} + +int abort_on_second_event(const CncSimEvent *, void *user_data) { + auto *count = static_cast(user_data); + ++(*count); + return *count >= 2 ? 1 : 0; +} + +bool expect(bool value, const char *message) { + if (!value) { + std::cerr << "FAIL: " << message << '\n'; + return false; + } + return true; +} + +} // namespace + +int main() { + bool ok = true; + std::vector events; + CanonEventSink sink; + sink.set_callback(collect_event, &events); + + CncSimPose end{}; + end.x = 10.0; + end.y = 5.0; + sink.use_length_units(1.0, 1); + sink.select_plane(17, 1); + sink.set_feed_rate(500.0, 2); + sink.straight_feed(3, end); + sink.program_end(4); + + ok &= expect(events.size() == 5, "expected five emitted events"); + ok &= expect(events[3].type == CNC_SIM_EVENT_LINEAR_FEED, "expected linear event"); + ok &= expect(events[3].end.x == 10.0 && events[3].end.y == 5.0, "expected updated end pose"); + ok &= expect(sink.position().x == 10.0 && sink.position().y == 5.0, "expected sink position update"); + + int abort_count = 0; + sink.reset(); + sink.set_callback(abort_on_second_event, &abort_count); + sink.select_plane(17, 1); + sink.set_feed_rate(100.0, 2); + sink.straight_feed(3, end); + ok &= expect(sink.callback_aborted(), "expected callback abort state"); + ok &= expect(abort_count == 2, "expected no further callbacks after abort"); + + return ok ? 0 : 1; +} + diff --git a/core/tests/cnc_sim_api_linuxcnc_rs274_smoke.cpp b/core/tests/cnc_sim_api_linuxcnc_rs274_smoke.cpp new file mode 100644 index 0000000..c0136cc --- /dev/null +++ b/core/tests/cnc_sim_api_linuxcnc_rs274_smoke.cpp @@ -0,0 +1,162 @@ +#include "cnc_sim_api.h" + +#include +#include + +namespace { + +int collect_event(const CncSimEvent *event, void *user_data) { + auto *events = static_cast *>(user_data); + events->push_back(*event); + return 0; +} + +int abort_on_second_event(const CncSimEvent *, void *user_data) { + auto *count = static_cast(user_data); + ++(*count); + return *count >= 2 ? 1 : 0; +} + +bool expect(bool value, const char *message) { + if (!value) { + std::cerr << "FAIL: " << message << '\n'; + return false; + } + return true; +} + +} // namespace + +int main() { + const char config[] = + "{\"backend\":\"linuxcnc-rs274\",\"rtcp\":{\"enabled\":true,\"toolLength\":100,\"toolLengths\":{\"7\":125}}}"; + const char program[] = + "G21 G90 G17\n" + "T1 M6\n" + "M428 M429 M430\n" + "G49\n" + "G43.4 H7\n" + "S8000 M3\n" + "G0 X0 Y0 Z5\n" + "F600\n" + "G1 Z-1\n" + "G1 X40 Y0\n" + "G3 X40 Y40 I0 J20\n" + "G1 X0 Y40\n" + "G1 X0 Y0\n" + "M30\n"; + + std::vector events; + CncSimHandle *sim = cnc_sim_create(); + cnc_sim_set_event_callback(sim, collect_event, &events); + + bool ok = true; + ok &= expect(cnc_sim_load_config_json(sim, config, sizeof(config) - 1) == 0, cnc_sim_last_error(sim)); + ok &= expect(cnc_sim_parse_program(sim, program, sizeof(program) - 1) == 0, cnc_sim_last_error(sim)); + + bool saw_tool = false; + bool saw_rapid = false; + bool saw_feed = false; + bool saw_arc = false; + bool saw_end = false; + bool saw_rtcp = false; + bool saw_m428 = false; + bool saw_m429 = false; + bool saw_m430 = false; + bool saw_g49 = false; + bool saw_g434 = false; + bool saw_g5x_offset = false; + bool saw_xy_rotation = false; + bool saw_g92_clear = false; + for (const auto &event : events) { + saw_tool = saw_tool || event.type == CNC_SIM_EVENT_TOOL_CHANGE; + saw_rapid = saw_rapid || event.type == CNC_SIM_EVENT_RAPID; + saw_feed = saw_feed || event.type == CNC_SIM_EVENT_LINEAR_FEED; + saw_arc = saw_arc || event.type == CNC_SIM_EVENT_ARC_FEED; + saw_end = saw_end || event.type == CNC_SIM_EVENT_PROGRAM_END; + saw_rtcp = saw_rtcp || + (event.type == CNC_SIM_EVENT_RTCP_PIVOT && + event.end.z == 130.0); + saw_m428 = saw_m428 || + (event.type == CNC_SIM_EVENT_KINEMATICS_SWITCH && + event.line == 3 && + event.reserved == 1 && + event.feed == 1.0); + saw_m429 = saw_m429 || + (event.type == CNC_SIM_EVENT_KINEMATICS_SWITCH && + event.line == 3 && + event.reserved == 0 && + event.feed == 0.0); + saw_m430 = saw_m430 || + (event.type == CNC_SIM_EVENT_KINEMATICS_SWITCH && + event.line == 3 && + event.reserved == 2 && + event.feed == 1.0); + saw_g49 = saw_g49 || + (event.type == CNC_SIM_EVENT_RTCP_STATE && + event.line == 4 && + event.feed == 0.0); + saw_g434 = saw_g434 || + (event.type == CNC_SIM_EVENT_RTCP_STATE && + event.line == 5 && + event.feed == 1.0 && + event.tool == 7 && + event.dwell_seconds == 125.0); + } + + ok &= expect(saw_tool, "expected LinuxCNC tool-change event"); + ok &= expect(saw_rapid, "expected LinuxCNC rapid event"); + ok &= expect(saw_feed, "expected LinuxCNC linear-feed event"); + ok &= expect(saw_arc, "expected LinuxCNC arc-feed event"); + ok &= expect(saw_end, "expected LinuxCNC program-end event"); + ok &= expect(saw_rtcp, "expected LinuxCNC RTCP pivot event"); + ok &= expect(saw_m428, "expected LinuxCNC M428 kinematics switch"); + ok &= expect(saw_m429, "expected LinuxCNC M429 kinematics switch"); + ok &= expect(saw_m430, "expected LinuxCNC M430 kinematics switch"); + ok &= expect(saw_g49, "expected LinuxCNC G49 RTCP off state"); + ok &= expect(saw_g434, "expected LinuxCNC G43.4 RTCP on state with H code"); + + const char coordinate_program[] = + "G21 G90 G17\n" + "G10 L2 P1 X12.5 Y-3 Z4 R30\n" + "G92 X1 Y2 Z3\n" + "G92.1\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, coordinate_program, sizeof(coordinate_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_g5x_offset = saw_g5x_offset || + (event.type == CNC_SIM_EVENT_SET_G5X_OFFSET && + event.tool == 1 && + event.start.x == 12.5 && + event.start.y == -3.0 && + event.start.z == 4.0); + saw_xy_rotation = saw_xy_rotation || + (event.type == CNC_SIM_EVENT_SET_XY_ROTATION && + event.feed == 30.0); + saw_g92_clear = saw_g92_clear || + (event.type == CNC_SIM_EVENT_SET_G92_OFFSET && + event.start.x == 0.0 && + event.start.y == 0.0 && + event.start.z == 0.0); + } + ok &= expect(saw_g5x_offset, "expected LinuxCNC G10 L2 G5X offset event"); + ok &= expect(saw_xy_rotation, "expected LinuxCNC G10 L2 XY rotation event"); + ok &= expect(saw_g92_clear, "expected LinuxCNC G92.1 clear offset event"); + + cnc_sim_destroy(sim); + + int abort_count = 0; + sim = cnc_sim_create(); + cnc_sim_set_event_callback(sim, abort_on_second_event, &abort_count); + ok &= expect(cnc_sim_load_config_json(sim, config, sizeof(config) - 1) == 0, cnc_sim_last_error(sim)); + ok &= expect(cnc_sim_parse_program(sim, program, sizeof(program) - 1) != 0, + "expected callback abort to stop LinuxCNC parsing"); + ok &= expect(abort_count == 2, "expected LinuxCNC parser to stop callbacks after abort"); + ok &= expect(std::string(cnc_sim_last_error(sim)) == "event callback aborted parsing", + "expected callback abort error"); + cnc_sim_destroy(sim); + + return ok ? 0 : 1; +} diff --git a/core/tests/cnc_sim_api_smoke.cpp b/core/tests/cnc_sim_api_smoke.cpp new file mode 100644 index 0000000..a81d3d1 --- /dev/null +++ b/core/tests/cnc_sim_api_smoke.cpp @@ -0,0 +1,141 @@ +#include "cnc_sim_api.h" + +#include +#include + +namespace { + +int collect_event(const CncSimEvent *event, void *user_data) { + auto *events = static_cast *>(user_data); + events->push_back(*event); + return 0; +} + +bool expect(bool value, const char *message) { + if (!value) { + std::cerr << "FAIL: " << message << '\n'; + return false; + } + return true; +} + +} // namespace + +int main() { + const char program[] = + "G21 G90 G17\n" + "T1 M6\n" + "M428 M429 M430\n" + "G49\n" + "G43.4 H7\n" + "S8000 M3\n" + "G0 X0 Y0 Z5\n" + "F600\n" + "G1 Z-1\n" + "G1 X20 Y0\n" + "G3 X20 Y20 I0 J10\n" + "G91 G1 X5\n" + "G90 G4 P0.25\n" + "G2 X25 Y25 R5\n" + "M30\n"; + + std::vector events; + CncSimHandle *sim = cnc_sim_create(); + cnc_sim_set_event_callback(sim, collect_event, &events); + const char smoke_config[] = + "{\"backend\":\"smoke\",\"rtcp\":{\"enabled\":true,\"toolLength\":100,\"toolLengths\":{\"7\":125}}}"; + int config_rc = cnc_sim_load_config_json(sim, smoke_config, sizeof(smoke_config) - 1); + + int rc = cnc_sim_parse_program(sim, program, sizeof(program) - 1); + bool ok = true; + ok &= expect(config_rc == 0, cnc_sim_last_error(sim)); + ok &= expect(rc == 0, cnc_sim_last_error(sim)); + ok &= expect(events.size() >= 8, "expected at least 8 events"); + ok &= expect(events[0].type == CNC_SIM_EVENT_SET_PLANE || events[0].type == CNC_SIM_EVENT_SET_UNITS, + "expected modal setup event first"); + + bool saw_rapid = false; + bool saw_feed = false; + bool saw_arc = false; + bool saw_dwell = false; + bool saw_end = false; + bool saw_units = false; + bool saw_plane = false; + bool saw_rtcp = false; + bool saw_m428 = false; + bool saw_m429 = false; + bool saw_m430 = false; + bool saw_g49 = false; + bool saw_g434 = false; + bool saw_incremental_move = false; + bool saw_r_arc_center = false; + for (const auto &event : events) { + saw_rapid = saw_rapid || event.type == CNC_SIM_EVENT_RAPID; + saw_feed = saw_feed || event.type == CNC_SIM_EVENT_LINEAR_FEED; + saw_arc = saw_arc || event.type == CNC_SIM_EVENT_ARC_FEED; + saw_dwell = saw_dwell || event.type == CNC_SIM_EVENT_DWELL; + saw_end = saw_end || event.type == CNC_SIM_EVENT_PROGRAM_END; + saw_units = saw_units || event.type == CNC_SIM_EVENT_SET_UNITS; + saw_plane = saw_plane || event.type == CNC_SIM_EVENT_SET_PLANE; + saw_rtcp = saw_rtcp || + (event.type == CNC_SIM_EVENT_RTCP_PIVOT && + event.line == 7 && + event.end.z == 130.0); + saw_m428 = saw_m428 || + (event.type == CNC_SIM_EVENT_KINEMATICS_SWITCH && + event.line == 3 && + event.reserved == 1 && + event.feed == 1.0); + saw_m429 = saw_m429 || + (event.type == CNC_SIM_EVENT_KINEMATICS_SWITCH && + event.line == 3 && + event.reserved == 0 && + event.feed == 0.0); + saw_m430 = saw_m430 || + (event.type == CNC_SIM_EVENT_KINEMATICS_SWITCH && + event.line == 3 && + event.reserved == 2 && + event.feed == 1.0); + saw_g49 = saw_g49 || + (event.type == CNC_SIM_EVENT_RTCP_STATE && + event.line == 4 && + event.feed == 0.0); + saw_g434 = saw_g434 || + (event.type == CNC_SIM_EVENT_RTCP_STATE && + event.line == 5 && + event.feed == 1.0 && + event.tool == 7 && + event.dwell_seconds == 125.0); + saw_incremental_move = saw_incremental_move || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.start.x == 20.0 && event.end.x == 25.0); + saw_r_arc_center = saw_r_arc_center || + (event.type == CNC_SIM_EVENT_ARC_FEED && + event.line == 14 && + (event.center.x != event.start.x || event.center.y != event.start.y)); + } + ok &= expect(saw_rapid, "expected rapid event"); + ok &= expect(saw_feed, "expected linear feed event"); + ok &= expect(saw_arc, "expected arc feed event"); + ok &= expect(saw_dwell, "expected dwell event"); + ok &= expect(saw_end, "expected program end event"); + ok &= expect(saw_units, "expected units event from multi-G line"); + ok &= expect(saw_plane, "expected plane event from multi-G line"); + ok &= expect(saw_rtcp, "expected RTCP pivot event from config"); + ok &= expect(saw_m428, "expected M428 original kinematics switch"); + ok &= expect(saw_m429, "expected M429 identity kinematics switch"); + ok &= expect(saw_m430, "expected M430 five-axis BC kinematics switch"); + ok &= expect(saw_g49, "expected G49 RTCP off state"); + ok &= expect(saw_g434, "expected G43.4 RTCP on state with H code"); + ok &= expect(saw_incremental_move, "expected G91 incremental move"); + ok &= expect(saw_r_arc_center, "expected R arc center calculation"); + + const char linuxcnc_config[] = "{\"backend\":\"linuxcnc-rs274\"}"; + config_rc = cnc_sim_load_config_json(sim, linuxcnc_config, sizeof(linuxcnc_config) - 1); + rc = cnc_sim_parse_program(sim, program, sizeof(program) - 1); + ok &= expect(config_rc == 0, cnc_sim_last_error(sim)); + ok &= expect(rc != 0, "expected uncompiled linuxcnc-rs274 backend to fail clearly"); + + cnc_sim_destroy(sim); + return ok ? 0 : 1; +} diff --git a/core/tests/linuxcnc_canon_bridge_smoke.cpp b/core/tests/linuxcnc_canon_bridge_smoke.cpp new file mode 100644 index 0000000..c2ad052 --- /dev/null +++ b/core/tests/linuxcnc_canon_bridge_smoke.cpp @@ -0,0 +1,96 @@ +#include "canon_event_sink.h" +#include "linuxcnc_canon_bridge.h" + +#include "canon.hh" + +#include +#include + +namespace { + +int collect_event(const CncSimEvent *event, void *user_data) { + auto *events = static_cast *>(user_data); + events->push_back(*event); + return 0; +} + +bool expect(bool value, const char *message) { + if (!value) { + std::cerr << "FAIL: " << message << '\n'; + return false; + } + return true; +} + +} // namespace + +int main() { + std::vector events; + CanonEventSink sink; + sink.set_callback(collect_event, &events); + cnc_sim_linuxcnc_set_canon_sink(&sink); + + INIT_CANON(); + USE_LENGTH_UNITS(CANON_UNITS_MM); + SELECT_PLANE(CANON_PLANE::XY); + SET_FEED_RATE(500.0); + SELECT_TOOL(7); + CHANGE_TOOL(); + STRAIGHT_TRAVERSE(10, 0.0, 0.0, 5.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0); + STRAIGHT_FEED(11, 10.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0); + ARC_FEED(12, 10.0, 10.0, 10.0, 5.0, 1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0); + SET_G5X_OFFSET(1, 10.0, 20.0, 30.0, 1.0, 2.0, 3.0, 0.0, 0.0, 0.0); + SET_G92_OFFSET(1.0, 2.0, 3.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0); + SET_XY_ROTATION(15.0); + PROGRAM_END(); + + bool ok = true; + ok &= expect(events.size() >= 8, "expected bridge events"); + ok &= expect(events[0].type == CNC_SIM_EVENT_SET_UNITS, "expected units event"); + ok &= expect(events[1].type == CNC_SIM_EVENT_SET_PLANE, "expected plane event"); + + bool saw_tool = false; + bool saw_rapid = false; + bool saw_feed = false; + bool saw_arc = false; + bool saw_g5x = false; + bool saw_g92 = false; + bool saw_rotation = false; + bool saw_end = false; + for (const auto &event : events) { + saw_tool = saw_tool || (event.type == CNC_SIM_EVENT_TOOL_CHANGE && event.tool == 7); + saw_rapid = saw_rapid || (event.type == CNC_SIM_EVENT_RAPID && event.end.z == 5.0); + saw_feed = saw_feed || (event.type == CNC_SIM_EVENT_LINEAR_FEED && event.end.x == 10.0); + saw_arc = saw_arc || (event.type == CNC_SIM_EVENT_ARC_FEED && + event.end.y == 10.0 && + event.center.x == 10.0 && + event.center.y == 5.0); + saw_g5x = saw_g5x || (event.type == CNC_SIM_EVENT_SET_G5X_OFFSET && + event.tool == 1 && + event.start.x == 10.0 && + event.start.y == 20.0 && + event.start.z == 30.0 && + event.start.a == 1.0 && + event.start.b == 2.0 && + event.start.c == 3.0); + saw_g92 = saw_g92 || (event.type == CNC_SIM_EVENT_SET_G92_OFFSET && + event.start.x == 1.0 && + event.start.y == 2.0 && + event.start.z == 3.0); + saw_rotation = saw_rotation || (event.type == CNC_SIM_EVENT_SET_XY_ROTATION && + event.feed == 15.0); + saw_end = saw_end || event.type == CNC_SIM_EVENT_PROGRAM_END; + } + + ok &= expect(saw_tool, "expected tool change"); + ok &= expect(saw_rapid, "expected rapid move"); + ok &= expect(saw_feed, "expected linear feed"); + ok &= expect(saw_arc, "expected arc feed"); + ok &= expect(saw_g5x, "expected G5X offset event"); + ok &= expect(saw_g92, "expected G92 offset event"); + ok &= expect(saw_rotation, "expected XY rotation event"); + ok &= expect(saw_end, "expected program end"); + + cnc_sim_linuxcnc_set_canon_sink(nullptr); + return ok ? 0 : 1; +} diff --git a/core/tests/rtcp_kinematics_smoke.cpp b/core/tests/rtcp_kinematics_smoke.cpp new file mode 100644 index 0000000..f191e16 --- /dev/null +++ b/core/tests/rtcp_kinematics_smoke.cpp @@ -0,0 +1,67 @@ +#include "rtcp_kinematics.h" + +#include +#include + +namespace { + +bool near(double actual, double expected) { + return std::fabs(actual - expected) < 1e-9; +} + +bool expect_near(double actual, double expected, const char *message) { + if (!near(actual, expected)) { + std::cerr << "FAIL: " << message << " actual=" << actual << " expected=" << expected << '\n'; + return false; + } + return true; +} + +bool expect_pose_near(const CncSimPose &actual, const CncSimPose &expected, const char *message) { + bool ok = true; + ok &= expect_near(actual.x, expected.x, message); + ok &= expect_near(actual.y, expected.y, message); + ok &= expect_near(actual.z, expected.z, message); + ok &= expect_near(actual.a, expected.a, message); + ok &= expect_near(actual.b, expected.b, message); + ok &= expect_near(actual.c, expected.c, message); + return ok; +} + +} // namespace + +int main() { + bool ok = true; + + CncSimPose tip{}; + tip.x = 10.0; + tip.y = 20.0; + tip.z = 30.0; + + CncSimPose pivot = rtcp_pivot_from_tool_tip(tip, 100.0); + ok &= expect_near(pivot.x, 10.0, "zero-angle pivot x"); + ok &= expect_near(pivot.y, 20.0, "zero-angle pivot y"); + ok &= expect_near(pivot.z, 130.0, "zero-angle pivot z"); + + tip.a = 90.0; + pivot = rtcp_pivot_from_tool_tip(tip, 100.0); + ok &= expect_near(pivot.x, 10.0, "A90 pivot x"); + ok &= expect_near(pivot.y, -80.0, "A90 pivot y"); + ok &= expect_near(pivot.z, 30.0, "A90 pivot z"); + + tip.a = 0.0; + tip.b = 90.0; + pivot = rtcp_pivot_from_tool_tip(tip, 100.0); + ok &= expect_near(pivot.x, 110.0, "B90 pivot x"); + ok &= expect_near(pivot.y, 20.0, "B90 pivot y"); + ok &= expect_near(pivot.z, 30.0, "B90 pivot z"); + + tip.a = 35.0; + tip.b = -20.0; + tip.c = 15.0; + pivot = rtcp_pivot_from_tool_tip(tip, 123.4); + const CncSimPose roundtrip = rtcp_tool_tip_from_pivot(pivot, 123.4); + ok &= expect_pose_near(roundtrip, tip, "RTCP pivot/tool-tip roundtrip"); + + return ok ? 0 : 1; +} diff --git a/core/tests/simulator_gcode_controls_smoke.cpp b/core/tests/simulator_gcode_controls_smoke.cpp new file mode 100644 index 0000000..a694ce2 --- /dev/null +++ b/core/tests/simulator_gcode_controls_smoke.cpp @@ -0,0 +1,59 @@ +#include "simulator_gcode_controls.h" + +#include +#include + +namespace { + +bool expect(bool value, const char *message) { + if (!value) { + std::cerr << "FAIL: " << message << '\n'; + return false; + } + return true; +} + +} // namespace + +int main() { + bool ok = true; + std::vector actions; + + ok &= expect(parse_simulator_gcode_control_line("M428 M429 M430 ; switch modes", &actions), + "expected M428/M429/M430 control line"); + ok &= expect(actions.size() == 3, "expected three kinematics actions"); + ok &= expect(actions[0].kind == SimulatorGcodeControlKind::KinematicsSwitch && + actions[0].value == 1 && + actions[0].rtcp_enabled, + "expected M428 original kinematics action"); + ok &= expect(actions[1].kind == SimulatorGcodeControlKind::KinematicsSwitch && + actions[1].value == 0 && + !actions[1].rtcp_enabled, + "expected M429 identity kinematics action"); + ok &= expect(actions[2].kind == SimulatorGcodeControlKind::KinematicsSwitch && + actions[2].value == 2 && + actions[2].rtcp_enabled, + "expected M430 five-axis BC kinematics action"); + + ok &= expect(parse_simulator_gcode_control_line("G49 (cancel RTCP)", &actions), + "expected G49 control line"); + ok &= expect(actions.size() == 1 && + actions[0].kind == SimulatorGcodeControlKind::RtcpState && + !actions[0].rtcp_enabled && + actions[0].h_code == 0, + "expected G49 RTCP off action"); + + ok &= expect(parse_simulator_gcode_control_line("G43.4 H7", &actions), + "expected G43.4 H7 control line"); + ok &= expect(actions.size() == 1 && + actions[0].kind == SimulatorGcodeControlKind::RtcpState && + actions[0].rtcp_enabled && + actions[0].h_code == 7, + "expected G43.4 RTCP on action with H7"); + + ok &= expect(!parse_simulator_gcode_control_line("G43.4 H7 X10", &actions), + "expected mixed RTCP/motion line to fall through to interpreter"); + ok &= expect(actions.empty(), "expected no actions for mixed RTCP/motion line"); + + return ok ? 0 : 1; +} diff --git a/core/tools/cnc_sim_dump.cpp b/core/tools/cnc_sim_dump.cpp new file mode 100644 index 0000000..7920ac0 --- /dev/null +++ b/core/tools/cnc_sim_dump.cpp @@ -0,0 +1,120 @@ +#include "cnc_sim_api.h" + +#include +#include +#include + +namespace { + +const char *event_type_name(CncSimEventType type) { + switch (type) { + case CNC_SIM_EVENT_ERROR: + return "error"; + case CNC_SIM_EVENT_COMMENT: + return "comment"; + case CNC_SIM_EVENT_SET_UNITS: + return "set-units"; + case CNC_SIM_EVENT_SET_PLANE: + return "set-plane"; + case CNC_SIM_EVENT_SET_FEED: + return "set-feed"; + case CNC_SIM_EVENT_SET_SPINDLE: + return "set-spindle"; + case CNC_SIM_EVENT_TOOL_CHANGE: + return "tool-change"; + case CNC_SIM_EVENT_DWELL: + return "dwell"; + case CNC_SIM_EVENT_RAPID: + return "rapid"; + case CNC_SIM_EVENT_LINEAR_FEED: + return "linear-feed"; + case CNC_SIM_EVENT_ARC_FEED: + return "arc-feed"; + case CNC_SIM_EVENT_PROBE: + return "probe"; + case CNC_SIM_EVENT_PROGRAM_END: + return "program-end"; + case CNC_SIM_EVENT_RTCP_PIVOT: + return "rtcp-pivot"; + case CNC_SIM_EVENT_KINEMATICS_SWITCH: + return "kinematics-switch"; + case CNC_SIM_EVENT_RTCP_STATE: + return "rtcp-state"; + case CNC_SIM_EVENT_SET_G5X_OFFSET: + return "set-g5x-offset"; + case CNC_SIM_EVENT_SET_G92_OFFSET: + return "set-g92-offset"; + case CNC_SIM_EVENT_SET_XY_ROTATION: + return "set-xy-rotation"; + default: + return "none"; + } +} + +void print_pose(const CncSimPose &pose) { + std::cout << "{\"x\":" << pose.x + << ",\"y\":" << pose.y + << ",\"z\":" << pose.z + << ",\"a\":" << pose.a + << ",\"b\":" << pose.b + << ",\"c\":" << pose.c + << ",\"u\":" << pose.u + << ",\"v\":" << pose.v + << ",\"w\":" << pose.w << '}'; +} + +int print_event(const CncSimEvent *event, void *user_data) { + auto *first = static_cast(user_data); + if (!*first) { + std::cout << ",\n"; + } + *first = false; + std::cout << " {\"type\":\"" << event_type_name(event->type) + << "\",\"line\":" << event->line + << ",\"plane\":" << event->plane + << ",\"tool\":" << event->tool + << ",\"feed\":" << event->feed + << ",\"spindle\":" << event->spindle + << ",\"dwellSeconds\":" << event->dwell_seconds + << ",\"arcTurns\":" << event->arc_turns + << ",\"reserved\":" << event->reserved + << ",\"start\":"; + print_pose(event->start); + std::cout << ",\"end\":"; + print_pose(event->end); + std::cout << ",\"center\":"; + print_pose(event->center); + std::cout << '}'; + return 0; +} + +std::string read_all(const char *path) { + if (!path || std::string(path) == "-") { + std::ostringstream out; + out << std::cin.rdbuf(); + return out.str(); + } + std::ifstream file(path); + std::ostringstream out; + out << file.rdbuf(); + return out.str(); +} + +} // namespace + +int main(int argc, char **argv) { + std::string program = read_all(argc > 1 ? argv[1] : "-"); + CncSimHandle *sim = cnc_sim_create(); + bool first = true; + cnc_sim_set_event_callback(sim, print_event, &first); + + std::cout << "[\n"; + const int rc = cnc_sim_parse_program(sim, program.c_str(), program.size()); + std::cout << "\n]\n"; + + if (rc != 0) { + std::cerr << cnc_sim_last_error(sim) << '\n'; + } + cnc_sim_destroy(sim); + return rc == 0 ? 0 : 1; +} diff --git a/core/tools/linuxcnc_rs274_dump.cpp b/core/tools/linuxcnc_rs274_dump.cpp new file mode 100644 index 0000000..0fd20b6 --- /dev/null +++ b/core/tools/linuxcnc_rs274_dump.cpp @@ -0,0 +1,250 @@ +#include + +#include "canon_event_sink.h" +#include "linuxcnc_canon_bridge.h" +#include "simulator_gcode_controls.h" + +#include "linuxcnc.h" +#include "nml_intf/interp_return.hh" +#include "nml_intf/canon.hh" +#include "rs274ngc/interp_base.hh" +#include "emc/tooldata/tooldata.hh" + +#include +#include +#include +#include +#include + +int _task = 0; +char _parameter_file_name[LINELEN]; + +extern "C" PyObject *PyInit_interpreter(void); +extern "C" PyObject *PyInit_emccanon(void); +extern "C" struct _inittab builtin_modules[]; +struct _inittab builtin_modules[] = { + {"interpreter", PyInit_interpreter}, + {"emccanon", PyInit_emccanon}, + {nullptr, nullptr}, +}; + +namespace { + +const char *event_type_name(CncSimEventType type) { + switch (type) { + case CNC_SIM_EVENT_ERROR: + return "error"; + case CNC_SIM_EVENT_COMMENT: + return "comment"; + case CNC_SIM_EVENT_SET_UNITS: + return "set-units"; + case CNC_SIM_EVENT_SET_PLANE: + return "set-plane"; + case CNC_SIM_EVENT_SET_FEED: + return "set-feed"; + case CNC_SIM_EVENT_SET_SPINDLE: + return "set-spindle"; + case CNC_SIM_EVENT_TOOL_CHANGE: + return "tool-change"; + case CNC_SIM_EVENT_DWELL: + return "dwell"; + case CNC_SIM_EVENT_RAPID: + return "rapid"; + case CNC_SIM_EVENT_LINEAR_FEED: + return "linear-feed"; + case CNC_SIM_EVENT_ARC_FEED: + return "arc-feed"; + case CNC_SIM_EVENT_PROBE: + return "probe"; + case CNC_SIM_EVENT_PROGRAM_END: + return "program-end"; + case CNC_SIM_EVENT_RTCP_PIVOT: + return "rtcp-pivot"; + case CNC_SIM_EVENT_KINEMATICS_SWITCH: + return "kinematics-switch"; + case CNC_SIM_EVENT_RTCP_STATE: + return "rtcp-state"; + case CNC_SIM_EVENT_SET_G5X_OFFSET: + return "set-g5x-offset"; + case CNC_SIM_EVENT_SET_G92_OFFSET: + return "set-g92-offset"; + case CNC_SIM_EVENT_SET_XY_ROTATION: + return "set-xy-rotation"; + default: + return "none"; + } +} + +void print_pose(const CncSimPose &pose) { + std::cout << "{\"x\":" << pose.x + << ",\"y\":" << pose.y + << ",\"z\":" << pose.z + << ",\"a\":" << pose.a + << ",\"b\":" << pose.b + << ",\"c\":" << pose.c + << ",\"u\":" << pose.u + << ",\"v\":" << pose.v + << ",\"w\":" << pose.w << '}'; +} + +int print_event(const CncSimEvent *event, void *user_data) { + auto *first = static_cast(user_data); + if (!*first) { + std::cout << ",\n"; + } + *first = false; + std::cout << " {\"type\":\"" << event_type_name(event->type) + << "\",\"line\":" << event->line + << ",\"plane\":" << event->plane + << ",\"tool\":" << event->tool + << ",\"feed\":" << event->feed + << ",\"spindle\":" << event->spindle + << ",\"dwellSeconds\":" << event->dwell_seconds + << ",\"arcTurns\":" << event->arc_turns + << ",\"reserved\":" << event->reserved + << ",\"start\":"; + print_pose(event->start); + std::cout << ",\"end\":"; + print_pose(event->end); + std::cout << ",\"center\":"; + print_pose(event->center); + std::cout << '}'; + return 0; +} + +std::string read_all(const char *path) { + if (!path || std::string(path) == "-") { + std::ostringstream out; + out << std::cin.rdbuf(); + return out.str(); + } + std::ifstream file(path); + std::ostringstream out; + out << file.rdbuf(); + return out.str(); +} + +bool trace_enabled() { + return std::getenv("CNC_SIM_TRACE_RS274") != nullptr; +} + +bool execute_line(InterpBase *interp, + CanonEventSink &sink, + const std::string &line, + int line_number, + bool *program_done) { + std::vector control_actions; + if (parse_simulator_gcode_control_line(line, &control_actions)) { + for (const auto &action : control_actions) { + emit_simulator_gcode_control_action(sink, action, line_number); + if (sink.callback_aborted()) { + std::cerr << "event callback aborted parsing\n"; + return false; + } + } + return true; + } + + if (trace_enabled()) { + std::cerr << "rs274:read line " << line_number << ": " << line << '\n'; + } + const int read_rc = interp->read(line.c_str()); + if (read_rc == INTERP_ENDFILE || read_rc == INTERP_EXIT) { + *program_done = true; + return true; + } + if (read_rc != 0) { + char error[1024]{}; + interp->error_text(read_rc, error, sizeof(error)); + std::cerr << "read failed: " << error << '\n'; + return false; + } + if (trace_enabled()) { + std::cerr << "rs274:execute line " << line_number << '\n'; + } + const int exec_rc = interp->execute(); + if (exec_rc == INTERP_EXIT || exec_rc == INTERP_ENDFILE) { + *program_done = true; + return true; + } + if (exec_rc == INTERP_EXECUTE_FINISH) { + return true; + } + if (exec_rc != 0) { + char error[1024]{}; + interp->error_text(exec_rc, error, sizeof(error)); + std::cerr << "execute failed: " << error << '\n'; + return false; + } + if (trace_enabled()) { + std::cerr << "rs274:done line " << line_number << '\n'; + } + return true; +} + +void init_minimal_tooldata() { + tool_mmap_creator(nullptr, 0); + tooldata_reset(); + + CANON_TOOL_TABLE spindle = tooldata_entry_init(); + spindle.toolno = 0; + spindle.pocketno = 0; + tooldata_put(spindle, 0); + + CANON_TOOL_TABLE tool = tooldata_entry_init(); + tool.toolno = 1; + tool.pocketno = 1; + tool.diameter = 6.0; + tooldata_put(tool, 1); +} + +} // namespace + +int main(int argc, char **argv) { + const std::string program = read_all(argc > 1 ? argv[1] : "-"); + if (const char *parameter_file = std::getenv("CNC_SIM_RS274_VAR")) { + SET_PARAMETER_FILE_NAME(parameter_file); + } + init_minimal_tooldata(); + + CanonEventSink sink; + bool first = true; + sink.set_callback(print_event, &first); + cnc_sim_linuxcnc_set_canon_sink(&sink); + + std::cout << "[\n"; + InterpBase *interp = makeInterp(); + if (trace_enabled()) { + std::cerr << "rs274:init\n"; + } + const int init_rc = interp->init(); + if (init_rc != 0) { + char error[1024]{}; + interp->error_text(init_rc, error, sizeof(error)); + std::cerr << "init failed: " << error << '\n'; + std::cout << "\n]\n"; + return 1; + } + if (trace_enabled()) { + std::cerr << "rs274:init done\n"; + } + + std::istringstream input(program); + std::string line; + bool ok = true; + bool program_done = false; + int line_number = 0; + while (!program_done && std::getline(input, line)) { + ++line_number; + if (!execute_line(interp, sink, line, line_number, &program_done)) { + ok = false; + break; + } + } + std::cout << "\n]\n"; + + interp->exit(); + delete interp; + cnc_sim_linuxcnc_set_canon_sink(nullptr); + return ok ? 0 : 1; +} diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..1bcd1b3 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,80 @@ +# WASM CNC simulator architecture + +## Native code boundary + +The wasm core should expose a small C ABI and hide all LinuxCNC internals. The browser should never call LinuxCNC classes directly. This keeps the UI independent from whether the backend is LinuxCNC RS274, Fanuc preprocessing, Siemens preprocessing, or a future independent interpreter. + +Current ABI entry points: + +- `cnc_sim_create` +- `cnc_sim_destroy` +- `cnc_sim_reset` +- `cnc_sim_set_dialect` +- `cnc_sim_load_config_json` +- `cnc_sim_parse_program` +- `cnc_sim_last_error` + +The event callback emits normalized `CncSimEvent` records. JavaScript can transform those records into JSON, binary buffers, or renderable typed arrays. + +## LinuxCNC integration plan + +1. Build a native `CanonEventSink` that implements all functions declared in `canon.hh`. +2. Link the sink with `src/emc/rs274ngc` and `src/emc/nml_intf` instead of the task controller. +3. Stub or remove Python remap support for the first browser target. +4. Compile with Emscripten after replacing `dlopen`, Python, HAL and filesystem-only features. +5. Compare event output with native LinuxCNC using the same G-code corpus. + +## Dialect expansion + +LinuxCNC support should be the baseline. Fanuc and Siemens support should be implemented as dialect adapters, not by forking the simulator core. + +Fanuc high-priority items: + +- Macro B variables and expression semantics +- `G65`, `G66`, `G67` macro calls +- common fixed cycles +- cutter compensation and work offsets +- lathe cycles where required + +Siemens high-priority items: + +- named variables and arithmetic expressions +- `CYCLE*` canned cycles +- `TRANS`, `ROT`, `SCALE`, `MIRROR` +- `TRAORI` and `CYCLE800` +- frame and workpiece coordinate transforms + +## Commercial simulator parity + +Feature parity needs more than G-code parsing: + +- exact toolpath display with modal state inspection +- configurable machine kinematics and limits +- holder, fixture and stock collision detection +- material removal simulation +- time estimation with acceleration and lookahead +- diagnostics for unsupported controller-specific words +- reproducible comparison tests for each controller dialect + +## Five-axis and RTCP + +The first RTCP implementation is a geometry kernel, not the full LinuxCNC motion +controller. It treats programmed XYZ as the tool-center point, applies A/B/C +orientation to a local tool vector, and computes the compensated pivot/spindle +point needed to keep the tool tip fixed. + +RTCP is opt-in through config JSON. When enabled, the original motion events +remain programmed tool-tip motion and an additional `rtcp-pivot` event is emitted +for each rapid/feed/arc event. This keeps the ABI useful for both toolpath +display and machine-axis/pivot visualization. + +Current files: + +- `core/src/rtcp_kinematics.h` +- `core/src/rtcp_kinematics.cpp` +- `core/tests/rtcp_kinematics_smoke.cpp` + +The next integration step is to add machine configuration for rotary topology, +pivot offsets, tool length sources, and rotation order, then apply RTCP +compensation while converting LinuxCNC Canon motion events into renderable +machine/tool-tip trajectories. diff --git a/docs/linuxcnc-porting.md b/docs/linuxcnc-porting.md new file mode 100644 index 0000000..8c7924c --- /dev/null +++ b/docs/linuxcnc-porting.md @@ -0,0 +1,194 @@ +# LinuxCNC to WASM porting steps + +This file defines the incremental path for replacing the temporary smoke parser with the LinuxCNC interpreter. + +## Step 0: Stable simulator ABI + +Status: done. + +The browser calls only `cnc_sim_*` functions from `core/include/cnc_sim_api.h`. This ABI remains stable while the backend changes. + +Test: + +```bash +g++ -std=c++17 -I core/include core/src/cnc_sim_api.cpp core/tests/cnc_sim_api_smoke.cpp -o /tmp/cnc_sim_api_smoke +/tmp/cnc_sim_api_smoke +``` + +## Step 1: Temporary event parser + +Status: in progress. + +This parser only exists to test the ABI and UI before Emscripten and LinuxCNC are wired in. It currently handles: + +- multiple G/M words on one line +- `G0`, `G1`, `G2`, `G3` +- `G17`, `G18`, `G19` +- `G20`, `G21`, `G70`, `G71` +- `G90`, `G91` +- `G4 P...` +- `F`, `S`, `T`, `M3`, `M4`, `M5`, `M6`, `M2`, `M30` +- IJK and R arcs + +It is not the production interpreter. + +## Step 2: Canon event sink + +Status: started. + +Create a C++ file that implements the Canon functions declared by LinuxCNC `src/emc/nml_intf/canon.hh`. + +High-priority callbacks: + +- `INIT_CANON` +- `USE_LENGTH_UNITS` +- `SELECT_PLANE` +- `SET_FEED_RATE` +- `SET_SPINDLE_SPEED` +- `SELECT_TOOL` +- `CHANGE_TOOL` +- `STRAIGHT_TRAVERSE` +- `STRAIGHT_FEED` +- `ARC_FEED` +- `DWELL` +- `PROGRAM_END` +- `FINISH` + +The sink will translate these callbacks to `CncSimEvent`. + +Current bridge files: + +- `core/src/canon_event_sink.h` +- `core/src/canon_event_sink.cpp` +- `core/src/linuxcnc_canon_bridge.h` +- `core/src/linuxcnc_canon_bridge.cpp` + +The LinuxCNC bridge is optional and not built by default: + +```bash +cmake -S core -B build/native-linuxcnc \ + -DCNC_SIM_ENABLE_LINUXCNC_BRIDGE=ON \ + -DCNC_SIM_LINUXCNC_ROOT=/path/to/linuxcnc +``` + +This bridge is intentionally thin. The production parser still needs the `rs274ngc` interpreter linked on top of it. + +Bridge smoke test: + +```bash +./test-linuxcnc-bridge-native.sh +``` + +Set `LINUXCNC_ROOT=/path/to/linuxcnc` if the LinuxCNC source tree is not next to `wasm-simulator`. + +## Step 3a: Native `librs274` runner + +Status: started. + +Before porting `rs274ngc` sources to wasm, use the already-built native LinuxCNC `librs274` to validate the bridge and event schema: + +```bash +./test-linuxcnc-rs274-native.sh +``` + +This builds `core/tools/linuxcnc_rs274_dump.cpp`, links LinuxCNC `librs274`, and writes: + +- `/tmp/cnc_sim_linuxcnc_basic_motion.json` +- `/tmp/cnc_sim_linuxcnc_basic_mill.json` + +This is a native-only stepping stone. Once stable, the same event sink is used by the wasm build. + +`T... M6` now works in the native runner after initializing LinuxCNC mmap tooldata with a minimal tool table. + +## Step 3b: API-level native `librs274` backend + +Status: started. + +The public `cnc_sim_api` can now select backends through config JSON: + +```json +{"backend":"smoke"} +``` + +or, in a native build compiled with `CNC_SIM_ENABLE_LINUXCNC_RS274_BACKEND`: + +```json +{"backend":"linuxcnc-rs274"} +``` + +API-level native smoke: + +```bash +./test-linuxcnc-api-native.sh +``` + +## Step 4a: Source compilation map + +Status: started. + +Before replacing `librs274` with source-level compilation, keep the source manifest and syntax probe green: + +```bash +./test-linuxcnc-source-syntax.sh +./test-linuxcnc-source-objects.sh +``` + +The manifest is: + +- `linuxcnc-rs274-source-files.txt` +- `docs/linuxcnc-rs274-source-map.md` + +The first compile condition discovered is that LinuxCNC user-space source probes need `-DULAPI`. + +## Step 3: Native LinuxCNC interpreter comparison + +Build a native adapter that links: + +- `src/emc/rs274ngc` +- `src/emc/nml_intf` +- the Canon event sink + +Then run the same G-code corpus through both the temporary parser and LinuxCNC-backed parser. Numeric differences are expected around arc canonicalization; they must be recorded and bounded. + +## Step 4: Remove browser-hostile dependencies + +LinuxCNC interpreter sources currently involve Python/Boost.Python remap paths. For the first WASM target: + +- disable Python remap +- disable dynamic module loading +- replace file-backed parameter persistence with in-memory buffers +- provide browser-safe tool table and INI/config loading through JSON + +## Step 5: Emscripten build + +Generate: + +- `web/public/cnc_sim.js` +- `web/public/cnc_sim.wasm` + +Command: + +```bash +./build-wasm.sh +``` + +## Step 6: Controller dialects + +Fanuc and Siemens support should remain outside the LinuxCNC core as preprocessors/adapters. They normalize controller-specific constructs into the internal event/interpreter input layer. + +## Functional parity matrix + +This matrix tracks LinuxCNC feature coverage for the web/WASM simulator. A feature is not considered covered until it has a native regression in at least the `librs274` runner and the source-link runner. + +| Area | Status | Regression | +| --- | --- | --- | +| Basic modal motion `G0/G1/G2/G3` | covered | `tests/gcode/linuxcnc_basic_motion.ngc`, `tests/gcode/basic_mill.ngc` | +| Tool select/change `T... M6` | covered with minimal native tooldata | `tests/gcode/basic_mill.ngc` | +| RTCP controls `G43.4/G43.5/G49` | covered as simulator-owned control lines | `tests/gcode/linuxcnc_rtcp_controls.ngc` | +| Kinematics switch `M428/M429/M430` | covered as simulator-owned control lines | `tests/gcode/linuxcnc_rtcp_controls.ngc` | +| Canned cycle `G81/G80` | covered for drilling expand-to-canon path | `tests/gcode/linuxcnc_canned_cycle.ngc` | +| Coordinate offset Canon events | bridge-level covered | `core/tests/linuxcnc_canon_bridge_smoke.cpp` | +| Cutter compensation | pending | add LinuxCNC corpus and tolerance checks | +| O-word subroutines and calls | pending | current line-by-line runner does not execute sub bodies like LinuxCNC task planner | +| Broader canned cycles `G82`-`G89` | pending | add corpus after `G81` baseline | +| Full source-level wasm build | pending | replace Python/HAL/INI/tooldata support dependencies | diff --git a/docs/linuxcnc-rs274-source-map.md b/docs/linuxcnc-rs274-source-map.md new file mode 100644 index 0000000..4ed293c --- /dev/null +++ b/docs/linuxcnc-rs274-source-map.md @@ -0,0 +1,125 @@ +# LinuxCNC rs274 source map + +This is the working map for moving from native `librs274` linking to source-level compilation and then to wasm. + +## Compile assumptions + +Native user-space syntax checks require: + +- `-DULAPI` +- LinuxCNC include roots: + - `linuxcnc/src` + - `linuxcnc/src/emc` + - `linuxcnc/src/emc/nml_intf` + - `linuxcnc/src/emc/rs274ngc` + - `linuxcnc/src/emc/motion` + - `linuxcnc/src/emc/pythonplugin` + - `linuxcnc/include` +- Python development headers for current upstream sources, because the interpreter still embeds Python/Boost.Python paths. + +## Source groups + +### Interpreter core + +These are the first group to keep compiling while we peel away native-only dependencies: + +- `interp_arc.cc` +- `interp_array.cc` +- `interp_base.cc` +- `interp_check.cc` +- `interp_convert.cc` +- `interp_cycles.cc` +- `interp_execute.cc` +- `interp_find.cc` +- `interp_g7x.cc` +- `interp_inspection.cc` +- `interp_internal.cc` +- `interp_inverse.cc` +- `interp_namedparams.cc` +- `interp_o_word.cc` +- `interp_python.cc` +- `interp_queue.cc` +- `interp_read.cc` +- `interp_remap.cc` +- `interp_setup.cc` +- `interp_write.cc` +- `modal_state.cc` +- `nurbs_additional_functions.cc` +- `rs274ngc_pre.cc` + +### Python binding modules + +These are not needed for the browser simulator ABI and should not be part of the wasm core: + +- `canonmodule.cc` +- `gcodemodule.cc` +- `interpmodule.cc` +- `pyarrays.cc` +- `pyblock.cc` +- `pyemctypes.cc` +- `pyinterp1.cc` +- `pyparamclass.cc` + +### Browser-hostile dependencies to replace + +- Python/Boost.Python remap and named parameter hooks. +- `dlopen`/`dlsym` interpreter loading in `interp_base.cc`. +- mmap-backed `tooldata_mmap.cc`. +- persistent parameter file writes. +- dynamic INI/HAL queries. + +## Current syntax probe + +Run: + +```bash +./test-linuxcnc-source-syntax.sh +``` + +The probe checks a representative subset of source files with `-DULAPI`. It is not a full source build yet; it is a guardrail before the full source backend is introduced. + +## Current object probe + +Run: + +```bash +./test-linuxcnc-source-objects.sh +``` + +This compiles every `core:` entry in `linuxcnc-rs274-source-files.txt` into object files. As of this step, the core interpreter sources compile to `.o` in the native environment with `-DULAPI`. + +The next boundary is linking. Expected link risks: + +- `PythonPlugin` and Boost.Python symbols from remap/named parameter paths. +- Python module initialization symbols if the source backend reuses the existing builtin module setup. +- `tooldata_*` implementations, currently mmap-backed in native LinuxCNC and unsuitable for wasm. +- dynamic loader code in `interp_base.cc`. +- parameter file persistence in `rs274ngc_pre.cc`. + +## Current source-link probe + +Run: + +```bash +./test-linuxcnc-source-link.sh +``` + +This compiles every `core:` interpreter source into local objects and links the +native runner without `librs274`. It still uses built LinuxCNC support objects +and libraries for the dependencies that have not been replaced yet: + +- Boost.Python binding module initializers required by the current interpreter constructor. +- `libpyplugin` for Python remap and named-parameter hooks. +- `liblinuxcncini` and `liblinuxcnchal` for INI/HAL named parameter paths. +- `liblinuxcnc-uspace-posix` for `rtapi_*` user-space helpers. +- `libtooldata` for the current mmap-backed native tool table. + +This is not wasm-ready yet, but it proves the simulator can own and compile the +RS274 interpreter core sources directly. The next source-port boundary is to +replace each support dependency with browser-safe shims instead of pulling in +the LinuxCNC task, motion, HAL, and Python runtime layers. + +The native runner used by this probe now recognizes simulator-owned control +lines before handing code to LinuxCNC: `M428`, `M429`, `M430`, `G43.4`, +`G43.5`, and `G49`. This keeps the direct runner aligned with the public +`linuxcnc-rs274` API backend for RTCP and kinematics switch tests. diff --git a/linuxcnc-rs274-source-files.txt b/linuxcnc-rs274-source-files.txt new file mode 100644 index 0000000..e393519 --- /dev/null +++ b/linuxcnc-rs274-source-files.txt @@ -0,0 +1,37 @@ +# LinuxCNC rs274 source manifest for the wasm simulator port. +# Format: group:path:note +core:src/emc/rs274ngc/interp_arc.cc:arc geometry and canonical arc conversion +core:src/emc/rs274ngc/interp_array.cc:block array initialization +core:src/emc/rs274ngc/interp_base.cc:base interpreter interface; contains dlopen path to replace +core:src/emc/rs274ngc/interp_check.cc:error text and block checks +core:src/emc/rs274ngc/interp_convert.cc:block-to-canon conversion +core:src/emc/rs274ngc/interp_cycles.cc:canned cycles +core:src/emc/rs274ngc/interp_execute.cc:execute parsed blocks +core:src/emc/rs274ngc/interp_find.cc:geometry and tool lookup helpers +core:src/emc/rs274ngc/interp_g7x.cc:lathe roughing cycles +core:src/emc/rs274ngc/interp_inspection.cc:inspection helpers +core:src/emc/rs274ngc/interp_internal.cc:internal setup and utility logic +core:src/emc/rs274ngc/interp_inverse.cc:inverse time/feed helpers +core:src/emc/rs274ngc/interp_namedparams.cc:named parameters; Python hooks to replace +core:src/emc/rs274ngc/interp_o_word.cc:O-word and subroutine flow +core:src/emc/rs274ngc/interp_python.cc:Python remap support; browser-hostile +core:src/emc/rs274ngc/interp_queue.cc:canonical command queue +core:src/emc/rs274ngc/interp_read.cc:line lexer/parser +core:src/emc/rs274ngc/interp_remap.cc:remap support; Python hooks to replace +core:src/emc/rs274ngc/interp_setup.cc:setup initialization helpers +core:src/emc/rs274ngc/interp_write.cc:modal state writers +core:src/emc/rs274ngc/modal_state.cc:state tag modal representation +core:src/emc/rs274ngc/nurbs_additional_functions.cc:NURBS helpers +core:src/emc/rs274ngc/rs274ngc_pre.cc:Interp class implementation; Python and parameter-file work to replace +binding:src/emc/rs274ngc/canonmodule.cc:Python module; exclude from wasm core +binding:src/emc/rs274ngc/gcodemodule.cc:Python gcode module; exclude from wasm core +binding:src/emc/rs274ngc/interpmodule.cc:Boost.Python module; exclude from wasm core +binding:src/emc/rs274ngc/pyarrays.cc:Boost.Python bindings; exclude from wasm core +binding:src/emc/rs274ngc/pyblock.cc:Boost.Python bindings; exclude from wasm core +binding:src/emc/rs274ngc/pyemctypes.cc:Boost.Python bindings; exclude from wasm core +binding:src/emc/rs274ngc/pyinterp1.cc:Boost.Python bindings; exclude from wasm core +binding:src/emc/rs274ngc/pyparamclass.cc:Boost.Python bindings; exclude from wasm core +tooldata:src/emc/tooldata/tooldata_common.cc:shared tool table parser/format logic +tooldata:src/emc/tooldata/tooldata_mmap.cc:native mmap implementation; replace for wasm +tooldata:src/emc/tooldata/tooldata_db.cc:external database integration; exclude from wasm core + diff --git a/rs274ngc.var b/rs274ngc.var new file mode 100644 index 0000000..1776377 --- /dev/null +++ b/rs274ngc.var @@ -0,0 +1,119 @@ +5161 0.000000 +5162 0.000000 +5163 0.000000 +5164 0.000000 +5165 0.000000 +5166 0.000000 +5167 0.000000 +5168 0.000000 +5169 0.000000 +5181 0.000000 +5182 0.000000 +5183 0.000000 +5184 0.000000 +5185 0.000000 +5186 0.000000 +5187 0.000000 +5188 0.000000 +5189 0.000000 +5210 0.000000 +5211 0.000000 +5212 0.000000 +5213 0.000000 +5214 0.000000 +5215 0.000000 +5216 0.000000 +5217 0.000000 +5218 0.000000 +5219 0.000000 +5220 1.000000 +5221 0.000000 +5222 0.000000 +5223 0.000000 +5224 0.000000 +5225 0.000000 +5226 0.000000 +5227 0.000000 +5228 0.000000 +5229 0.000000 +5230 0.000000 +5241 0.000000 +5242 0.000000 +5243 0.000000 +5244 0.000000 +5245 0.000000 +5246 0.000000 +5247 0.000000 +5248 0.000000 +5249 0.000000 +5250 0.000000 +5261 0.000000 +5262 0.000000 +5263 0.000000 +5264 0.000000 +5265 0.000000 +5266 0.000000 +5267 0.000000 +5268 0.000000 +5269 0.000000 +5270 0.000000 +5281 0.000000 +5282 0.000000 +5283 0.000000 +5284 0.000000 +5285 0.000000 +5286 0.000000 +5287 0.000000 +5288 0.000000 +5289 0.000000 +5290 0.000000 +5301 0.000000 +5302 0.000000 +5303 0.000000 +5304 0.000000 +5305 0.000000 +5306 0.000000 +5307 0.000000 +5308 0.000000 +5309 0.000000 +5310 0.000000 +5321 0.000000 +5322 0.000000 +5323 0.000000 +5324 0.000000 +5325 0.000000 +5326 0.000000 +5327 0.000000 +5328 0.000000 +5329 0.000000 +5330 0.000000 +5341 0.000000 +5342 0.000000 +5343 0.000000 +5344 0.000000 +5345 0.000000 +5346 0.000000 +5347 0.000000 +5348 0.000000 +5349 0.000000 +5350 0.000000 +5361 0.000000 +5362 0.000000 +5363 0.000000 +5364 0.000000 +5365 0.000000 +5366 0.000000 +5367 0.000000 +5368 0.000000 +5369 0.000000 +5370 0.000000 +5381 0.000000 +5382 0.000000 +5383 0.000000 +5384 0.000000 +5385 0.000000 +5386 0.000000 +5387 0.000000 +5388 0.000000 +5389 0.000000 +5390 0.000000 diff --git a/test-all-native.sh b/test-all-native.sh new file mode 100755 index 0000000..caa236b --- /dev/null +++ b/test-all-native.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")" + +./test-native.sh +./test-linuxcnc-source-syntax.sh +./test-linuxcnc-source-objects.sh +./test-linuxcnc-source-link.sh +./test-linuxcnc-bridge-native.sh +./test-linuxcnc-rs274-native.sh +./test-linuxcnc-api-native.sh + +echo "all native tests passed" diff --git a/test-linuxcnc-api-native.sh b/test-linuxcnc-api-native.sh new file mode 100755 index 0000000..014298c --- /dev/null +++ b/test-linuxcnc-api-native.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")" + +linuxcnc_root=${LINUXCNC_ROOT:-../linuxcnc} +cxx=${CXX:-g++} +build_dir=${TMPDIR:-/tmp}/cnc_sim_native +mkdir -p "$build_dir" +var_file="$build_dir/rs274ngc-api.var" +cp "$linuxcnc_root/tests/halui/jogging/sim.var" "$var_file" + +"$cxx" -std=c++17 \ + -DCNC_SIM_ENABLE_LINUXCNC_RS274_BACKEND \ + $(python3.13-config --includes 2>/dev/null || python3-config --includes) \ + -I core/include \ + -I core/src \ + -I "$linuxcnc_root/src" \ + -I "$linuxcnc_root/src/emc" \ + -I "$linuxcnc_root/src/emc/nml_intf" \ + -I "$linuxcnc_root/src/emc/rs274ngc" \ + -I "$linuxcnc_root/src/emc/motion" \ + -I "$linuxcnc_root/include" \ + core/src/canon_event_sink.cpp \ + core/src/cnc_sim_api.cpp \ + core/src/gcode_backend.cpp \ + core/src/linuxcnc_canon_bridge.cpp \ + core/src/linuxcnc_rs274_backend.cpp \ + core/src/rtcp_kinematics.cpp \ + core/src/simulator_gcode_controls.cpp \ + core/src/smoke_gcode_parser.cpp \ + core/tests/cnc_sim_api_linuxcnc_rs274_smoke.cpp \ + -L "$linuxcnc_root/lib" \ + -Wl,-rpath,"$linuxcnc_root/lib" \ + -lrs274 \ + -ltooldata \ + -lpython3.13 \ + -o "$build_dir/cnc_sim_api_linuxcnc_rs274_smoke" + +CNC_SIM_RS274_VAR="$var_file" "$build_dir/cnc_sim_api_linuxcnc_rs274_smoke" + +echo "linuxcnc rs274 API backend smoke passed" diff --git a/test-linuxcnc-bridge-native.sh b/test-linuxcnc-bridge-native.sh new file mode 100755 index 0000000..878635d --- /dev/null +++ b/test-linuxcnc-bridge-native.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")" + +linuxcnc_root=${LINUXCNC_ROOT:-../linuxcnc} +cxx=${CXX:-g++} +build_dir=${TMPDIR:-/tmp}/cnc_sim_native +mkdir -p "$build_dir" + +"$cxx" -std=c++17 \ + -I core/include \ + -I core/src \ + -I "$linuxcnc_root/src/emc" \ + -I "$linuxcnc_root/src/emc/nml_intf" \ + -I "$linuxcnc_root/src/emc/rs274ngc" \ + -I "$linuxcnc_root/src/emc/motion" \ + -I "$linuxcnc_root/src" \ + -I "$linuxcnc_root/include" \ + core/src/canon_event_sink.cpp \ + core/src/linuxcnc_canon_bridge.cpp \ + core/src/rtcp_kinematics.cpp \ + core/tests/linuxcnc_canon_bridge_smoke.cpp \ + -o "$build_dir/linuxcnc_canon_bridge_smoke" + +"$build_dir/linuxcnc_canon_bridge_smoke" + +echo "linuxcnc canon bridge smoke passed" diff --git a/test-linuxcnc-rs274-native.sh b/test-linuxcnc-rs274-native.sh new file mode 100755 index 0000000..c61f448 --- /dev/null +++ b/test-linuxcnc-rs274-native.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")" + +linuxcnc_root=${LINUXCNC_ROOT:-../linuxcnc} +cxx=${CXX:-g++} +build_dir=${TMPDIR:-/tmp}/cnc_sim_native +mkdir -p "$build_dir" +var_file="$build_dir/rs274ngc.var" +cp "$linuxcnc_root/tests/halui/jogging/sim.var" "$var_file" + +"$cxx" -std=c++17 \ + $(python3.13-config --includes 2>/dev/null || python3-config --includes) \ + -I core/include \ + -I core/src \ + -I "$linuxcnc_root/src" \ + -I "$linuxcnc_root/src/emc" \ + -I "$linuxcnc_root/src/emc/nml_intf" \ + -I "$linuxcnc_root/src/emc/rs274ngc" \ + -I "$linuxcnc_root/src/emc/motion" \ + -I "$linuxcnc_root/include" \ + core/src/canon_event_sink.cpp \ + core/src/linuxcnc_canon_bridge.cpp \ + core/src/rtcp_kinematics.cpp \ + core/src/simulator_gcode_controls.cpp \ + core/tools/linuxcnc_rs274_dump.cpp \ + -L "$linuxcnc_root/lib" \ + -Wl,-rpath,"$linuxcnc_root/lib" \ + -lrs274 \ + -ltooldata \ + -lpython3.13 \ + -o "$build_dir/linuxcnc_rs274_dump" + +CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_basic_motion.ngc >/tmp/cnc_sim_linuxcnc_basic_motion.json +CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/basic_mill.ngc >/tmp/cnc_sim_linuxcnc_basic_mill.json +CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_rtcp_controls.ngc >/tmp/cnc_sim_linuxcnc_rtcp_controls.json +CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_canned_cycle.ngc >/tmp/cnc_sim_linuxcnc_canned_cycle.json +CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_coordinate_offsets.ngc >/tmp/cnc_sim_linuxcnc_coordinate_offsets.json +python3 - <<'PY' +import json +from pathlib import Path + +def check(path, required): + events = json.loads(Path(path).read_text()) + types = [event["type"] for event in events] + missing = sorted(required - set(types)) + if missing: + raise SystemExit(f"{path}: missing events: {', '.join(missing)}") + final_motion = [event for event in events if event["type"] in {"rapid", "linear-feed", "arc-feed"}][-1] + if final_motion["end"]["x"] != 0 or final_motion["end"]["y"] != 0: + raise SystemExit(f"{path}: unexpected final XY position") + return events + +required_motion = {"set-units", "set-plane", "set-spindle", "rapid", "set-feed", "linear-feed", "arc-feed", "dwell", "program-end"} +check("/tmp/cnc_sim_linuxcnc_basic_motion.json", required_motion) +check("/tmp/cnc_sim_linuxcnc_basic_mill.json", required_motion | {"tool-change"}) + +controls = json.loads(Path("/tmp/cnc_sim_linuxcnc_rtcp_controls.json").read_text()) +kin = [event for event in controls if event["type"] == "kinematics-switch"] +if [(event["reserved"], event["feed"]) for event in kin] != [(1, 1), (0, 0), (2, 1)]: + raise SystemExit("unexpected kinematics switch sequence") +rtcp = [event for event in controls if event["type"] == "rtcp-state"] +if [(event["line"], event["tool"], event["feed"]) for event in rtcp] != [(3, 0, 0), (4, 7, 1)]: + raise SystemExit("unexpected RTCP state sequence") + +cycle = json.loads(Path("/tmp/cnc_sim_linuxcnc_canned_cycle.json").read_text()) +motions = [event for event in cycle if event["type"] in {"rapid", "linear-feed"}] +expected_cycle = [ + ("rapid", 0, 0, 5), + ("rapid", 20, 0, 5), + ("rapid", 20, 0, 3), + ("linear-feed", 20, 0, -2), + ("rapid", 20, 0, 3), +] +actual_cycle = [(event["type"], event["end"]["x"], event["end"]["y"], event["end"]["z"]) for event in motions] +if actual_cycle != expected_cycle: + raise SystemExit(f"unexpected G81/G80 expansion: {actual_cycle!r}") + +coords = json.loads(Path("/tmp/cnc_sim_linuxcnc_coordinate_offsets.json").read_text()) +g5x = [event for event in coords if event["type"] == "set-g5x-offset"] +g92 = [event for event in coords if event["type"] == "set-g92-offset"] +rot = [event for event in coords if event["type"] == "set-xy-rotation"] +if not any( + event["tool"] == 1 and + event["start"]["x"] == 12.5 and + event["start"]["y"] == -3 and + event["start"]["z"] == 4 + for event in g5x +): + raise SystemExit("missing G10 L2 G5X offset event") +if not any(event["feed"] == 30 for event in rot): + raise SystemExit("missing G10 L2 XY rotation event") +if not any( + event["start"]["x"] == 0 and + event["start"]["y"] == 0 and + event["start"]["z"] == 0 + for event in g92 +): + raise SystemExit("missing G92.1 clear offset event") +PY + +echo "linuxcnc rs274 native smoke passed" +echo "dumped /tmp/cnc_sim_linuxcnc_basic_motion.json" +echo "dumped /tmp/cnc_sim_linuxcnc_basic_mill.json" +echo "dumped /tmp/cnc_sim_linuxcnc_rtcp_controls.json" +echo "dumped /tmp/cnc_sim_linuxcnc_canned_cycle.json" +echo "dumped /tmp/cnc_sim_linuxcnc_coordinate_offsets.json" diff --git a/test-linuxcnc-source-link.sh b/test-linuxcnc-source-link.sh new file mode 100755 index 0000000..dc1a033 --- /dev/null +++ b/test-linuxcnc-source-link.sh @@ -0,0 +1,157 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")" + +linuxcnc_root=${LINUXCNC_ROOT:-../linuxcnc} +cxx=${CXX:-g++} +build_dir=${TMPDIR:-/tmp}/cnc_sim_rs274_source_link +python_includes=$(python3.13-config --includes 2>/dev/null || python3-config --includes) + +rm -rf "$build_dir" +mkdir -p "$build_dir" + +common_flags=( + -std=c++17 + -DULAPI + $python_includes + -I core/include + -I core/src + -I "$linuxcnc_root/src" + -I "$linuxcnc_root/src/emc" + -I "$linuxcnc_root/src/emc/nml_intf" + -I "$linuxcnc_root/src/emc/rs274ngc" + -I "$linuxcnc_root/src/emc/motion" + -I "$linuxcnc_root/src/emc/pythonplugin" + -I "$linuxcnc_root/include" +) + +objects=() +while IFS=: read -r group path note; do + case "$group" in + core) + obj="$build_dir/$(basename "$path" .cc).o" + "$cxx" "${common_flags[@]}" -c "$linuxcnc_root/$path" -o "$obj" + objects+=("$obj") + ;; + ""|\#*|binding|tooldata) + ;; + *) + echo "unknown manifest group: $group" >&2 + exit 1 + ;; + esac +done < linuxcnc-rs274-source-files.txt + +for source in \ + core/src/canon_event_sink.cpp \ + core/src/linuxcnc_canon_bridge.cpp \ + core/src/rtcp_kinematics.cpp \ + core/src/simulator_gcode_controls.cpp \ + core/tools/linuxcnc_rs274_dump.cpp; do + obj="$build_dir/$(basename "$source" .cpp).o" + "$cxx" "${common_flags[@]}" -c "$source" -o "$obj" + objects+=("$obj") +done + +support_objects=( + "$linuxcnc_root/src/objects/emc/rs274ngc/interpmodule.o" + "$linuxcnc_root/src/objects/emc/rs274ngc/canonmodule.o" + "$linuxcnc_root/src/objects/emc/rs274ngc/pyarrays.o" + "$linuxcnc_root/src/objects/emc/rs274ngc/pyblock.o" + "$linuxcnc_root/src/objects/emc/rs274ngc/pyemctypes.o" + "$linuxcnc_root/src/objects/emc/rs274ngc/pyinterp1.o" + "$linuxcnc_root/src/objects/emc/rs274ngc/pyparamclass.o" + "$linuxcnc_root/src/objects/emc/nml_intf/emcops.o" + "$linuxcnc_root/src/objects/emc/sai/dummyemcstat.o" + "$linuxcnc_root/src/objects/libnml/nml/stat_msg.o" +) + +for obj in "${support_objects[@]}"; do + if [[ ! -f "$obj" ]]; then + echo "missing LinuxCNC support object: $obj" >&2 + echo "build LinuxCNC first or set LINUXCNC_ROOT to a built tree" >&2 + exit 1 + fi +done + +"$cxx" "${objects[@]}" "${support_objects[@]}" \ + -L "$linuxcnc_root/lib" \ + -Wl,-rpath,"$linuxcnc_root/lib" \ + -llinuxcnc-uspace-posix \ + -llinuxcncini \ + -llinuxcnchal \ + -lpyplugin \ + -ltooldata \ + -lnml \ + -lposemath \ + -lboost_python313 \ + -lpython3.13 \ + -lfmt \ + -ldl \ + -o "$build_dir/linuxcnc_rs274_source_dump" + +var_file="$build_dir/rs274ngc-source.var" +cp "$linuxcnc_root/tests/halui/jogging/sim.var" "$var_file" + +CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/basic_mill.ngc \ + >/tmp/cnc_sim_linuxcnc_source_basic_mill.json +CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_rtcp_controls.ngc \ + >/tmp/cnc_sim_linuxcnc_source_rtcp_controls.json +CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_canned_cycle.ngc \ + >/tmp/cnc_sim_linuxcnc_source_canned_cycle.json + +python3 - <<'PY' +import json +from pathlib import Path + +events = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_basic_mill.json").read_text()) +types = {event["type"] for event in events} +required = { + "set-units", + "set-plane", + "set-spindle", + "tool-change", + "rapid", + "set-feed", + "linear-feed", + "arc-feed", + "program-end", +} +missing = sorted(required - types) +if missing: + raise SystemExit("missing source-linked events: " + ", ".join(missing)) + +final_motion = [event for event in events if event["type"] in {"rapid", "linear-feed", "arc-feed"}][-1] +if final_motion["end"]["x"] != 0 or final_motion["end"]["y"] != 0: + raise SystemExit("unexpected source-linked final XY position") + +controls = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_rtcp_controls.json").read_text()) +kin = [event for event in controls if event["type"] == "kinematics-switch"] +if [(event["reserved"], event["feed"]) for event in kin] != [(1, 1), (0, 0), (2, 1)]: + raise SystemExit("unexpected source-linked kinematics switch sequence") +rtcp = [event for event in controls if event["type"] == "rtcp-state"] +if [(event["line"], event["tool"], event["feed"]) for event in rtcp] != [(3, 0, 0), (4, 7, 1)]: + raise SystemExit("unexpected source-linked RTCP state sequence") + +cycle = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_canned_cycle.json").read_text()) +motions = [event for event in cycle if event["type"] in {"rapid", "linear-feed"}] +expected_cycle = [ + ("rapid", 0, 0, 5), + ("rapid", 20, 0, 5), + ("rapid", 20, 0, 3), + ("linear-feed", 20, 0, -2), + ("rapid", 20, 0, 3), +] +actual_cycle = [(event["type"], event["end"]["x"], event["end"]["y"], event["end"]["z"]) for event in motions] +if actual_cycle != expected_cycle: + raise SystemExit(f"unexpected source-linked G81/G80 expansion: {actual_cycle!r}") +PY + +echo "linuxcnc rs274 source link smoke passed (${#objects[@]} local objects)" +echo "dumped /tmp/cnc_sim_linuxcnc_source_basic_mill.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_rtcp_controls.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_canned_cycle.json" diff --git a/test-linuxcnc-source-objects.sh b/test-linuxcnc-source-objects.sh new file mode 100755 index 0000000..aabb3e4 --- /dev/null +++ b/test-linuxcnc-source-objects.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")" + +linuxcnc_root=${LINUXCNC_ROOT:-../linuxcnc} +cxx=${CXX:-g++} +build_dir=${TMPDIR:-/tmp}/cnc_sim_rs274_objects +python_includes=$(python3.13-config --includes 2>/dev/null || python3-config --includes) + +rm -rf "$build_dir" +mkdir -p "$build_dir" + +common_flags=( + -std=c++17 + -DULAPI + -I "$linuxcnc_root/src" + -I "$linuxcnc_root/src/emc" + -I "$linuxcnc_root/src/emc/nml_intf" + -I "$linuxcnc_root/src/emc/rs274ngc" + -I "$linuxcnc_root/src/emc/motion" + -I "$linuxcnc_root/src/emc/pythonplugin" + -I "$linuxcnc_root/include" +) + +sources=() +while IFS=: read -r group path note; do + case "$group" in + core) + sources+=("$path") + ;; + ""|\#*|binding|tooldata) + ;; + *) + echo "unknown manifest group: $group" >&2 + exit 1 + ;; + esac +done < linuxcnc-rs274-source-files.txt + +for source in "${sources[@]}"; do + obj="$build_dir/$(basename "$source" .cc).o" + # shellcheck disable=SC2086 + "$cxx" "${common_flags[@]}" $python_includes -c "$linuxcnc_root/$source" -o "$obj" +done + +echo "linuxcnc rs274 source object build passed (${#sources[@]} objects)" +echo "built objects in $build_dir" + diff --git a/test-linuxcnc-source-syntax.sh b/test-linuxcnc-source-syntax.sh new file mode 100755 index 0000000..917e75d --- /dev/null +++ b/test-linuxcnc-source-syntax.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")" + +linuxcnc_root=${LINUXCNC_ROOT:-../linuxcnc} +cxx=${CXX:-g++} +python_includes=$(python3.13-config --includes 2>/dev/null || python3-config --includes) + +while IFS=: read -r group path note; do + case "$group" in + ""|\#*) continue ;; + esac + if [[ ! -f "$linuxcnc_root/$path" ]]; then + echo "missing manifest file: $path" >&2 + exit 1 + fi +done < linuxcnc-rs274-source-files.txt + +common_flags=( + -std=c++17 + -DULAPI + -fsyntax-only + -I "$linuxcnc_root/src" + -I "$linuxcnc_root/src/emc" + -I "$linuxcnc_root/src/emc/nml_intf" + -I "$linuxcnc_root/src/emc/rs274ngc" + -I "$linuxcnc_root/src/emc/motion" + -I "$linuxcnc_root/src/emc/pythonplugin" + -I "$linuxcnc_root/include" +) + +probe_sources=( + src/emc/rs274ngc/interp_base.cc + src/emc/rs274ngc/modal_state.cc + src/emc/rs274ngc/interp_arc.cc + src/emc/rs274ngc/interp_find.cc + src/emc/rs274ngc/interp_read.cc + src/emc/rs274ngc/nurbs_additional_functions.cc +) + +for source in "${probe_sources[@]}"; do + # shellcheck disable=SC2086 + "$cxx" "${common_flags[@]}" $python_includes "$linuxcnc_root/$source" +done + +echo "linuxcnc rs274 source syntax probe passed" + diff --git a/test-native.sh b/test-native.sh new file mode 100755 index 0000000..0279b87 --- /dev/null +++ b/test-native.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")" + +cxx=${CXX:-g++} +build_dir=${TMPDIR:-/tmp}/cnc_sim_native +mkdir -p "$build_dir" + +"$cxx" -std=c++17 -I core/include -I core/src \ + core/src/canon_event_sink.cpp \ + core/src/rtcp_kinematics.cpp \ + core/tests/canon_event_sink_smoke.cpp \ + -o "$build_dir/canon_event_sink_smoke" + +"$cxx" -std=c++17 -I core/include -I core/src \ + core/src/rtcp_kinematics.cpp \ + core/tests/rtcp_kinematics_smoke.cpp \ + -o "$build_dir/rtcp_kinematics_smoke" + +"$cxx" -std=c++17 -I core/include -I core/src \ + core/src/canon_event_sink.cpp \ + core/src/rtcp_kinematics.cpp \ + core/src/simulator_gcode_controls.cpp \ + core/tests/simulator_gcode_controls_smoke.cpp \ + -o "$build_dir/simulator_gcode_controls_smoke" + +"$cxx" -std=c++17 -I core/include -I core/src \ + core/src/canon_event_sink.cpp \ + core/src/cnc_sim_api.cpp \ + core/src/gcode_backend.cpp \ + core/src/rtcp_kinematics.cpp \ + core/src/simulator_gcode_controls.cpp \ + core/src/smoke_gcode_parser.cpp \ + core/tests/cnc_sim_api_smoke.cpp \ + -o "$build_dir/cnc_sim_api_smoke" + +"$cxx" -std=c++17 -I core/include -I core/src \ + core/src/canon_event_sink.cpp \ + core/src/cnc_sim_api.cpp \ + core/src/gcode_backend.cpp \ + core/src/rtcp_kinematics.cpp \ + core/src/simulator_gcode_controls.cpp \ + core/src/smoke_gcode_parser.cpp \ + core/tools/cnc_sim_dump.cpp \ + -o "$build_dir/cnc_sim_dump" + +"$build_dir/canon_event_sink_smoke" +"$build_dir/rtcp_kinematics_smoke" +"$build_dir/simulator_gcode_controls_smoke" +"$build_dir/cnc_sim_api_smoke" +"$build_dir/cnc_sim_dump" tests/gcode/basic_mill.ngc >/tmp/cnc_sim_basic_mill.json +"$build_dir/cnc_sim_dump" tests/gcode/incremental_and_r_arc.ngc >/tmp/cnc_sim_incremental_and_r_arc.json + +echo "native tests passed" +echo "dumped /tmp/cnc_sim_basic_mill.json" +echo "dumped /tmp/cnc_sim_incremental_and_r_arc.json" diff --git a/tests/gcode/basic_mill.ngc b/tests/gcode/basic_mill.ngc new file mode 100644 index 0000000..91c47e0 --- /dev/null +++ b/tests/gcode/basic_mill.ngc @@ -0,0 +1,13 @@ +G21 G90 G17 +T1 M6 +S8000 M3 +G0 X0 Y0 Z5 +F600 +G1 Z-1 +G1 X40 Y0 +G3 X40 Y40 I0 J20 +G1 X0 Y40 +G1 X0 Y0 +G4 P0.2 +M30 + diff --git a/tests/gcode/incremental_and_r_arc.ngc b/tests/gcode/incremental_and_r_arc.ngc new file mode 100644 index 0000000..e3efc81 --- /dev/null +++ b/tests/gcode/incremental_and_r_arc.ngc @@ -0,0 +1,9 @@ +G21 G90 G17 +G0 X0 Y0 +G91 +G1 X10 F120 +G1 Y10 +G90 +G2 X20 Y10 R5 +M30 + diff --git a/tests/gcode/linuxcnc_basic_motion.ngc b/tests/gcode/linuxcnc_basic_motion.ngc new file mode 100644 index 0000000..5d6238d --- /dev/null +++ b/tests/gcode/linuxcnc_basic_motion.ngc @@ -0,0 +1,12 @@ +G21 G90 G17 +S8000 M3 +G0 X0 Y0 Z5 +F600 +G1 Z-1 +G1 X40 Y0 +G3 X40 Y40 I0 J20 +G1 X0 Y40 +G1 X0 Y0 +G4 P0.2 +M30 + diff --git a/tests/gcode/linuxcnc_canned_cycle.ngc b/tests/gcode/linuxcnc_canned_cycle.ngc new file mode 100644 index 0000000..636964f --- /dev/null +++ b/tests/gcode/linuxcnc_canned_cycle.ngc @@ -0,0 +1,5 @@ +G21 G90 G17 +G0 X0 Y0 Z5 +G81 X20 Y0 Z-2 R3 F120 +G80 +M30 diff --git a/tests/gcode/linuxcnc_coordinate_offsets.ngc b/tests/gcode/linuxcnc_coordinate_offsets.ngc new file mode 100644 index 0000000..7274e0f --- /dev/null +++ b/tests/gcode/linuxcnc_coordinate_offsets.ngc @@ -0,0 +1,5 @@ +G21 G90 G17 +G10 L2 P1 X12.5 Y-3 Z4 R30 +G92 X1 Y2 Z3 +G92.1 +M30 diff --git a/tests/gcode/linuxcnc_rtcp_controls.ngc b/tests/gcode/linuxcnc_rtcp_controls.ngc new file mode 100644 index 0000000..408d521 --- /dev/null +++ b/tests/gcode/linuxcnc_rtcp_controls.ngc @@ -0,0 +1,5 @@ +G21 G90 G17 +M428 M429 M430 +G49 +G43.4 H7 +M30 diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..f3ecb5b --- /dev/null +++ b/web/index.html @@ -0,0 +1,91 @@ + + + + + + CNC WASM Simulator + + + +
+
+
CNC SIM
+
+ WASM OFFLINE + MEM + MM + RESET +
+
+ +
+ + +
+ +
+ + + + + + +
+
+ + +
+
+ + + + diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..d1b5584 --- /dev/null +++ b/web/package.json @@ -0,0 +1,19 @@ +{ + "name": "cnc-wasm-simulator-web", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "three": "^0.164.0" + }, + "devDependencies": { + "@types/three": "^0.164.0", + "typescript": "^5.4.5", + "vite": "^5.2.0" + } +} + diff --git a/web/src/app.js b/web/src/app.js new file mode 100644 index 0000000..59853bd --- /dev/null +++ b/web/src/app.js @@ -0,0 +1,221 @@ +import { createWasmSimulator } from "./wasm-core.js"; + +const elements = { + wasmState: document.querySelector("#wasmState"), + modeState: document.querySelector("#modeState"), + unitState: document.querySelector("#unitState"), + programState: document.querySelector("#programState"), + lineCount: document.querySelector("#lineCount"), + input: document.querySelector("#programInput"), + backendSelect: document.querySelector("#backendSelect"), + alarmList: document.querySelector("#alarmList"), + canvas: document.querySelector("#toolpathCanvas"), + parseBtn: document.querySelector("#parseBtn"), + resetBtn: document.querySelector("#resetBtn"), + holdBtn: document.querySelector("#holdBtn"), + stopBtn: document.querySelector("#stopBtn"), + axes: { + x: document.querySelector("#axisX"), + y: document.querySelector("#axisY"), + z: document.querySelector("#axisZ"), + a: document.querySelector("#axisA"), + b: document.querySelector("#axisB"), + c: document.querySelector("#axisC"), + }, +}; + +let simulatorPromise = null; +let lastEvents = []; + +function setStatus(node, text, alarm = false) { + node.textContent = text; + node.classList.toggle("alarm", alarm); +} + +function log(message, error = false) { + const line = document.createElement("div"); + line.className = `alarm-line${error ? " error" : ""}`; + line.textContent = message; + elements.alarmList.prepend(line); +} + +function formatAxis(value) { + return Number.isFinite(value) ? value.toFixed(3) : "0.000"; +} + +function updateDro(pose) { + elements.axes.x.value = formatAxis(pose.x); + elements.axes.y.value = formatAxis(pose.y); + elements.axes.z.value = formatAxis(pose.z); + elements.axes.a.value = formatAxis(pose.a); + elements.axes.b.value = formatAxis(pose.b); + elements.axes.c.value = formatAxis(pose.c); +} + +function resizeCanvas() { + const rect = elements.canvas.getBoundingClientRect(); + const dpr = window.devicePixelRatio || 1; + elements.canvas.width = Math.max(1, Math.floor(rect.width * dpr)); + elements.canvas.height = Math.max(1, Math.floor(rect.height * dpr)); + drawToolpath(lastEvents); +} + +function motionEvents(events) { + return events.filter((event) => event.type === "rapid" || event.type === "linear-feed" || event.type === "arc-feed"); +} + +function boundsFor(events) { + const motions = motionEvents(events); + if (!motions.length) { + return { minX: -50, maxX: 50, minY: -50, maxY: 50 }; + } + const xs = []; + const ys = []; + for (const event of motions) { + xs.push(event.start.x, event.end.x, event.center.x); + ys.push(event.start.y, event.end.y, event.center.y); + } + return { + minX: Math.min(...xs), + maxX: Math.max(...xs), + minY: Math.min(...ys), + maxY: Math.max(...ys), + }; +} + +function angleDelta(startAngle, endAngle, ccw) { + const full = Math.PI * 2; + let delta = endAngle - startAngle; + if (ccw && delta <= 0) { + delta += full; + } + if (!ccw && delta >= 0) { + delta -= full; + } + return delta; +} + +function drawGrid(ctx, width, height, dpr) { + ctx.fillStyle = "#090b0c"; + ctx.fillRect(0, 0, width, height); + ctx.strokeStyle = "#1e262a"; + ctx.lineWidth = 1 * dpr; + const step = 40 * dpr; + ctx.beginPath(); + for (let x = 0; x < width; x += step) { + ctx.moveTo(x, 0); + ctx.lineTo(x, height); + } + for (let y = 0; y < height; y += step) { + ctx.moveTo(0, y); + ctx.lineTo(width, y); + } + ctx.stroke(); +} + +function drawToolpath(events) { + const canvas = elements.canvas; + const ctx = canvas.getContext("2d"); + const dpr = window.devicePixelRatio || 1; + const width = canvas.width; + const height = canvas.height; + drawGrid(ctx, width, height, dpr); + + const bounds = boundsFor(events); + const margin = 44 * dpr; + const spanX = Math.max(1, bounds.maxX - bounds.minX); + const spanY = Math.max(1, bounds.maxY - bounds.minY); + const scale = Math.min((width - margin * 2) / spanX, (height - margin * 2) / spanY); + const toScreen = (pose) => ({ + x: margin + (pose.x - bounds.minX) * scale, + y: height - margin - (pose.y - bounds.minY) * scale, + }); + + for (const event of motionEvents(events)) { + const start = toScreen(event.start); + const end = toScreen(event.end); + ctx.beginPath(); + ctx.lineWidth = event.type === "rapid" ? 1.5 * dpr : 2.4 * dpr; + ctx.strokeStyle = event.type === "rapid" ? "#34d058" : event.type === "arc-feed" ? "#39b7d7" : "#f2b84b"; + if (event.type === "arc-feed") { + const center = toScreen(event.center); + const radius = Math.hypot(start.x - center.x, start.y - center.y); + const startAngle = Math.atan2(start.y - center.y, start.x - center.x); + const endAngle = Math.atan2(end.y - center.y, end.x - center.x); + const ccw = event.arcTurns > 0; + if (Number.isFinite(radius) && radius > 0.001) { + const delta = angleDelta(startAngle, endAngle, ccw); + ctx.arc(center.x, center.y, radius, startAngle, startAngle + delta, !ccw); + } else { + ctx.moveTo(start.x, start.y); + ctx.lineTo(end.x, end.y); + } + } else { + ctx.moveTo(start.x, start.y); + ctx.lineTo(end.x, end.y); + } + ctx.stroke(); + } +} + +function updateLineCount() { + const count = elements.input.value.split(/\r?\n/).length; + elements.lineCount.textContent = `${count} LINES`; +} + +async function getSimulator() { + if (!simulatorPromise) { + simulatorPromise = createWasmSimulator(); + } + return simulatorPromise; +} + +async function parseProgram() { + setStatus(elements.programState, "RUN"); + try { + const simulator = await getSimulator(); + setStatus(elements.wasmState, "WASM ONLINE"); + const events = simulator.parse(elements.input.value, "linuxcnc", { + backend: elements.backendSelect.value, + }); + lastEvents = events; + drawToolpath(events); + + const finalMove = [...motionEvents(events)].pop(); + if (finalMove) { + updateDro(finalMove.end); + } + + const unitEvent = [...events].reverse().find((event) => event.type === "set-units"); + if (unitEvent) { + elements.unitState.textContent = unitEvent.feed === 25.4 ? "INCH" : "MM"; + } + setStatus(elements.programState, "END"); + log(`OK ${events.length} EVENTS`); + } catch (error) { + setStatus(elements.wasmState, "WASM OFFLINE", true); + setStatus(elements.programState, "ALARM", true); + log(error.message, true); + } +} + +document.querySelectorAll("[data-mode]").forEach((button) => { + button.addEventListener("click", () => { + elements.modeState.textContent = button.dataset.mode.toUpperCase(); + }); +}); + +elements.parseBtn.addEventListener("click", parseProgram); +elements.resetBtn.addEventListener("click", () => { + lastEvents = []; + updateDro({ x: 0, y: 0, z: 0, a: 0, b: 0, c: 0 }); + drawToolpath([]); + setStatus(elements.programState, "RESET"); +}); +elements.holdBtn.addEventListener("click", () => setStatus(elements.programState, "HOLD")); +elements.stopBtn.addEventListener("click", () => setStatus(elements.programState, "STOP", true)); +elements.input.addEventListener("input", updateLineCount); +window.addEventListener("resize", resizeCanvas); + +updateLineCount(); +resizeCanvas(); diff --git a/web/src/index.ts b/web/src/index.ts new file mode 100644 index 0000000..bc3db47 --- /dev/null +++ b/web/src/index.ts @@ -0,0 +1,62 @@ +export type CncDialect = "linuxcnc" | "fanuc" | "siemens"; + +export type CncPose = { + x: number; + y: number; + z: number; + a: number; + b: number; + c: number; + u: number; + v: number; + w: number; +}; + +export type CncEventType = + | "error" + | "comment" + | "set-units" + | "set-plane" + | "set-feed" + | "set-spindle" + | "tool-change" + | "dwell" + | "rapid" + | "linear-feed" + | "arc-feed" + | "probe" + | "program-end" + | "rtcp-pivot" + | "kinematics-switch" + | "rtcp-state" + | "set-g5x-offset" + | "set-g92-offset" + | "set-xy-rotation"; + +export type CncEvent = { + type: CncEventType; + line: number; + plane: number; + tool: number; + feed: number; + spindle: number; + dwellSeconds: number; + start: CncPose; + end: CncPose; + center: CncPose; + arcTurns: number; + reserved: number; +}; + +export type SimulatorCore = { + parse( + program: string, + dialect: CncDialect, + options?: { backend?: string; rtcp?: { enabled: boolean; toolLength: number } }, + ): Promise; +}; + +export async function createSimulatorCore(): Promise { + const { createWasmSimulator } = await import("./wasm-core.js"); + return createWasmSimulator(); +} diff --git a/web/src/wasm-core.d.ts b/web/src/wasm-core.d.ts new file mode 100644 index 0000000..0c85d1d --- /dev/null +++ b/web/src/wasm-core.d.ts @@ -0,0 +1,8 @@ +import type { CncDialect, CncEvent } from "./index"; + +export type WasmSimulator = { + parse(program: string, dialect?: CncDialect, options?: { backend?: string }): CncEvent[]; + dispose(): void; +}; + +export function createWasmSimulator(): Promise; diff --git a/web/src/wasm-core.js b/web/src/wasm-core.js new file mode 100644 index 0000000..36af81a --- /dev/null +++ b/web/src/wasm-core.js @@ -0,0 +1,148 @@ +const DIALECT = { + linuxcnc: 0, + fanuc: 1, + siemens: 2, +}; + +const EVENT_TYPE = { + 1: "error", + 2: "comment", + 3: "set-units", + 4: "set-plane", + 5: "set-feed", + 6: "set-spindle", + 7: "tool-change", + 8: "dwell", + 9: "rapid", + 10: "linear-feed", + 11: "arc-feed", + 12: "probe", + 13: "program-end", + 14: "rtcp-pivot", + 15: "kinematics-switch", + 16: "rtcp-state", + 17: "set-g5x-offset", + 18: "set-g92-offset", + 19: "set-xy-rotation", +}; + +const EVENT_OFFSETS = { + type: 4, + line: 8, + plane: 12, + tool: 16, + feed: 24, + spindle: 32, + dwellSeconds: 40, + start: 48, + end: 120, + center: 192, + arcTurns: 264, + reserved: 268, +}; + +function readPose(view, offset) { + return { + x: view.getFloat64(offset + 0, true), + y: view.getFloat64(offset + 8, true), + z: view.getFloat64(offset + 16, true), + a: view.getFloat64(offset + 24, true), + b: view.getFloat64(offset + 32, true), + c: view.getFloat64(offset + 40, true), + u: view.getFloat64(offset + 48, true), + v: view.getFloat64(offset + 56, true), + w: view.getFloat64(offset + 64, true), + }; +} + +function readEvent(module, ptr) { + const view = new DataView(module.HEAPU8.buffer, ptr, 272); + return { + type: EVENT_TYPE[view.getInt32(EVENT_OFFSETS.type, true)] ?? "unknown", + line: view.getInt32(EVENT_OFFSETS.line, true), + plane: view.getInt32(EVENT_OFFSETS.plane, true), + tool: view.getInt32(EVENT_OFFSETS.tool, true), + feed: view.getFloat64(EVENT_OFFSETS.feed, true), + spindle: view.getFloat64(EVENT_OFFSETS.spindle, true), + dwellSeconds: view.getFloat64(EVENT_OFFSETS.dwellSeconds, true), + start: readPose(view, EVENT_OFFSETS.start), + end: readPose(view, EVENT_OFFSETS.end), + center: readPose(view, EVENT_OFFSETS.center), + arcTurns: view.getInt32(EVENT_OFFSETS.arcTurns, true), + reserved: view.getInt32(EVENT_OFFSETS.reserved, true), + }; +} + +async function loadModuleFactory() { + const module = await import("/cnc_sim.js"); + return module.default ?? module.createCncSimModule ?? globalThis.createCncSimModule; +} + +export async function createWasmSimulator() { + const createModule = await loadModuleFactory(); + if (!createModule) { + throw new Error("cnc_sim.js did not export createCncSimModule"); + } + + const module = await createModule(); + const create = module.cwrap("cnc_sim_create", "number", []); + const destroy = module.cwrap("cnc_sim_destroy", null, ["number"]); + const reset = module.cwrap("cnc_sim_reset", null, ["number"]); + const setDialect = module.cwrap("cnc_sim_set_dialect", "number", ["number", "number"]); + const setCallback = module.cwrap("cnc_sim_set_event_callback", "number", ["number", "number", "number"]); + const loadConfig = module.cwrap("cnc_sim_load_config_json", "number", ["number", "number", "number"]); + const parseProgram = module.cwrap("cnc_sim_parse_program", "number", ["number", "number", "number"]); + const lastError = module.cwrap("cnc_sim_last_error", "number", ["number"]); + + const handle = create(); + let callbackPtr = 0; + + return { + parse(program, dialect = "linuxcnc", options = {}) { + const events = []; + reset(handle); + setDialect(handle, DIALECT[dialect] ?? DIALECT.linuxcnc); + + const config = JSON.stringify({ + backend: options.backend ?? "smoke", + ...(options.rtcp ? { rtcp: options.rtcp } : {}), + }); + const configBytes = module.lengthBytesUTF8(config) + 1; + const configPtr = module._malloc(configBytes); + module.stringToUTF8(config, configPtr, configBytes); + const configRc = loadConfig(handle, configPtr, configBytes - 1); + module._free(configPtr); + if (configRc !== 0) { + throw new Error(module.UTF8ToString(lastError(handle))); + } + + if (callbackPtr) { + module.removeFunction(callbackPtr); + } + callbackPtr = module.addFunction((eventPtr) => { + events.push(readEvent(module, eventPtr)); + return 0; + }, "iii"); + setCallback(handle, callbackPtr, 0); + + const bytes = module.lengthBytesUTF8(program) + 1; + const ptr = module._malloc(bytes); + module.stringToUTF8(program, ptr, bytes); + const rc = parseProgram(handle, ptr, bytes - 1); + module._free(ptr); + + if (rc !== 0) { + throw new Error(module.UTF8ToString(lastError(handle))); + } + return events; + }, + + dispose() { + if (callbackPtr) { + module.removeFunction(callbackPtr); + callbackPtr = 0; + } + destroy(handle); + }, + }; +} diff --git a/web/styles.css b/web/styles.css new file mode 100644 index 0000000..25e792a --- /dev/null +++ b/web/styles.css @@ -0,0 +1,279 @@ +:root { + color-scheme: dark; + --bg: #151719; + --panel: #24282b; + --panel-2: #1b1f22; + --line: #3c4246; + --text: #e8ecef; + --muted: #9ba6ad; + --green: #34d058; + --amber: #f2b84b; + --red: #f25f5c; + --cyan: #39b7d7; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-width: 1040px; + background: var(--bg); + color: var(--text); + font-family: Arial, Helvetica, sans-serif; + letter-spacing: 0; +} + +button, +textarea, +input { + font: inherit; +} + +.cnc-shell { + min-height: 100vh; + display: grid; + grid-template-rows: 54px 1fr; +} + +.top-bar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 18px; + background: #101214; + border-bottom: 1px solid var(--line); +} + +.brand { + font-weight: 700; + font-size: 22px; + color: var(--cyan); +} + +.status-strip { + display: flex; + gap: 10px; +} + +.status { + min-width: 92px; + height: 30px; + display: inline-flex; + align-items: center; + justify-content: center; + border: 1px solid var(--line); + background: #202428; + color: var(--green); + font-size: 13px; + font-weight: 700; +} + +.status.alarm { + color: var(--red); +} + +.machine-panel { + display: grid; + grid-template-columns: 290px minmax(480px, 1fr) 390px; + min-height: 0; +} + +.left-panel, +.right-panel, +.center-panel { + min-height: 0; + border-right: 1px solid var(--line); + background: var(--panel); +} + +.center-panel { + display: grid; + grid-template-rows: 1fr 58px; + background: #111416; +} + +.right-panel { + border-right: 0; + display: grid; + grid-template-rows: 42px 1fr 150px; +} + +.dro { + padding: 16px; + background: var(--panel-2); + border-bottom: 1px solid var(--line); +} + +.dro-row { + display: grid; + grid-template-columns: 34px 1fr; + align-items: baseline; + height: 48px; + border-bottom: 1px solid #30363a; +} + +.dro-row:last-child { + border-bottom: 0; +} + +.dro-row span { + color: var(--amber); + font-size: 22px; + font-weight: 700; +} + +.dro-row output { + text-align: right; + font-family: "Courier New", monospace; + font-size: 28px; + color: #f5f7f8; +} + +.meters { + display: grid; + gap: 18px; + padding: 18px 16px; + border-bottom: 1px solid var(--line); +} + +.meters label { + display: grid; + gap: 8px; + color: var(--muted); + font-size: 12px; + font-weight: 700; +} + +.meters input { + width: 100%; +} + +.keypad { + padding: 16px; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; +} + +button { + height: 44px; + border: 1px solid #565f65; + border-radius: 4px; + background: #30363a; + color: var(--text); + cursor: pointer; + font-size: 13px; + font-weight: 700; +} + +button:hover { + background: #3a4248; +} + +button.cycle { + background: #205f37; + border-color: #2d8a4e; +} + +button.stop { + background: #7a2726; + border-color: #a83a38; +} + +#toolpathCanvas { + width: 100%; + height: 100%; + display: block; + background: #090b0c; +} + +.soft-keys { + display: grid; + grid-template-columns: repeat(6, 1fr); + gap: 8px; + padding: 8px; + border-top: 1px solid var(--line); + background: #1b1f22; +} + +.program-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding: 0 12px; + border-bottom: 1px solid var(--line); + background: var(--panel-2); + color: var(--amber); + font-size: 13px; + font-weight: 700; +} + +.backend-select { + display: inline-flex; + align-items: center; + gap: 6px; + color: var(--muted); + font-size: 12px; +} + +.backend-select select { + height: 26px; + border: 1px solid var(--line); + border-radius: 4px; + background: #101214; + color: var(--text); + font-size: 12px; + font-weight: 700; +} + +textarea { + width: 100%; + height: 100%; + resize: none; + border: 0; + outline: none; + padding: 14px; + background: #121517; + color: #e5f5e9; + font-family: "Courier New", monospace; + font-size: 15px; + line-height: 1.5; +} + +.alarm-list { + overflow: auto; + padding: 10px 12px; + border-top: 1px solid var(--line); + background: #1a1d20; + color: var(--muted); + font-family: "Courier New", monospace; + font-size: 13px; +} + +.alarm-line { + padding: 4px 0; + border-bottom: 1px solid #2a3034; +} + +.alarm-line.error { + color: var(--red); +} + +@media (max-width: 1180px) { + body { + min-width: 0; + } + + .machine-panel { + grid-template-columns: 260px 1fr; + } + + .right-panel { + grid-column: 1 / -1; + min-height: 360px; + border-top: 1px solid var(--line); + } +}