Initial wasm simulator checkpoint

This commit is contained in:
CNC Local
2026-05-22 04:43:21 +08:00
commit 55d9fff9f0
54 changed files with 5523 additions and 0 deletions

10
.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
build/
dist/
node_modules/
*.o
*.a
*.so
*.wasm
*.js.map
*.log
.DS_Store

163
README.zh-CN.md Normal file
View File

@@ -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.

16
build-wasm.sh Executable file
View File

@@ -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/

174
core/CMakeLists.txt Normal file
View File

@@ -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_OBJECTS:cnc_sim_objects>
)
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_OBJECTS:cnc_sim_objects>
)
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()

View File

@@ -0,0 +1,88 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
#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

View File

@@ -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 &center, 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;
}

View File

@@ -0,0 +1,64 @@
#pragma once
#include "cnc_sim_api.h"
#include <unordered_map>
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 &center, 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<int, double> tool_lengths_;
};

235
core/src/cnc_sim_api.cpp Normal file
View File

@@ -0,0 +1,235 @@
#include "cnc_sim_api.h"
#include "canon_event_sink.h"
#include "gcode_backend.h"
#include <algorithm>
#include <cctype>
#include <cstdlib>
#include <string>
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<char>(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<unsigned char>(ch))) {
compact.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(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<unsigned char>(ch))) {
compact.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(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<size_t>(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<int>(h_code), tool_length);
}
pos = static_cast<size_t>(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"

View File

@@ -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;
}
}

20
core/src/gcode_backend.h Normal file
View File

@@ -0,0 +1,20 @@
#pragma once
#include "canon_event_sink.h"
#include <cstddef>
#include <string>
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);

View File

@@ -0,0 +1,703 @@
#include "linuxcnc_canon_bridge.h"
#include "canon_event_sink.h"
#include "canon.hh"
#include <cstdarg>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <string>
#include <vector>
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 &current_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<NURBS_CONTROL_POINT> &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<NURBS_G6_CONTROL_POINT> &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;
}

View File

@@ -0,0 +1,7 @@
#pragma once
class CanonEventSink;
void cnc_sim_linuxcnc_set_canon_sink(CanonEventSink *sink);
CanonEventSink *cnc_sim_linuxcnc_get_canon_sink();

View File

@@ -0,0 +1,179 @@
#include "linuxcnc_rs274_backend.h"
#include <Python.h>
#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 <sstream>
#include <string>
#include <vector>
#include <cstdlib>
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<SimulatorGcodeControlAction> 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;
}

View File

@@ -0,0 +1,12 @@
#pragma once
#include "canon_event_sink.h"
#include <cstddef>
#include <string>
int parse_linuxcnc_rs274_backend(CanonEventSink &sink,
const char *program,
size_t program_len,
std::string *error);

View File

@@ -0,0 +1,72 @@
#include "rtcp_kinematics.h"
#include <cmath>
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;
}

View File

@@ -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);

View File

@@ -0,0 +1,145 @@
#include "simulator_gcode_controls.h"
#include <cctype>
#include <cmath>
#include <cstdlib>
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<char>(std::toupper(static_cast<unsigned char>(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<SimulatorGcodeControlAction> *actions) {
const char *text = stripped.c_str();
bool saw_code = false;
for (size_t i = 0; text[i] != '\0';) {
if (std::isspace(static_cast<unsigned char>(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<size_t>(end - text);
}
return saw_code;
}
bool parse_rtcp_control_line(const std::string &stripped,
std::vector<SimulatorGcodeControlAction> *actions) {
const char *text = stripped.c_str();
std::vector<SimulatorGcodeControlAction> parsed;
int h_code = 0;
bool saw_rtcp_code = false;
for (size_t i = 0; text[i] != '\0';) {
if (std::isspace(static_cast<unsigned char>(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<int>(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<int>(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<size_t>(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<SimulatorGcodeControlAction> *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);
}
}

View File

@@ -0,0 +1,24 @@
#pragma once
#include "canon_event_sink.h"
#include <string>
#include <vector>
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<SimulatorGcodeControlAction> *actions);
void emit_simulator_gcode_control_action(CanonEventSink &sink,
const SimulatorGcodeControlAction &action,
int line);

View File

@@ -0,0 +1,399 @@
#include "smoke_gcode_parser.h"
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdlib>
#include <sstream>
#include <string>
#include <unordered_map>
#include <vector>
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<char>(std::toupper(static_cast<unsigned char>(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<Word> parse_word_list(const std::string &line) {
std::vector<Word> words;
const char *text = line.c_str();
char *end = nullptr;
for (size_t i = 0; text[i] != '\0';) {
if (std::isspace(static_cast<unsigned char>(text[i]))) {
++i;
continue;
}
char letter = text[i];
if (!std::isalpha(static_cast<unsigned char>(letter))) {
++i;
continue;
}
++i;
double value = std::strtod(text + i, &end);
if (end == text + i) {
continue;
}
words.push_back({letter, value});
i = static_cast<size_t>(end - text);
}
return words;
}
std::unordered_map<char, double> last_words_by_letter(const std::vector<Word> &word_list) {
std::unordered_map<char, double> words;
for (const Word &word : word_list) {
words[word.letter] = word.value;
}
return words;
}
bool has_axis_word(const std::unordered_map<char, double> &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<char, double> &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<char, double> &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<char, double> &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<char, double> &words, char letter, int fallback) {
auto it = words.find(letter);
if (it == words.end()) {
return fallback;
}
return static_cast<int>(std::lround(it->second));
}
int rounded_value(double value) {
return static_cast<int>(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<int> 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;
}

View File

@@ -0,0 +1,18 @@
#pragma once
#include "canon_event_sink.h"
#include <cstddef>
#include <string>
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;
};

View File

@@ -0,0 +1,61 @@
#include "canon_event_sink.h"
#include <iostream>
#include <vector>
namespace {
int collect_event(const CncSimEvent *event, void *user_data) {
auto *events = static_cast<std::vector<CncSimEvent> *>(user_data);
events->push_back(*event);
return 0;
}
int abort_on_second_event(const CncSimEvent *, void *user_data) {
auto *count = static_cast<int *>(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<CncSimEvent> 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;
}

View File

@@ -0,0 +1,162 @@
#include "cnc_sim_api.h"
#include <iostream>
#include <vector>
namespace {
int collect_event(const CncSimEvent *event, void *user_data) {
auto *events = static_cast<std::vector<CncSimEvent> *>(user_data);
events->push_back(*event);
return 0;
}
int abort_on_second_event(const CncSimEvent *, void *user_data) {
auto *count = static_cast<int *>(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<CncSimEvent> 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;
}

View File

@@ -0,0 +1,141 @@
#include "cnc_sim_api.h"
#include <iostream>
#include <vector>
namespace {
int collect_event(const CncSimEvent *event, void *user_data) {
auto *events = static_cast<std::vector<CncSimEvent> *>(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<CncSimEvent> 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;
}

View File

@@ -0,0 +1,96 @@
#include "canon_event_sink.h"
#include "linuxcnc_canon_bridge.h"
#include "canon.hh"
#include <iostream>
#include <vector>
namespace {
int collect_event(const CncSimEvent *event, void *user_data) {
auto *events = static_cast<std::vector<CncSimEvent> *>(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<CncSimEvent> 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;
}

View File

@@ -0,0 +1,67 @@
#include "rtcp_kinematics.h"
#include <cmath>
#include <iostream>
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;
}

View File

@@ -0,0 +1,59 @@
#include "simulator_gcode_controls.h"
#include <iostream>
#include <vector>
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<SimulatorGcodeControlAction> 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;
}

120
core/tools/cnc_sim_dump.cpp Normal file
View File

@@ -0,0 +1,120 @@
#include "cnc_sim_api.h"
#include <fstream>
#include <iostream>
#include <sstream>
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<bool *>(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;
}

View File

@@ -0,0 +1,250 @@
#include <Python.h>
#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 <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <cstdlib>
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<bool *>(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<SimulatorGcodeControlAction> 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;
}

80
docs/architecture.md Normal file
View File

@@ -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.

194
docs/linuxcnc-porting.md Normal file
View File

@@ -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 |

View File

@@ -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.

View File

@@ -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

119
rs274ngc.var Normal file
View File

@@ -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

14
test-all-native.sh Executable file
View File

@@ -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"

42
test-linuxcnc-api-native.sh Executable file
View File

@@ -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"

28
test-linuxcnc-bridge-native.sh Executable file
View File

@@ -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"

108
test-linuxcnc-rs274-native.sh Executable file
View File

@@ -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"

157
test-linuxcnc-source-link.sh Executable file
View File

@@ -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"

49
test-linuxcnc-source-objects.sh Executable file
View File

@@ -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"

48
test-linuxcnc-source-syntax.sh Executable file
View File

@@ -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"

57
test-native.sh Executable file
View File

@@ -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"

View File

@@ -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

View File

@@ -0,0 +1,9 @@
G21 G90 G17
G0 X0 Y0
G91
G1 X10 F120
G1 Y10
G90
G2 X20 Y10 R5
M30

View File

@@ -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

View File

@@ -0,0 +1,5 @@
G21 G90 G17
G0 X0 Y0 Z5
G81 X20 Y0 Z-2 R3 F120
G80
M30

View File

@@ -0,0 +1,5 @@
G21 G90 G17
G10 L2 P1 X12.5 Y-3 Z4 R30
G92 X1 Y2 Z3
G92.1
M30

View File

@@ -0,0 +1,5 @@
G21 G90 G17
M428 M429 M430
G49
G43.4 H7
M30

91
web/index.html Normal file
View File

@@ -0,0 +1,91 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CNC WASM Simulator</title>
<link rel="stylesheet" href="/styles.css" />
</head>
<body>
<main class="cnc-shell">
<section class="top-bar">
<div class="brand">CNC SIM</div>
<div class="status-strip">
<span id="wasmState" class="status alarm">WASM OFFLINE</span>
<span id="modeState" class="status">MEM</span>
<span id="unitState" class="status">MM</span>
<span id="programState" class="status">RESET</span>
</div>
</section>
<section class="machine-panel">
<aside class="left-panel">
<div class="dro">
<div class="dro-row"><span>X</span><output id="axisX">0.000</output></div>
<div class="dro-row"><span>Y</span><output id="axisY">0.000</output></div>
<div class="dro-row"><span>Z</span><output id="axisZ">0.000</output></div>
<div class="dro-row"><span>A</span><output id="axisA">0.000</output></div>
<div class="dro-row"><span>B</span><output id="axisB">0.000</output></div>
<div class="dro-row"><span>C</span><output id="axisC">0.000</output></div>
</div>
<div class="meters">
<label>FEED <input id="feedOverride" type="range" min="0" max="150" value="100" /></label>
<label>SPINDLE <input id="spindleOverride" type="range" min="0" max="150" value="100" /></label>
</div>
<div class="keypad">
<button data-mode="edit">EDIT</button>
<button data-mode="mem">MEM</button>
<button data-mode="mdi">MDI</button>
<button data-mode="jog">JOG</button>
<button id="resetBtn">RESET</button>
<button id="parseBtn" class="cycle">CYCLE START</button>
<button id="holdBtn">FEED HOLD</button>
<button id="stopBtn" class="stop">STOP</button>
</div>
</aside>
<section class="center-panel">
<canvas id="toolpathCanvas" width="1200" height="760"></canvas>
<div class="soft-keys">
<button>POS</button>
<button>PROG</button>
<button>OFFSET</button>
<button>TOOL</button>
<button>GRAPH</button>
<button>ALARM</button>
</div>
</section>
<aside class="right-panel">
<div class="program-header">
<span>O0001</span>
<label class="backend-select">
<span>BACKEND</span>
<select id="backendSelect">
<option value="smoke">SMOKE</option>
<option value="linuxcnc-rs274">LINUXCNC</option>
</select>
</label>
<span id="lineCount">0 LINES</span>
</div>
<textarea id="programInput" spellcheck="false">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
M30</textarea>
<div id="alarmList" class="alarm-list"></div>
</aside>
</section>
</main>
<script type="module" src="/src/app.js"></script>
</body>
</html>

19
web/package.json Normal file
View File

@@ -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"
}
}

221
web/src/app.js Normal file
View File

@@ -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();

62
web/src/index.ts Normal file
View File

@@ -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<CncEvent[]>;
};
export async function createSimulatorCore(): Promise<SimulatorCore> {
const { createWasmSimulator } = await import("./wasm-core.js");
return createWasmSimulator();
}

8
web/src/wasm-core.d.ts vendored Normal file
View File

@@ -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<WasmSimulator>;

148
web/src/wasm-core.js Normal file
View File

@@ -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);
},
};
}

279
web/styles.css Normal file
View File

@@ -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);
}
}