diff --git a/README.zh-CN.md b/README.zh-CN.md index c087b3a..6997214 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -57,20 +57,28 @@ The first useful target is: - 已有最小 LinuxCNC tooldata 初始化,native runner 和 API 后端都能覆盖 `T... M6` 换刀路径。 - 已有 LinuxCNC RS274 源码级链接 smoke:本项目直接编译 23 个解释器 core 源文件,不再通过 `librs274` 取得解释器主体。 - 已有五轴 RTCP 第一版几何内核,可根据编程刀尖点、A/B/C 姿态和刀长计算枢轴/主轴点补偿位置。 -- 已有坐标系事件回归语料,覆盖 `G10 L2`、`G10 L20` 的 G5X/XY 旋转事件和 `G92.1` 清零事件。 -- 已有探针事件回归语料,LinuxCNC `G38.3` 会输出独立 `probe` 事件,不再混作普通直线进给。 +- 已有坐标系事件回归语料,覆盖 `G10 L2`、`G10 L20`、`G10 P0`、`G54`-`G59.3` 全部工件坐标系选择、`G52`、`G92/G92.1/G92.2/G92.3` 的 G5X/G92/XY 旋转事件。 +- 已有探针事件回归语料,LinuxCNC `G38.2/G38.3/G38.4/G38.5` 会输出独立 `probe` 事件,不再混作普通直线进给;`probe.reserved` 保留 LinuxCNC `probe_type`:`0=trip/error`、`1=trip/no-error`、`2=clear/error`、`3=clear/no-error`。 - 已有主轴方向状态回归语料,`M3/M4/M5` 会通过 `set-spindle.reserved` 输出 `1=顺时针`、`2=逆时针`、`0=停止`。 +- 已有主轴速度模式回归语料,`G96/G97` 会通过 `comment.reserved` 临时输出 `960=CSS`、`970=constant RPM`,`feed` 暂存 LinuxCNC `SET_SPINDLE_MODE` 的 mode/max-RPM 值。 +- 已有主轴定向回归语料,`M19 R... P... Q...` 会通过 `comment.reserved` 临时输出 `1901=orient spindle`、`1902=wait orient complete`,`feed` 暂存 R 角度,`arcTurns` 暂存 P 模式,`dwellSeconds` 暂存 Q 超时。 +- 已有主轴同步进给和螺纹回归语料,`G33/G33.1/G76` 会通过 `comment.reserved` 临时输出 `3301=speed-feed sync start`、`3302=velocity sync start`、`3300=sync stop`,`feed` 暂存每转进给量。 +- 已有车床直径/半径模式回归语料,`G7/G8` 会切换 X 轴输入缩放,`G76` 会在直径模式下同步缩放线程几何参数。 +- 已有 LinuxCNC 常用只读命名参数回归语料,`#<_absolute>`、`#<_incremental>`、`#<_metric>`、`#<_imperial>`、`#<_lathe_diameter_mode>`、`#<_lathe_radius_mode>` 能在 O-word 条件里直接读取当前模态状态。 +- 已有覆盖控制回归语料,`M48/M49/M50/M51/M52/M53` 会通过 `comment.reserved` 临时输出 `50=feed override`、`51=speed override`、`52=adaptive feed`、`53=feed hold`,`feed` 暂存启用状态,`tool` 对 speed override 暂存 spindle 号。 +- 已有 I/O M-code 回归语料,`M62/M63/M64/M65/M66/M67/M68` 会通过 `comment.reserved` 临时输出对应 M 码号,`tool` 暂存 P/E 通道号,`feed` 暂存输出值或 M66 等待模式,`arcTurns` 暂存 M66 输入类型,`dwellSeconds` 暂存 M66 超时。 +- 已有 modal state 回归语料,`M70/M71/M72` 在 smoke parser 中支持主程序级保存、失效、恢复,`M73` 支持 O-word 子程序返回自动恢复;native/source 语料验证恢复后的距离模式、单位和 XY 平面运动。 - 已有注释事件行号回归语料,LinuxCNC `COMMENT` 事件会保留当前 G-code 行号。 - 已有当前刀号事件行号回归语料,LinuxCNC `M61 Q...` 会输出带行号的 `tool-change` 事件。 - 已有停止类事件回归语料,`M0/M1/M60` 会通过 `program-end.reserved` 输出 `1=程序停`、`2=可选停`、`3=托盘交换`。 - 已有冷却液状态回归语料,`M7/M8/M9` 会通过 `comment.reserved` 临时输出 `10/11=mist off/on`、`20/21=flood off/on`。 - 已有进给模式回归语料,`G93/G94/G95` 会通过 `comment.reserved` 临时输出 `93/94/95`,并通过 `set-feed` 记录 Canon feed mode 回调。 - 已有运动控制模式回归语料,`G61/G61.1/G64` 会通过 `comment.reserved` 临时输出 `611/612/640`,`feed` 暂存 G64 tolerance。 -- smoke parser 已有固定循环第一版展开,覆盖 `G73` 高速啄钻、`G81` 普通钻、`G82` 孔底暂停钻、`G83` 啄钻、`G85` 镗孔进给退刀、`G86` 镗孔停主轴快速退刀、`G89` 镗孔暂停后进给退刀,以及 `G80` 取消、`G98/G99` 返回模式和 `L` 重复次数。 -- LinuxCNC native/source 对照已扩展到 `G82` 孔底暂停和 `G83` 啄钻关键事件,覆盖孔底 dwell、啄钻深度序列和 R 平面退刀。 +- smoke parser 已有固定循环第一版展开,覆盖 `G73` 高速啄钻、`G74/G84` 攻丝、`G81` 普通钻、`G82` 孔底暂停钻、`G83` 啄钻、`G85` 镗孔进给退刀、`G86` 镗孔停主轴快速退刀、`G87` 背镗、`G88` 手动退刀镗孔、`G89` 镗孔暂停后进给退刀,以及 `G80` 取消、`G98/G99` 返回模式和 `L` 重复次数;这些循环已按 LinuxCNC `G17/G18/G19` 平面映射进刀轴,`G87` 会按平面重映射 `I/J/K`。 +- LinuxCNC native/source 对照已扩展到 `G82` 孔底暂停、`G83` 啄钻和 `G18/G19` 固定循环平面关键事件,覆盖 `G74/G84/G86/G87/G88` 非 XY 平面序列、孔底 dwell、啄钻深度序列、R 平面退刀、手动退刀停机和背镗偏置路径。 - smoke parser 已有 Fanuc 风格 `M98 P... L...` / `M99` 子程序调用第一版,支持同文件 `O...` 子程序块和重复调用展开;未知子程序、裸 `M99` 和过深嵌套会返回明确错误。 - smoke parser 已有 LinuxCNC 风格 O-word 子程序第一版,覆盖数字 `O100 ...` 和命名 `O ...` 的 `sub` / `call [args]` / `return` / `endsub`,支持简单数字实参、`#1 = ...` 编号参数赋值、`# = ...` 命名参数赋值、`X#1` / `X#` 参数引用、`+ - * /` 可嵌套方括号表达式,以及 `ABS[]`、`SQRT[]`、`EXP[]`、`LN[]`、角度制 `SIN[]` / `COS[]` / `TAN[]` / `ASIN[]` / `ACOS[]`、LinuxCNC 双参数 `ATAN[]/[]`、`FIX[]` / `FUP[]` / `ROUND[]`,并拒绝和 `M98/M99` 风格混用。 -- LinuxCNC native/source runner 已支持显式文件模式执行,O-word 子程序语料会通过 LinuxCNC 自身 `open/read/execute` 路径验证,而不是逐行模拟。 +- LinuxCNC native/source runner 已支持显式文件模式执行,O-word 子程序语料会通过 LinuxCNC 自身 `open/read/execute` 路径验证,而不是逐行模拟;native/source 回归现已同时覆盖数字 `O100` 和命名 `O` 子程序、实参传递、命名参数赋值、`return` 跳过体尾,以及 `if` / `elseif` / `else`、`while`、`repeat`、`do` / `while`、`break`、`continue` 控制流。 - LinuxCNC Canon bridge 会把当前解释行号补到没有自带 `lineno` 的回调事件上,方便 Web 端诊断和源码高亮。 - 下一步是继续替换源码级链接中残留的 Python、INI/HAL、文件系统和动态加载依赖,并把 RTCP 内核接入 canon 事件流和机型配置。 @@ -97,11 +105,11 @@ The first useful target is: - `M7` / `M8` / `M9` 冷却液状态回归 - `G93` / `G94` / `G95` 进给模式回归 - `G61` / `G61.1` / `G64` 运动控制模式回归 -- smoke parser `G73` / `G81` / `G82` / `G83` / `G85` / `G86` / `G89` / `G80` / `G98` / `G99` / `L` 固定循环展开回归 -- LinuxCNC native/source `G82` / `G83` 固定循环对照回归 +- smoke parser `G73` / `G74` / `G81` / `G82` / `G83` / `G84` / `G85` / `G86` / `G87` / `G88` / `G89` / `G80` / `G98` / `G99` / `L` 固定循环展开回归,以及 `G18/G19` 平面进刀轴和 `G87 I/J/K` 平面映射回归 +- LinuxCNC native/source `G74` / `G82` / `G83` / `G84` / `G86` / `G87` / `G88` / `G18` / `G19` 固定循环对照回归 - smoke parser `M98 P... L...` / `M99` 子程序调用回归 - smoke parser LinuxCNC 风格 `O... sub/call/return/endsub` 子程序调用回归 -- `G10 L2` / `G10 L20` / `G92.1` 坐标系事件回归 +- `G10 L2` / `G10 L20` / `G10 P0` / `G54`-`G59.3` / `G92.1` 坐标系事件回归 - `G38.3` 探针事件回归 ## 后端选择 @@ -170,7 +178,7 @@ Canon bridge 已不再丢弃坐标系相关回调,会输出: - `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` 清零;`tests/gcode/linuxcnc_coordinate_l20.ngc` 覆盖 `G10 L20 P2` 按当前位置和目标工件坐标计算 G55 偏置。测试脚本会在坐标系语料之间重置参数文件,避免 LinuxCNC 持久参数互相污染。后续继续扩展到更多坐标系 P 号、持久参数文件读写策略和旋转叠加运动路径。 +目前已有多组回归语料:`tests/gcode/linuxcnc_coordinate_offsets.ngc` 覆盖 `G10 L2 P1 X... Y... Z... R...` 触发 `set-g5x-offset` / `set-xy-rotation`,以及 `G92.1` 触发 `set-g92-offset` 清零;`tests/gcode/linuxcnc_coordinate_l20.ngc` 覆盖 `G10 L20 P2` 按当前位置和目标工件坐标计算 G55 偏置;`tests/gcode/linuxcnc_coordinate_p0.ngc` 覆盖 `G10 L2 P0` 指向当前活动坐标系;`tests/gcode/linuxcnc_coordinate_select_all.ngc` 覆盖 `G54`-`G59.3` 选择时的 G5X index 和偏置载入。测试脚本会在坐标系语料之间重置参数文件,避免 LinuxCNC 持久参数互相污染。后续继续扩展持久参数文件读写策略和旋转叠加运动路径。 LinuxCNC 中部分 Canon 函数不携带行号参数,bridge 会在调用解释器 `read/execute` 前记录当前输入行,并用它填充这类事件的 `line` 字段。 ## CMake native LinuxCNC 后端 diff --git a/core/src/canon_event_sink.cpp b/core/src/canon_event_sink.cpp index e6727c4..1fb874a 100644 --- a/core/src/canon_event_sink.cpp +++ b/core/src/canon_event_sink.cpp @@ -6,12 +6,22 @@ void CanonEventSink::reset() { position_ = {}; plane_ = 17; unit_scale_ = 1.0; + traverse_rate_ = 0.0; feed_ = 0.0; spindle_ = 0.0; spindle_direction_ = 0; selected_tool_ = 0; + probe_position_ = {}; + probe_tripped_ = false; + tool_length_offset_ = {}; callback_status_ = 0; kinematics_type_ = 1; + feed_override_ = false; + speed_override_ = false; + adaptive_feed_ = false; + feed_hold_ = false; + mist_on_ = false; + flood_on_ = false; } void CanonEventSink::set_callback(CncSimEventCallback callback, void *user_data) { @@ -48,6 +58,10 @@ void CanonEventSink::set_tool_length(int h_code, double tool_length) { } } +void CanonEventSink::set_tool_length_offset(const CncSimPose &offset) { + tool_length_offset_ = offset; +} + void CanonEventSink::set_rtcp_state(bool enabled, int h_code, int line) { rtcp_enabled_ = enabled; rtcp_h_code_ = enabled ? h_code : 0; @@ -104,6 +118,10 @@ void CanonEventSink::select_plane(int plane, int line) { emit(event); } +void CanonEventSink::set_traverse_rate(double rate) { + traverse_rate_ = rate; +} + void CanonEventSink::set_feed_rate(double feed, int line) { feed_ = feed; CncSimEvent event = base_event(CNC_SIM_EVENT_SET_FEED, line); @@ -111,9 +129,10 @@ void CanonEventSink::set_feed_rate(double feed, int line) { emit(event); } -void CanonEventSink::set_feed_mode(int mode, int line) { +void CanonEventSink::set_feed_mode(int spindle, int mode, int line) { CncSimEvent event = base_event(CNC_SIM_EVENT_SET_FEED, line); event.feed = feed_; + event.tool = spindle; event.reserved = mode; emit(event); } @@ -126,6 +145,89 @@ void CanonEventSink::set_spindle_speed(double spindle, int line) { emit(event); } +void CanonEventSink::set_spindle_mode(int spindle, double mode, int line) { + CncSimEvent event = base_event(CNC_SIM_EVENT_COMMENT, line); + event.tool = spindle; + event.feed = mode; + event.reserved = mode == 0.0 ? 970 : 960; + emit(event); +} + +void CanonEventSink::set_speed_feed_sync(int spindle, double feed_per_revolution, bool velocity_mode, bool enabled, int line) { + CncSimEvent event = base_event(CNC_SIM_EVENT_COMMENT, line); + event.tool = spindle; + event.feed = feed_per_revolution; + event.reserved = enabled ? (velocity_mode ? 3302 : 3301) : 3300; + emit(event); +} + +void CanonEventSink::orient_spindle(int spindle, double orientation, int mode, int line) { + CncSimEvent event = base_event(CNC_SIM_EVENT_COMMENT, line); + event.tool = spindle; + event.feed = orientation; + event.arc_turns = mode; + event.reserved = 1901; + emit(event); +} + +void CanonEventSink::wait_spindle_orient_complete(int spindle, double timeout, int line) { + CncSimEvent event = base_event(CNC_SIM_EVENT_COMMENT, line); + event.tool = spindle; + event.dwell_seconds = timeout; + event.reserved = 1902; + emit(event); +} + +void CanonEventSink::set_digital_output(int index, bool enabled, bool motion_synchronized, int line) { + CncSimEvent event = base_event(CNC_SIM_EVENT_COMMENT, line); + event.tool = index; + event.feed = enabled ? 1.0 : 0.0; + event.reserved = motion_synchronized ? (enabled ? 62 : 63) : (enabled ? 64 : 65); + emit(event); +} + +void CanonEventSink::set_analog_output(int index, double value, bool motion_synchronized, int line) { + CncSimEvent event = base_event(CNC_SIM_EVENT_COMMENT, line); + event.tool = index; + event.feed = value; + event.reserved = motion_synchronized ? 67 : 68; + emit(event); +} + +void CanonEventSink::wait_input(int index, int input_type, int wait_type, double timeout, int line) { + CncSimEvent event = base_event(CNC_SIM_EVENT_COMMENT, line); + event.tool = index; + event.feed = wait_type; + event.dwell_seconds = timeout; + event.arc_turns = input_type; + event.reserved = 66; + emit(event); +} + +void CanonEventSink::set_override_control(int control, bool enabled, int spindle, int line) { + switch (control) { + case 50: + feed_override_ = enabled; + break; + case 51: + speed_override_ = enabled; + break; + case 52: + adaptive_feed_ = enabled; + break; + case 53: + feed_hold_ = enabled; + break; + default: + break; + } + CncSimEvent event = base_event(CNC_SIM_EVENT_COMMENT, line); + event.tool = spindle; + event.feed = enabled ? 1.0 : 0.0; + event.reserved = control; + emit(event); +} + void CanonEventSink::start_spindle(int direction, int line) { spindle_direction_ = direction < 0 ? 2 : 1; CncSimEvent event = base_event(CNC_SIM_EVENT_SET_SPINDLE, line); @@ -181,11 +283,14 @@ void CanonEventSink::arc_feed(int line, const CncSimPose &end, const CncSimPose position_ = end; } -void CanonEventSink::straight_probe(int line, const CncSimPose &end) { +void CanonEventSink::straight_probe(int line, const CncSimPose &end, int probe_type) { CncSimEvent event = base_event(CNC_SIM_EVENT_PROBE, line); event.start = position_; event.end = end; + event.reserved = probe_type; emit(event); + probe_position_ = end; + probe_tripped_ = (probe_type & 2) == 0; position_ = end; } @@ -224,6 +329,10 @@ double CanonEventSink::unit_scale() const { return unit_scale_; } +double CanonEventSink::traverse_rate() const { + return traverse_rate_; +} + double CanonEventSink::feed_rate() const { return feed_; } @@ -236,6 +345,18 @@ int CanonEventSink::spindle_direction() const { return spindle_direction_; } +const CncSimPose &CanonEventSink::tool_length_offset() const { + return tool_length_offset_; +} + +const CncSimPose &CanonEventSink::probe_position() const { + return probe_position_; +} + +bool CanonEventSink::probe_tripped() const { + return probe_tripped_; +} + int CanonEventSink::selected_tool() const { return selected_tool_; } @@ -252,6 +373,38 @@ double CanonEventSink::rtcp_tool_length() const { return rtcp_tool_length_; } +bool CanonEventSink::feed_override() const { + return feed_override_; +} + +bool CanonEventSink::speed_override() const { + return speed_override_; +} + +bool CanonEventSink::adaptive_feed() const { + return adaptive_feed_; +} + +bool CanonEventSink::feed_hold() const { + return feed_hold_; +} + +bool CanonEventSink::mist_on() const { + return mist_on_; +} + +bool CanonEventSink::flood_on() const { + return flood_on_; +} + +void CanonEventSink::set_mist_on(bool enabled) { + mist_on_ = enabled; +} + +void CanonEventSink::set_flood_on(bool enabled) { + flood_on_ = enabled; +} + void CanonEventSink::emit(CncSimEvent event) { if (callback_ && callback_status_ == 0) { callback_status_ = callback_(&event, user_data_); diff --git a/core/src/canon_event_sink.h b/core/src/canon_event_sink.h index 63183dd..b76fa3b 100644 --- a/core/src/canon_event_sink.h +++ b/core/src/canon_event_sink.h @@ -12,6 +12,7 @@ public: bool callback_aborted() const; void configure_rtcp(bool enabled, double tool_length); void set_tool_length(int h_code, double tool_length); + void set_tool_length_offset(const CncSimPose &offset); 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); @@ -20,9 +21,18 @@ public: void use_length_units(double scale, int line); void select_plane(int plane, int line); + void set_traverse_rate(double rate); void set_feed_rate(double feed, int line); - void set_feed_mode(int mode, int line); + void set_feed_mode(int spindle, int mode, int line); void set_spindle_speed(double spindle, int line); + void set_spindle_mode(int spindle, double mode, int line); + void set_speed_feed_sync(int spindle, double feed_per_revolution, bool velocity_mode, bool enabled, int line); + void orient_spindle(int spindle, double orientation, int mode, int line); + void wait_spindle_orient_complete(int spindle, double timeout, int line); + void set_digital_output(int index, bool enabled, bool motion_synchronized, int line); + void set_analog_output(int index, double value, bool motion_synchronized, int line); + void wait_input(int index, int input_type, int wait_type, double timeout, int line); + void set_override_control(int control, bool enabled, int spindle, int line); void start_spindle(int direction, int line); void stop_spindle(int line); void select_tool(int tool); @@ -30,7 +40,7 @@ public: void straight_traverse(int line, const CncSimPose &end); void straight_feed(int line, const CncSimPose &end); void arc_feed(int line, const CncSimPose &end, const CncSimPose ¢er, int turns); - void straight_probe(int line, const CncSimPose &end); + void straight_probe(int line, const CncSimPose &end, int probe_type = 0); void dwell(double seconds, int line); void program_end(int line); void program_stop(int line, int kind); @@ -39,13 +49,25 @@ public: const CncSimPose &position() const; int plane() const; double unit_scale() const; + double traverse_rate() const; double feed_rate() const; double spindle_speed() const; int spindle_direction() const; + const CncSimPose &tool_length_offset() const; + const CncSimPose &probe_position() const; + bool probe_tripped() const; int selected_tool() const; int kinematics_type() const; bool rtcp_enabled() const; double rtcp_tool_length() const; + bool feed_override() const; + bool speed_override() const; + bool adaptive_feed() const; + bool feed_hold() const; + bool mist_on() const; + bool flood_on() const; + void set_mist_on(bool enabled); + void set_flood_on(bool enabled); private: void emit(CncSimEvent event); @@ -58,14 +80,24 @@ private: CncSimPose position_{}; int plane_ = 17; double unit_scale_ = 1.0; + double traverse_rate_ = 0.0; double feed_ = 0.0; double spindle_ = 0.0; int spindle_direction_ = 0; int selected_tool_ = 0; + CncSimPose probe_position_{}; + bool probe_tripped_ = false; bool rtcp_enabled_ = false; double default_rtcp_tool_length_ = 0.0; double rtcp_tool_length_ = 0.0; int rtcp_h_code_ = 0; + CncSimPose tool_length_offset_{}; int kinematics_type_ = 1; + bool feed_override_ = false; + bool speed_override_ = false; + bool adaptive_feed_ = false; + bool feed_hold_ = false; + bool mist_on_ = false; + bool flood_on_ = false; std::unordered_map tool_lengths_; }; diff --git a/core/src/linuxcnc_canon_bridge.cpp b/core/src/linuxcnc_canon_bridge.cpp index 95c13da..30a6202 100644 --- a/core/src/linuxcnc_canon_bridge.cpp +++ b/core/src/linuxcnc_canon_bridge.cpp @@ -19,6 +19,11 @@ CanonEventSink *active_sink = nullptr; int active_line = 0; bool flood_on = false; bool mist_on = false; +bool block_delete = false; +bool optional_program_stop = false; +CANON_MOTION_MODE motion_control_mode = CANON_EXACT_STOP; +double motion_control_tolerance = 0.0; +double naivecam_tolerance = 0.0; std::string parameter_file_name = "rs274ngc.var"; CncSimPose make_pose(double x, double y, double z, @@ -102,14 +107,23 @@ void emit_state_event(CncSimEventType type, int reserved) { active_sink->emit_raw(event); } +void reset_bridge_state() { + flood_on = false; + mist_on = false; + block_delete = false; + optional_program_stop = false; + motion_control_mode = CANON_EXACT_STOP; + motion_control_tolerance = 0.0; + naivecam_tolerance = 0.0; +} + } // namespace void cnc_sim_linuxcnc_set_canon_sink(CanonEventSink *sink) { trace_call("cnc_sim_linuxcnc_set_canon_sink"); active_sink = sink; active_line = 0; - flood_on = false; - mist_on = false; + reset_bridge_state(); } CanonEventSink *cnc_sim_linuxcnc_get_canon_sink() { @@ -125,8 +139,7 @@ void INIT_CANON() { if (active_sink) { active_sink->reset(); } - flood_on = false; - mist_on = false; + reset_bridge_state(); } void USE_LENGTH_UNITS(CANON_UNITS units) { @@ -291,19 +304,24 @@ void CANON_UPDATE_END_POINT(double x, double y, double z, } } -void SET_TRAVERSE_RATE(double) { +void SET_TRAVERSE_RATE(double rate) { + if (active_sink) { + active_sink->set_traverse_rate(rate); + } } void SET_FEED_REFERENCE(CANON_FEED_REFERENCE) { } -void SET_FEED_MODE(int, int) { +void SET_FEED_MODE(int spindle, int mode) { if (active_sink) { - active_sink->set_feed_mode(0, event_line()); + active_sink->set_feed_mode(spindle, mode, event_line()); } } void SET_MOTION_CONTROL_MODE(CANON_MOTION_MODE mode, double tolerance) { + motion_control_mode = mode; + motion_control_tolerance = tolerance; int reserved = 0; switch (mode) { case CANON_EXACT_PATH: @@ -330,7 +348,8 @@ void SET_MOTION_CONTROL_MODE(CANON_MOTION_MODE mode, double tolerance) { } } -void SET_NAIVECAM_TOLERANCE(double) { +void SET_NAIVECAM_TOLERANCE(double tolerance) { + naivecam_tolerance = tolerance; } void SET_CUTTER_RADIUS_COMPENSATION(double) { @@ -342,10 +361,16 @@ void START_CUTTER_RADIUS_COMPENSATION(int) { void STOP_CUTTER_RADIUS_COMPENSATION() { } -void START_SPEED_FEED_SYNCH(int, double, bool) { +void START_SPEED_FEED_SYNCH(int spindle, double feed_per_revolution, bool velocity_mode) { + if (active_sink) { + active_sink->set_speed_feed_sync(spindle, feed_per_revolution, velocity_mode, true, event_line()); + } } void STOP_SPEED_FEED_SYNCH() { + if (active_sink) { + active_sink->set_speed_feed_sync(0, 0.0, false, false, event_line()); + } } void NURBS_G5_FEED(int lineno, const std::vector &points, unsigned int, CANON_PLANE) { @@ -378,7 +403,7 @@ void RIGID_TAP(int lineno, double x, double y, double z, double) { end.x = x; end.y = y; end.z = z; - active_sink->straight_feed(lineno, end); + active_sink->straight_feed(event_line(lineno), end); } } @@ -386,16 +411,21 @@ 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) { + unsigned char probe_type) { if (active_sink) { - active_sink->straight_probe(event_line(lineno), make_pose(x, y, z, a, b, c, u, v, w)); + active_sink->straight_probe(event_line(lineno), + make_pose(x, y, z, a, b, c, u, v, w), + probe_type); } } void STOP() { } -void SET_SPINDLE_MODE(int, double) { +void SET_SPINDLE_MODE(int spindle, double mode) { + if (active_sink) { + active_sink->set_spindle_mode(spindle, mode, event_line()); + } } void SPINDLE_RETRACT_TRAVERSE() { @@ -422,10 +452,16 @@ void STOP_SPINDLE_TURNING(int) { void SPINDLE_RETRACT() { } -void ORIENT_SPINDLE(int, double, int) { +void ORIENT_SPINDLE(int spindle, double orientation, int mode) { + if (active_sink) { + active_sink->orient_spindle(spindle, orientation, mode, event_line()); + } } -void WAIT_SPINDLE_ORIENT_COMPLETE(int, double) { +void WAIT_SPINDLE_ORIENT_COMPLETE(int spindle, double timeout) { + if (active_sink) { + active_sink->wait_spindle_orient_complete(spindle, timeout, event_line()); + } } void LOCK_SPINDLE_Z() { @@ -444,14 +480,16 @@ void USE_TOOL_LENGTH_OFFSET(const EmcPose &offset) { if (!active_sink) { return; } + const CncSimPose tool_offset = make_pose(offset.tran.x, offset.tran.y, offset.tran.z, + offset.a, offset.b, offset.c, + offset.u, offset.v, offset.w); + active_sink->set_tool_length_offset(tool_offset); CncSimEvent event{}; event.version = 1; event.type = CNC_SIM_EVENT_COMMENT; event.line = event_line(); event.reserved = 430; - event.start = make_pose(offset.tran.x, offset.tran.y, offset.tran.z, - offset.a, offset.b, offset.c, - offset.u, offset.v, offset.w); + event.start = tool_offset; active_sink->emit_raw(event); } @@ -482,36 +520,66 @@ void COMMENT(const char *message) { } void DISABLE_ADAPTIVE_FEED() { + if (active_sink) { + active_sink->set_override_control(52, false, 0, event_line()); + } } void ENABLE_ADAPTIVE_FEED() { + if (active_sink) { + active_sink->set_override_control(52, true, 0, event_line()); + } } void DISABLE_FEED_OVERRIDE() { + if (active_sink) { + active_sink->set_override_control(50, false, 0, event_line()); + } } void ENABLE_FEED_OVERRIDE() { + if (active_sink) { + active_sink->set_override_control(50, true, 0, event_line()); + } } -void DISABLE_SPEED_OVERRIDE(int) { +void DISABLE_SPEED_OVERRIDE(int spindle) { + if (active_sink) { + active_sink->set_override_control(51, false, spindle, event_line()); + } } -void ENABLE_SPEED_OVERRIDE(int) { +void ENABLE_SPEED_OVERRIDE(int spindle) { + if (active_sink) { + active_sink->set_override_control(51, true, spindle, event_line()); + } } void DISABLE_FEED_HOLD() { + if (active_sink) { + active_sink->set_override_control(53, false, 0, event_line()); + } } void ENABLE_FEED_HOLD() { + if (active_sink) { + active_sink->set_override_control(53, true, 0, event_line()); + } } void FLOOD_OFF() { flood_on = false; + if (active_sink) { + active_sink->set_flood_on(false); + } emit_state_event(CNC_SIM_EVENT_COMMENT, 20); } void FLOOD_ON() { flood_on = true; + if (active_sink) { + active_sink->set_flood_on(true); + } emit_state_event(CNC_SIM_EVENT_COMMENT, 21); } @@ -532,11 +600,17 @@ void LOGCLOSE() { void MIST_OFF() { mist_on = false; + if (active_sink) { + active_sink->set_mist_on(false); + } emit_state_event(CNC_SIM_EVENT_COMMENT, 10); } void MIST_ON() { mist_on = true; + if (active_sink) { + active_sink->set_mist_on(true); + } emit_state_event(CNC_SIM_EVENT_COMMENT, 11); } @@ -564,11 +638,12 @@ void NURB_CONTROL_POINT(int, double, double, double, double) { void NURB_FEED(double, double) { } -void SET_BLOCK_DELETE(bool) { +void SET_BLOCK_DELETE(bool enabled) { + block_delete = enabled; } bool GET_BLOCK_DELETE(void) { - return false; + return block_delete; } void OPTIONAL_PROGRAM_STOP() { @@ -577,11 +652,12 @@ void OPTIONAL_PROGRAM_STOP() { } } -void SET_OPTIONAL_PROGRAM_STOP(bool) { +void SET_OPTIONAL_PROGRAM_STOP(bool state) { + optional_program_stop = state; } bool GET_OPTIONAL_PROGRAM_STOP() { - return false; + return optional_program_stop; } void PROGRAM_STOP() { @@ -590,25 +666,46 @@ void PROGRAM_STOP() { } } -void SET_MOTION_OUTPUT_BIT(int) { +void SET_MOTION_OUTPUT_BIT(int index) { + if (active_sink) { + active_sink->set_digital_output(index, true, true, event_line()); + } } -void CLEAR_MOTION_OUTPUT_BIT(int) { +void CLEAR_MOTION_OUTPUT_BIT(int index) { + if (active_sink) { + active_sink->set_digital_output(index, false, true, event_line()); + } } -void SET_AUX_OUTPUT_BIT(int) { +void SET_AUX_OUTPUT_BIT(int index) { + if (active_sink) { + active_sink->set_digital_output(index, true, false, event_line()); + } } -void CLEAR_AUX_OUTPUT_BIT(int) { +void CLEAR_AUX_OUTPUT_BIT(int index) { + if (active_sink) { + active_sink->set_digital_output(index, false, false, event_line()); + } } -void SET_MOTION_OUTPUT_VALUE(int, double) { +void SET_MOTION_OUTPUT_VALUE(int index, double value) { + if (active_sink) { + active_sink->set_analog_output(index, value, true, event_line()); + } } -void SET_AUX_OUTPUT_VALUE(int, double) { +void SET_AUX_OUTPUT_VALUE(int index, double value) { + if (active_sink) { + active_sink->set_analog_output(index, value, false, event_line()); + } } -int WAIT(int, int, int wait_type, double) { +int WAIT(int index, int input_type, int wait_type, double timeout) { + if (active_sink) { + active_sink->wait_input(index, input_type, wait_type, timeout, event_line()); + } return wait_type; } @@ -645,15 +742,15 @@ int GET_EXTERNAL_MIST() { } CANON_MOTION_MODE GET_EXTERNAL_MOTION_CONTROL_MODE() { - return CANON_EXACT_STOP; + return motion_control_mode; } double GET_EXTERNAL_MOTION_CONTROL_TOLERANCE() { - return 0.0; + return motion_control_tolerance; } double GET_EXTERNAL_MOTION_CONTROL_NAIVECAM_TOLERANCE() { - return 0.0; + return naivecam_tolerance; } void GET_EXTERNAL_PARAMETER_FILE_NAME(char *filename, int max_size) { @@ -680,22 +777,22 @@ 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_POSITION_A() { return active_sink ? active_sink->probe_position().a : 0.0; } +double GET_EXTERNAL_PROBE_POSITION_B() { return active_sink ? active_sink->probe_position().b : 0.0; } +double GET_EXTERNAL_PROBE_POSITION_C() { return active_sink ? active_sink->probe_position().c : 0.0; } +double GET_EXTERNAL_PROBE_POSITION_X() { return active_sink ? active_sink->probe_position().x : 0.0; } +double GET_EXTERNAL_PROBE_POSITION_Y() { return active_sink ? active_sink->probe_position().y : 0.0; } +double GET_EXTERNAL_PROBE_POSITION_Z() { return active_sink ? active_sink->probe_position().z : 0.0; } +double GET_EXTERNAL_PROBE_POSITION_U() { return active_sink ? active_sink->probe_position().u : 0.0; } +double GET_EXTERNAL_PROBE_POSITION_V() { return active_sink ? active_sink->probe_position().v : 0.0; } +double GET_EXTERNAL_PROBE_POSITION_W() { return active_sink ? active_sink->probe_position().w : 0.0; } double GET_EXTERNAL_PROBE_VALUE() { return 0.0; } int GET_EXTERNAL_PROBE_TRIPPED_VALUE() { - return 0; + return active_sink && active_sink->probe_tripped() ? 1 : 0; } int GET_EXTERNAL_QUEUE_EMPTY() { @@ -719,15 +816,15 @@ CANON_DIRECTION GET_EXTERNAL_SPINDLE(int) { return 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; } +double GET_EXTERNAL_TOOL_LENGTH_XOFFSET() { return active_sink ? active_sink->tool_length_offset().x : 0.0; } +double GET_EXTERNAL_TOOL_LENGTH_YOFFSET() { return active_sink ? active_sink->tool_length_offset().y : 0.0; } +double GET_EXTERNAL_TOOL_LENGTH_ZOFFSET() { return active_sink ? active_sink->tool_length_offset().z : 0.0; } +double GET_EXTERNAL_TOOL_LENGTH_AOFFSET() { return active_sink ? active_sink->tool_length_offset().a : 0.0; } +double GET_EXTERNAL_TOOL_LENGTH_BOFFSET() { return active_sink ? active_sink->tool_length_offset().b : 0.0; } +double GET_EXTERNAL_TOOL_LENGTH_COFFSET() { return active_sink ? active_sink->tool_length_offset().c : 0.0; } +double GET_EXTERNAL_TOOL_LENGTH_UOFFSET() { return active_sink ? active_sink->tool_length_offset().u : 0.0; } +double GET_EXTERNAL_TOOL_LENGTH_VOFFSET() { return active_sink ? active_sink->tool_length_offset().v : 0.0; } +double GET_EXTERNAL_TOOL_LENGTH_WOFFSET() { return active_sink ? active_sink->tool_length_offset().w : 0.0; } int GET_EXTERNAL_TOOL_SLOT() { return active_sink ? active_sink->selected_tool() : 0; @@ -755,23 +852,23 @@ int GET_EXTERNAL_TC_REASON() { } double GET_EXTERNAL_TRAVERSE_RATE() { - return 0.0; + return active_sink ? active_sink->traverse_rate() : 0.0; } int GET_EXTERNAL_FEED_OVERRIDE_ENABLE() { - return 1; + return active_sink ? (active_sink->feed_override() ? 1 : 0) : 1; } int GET_EXTERNAL_SPINDLE_OVERRIDE_ENABLE(int) { - return 1; + return active_sink ? (active_sink->speed_override() ? 1 : 0) : 1; } int GET_EXTERNAL_ADAPTIVE_FEED_ENABLE() { - return 1; + return active_sink ? (active_sink->adaptive_feed() ? 1 : 0) : 1; } int GET_EXTERNAL_FEED_HOLD_ENABLE() { - return 1; + return active_sink ? (active_sink->feed_hold() ? 1 : 0) : 1; } int GET_EXTERNAL_DIGITAL_INPUT(int, int def) { diff --git a/core/src/smoke_gcode_parser.cpp b/core/src/smoke_gcode_parser.cpp index 7ba791a..a5aeb33 100644 --- a/core/src/smoke_gcode_parser.cpp +++ b/core/src/smoke_gcode_parser.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -34,6 +35,13 @@ struct CallFrame { size_t sub_start; int remaining; bool oword; + bool has_modal_state; + bool restore_modal_on_return; + bool modal_absolute; + bool modal_arc_absolute; + bool modal_lathe_diameter_mode; + int modal_plane; + double modal_unit_scale; std::array params; std::unordered_map named_params; }; @@ -49,6 +57,12 @@ struct LoopFrame { bool repeat_initialized; }; +struct PendingParameterAssignment { + int index; + std::string name; + double value; +}; + enum class OwordKind { None, If, @@ -71,6 +85,14 @@ enum class OwordKind { constexpr size_t kMaxSubprogramDepth = 16; constexpr int kMaxOwordLoopIterations = 100000; constexpr int kMaxNumberedParameter = 5602; +constexpr int kModalMotionG33 = 33; +constexpr int kModalMotionG331 = 331; +constexpr int kModalMotionG76 = 76; +constexpr int kCutterCompOffEvent = 400; +constexpr int kCutterCompLeftEvent = 410; +constexpr int kCutterCompRightEvent = 420; +constexpr int kCutterCompLeftExplicitEvent = 411; +constexpr int kCutterCompRightExplicitEvent = 421; std::string trim(const std::string &value) { auto first = std::find_if_not(value.begin(), value.end(), [](unsigned char ch) { return std::isspace(ch); }); @@ -81,6 +103,163 @@ std::string trim(const std::string &value) { return std::string(first, last); } +std::string uppercase(std::string value) { + std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) { + return static_cast(std::toupper(ch)); + }); + return value; +} + +std::string normalize_named_parameter(std::string name) { + name = trim(name); + std::string normalized; + normalized.reserve(name.size()); + for (unsigned char ch : name) { + if (std::isspace(ch)) { + continue; + } + normalized.push_back(static_cast(std::tolower(ch))); + } + return normalized; +} + +std::string ini_lookup_key(std::string value) { + value = trim(value); + std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) { + return static_cast(std::toupper(ch)); + }); + return value; +} + +std::string hal_lookup_key(std::string value) { + value = trim(value); + std::string normalized; + normalized.reserve(value.size()); + for (unsigned char ch : value) { + if (std::isspace(ch) || ch == '[' || ch == ']') { + continue; + } + normalized.push_back(static_cast(std::tolower(ch))); + } + return normalized; +} + +bool parse_smoke_ini_named_parameter(const std::string &name, + std::string *section, + std::string *key) { + if (name.rfind("_ini[", 0) != 0) { + return false; + } + const size_t close = name.find(']', 5); + if (close == std::string::npos || close + 1 >= name.size()) { + return false; + } + *section = ini_lookup_key(name.substr(5, close - 5)); + *key = ini_lookup_key(name.substr(close + 1)); + return !section->empty() && !key->empty(); +} + +bool parse_smoke_hal_named_parameter(const std::string &name, std::string *pin) { + if (name.rfind("_hal[", 0) != 0) { + return false; + } + const size_t close = name.find(']', 5); + if (close == std::string::npos || close + 1 != name.size()) { + return false; + } + *pin = hal_lookup_key(name.substr(5, close - 5)); + return !pin->empty(); +} + +bool smoke_ini_value(const std::string §ion, const std::string &key, double *value) { + const char *path = std::getenv("INI_FILE_NAME"); + if (!path || !*path) { + return false; + } + std::ifstream file(path); + if (!file) { + return false; + } + + std::string current_section; + std::string line; + while (std::getline(file, line)) { + std::string text = trim(line); + if (text.empty() || text[0] == '#' || text[0] == ';') { + continue; + } + if (text.front() == '[' && text.find(']') != std::string::npos) { + current_section = ini_lookup_key(text.substr(1, text.find(']') - 1)); + continue; + } + if (current_section != section) { + continue; + } + const size_t equals = text.find('='); + if (equals == std::string::npos) { + continue; + } + if (ini_lookup_key(text.substr(0, equals)) != key) { + continue; + } + char *end = nullptr; + const std::string raw_value = trim(text.substr(equals + 1)); + const double parsed = std::strtod(raw_value.c_str(), &end); + if (end == raw_value.c_str()) { + return false; + } + *value = parsed; + return true; + } + return false; +} + +bool smoke_hal_value(const std::string &pin, double *value) { + const char *path = std::getenv("HAL_PIN_FILE"); + if (path && *path) { + std::ifstream file(path); + if (!file) { + return false; + } + std::string line; + while (std::getline(file, line)) { + std::string text = trim(line); + if (text.empty() || text[0] == '#' || text[0] == ';') { + continue; + } + const size_t equals = text.find('='); + if (equals == std::string::npos) { + continue; + } + if (hal_lookup_key(text.substr(0, equals)) != pin) { + continue; + } + char *end = nullptr; + const std::string raw_value = trim(text.substr(equals + 1)); + const double parsed = std::strtod(raw_value.c_str(), &end); + if (end == raw_value.c_str()) { + return false; + } + *value = parsed; + return true; + } + return false; + } + + const std::string env_name = "HAL_PIN_" + pin; + const char *env_value = std::getenv(env_name.c_str()); + if (!env_value || !*env_value) { + return false; + } + char *end = nullptr; + const double parsed = std::strtod(env_value, &end); + if (end == env_value) { + return false; + } + *value = parsed; + return true; +} + std::string strip_comments(const std::string &line, std::string *comment) { std::string out; bool in_paren = false; @@ -115,10 +294,67 @@ std::string strip_comments(const std::string &line, std::string *comment) { return out; } -std::vector parse_word_list(const std::string &line) { - std::vector words; +bool parse_linuxcnc_real_number(const char *text, double *value, size_t *consumed); + +bool parse_linuxcnc_real_value(const char *text, double *value, size_t *consumed) { + if (!text || text[0] == '\0') { + return false; + } + const char c = text[0]; + const char c1 = text[1]; + if (c == '+' && c1 != '\0' && !std::isdigit(static_cast(c1)) && c1 != '.') { + size_t nested_consumed = 0; + if (!parse_linuxcnc_real_value(text + 1, value, &nested_consumed)) { + return false; + } + *consumed = nested_consumed + 1; + return true; + } + if (c == '-' && c1 != '\0' && !std::isdigit(static_cast(c1)) && c1 != '.') { + size_t nested_consumed = 0; + if (!parse_linuxcnc_real_value(text + 1, value, &nested_consumed)) { + return false; + } + *value = -*value; + *consumed = nested_consumed + 1; + return true; + } + return parse_linuxcnc_real_number(text, value, consumed); +} + +bool parse_linuxcnc_real_number(const char *text, double *value, size_t *consumed) { + size_t end = 0; + while (text[end] == '+' || text[end] == '-') { + ++end; + } + const size_t digits_start = end; + while (std::isdigit(static_cast(text[end])) || text[end] == '.') { + ++end; + } + if (end == digits_start) { + return false; + } + const std::string raw(text, text + end); + std::istringstream input(raw); + double parsed = 0.0; + if (!(input >> parsed)) { + return false; + } + if (input.get() != std::char_traits::eof()) { + return false; + } + *value = parsed; + *consumed = end; + return true; +} + +bool looks_like_real_start(char ch) { + return std::isdigit(static_cast(ch)) || ch == '+' || ch == '-' || ch == '.'; +} + +bool parse_word_list_impl(const std::string &line, std::vector *words, std::string *error, bool strict) { + words->clear(); const char *text = line.c_str(); - char *end = nullptr; for (size_t i = 0; text[i] != '\0';) { if (std::isspace(static_cast(text[i]))) { ++i; @@ -130,16 +366,48 @@ std::vector parse_word_list(const std::string &line) { continue; } ++i; - double value = std::strtod(text + i, &end); - if (end == text + i) { + if (letter == 'O') { + if (!text[i] || text[i] == '<' || std::isalpha(static_cast(text[i]))) { + continue; + } + } + double value = 0.0; + size_t consumed = 0; + if (!parse_linuxcnc_real_value(text + i, &value, &consumed)) { + if (strict && looks_like_real_start(text[i])) { + if (error) { + const std::string token(text + i); + *error = "bad number format (conversion failed) parsing '" + token + "'"; + } + return false; + } continue; } - words.push_back({letter, value}); - i = static_cast(end - text); + const size_t next = i + consumed; + if (strict && text[next] != '\0' && looks_like_real_start(text[next])) { + if (error) { + const std::string token(text + i, consumed); + *error = "bad number format (trailing characters) parsing '" + token + "'"; + } + return false; + } + words->push_back({letter, value}); + i = next; } + return true; +} + +std::vector parse_word_list(const std::string &line) { + std::vector words; + std::string error; + parse_word_list_impl(line, &words, &error, false); return words; } +bool parse_word_list_strict(const std::string &line, std::vector *words, std::string *error) { + return parse_word_list_impl(line, words, error, true); +} + std::unordered_map last_words_by_letter(const std::vector &word_list) { std::unordered_map words; for (const Word &word : word_list) { @@ -194,7 +462,7 @@ std::string first_o_identifier(const std::string &line, const std::vector if (close == std::string::npos) { continue; } - const std::string name = trim(line.substr(pos + 1, close - pos - 1)); + const std::string name = normalize_named_parameter(line.substr(pos + 1, close - pos - 1)); if (!name.empty()) { return "S:" + name; } @@ -314,33 +582,84 @@ OwordKind oword_kind(const std::string &line) { return OwordKind::None; } -double parameter_value(const std::vector &call_stack, - const std::unordered_map ¶meters, - int index) { +bool parameter_value(const std::vector &call_stack, + const std::unordered_map ¶meters, + int index, + double *value) { if (index > 0 && index <= 30 && !call_stack.empty() && call_stack.back().oword) { - return call_stack.back().params[static_cast(index)]; + *value = call_stack.back().params[static_cast(index)]; + return true; } const auto it = parameters.find(index); if (it != parameters.end()) { - return it->second; + *value = it->second; + return true; } - return 0.0; + *value = 0.0; + return true; } -double named_parameter_value(const std::vector &call_stack, - const std::unordered_map &named_parameters, - const std::string &name) { +bool named_parameter_exists(const std::vector &call_stack, + const std::unordered_map &named_parameters, + const std::string &name) { + std::string hal_pin; + if (parse_smoke_hal_named_parameter(name, &hal_pin)) { + double value = 0.0; + return smoke_hal_value(hal_pin, &value); + } + std::string ini_section; + std::string ini_key; + if (parse_smoke_ini_named_parameter(name, &ini_section, &ini_key)) { + double value = 0.0; + return smoke_ini_value(ini_section, ini_key, &value); + } if (!call_stack.empty() && call_stack.back().oword) { const auto local = call_stack.back().named_params.find(name); if (local != call_stack.back().named_params.end()) { - return local->second; + return true; + } + } + return named_parameters.find(name) != named_parameters.end(); +} + +bool named_parameter_value(const std::vector &call_stack, + const std::unordered_map &named_parameters, + const std::string &name, + double *value) { + std::string hal_pin; + if (parse_smoke_hal_named_parameter(name, &hal_pin)) { + return smoke_hal_value(hal_pin, value); + } + std::string ini_section; + std::string ini_key; + if (parse_smoke_ini_named_parameter(name, &ini_section, &ini_key)) { + return smoke_ini_value(ini_section, ini_key, value); + } + if (!call_stack.empty() && call_stack.back().oword) { + const auto local = call_stack.back().named_params.find(name); + if (local != call_stack.back().named_params.end()) { + *value = local->second; + return true; } } const auto global = named_parameters.find(name); if (global != named_parameters.end()) { - return global->second; + *value = global->second; + return true; } - return 0.0; + *value = 0.0; + return false; +} + +bool indexed_parameter_value(const std::vector &call_stack, + const std::unordered_map ¶meters, + double index_value, + double *value) { + const int index = rounded_value(index_value); + if (index <= 0 || index >= kMaxNumberedParameter) { + return false; + } + return parameter_value(call_stack, parameters, index, value); } size_t find_matching_square_bracket(const std::string &text, size_t open) { @@ -379,6 +698,81 @@ bool truthy(double value) { return value != 0.0; } +bool is_smoke_readonly_named_parameter(const std::string &name) { + return name == "_task" || + name == "_TASK" || + name == "_LATHE_DIAMETER_MODE" || + name == "_lathe_diameter_mode" || + name == "_LATHE_RADIUS_MODE" || + name == "_lathe_radius_mode" || + name == "_METRIC" || + name == "_metric" || + name == "_IMPERIAL" || + name == "_imperial" || + name == "_ABSOLUTE" || + name == "_absolute" || + name == "_INCREMENTAL" || + name == "_incremental" || + name == "_FEED" || + name == "_feed" || + name == "_RPM" || + name == "_rpm" || + name == "_SPINDLE_ON" || + name == "_spindle_on" || + name == "_SPINDLE_CW" || + name == "_spindle_cw" || + name == "_MOTION_MODE" || + name == "_motion_mode" || + name == "_PLANE" || + name == "_plane" || + name == "_CURRENT_TOOL" || + name == "_current_tool" || + name == "_CURRENT_POCKET" || + name == "_current_pocket" || + name == "_SELECTED_TOOL" || + name == "_selected_tool" || + name == "_SELECTED_POCKET" || + name == "_selected_pocket" || + name == "_TOOL_OFFSET" || + name == "_tool_offset" || + name == "_X" || + name == "_x" || + name == "_Y" || + name == "_y" || + name == "_Z" || + name == "_z" || + name == "_A" || + name == "_a" || + name == "_B" || + name == "_b" || + name == "_C" || + name == "_c" || + name == "_U" || + name == "_u" || + name == "_V" || + name == "_v" || + name == "_W" || + name == "_w" || + name == "_ABS_X" || + name == "_abs_x" || + name == "_ABS_Y" || + name == "_abs_y" || + name == "_ABS_Z" || + name == "_abs_z" || + name == "_ABS_A" || + name == "_abs_a" || + name == "_ABS_B" || + name == "_abs_b" || + name == "_ABS_C" || + name == "_abs_c" || + name == "_ABS_U" || + name == "_abs_u" || + name == "_ABS_V" || + name == "_abs_v" || + name == "_ABS_W" || + name == "_abs_w"; +} + class ExpressionParser { public: ExpressionParser(const std::string &text, @@ -524,13 +918,6 @@ private: double parse_primary() { skip_space(); - if (consume('(')) { - const double value = parse_logical(); - if (!consume(')')) { - ok_ = false; - } - return value; - } if (consume('[')) { const double value = parse_logical(); if (!consume(']')) { @@ -539,34 +926,20 @@ private: return value; } if (consume('#')) { - if (consume('<')) { - const size_t start = pos_; - while (pos_ < text_.size() && text_[pos_] != '>') { - ++pos_; - } - if (pos_ >= text_.size() || text_[pos_] != '>') { - ok_ = false; - return 0.0; - } - const std::string name = trim(text_.substr(start, pos_ - start)); - ++pos_; - if (name.empty()) { - ok_ = false; - return 0.0; - } - return named_parameter_value(call_stack_, named_parameters_, name); + return parse_parameter_reference(); + } + if (pos_ < text_.size() && std::isalpha(static_cast(text_[pos_]))) { + const std::string name = uppercase(parse_identifier()); + if (name == "EXISTS") { + return parse_exists_function(); } - const int index = parse_integer(); - if (index <= 0 || index >= kMaxNumberedParameter) { + if (!consume('[')) { ok_ = false; return 0.0; } - return parameter_value(call_stack_, parameters_, index); - } - if (pos_ < text_.size() && std::isalpha(static_cast(text_[pos_]))) { - const std::string name = parse_identifier(); - const double argument = parse_primary(); - if (!ok_) { + const double argument = parse_logical(); + if (!consume(']') || !ok_) { + ok_ = false; return 0.0; } if (name == "ABS") { @@ -633,13 +1006,13 @@ private: return 0.0; } - char *end = nullptr; - const double value = std::strtod(text_.c_str() + pos_, &end); - if (end == text_.c_str() + pos_) { + double value = 0.0; + size_t consumed = 0; + if (!parse_linuxcnc_real_value(text_.c_str() + pos_, &value, &consumed)) { ok_ = false; return 0.0; } - pos_ = static_cast(end - text_.c_str()); + pos_ += consumed; return value; } @@ -667,6 +1040,110 @@ private: return value; } + double parse_parameter_reference() { + if (consume('#')) { + const double index_value = parse_parameter_reference(); + double value = 0.0; + if (!ok_ || !indexed_parameter_value(call_stack_, parameters_, index_value, &value)) { + ok_ = false; + return 0.0; + } + return value; + } + if (consume('[')) { + const double index_value = parse_logical(); + if (!consume(']')) { + ok_ = false; + return 0.0; + } + double value = 0.0; + if (!indexed_parameter_value(call_stack_, parameters_, index_value, &value)) { + ok_ = false; + return 0.0; + } + return value; + } + if (consume('<')) { + const size_t start = pos_; + while (pos_ < text_.size() && text_[pos_] != '>') { + ++pos_; + } + if (pos_ >= text_.size() || text_[pos_] != '>') { + ok_ = false; + return 0.0; + } + const std::string name = normalize_named_parameter(text_.substr(start, pos_ - start)); + ++pos_; + if (name.empty()) { + ok_ = false; + return 0.0; + } + double value = 0.0; + if (!named_parameter_value(call_stack_, named_parameters_, name, &value)) { + ok_ = false; + return 0.0; + } + return value; + } + const int index = parse_integer(); + if (index <= 0 || index >= kMaxNumberedParameter) { + ok_ = false; + return 0.0; + } + double value = 0.0; + if (!parameter_value(call_stack_, parameters_, index, &value)) { + ok_ = false; + return 0.0; + } + return value; + } + + double parse_exists_function() { + if (!consume('[')) { + ok_ = false; + return 0.0; + } + if (!consume('#')) { + ok_ = false; + return 0.0; + } + if (consume('<')) { + const size_t start = pos_; + while (pos_ < text_.size() && text_[pos_] != '>') { + ++pos_; + } + if (pos_ >= text_.size() || text_[pos_] != '>') { + ok_ = false; + return 0.0; + } + const std::string name = normalize_named_parameter(text_.substr(start, pos_ - start)); + ++pos_; + if (name.empty() || !consume(']')) { + ok_ = false; + return 0.0; + } + return named_parameter_exists(call_stack_, named_parameters_, name) ? 1.0 : 0.0; + } + double index_value = 0.0; + if (consume('#')) { + index_value = parse_parameter_reference(); + } else if (consume('[')) { + index_value = parse_logical(); + if (!consume(']')) { + ok_ = false; + return 0.0; + } + } else { + index_value = parse_primary(); + } + if (!ok_ || !consume(']')) { + ok_ = false; + return 0.0; + } + const int index = rounded_value(index_value); + return index >= 1 && index < kMaxNumberedParameter ? 1.0 : 0.0; + } + bool consume(char ch) { skip_space(); if (pos_ < text_.size() && text_[pos_] == ch) { @@ -963,6 +1440,50 @@ std::string number_to_string(double value) { return out.str(); } +bool unary_function_span(const std::string &line, size_t start, size_t *end) { + static const std::array kUnaryFunctions = { + "ABS", "ACOS", "ASIN", "ATAN", "COS", "EXP", "FIX", + "FUP", "LN", "ROUND", "SIN", "SQRT", "TAN"}; + std::string upper_line = uppercase(line.substr(start)); + for (const char *name : kUnaryFunctions) { + const size_t name_len = std::char_traits::length(name); + if (upper_line.rfind(name, 0) != 0 || start + name_len >= line.size() || + line[start + name_len] != '[') { + continue; + } + const size_t first_close = find_matching_square_bracket(line, start + name_len); + if (first_close == std::string::npos) { + return false; + } + if (std::string(name) == "ATAN") { + size_t slash = first_close + 1; + while (slash < line.size() && std::isspace(static_cast(line[slash]))) { + ++slash; + } + if (slash >= line.size() || line[slash] != '/') { + return false; + } + size_t second_open = slash + 1; + while (second_open < line.size() && + std::isspace(static_cast(line[second_open]))) { + ++second_open; + } + if (second_open >= line.size() || line[second_open] != '[') { + return false; + } + const size_t second_close = find_matching_square_bracket(line, second_open); + if (second_close == std::string::npos) { + return false; + } + *end = second_close + 1; + return true; + } + *end = first_close + 1; + return true; + } + return false; +} + std::string expand_expressions_and_parameters(const std::string &line, const std::vector &call_stack, const std::unordered_map ¶meters, @@ -971,6 +1492,26 @@ std::string expand_expressions_and_parameters(const std::string &line, std::string out; out.reserve(line.size()); for (size_t i = 0; i < line.size();) { + if (std::isalpha(static_cast(line[i])) && i + 1 < line.size()) { + size_t function_end = 0; + if (unary_function_span(line, i + 1, &function_end)) { + double value = 0.0; + if (!evaluate_expression(line.substr(i + 1, function_end - i - 1), + call_stack, + parameters, + named_parameters, + &value)) { + if (error) { + *error = "unsupported word unary function expression"; + } + return {}; + } + out.push_back(line[i]); + out += number_to_string(value); + i = function_end; + continue; + } + } if (line[i] == '[') { const size_t close = find_matching_square_bracket(line, i); if (close == std::string::npos) { @@ -998,6 +1539,72 @@ std::string expand_expressions_and_parameters(const std::string &line, } size_t j = i + 1; + if (j < line.size() && line[j] == '#') { + size_t end = j + 1; + if (end < line.size() && line[end] == '[') { + const size_t close = find_matching_square_bracket(line, end); + if (close == std::string::npos) { + if (error) { + *error = "unterminated indirect parameter expression"; + } + return {}; + } + end = close + 1; + } else if (end < line.size() && line[end] == '<') { + const size_t close = line.find('>', end + 1); + if (close == std::string::npos) { + if (error) { + *error = "unterminated indirect named parameter expression"; + } + return {}; + } + end = close + 1; + } else { + while (end < line.size() && std::isdigit(static_cast(line[end]))) { + ++end; + } + } + double value = 0.0; + if (!evaluate_expression(line.substr(i, end - i), call_stack, parameters, named_parameters, &value)) { + if (error) { + *error = "unsupported indirect parameter expression"; + } + return {}; + } + out += number_to_string(value); + i = end; + continue; + } + if (j < line.size() && line[j] == '[') { + const size_t close = find_matching_square_bracket(line, j); + if (close == std::string::npos) { + if (error) { + *error = "unterminated indexed parameter expression"; + } + return {}; + } + double index_value = 0.0; + if (!evaluate_expression(line.substr(j + 1, close - j - 1), + call_stack, + parameters, + named_parameters, + &index_value)) { + if (error) { + *error = "unsupported indexed parameter expression"; + } + return {}; + } + double value = 0.0; + if (!indexed_parameter_value(call_stack, parameters, index_value, &value)) { + if (error) { + *error = "indexed parameter is out of range"; + } + return {}; + } + out += number_to_string(value); + i = close + 1; + continue; + } if (j < line.size() && line[j] == '<') { const size_t close = line.find('>', j + 1); if (close == std::string::npos) { @@ -1006,14 +1613,21 @@ std::string expand_expressions_and_parameters(const std::string &line, } return {}; } - const std::string name = trim(line.substr(j + 1, close - j - 1)); + const std::string name = normalize_named_parameter(line.substr(j + 1, close - j - 1)); if (name.empty()) { if (error) { *error = "empty named parameter expression"; } return {}; } - out += number_to_string(named_parameter_value(call_stack, named_parameters, name)); + double value = 0.0; + if (!named_parameter_value(call_stack, named_parameters, name, &value)) { + if (error) { + *error = "named parameter is not defined"; + } + return {}; + } + out += number_to_string(value); i = close + 1; continue; } @@ -1033,7 +1647,14 @@ std::string expand_expressions_and_parameters(const std::string &line, } return {}; } - out += number_to_string(parameter_value(call_stack, parameters, index)); + double value = 0.0; + if (!parameter_value(call_stack, parameters, index, &value)) { + if (error) { + *error = "numbered parameter is not defined"; + } + return {}; + } + out += number_to_string(value); i = j; } return out; @@ -1077,6 +1698,8 @@ bool parse_parameter_assignment(const std::string &line, int *index, std::string *name, double *value, + size_t *consumed_chars, + std::string *remaining_line, std::string *error) { const std::string text = trim(line); if (text.empty() || text[0] != '#') { @@ -1084,6 +1707,7 @@ bool parse_parameter_assignment(const std::string &line, } size_t pos = 1; + int parsed_index = 0; std::string parsed_name; if (pos < text.size() && text[pos] == '<') { const size_t close = text.find('>', pos + 1); @@ -1093,7 +1717,7 @@ bool parse_parameter_assignment(const std::string &line, } return true; } - parsed_name = trim(text.substr(pos + 1, close - pos - 1)); + parsed_name = normalize_named_parameter(text.substr(pos + 1, close - pos - 1)); if (parsed_name.empty()) { if (error) { *error = "empty named parameter assignment"; @@ -1101,15 +1725,47 @@ bool parse_parameter_assignment(const std::string &line, return true; } pos = close + 1; + } else if (pos < text.size() && text[pos] == '[') { + const size_t close = find_matching_square_bracket(text, pos); + if (close == std::string::npos) { + if (error) { + *error = "unterminated indexed parameter assignment"; + } + return true; + } + double index_value = 0.0; + if (!evaluate_expression(text.substr(pos + 1, close - pos - 1), + call_stack, + parameters, + named_parameters, + &index_value)) { + if (error) { + *error = "unsupported indexed parameter assignment expression"; + } + return true; + } + parsed_index = rounded_value(index_value); + if (std::fabs(index_value - parsed_index) > 0.0001) { + if (error) { + *error = "indexed parameter assignment must evaluate to an integer"; + } + return true; + } + if (parsed_index <= 0 || parsed_index >= kMaxNumberedParameter) { + if (error) { + *error = "numbered parameter assignment is out of range"; + } + return true; + } + pos = close + 1; } if (pos >= text.size() || !std::isdigit(static_cast(text[pos]))) { - if (parsed_name.empty()) { + if (parsed_name.empty() && parsed_index == 0) { return false; } } - int parsed_index = 0; - if (parsed_name.empty()) { + if (parsed_name.empty() && parsed_index == 0) { while (pos < text.size() && std::isdigit(static_cast(text[pos]))) { parsed_index = parsed_index * 10 + (text[pos] - '0'); ++pos; @@ -1128,15 +1784,33 @@ bool parse_parameter_assignment(const std::string &line, return false; } ++pos; - std::string rhs = trim(text.substr(pos)); - if (rhs.empty()) { + while (pos < text.size() && std::isspace(static_cast(text[pos]))) { + ++pos; + } + if (pos >= text.size()) { if (error) { *error = "parameter assignment requires a value"; } return true; } - if (rhs.front() == '[' && rhs.back() == ']') { - rhs = rhs.substr(1, rhs.size() - 2); + + size_t value_end = pos; + std::string rhs; + if (text[pos] == '[') { + const size_t close = find_matching_square_bracket(text, pos); + if (close == std::string::npos) { + if (error) { + *error = "unterminated parameter assignment expression"; + } + return true; + } + rhs = text.substr(pos + 1, close - pos - 1); + value_end = close + 1; + } else { + while (value_end < text.size() && !std::isspace(static_cast(text[value_end]))) { + ++value_end; + } + rhs = text.substr(pos, value_end - pos); } double parsed_value = 0.0; @@ -1149,6 +1823,79 @@ bool parse_parameter_assignment(const std::string &line, *index = parsed_index; *name = parsed_name; *value = parsed_value; + if (consumed_chars) { + *consumed_chars = value_end; + } + if (remaining_line) { + *remaining_line = trim(text.substr(value_end)); + } + return true; +} + +bool extract_parameter_assignments(const std::string &line, + const std::vector &call_stack, + const std::unordered_map ¶meters, + const std::unordered_map &named_parameters, + std::vector *assignments, + std::string *gcode_line, + std::string *error) { + std::string out; + out.reserve(line.size()); + for (size_t i = 0; i < line.size();) { + if (line[i] == '[') { + const size_t close = find_matching_square_bracket(line, i); + if (close == std::string::npos) { + out.push_back(line[i++]); + continue; + } + out.append(line, i, close - i + 1); + i = close + 1; + continue; + } + if (line[i] != '#') { + out.push_back(line[i++]); + continue; + } + if (i > 0 && !std::isspace(static_cast(line[i - 1]))) { + out.push_back(line[i++]); + continue; + } + + int assigned_parameter = 0; + std::string assigned_named_parameter; + double assigned_value = 0.0; + std::string remaining_after_assignment; + size_t consumed = 0; + std::string parameter_error; + if (!parse_parameter_assignment(line.substr(i), + call_stack, + parameters, + named_parameters, + &assigned_parameter, + &assigned_named_parameter, + &assigned_value, + &consumed, + &remaining_after_assignment, + ¶meter_error)) { + out.push_back(line[i++]); + continue; + } + if (!parameter_error.empty()) { + if (error) { + *error = parameter_error; + } + return false; + } + if (!assigned_named_parameter.empty() && is_smoke_readonly_named_parameter(assigned_named_parameter)) { + if (error) { + *error = "attempt to assign read-only named parameter"; + } + return false; + } + assignments->push_back({assigned_parameter, assigned_named_parameter, assigned_value}); + i += consumed; + } + *gcode_line = trim(out); return true; } @@ -1226,12 +1973,114 @@ bool has_uvw_word(const std::unordered_map &words) { words.find('W') != words.end(); } -void apply_axis(CncSimPose *pose, const std::unordered_map &words, char axis, double CncSimPose::*member, bool absolute, double scale) { +struct CycleAxes { + char first_word; + char second_word; + char depth_word; + double CncSimPose::*first; + double CncSimPose::*second; + double CncSimPose::*depth; +}; + +CycleAxes cycle_axes_for_plane(int plane) { + if (plane == 18) { + return CycleAxes{'Z', 'X', 'Y', &CncSimPose::z, &CncSimPose::x, &CncSimPose::y}; + } + if (plane == 19) { + return CycleAxes{'Y', 'Z', 'X', &CncSimPose::y, &CncSimPose::z, &CncSimPose::x}; + } + return CycleAxes{'X', 'Y', 'Z', &CncSimPose::x, &CncSimPose::y, &CncSimPose::z}; +} + +struct G87CycleValues { + double first_offset; + double second_offset; + double middle_depth; +}; + +G87CycleValues g87_cycle_values_for_plane(int plane, double i, double j, double k) { + if (plane == 18) { + return G87CycleValues{k, i, j}; + } + if (plane == 19) { + return G87CycleValues{j, k, i}; + } + return G87CycleValues{i, j, k}; +} + +CncSimPose g74_g84_pose_for_plane(int plane, const CncSimPose &hole, const CycleAxes &axes, double depth) { + const double first = hole.*axes.first; + const double second = hole.*axes.second; + CncSimPose pose = hole; + if (plane == 18) { + pose.x = depth; + pose.y = first; + pose.z = second; + } else if (plane == 19) { + pose.x = second; + pose.y = depth; + pose.z = first; + } else { + pose.x = first; + pose.y = second; + pose.z = depth; + } + return pose; +} + +bool has_cycle_position_word_for_plane(const std::unordered_map &words, const CycleAxes &axes) { + return words.find(axes.first_word) != words.end() || + words.find(axes.second_word) != words.end(); +} + +bool has_disallowed_cycle_axis_word(const std::unordered_map &words, const CycleAxes &axes) { + static constexpr char axes_words[] = {'X', 'Y', 'Z', 'A', 'B', 'C', 'U', 'V', 'W'}; + for (char axis : axes_words) { + if (axis == axes.first_word || axis == axes.second_word || axis == axes.depth_word) { + continue; + } + if (words.find(axis) != words.end()) { + return true; + } + } + return false; +} + +double pose_axis(const CncSimPose &pose, double CncSimPose::*member) { + return pose.*member; +} + +void set_pose_axis(CncSimPose *pose, double CncSimPose::*member, double value) { + pose->*member = value; +} + +double coordinate_word_value(char axis, double value, double scale, bool lathe_diameter_mode) { + if (axis == 'A' || axis == 'B' || axis == 'C') { + return value; + } + const double scaled = value * scale; + return axis == 'X' && lathe_diameter_mode ? scaled * 0.5 : scaled; +} + +double coordinate_word_value(const std::unordered_map &words, + char axis, + double scale, + bool lathe_diameter_mode) { + return coordinate_word_value(axis, words.at(axis), scale, lathe_diameter_mode); +} + +void apply_axis(CncSimPose *pose, + const std::unordered_map &words, + char axis, + double CncSimPose::*member, + bool absolute, + double scale, + bool lathe_diameter_mode) { auto it = words.find(axis); if (it == words.end()) { return; } - const double value = it->second * scale; + const double value = coordinate_word_value(axis, it->second, scale, lathe_diameter_mode); if (absolute) { pose->*member = value; } else { @@ -1239,27 +2088,33 @@ void apply_axis(CncSimPose *pose, const std::unordered_map &words, } } -void apply_axes(CncSimPose *pose, const std::unordered_map &words, bool absolute, double scale) { - apply_axis(pose, words, 'X', &CncSimPose::x, absolute, scale); - apply_axis(pose, words, 'Y', &CncSimPose::y, absolute, scale); - apply_axis(pose, words, 'Z', &CncSimPose::z, absolute, scale); - apply_axis(pose, words, 'A', &CncSimPose::a, absolute, 1.0); - apply_axis(pose, words, 'B', &CncSimPose::b, absolute, 1.0); - apply_axis(pose, words, 'C', &CncSimPose::c, absolute, 1.0); - apply_axis(pose, words, 'U', &CncSimPose::u, absolute, scale); - apply_axis(pose, words, 'V', &CncSimPose::v, absolute, scale); - apply_axis(pose, words, 'W', &CncSimPose::w, absolute, scale); +void apply_axes(CncSimPose *pose, + const std::unordered_map &words, + bool absolute, + double scale, + bool lathe_diameter_mode) { + apply_axis(pose, words, 'X', &CncSimPose::x, absolute, scale, lathe_diameter_mode); + apply_axis(pose, words, 'Y', &CncSimPose::y, absolute, scale, lathe_diameter_mode); + apply_axis(pose, words, 'Z', &CncSimPose::z, absolute, scale, lathe_diameter_mode); + apply_axis(pose, words, 'A', &CncSimPose::a, absolute, scale, lathe_diameter_mode); + apply_axis(pose, words, 'B', &CncSimPose::b, absolute, scale, lathe_diameter_mode); + apply_axis(pose, words, 'C', &CncSimPose::c, absolute, scale, lathe_diameter_mode); + apply_axis(pose, words, 'U', &CncSimPose::u, absolute, scale, lathe_diameter_mode); + apply_axis(pose, words, 'V', &CncSimPose::v, absolute, scale, lathe_diameter_mode); + apply_axis(pose, words, 'W', &CncSimPose::w, absolute, scale, lathe_diameter_mode); } -void apply_cycle_position_axes(CncSimPose *pose, const std::unordered_map &words, bool absolute, double scale) { - apply_axis(pose, words, 'X', &CncSimPose::x, absolute, scale); - apply_axis(pose, words, 'Y', &CncSimPose::y, absolute, scale); - apply_axis(pose, words, 'U', &CncSimPose::u, absolute, scale); - apply_axis(pose, words, 'V', &CncSimPose::v, absolute, scale); - apply_axis(pose, words, 'W', &CncSimPose::w, absolute, scale); +void apply_cycle_position_axes(CncSimPose *pose, + const std::unordered_map &words, + const CycleAxes &axes, + bool absolute, + double scale, + bool lathe_diameter_mode) { + apply_axis(pose, words, axes.first_word, axes.first, absolute, scale, lathe_diameter_mode); + apply_axis(pose, words, axes.second_word, axes.second, absolute, scale, lathe_diameter_mode); } -void set_arc_center(CncSimEvent *event, const std::unordered_map &words, int plane, double scale) { +void set_arc_center(CncSimEvent *event, const std::unordered_map &words, int plane, double scale, bool absolute) { event->center = event->start; if (words.count('R')) { const double radius = words.at('R') * scale; @@ -1317,14 +2172,14 @@ void set_arc_center(CncSimEvent *event, const std::unordered_map & 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; + event->center.x = absolute ? i : event->start.x + i; + event->center.y = absolute ? j : event->start.y + j; } else if (plane == 18) { - event->center.x = event->start.x + i; - event->center.z = event->start.z + k; + event->center.x = absolute ? i : event->start.x + i; + event->center.z = absolute ? k : event->start.z + k; } else if (plane == 19) { - event->center.y = event->start.y + j; - event->center.z = event->start.z + k; + event->center.y = absolute ? j : event->start.y + j; + event->center.z = absolute ? k : event->start.z + k; } } @@ -1336,6 +2191,21 @@ int rounded_word(const std::unordered_map &words, char letter, int return static_cast(std::lround(it->second)); } +bool integer_word_value(const std::unordered_map &words, + char letter, + int *value) { + auto it = words.find(letter); + if (it == words.end()) { + return false; + } + const double rounded = std::round(it->second); + if (std::fabs(it->second - rounded) > 0.0001) { + return false; + } + *value = static_cast(rounded); + return true; +} + int positive_integer_word(const std::unordered_map &words, char letter, int fallback) { auto it = words.find(letter); if (it == words.end()) { @@ -1352,6 +2222,338 @@ bool g_code_is(double actual, double expected) { return std::fabs(actual - expected) < 0.0001; } +void set_pose_axis(CncSimPose *pose, char axis, double value) { + switch (axis) { + case 'X': + pose->x = value; + break; + case 'Y': + pose->y = value; + break; + case 'Z': + pose->z = value; + break; + case 'A': + pose->a = value; + break; + case 'B': + pose->b = value; + break; + case 'C': + pose->c = value; + break; + case 'U': + pose->u = value; + break; + case 'V': + pose->v = value; + break; + case 'W': + pose->w = value; + break; + default: + break; + } +} + +double pose_axis(const CncSimPose &pose, char axis) { + switch (axis) { + case 'X': + return pose.x; + case 'Y': + return pose.y; + case 'Z': + return pose.z; + case 'A': + return pose.a; + case 'B': + return pose.b; + case 'C': + return pose.c; + case 'U': + return pose.u; + case 'V': + return pose.v; + case 'W': + return pose.w; + default: + return 0.0; + } +} + +bool has_g_code(const std::vector &word_list, double expected) { + for (const Word &word : word_list) { + if (word.letter == 'G' && g_code_is(word.value, expected)) { + return true; + } + } + return false; +} + +bool has_motion_g_code(const std::vector &word_list) { + for (const Word &word : word_list) { + if (word.letter != 'G') { + continue; + } + if (g_code_is(word.value, 33.1) || + g_code_is(word.value, 38.2) || + g_code_is(word.value, 38.3) || + g_code_is(word.value, 38.4) || + g_code_is(word.value, 38.5)) { + return true; + } + const int g = rounded_value(word.value); + if (g == 0 || g == 1 || g == 2 || g == 3 || + g == 33 || g == 73 || g == 76 || + (g >= 81 && g <= 89)) { + return true; + } + } + return false; +} + +bool p_word_has_linuxcnc_use(const std::vector &word_list, int modal_motion) { + if (modal_motion == 2 || modal_motion == 3 || modal_motion == 76 || + modal_motion == 82 || modal_motion == 86 || modal_motion == 88 || + modal_motion == 89 || modal_motion == 74 || modal_motion == 84) { + return true; + } + for (const Word &word : word_list) { + if (word.letter == 'G') { + if (g_code_is(word.value, 5.0) || + g_code_is(word.value, 5.2) || + g_code_is(word.value, 6.0) || + g_code_is(word.value, 6.2)) { + return true; + } + const int g = rounded_value(word.value); + if (g == 2 || g == 3 || g == 4 || g == 10 || g == 64 || + g == 70 || g == 74 || g == 76 || g == 82 || g == 84 || + g == 86 || g == 88 || g == 89) { + return true; + } + } else if (word.letter == 'M') { + const int m = rounded_value(word.value); + if ((m >= 50 && m <= 53) || (m >= 62 && m <= 66) || + m == 19 || m == 98) { + return true; + } + } + } + return false; +} + +bool q_word_has_linuxcnc_use(const std::vector &word_list, int modal_motion) { + if (modal_motion == 73 || modal_motion == 76 || modal_motion == 83) { + return true; + } + for (const Word &word : word_list) { + if (word.letter == 'G') { + if (g_code_is(word.value, 5.0) || + g_code_is(word.value, 6.0) || + g_code_is(word.value, 6.2)) { + return true; + } + const int g = rounded_value(word.value); + if (g == 10 || g == 64 || g == 70 || g == 73 || g == 76 || + g == 83) { + return true; + } + } else if (word.letter == 'M') { + const int m = rounded_value(word.value); + if (m == 19 || m == 61 || m == 66 || m == 67 || m == 68) { + return true; + } + } + } + return false; +} + +bool r_word_has_linuxcnc_use(const std::vector &word_list, int modal_motion) { + if (modal_motion == 2 || modal_motion == 3 || modal_motion == 73 || + modal_motion == 74 || modal_motion == 76 || + (modal_motion >= 81 && modal_motion <= 89)) { + return true; + } + for (const Word &word : word_list) { + if (word.letter == 'G') { + if (g_code_is(word.value, 41.1) || + g_code_is(word.value, 42.1) || + g_code_is(word.value, 6.2)) { + return true; + } + const int g = rounded_value(word.value); + if (g == 2 || g == 3 || g == 10 || g == 73 || g == 74 || + g == 76 || (g >= 81 && g <= 89)) { + return true; + } + } else if (word.letter == 'M' && rounded_value(word.value) == 19) { + return true; + } + } + return false; +} + +bool l_word_has_linuxcnc_use(const std::vector &word_list, int modal_motion) { + if (modal_motion == 73 || modal_motion == 76 || + (modal_motion >= 81 && modal_motion <= 89)) { + return true; + } + for (const Word &word : word_list) { + if (word.letter == 'G') { + if (g_code_is(word.value, 5.2) || + g_code_is(word.value, 6.2) || + g_code_is(word.value, 41.1) || + g_code_is(word.value, 42.1)) { + return true; + } + const int g = rounded_value(word.value); + if (g == 10 || g == 41 || g == 42 || g == 73 || g == 76 || + (g >= 81 && g <= 89)) { + return true; + } + } else if (word.letter == 'M') { + const int m = rounded_value(word.value); + if (m == 66 || m == 98) { + return true; + } + } + } + return false; +} + +bool i_word_has_linuxcnc_use(const std::vector &word_list, int modal_motion) { + if (modal_motion == 2 || modal_motion == 3 || modal_motion == kModalMotionG331 || + modal_motion == 76 || modal_motion == 87) { + return true; + } + for (const Word &word : word_list) { + if (word.letter != 'G') { + continue; + } + if (g_code_is(word.value, 5.0) || + g_code_is(word.value, 5.1) || + g_code_is(word.value, 6.0) || + g_code_is(word.value, 6.1) || + g_code_is(word.value, 33.1)) { + return true; + } + const int g = rounded_value(word.value); + if (g == 2 || g == 3 || g == 10 || g == 76 || g == 87) { + return true; + } + } + return false; +} + +bool j_word_has_linuxcnc_use(const std::vector &word_list, int modal_motion) { + if (modal_motion == 2 || modal_motion == 3 || modal_motion == 76 || + modal_motion == 87) { + return true; + } + for (const Word &word : word_list) { + if (word.letter != 'G') { + continue; + } + if (g_code_is(word.value, 5.0) || + g_code_is(word.value, 5.1) || + g_code_is(word.value, 6.0) || + g_code_is(word.value, 6.1)) { + return true; + } + const int g = rounded_value(word.value); + if (g == 2 || g == 3 || g == 10 || g == 76 || g == 87) { + return true; + } + } + return false; +} + +bool k_word_has_linuxcnc_use(const std::vector &word_list, int modal_motion) { + if (modal_motion == 2 || modal_motion == 3 || + modal_motion == kModalMotionG33 || modal_motion == kModalMotionG331 || + modal_motion == 76 || modal_motion == 87) { + return true; + } + for (const Word &word : word_list) { + if (word.letter != 'G') { + continue; + } + if (g_code_is(word.value, 6.2) || + g_code_is(word.value, 33.1)) { + return true; + } + const int g = rounded_value(word.value); + if (g == 2 || g == 3 || g == 33 || g == 76 || g == 87) { + return true; + } + } + return false; +} + +int coordinate_system_index_for_g_code(double g_code) { + if (g_code_is(g_code, 59.1)) { + return 7; + } + if (g_code_is(g_code, 59.2)) { + return 8; + } + if (g_code_is(g_code, 59.3)) { + return 9; + } + const int g = rounded_value(g_code); + if (g >= 54 && g <= 59 && g_code_is(g_code, static_cast(g))) { + return g - 53; + } + return 0; +} + +int probe_type_for_g_code(double g_code) { + if (g_code_is(g_code, 38.2)) { + return 0; + } + if (g_code_is(g_code, 38.3)) { + return 1; + } + if (g_code_is(g_code, 38.4)) { + return 2; + } + if (g_code_is(g_code, 38.5)) { + return 3; + } + return -1; +} + +bool modal_motion_is_probe(int modal_motion) { + return modal_motion >= 382 && modal_motion <= 385; +} + +int modal_probe_type(int modal_motion) { + return modal_motion - 382; +} + +bool pose_near(const CncSimPose &lhs, const CncSimPose &rhs) { + constexpr double kTolerance = 0.000001; + return std::fabs(lhs.x - rhs.x) < kTolerance && + std::fabs(lhs.y - rhs.y) < kTolerance && + std::fabs(lhs.z - rhs.z) < kTolerance && + std::fabs(lhs.a - rhs.a) < kTolerance && + std::fabs(lhs.b - rhs.b) < kTolerance && + std::fabs(lhs.c - rhs.c) < kTolerance && + std::fabs(lhs.u - rhs.u) < kTolerance && + std::fabs(lhs.v - rhs.v) < kTolerance && + std::fabs(lhs.w - rhs.w) < kTolerance; +} + +bool modal_motion_is_threading(int modal_motion) { + return modal_motion == kModalMotionG33 || + modal_motion == kModalMotionG331; +} + +bool modal_motion_is_threading_cycle(int modal_motion) { + return modal_motion == kModalMotionG76; +} + } // namespace SmokeGcodeParser::SmokeGcodeParser(CanonEventSink &sink) @@ -1379,6 +2581,24 @@ void emit_comment_state(CanonEventSink &sink, int line, int reserved) { sink.emit_raw(event); } +void emit_tool_length_offset(CanonEventSink &sink, const CncSimPose &offset, int line) { + CncSimEvent event{}; + event.version = 1; + event.type = CNC_SIM_EVENT_COMMENT; + event.line = line; + event.reserved = 430; + event.start = offset; + sink.emit_raw(event); +} + +CncSimPose smoke_tool_table_offset(int h_code, double unit_scale) { + CncSimPose offset{}; + if (h_code == 1) { + offset.z = 12.5 * unit_scale; + } + return offset; +} + } // namespace int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string *error) { @@ -1392,6 +2612,10 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string sink_.reset(); sink_.clear_callback_status(); absolute_ = true; + arc_absolute_ = false; + lathe_diameter_mode_ = false; + cutter_comp_on_ = false; + feed_mode_ = 0; canned_cycle_ = 0; canned_return_to_initial_ = false; canned_has_r_ = false; @@ -1399,11 +2623,19 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string canned_has_p_ = false; canned_has_q_ = false; canned_has_d_ = false; + canned_has_i_ = false; + canned_has_j_ = false; + canned_has_k_ = false; canned_r_ = 0.0; canned_z_ = 0.0; canned_p_ = 0.0; canned_q_ = 0.0; canned_d_ = 1.0; + canned_i_ = 0.0; + canned_j_ = 0.0; + canned_k_ = 0.0; + oword_return_value_ = 0.0; + oword_value_returned_ = false; const std::string source(program, program + program_len); const std::vector lines = split_source_lines(source); @@ -1414,9 +2646,185 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string std::unordered_map named_parameters; std::vector conditional_stack; std::unordered_map loop_iterations; + std::unordered_map g5x_offsets; + std::unordered_map g5x_rotations; + CncSimPose g92_parameters{}; + CncSimPose g28_home{}; + CncSimPose g30_home{}; + CncSimPose tool_length_offset{}; + struct ModalState { + bool absolute; + bool arc_absolute; + bool lathe_diameter_mode; + int plane; + double unit_scale; + }; + ModalState saved_modal_state{}; + bool has_saved_modal_state = false; + int active_g5x_index = 1; size_t pc = 0; int modal_motion = -1; + auto current_modal_state = [&]() { + return ModalState{absolute_, arc_absolute_, lathe_diameter_mode_, sink_.plane(), sink_.unit_scale()}; + }; + auto restore_modal_state = [&](const ModalState &state, int line) { + absolute_ = state.absolute; + arc_absolute_ = state.arc_absolute; + lathe_diameter_mode_ = state.lathe_diameter_mode; + if (sink_.unit_scale() != state.unit_scale) { + sink_.use_length_units(state.unit_scale, line); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + } + if (sink_.plane() != state.plane) { + sink_.select_plane(state.plane, line); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + } + return true; + }; + auto frame_modal_state = [](const CallFrame &frame) { + return ModalState{frame.modal_absolute, + frame.modal_arc_absolute, + frame.modal_lathe_diameter_mode, + frame.modal_plane, + frame.modal_unit_scale}; + }; + auto save_modal_state_to_frame = [](CallFrame *frame, const ModalState &state, bool restore_on_return) { + frame->has_modal_state = true; + frame->restore_modal_on_return = restore_on_return; + frame->modal_absolute = state.absolute; + frame->modal_arc_absolute = state.arc_absolute; + frame->modal_lathe_diameter_mode = state.lathe_diameter_mode; + frame->modal_plane = state.plane; + frame->modal_unit_scale = state.unit_scale; + }; + auto refresh_readonly_named_parameters = [&](int line_number) { + named_parameters["_lathe_diameter_mode"] = lathe_diameter_mode_ ? 1.0 : 0.0; + named_parameters["_lathe_radius_mode"] = lathe_diameter_mode_ ? 0.0 : 1.0; + named_parameters["_LATHE_DIAMETER_MODE"] = lathe_diameter_mode_ ? 1.0 : 0.0; + named_parameters["_LATHE_RADIUS_MODE"] = lathe_diameter_mode_ ? 0.0 : 1.0; + named_parameters["_metric"] = sink_.unit_scale() == 1.0 ? 1.0 : 0.0; + named_parameters["_imperial"] = sink_.unit_scale() == 25.4 ? 1.0 : 0.0; + named_parameters["_METRIC"] = sink_.unit_scale() == 1.0 ? 1.0 : 0.0; + named_parameters["_IMPERIAL"] = sink_.unit_scale() == 25.4 ? 1.0 : 0.0; + named_parameters["_absolute"] = absolute_ ? 1.0 : 0.0; + named_parameters["_incremental"] = absolute_ ? 0.0 : 1.0; + named_parameters["_inverse_time"] = feed_mode_ == 1 ? 1.0 : 0.0; + named_parameters["_units_per_minute"] = feed_mode_ == 0 ? 1.0 : 0.0; + named_parameters["_units_per_rev"] = feed_mode_ == 2 ? 1.0 : 0.0; + named_parameters["_coord_system"] = active_g5x_index < 7 + ? static_cast(530 + 10 * active_g5x_index) + : static_cast(584 + active_g5x_index); + named_parameters["_retract_r_plane"] = canned_return_to_initial_ ? 1.0 : 0.0; + named_parameters["_retract_old_z"] = canned_return_to_initial_ ? 0.0 : 1.0; + named_parameters["_ijk_absolute_mode"] = arc_absolute_ ? 1.0 : 0.0; + named_parameters["_feed"] = sink_.feed_rate(); + named_parameters["_rpm"] = sink_.spindle_speed(); + named_parameters["_spindle_on"] = sink_.spindle_direction() != 0 ? 1.0 : 0.0; + named_parameters["_spindle_cw"] = sink_.spindle_direction() == 1 ? 1.0 : 0.0; + named_parameters["_mist"] = sink_.mist_on() ? 1.0 : 0.0; + named_parameters["_flood"] = sink_.flood_on() ? 1.0 : 0.0; + named_parameters["_speed_override"] = sink_.speed_override() ? 1.0 : 0.0; + named_parameters["_feed_override"] = sink_.feed_override() ? 1.0 : 0.0; + named_parameters["_adaptive_feed"] = sink_.adaptive_feed() ? 1.0 : 0.0; + named_parameters["_feed_hold"] = sink_.feed_hold() ? 1.0 : 0.0; + named_parameters["_ABSOLUTE"] = absolute_ ? 1.0 : 0.0; + named_parameters["_INCREMENTAL"] = absolute_ ? 0.0 : 1.0; + named_parameters["_INVERSE_TIME"] = named_parameters["_inverse_time"]; + named_parameters["_UNITS_PER_MINUTE"] = named_parameters["_units_per_minute"]; + named_parameters["_UNITS_PER_REV"] = named_parameters["_units_per_rev"]; + named_parameters["_COORD_SYSTEM"] = named_parameters["_coord_system"]; + named_parameters["_RETRACT_R_PLANE"] = named_parameters["_retract_r_plane"]; + named_parameters["_RETRACT_OLD_Z"] = named_parameters["_retract_old_z"]; + named_parameters["_IJK_ABSOLUTE_MODE"] = named_parameters["_ijk_absolute_mode"]; + named_parameters["_FEED"] = sink_.feed_rate(); + named_parameters["_RPM"] = sink_.spindle_speed(); + named_parameters["_SPINDLE_ON"] = sink_.spindle_direction() != 0 ? 1.0 : 0.0; + named_parameters["_SPINDLE_CW"] = sink_.spindle_direction() == 1 ? 1.0 : 0.0; + named_parameters["_MIST"] = named_parameters["_mist"]; + named_parameters["_FLOOD"] = named_parameters["_flood"]; + named_parameters["_SPEED_OVERRIDE"] = named_parameters["_speed_override"]; + named_parameters["_FEED_OVERRIDE"] = named_parameters["_feed_override"]; + named_parameters["_ADAPTIVE_FEED"] = named_parameters["_adaptive_feed"]; + named_parameters["_FEED_HOLD"] = named_parameters["_feed_hold"]; + named_parameters["_motion_mode"] = modal_motion >= 0 ? static_cast(modal_motion) : 80.0; + named_parameters["_MOTION_MODE"] = named_parameters["_motion_mode"]; + named_parameters["_plane"] = static_cast(sink_.plane()); + named_parameters["_PLANE"] = named_parameters["_plane"]; + named_parameters["_current_tool"] = static_cast(sink_.selected_tool()); + named_parameters["_CURRENT_TOOL"] = named_parameters["_current_tool"]; + named_parameters["_selected_tool"] = static_cast(sink_.selected_tool()); + named_parameters["_SELECTED_TOOL"] = named_parameters["_selected_tool"]; + named_parameters["_current_pocket"] = static_cast(sink_.selected_tool()); + named_parameters["_CURRENT_POCKET"] = named_parameters["_current_pocket"]; + named_parameters["_selected_pocket"] = static_cast(sink_.selected_tool()); + named_parameters["_SELECTED_POCKET"] = named_parameters["_selected_pocket"]; + named_parameters["_line"] = static_cast(line_number); + named_parameters["_LINE"] = named_parameters["_line"]; + named_parameters["_value"] = oword_return_value_; + named_parameters["_VALUE"] = oword_return_value_; + named_parameters["_value_returned"] = oword_value_returned_ ? 1.0 : 0.0; + named_parameters["_VALUE_RETURNED"] = named_parameters["_value_returned"]; + named_parameters["_call_level"] = static_cast(call_stack.size()); + named_parameters["_CALL_LEVEL"] = named_parameters["_call_level"]; + named_parameters["_remap_level"] = 0.0; + named_parameters["_REMAP_LEVEL"] = 0.0; + named_parameters["_task"] = 1.0; + named_parameters["_TASK"] = 1.0; + const bool has_tool_offset = tool_length_offset.x != 0.0 || + tool_length_offset.y != 0.0 || + tool_length_offset.z != 0.0 || + tool_length_offset.a != 0.0 || + tool_length_offset.b != 0.0 || + tool_length_offset.c != 0.0 || + tool_length_offset.u != 0.0 || + tool_length_offset.v != 0.0 || + tool_length_offset.w != 0.0; + named_parameters["_tool_offset"] = has_tool_offset ? 1.0 : 0.0; + named_parameters["_TOOL_OFFSET"] = named_parameters["_tool_offset"]; + const CncSimPose &position = sink_.position(); + named_parameters["_x"] = position.x; + named_parameters["_y"] = position.y; + named_parameters["_z"] = position.z; + named_parameters["_a"] = position.a; + named_parameters["_b"] = position.b; + named_parameters["_c"] = position.c; + named_parameters["_u"] = position.u; + named_parameters["_v"] = position.v; + named_parameters["_w"] = position.w; + named_parameters["_X"] = position.x; + named_parameters["_Y"] = position.y; + named_parameters["_Z"] = position.z; + named_parameters["_A"] = position.a; + named_parameters["_B"] = position.b; + named_parameters["_C"] = position.c; + named_parameters["_U"] = position.u; + named_parameters["_V"] = position.v; + named_parameters["_W"] = position.w; + named_parameters["_abs_x"] = position.x; + named_parameters["_abs_y"] = position.y; + named_parameters["_abs_z"] = position.z; + named_parameters["_abs_a"] = position.a; + named_parameters["_abs_b"] = position.b; + named_parameters["_abs_c"] = position.c; + named_parameters["_abs_u"] = position.u; + named_parameters["_abs_v"] = position.v; + named_parameters["_abs_w"] = position.w; + named_parameters["_ABS_X"] = position.x; + named_parameters["_ABS_Y"] = position.y; + named_parameters["_ABS_Z"] = position.z; + named_parameters["_ABS_A"] = position.a; + named_parameters["_ABS_B"] = position.b; + named_parameters["_ABS_C"] = position.c; + named_parameters["_ABS_U"] = position.u; + named_parameters["_ABS_V"] = position.v; + named_parameters["_ABS_W"] = position.w; + }; + while (pc < lines.size()) { const size_t current_pc = pc; ++pc; @@ -1441,36 +2849,50 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string continue; } + refresh_readonly_named_parameters(line_number); std::string parameter_error; - int assigned_parameter = 0; - std::string assigned_named_parameter; - double assigned_value = 0.0; - if (parse_parameter_assignment(line, - call_stack, - parameters, - named_parameters, - &assigned_parameter, - &assigned_named_parameter, - &assigned_value, - ¶meter_error)) { - if (!parameter_error.empty()) { - if (error) { - *error = parameter_error; - } - return -1; + std::vector pending_assignments; + if (!extract_parameter_assignments(line, + call_stack, + parameters, + named_parameters, + &pending_assignments, + &line, + ¶meter_error)) { + if (error) { + *error = parameter_error; } - if (!assigned_named_parameter.empty()) { - if (!call_stack.empty() && call_stack.back().oword && assigned_named_parameter[0] != '_') { - call_stack.back().named_params[assigned_named_parameter] = assigned_value; + return -1; + } + + auto commit_pending_assignments = [&]() { + for (const auto &assignment : pending_assignments) { + if (!assignment.name.empty()) { + if (!call_stack.empty() && call_stack.back().oword && assignment.name[0] != '_') { + call_stack.back().named_params[assignment.name] = assignment.value; + } else { + named_parameters[assignment.name] = assignment.value; + } + } else if (assignment.index > 0 && assignment.index <= 30 && !call_stack.empty() && call_stack.back().oword) { + call_stack.back().params[static_cast(assignment.index)] = assignment.value; } else { - named_parameters[assigned_named_parameter] = assigned_value; + parameters[assignment.index] = assignment.value; } - } else if (assigned_parameter > 0 && assigned_parameter <= 30 && !call_stack.empty() && call_stack.back().oword) { - call_stack.back().params[static_cast(assigned_parameter)] = assigned_value; - } else { - parameters[assigned_parameter] = assigned_value; } - continue; + pending_assignments.clear(); + }; + + if (line.empty()) { + if (!pending_assignments.empty()) { + commit_pending_assignments(); + continue; + } + } + if (line.find('=') != std::string::npos) { + if (error) { + *error = "Bad character '=' used"; + } + return -1; } const std::string expanded_line = expand_expressions_and_parameters(line, call_stack, parameters, named_parameters, ¶meter_error); @@ -1482,7 +2904,11 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string } const std::string keyword_line = line; line = expanded_line; - auto word_list = parse_word_list(line); + std::vector word_list; + if (!parse_word_list_strict(line, &word_list, error)) { + return -1; + } + refresh_readonly_named_parameters(line_number); const std::string o_identifier = first_o_identifier(keyword_line, word_list); const bool has_o_word = !o_identifier.empty(); if (word_list.empty() && !has_o_word) { @@ -1492,6 +2918,146 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string auto words = last_words_by_letter(word_list); std::vector m_codes; const int previous_canned_cycle = canned_cycle_; + bool machine_coordinate_move = false; + bool current_line_has_probe_motion = false; + int h_word_value = 0; + const bool has_h_word = words.count('H') != 0; + if (has_h_word) { + if (!integer_word_value(words, 'H', &h_word_value)) { + if (error) { + *error = "H word must be an integer"; + } + return -1; + } + if (h_word_value < -1) { + if (error) { + *error = "Negative H word used"; + } + return -1; + } + if (!has_g_code(word_list, 43.0) && + !has_g_code(word_list, 43.4) && + !has_g_code(word_list, 43.5) && + !has_g_code(word_list, 43.2) && + !has_g_code(word_list, 76.0)) { + if (error) { + *error = "H word with no G43 or G76 to use it"; + } + return -1; + } + } + if (words.count('D') && + !has_g_code(word_list, 41.0) && + !has_g_code(word_list, 41.1) && + !has_g_code(word_list, 42.0) && + !has_g_code(word_list, 42.1) && + !has_g_code(word_list, 73.0) && + !has_g_code(word_list, 83.0) && + !((previous_canned_cycle == 73 || previous_canned_cycle == 83) && !has_g_code(word_list, 80.0)) && + !has_g_code(word_list, 96.0)) { + if (error) { + *error = "D word with no G41, G41.1, G42, G42.1, G73, G83 or G96 to use it"; + } + return -1; + } + if (words.count('E') && + !has_g_code(word_list, 70.0) && + !has_g_code(word_list, 76.0) && + !has_g_code(word_list, 33.0) && + !has_g_code(word_list, 33.1) && + !has_m_code(word_list, 66) && + !has_m_code(word_list, 67) && + !has_m_code(word_list, 68)) { + if (error) { + *error = "E word with no G70, G76, M66, M67 or M68 to use it"; + } + return -1; + } + if (words.count('P') && !p_word_has_linuxcnc_use(word_list, modal_motion)) { + if (error) { + *error = "P word with no G2 G3 G4 G10 G64 G5 G5.2 G6, G6.2, G76 G82 G86 G88 G89 or M50 M51 M52 M53 M62 M63 M64 M65 M66 M98 or user M code to use it"; + } + return -1; + } + if (words.count('P') && (has_g_code(word_list, 2.0) || has_g_code(word_list, 3.0))) { + const int p_value = rounded_word(words, 'P', 0); + if (std::fabs(static_cast(p_value) - words.at('P')) > 0.001) { + if (error) { + *error = "P value not an integer with G2 or G3"; + } + return -1; + } + if (p_value < 1) { + if (error) { + *error = "P value should be 1 or greater with G2 or G3"; + } + return -1; + } + } + if (words.count('P') && has_m_code(word_list, 19)) { + const int p_value = rounded_word(words, 'P', 0); + if (std::fabs(static_cast(p_value) - words.at('P')) > 0.001) { + if (error) { + *error = "P value not an integer with M19"; + } + return -1; + } + if (p_value < 0 || p_value > 2) { + if (error) { + *error = "P value must be 0,1,or 2 with M19"; + } + return -1; + } + } + if (words.count('Q') && !q_word_has_linuxcnc_use(word_list, modal_motion)) { + if (error) { + *error = "Q word with no G5, G6, G10, G64, G73, G76, G83, M19, M66, M67, M68 or user M code that uses it"; + } + return -1; + } + if (words.count('Q') && has_m_code(word_list, 19) && words.at('Q') <= 0.0) { + if (error) { + *error = "Q word with M19 requires a value > 0"; + } + return -1; + } + if (words.count('R') && !r_word_has_linuxcnc_use(word_list, modal_motion)) { + if (error) { + *error = "R word with no G2, G3, G10, G76, G81 to G89, G73, G74, G41.1, G42.1 or M19 to use it"; + } + return -1; + } + if (words.count('R') && has_m_code(word_list, 19) && + (words.at('R') < 0.0 || words.at('R') > 360.0)) { + if (error) { + *error = "R value must be within 0..360 with M19"; + } + return -1; + } + if (words.count('L') && !l_word_has_linuxcnc_use(word_list, modal_motion)) { + if (error) { + *error = "L word with no G10, cutter compensation, canned cycle, digital/analog input, M98 or NURBS code"; + } + return -1; + } + if (words.count('I') && !i_word_has_linuxcnc_use(word_list, modal_motion)) { + if (error) { + *error = "I word with no G2, G3, G5, G5.1, G6, G6.1, G10, G33.1, G76, or G87 to use it"; + } + return -1; + } + if (words.count('J') && !j_word_has_linuxcnc_use(word_list, modal_motion)) { + if (error) { + *error = "J word with no G2, G3, G5, G5.1, G6, G6.1, G10, G76 or G87 to use it"; + } + return -1; + } + if (words.count('K') && !k_word_has_linuxcnc_use(word_list, modal_motion)) { + if (error) { + *error = "K word with no G2, G3, G6.2, G33, G33.1, G76, or G87 to use it"; + } + return -1; + } const OwordKind current_oword_kind = has_o_word ? oword_kind(keyword_line) : OwordKind::None; const bool oword_call = current_oword_kind == OwordKind::Call; const bool oword_return = current_oword_kind == OwordKind::Return; @@ -1759,20 +3325,249 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string continue; } + bool coordinate_control = false; + if (has_g_code(word_list, 10.0)) { + const int l = rounded_word(words, 'L', 0); + if (words.count('P') && + std::fabs(static_cast(rounded_word(words, 'P', 0)) - words.at('P')) > 0.0001) { + if (error) { + *error = "P value not an integer with G10"; + } + return -1; + } + const int requested_p = rounded_word(words, 'P', active_g5x_index); + if ((l == 2 || l == 20) && (requested_p < 0 || requested_p > 9)) { + if (error) { + *error = "P value out of range (0-9) with G10 L2/L20"; + } + return -1; + } + const int p = requested_p == 0 ? active_g5x_index : requested_p; + if ((l == 2 || l == 20) && p > 0) { + if (l == 2 && (words.count('I') || words.count('J'))) { + if (error) { + *error = "I J words not allowed with G10 L2"; + } + return -1; + } + if (l == 20 && words.count('R')) { + if (error) { + *error = "R not allowed in G10 L20"; + } + return -1; + } + if (cutter_comp_on_ && p == active_g5x_index) { + if (error) { + *error = "Cannot change the active coordinate system with cutter radius compensation on"; + } + return -1; + } + CncSimPose offset = g5x_offsets.count(p) ? g5x_offsets[p] : CncSimPose{}; + static constexpr char axes[] = {'X', 'Y', 'Z', 'A', 'B', 'C', 'U', 'V', 'W'}; + for (char axis : axes) { + if (!words.count(axis)) { + continue; + } + const double value = coordinate_word_value(words, axis, sink_.unit_scale(), lathe_diameter_mode_); + set_pose_axis(&offset, + axis, + l == 20 ? pose_axis(sink_.position(), axis) - value : value); + } + g5x_offsets[p] = offset; + if (words.count('R')) { + g5x_rotations[p] = words.at('R'); + } + if (p == active_g5x_index) { + sink_.set_g5x_offset(p, offset, line_number); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + if (words.count('R')) { + sink_.set_xy_rotation(words.at('R'), line_number); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + } + } + coordinate_control = true; + } else if (l == 2 || l == 20) { + if (error) { + *error = "G10 L2/L20 requires a P coordinate system index from 0 to 9"; + } + return -1; + } + } + for (const Word &word : word_list) { if (word.letter == 'G') { const int g = rounded_value(word.value); + const int coordinate_system_index = coordinate_system_index_for_g_code(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_code_is(word.value, 43.0)) { + if (cutter_comp_on_) { + if (error) { + *error = "Cannot change tool offset with cutter radius compensation on"; + } + return -1; + } + const int h_code = has_h_word ? h_word_value : sink_.selected_tool(); + tool_length_offset = smoke_tool_table_offset(h_code, sink_.unit_scale()); + emit_tool_length_offset(sink_, tool_length_offset, line_number); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + } else if (g_code_is(word.value, 43.1) || g_code_is(word.value, 43.2)) { + if (cutter_comp_on_) { + if (error) { + *error = "Cannot change tool offset with cutter radius compensation on"; + } + return -1; + } + if (g_code_is(word.value, 43.1) && has_motion_g_code(word_list)) { + if (error) { + *error = "Cannot command motion on the same line as G43.1"; + } + return -1; + } + const bool has_tool_axis_word = has_axis_word(words); + if (g_code_is(word.value, 43.2) && has_h_word && has_tool_axis_word) { + if (error) { + *error = "G43.2: Can not have both H and axis words"; + } + return -1; + } + if (g_code_is(word.value, 43.2) && !has_h_word && !has_tool_axis_word) { + if (error) { + *error = "G43.2: No axes specified and H word missing"; + } + return -1; + } + CncSimPose offset = tool_length_offset; + static constexpr char axes[] = {'X', 'Y', 'Z', 'A', 'B', 'C', 'U', 'V', 'W'}; + for (char axis : axes) { + if (!words.count(axis)) { + continue; + } + const double value = coordinate_word_value(words, axis, sink_.unit_scale(), lathe_diameter_mode_); + set_pose_axis(&offset, + axis, + g_code_is(word.value, 43.2) ? pose_axis(offset, axis) + value : value); + } + if (g_code_is(word.value, 43.2) && has_h_word) { + const CncSimPose table_offset = smoke_tool_table_offset(h_word_value, sink_.unit_scale()); + offset.x += table_offset.x; + offset.y += table_offset.y; + offset.z += table_offset.z; + offset.a += table_offset.a; + offset.b += table_offset.b; + offset.c += table_offset.c; + offset.u += table_offset.u; + offset.v += table_offset.v; + offset.w += table_offset.w; + } + tool_length_offset = offset; + emit_tool_length_offset(sink_, tool_length_offset, line_number); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + } else if (g_code_is(word.value, 40.0) || + g_code_is(word.value, 41.0) || + g_code_is(word.value, 42.0) || + g_code_is(word.value, 41.1) || + g_code_is(word.value, 42.1)) { + const bool cutter_comp_off = g_code_is(word.value, 40.0); + const bool cutter_comp_left = g_code_is(word.value, 41.0) || g_code_is(word.value, 41.1); + const bool explicit_comp = g_code_is(word.value, 41.1) || g_code_is(word.value, 42.1); + const bool plane_ok = sink_.plane() == 17 || sink_.plane() == 18; + if (cutter_comp_off && !cutter_comp_on_) { + continue; + } + if (!cutter_comp_off && !plane_ok) { + if (error) { + *error = "Cutter radius compensation allowed only in XY or XZ planes"; + } + return -1; + } + if (!cutter_comp_off && cutter_comp_on_) { + if (error) { + *error = "Cannot turn cutter radius comp on when on"; + } + return -1; + } + if (explicit_comp) { + if (!words.count('D')) { + if (error) { + *error = "G41.1/G42.1 requires a D word"; + } + return -1; + } + if (words.count('L') && sink_.plane() != 18) { + if (error) { + *error = "G41.1/G42.1 with L word requires G18"; + } + return -1; + } + } else if (words.count('D')) { + const double d_value = words.at('D'); + if (std::fabs(d_value - std::round(d_value)) > 0.0001 || d_value < 0.0) { + if (error) { + *error = "G41/G42 requires a non-negative whole-number D word"; + } + return -1; + } + } + if (cutter_comp_off) { + cutter_comp_on_ = false; + emit_comment_state(sink_, line_number, kCutterCompOffEvent); + } else { + cutter_comp_on_ = true; + emit_comment_state(sink_, line_number, + explicit_comp ? (cutter_comp_left ? kCutterCompLeftExplicitEvent : kCutterCompRightExplicitEvent) + : (cutter_comp_left ? kCutterCompLeftEvent : kCutterCompRightEvent)); + } + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } } else if (g == 49) { + if (cutter_comp_on_) { + if (error) { + *error = "Cannot change tool offset with cutter radius compensation on"; + } + return -1; + } + tool_length_offset = {}; + emit_tool_length_offset(sink_, tool_length_offset, line_number); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } sink_.set_rtcp_state(false, 0, line_number); if (stop_if_callback_aborted(sink_, error)) { return -1; } + } else if (g == 7) { + lathe_diameter_mode_ = true; + } else if (g == 8) { + lathe_diameter_mode_ = false; } else if (g == 17 || g == 18 || g == 19) { + if (cutter_comp_on_ && + ((g == 17 && sink_.plane() != 17) || + (g == 18 && sink_.plane() != 18) || + (g == 19 && sink_.plane() != 19))) { + if (error) { + *error = "Cannot change planes with cutter radius compensation on"; + } + return -1; + } + if (cutter_comp_on_ && g == 19) { + if (error) { + *error = "Cutter radius compensation allowed only in XY or XZ planes"; + } + return -1; + } sink_.select_plane(g, line_number); if (stop_if_callback_aborted(sink_, error)) { return -1; @@ -1787,6 +3582,35 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string if (stop_if_callback_aborted(sink_, error)) { return -1; } + } else if (coordinate_system_index != 0) { + if (cutter_comp_on_) { + if (error) { + *error = "Cannot change coordinate systems with cutter radius compensation on"; + } + return -1; + } + active_g5x_index = coordinate_system_index; + const auto offset = g5x_offsets.find(active_g5x_index); + if (offset != g5x_offsets.end()) { + sink_.set_g5x_offset(active_g5x_index, offset->second, line_number); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + } + const auto rotation = g5x_rotations.find(active_g5x_index); + if (rotation != g5x_rotations.end()) { + sink_.set_xy_rotation(rotation->second, line_number); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + } + coordinate_control = true; + } else if (g == 53) { + machine_coordinate_move = true; + } else if (g_code_is(word.value, 90.1)) { + arc_absolute_ = true; + } else if (g_code_is(word.value, 91.1)) { + arc_absolute_ = false; } else if (g == 90) { absolute_ = true; } else if (g == 91) { @@ -1796,16 +3620,41 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string if (stop_if_callback_aborted(sink_, error)) { return -1; } - sink_.set_feed_mode(g == 95 ? 1 : 0, line_number); + feed_mode_ = g == 95 ? 2 : (g == 93 ? 1 : 0); + sink_.set_feed_mode(0, g == 95 ? 1 : 0, line_number); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + } else if (g == 96 || g == 97) { + if (g == 96 && !words.count('S')) { + if (error) { + *error = "S word missing with G96"; + } + return -1; + } + const double mode = g == 97 ? 0.0 : (words.count('D') ? std::fabs(words.at('D')) : 1e30); + sink_.set_spindle_mode(0, mode, line_number); if (stop_if_callback_aborted(sink_, error)) { return -1; } } else if (g == 61) { + if (cutter_comp_on_) { + if (error) { + *error = "Cannot change control mode with cutter radius compensation on"; + } + return -1; + } emit_comment_state(sink_, line_number, g_code_is(word.value, 61.1) ? 612 : 611); if (stop_if_callback_aborted(sink_, error)) { return -1; } } else if (g == 64) { + if (cutter_comp_on_) { + if (error) { + *error = "Cannot change control mode with cutter radius compensation on"; + } + return -1; + } CncSimEvent event{}; event.version = 1; event.type = CNC_SIM_EVENT_COMMENT; @@ -1816,30 +3665,212 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string if (stop_if_callback_aborted(sink_, error)) { return -1; } + } else if (g == 76) { + if (cutter_comp_on_) { + if (error) { + *error = "Cannot use G76 threading cycle with cutter radius compensation on"; + } + return -1; + } + modal_motion = kModalMotionG76; + canned_cycle_ = 0; + } else if (g_code_is(word.value, 33.1)) { + modal_motion = kModalMotionG331; + canned_cycle_ = 0; + } else if (g_code_is(word.value, 33.0)) { + modal_motion = kModalMotionG33; + canned_cycle_ = 0; } else if (g == 0 || g == 1 || g == 2 || g == 3) { modal_motion = g; canned_cycle_ = 0; + } else if (probe_type_for_g_code(word.value) >= 0) { + if (cutter_comp_on_) { + if (error) { + *error = "Cannot probe with cutter radius comp on"; + } + return -1; + } + const int probe_type = probe_type_for_g_code(word.value); + modal_motion = 382 + probe_type; + canned_cycle_ = 0; + current_line_has_probe_motion = true; } else if (g == 4) { + if (!words.count('P')) { + if (error) { + *error = "Dwell time missing with G4"; + } + return -1; + } + if (has_g_code(word_list, 2.0) || has_g_code(word_list, 3.0)) { + if (error) { + *error = "G4 not allowed with G2 or G3 because they both use P"; + } + return -1; + } sink_.dwell(words.count('P') ? words['P'] : 0.0, line_number); if (stop_if_callback_aborted(sink_, error)) { return -1; } + } else if (g_code_is(word.value, 28.1)) { + if (cutter_comp_on_) { + if (error) { + *error = "Cannot set reference point with cutter compensation in effect"; + } + return -1; + } + g28_home = sink_.position(); + coordinate_control = true; + } else if (g_code_is(word.value, 30.1)) { + if (cutter_comp_on_) { + if (error) { + *error = "Cannot set reference point with cutter compensation in effect"; + } + return -1; + } + g30_home = sink_.position(); + coordinate_control = true; + } else if (g == 28 || g == 30) { + if (cutter_comp_on_) { + if (error) { + *error = "Cannot use g28 or g30 with cutter radius comp"; + } + return -1; + } + const bool has_axes = has_axis_word(words); + CncSimPose waypoint = sink_.position(); + apply_axes(&waypoint, words, absolute_, sink_.unit_scale(), lathe_diameter_mode_); + sink_.straight_traverse(line_number, waypoint); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + + const CncSimPose &home = g == 28 ? g28_home : g30_home; + CncSimPose target = waypoint; + static constexpr char axes[] = {'X', 'Y', 'Z', 'A', 'B', 'C', 'U', 'V', 'W'}; + if (has_axes) { + for (char axis : axes) { + if (words.count(axis)) { + set_pose_axis(&target, axis, pose_axis(home, axis)); + } + } + } else { + target = home; + } + sink_.straight_traverse(line_number, target); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + coordinate_control = true; + } else if (g == 52) { + if (cutter_comp_on_) { + if (error) { + *error = "Cannot change axis offsets with cutter radius comp"; + } + return -1; + } + CncSimPose offset = g92_parameters; + static constexpr char axes[] = {'X', 'Y', 'Z', 'A', 'B', 'C', 'U', 'V', 'W'}; + for (char axis : axes) { + if (!words.count(axis)) { + continue; + } + set_pose_axis(&offset, axis, coordinate_word_value(words, axis, sink_.unit_scale(), lathe_diameter_mode_)); + } + g92_parameters = offset; + sink_.set_g92_offset(offset, line_number); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + coordinate_control = true; + } else if (g_code_is(word.value, 92.1) || g_code_is(word.value, 92.2)) { + if (cutter_comp_on_) { + if (error) { + *error = "Cannot change axis offsets with cutter radius comp"; + } + return -1; + } + sink_.set_g92_offset(CncSimPose{}, line_number); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + if (g_code_is(word.value, 92.1)) { + g92_parameters = {}; + } + coordinate_control = true; + } else if (g_code_is(word.value, 92.3)) { + if (cutter_comp_on_) { + if (error) { + *error = "Cannot change axis offsets with cutter radius comp"; + } + return -1; + } + sink_.set_g92_offset(g92_parameters, line_number); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + coordinate_control = true; + } else if (g == 92) { + if (cutter_comp_on_) { + if (error) { + *error = "Cannot change axis offsets with cutter radius comp"; + } + return -1; + } + CncSimPose offset = g92_parameters; + static constexpr char axes[] = {'X', 'Y', 'Z', 'A', 'B', 'C', 'U', 'V', 'W'}; + for (char axis : axes) { + if (!words.count(axis)) { + continue; + } + const double value = coordinate_word_value(words, axis, sink_.unit_scale(), lathe_diameter_mode_); + set_pose_axis(&offset, axis, pose_axis(sink_.position(), axis) - value); + } + g92_parameters = offset; + sink_.set_g92_offset(offset, line_number); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + coordinate_control = true; } else if (g == 80) { canned_cycle_ = 0; modal_motion = -1; - } else if (g == 73 || g == 81 || g == 82 || g == 83 || g == 85 || g == 86 || g == 89) { + } else if (g == 73 || g == 74 || g == 81 || g == 82 || g == 83 || g == 84 || g == 85 || g == 86 || g == 87 || g == 88 || g == 89) { canned_cycle_ = g; modal_motion = -1; } else if (g == 98) { + if (cutter_comp_on_) { + if (error) { + *error = "Cannot change retract mode with cutter radius compensation on"; + } + return -1; + } canned_return_to_initial_ = true; } else if (g == 99) { + if (cutter_comp_on_) { + if (error) { + *error = "Cannot change retract mode with cutter radius compensation on"; + } + return -1; + } canned_return_to_initial_ = false; } } else if (word.letter == 'M') { m_codes.push_back(rounded_value(word.value)); } } + if (m_codes.size() > 4) { + if (error) { + *error = "Too many M codes on line"; + } + return -1; + } + if (modal_motion_is_threading(modal_motion) && words.count('F')) { + if (error) { + *error = "F word used with G33/G33.1"; + } + return -1; + } if (words.count('F')) { sink_.set_feed_rate(words['F'] * sink_.unit_scale(), line_number); if (stop_if_callback_aborted(sink_, error)) { @@ -1903,8 +3934,42 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string } return -1; } - pc = call_stack.back().return_pc; + CallFrame &frame = call_stack.back(); + if (frame.restore_modal_on_return && frame.has_modal_state && + !restore_modal_state(frame_modal_state(frame), line_number)) { + return -1; + } + oword_return_value_ = 0.0; + oword_value_returned_ = false; + const size_t bracket = keyword_line.find('['); + if (bracket != std::string::npos) { + const size_t close = find_matching_square_bracket(keyword_line, bracket); + if (close == std::string::npos) { + if (error) { + *error = "unterminated O-word return value expression"; + } + return -1; + } + const std::string value_text = keyword_line.substr(bracket + 1, close - bracket - 1); + if (!value_text.empty()) { + double return_value = 0.0; + if (!evaluate_expression(value_text, + call_stack, + parameters, + named_parameters, + &return_value)) { + if (error) { + *error = "unsupported O-word return value expression"; + } + return -1; + } + oword_return_value_ = return_value; + oword_value_returned_ = true; + } + } + pc = frame.return_pc; call_stack.pop_back(); + refresh_readonly_named_parameters(line_number); subprogram_control = true; } for (int m : m_codes) { @@ -1937,29 +4002,278 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string return -1; } } else if (m == 6) { + if (cutter_comp_on_) { + if (error) { + *error = "Cannot change tools with cutter radius compensation on"; + } + return -1; + } sink_.change_tool(line_number); if (stop_if_callback_aborted(sink_, error)) { return -1; } } else if (m == 7) { + sink_.set_mist_on(true); emit_comment_state(sink_, line_number, 11); if (stop_if_callback_aborted(sink_, error)) { return -1; } } else if (m == 8) { + sink_.set_flood_on(true); emit_comment_state(sink_, line_number, 21); if (stop_if_callback_aborted(sink_, error)) { return -1; } } else if (m == 9) { + sink_.set_mist_on(false); emit_comment_state(sink_, line_number, 10); if (stop_if_callback_aborted(sink_, error)) { return -1; } + sink_.set_flood_on(false); emit_comment_state(sink_, line_number, 20); if (stop_if_callback_aborted(sink_, error)) { return -1; } + } else if (m == 48 || m == 49) { + if (cutter_comp_on_) { + if (error) { + *error = m == 48 ? "Cannot enable overrides with cutter radius compensation on" + : "Cannot disable overrides with cutter radius compensation on"; + } + return -1; + } + const bool enabled = m == 48; + sink_.set_override_control(50, enabled, 0, line_number); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + sink_.set_override_control(51, enabled, 0, line_number); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + } else if (m == 50) { + if (cutter_comp_on_) { + if (error) { + *error = rounded_word(words, 'P', 1) != 0 + ? "Cannot enable overrides with cutter radius compensation on" + : "Cannot disable overrides with cutter radius compensation on"; + } + return -1; + } + sink_.set_override_control(50, rounded_word(words, 'P', 1) != 0, 0, line_number); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + } else if (m == 51) { + if (cutter_comp_on_) { + if (error) { + *error = rounded_word(words, 'P', 1) != 0 + ? "Cannot enable overrides with cutter radius compensation on" + : "Cannot disable overrides with cutter radius compensation on"; + } + return -1; + } + sink_.set_override_control(51, rounded_word(words, 'P', 1) != 0, 0, line_number); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + } else if (m == 52) { + if (cutter_comp_on_) { + if (error) { + *error = rounded_word(words, 'P', 1) != 0 + ? "Cannot enable overrides with cutter radius compensation on" + : "Cannot disable overrides with cutter radius compensation on"; + } + return -1; + } + sink_.set_override_control(52, rounded_word(words, 'P', 1) != 0, 0, line_number); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + } else if (m == 53) { + if (cutter_comp_on_) { + if (error) { + *error = rounded_word(words, 'P', 1) != 0 + ? "Cannot enable overrides with cutter radius compensation on" + : "Cannot disable overrides with cutter radius compensation on"; + } + return -1; + } + sink_.set_override_control(53, rounded_word(words, 'P', 1) != 0, 0, line_number); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + } else if (m == 70 || m == 73) { + const ModalState state = current_modal_state(); + if (call_stack.empty()) { + saved_modal_state = state; + has_saved_modal_state = true; + } else { + save_modal_state_to_frame(&call_stack.back(), state, m == 73); + } + } else if (m == 71) { + if (call_stack.empty()) { + has_saved_modal_state = false; + } else { + call_stack.back().has_modal_state = false; + call_stack.back().restore_modal_on_return = false; + } + } else if (m == 72) { + ModalState state{}; + bool has_modal_state = false; + if (call_stack.empty()) { + state = saved_modal_state; + has_modal_state = has_saved_modal_state; + } else { + state = frame_modal_state(call_stack.back()); + has_modal_state = call_stack.back().has_modal_state; + } + if (!has_modal_state) { + if (error) { + *error = "M72 restore requires a previous M70/M73 modal state save"; + } + return -1; + } + if (!restore_modal_state(state, line_number)) { + return -1; + } + } else if (m == 61) { + const int tool_number = rounded_word(words, 'Q', -1); + if (!words.count('Q') || tool_number < 0) { + if (error) { + *error = "Need non-negative Q-word to specify tool number with M61"; + } + return -1; + } + sink_.select_tool(tool_number); + sink_.change_tool(line_number); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + } else if (m == 62 || m == 63 || m == 64 || m == 65) { + if (cutter_comp_on_) { + if (error) { + *error = (m == 62 || m == 63) + ? "Cannot set motion output with cutter radius compensation on" + : "Cannot set auxiliary digital output with cutter radius compensation on"; + } + return -1; + } + if (!words.count('P')) { + if (error) { + *error = "M62-M65 requires a P output index"; + } + return -1; + } + const int output_index = rounded_word(words, 'P', 0); + if (output_index < 0) { + if (error) { + *error = "Invalid digital output index with M62-M65"; + } + return -1; + } + sink_.set_digital_output(output_index, + m == 62 || m == 64, + m == 62 || m == 63, + line_number); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + } else if (m == 66) { + if (cutter_comp_on_) { + if (error) { + *error = words.count('P') + ? "Cannot wait for digital input with cutter radius compensation on" + : "Cannot wait for analog input with cutter radius compensation on"; + } + return -1; + } + if (words.count('P') && words.count('E')) { + if (error) { + *error = "M66 cannot select both P digital and E analog inputs"; + } + return -1; + } + if (!words.count('P') && !words.count('E')) { + if (error) { + *error = "M66 requires a P digital or E analog input index"; + } + return -1; + } + const int input_type = words.count('P') ? 1 : 0; + const int input_index = rounded_word(words, words.count('P') ? 'P' : 'E', 0); + if (input_index < 0) { + if (error) { + *error = input_type == 1 ? "Digital input index out of bounds with M66" + : "Analog input index out of bounds with M66"; + } + return -1; + } + const int wait_type = input_type == 0 ? 0 : rounded_word(words, 'L', 0); + if (input_type == 0 && words.count('L') && rounded_word(words, 'L', 0) != 0) { + if (error) { + *error = "M66 analog input only permits L0 immediate mode"; + } + return -1; + } + if (wait_type > 0 && (!words.count('Q') || words.at('Q') <= 0.0)) { + if (error) { + *error = "M66 non-immediate wait requires Q timeout greater than zero"; + } + return -1; + } + sink_.wait_input(input_index, + input_type, + wait_type, + words.count('Q') ? words.at('Q') : 0.0, + line_number); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + } else if (m == 19) { + if (words.count('R') || words.count('P')) { + sink_.orient_spindle(0, + words.count('R') ? words.at('R') : 0.0, + rounded_word(words, 'P', 0), + line_number); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + } + if (words.count('Q')) { + sink_.wait_spindle_orient_complete(0, words.at('Q'), line_number); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + } + } else if (m == 67 || m == 68) { + if (cutter_comp_on_) { + if (error) { + *error = m == 67 ? "Cannot set motion analog output with cutter radius compensation on" + : "Cannot set auxiliary analog output with cutter radius compensation on"; + } + return -1; + } + if (!words.count('E')) { + if (error) { + *error = "M67/M68 requires an E analog output index"; + } + return -1; + } + if (rounded_word(words, 'E', 0) < 0) { + if (error) { + *error = m == 67 ? "Invalid analog index with M67" : "Invalid analog index with M68"; + } + return -1; + } + sink_.set_analog_output(rounded_word(words, 'E', 0), + words.count('Q') ? words.at('Q') : 0.0, + m == 67, + 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)) { @@ -1981,7 +4295,13 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string return -1; } } else if (m == 98) { - const int sub_number = rounded_word(words, 'P', -1); + int sub_number = -1; + if (!integer_word_value(words, 'P', &sub_number)) { + if (error) { + *error = "M98 requires an integer P subprogram number"; + } + return -1; + } const int repeat_count = std::max(1, rounded_word(words, 'L', 1)); const auto subprogram = subprograms.find(numeric_o_identifier(sub_number)); if (subprogram == subprograms.end()) { @@ -2021,8 +4341,16 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string CallFrame &frame = call_stack.back(); --frame.remaining; if (frame.remaining > 0) { + if (frame.restore_modal_on_return && frame.has_modal_state && + !restore_modal_state(frame_modal_state(frame), line_number)) { + return -1; + } pc = frame.sub_start; } else { + if (frame.restore_modal_on_return && frame.has_modal_state && + !restore_modal_state(frame_modal_state(frame), line_number)) { + return -1; + } pc = frame.return_pc; call_stack.pop_back(); } @@ -2037,20 +4365,53 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string } } if (subprogram_control) { + commit_pending_assignments(); continue; } if (program_end) { + commit_pending_assignments(); break; } + if (coordinate_control) { + commit_pending_assignments(); + continue; + } + if (machine_coordinate_move && !absolute_) { + if (error) { + *error = "Cannot use G53 incremental"; + } + return -1; + } + if (machine_coordinate_move && modal_motion != 0 && modal_motion != 1) { + if (error) { + *error = "Must use G0 or G1 with G53"; + } + return -1; + } + if (machine_coordinate_move && cutter_comp_on_) { + if (error) { + *error = "Cannot use g53 with cutter radius comp"; + } + return -1; + } + if (current_line_has_probe_motion && !has_axis_word(words)) { + if (error) { + *error = "G38 probe move requires at least one axis word"; + } + return -1; + } if (canned_cycle_ != 0) { const CncSimPose initial = sink_.position(); + const CycleAxes cycle_axes = cycle_axes_for_plane(sink_.plane()); + const double initial_depth = pose_axis(initial, cycle_axes.depth); if (words.count('R')) { - canned_r_ = absolute_ ? words.at('R') * sink_.unit_scale() : initial.z + words.at('R') * sink_.unit_scale(); + canned_r_ = absolute_ ? words.at('R') * sink_.unit_scale() : initial_depth + words.at('R') * sink_.unit_scale(); canned_has_r_ = true; } - if (words.count('Z')) { - canned_z_ = absolute_ ? words.at('Z') * sink_.unit_scale() : canned_r_ + words.at('Z') * sink_.unit_scale(); + if (words.count(cycle_axes.depth_word)) { + canned_z_ = absolute_ ? words.at(cycle_axes.depth_word) * sink_.unit_scale() + : canned_r_ + words.at(cycle_axes.depth_word) * sink_.unit_scale(); canned_has_z_ = true; } if (words.count('P')) { @@ -2065,35 +4426,49 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string canned_d_ = words.at('D') * sink_.unit_scale(); canned_has_d_ = true; } - - const bool cycle_requested = has_cycle_position_word(words) || words.count('Z') || words.count('R'); - if (cycle_requested && sink_.plane() != 17) { - if (error) { - *error = "smoke parser canned cycles currently require G17 XY plane"; - } - return -1; + if (words.count('I')) { + canned_i_ = words.at('I') * sink_.unit_scale(); + canned_has_i_ = true; } + if (words.count('J')) { + canned_j_ = words.at('J') * sink_.unit_scale(); + canned_has_j_ = true; + } + if (words.count('K')) { + canned_k_ = words.at('K') * sink_.unit_scale(); + canned_has_k_ = true; + } + + const bool cycle_requested = has_cycle_position_word_for_plane(words, cycle_axes) || + words.count(cycle_axes.depth_word) || + words.count('R'); if (cycle_requested && sink_.feed_rate() == 0.0) { if (error) { *error = "Cannot feed with zero feed rate"; } return -1; } + if (cycle_requested && feed_mode_ == 1) { + if (error) { + *error = "Cannot use inverse time feed with canned cycles"; + } + return -1; + } if (cycle_requested && has_rotary_word(words)) { if (error) { *error = "rotary axis word is not allowed in canned cycle"; } return -1; } - if (cycle_requested && has_uvw_word(words)) { + if (cycle_requested && (has_uvw_word(words) || has_disallowed_cycle_axis_word(words, cycle_axes))) { if (error) { - *error = "UVW axis word is not allowed in G17 canned cycle"; + *error = "axis word is not allowed in the selected canned cycle plane"; } return -1; } const bool same_canned_cycle_mode = previous_canned_cycle == canned_cycle_; const bool current_line_has_r = words.count('R') != 0; - const bool current_line_has_z = words.count('Z') != 0; + const bool current_line_has_z = words.count(cycle_axes.depth_word) != 0; if (cycle_requested && ((!current_line_has_r && !same_canned_cycle_mode) || (!current_line_has_z && !same_canned_cycle_mode) || @@ -2121,23 +4496,66 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string return -1; } if (cycle_requested && - (canned_cycle_ == 82 || canned_cycle_ == 86 || canned_cycle_ == 89) && + (canned_cycle_ == 82 || canned_cycle_ == 86 || canned_cycle_ == 88 || canned_cycle_ == 89) && ((!current_line_has_p && !same_canned_cycle_mode) || !canned_has_p_)) { if (error) { if (canned_cycle_ == 82) { *error = "G82 canned cycle requires P dwell word"; } else { - *error = canned_cycle_ == 86 ? "G86 canned cycle requires P dwell word" : "G89 canned cycle requires P dwell word"; + *error = canned_cycle_ == 86 + ? "G86 canned cycle requires P dwell word" + : (canned_cycle_ == 88 + ? "G88 canned cycle requires P dwell word" + : "G89 canned cycle requires P dwell word"); } } return -1; } + if (cycle_requested && canned_cycle_ == 74 && sink_.spindle_direction() != 2) { + if (error) { + *error = "G74 canned cycle requires a counterclockwise running spindle"; + } + return -1; + } + if (cycle_requested && canned_cycle_ == 84 && sink_.spindle_direction() != 1) { + if (error) { + *error = "G84 canned cycle requires a clockwise running spindle"; + } + return -1; + } if (cycle_requested && canned_cycle_ == 86 && sink_.spindle_direction() == 0) { if (error) { *error = "G86 canned cycle requires a running spindle"; } return -1; } + const bool current_line_has_i = words.count('I') != 0; + const bool current_line_has_j = words.count('J') != 0; + const bool current_line_has_k = words.count('K') != 0; + if (cycle_requested && canned_cycle_ == 87 && + ((!current_line_has_i && !same_canned_cycle_mode) || + (!current_line_has_j && !same_canned_cycle_mode) || + (!current_line_has_k && !same_canned_cycle_mode) || + !canned_has_i_ || + !canned_has_j_ || + !canned_has_k_)) { + if (error) { + *error = "G87 canned cycle requires I, J, and K words"; + } + return -1; + } + if (cycle_requested && canned_cycle_ == 87 && sink_.spindle_direction() == 0) { + if (error) { + *error = "G87 canned cycle requires a running spindle"; + } + return -1; + } + if (cycle_requested && canned_cycle_ == 88 && sink_.spindle_direction() == 0) { + if (error) { + *error = "G88 canned cycle requires a running spindle"; + } + return -1; + } const int repeat_count = positive_integer_word(words, 'L', 1); if (cycle_requested && repeat_count < 1) { if (error) { @@ -2148,25 +4566,26 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string if (cycle_requested) { CncSimPose hole = initial; - apply_cycle_position_axes(&hole, words, absolute_, sink_.unit_scale()); - const double x_increment = (!absolute_ && words.count('X')) ? words.at('X') * sink_.unit_scale() : 0.0; - const double y_increment = (!absolute_ && words.count('Y')) ? words.at('Y') * sink_.unit_scale() : 0.0; - const double a_increment = (!absolute_ && words.count('A')) ? words.at('A') : 0.0; - const double b_increment = (!absolute_ && words.count('B')) ? words.at('B') : 0.0; - const double c_increment = (!absolute_ && words.count('C')) ? words.at('C') : 0.0; + apply_cycle_position_axes(&hole, words, cycle_axes, absolute_, sink_.unit_scale(), lathe_diameter_mode_); + const double first_increment = (!absolute_ && words.count(cycle_axes.first_word)) + ? words.at(cycle_axes.first_word) * sink_.unit_scale() + : 0.0; + const double second_increment = (!absolute_ && words.count(cycle_axes.second_word)) + ? words.at(cycle_axes.second_word) * sink_.unit_scale() + : 0.0; const int cycle_start_spindle_direction = sink_.spindle_direction(); auto run_cycle_at_hole = [&](const CncSimPose &target_hole) -> bool { CncSimPose rapid_hole = target_hole; - rapid_hole.z = sink_.position().z; + set_pose_axis(&rapid_hole, cycle_axes.depth, pose_axis(sink_.position(), cycle_axes.depth)); sink_.straight_traverse(line_number, rapid_hole); if (stop_if_callback_aborted(sink_, error)) { return false; } CncSimPose r_plane = target_hole; - r_plane.z = canned_r_; - if (sink_.position().z != r_plane.z) { + set_pose_axis(&r_plane, cycle_axes.depth, canned_r_); + if (pose_axis(sink_.position(), cycle_axes.depth) != canned_r_) { sink_.straight_traverse(line_number, r_plane); if (stop_if_callback_aborted(sink_, error)) { return false; @@ -2178,7 +4597,7 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string while (next_depth > canned_z_) { next_depth = std::max(canned_z_, next_depth - canned_q_); CncSimPose peck = r_plane; - peck.z = next_depth; + set_pose_axis(&peck, cycle_axes.depth, next_depth); sink_.straight_feed(line_number, peck); if (stop_if_callback_aborted(sink_, error)) { return false; @@ -2187,7 +4606,7 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string CncSimPose retract = canned_cycle_ == 73 ? peck : r_plane; const double peck_clearance = canned_has_d_ ? canned_d_ : 1.0 * sink_.unit_scale(); if (canned_cycle_ == 73) { - retract.z = std::min(canned_r_, next_depth + peck_clearance); + set_pose_axis(&retract, cycle_axes.depth, std::min(canned_r_, next_depth + peck_clearance)); } sink_.straight_traverse(line_number, retract); if (stop_if_callback_aborted(sink_, error)) { @@ -2195,7 +4614,7 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string } if (canned_cycle_ == 83) { CncSimPose return_depth = r_plane; - return_depth.z = std::min(canned_r_, next_depth + peck_clearance); + set_pose_axis(&return_depth, cycle_axes.depth, std::min(canned_r_, next_depth + peck_clearance)); sink_.straight_traverse(line_number, return_depth); if (stop_if_callback_aborted(sink_, error)) { return false; @@ -2203,9 +4622,94 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string } } } + } else if (canned_cycle_ == 87) { + const G87CycleValues g87_values = + g87_cycle_values_for_plane(sink_.plane(), canned_i_, canned_j_, canned_k_); + CncSimPose offset = r_plane; + set_pose_axis(&offset, + cycle_axes.first, + pose_axis(offset, cycle_axes.first) + g87_values.first_offset); + set_pose_axis(&offset, + cycle_axes.second, + pose_axis(offset, cycle_axes.second) + g87_values.second_offset); + sink_.straight_traverse(line_number, offset); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + sink_.stop_spindle(line_number); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + + CncSimPose offset_bottom = offset; + set_pose_axis(&offset_bottom, cycle_axes.depth, canned_z_); + sink_.straight_traverse(line_number, offset_bottom); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + + CncSimPose bottom = r_plane; + set_pose_axis(&bottom, cycle_axes.depth, canned_z_); + sink_.straight_traverse(line_number, bottom); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + + sink_.start_spindle(cycle_start_spindle_direction == 2 ? -1 : 1, line_number); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + + CncSimPose middle = bottom; + set_pose_axis(&middle, + cycle_axes.depth, + absolute_ ? g87_values.middle_depth : canned_z_ + g87_values.middle_depth); + sink_.straight_feed(line_number, middle); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + sink_.straight_feed(line_number, bottom); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + + sink_.stop_spindle(line_number); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + sink_.straight_traverse(line_number, offset_bottom); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + + CncSimPose offset_clear = offset; + if (canned_return_to_initial_ && initial_depth > canned_r_) { + set_pose_axis(&offset_clear, cycle_axes.depth, initial_depth); + } + sink_.straight_traverse(line_number, offset_clear); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + + CncSimPose hole_clear = r_plane; + if (canned_return_to_initial_ && initial_depth > canned_r_) { + set_pose_axis(&hole_clear, cycle_axes.depth, initial_depth); + } + sink_.straight_traverse(line_number, hole_clear); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + + sink_.start_spindle(cycle_start_spindle_direction == 2 ? -1 : 1, line_number); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } } else { CncSimPose bottom = r_plane; - bottom.z = canned_z_; + set_pose_axis(&bottom, cycle_axes.depth, canned_z_); + if (canned_cycle_ == 74 || canned_cycle_ == 84) { + bottom = g74_g84_pose_for_plane(sink_.plane(), r_plane, cycle_axes, canned_z_); + } sink_.straight_feed(line_number, bottom); if (stop_if_callback_aborted(sink_, error)) { return false; @@ -2216,7 +4720,21 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string return false; } } - if (canned_cycle_ == 86 || canned_cycle_ == 89) { + if (canned_cycle_ == 74 || canned_cycle_ == 84) { + sink_.stop_spindle(line_number); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + sink_.start_spindle(canned_cycle_ == 84 ? -1 : 1, line_number); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + sink_.dwell(canned_p_, line_number); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + } + if (canned_cycle_ == 86 || canned_cycle_ == 88 || canned_cycle_ == 89) { sink_.dwell(canned_p_, line_number); if (stop_if_callback_aborted(sink_, error)) { return false; @@ -2228,23 +4746,58 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string return false; } } - if (canned_cycle_ == 85 || canned_cycle_ == 89) { + if (canned_cycle_ == 88) { + sink_.stop_spindle(line_number); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + sink_.program_stop(line_number, 1); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + sink_.start_spindle(cycle_start_spindle_direction == 2 ? -1 : 1, line_number); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + } + if (canned_cycle_ == 74 || canned_cycle_ == 84 || canned_cycle_ == 85 || canned_cycle_ == 89) { CncSimPose feed_retract = r_plane; - if (canned_cycle_ == 89 && canned_return_to_initial_ && initial.z > canned_r_) { - feed_retract.z = initial.z; + double feed_retract_depth = canned_r_; + if ((canned_cycle_ == 74 || canned_cycle_ == 84 || canned_cycle_ == 89) && + canned_return_to_initial_ && initial_depth > canned_r_) { + feed_retract_depth = initial_depth; + set_pose_axis(&feed_retract, cycle_axes.depth, initial_depth); + } + if (canned_cycle_ == 74 || canned_cycle_ == 84) { + feed_retract = g74_g84_pose_for_plane(sink_.plane(), + r_plane, + cycle_axes, + feed_retract_depth); } sink_.straight_feed(line_number, feed_retract); if (stop_if_callback_aborted(sink_, error)) { return false; } } + if (canned_cycle_ == 74 || canned_cycle_ == 84) { + sink_.stop_spindle(line_number); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + sink_.start_spindle(canned_cycle_ == 84 ? 1 : -1, line_number); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + } } CncSimPose retract = r_plane; - if (canned_return_to_initial_ && initial.z > canned_r_) { - retract.z = initial.z; + if (canned_return_to_initial_ && initial_depth > canned_r_) { + set_pose_axis(&retract, cycle_axes.depth, initial_depth); } - if (sink_.position().z != retract.z) { + if (canned_cycle_ != 74 && canned_cycle_ != 84 && + canned_cycle_ != 87 && canned_cycle_ != 88 && + pose_axis(sink_.position(), cycle_axes.depth) != pose_axis(retract, cycle_axes.depth)) { sink_.straight_traverse(line_number, retract); if (stop_if_callback_aborted(sink_, error)) { return false; @@ -2261,30 +4814,283 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string for (int repeat = 0; repeat < repeat_count; ++repeat) { if (repeat > 0 && !absolute_) { - hole.x += x_increment; - hole.y += y_increment; - hole.a += a_increment; - hole.b += b_increment; - hole.c += c_increment; + set_pose_axis(&hole, cycle_axes.first, pose_axis(hole, cycle_axes.first) + first_increment); + set_pose_axis(&hole, cycle_axes.second, pose_axis(hole, cycle_axes.second) + second_increment); } if (!run_cycle_at_hole(hole)) { return -1; } } } - } else if (has_axis_word(words) && (modal_motion == 0 || modal_motion == 1 || modal_motion == 2 || modal_motion == 3)) { + } else if (has_axis_word(words) && + (modal_motion == 0 || + modal_motion == 1 || + modal_motion == 2 || + modal_motion == 3 || + modal_motion_is_threading(modal_motion) || + modal_motion_is_threading_cycle(modal_motion) || + modal_motion_is_probe(modal_motion))) { CncSimEvent event{}; event.start = sink_.position(); event.end = sink_.position(); - apply_axes(&event.end, words, absolute_, sink_.unit_scale()); + apply_axes(&event.end, words, absolute_, sink_.unit_scale(), lathe_diameter_mode_); + if (modal_motion_is_threading(modal_motion) && !words.count('K')) { + if (error) { + *error = "K word missing with G33/G33.1"; + } + return -1; + } + if (modal_motion_is_threading_cycle(modal_motion)) { + if (!words.count('P')) { + if (error) { + *error = "P word missing with G76"; + } + return -1; + } + if (!words.count('I') || !words.count('J') || !words.count('K')) { + if (error) { + *error = "I J or K words missing with G76"; + } + return -1; + } + if (!words.count('Z')) { + if (error) { + *error = "Z word missing with G76"; + } + return -1; + } + if (has_rotary_word(words) || has_uvw_word(words)) { + if (error) { + *error = "Cannot move rotary axes with G76"; + } + return -1; + } + const double i_number = words.at('I') * sink_.unit_scale(); + const double j_number = words.at('J') * sink_.unit_scale(); + const double k_number = words.at('K') * sink_.unit_scale(); + if (i_number == 0.0) { + if (error) { + *error = "In G76, I must not be 0"; + } + return -1; + } + if (j_number <= 0.0) { + if (error) { + *error = "In G76, J must be greater than 0"; + } + return -1; + } + if (k_number <= j_number) { + if (error) { + *error = "In G76, K must be greater than J"; + } + return -1; + } + if (sink_.spindle_direction() == 0) { + if (error) { + *error = "Spindle not turning in G76"; + } + return -1; + } + } + if (modal_motion_is_probe(modal_motion) && sink_.feed_rate() == 0.0) { + if (error) { + *error = "Cannot probe with zero feed rate"; + } + return -1; + } + if ((modal_motion == 1 || modal_motion == 2 || modal_motion == 3) && + sink_.feed_rate() == 0.0) { + if (error) { + *error = "Cannot do feed motion with zero feed rate"; + } + return -1; + } + if (modal_motion_is_probe(modal_motion) && feed_mode_ == 2) { + if (error) { + *error = "Cannot probe with feed per rev mode"; + } + return -1; + } + if (modal_motion_is_probe(modal_motion) && + (modal_probe_type(modal_motion) & 1) == 0 && + pose_near(event.start, event.end)) { + if (error) { + *error = "Start point too close to probe point"; + } + return -1; + } 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()); + set_arc_center(&event, words, sink_.plane(), sink_.unit_scale(), arc_absolute_); } if (modal_motion == 0) { sink_.straight_traverse(line_number, event.end); } else if (modal_motion == 1) { sink_.straight_feed(line_number, event.end); + } else if (modal_motion_is_threading(modal_motion)) { + sink_.set_speed_feed_sync(0, words.at('K') * sink_.unit_scale(), false, true, line_number); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + sink_.straight_feed(line_number, event.end); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + sink_.set_speed_feed_sync(0, 0.0, false, false, line_number); + } else if (modal_motion_is_threading_cycle(modal_motion)) { + const CncSimPose start = sink_.position(); + const double scale = sink_.unit_scale(); + const double diameter_scale = lathe_diameter_mode_ ? 0.5 : 1.0; + const double i_number = words.at('I') * scale * diameter_scale; + const double j_number = words.at('J') * scale * diameter_scale; + const double k_number = words.at('K') * scale * diameter_scale; + const double pitch = words.at('P') * scale; + const bool boring = i_number > 0.0; + const double safe_x = start.x; + const double start_y = start.y; + const double start_z = start.z; + const double end_z = event.end.z; + const double full_dia_depth = std::fabs(i_number); + const double start_depth = std::fabs(i_number) + std::fabs(j_number); + const double cut_increment = std::fabs(j_number); + const double full_threadheight = std::fabs(k_number); + const double end_depth = std::fabs(k_number) + std::fabs(i_number); + double compound_angle = words.count('Q') ? words.at('Q') : 0.0; + compound_angle = degrees_to_radians(end_z > start_z ? -compound_angle : compound_angle); + const int spring_cuts = std::max(0, rounded_word(words, 'H', 0)); + double degression = words.count('R') ? words.at('R') : 1.0; + if (degression < 1.0) { + degression = 1.0; + } + double taper_dist = words.count('E') ? words.at('E') * scale : 0.0; + if (taper_dist < 0.0) { + taper_dist = 0.0; + } + const double taper_pitch = taper_dist > 0.0 + ? pitch * std::hypot(taper_dist, full_threadheight) / taper_dist + : pitch; + if (end_z > start_z) { + taper_dist = -taper_dist; + } + const int taper_flags = std::max(0, rounded_word(words, 'L', 0)); + const bool entry_taper = (taper_flags & 1) != 0; + const bool exit_taper = (taper_flags & 2) != 0; + const double target_z = end_z + full_threadheight * std::tan(compound_angle); + + auto pass_pose = [&](double x, double z) { + CncSimPose pose = sink_.position(); + pose.x = x; + pose.y = start_y; + pose.z = z; + return pose; + }; + auto thread_x = [&](double depth) { + return boring ? safe_x + depth : safe_x - depth; + }; + auto clearance_x = [&](double depth) { + return boring ? safe_x + depth - end_depth : safe_x - depth + end_depth; + }; + auto run_thread_pass = [&](double depth, double zoff) -> bool { + sink_.straight_traverse(line_number, pass_pose(clearance_x(depth), start_z - zoff)); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + + if (taper_dist != 0.0 && entry_taper) { + sink_.set_override_control(50, false, 0, line_number); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + sink_.set_speed_feed_sync(0, taper_pitch, false, true, line_number); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + sink_.straight_feed(line_number, + pass_pose(boring ? safe_x + depth - full_threadheight + : safe_x - depth + full_threadheight, + start_z - zoff)); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + sink_.straight_feed(line_number, pass_pose(thread_x(depth), start_z - zoff - taper_dist)); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + sink_.set_speed_feed_sync(0, pitch, false, true, line_number); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + } else { + sink_.straight_traverse(line_number, pass_pose(thread_x(depth), start_z - zoff)); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + sink_.set_override_control(50, false, 0, line_number); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + sink_.set_speed_feed_sync(0, pitch, false, true, line_number); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + } + + if (taper_dist != 0.0 && exit_taper) { + sink_.straight_feed(line_number, pass_pose(thread_x(depth), target_z - zoff + taper_dist)); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + sink_.set_speed_feed_sync(0, taper_pitch, false, true, line_number); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + sink_.straight_feed(line_number, + pass_pose(boring ? safe_x + depth - full_threadheight + : safe_x - depth + full_threadheight, + target_z - zoff)); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + } else { + sink_.straight_feed(line_number, pass_pose(thread_x(depth), target_z - zoff)); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + } + + sink_.set_speed_feed_sync(0, 0.0, false, false, line_number); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + sink_.straight_traverse(line_number, pass_pose(clearance_x(depth), target_z - zoff)); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + sink_.set_override_control(50, true, 0, line_number); + return !stop_if_callback_aborted(sink_, error); + }; + + double depth = start_depth; + double zoff = (depth - full_dia_depth) * std::tan(compound_angle); + int pass = 1; + while (depth < end_depth) { + if (!run_thread_pass(depth, zoff)) { + return -1; + } + depth = full_dia_depth + cut_increment * std::pow(++pass, 1.0 / degression); + zoff = (depth - full_dia_depth) * std::tan(compound_angle); + } + depth = end_depth; + zoff = (depth - full_dia_depth) * std::tan(compound_angle); + for (int i = 0; i < spring_cuts + 1; ++i) { + if (!run_thread_pass(depth, zoff)) { + return -1; + } + } + sink_.straight_traverse(line_number, event.end); + } else if (modal_motion_is_probe(modal_motion)) { + sink_.straight_probe(line_number, event.end, modal_probe_type(modal_motion)); } else { sink_.arc_feed(line_number, event.end, event.center, event.arc_turns); } @@ -2292,6 +5098,7 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string return -1; } } + commit_pending_assignments(); } if (!conditional_stack.empty()) { diff --git a/core/src/smoke_gcode_parser.h b/core/src/smoke_gcode_parser.h index b155f2f..e9cc931 100644 --- a/core/src/smoke_gcode_parser.h +++ b/core/src/smoke_gcode_parser.h @@ -14,6 +14,10 @@ public: private: CanonEventSink &sink_; bool absolute_ = true; + bool arc_absolute_ = false; + bool lathe_diameter_mode_ = false; + bool cutter_comp_on_ = false; + int feed_mode_ = 0; int canned_cycle_ = 0; bool canned_return_to_initial_ = false; bool canned_has_r_ = false; @@ -21,9 +25,17 @@ private: bool canned_has_p_ = false; bool canned_has_q_ = false; bool canned_has_d_ = false; + bool canned_has_i_ = false; + bool canned_has_j_ = false; + bool canned_has_k_ = false; double canned_r_ = 0.0; double canned_z_ = 0.0; double canned_p_ = 0.0; double canned_q_ = 0.0; double canned_d_ = 1.0; + double canned_i_ = 0.0; + double canned_j_ = 0.0; + double canned_k_ = 0.0; + double oword_return_value_ = 0.0; + bool oword_value_returned_ = false; }; diff --git a/core/tests/cnc_sim_api_linuxcnc_rs274_smoke.cpp b/core/tests/cnc_sim_api_linuxcnc_rs274_smoke.cpp index 4f3e480..0b88e19 100644 --- a/core/tests/cnc_sim_api_linuxcnc_rs274_smoke.cpp +++ b/core/tests/cnc_sim_api_linuxcnc_rs274_smoke.cpp @@ -30,6 +30,27 @@ bool near(double lhs, double rhs) { return std::fabs(lhs - rhs) < 0.000001; } +bool saw_comment_state(const std::vector &events, + int line, + int reserved, + int tool, + double feed, + int arc_turns = 0, + double dwell_seconds = 0.0) { + for (const auto &event : events) { + if (event.type == CNC_SIM_EVENT_COMMENT && + event.line == line && + event.reserved == reserved && + event.tool == tool && + near(event.feed, feed) && + event.arc_turns == arc_turns && + near(event.dwell_seconds, dwell_seconds)) { + return true; + } + } + return false; +} + } // namespace int main() { @@ -73,14 +94,30 @@ int main() { bool saw_g5x_offset = false; bool saw_xy_rotation = false; bool saw_g92_clear = false; + bool saw_g92_suspend = false; + bool saw_g92_restore = false; + bool saw_g92_restore_after_clear = false; + bool saw_g52_initial = false; + bool saw_g52_sticky_axis = false; + bool saw_g52_suspend = false; + bool saw_g52_restore = false; + bool saw_g52_end_line = false; bool saw_coordinate_end_line = false; bool saw_l20_g55_offset = false; bool saw_l20_end_line = false; + bool saw_g10_p0_g55 = false; + bool saw_g10_p0_end_line = false; bool saw_probe = false; + bool saw_probe_error_variant = false; + bool saw_probe_no_error_variant = false; + bool saw_probe_clear_error_variant = false; + bool saw_probe_clear_no_error_variant = false; bool saw_probe_as_feed = false; bool saw_spindle_cw = false; bool saw_spindle_ccw = false; bool saw_spindle_stop = false; + bool saw_spindle_orient = false; + bool saw_spindle_orient_wait = false; bool saw_comment_line = false; bool saw_tool_number_line = false; bool saw_program_stop = false; @@ -96,9 +133,33 @@ int main() { bool saw_g95_mode = false; bool saw_g94_feed_mode = false; bool saw_g95_feed_mode = false; + bool saw_g96_mode = false; + bool saw_g97_mode = false; + bool saw_g33_sync_start = false; + bool saw_g33_sync_stop = false; + bool saw_g33_thread_line = false; + bool saw_g331_sync_start = false; + bool saw_g331_sync_stop = false; + bool saw_g331_rigid_tap_line = false; bool saw_g61_mode = false; bool saw_g611_mode = false; bool saw_g64_mode = false; + bool saw_g18_arc = false; + bool saw_g19_arc = false; + bool saw_g911_arc = false; + bool saw_g901_arc = false; + bool saw_g911_restored_arc = false; + bool saw_g28_waypoint = false; + bool saw_g28_home_axis = false; + bool saw_g30_waypoint = false; + bool saw_g30_home_all_axes = false; + bool saw_g53_rapid = false; + bool saw_g53_modal_rapid = false; + bool saw_g53_feed = false; + bool saw_g431_tool_length = false; + bool saw_g432_tool_length = false; + bool saw_g432_h_tool_length = false; + bool saw_g49_dynamic_tool_length_clear = false; bool saw_tool_length = false; bool saw_tool_length_clear = false; bool saw_cutter_left_first = false; @@ -159,6 +220,229 @@ int main() { 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 arc_planes_program[] = + "G21 G90 G17\n" + "G0 X0 Y0 Z0\n" + "F100\n" + "G18\n" + "G2 X10 Z0 I5 K0\n" + "G0 X0 Y0 Z0\n" + "G19\n" + "G3 Y10 Z0 J5 K0\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, arc_planes_program, sizeof(arc_planes_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_g18_arc = saw_g18_arc || + (event.type == CNC_SIM_EVENT_ARC_FEED && + event.line == 5 && + event.plane == 18 && + event.start.x == 0.0 && + event.start.z == 0.0 && + event.end.x == 10.0 && + event.end.z == 0.0 && + event.center.x == 5.0 && + event.center.z == 0.0 && + event.arc_turns == -1); + saw_g19_arc = saw_g19_arc || + (event.type == CNC_SIM_EVENT_ARC_FEED && + event.line == 8 && + event.plane == 19 && + event.start.y == 0.0 && + event.start.z == 0.0 && + event.end.y == 10.0 && + event.end.z == 0.0 && + event.center.y == 5.0 && + event.center.z == 0.0 && + event.arc_turns == 1); + } + ok &= expect(saw_g18_arc, "expected LinuxCNC G18 XZ arc center mapping"); + ok &= expect(saw_g19_arc, "expected LinuxCNC G19 YZ arc center mapping"); + + const char arc_distance_program[] = + "G21 G90 G17\n" + "G0 X0 Y0 Z0\n" + "F100\n" + "G91.1\n" + "G3 X10 Y0 I5 J0\n" + "G90.1\n" + "G3 X20 Y0 I15 J0\n" + "G91.1\n" + "G3 X30 Y0 I5 J0\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, arc_distance_program, sizeof(arc_distance_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_g911_arc = saw_g911_arc || + (event.type == CNC_SIM_EVENT_ARC_FEED && + event.line == 5 && + event.center.x == 5.0 && + event.center.y == 0.0); + saw_g901_arc = saw_g901_arc || + (event.type == CNC_SIM_EVENT_ARC_FEED && + event.line == 7 && + event.center.x == 15.0 && + event.center.y == 0.0); + saw_g911_restored_arc = saw_g911_restored_arc || + (event.type == CNC_SIM_EVENT_ARC_FEED && + event.line == 9 && + event.center.x == 25.0 && + event.center.y == 0.0); + } + ok &= expect(saw_g911_arc, "expected LinuxCNC G91.1 incremental IJK arc center"); + ok &= expect(saw_g901_arc, "expected LinuxCNC G90.1 absolute IJK arc center"); + ok &= expect(saw_g911_restored_arc, "expected LinuxCNC G91.1 restored incremental IJK arc center"); + + const char predefined_position_program[] = + "G21 G90 G17\n" + "G0 X1 Y2 Z3\n" + "G28.1\n" + "G0 X10 Y20 Z30\n" + "G28 Z5\n" + "G0 X4 Y5 Z6\n" + "G30.1\n" + "G0 X40 Y50 Z60\n" + "G30\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + predefined_position_program, + sizeof(predefined_position_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_g28_waypoint = saw_g28_waypoint || + (event.type == CNC_SIM_EVENT_RAPID && + event.line == 5 && + event.start.x == 10.0 && + event.start.y == 20.0 && + event.start.z == 30.0 && + event.end.x == 10.0 && + event.end.y == 20.0 && + event.end.z == 5.0); + saw_g28_home_axis = saw_g28_home_axis || + (event.type == CNC_SIM_EVENT_RAPID && + event.line == 5 && + event.start.x == 10.0 && + event.start.y == 20.0 && + event.start.z == 5.0 && + event.end.x == 10.0 && + event.end.y == 20.0 && + event.end.z == 3.0); + saw_g30_home_all_axes = saw_g30_home_all_axes || + (event.type == CNC_SIM_EVENT_RAPID && + event.line == 9 && + event.start.x == 40.0 && + event.start.y == 50.0 && + event.start.z == 60.0 && + event.end.x == 4.0 && + event.end.y == 5.0 && + event.end.z == 6.0); + saw_g30_waypoint = saw_g30_waypoint || + (event.type == CNC_SIM_EVENT_RAPID && + event.line == 9 && + event.start.x == 40.0 && + event.start.y == 50.0 && + event.start.z == 60.0 && + event.end.x == 40.0 && + event.end.y == 50.0 && + event.end.z == 60.0); + } + ok &= expect(saw_g28_waypoint, "expected LinuxCNC G28 waypoint rapid"); + ok &= expect(saw_g28_home_axis, "expected LinuxCNC G28 selected-axis home rapid"); + ok &= expect(saw_g30_waypoint, "expected LinuxCNC G30 waypoint rapid"); + ok &= expect(saw_g30_home_all_axes, "expected LinuxCNC G30 all-axis home rapid"); + + const char machine_coordinate_program[] = + "G21 G90 G17\n" + "G0 X5 Y6 Z7\n" + "G53 G0 X0 Z1\n" + "G53 X2\n" + "G1 F100\n" + "G53 G1 Y3\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + machine_coordinate_program, + sizeof(machine_coordinate_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_g53_rapid = saw_g53_rapid || + (event.type == CNC_SIM_EVENT_RAPID && + event.line == 3 && + event.start.x == 5.0 && + event.start.y == 6.0 && + event.start.z == 7.0 && + event.end.x == 0.0 && + event.end.y == 6.0 && + event.end.z == 1.0); + saw_g53_modal_rapid = saw_g53_modal_rapid || + (event.type == CNC_SIM_EVENT_RAPID && + event.line == 4 && + event.start.x == 0.0 && + event.start.y == 6.0 && + event.start.z == 1.0 && + event.end.x == 2.0 && + event.end.y == 6.0 && + event.end.z == 1.0); + saw_g53_feed = saw_g53_feed || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 6 && + event.start.x == 2.0 && + event.start.y == 6.0 && + event.start.z == 1.0 && + event.end.x == 2.0 && + event.end.y == 3.0 && + event.end.z == 1.0); + } + ok &= expect(saw_g53_rapid, "expected LinuxCNC G53 explicit rapid move"); + ok &= expect(saw_g53_modal_rapid, "expected LinuxCNC G53 modal rapid move"); + ok &= expect(saw_g53_feed, "expected LinuxCNC G53 feed move"); + + const char dynamic_tool_length_program[] = + "G21 G90 G17\n" + "G43.1 X1 Z2\n" + "G43.2 X0.25 Z0.5\n" + "G43.2 H1\n" + "G49\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + dynamic_tool_length_program, + sizeof(dynamic_tool_length_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_g431_tool_length = saw_g431_tool_length || + (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 2 && + event.reserved == 430 && + event.start.x == 1.0 && + event.start.z == 2.0); + saw_g432_tool_length = saw_g432_tool_length || + (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 3 && + event.reserved == 430 && + event.start.x == 1.25 && + event.start.z == 2.5); + saw_g49_dynamic_tool_length_clear = saw_g49_dynamic_tool_length_clear || + (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 5 && + event.reserved == 430 && + event.start.x == 0.0 && + event.start.z == 0.0); + saw_g432_h_tool_length = saw_g432_h_tool_length || + (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 4 && + event.reserved == 430 && + event.start.x == 1.25 && + event.start.z == 15.0); + } + ok &= expect(saw_g431_tool_length, "expected LinuxCNC G43.1 dynamic tool length event"); + ok &= expect(saw_g432_tool_length, "expected LinuxCNC G43.2 additive tool length event"); + ok &= expect(saw_g432_h_tool_length, "expected LinuxCNC G43.2 H tool-table additive event"); + ok &= expect(saw_g49_dynamic_tool_length_clear, "expected LinuxCNC G49 dynamic tool length clear event"); + const char spindle_program[] = "G21 G90 G17\n" "S1200 M3\n" @@ -189,6 +473,249 @@ int main() { ok &= expect(saw_spindle_ccw, "expected LinuxCNC M4 counterclockwise spindle state"); ok &= expect(saw_spindle_stop, "expected LinuxCNC M5 stopped spindle state"); + const char spindle_mode_program[] = + "G21 G90 G17\n" + "G97 S1000\n" + "G96 S120 D2500\n" + "G97 S900\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, spindle_mode_program, sizeof(spindle_mode_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_g97_mode = saw_g97_mode || + (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 2 && + event.reserved == 970 && + event.feed == 0.0); + saw_g96_mode = saw_g96_mode || + (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 3 && + event.reserved == 960 && + event.feed == 2500.0); + } + ok &= expect(saw_g97_mode, "expected LinuxCNC G97 constant-RPM spindle mode state"); + ok &= expect(saw_g96_mode, "expected LinuxCNC G96 CSS spindle mode state"); + + const char spindle_orient_program[] = + "G21 G90 G17\n" + "M19 R45 P1 Q2\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, spindle_orient_program, sizeof(spindle_orient_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_spindle_orient = saw_spindle_orient || + (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 2 && + event.reserved == 1901 && + event.tool == 0 && + event.feed == 45.0 && + event.arc_turns == 1); + saw_spindle_orient_wait = saw_spindle_orient_wait || + (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 2 && + event.reserved == 1902 && + event.tool == 0 && + event.dwell_seconds == 2.0); + } + ok &= expect(saw_spindle_orient, "expected LinuxCNC M19 spindle orient state"); + ok &= expect(saw_spindle_orient_wait, "expected LinuxCNC M19 spindle orient wait state"); + + const char io_controls_program[] = + "G21 G90 G17\n" + "M62 P1\n" + "M63 P1\n" + "M64 P2\n" + "M65 P2\n" + "M66 P0 L3 Q5\n" + "M66 E1 L0\n" + "M67 E0 Q1.25\n" + "M68 E1 Q2.5\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, io_controls_program, sizeof(io_controls_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_m62 = false; + bool saw_m63 = false; + bool saw_m64 = false; + bool saw_m65 = false; + bool saw_m66_digital = false; + bool saw_m66_analog = false; + bool saw_m67 = false; + bool saw_m68 = false; + for (const auto &event : events) { + saw_m62 = saw_m62 || (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 2 && + event.reserved == 62 && + event.tool == 1 && + event.feed == 1.0); + saw_m63 = saw_m63 || (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 3 && + event.reserved == 63 && + event.tool == 1 && + event.feed == 0.0); + saw_m64 = saw_m64 || (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 4 && + event.reserved == 64 && + event.tool == 2 && + event.feed == 1.0); + saw_m65 = saw_m65 || (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 5 && + event.reserved == 65 && + event.tool == 2 && + event.feed == 0.0); + saw_m66_digital = saw_m66_digital || (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 6 && + event.reserved == 66 && + event.tool == 0 && + event.arc_turns == 1 && + event.feed == 3.0 && + event.dwell_seconds == 5.0); + saw_m66_analog = saw_m66_analog || (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 7 && + event.reserved == 66 && + event.tool == 1 && + event.arc_turns == 0 && + event.feed == 0.0 && + event.dwell_seconds == 0.0); + saw_m67 = saw_m67 || (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 8 && + event.reserved == 67 && + event.tool == 0 && + event.feed == 1.25); + saw_m68 = saw_m68 || (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 9 && + event.reserved == 68 && + event.tool == 1 && + event.feed == 2.5); + } + ok &= expect(saw_m62, "expected LinuxCNC M62 motion digital output on state"); + ok &= expect(saw_m63, "expected LinuxCNC M63 motion digital output off state"); + ok &= expect(saw_m64, "expected LinuxCNC M64 auxiliary digital output on state"); + ok &= expect(saw_m65, "expected LinuxCNC M65 auxiliary digital output off state"); + ok &= expect(saw_m66_digital, "expected LinuxCNC M66 digital input wait state"); + ok &= expect(saw_m66_analog, "expected LinuxCNC M66 analog input wait state"); + ok &= expect(saw_m67, "expected LinuxCNC M67 motion analog output state"); + ok &= expect(saw_m68, "expected LinuxCNC M68 auxiliary analog output state"); + + const char override_controls_program[] = + "G21 G90 G17\n" + "M48\n" + "M49\n" + "M50 P1\n" + "M50 P0\n" + "M51 P1\n" + "M51 P0\n" + "M52 P1\n" + "M52 P0\n" + "M53 P1\n" + "M53 P0\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, override_controls_program, sizeof(override_controls_program) - 1) == 0, + cnc_sim_last_error(sim)); + ok &= expect(saw_comment_state(events, 2, 50, 0, 1.0), "expected LinuxCNC M48 feed override enable state"); + ok &= expect(saw_comment_state(events, 2, 51, 0, 1.0), "expected LinuxCNC M48 speed override enable state"); + ok &= expect(saw_comment_state(events, 3, 50, 0, 0.0), "expected LinuxCNC M49 feed override disable state"); + ok &= expect(saw_comment_state(events, 3, 51, 0, 0.0), "expected LinuxCNC M49 speed override disable state"); + ok &= expect(saw_comment_state(events, 4, 50, 0, 1.0), "expected LinuxCNC M50 feed override enable state"); + ok &= expect(saw_comment_state(events, 5, 50, 0, 0.0), "expected LinuxCNC M50 feed override disable state"); + ok &= expect(saw_comment_state(events, 6, 51, 0, 1.0), "expected LinuxCNC M51 speed override enable state"); + ok &= expect(saw_comment_state(events, 7, 51, 0, 0.0), "expected LinuxCNC M51 speed override disable state"); + ok &= expect(saw_comment_state(events, 8, 52, 0, 1.0), "expected LinuxCNC M52 adaptive feed enable state"); + ok &= expect(saw_comment_state(events, 9, 52, 0, 0.0), "expected LinuxCNC M52 adaptive feed disable state"); + ok &= expect(saw_comment_state(events, 10, 53, 0, 1.0), "expected LinuxCNC M53 feed hold enable state"); + ok &= expect(saw_comment_state(events, 11, 53, 0, 0.0), "expected LinuxCNC M53 feed hold disable state"); + + const char modal_state_program[] = + "G21 G90 G17\n" + "G0 X0 Y0 Z0\n" + "M70\n" + "G91 G18 G20\n" + "G0 X1 Z1\n" + "M72\n" + "G0 X2 Y0 Z2\n" + "F100 G2 X4 Y0 I1 J0\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, modal_state_program, sizeof(modal_state_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_modal_restored_absolute_mm = false; + bool saw_modal_restored_xy_arc = false; + for (const auto &event : events) { + saw_modal_restored_absolute_mm = saw_modal_restored_absolute_mm || + (event.type == CNC_SIM_EVENT_RAPID && + event.line == 7 && + near(event.end.x, 2.0) && + near(event.end.y, 0.0) && + near(event.end.z, 2.0)); + saw_modal_restored_xy_arc = saw_modal_restored_xy_arc || + (event.type == CNC_SIM_EVENT_ARC_FEED && + event.line == 8 && + event.plane == 17 && + near(event.end.x, 4.0) && + near(event.end.y, 0.0)); + } + ok &= expect(saw_modal_restored_absolute_mm, "expected LinuxCNC M72 restored absolute millimeter move"); + ok &= expect(saw_modal_restored_xy_arc, "expected LinuxCNC M72 restored XY plane arc"); + + const char modal_invalidate_program[] = + "G21 G90 G17\n" + "M70\n" + "M71\n" + "M72\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, modal_invalidate_program, sizeof(modal_invalidate_program) - 1) != 0, + "expected LinuxCNC M71 invalidated modal state to reject M72 restore"); + + const char threading_sync_program[] = + "G21 G90 G17\n" + "S500 M3\n" + "G0 Z5\n" + "G33 Z0 K1\n" + "G0 Z5\n" + "G33.1 Z0 K1\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, threading_sync_program, sizeof(threading_sync_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_g33_sync_start = saw_g33_sync_start || + (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 4 && + event.reserved == 3301 && + event.feed == 1.0); + saw_g33_sync_stop = saw_g33_sync_stop || + (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 4 && + event.reserved == 3300); + saw_g33_thread_line = saw_g33_thread_line || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + event.end.z == 0.0); + saw_g331_sync_start = saw_g331_sync_start || + (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 6 && + event.reserved == 3301 && + event.feed == 1.0); + saw_g331_sync_stop = saw_g331_sync_stop || + (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 6 && + event.reserved == 3300); + saw_g331_rigid_tap_line = saw_g331_rigid_tap_line || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 6 && + event.end.z == 0.0); + } + ok &= expect(saw_g33_sync_start, "expected LinuxCNC G33 speed-feed sync start"); + ok &= expect(saw_g33_sync_stop, "expected LinuxCNC G33 speed-feed sync stop"); + ok &= expect(saw_g33_thread_line, "expected LinuxCNC G33 threading feed line"); + ok &= expect(saw_g331_sync_start, "expected LinuxCNC G33.1 speed-feed sync start"); + ok &= expect(saw_g331_sync_stop, "expected LinuxCNC G33.1 speed-feed sync stop"); + ok &= expect(saw_g331_rigid_tap_line, "expected LinuxCNC G33.1 rigid tap line number"); + const char tool_number_program[] = "G21 G90 G17\n" "M61 Q1\n" @@ -297,7 +824,7 @@ int main() { saw_g95_feed_mode = saw_g95_feed_mode || (event.type == CNC_SIM_EVENT_SET_FEED && event.line == 4 && - event.reserved == 0); + event.reserved == 1); } ok &= expect(saw_g93_mode, "expected LinuxCNC G93 feed mode state"); ok &= expect(saw_g94_mode, "expected LinuxCNC G94 feed mode state"); @@ -464,7 +991,13 @@ int main() { "G21 G90 G17\n" "G0 X0 Y0 Z5\n" "F100\n" - "G38.3 Z0\n" + "G38.2 Z4\n" + "G0 Z5\n" + "G38.3 Z3\n" + "G0 Z5\n" + "G38.4 Z2\n" + "G0 Z5\n" + "G38.5 Z1\n" "M30\n"; events.clear(); ok &= expect(cnc_sim_parse_program(sim, probe_program, sizeof(probe_program) - 1) == 0, @@ -472,16 +1005,47 @@ int main() { for (const auto &event : events) { saw_probe = saw_probe || (event.type == CNC_SIM_EVENT_PROBE && - event.line == 4 && + event.line == 6 && event.feed == 100.0 && event.start.z == 5.0 && - event.end.z == 0.0); + event.end.z == 3.0); + saw_probe_error_variant = saw_probe_error_variant || + (event.type == CNC_SIM_EVENT_PROBE && + event.line == 4 && + event.reserved == 0 && + event.start.z == 5.0 && + event.end.z == 4.0); + saw_probe_no_error_variant = saw_probe_no_error_variant || + (event.type == CNC_SIM_EVENT_PROBE && + event.line == 6 && + event.reserved == 1 && + event.start.z == 5.0 && + event.end.z == 3.0); + saw_probe_clear_error_variant = saw_probe_clear_error_variant || + (event.type == CNC_SIM_EVENT_PROBE && + event.line == 8 && + event.reserved == 2 && + event.start.z == 5.0 && + event.end.z == 2.0); + saw_probe_clear_no_error_variant = saw_probe_clear_no_error_variant || + (event.type == CNC_SIM_EVENT_PROBE && + event.line == 10 && + event.reserved == 3 && + event.start.z == 5.0 && + event.end.z == 1.0); saw_probe_as_feed = saw_probe_as_feed || (event.type == CNC_SIM_EVENT_LINEAR_FEED && - event.line == 4); + (event.line == 4 || + event.line == 6 || + event.line == 8 || + event.line == 10)); } ok &= expect(saw_probe, "expected LinuxCNC G38.3 probe event"); - ok &= expect(!saw_probe_as_feed, "expected LinuxCNC G38.3 to avoid linear-feed event"); + ok &= expect(saw_probe_error_variant, "expected LinuxCNC G38.2 probe event"); + ok &= expect(saw_probe_no_error_variant, "expected LinuxCNC G38.3 probe event with probe type"); + ok &= expect(saw_probe_clear_error_variant, "expected LinuxCNC G38.4 probe-clear event"); + ok &= expect(saw_probe_clear_no_error_variant, "expected LinuxCNC G38.5 probe-clear no-error event"); + ok &= expect(!saw_probe_as_feed, "expected LinuxCNC G38 probe moves to avoid linear-feed events"); const char l20_program[] = "G21 G90 G17\n" @@ -507,11 +1071,37 @@ int main() { ok &= expect(saw_l20_g55_offset, "expected LinuxCNC G10 L20 P2 offset after G55 selection"); ok &= expect(saw_l20_end_line, "expected LinuxCNC G10 L20 program end line number"); + const char p0_coordinate_program[] = + "G21 G90 G17\n" + "G55\n" + "G10 L2 P0 X7 Y8 Z9\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, p0_coordinate_program, sizeof(p0_coordinate_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_g10_p0_g55 = saw_g10_p0_g55 || + (event.type == CNC_SIM_EVENT_SET_G5X_OFFSET && + event.line == 3 && + event.tool == 2 && + event.start.x == 7.0 && + event.start.y == 8.0 && + event.start.z == 9.0); + saw_g10_p0_end_line = saw_g10_p0_end_line || + (event.type == CNC_SIM_EVENT_PROGRAM_END && + event.line == 4); + } + ok &= expect(saw_g10_p0_g55, "expected LinuxCNC G10 L2 P0 to target active G55"); + ok &= expect(saw_g10_p0_end_line, "expected LinuxCNC G10 P0 program end line number"); + const char coordinate_program[] = "G21 G90 G17\n" "G10 L2 P1 X12.5 Y-3 Z4 R30\n" "G92 X1 Y2 Z3\n" + "G92.2\n" + "G92.3\n" "G92.1\n" + "G92.3\n" "M30\n"; events.clear(); ok &= expect(cnc_sim_parse_program(sim, coordinate_program, sizeof(coordinate_program) - 1) == 0, @@ -530,19 +1120,86 @@ int main() { event.feed == 30.0); saw_g92_clear = saw_g92_clear || (event.type == CNC_SIM_EVENT_SET_G92_OFFSET && - event.line == 4 && + event.line == 6 && event.start.x == 0.0 && event.start.y == 0.0 && event.start.z == 0.0); + saw_g92_suspend = saw_g92_suspend || + (event.type == CNC_SIM_EVENT_SET_G92_OFFSET && + event.line == 4 && + event.start.x == 0.0 && + event.start.y == 0.0 && + event.start.z == 0.0); + saw_g92_restore = saw_g92_restore || + (event.type == CNC_SIM_EVENT_SET_G92_OFFSET && + event.line == 5 && + near(event.start.x, -10.325317547305483) && + near(event.start.y, 6.848076211353316) && + event.start.z == -7.0); + saw_g92_restore_after_clear = saw_g92_restore_after_clear || + (event.type == CNC_SIM_EVENT_SET_G92_OFFSET && + event.line == 7 && + event.start.x == 0.0 && + event.start.y == 0.0 && + event.start.z == 0.0); saw_coordinate_end_line = saw_coordinate_end_line || (event.type == CNC_SIM_EVENT_PROGRAM_END && - event.line == 5); + event.line == 8); } 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_suspend, "expected LinuxCNC G92.2 suspend offset event"); + ok &= expect(saw_g92_restore, "expected LinuxCNC G92.3 restore suspended rotated-offset event"); ok &= expect(saw_g92_clear, "expected LinuxCNC G92.1 clear offset event"); + ok &= expect(saw_g92_restore_after_clear, "expected LinuxCNC G92.3 to restore zero after G92.1"); ok &= expect(saw_coordinate_end_line, "expected LinuxCNC coordinate program end line number"); + const char g52_program[] = + "G21 G90 G17\n" + "G52 X1 Y2\n" + "G52 Y3\n" + "G92.2\n" + "G92.3\n" + "G92.1\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, g52_program, sizeof(g52_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_g52_initial = saw_g52_initial || + (event.type == CNC_SIM_EVENT_SET_G92_OFFSET && + event.line == 2 && + event.start.x == 1.0 && + event.start.y == 2.0 && + event.start.z == 0.0); + saw_g52_sticky_axis = saw_g52_sticky_axis || + (event.type == CNC_SIM_EVENT_SET_G92_OFFSET && + event.line == 3 && + event.start.x == 1.0 && + event.start.y == 3.0 && + event.start.z == 0.0); + saw_g52_suspend = saw_g52_suspend || + (event.type == CNC_SIM_EVENT_SET_G92_OFFSET && + event.line == 4 && + event.start.x == 0.0 && + event.start.y == 0.0 && + event.start.z == 0.0); + saw_g52_restore = saw_g52_restore || + (event.type == CNC_SIM_EVENT_SET_G92_OFFSET && + event.line == 5 && + event.start.x == 1.0 && + event.start.y == 3.0 && + event.start.z == 0.0); + saw_g52_end_line = saw_g52_end_line || + (event.type == CNC_SIM_EVENT_PROGRAM_END && + event.line == 7); + } + ok &= expect(saw_g52_initial, "expected LinuxCNC G52 initial local offset event"); + ok &= expect(saw_g52_sticky_axis, "expected LinuxCNC G52 to retain omitted axes"); + ok &= expect(saw_g52_suspend, "expected LinuxCNC G52/G92 shared offset to suspend with G92.2"); + ok &= expect(saw_g52_restore, "expected LinuxCNC G52/G92 shared offset to restore with G92.3"); + ok &= expect(saw_g52_end_line, "expected LinuxCNC G52 program end line number"); + cnc_sim_destroy(sim); int abort_count = 0; diff --git a/core/tests/cnc_sim_api_smoke.cpp b/core/tests/cnc_sim_api_smoke.cpp index 640dbd4..4aa1922 100644 --- a/core/tests/cnc_sim_api_smoke.cpp +++ b/core/tests/cnc_sim_api_smoke.cpp @@ -1,7 +1,10 @@ #include "cnc_sim_api.h" +#include #include +#include #include +#include #include namespace { @@ -24,6 +27,52 @@ bool near(double lhs, double rhs) { return std::fabs(lhs - rhs) < 0.000001; } +class ScopedEnv { +public: + ScopedEnv(const char *name, const char *value) : name_(name) { + const char *old = std::getenv(name); + if (old) { + had_old_ = true; + old_ = old; + } + setenv(name, value, 1); + } + + ~ScopedEnv() { + if (had_old_) { + setenv(name_, old_.c_str(), 1); + } else { + unsetenv(name_); + } + } + +private: + const char *name_; + bool had_old_ = false; + std::string old_; +}; + +bool saw_comment_state(const std::vector &events, + int line, + int reserved, + int tool, + double feed, + int arc_turns = 0, + double dwell_seconds = 0.0) { + for (const auto &event : events) { + if (event.type == CNC_SIM_EVENT_COMMENT && + event.line == line && + event.reserved == reserved && + event.tool == tool && + near(event.feed, feed) && + event.arc_turns == arc_turns && + near(event.dwell_seconds, dwell_seconds)) { + return true; + } + } + return false; +} + } // namespace int main() { @@ -42,6 +91,7 @@ int main() { "G91 G1 X5\n" "G90 G4 P0.25\n" "G2 X25 Y25 R5\n" + "M19 R45 P1 Q2\n" "M30\n"; std::vector events; @@ -73,6 +123,8 @@ int main() { bool saw_g49 = false; bool saw_g434 = false; bool saw_spindle_cw = false; + bool saw_spindle_orient = false; + bool saw_spindle_orient_wait = false; bool saw_spindle_ccw = false; bool saw_spindle_stop = false; bool saw_program_stop = false; @@ -86,11 +138,49 @@ int main() { bool saw_g94_mode = false; bool saw_g95_mode = false; bool saw_g95_feed_mode = false; + bool saw_g96_mode = false; + bool saw_g97_mode = false; bool saw_g61_mode = false; bool saw_g611_mode = false; bool saw_g64_mode = false; + bool saw_smoke_g10_l2 = false; + bool saw_smoke_g10_rotation = false; + bool saw_smoke_g92 = false; + bool saw_smoke_g921 = false; + bool saw_smoke_g922 = false; + bool saw_smoke_g923_restore = false; + bool saw_smoke_g923_after_g921 = false; + bool saw_smoke_g52_initial = false; + bool saw_smoke_g52_sticky_axis = false; + bool saw_smoke_g52_suspend = false; + bool saw_smoke_g52_restore = false; + bool saw_smoke_l20_g55 = false; + bool saw_smoke_l20_g591 = false; + bool saw_smoke_g10_p0_g55 = false; bool saw_incremental_move = false; bool saw_r_arc_center = false; + bool saw_g18_arc = false; + bool saw_g19_arc = false; + bool saw_g911_arc = false; + bool saw_g901_arc = false; + bool saw_g911_restored_arc = false; + bool saw_g28_waypoint = false; + bool saw_g28_home_axis = false; + bool saw_g30_waypoint = false; + bool saw_g30_home_all_axes = false; + bool saw_g53_rapid = false; + bool saw_g53_modal_rapid = false; + bool saw_g53_feed = false; + bool saw_g33_sync_start = false; + bool saw_g33_sync_stop = false; + bool saw_g33_feed = false; + bool saw_g331_sync_start = false; + bool saw_g331_sync_stop = false; + bool saw_g331_feed = false; + bool saw_g431_tool_length = false; + bool saw_g432_tool_length = false; + bool saw_g432_h_tool_length = false; + bool saw_g49_tool_length_clear = false; bool saw_g81_feed = false; bool saw_g81_g98_return = false; bool saw_g82_feed = false; @@ -101,6 +191,8 @@ int main() { bool saw_g83_d_retract_to_r = false; bool saw_g83_d_return_to_depth = false; bool saw_g83_d_second_peck = false; + bool saw_g83_modal_d_return_to_depth = false; + bool saw_g83_modal_d_second_peck = false; bool saw_g85_feed_bottom = false; bool saw_g85_feed_retract = false; bool saw_g85_clearance_retract = false; @@ -119,6 +211,18 @@ int main() { bool saw_g86_spindle_stop = false; bool saw_g86_retract = false; bool saw_g86_spindle_restart = false; + bool saw_g88_feed = false; + bool saw_g88_dwell = false; + bool saw_g88_spindle_stop = false; + bool saw_g88_program_stop = false; + bool saw_g88_spindle_restart = false; + bool saw_g88_auto_retract = false; + bool saw_g84_feed = false; + bool saw_g84_spindle_stop_at_bottom = false; + bool saw_g84_reverse_spindle = false; + bool saw_g84_dwell = false; + bool saw_g84_feed_retract = false; + bool saw_g84_restore_spindle = false; bool saw_g82_sticky_p_first = false; bool saw_g82_sticky_p_second = false; bool saw_g81_sticky_zr_second = false; @@ -142,9 +246,33 @@ int main() { bool saw_named_parameter_call_arg = false; bool saw_named_parameter_local = false; bool saw_named_parameter_global_after_call = false; + bool saw_named_parameter_normalized_reference = false; + bool saw_named_parameter_normalized_exists = false; + bool saw_ini_named_parameter_motion = false; + bool saw_ini_named_parameter_exists_motion = false; + bool saw_hal_named_parameter_motion = false; + bool saw_hal_named_parameter_exists_motion = false; + bool saw_override_named_parameters = false; + bool saw_oword_return_value = false; + bool saw_oword_returned_flag = false; + bool saw_line_named_parameter = false; + bool saw_oword_line_named_parameter = false; + bool saw_call_level_named_parameter = false; + bool saw_remap_level_named_parameter = false; + bool saw_inner_line_named_parameter = false; + bool saw_parallel_assignment_old_value_motion = false; + bool saw_parallel_assignment_new_value_motion = false; + bool saw_parallel_multi_assignment_old_values_motion = false; + bool saw_parallel_multi_assignment_new_values_motion = false; + bool saw_embedded_assignment_old_values_motion = false; + bool saw_embedded_assignment_new_values_motion = false; + bool saw_repeated_assignment_old_value_motion = false; + bool saw_repeated_assignment_last_value_motion = false; bool saw_nested_bracket_assignment = false; bool saw_nested_bracket_motion = false; bool saw_math_function_assignment = false; + bool saw_word_unary_function_motion = false; + bool saw_word_atan_function_motion = false; bool saw_trig_function_motion = false; bool saw_inverse_trig_function_motion = false; bool saw_atan_function_motion = false; @@ -152,6 +280,7 @@ int main() { bool saw_exp_ln_function_motion = false; bool saw_power_expression_motion = false; bool saw_power_precedence_motion = false; + bool saw_signed_power_motion = false; bool saw_mod_expression_motion = false; bool saw_negative_mod_motion = false; bool saw_mod_left_associative_motion = false; @@ -160,6 +289,13 @@ int main() { bool saw_logical_expression_motion = false; bool saw_logical_precedence_motion = false; bool saw_equality_tolerance_motion = false; + bool saw_exists_present_motion = false; + bool saw_exists_missing_motion = false; + bool saw_exists_numbered_motion = false; + bool saw_exists_indirect_numbered_motion = false; + bool saw_indexed_parameter_motion = false; + bool saw_indirect_parameter_motion = false; + bool saw_indexed_parameter_condition_motion = false; bool saw_elseif_branch_motion = false; bool saw_else_branch_motion = false; bool saw_nested_if_branch_motion = false; @@ -177,8 +313,10 @@ int main() { bool saw_repeat_continue_break_body_counter_motion = false; int do_while_motion_count = 0; bool saw_do_while_false_once_motion = false; - bool saw_do_continue_break_final_counter_motion = false; - bool saw_do_continue_break_body_counter_motion = false; + bool saw_do_break_body_motion = false; + bool saw_do_break_final_counter_motion = false; + bool saw_do_continue_final_counter_motion = false; + bool saw_do_continue_body_counter_motion = 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; @@ -221,6 +359,19 @@ int main() { event.line == 6 && event.spindle == 8000.0 && event.reserved == 1); + saw_spindle_orient = saw_spindle_orient || + (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 15 && + event.reserved == 1901 && + event.tool == 0 && + event.feed == 45.0 && + event.arc_turns == 1); + saw_spindle_orient_wait = saw_spindle_orient_wait || + (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 15 && + event.reserved == 1902 && + event.tool == 0 && + event.dwell_seconds == 2.0); saw_incremental_move = saw_incremental_move || (event.type == CNC_SIM_EVENT_LINEAR_FEED && event.start.x == 20.0 && event.end.x == 25.0); @@ -243,9 +394,1048 @@ int main() { 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_spindle_cw, "expected M3 clockwise spindle state"); + ok &= expect(saw_spindle_orient, "expected M19 spindle orient state"); + ok &= expect(saw_spindle_orient_wait, "expected M19 spindle orient wait state"); ok &= expect(saw_incremental_move, "expected G91 incremental move"); ok &= expect(saw_r_arc_center, "expected R arc center calculation"); + const char io_controls_program[] = + "G21 G90 G17\n" + "M62 P1\n" + "M63 P1\n" + "M64 P2\n" + "M65 P2\n" + "M66 P0 L3 Q5\n" + "M66 E1 L0\n" + "M67 E0 Q1.25\n" + "M68 E1 Q2.5\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, io_controls_program, sizeof(io_controls_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_m62 = false; + bool saw_m63 = false; + bool saw_m64 = false; + bool saw_m65 = false; + bool saw_m66_digital = false; + bool saw_m66_analog = false; + bool saw_m67 = false; + bool saw_m68 = false; + for (const auto &event : events) { + saw_m62 = saw_m62 || (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 2 && + event.reserved == 62 && + event.tool == 1 && + event.feed == 1.0); + saw_m63 = saw_m63 || (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 3 && + event.reserved == 63 && + event.tool == 1 && + event.feed == 0.0); + saw_m64 = saw_m64 || (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 4 && + event.reserved == 64 && + event.tool == 2 && + event.feed == 1.0); + saw_m65 = saw_m65 || (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 5 && + event.reserved == 65 && + event.tool == 2 && + event.feed == 0.0); + saw_m66_digital = saw_m66_digital || (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 6 && + event.reserved == 66 && + event.tool == 0 && + event.arc_turns == 1 && + event.feed == 3.0 && + event.dwell_seconds == 5.0); + saw_m66_analog = saw_m66_analog || (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 7 && + event.reserved == 66 && + event.tool == 1 && + event.arc_turns == 0 && + event.feed == 0.0 && + event.dwell_seconds == 0.0); + saw_m67 = saw_m67 || (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 8 && + event.reserved == 67 && + event.tool == 0 && + event.feed == 1.25); + saw_m68 = saw_m68 || (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 9 && + event.reserved == 68 && + event.tool == 1 && + event.feed == 2.5); + } + ok &= expect(saw_m62, "expected M62 motion digital output on state"); + ok &= expect(saw_m63, "expected M63 motion digital output off state"); + ok &= expect(saw_m64, "expected M64 auxiliary digital output on state"); + ok &= expect(saw_m65, "expected M65 auxiliary digital output off state"); + ok &= expect(saw_m66_digital, "expected M66 digital input wait state"); + ok &= expect(saw_m66_analog, "expected M66 analog input wait state"); + ok &= expect(saw_m67, "expected M67 motion analog output state"); + ok &= expect(saw_m68, "expected M68 auxiliary analog output state"); + + const char tool_number_program[] = + "G21 G90 G17\n" + "M61 Q3\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, tool_number_program, sizeof(tool_number_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_m61_tool_number = false; + for (const auto &event : events) { + saw_m61_tool_number = saw_m61_tool_number || + (event.type == CNC_SIM_EVENT_TOOL_CHANGE && + event.line == 2 && + event.tool == 3); + } + ok &= expect(saw_m61_tool_number, "expected M61 tool-number event"); + + const char invalid_tool_number_program[] = + "G21 G90 G17\n" + "M61 Q-1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + invalid_tool_number_program, + sizeof(invalid_tool_number_program) - 1) != 0, + "expected M61 to reject negative Q tool number"); + + const char override_controls_program[] = + "G21 G90 G17\n" + "M48\n" + "M49\n" + "M50 P1\n" + "M50 P0\n" + "M51 P1\n" + "M51 P0\n" + "M52 P1\n" + "M52 P0\n" + "M53 P1\n" + "M53 P0\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, override_controls_program, sizeof(override_controls_program) - 1) == 0, + cnc_sim_last_error(sim)); + ok &= expect(saw_comment_state(events, 2, 50, 0, 1.0), "expected M48 feed override enable state"); + ok &= expect(saw_comment_state(events, 2, 51, 0, 1.0), "expected M48 speed override enable state"); + ok &= expect(saw_comment_state(events, 3, 50, 0, 0.0), "expected M49 feed override disable state"); + ok &= expect(saw_comment_state(events, 3, 51, 0, 0.0), "expected M49 speed override disable state"); + ok &= expect(saw_comment_state(events, 4, 50, 0, 1.0), "expected M50 feed override enable state"); + ok &= expect(saw_comment_state(events, 5, 50, 0, 0.0), "expected M50 feed override disable state"); + ok &= expect(saw_comment_state(events, 6, 51, 0, 1.0), "expected M51 speed override enable state"); + ok &= expect(saw_comment_state(events, 7, 51, 0, 0.0), "expected M51 speed override disable state"); + ok &= expect(saw_comment_state(events, 8, 52, 0, 1.0), "expected M52 adaptive feed enable state"); + ok &= expect(saw_comment_state(events, 9, 52, 0, 0.0), "expected M52 adaptive feed disable state"); + ok &= expect(saw_comment_state(events, 10, 53, 0, 1.0), "expected M53 feed hold enable state"); + ok &= expect(saw_comment_state(events, 11, 53, 0, 0.0), "expected M53 feed hold disable state"); + + const char modal_state_program[] = + "G21 G90 G17\n" + "G0 X0 Y0 Z0\n" + "M70\n" + "G91 G18 G20\n" + "G0 X1 Z1\n" + "M72\n" + "G0 X2 Y0 Z2\n" + "F100 G2 X4 Y0 I1 J0\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, modal_state_program, sizeof(modal_state_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_modal_restored_absolute_mm = false; + bool saw_modal_restored_xy_arc = false; + for (const auto &event : events) { + saw_modal_restored_absolute_mm = saw_modal_restored_absolute_mm || + (event.type == CNC_SIM_EVENT_RAPID && + event.line == 7 && + near(event.end.x, 2.0) && + near(event.end.y, 0.0) && + near(event.end.z, 2.0)); + saw_modal_restored_xy_arc = saw_modal_restored_xy_arc || + (event.type == CNC_SIM_EVENT_ARC_FEED && + event.line == 8 && + event.plane == 17 && + near(event.end.x, 4.0) && + near(event.end.y, 0.0)); + } + ok &= expect(saw_modal_restored_absolute_mm, "expected M72 restored absolute millimeter move"); + ok &= expect(saw_modal_restored_xy_arc, "expected M72 restored XY plane arc"); + + const char modal_autorestore_program[] = + "G21 G91 G17\n" + "G0 X0 Y0 Z0\n" + "O100 call\n" + "G0 X2\n" + "M30\n" + "O100 sub\n" + "M73\n" + "G90\n" + "G0 X10\n" + "O100 endsub\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, modal_autorestore_program, sizeof(modal_autorestore_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_modal_autorestore_sub_move = false; + bool saw_modal_autorestore_caller_move = false; + for (const auto &event : events) { + saw_modal_autorestore_sub_move = saw_modal_autorestore_sub_move || + (event.type == CNC_SIM_EVENT_RAPID && + event.line == 9 && + near(event.end.x, 10.0)); + saw_modal_autorestore_caller_move = saw_modal_autorestore_caller_move || + (event.type == CNC_SIM_EVENT_RAPID && + event.line == 4 && + near(event.start.x, 10.0) && + near(event.end.x, 12.0)); + } + ok &= expect(saw_modal_autorestore_sub_move, "expected M73 subroutine changed modal state move"); + ok &= expect(saw_modal_autorestore_caller_move, "expected M73 autorestored incremental caller move"); + + const char modal_invalidate_program[] = + "G21 G90 G17\n" + "M70\n" + "M71\n" + "M72\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, modal_invalidate_program, sizeof(modal_invalidate_program) - 1) != 0, + "expected M71 invalidated modal state to reject M72 restore"); + + const char arc_planes_program[] = + "G21 G90 G17\n" + "G0 X0 Y0 Z0\n" + "F100\n" + "G18\n" + "G2 X10 Z0 I5 K0\n" + "G0 X0 Y0 Z0\n" + "G19\n" + "G3 Y10 Z0 J5 K0\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, arc_planes_program, sizeof(arc_planes_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_g18_arc = saw_g18_arc || + (event.type == CNC_SIM_EVENT_ARC_FEED && + event.line == 5 && + event.plane == 18 && + event.start.x == 0.0 && + event.start.z == 0.0 && + event.end.x == 10.0 && + event.end.z == 0.0 && + event.center.x == 5.0 && + event.center.z == 0.0 && + event.arc_turns == -1); + saw_g19_arc = saw_g19_arc || + (event.type == CNC_SIM_EVENT_ARC_FEED && + event.line == 8 && + event.plane == 19 && + event.start.y == 0.0 && + event.start.z == 0.0 && + event.end.y == 10.0 && + event.end.z == 0.0 && + event.center.y == 5.0 && + event.center.z == 0.0 && + event.arc_turns == 1); + } + ok &= expect(saw_g18_arc, "expected smoke G18 XZ arc center mapping"); + ok &= expect(saw_g19_arc, "expected smoke G19 YZ arc center mapping"); + + const char arc_distance_program[] = + "G21 G90 G17\n" + "G0 X0 Y0 Z0\n" + "F100\n" + "G91.1\n" + "G3 X10 Y0 I5 J0\n" + "G90.1\n" + "G3 X20 Y0 I15 J0\n" + "G91.1\n" + "G3 X30 Y0 I5 J0\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, arc_distance_program, sizeof(arc_distance_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_g911_arc = saw_g911_arc || + (event.type == CNC_SIM_EVENT_ARC_FEED && + event.line == 5 && + event.center.x == 5.0 && + event.center.y == 0.0); + saw_g901_arc = saw_g901_arc || + (event.type == CNC_SIM_EVENT_ARC_FEED && + event.line == 7 && + event.center.x == 15.0 && + event.center.y == 0.0); + saw_g911_restored_arc = saw_g911_restored_arc || + (event.type == CNC_SIM_EVENT_ARC_FEED && + event.line == 9 && + event.center.x == 25.0 && + event.center.y == 0.0); + } + ok &= expect(saw_g911_arc, "expected smoke G91.1 incremental IJK arc center"); + ok &= expect(saw_g901_arc, "expected smoke G90.1 absolute IJK arc center"); + ok &= expect(saw_g911_restored_arc, "expected smoke G91.1 restored incremental IJK arc center"); + + const char predefined_position_program[] = + "G21 G90 G17\n" + "G0 X1 Y2 Z3\n" + "G28.1\n" + "G0 X10 Y20 Z30\n" + "G28 Z5\n" + "G0 X4 Y5 Z6\n" + "G30.1\n" + "G0 X40 Y50 Z60\n" + "G30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + predefined_position_program, + sizeof(predefined_position_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_g28_waypoint = saw_g28_waypoint || + (event.type == CNC_SIM_EVENT_RAPID && + event.line == 5 && + event.start.x == 10.0 && + event.start.y == 20.0 && + event.start.z == 30.0 && + event.end.x == 10.0 && + event.end.y == 20.0 && + event.end.z == 5.0); + saw_g28_home_axis = saw_g28_home_axis || + (event.type == CNC_SIM_EVENT_RAPID && + event.line == 5 && + event.start.x == 10.0 && + event.start.y == 20.0 && + event.start.z == 5.0 && + event.end.x == 10.0 && + event.end.y == 20.0 && + event.end.z == 3.0); + saw_g30_home_all_axes = saw_g30_home_all_axes || + (event.type == CNC_SIM_EVENT_RAPID && + event.line == 9 && + event.start.x == 40.0 && + event.start.y == 50.0 && + event.start.z == 60.0 && + event.end.x == 4.0 && + event.end.y == 5.0 && + event.end.z == 6.0); + saw_g30_waypoint = saw_g30_waypoint || + (event.type == CNC_SIM_EVENT_RAPID && + event.line == 9 && + event.start.x == 40.0 && + event.start.y == 50.0 && + event.start.z == 60.0 && + event.end.x == 40.0 && + event.end.y == 50.0 && + event.end.z == 60.0); + } + ok &= expect(saw_g28_waypoint, "expected smoke G28 waypoint rapid"); + ok &= expect(saw_g28_home_axis, "expected smoke G28 selected-axis home rapid"); + ok &= expect(saw_g30_waypoint, "expected smoke G30 waypoint rapid"); + ok &= expect(saw_g30_home_all_axes, "expected smoke G30 all-axis home rapid"); + + const char machine_coordinate_program[] = + "G21 G90 G17\n" + "G0 X5 Y6 Z7\n" + "G53 G0 X0 Z1\n" + "G53 X2\n" + "G1 F100\n" + "G53 G1 Y3\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + machine_coordinate_program, + sizeof(machine_coordinate_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_g53_rapid = saw_g53_rapid || + (event.type == CNC_SIM_EVENT_RAPID && + event.line == 3 && + event.start.x == 5.0 && + event.start.y == 6.0 && + event.start.z == 7.0 && + event.end.x == 0.0 && + event.end.y == 6.0 && + event.end.z == 1.0); + saw_g53_modal_rapid = saw_g53_modal_rapid || + (event.type == CNC_SIM_EVENT_RAPID && + event.line == 4 && + event.start.x == 0.0 && + event.start.y == 6.0 && + event.start.z == 1.0 && + event.end.x == 2.0 && + event.end.y == 6.0 && + event.end.z == 1.0); + saw_g53_feed = saw_g53_feed || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 6 && + event.start.x == 2.0 && + event.start.y == 6.0 && + event.start.z == 1.0 && + event.end.x == 2.0 && + event.end.y == 3.0 && + event.end.z == 1.0); + } + ok &= expect(saw_g53_rapid, "expected smoke G53 explicit rapid move"); + ok &= expect(saw_g53_modal_rapid, "expected smoke G53 modal rapid move"); + ok &= expect(saw_g53_feed, "expected smoke G53 feed move"); + + const char threading_program[] = + "G21 G90 G17\n" + "S500 M3\n" + "G0 Z5\n" + "G33 Z0 K1\n" + "G0 Z5\n" + "G33.1 Z0 K1\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, threading_program, sizeof(threading_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_g33_sync_start = saw_g33_sync_start || + (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 4 && + event.reserved == 3301 && + near(event.feed, 1.0)); + saw_g33_sync_stop = saw_g33_sync_stop || + (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 4 && + event.reserved == 3300); + saw_g33_feed = saw_g33_feed || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + near(event.end.z, 0.0)); + saw_g331_sync_start = saw_g331_sync_start || + (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 6 && + event.reserved == 3301 && + near(event.feed, 1.0)); + saw_g331_sync_stop = saw_g331_sync_stop || + (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 6 && + event.reserved == 3300); + saw_g331_feed = saw_g331_feed || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 6 && + near(event.end.z, 0.0)); + } + ok &= expect(saw_g33_sync_start, "expected smoke G33 speed-feed sync start"); + ok &= expect(saw_g33_sync_stop, "expected smoke G33 speed-feed sync stop"); + ok &= expect(saw_g33_feed, "expected smoke G33 threading feed line"); + ok &= expect(saw_g331_sync_start, "expected smoke G33.1 speed-feed sync start"); + ok &= expect(saw_g331_sync_stop, "expected smoke G33.1 speed-feed sync stop"); + ok &= expect(saw_g331_feed, "expected smoke G33.1 rigid tap feed line"); + + const char threading_cycle_program[] = + "G20 G90 G18\n" + "S500 M3\n" + "G0 X1 Z0.1\n" + "G76 P0.05 Z-1 I-.075 J0.008 K0.045 Q29.5 L2 E0.045\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, threading_cycle_program, sizeof(threading_cycle_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_g76_sync_start = false; + bool saw_g76_sync_stop = false; + bool saw_g76_final_feed = false; + bool saw_g76_program_end = false; + for (const auto &event : events) { + saw_g76_sync_start = saw_g76_sync_start || + (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 4 && + event.reserved == 3301 && + near(event.feed, 0.05 * 25.4)); + saw_g76_sync_stop = saw_g76_sync_stop || + (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 4 && + event.reserved == 3300); + saw_g76_final_feed = saw_g76_final_feed || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + near(event.end.z, -25.4)); + saw_g76_program_end = saw_g76_program_end || + (event.type == CNC_SIM_EVENT_PROGRAM_END && + event.line == 5); + } + ok &= expect(saw_g76_sync_start, "expected smoke G76 speed-feed sync start"); + ok &= expect(saw_g76_sync_stop, "expected smoke G76 speed-feed sync stop"); + ok &= expect(saw_g76_final_feed, "expected smoke G76 final threading pass"); + ok &= expect(saw_g76_program_end, "expected smoke G76 program end"); + + const char lathe_mode_program[] = + "G21 G90 G17\n" + "G7\n" + "G0 X1 Z0\n" + "G8\n" + "G0 X1 Z0\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, lathe_mode_program, sizeof(lathe_mode_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_g7_scaled_x = false; + bool saw_g8_full_x = false; + bool saw_lathe_diameter_named_param = false; + bool saw_lathe_radius_named_param = false; + for (const auto &event : events) { + saw_g7_scaled_x = saw_g7_scaled_x || + (event.type == CNC_SIM_EVENT_RAPID && + event.line == 3 && + near(event.end.x, 0.5)); + saw_g8_full_x = saw_g8_full_x || + (event.type == CNC_SIM_EVENT_RAPID && + event.line == 5 && + near(event.end.x, 1.0)); + } + ok &= expect(saw_g7_scaled_x, "expected smoke G7 diameter-mode X scaling"); + ok &= expect(saw_g8_full_x, "expected smoke G8 radius-mode X scaling"); + + const char lathe_named_param_program[] = + "G21 G90 G17\n" + "G7\n" + "O10 if [#<_lathe_diameter_mode> EQ 1]\n" + "G1 X2 F100\n" + "O10 endif\n" + "G8\n" + "O20 if [#<_lathe_radius_mode> EQ 1]\n" + "G1 X3\n" + "O20 endif\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, lathe_named_param_program, sizeof(lathe_named_param_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_lathe_diameter_named_param = saw_lathe_diameter_named_param || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + near(event.end.x, 1.0)); + saw_lathe_radius_named_param = saw_lathe_radius_named_param || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 8 && + near(event.end.x, 3.0)); + } + ok &= expect(saw_lathe_diameter_named_param, "expected #<_lathe_diameter_mode> readonly named parameter"); + ok &= expect(saw_lathe_radius_named_param, "expected #<_lathe_radius_mode> readonly named parameter"); + + const char unit_named_param_program[] = + "G20 G90 G17\n" + "O10 if [#<_imperial> EQ 1]\n" + "G1 X1 F100\n" + "O10 endif\n" + "G21\n" + "O20 if [#<_metric> EQ 1]\n" + "G1 Y1\n" + "O20 endif\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, unit_named_param_program, sizeof(unit_named_param_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_imperial_named_param = false; + bool saw_metric_named_param = false; + for (const auto &event : events) { + saw_imperial_named_param = saw_imperial_named_param || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 3 && + near(event.end.x, 25.4)); + saw_metric_named_param = saw_metric_named_param || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 7 && + near(event.end.y, 1.0)); + } + ok &= expect(saw_imperial_named_param, "expected #<_imperial> readonly named parameter"); + ok &= expect(saw_metric_named_param, "expected #<_metric> readonly named parameter"); + + const char coordinate_system_named_param_program[] = + "G21 G90 G17\n" + "G55\n" + "O10 if [#<_coord_system> EQ 550]\n" + "G1 X1 F100\n" + "O10 endif\n" + "G59.3\n" + "O20 if [#<_coord_system> EQ 593]\n" + "G1 Y1\n" + "O20 endif\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + coordinate_system_named_param_program, + sizeof(coordinate_system_named_param_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_g55_coord_system_named_param = false; + bool saw_g593_coord_system_named_param = false; + for (const auto &event : events) { + saw_g55_coord_system_named_param = saw_g55_coord_system_named_param || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + near(event.end.x, 1.0)); + saw_g593_coord_system_named_param = saw_g593_coord_system_named_param || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 8 && + near(event.end.y, 1.0)); + } + ok &= expect(saw_g55_coord_system_named_param, "expected #<_coord_system> readonly named parameter for G55"); + ok &= expect(saw_g593_coord_system_named_param, "expected #<_coord_system> readonly named parameter for G59.3"); + + const char distance_named_param_program[] = + "G21 G90 G17\n" + "O10 if [#<_absolute> EQ 1]\n" + "G1 X1 F100\n" + "O10 endif\n" + "G91\n" + "O20 if [#<_incremental> EQ 1]\n" + "G1 Y1\n" + "O20 endif\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, distance_named_param_program, sizeof(distance_named_param_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_absolute_named_param = false; + bool saw_incremental_named_param = false; + for (const auto &event : events) { + saw_absolute_named_param = saw_absolute_named_param || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 3 && + near(event.end.x, 1.0)); + saw_incremental_named_param = saw_incremental_named_param || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 7 && + near(event.end.y, 1.0)); + } + ok &= expect(saw_absolute_named_param, "expected #<_absolute> readonly named parameter"); + ok &= expect(saw_incremental_named_param, "expected #<_incremental> readonly named parameter"); + + const char retract_named_param_program[] = + "G21 G90 G17\n" + "G98\n" + "O10 if [#<_retract_r_plane> EQ 1]\n" + "G1 X1 F100\n" + "O10 endif\n" + "G99\n" + "O20 if [#<_retract_old_z> EQ 1]\n" + "G1 Y1\n" + "O20 endif\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, retract_named_param_program, sizeof(retract_named_param_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_retract_r_plane_named_param = false; + bool saw_retract_old_z_named_param = false; + for (const auto &event : events) { + saw_retract_r_plane_named_param = saw_retract_r_plane_named_param || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + near(event.end.x, 1.0)); + saw_retract_old_z_named_param = saw_retract_old_z_named_param || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 8 && + near(event.end.y, 1.0)); + } + ok &= expect(saw_retract_r_plane_named_param, "expected #<_retract_r_plane> readonly named parameter"); + ok &= expect(saw_retract_old_z_named_param, "expected #<_retract_old_z> readonly named parameter"); + + const char ijk_mode_named_param_program[] = + "G21 G90 G17\n" + "G90.1\n" + "O10 if [#<_ijk_absolute_mode> EQ 1]\n" + "G1 X1 F100\n" + "O10 endif\n" + "G91.1\n" + "O20 if [#<_ijk_absolute_mode> EQ 0]\n" + "G1 Y1\n" + "O20 endif\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, ijk_mode_named_param_program, sizeof(ijk_mode_named_param_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_ijk_absolute_named_param = false; + bool saw_ijk_incremental_named_param = false; + for (const auto &event : events) { + saw_ijk_absolute_named_param = saw_ijk_absolute_named_param || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + near(event.end.x, 1.0)); + saw_ijk_incremental_named_param = saw_ijk_incremental_named_param || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 8 && + near(event.end.y, 1.0)); + } + ok &= expect(saw_ijk_absolute_named_param, "expected #<_ijk_absolute_mode> readonly named parameter for G90.1"); + ok &= expect(saw_ijk_incremental_named_param, "expected #<_ijk_absolute_mode> readonly named parameter for G91.1"); + + const char feed_mode_named_param_program[] = + "G21 G90 G17\n" + "G93\n" + "O10 if [#<_inverse_time> EQ 1]\n" + "G1 X1 F1\n" + "O10 endif\n" + "G94\n" + "O20 if [#<_units_per_minute> EQ 1]\n" + "G1 Y1 F100\n" + "O20 endif\n" + "G95\n" + "O30 if [#<_units_per_rev> EQ 1]\n" + "G94\n" + "G1 Z1 F100\n" + "O30 endif\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, feed_mode_named_param_program, sizeof(feed_mode_named_param_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_inverse_time_named_param = false; + bool saw_units_per_minute_named_param = false; + bool saw_units_per_rev_named_param = false; + for (const auto &event : events) { + saw_inverse_time_named_param = saw_inverse_time_named_param || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + near(event.end.x, 1.0)); + saw_units_per_minute_named_param = saw_units_per_minute_named_param || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 8 && + near(event.end.y, 1.0)); + saw_units_per_rev_named_param = saw_units_per_rev_named_param || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 13 && + near(event.end.z, 1.0)); + } + ok &= expect(saw_inverse_time_named_param, "expected #<_inverse_time> readonly named parameter"); + ok &= expect(saw_units_per_minute_named_param, "expected #<_units_per_minute> readonly named parameter"); + ok &= expect(saw_units_per_rev_named_param, "expected #<_units_per_rev> readonly named parameter"); + + const char feed_spindle_named_param_program[] = + "G21 G90 G17\n" + "F123\n" + "S456 M3\n" + "O10 if [[#<_feed> EQ 123] AND [#<_rpm> EQ 456]]\n" + "G1 X1\n" + "O10 endif\n" + "O20 if [[#<_spindle_on> EQ 1] AND [#<_spindle_cw> EQ 1]]\n" + "G1 Y1\n" + "O20 endif\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, feed_spindle_named_param_program, sizeof(feed_spindle_named_param_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_feed_rpm_named_param = false; + bool saw_spindle_named_param = false; + for (const auto &event : events) { + saw_feed_rpm_named_param = saw_feed_rpm_named_param || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 5 && + near(event.end.x, 1.0)); + saw_spindle_named_param = saw_spindle_named_param || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 8 && + near(event.end.y, 1.0)); + } + ok &= expect(saw_feed_rpm_named_param, "expected #<_feed> and #<_rpm> readonly named parameters"); + ok &= expect(saw_spindle_named_param, "expected #<_spindle_on> and #<_spindle_cw> readonly named parameters"); + + const char modal_position_named_param_program[] = + "G21 G90 G17\n" + "T7 M6\n" + "G18\n" + "G43.1 Z2\n" + "G1 X1 Z3 F100\n" + "O10 if [[#<_selected_tool> EQ 7] AND [#<_current_tool> EQ 7] AND [#<_plane> EQ 18] AND [#<_motion_mode> EQ 1]]\n" + "O11 if [[#<_tool_offset> EQ 1] AND [#<_x> EQ 1] AND [#<_z> EQ 3] AND [#<_abs_x> EQ 1]]\n" + "G1 Y1\n" + "O11 endif\n" + "O10 endif\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, modal_position_named_param_program, sizeof(modal_position_named_param_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_modal_position_named_params = false; + for (const auto &event : events) { + saw_modal_position_named_params = saw_modal_position_named_params || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 8 && + near(event.end.y, 1.0)); + } + ok &= expect(saw_modal_position_named_params, + "expected modal, tool, and current-position readonly named parameters"); + + const char dynamic_tool_length_program[] = + "G21 G90 G17\n" + "G43.1 X1 Z2\n" + "G43.2 X0.25 Z0.5\n" + "G43.2 H1\n" + "G49\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + dynamic_tool_length_program, + sizeof(dynamic_tool_length_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_g431_tool_length = saw_g431_tool_length || + (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 2 && + event.reserved == 430 && + event.start.x == 1.0 && + event.start.z == 2.0); + saw_g432_tool_length = saw_g432_tool_length || + (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 3 && + event.reserved == 430 && + event.start.x == 1.25 && + event.start.z == 2.5); + saw_g49_tool_length_clear = saw_g49_tool_length_clear || + (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 5 && + event.reserved == 430 && + event.start.x == 0.0 && + event.start.z == 0.0); + saw_g432_h_tool_length = saw_g432_h_tool_length || + (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 4 && + event.reserved == 430 && + event.start.x == 1.25 && + event.start.z == 15.0); + } + ok &= expect(saw_g431_tool_length, "expected smoke G43.1 dynamic tool length event"); + ok &= expect(saw_g432_tool_length, "expected smoke G43.2 additive tool length event"); + ok &= expect(saw_g432_h_tool_length, "expected smoke G43.2 H tool-table additive event"); + ok &= expect(saw_g49_tool_length_clear, "expected smoke G49 tool length clear event"); + + const char g43_tool_length_program[] = + "G21 G90 G17\n" + "G43 H1\n" + "G49\n" + "T1 M6\n" + "G43\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g43_tool_length_program, + sizeof(g43_tool_length_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_g43_h_tool_length = false; + bool saw_g43_current_tool_length = false; + for (const auto &event : events) { + saw_g43_h_tool_length = saw_g43_h_tool_length || + (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 2 && + event.reserved == 430 && + event.start.z == 12.5); + saw_g43_current_tool_length = saw_g43_current_tool_length || + (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 5 && + event.reserved == 430 && + event.start.z == 12.5); + } + ok &= expect(saw_g43_h_tool_length, "expected smoke G43 H1 tool-table offset"); + ok &= expect(saw_g43_current_tool_length, "expected smoke G43 to use current selected tool"); + + const char invalid_h_word_program[] = + "G21 G90 G17\n" + "H1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + invalid_h_word_program, + sizeof(invalid_h_word_program) - 1) != 0, + "expected bare H word to fail"); + + const char fractional_h_word_program[] = + "G21 G90 G17\n" + "G43 H1.5\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + fractional_h_word_program, + sizeof(fractional_h_word_program) - 1) != 0, + "expected fractional H word to fail"); + + const char bare_d_word_program[] = + "G21 G90 G17\n" + "D1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + bare_d_word_program, + sizeof(bare_d_word_program) - 1) != 0, + "expected bare D word to fail"); + + const char modal_motion_d_word_program[] = + "G21 G90 G17\n" + "G1 D1 X1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + modal_motion_d_word_program, + sizeof(modal_motion_d_word_program) - 1) != 0, + "expected D word with unrelated motion to fail"); + + const char cutter_comp_program[] = + "G21 G90 G17\n" + "G0 X0 Y0 Z0\n" + "F100\n" + "G41 D1\n" + "G1 X20 Y0\n" + "G40\n" + "G1 X0 Y0\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, cutter_comp_program, sizeof(cutter_comp_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_g41 = false; + bool saw_g40 = false; + for (const auto &event : events) { + saw_g41 = saw_g41 || (event.type == CNC_SIM_EVENT_COMMENT && event.reserved == 410); + saw_g40 = saw_g40 || (event.type == CNC_SIM_EVENT_COMMENT && event.reserved == 400); + } + ok &= expect(saw_g41, "expected G41 cutter compensation state event"); + ok &= expect(saw_g40, "expected G40 cutter compensation clear event"); + + const char invalid_cutter_plane_program[] = + "G21 G90 G19\n" + "G41 D1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + invalid_cutter_plane_program, + sizeof(invalid_cutter_plane_program) - 1) != 0, + "expected cutter compensation to reject G19"); + + const char invalid_cutter_g53_program[] = + "G21 G90 G17\n" + "G41 D1\n" + "G53 G0 X0\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + invalid_cutter_g53_program, + sizeof(invalid_cutter_g53_program) - 1) != 0, + "expected G53 to reject cutter compensation"); + + const char invalid_cutter_g28_program[] = + "G21 G90 G17\n" + "G41 D1\n" + "G28 X0\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + invalid_cutter_g28_program, + sizeof(invalid_cutter_g28_program) - 1) != 0, + "expected G28/G30 to reject cutter compensation"); + + const char invalid_cutter_g281_program[] = + "G21 G90 G17\n" + "G41 D1\n" + "G28.1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + invalid_cutter_g281_program, + sizeof(invalid_cutter_g281_program) - 1) != 0, + "expected G28.1/G30.1 to reject cutter compensation"); + + const char invalid_cutter_g92_program[] = + "G21 G90 G17\n" + "G41 D1\n" + "G92 X0\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + invalid_cutter_g92_program, + sizeof(invalid_cutter_g92_program) - 1) != 0, + "expected G92 to reject cutter compensation"); + + const char invalid_cutter_g64_program[] = + "G21 G90 G17\n" + "G41 D1\n" + "G64 P0.01\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + invalid_cutter_g64_program, + sizeof(invalid_cutter_g64_program) - 1) != 0, + "expected G64 to reject cutter compensation"); + + const char invalid_cutter_override_program[] = + "G21 G90 G17\n" + "G41 D1\n" + "M50 P0\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + invalid_cutter_override_program, + sizeof(invalid_cutter_override_program) - 1) != 0, + "expected override controls to reject cutter compensation"); + + const char invalid_cutter_digital_output_program[] = + "G21 G90 G17\n" + "G41 D1\n" + "M62 P1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + invalid_cutter_digital_output_program, + sizeof(invalid_cutter_digital_output_program) - 1) != 0, + "expected digital outputs to reject cutter compensation"); + + const char invalid_cutter_analog_output_program[] = + "G21 G90 G17\n" + "G41 D1\n" + "M67 E0 Q1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + invalid_cutter_analog_output_program, + sizeof(invalid_cutter_analog_output_program) - 1) != 0, + "expected analog outputs to reject cutter compensation"); + + const char invalid_cutter_g76_program[] = + "G21 G90 G17\n" + "G41 D1\n" + "G76 P1 Z-1 I1 J0.1 K0.2\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + invalid_cutter_g76_program, + sizeof(invalid_cutter_g76_program) - 1) != 0, + "expected G76 to reject cutter compensation"); + + const char invalid_cutter_g98_program[] = + "G21 G90 G17\n" + "G41 D1\n" + "G98\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + invalid_cutter_g98_program, + sizeof(invalid_cutter_g98_program) - 1) != 0, + "expected G98/G99 to reject cutter compensation"); + + const char invalid_cutter_tool_change_program[] = + "G21 G90 G17\n" + "T1\n" + "G41 D1\n" + "M6\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + invalid_cutter_tool_change_program, + sizeof(invalid_cutter_tool_change_program) - 1) != 0, + "expected M6 to reject cutter compensation"); + + const char invalid_cutter_wait_input_program[] = + "G21 G90 G17\n" + "G41 D1\n" + "M66 P0 L0\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + invalid_cutter_wait_input_program, + sizeof(invalid_cutter_wait_input_program) - 1) != 0, + "expected M66 to reject cutter compensation"); + + const char invalid_g431_motion_program[] = + "G21 G90 G17\n" + "G43.1 Z2 G1 X3\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + invalid_g431_motion_program, + sizeof(invalid_g431_motion_program) - 1) != 0, + "expected G43.1 to reject motion on the same line"); + + const char mixed_g432_h_axis_program[] = + "G21 G90 G17\n" + "G43.2 H1 Z2\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + mixed_g432_h_axis_program, + sizeof(mixed_g432_h_axis_program) - 1) != 0, + "expected G43.2 to reject mixed H and axis words"); + + const char invalid_g432_program[] = + "G21 G90 G17\n" + "G43.2\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + invalid_g432_program, + sizeof(invalid_g432_program) - 1) != 0, + "expected G43.2 to require H or axis words"); + const char spindle_program[] = "G21\n" "S1200 M3\n" @@ -269,6 +1459,29 @@ int main() { ok &= expect(saw_spindle_ccw, "expected M4 counterclockwise spindle state"); ok &= expect(saw_spindle_stop, "expected M5 stopped spindle state"); + const char spindle_mode_program[] = + "G21\n" + "G97 S1000\n" + "G96 S120 D2500\n" + "G97 S900\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, spindle_mode_program, sizeof(spindle_mode_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_g97_mode = saw_g97_mode || + (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 2 && + event.reserved == 970 && + event.feed == 0.0); + saw_g96_mode = saw_g96_mode || + (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 3 && + event.reserved == 960 && + event.feed == 2500.0); + } + ok &= expect(saw_g97_mode, "expected G97 constant-RPM spindle mode state"); + ok &= expect(saw_g96_mode, "expected G96 CSS spindle mode state"); + const char stop_program[] = "G21\n" "M0\n" @@ -384,6 +1597,164 @@ int main() { ok &= expect(saw_g611_mode, "expected G61.1 exact stop mode"); ok &= expect(saw_g64_mode, "expected G64 continuous mode"); + const char coordinate_program[] = + "G21 G90 G17\n" + "G10 L2 P1 X12.5 Y-3 Z4 R30\n" + "G92 X1 Y2 Z3\n" + "G92.2\n" + "G92.3\n" + "G92.1\n" + "G92.3\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_smoke_g10_l2 = saw_smoke_g10_l2 || + (event.type == CNC_SIM_EVENT_SET_G5X_OFFSET && + event.line == 2 && + event.tool == 1 && + event.start.x == 12.5 && + event.start.y == -3.0 && + event.start.z == 4.0); + saw_smoke_g10_rotation = saw_smoke_g10_rotation || + (event.type == CNC_SIM_EVENT_SET_XY_ROTATION && + event.line == 2 && + event.feed == 30.0); + saw_smoke_g92 = saw_smoke_g92 || + (event.type == CNC_SIM_EVENT_SET_G92_OFFSET && + event.line == 3 && + event.start.x == -1.0 && + event.start.y == -2.0 && + event.start.z == -3.0); + saw_smoke_g921 = saw_smoke_g921 || + (event.type == CNC_SIM_EVENT_SET_G92_OFFSET && + event.line == 6 && + event.start.x == 0.0 && + event.start.y == 0.0 && + event.start.z == 0.0); + saw_smoke_g922 = saw_smoke_g922 || + (event.type == CNC_SIM_EVENT_SET_G92_OFFSET && + event.line == 4 && + event.start.x == 0.0 && + event.start.y == 0.0 && + event.start.z == 0.0); + saw_smoke_g923_restore = saw_smoke_g923_restore || + (event.type == CNC_SIM_EVENT_SET_G92_OFFSET && + event.line == 5 && + event.start.x == -1.0 && + event.start.y == -2.0 && + event.start.z == -3.0); + saw_smoke_g923_after_g921 = saw_smoke_g923_after_g921 || + (event.type == CNC_SIM_EVENT_SET_G92_OFFSET && + event.line == 7 && + event.start.x == 0.0 && + event.start.y == 0.0 && + event.start.z == 0.0); + } + ok &= expect(saw_smoke_g10_l2, "expected smoke G10 L2 G5X offset event"); + ok &= expect(saw_smoke_g10_rotation, "expected smoke G10 L2 XY rotation event"); + ok &= expect(saw_smoke_g92, "expected smoke G92 offset event"); + ok &= expect(saw_smoke_g922, "expected smoke G92.2 suspend offset event"); + ok &= expect(saw_smoke_g923_restore, "expected smoke G92.3 restore suspended offset event"); + ok &= expect(saw_smoke_g921, "expected smoke G92.1 clear event"); + ok &= expect(saw_smoke_g923_after_g921, "expected smoke G92.3 to restore zero after G92.1"); + + const char g52_program[] = + "G21 G90 G17\n" + "G52 X1 Y2\n" + "G52 Y3\n" + "G92.2\n" + "G92.3\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, g52_program, sizeof(g52_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_smoke_g52_initial = saw_smoke_g52_initial || + (event.type == CNC_SIM_EVENT_SET_G92_OFFSET && + event.line == 2 && + event.start.x == 1.0 && + event.start.y == 2.0 && + event.start.z == 0.0); + saw_smoke_g52_sticky_axis = saw_smoke_g52_sticky_axis || + (event.type == CNC_SIM_EVENT_SET_G92_OFFSET && + event.line == 3 && + event.start.x == 1.0 && + event.start.y == 3.0 && + event.start.z == 0.0); + saw_smoke_g52_suspend = saw_smoke_g52_suspend || + (event.type == CNC_SIM_EVENT_SET_G92_OFFSET && + event.line == 4 && + event.start.x == 0.0 && + event.start.y == 0.0 && + event.start.z == 0.0); + saw_smoke_g52_restore = saw_smoke_g52_restore || + (event.type == CNC_SIM_EVENT_SET_G92_OFFSET && + event.line == 5 && + event.start.x == 1.0 && + event.start.y == 3.0 && + event.start.z == 0.0); + } + ok &= expect(saw_smoke_g52_initial, "expected smoke G52 initial local offset event"); + ok &= expect(saw_smoke_g52_sticky_axis, "expected smoke G52 to retain omitted axes"); + ok &= expect(saw_smoke_g52_suspend, "expected smoke G52/G92 shared offset to suspend with G92.2"); + ok &= expect(saw_smoke_g52_restore, "expected smoke G52/G92 shared offset to restore with G92.3"); + + const char l20_program[] = + "G21 G90 G17\n" + "G0 X5 Y6 Z7\n" + "G10 L20 P2 X1 Y2 Z3\n" + "G55\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, l20_program, sizeof(l20_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_smoke_l20_g55 = saw_smoke_l20_g55 || + (event.type == CNC_SIM_EVENT_SET_G5X_OFFSET && + event.line == 4 && + event.tool == 2 && + event.start.x == 4.0 && + event.start.y == 4.0 && + event.start.z == 4.0); + } + ok &= expect(saw_smoke_l20_g55, "expected smoke G10 L20 P2 offset after G55 selection"); + + const char g591_program[] = + "G21 G90 G17\n" + "G0 X7 Y8 Z9\n" + "G10 L20 P7 X1 Y2 Z3\n" + "G59.1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, g591_program, sizeof(g591_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_smoke_l20_g591 = saw_smoke_l20_g591 || + (event.type == CNC_SIM_EVENT_SET_G5X_OFFSET && + event.line == 4 && + event.tool == 7 && + event.start.x == 6.0 && + event.start.y == 6.0 && + event.start.z == 6.0); + } + ok &= expect(saw_smoke_l20_g591, "expected smoke G10 L20 P7 offset after G59.1 selection"); + + const char p0_coordinate_program[] = + "G21 G90 G17\n" + "G55\n" + "G10 L2 P0 X7 Y8 Z9\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, p0_coordinate_program, sizeof(p0_coordinate_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_smoke_g10_p0_g55 = saw_smoke_g10_p0_g55 || + (event.type == CNC_SIM_EVENT_SET_G5X_OFFSET && + event.line == 3 && + event.tool == 2 && + event.start.x == 7.0 && + event.start.y == 8.0 && + event.start.z == 9.0); + } + ok &= expect(saw_smoke_g10_p0_g55, "expected smoke G10 L2 P0 to target active G55"); + const char canned_cycle_program[] = "G21 G90 G17\n" "G0 X0 Y0 Z10\n" @@ -491,6 +1862,58 @@ int main() { ok &= expect(saw_g89_feed_retract, "expected smoke G89 feed retract to clear plane"); ok &= expect(!saw_g80_cancel, "expected G80 to cancel canned cycle motion"); + const char canned_cycle_plane_program[] = + "G21 G90\n" + "G18\n" + "G0 X0 Y5 Z0\n" + "G99 G81 X1 Y-2 Z4 R3 F100\n" + "G19\n" + "G0 X5 Y0 Z0\n" + "G99 G82 X-2 Y3 Z4 R2 P0.2 F100\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, canned_cycle_plane_program, sizeof(canned_cycle_plane_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_g18_g81_feed = false; + bool saw_g18_g81_retract = false; + bool saw_g19_g82_feed = false; + bool saw_g19_g82_dwell = false; + bool saw_g19_g82_retract = false; + for (const auto &event : events) { + saw_g18_g81_feed = saw_g18_g81_feed || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + event.end.x == 1.0 && + event.end.y == -2.0 && + event.end.z == 4.0); + saw_g18_g81_retract = saw_g18_g81_retract || + (event.type == CNC_SIM_EVENT_RAPID && + event.line == 4 && + event.end.x == 1.0 && + event.end.y == 3.0 && + event.end.z == 4.0); + saw_g19_g82_feed = saw_g19_g82_feed || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 7 && + event.end.x == -2.0 && + event.end.y == 3.0 && + event.end.z == 4.0); + saw_g19_g82_dwell = saw_g19_g82_dwell || + (event.type == CNC_SIM_EVENT_DWELL && + event.line == 7 && + event.dwell_seconds == 0.2); + saw_g19_g82_retract = saw_g19_g82_retract || + (event.type == CNC_SIM_EVENT_RAPID && + event.line == 7 && + event.end.x == 2.0 && + event.end.y == 3.0 && + event.end.z == 4.0); + } + ok &= expect(saw_g18_g81_feed, "expected smoke G18 G81 feed along Y depth axis"); + ok &= expect(saw_g18_g81_retract, "expected smoke G18 G81 retract to Y R plane"); + ok &= expect(saw_g19_g82_feed, "expected smoke G19 G82 feed along X depth axis"); + ok &= expect(saw_g19_g82_dwell, "expected smoke G19 G82 dwell at bottom"); + ok &= expect(saw_g19_g82_retract, "expected smoke G19 G82 retract to X R plane"); + const char repeated_cycle_program[] = "G21 G90 G17\n" "G0 X0 Y0 Z0\n" @@ -594,6 +2017,35 @@ int main() { ok &= expect(saw_g83_d_return_to_depth, "expected smoke G83 rapid return to peck clearance depth"); ok &= expect(saw_g83_d_second_peck, "expected smoke G83 feed from peck clearance depth"); + const char g83_modal_d_program[] = + "G21 G90 G17\n" + "G0 X0 Y0 Z5\n" + "G99 G83 X1 Z-3 R1 Q2 F100\n" + "X2 D0.25\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, g83_modal_d_program, sizeof(g83_modal_d_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_g83_modal_d_return_to_depth = saw_g83_modal_d_return_to_depth || + (event.type == CNC_SIM_EVENT_RAPID && + event.line == 4 && + event.start.x == 2.0 && + event.start.z == 1.0 && + event.end.x == 2.0 && + event.end.z == -0.75); + saw_g83_modal_d_second_peck = saw_g83_modal_d_second_peck || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + event.start.x == 2.0 && + event.start.z == -0.75 && + event.end.x == 2.0 && + event.end.z == -3.0); + } + ok &= expect(saw_g83_modal_d_return_to_depth, + "expected modal G83 line to accept D peck clearance"); + ok &= expect(saw_g83_modal_d_second_peck, + "expected modal G83 line to feed from D peck clearance"); + const char g86_program[] = "G21 G90 G17\n" "S900 M3\n" @@ -636,6 +2088,435 @@ int main() { ok &= expect(saw_g86_retract, "expected smoke G86 rapid retract to clear plane"); ok &= expect(saw_g86_spindle_restart, "expected smoke G86 spindle restart"); + const char g88_program[] = + "G21 G90 G17\n" + "S700 M3\n" + "G0 X0 Y0 Z5\n" + "G99 G88 X1 Z-2 R1 P0.07 F70\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, g88_program, sizeof(g88_program) - 1) == 0, + cnc_sim_last_error(sim)); + int g88_feed_index = -1; + int g88_dwell_index = -1; + int g88_stop_index = -1; + int g88_program_stop_index = -1; + int g88_restart_index = -1; + for (size_t event_index = 0; event_index < events.size(); ++event_index) { + const auto &event = events[event_index]; + saw_g88_feed = saw_g88_feed || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + event.start.x == 1.0 && + event.start.z == 1.0 && + event.end.x == 1.0 && + event.end.z == -2.0); + if (saw_g88_feed && g88_feed_index == -1) { + g88_feed_index = static_cast(event_index); + } + saw_g88_dwell = saw_g88_dwell || + (event.type == CNC_SIM_EVENT_DWELL && + event.line == 4 && + event.dwell_seconds == 0.07); + if (saw_g88_dwell && g88_dwell_index == -1) { + g88_dwell_index = static_cast(event_index); + } + saw_g88_spindle_stop = saw_g88_spindle_stop || + (event.type == CNC_SIM_EVENT_SET_SPINDLE && + event.line == 4 && + event.spindle == 0.0 && + event.reserved == 0); + if (saw_g88_spindle_stop && g88_stop_index == -1) { + g88_stop_index = static_cast(event_index); + } + saw_g88_program_stop = saw_g88_program_stop || + (event.type == CNC_SIM_EVENT_PROGRAM_END && + event.line == 4 && + event.reserved == 1); + if (saw_g88_program_stop && g88_program_stop_index == -1) { + g88_program_stop_index = static_cast(event_index); + } + saw_g88_spindle_restart = saw_g88_spindle_restart || + (event.type == CNC_SIM_EVENT_SET_SPINDLE && + event.line == 4 && + event.spindle == 700.0 && + event.reserved == 1); + if (saw_g88_spindle_restart && g88_restart_index == -1) { + g88_restart_index = static_cast(event_index); + } + saw_g88_auto_retract = saw_g88_auto_retract || + (event.type == CNC_SIM_EVENT_RAPID && + event.line == 4 && + event.start.x == 1.0 && + event.start.z == -2.0 && + event.end.x == 1.0 && + event.end.z == 1.0); + } + ok &= expect(saw_g88_feed, "expected smoke G88 feed to bottom"); + ok &= expect(saw_g88_dwell, "expected smoke G88 dwell at bottom"); + ok &= expect(saw_g88_spindle_stop, "expected smoke G88 spindle stop"); + ok &= expect(saw_g88_program_stop, "expected smoke G88 program stop for manual out"); + ok &= expect(saw_g88_spindle_restart, "expected smoke G88 spindle restart"); + ok &= expect(!saw_g88_auto_retract, "expected smoke G88 to leave retract to operator"); + ok &= expect(g88_feed_index < g88_dwell_index && + g88_dwell_index < g88_stop_index && + g88_stop_index < g88_program_stop_index && + g88_program_stop_index < g88_restart_index, + "expected smoke G88 LinuxCNC manual-out order"); + + const char g87_program[] = + "G21 G90 G17\n" + "S800 M3\n" + "G0 X0 Y0 Z5\n" + "G99 G87 X1 Y0 Z-4 R1 I0.2 J0.3 K-2 F80\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, g87_program, sizeof(g87_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_g87_offset_at_r = false; + bool saw_g87_stop_before_entry = false; + bool saw_g87_offset_bottom = false; + bool saw_g87_center_bottom = false; + bool saw_g87_restart_before_cut = false; + bool saw_g87_feed_middle = false; + bool saw_g87_feed_bottom = false; + bool saw_g87_stop_after_cut = false; + bool saw_g87_offset_exit = false; + bool saw_g87_clear_offset = false; + bool saw_g87_clear_center = false; + bool saw_g87_final_restart = false; + int g87_offset_at_r_index = -1; + int g87_stop_before_entry_index = -1; + int g87_offset_bottom_index = -1; + int g87_center_bottom_index = -1; + int g87_restart_before_cut_index = -1; + int g87_feed_middle_index = -1; + int g87_feed_bottom_index = -1; + int g87_stop_after_cut_index = -1; + int g87_offset_exit_index = -1; + int g87_clear_offset_index = -1; + int g87_clear_center_index = -1; + int g87_final_restart_index = -1; + for (size_t event_index = 0; event_index < events.size(); ++event_index) { + const auto &event = events[event_index]; + saw_g87_offset_at_r = saw_g87_offset_at_r || + (event.type == CNC_SIM_EVENT_RAPID && + event.line == 4 && + event.start.x == 1.0 && + event.start.y == 0.0 && + event.start.z == 1.0 && + event.end.x == 1.2 && + event.end.y == 0.3 && + event.end.z == 1.0); + if (saw_g87_offset_at_r && g87_offset_at_r_index == -1) { + g87_offset_at_r_index = static_cast(event_index); + } + saw_g87_stop_before_entry = saw_g87_stop_before_entry || + (event.type == CNC_SIM_EVENT_SET_SPINDLE && + event.line == 4 && + event.spindle == 0.0 && + event.reserved == 0); + if (saw_g87_stop_before_entry && g87_stop_before_entry_index == -1) { + g87_stop_before_entry_index = static_cast(event_index); + } + saw_g87_offset_bottom = saw_g87_offset_bottom || + (event.type == CNC_SIM_EVENT_RAPID && + event.line == 4 && + event.start.x == 1.2 && + event.start.y == 0.3 && + event.start.z == 1.0 && + event.end.x == 1.2 && + event.end.y == 0.3 && + event.end.z == -4.0); + if (saw_g87_offset_bottom && g87_offset_bottom_index == -1) { + g87_offset_bottom_index = static_cast(event_index); + } + saw_g87_center_bottom = saw_g87_center_bottom || + (event.type == CNC_SIM_EVENT_RAPID && + event.line == 4 && + event.start.x == 1.2 && + event.start.y == 0.3 && + event.start.z == -4.0 && + event.end.x == 1.0 && + event.end.y == 0.0 && + event.end.z == -4.0); + if (saw_g87_center_bottom && g87_center_bottom_index == -1) { + g87_center_bottom_index = static_cast(event_index); + } + saw_g87_restart_before_cut = saw_g87_restart_before_cut || + (event.type == CNC_SIM_EVENT_SET_SPINDLE && + event.line == 4 && + event.spindle == 800.0 && + event.reserved == 1); + if (saw_g87_restart_before_cut && g87_restart_before_cut_index == -1) { + g87_restart_before_cut_index = static_cast(event_index); + } + saw_g87_feed_middle = saw_g87_feed_middle || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + event.start.x == 1.0 && + event.start.z == -4.0 && + event.end.x == 1.0 && + event.end.z == -2.0); + if (saw_g87_feed_middle && g87_feed_middle_index == -1) { + g87_feed_middle_index = static_cast(event_index); + } + saw_g87_feed_bottom = saw_g87_feed_bottom || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + event.start.x == 1.0 && + event.start.z == -2.0 && + event.end.x == 1.0 && + event.end.z == -4.0); + if (saw_g87_feed_bottom && g87_feed_bottom_index == -1) { + g87_feed_bottom_index = static_cast(event_index); + } + saw_g87_stop_after_cut = saw_g87_stop_after_cut || + (event.type == CNC_SIM_EVENT_SET_SPINDLE && + event.line == 4 && + event.spindle == 0.0 && + event.reserved == 0 && + g87_feed_bottom_index != -1); + if (saw_g87_stop_after_cut && g87_stop_after_cut_index == -1) { + g87_stop_after_cut_index = static_cast(event_index); + } + saw_g87_offset_exit = saw_g87_offset_exit || + (event.type == CNC_SIM_EVENT_RAPID && + event.line == 4 && + event.start.x == 1.0 && + event.start.y == 0.0 && + event.start.z == -4.0 && + event.end.x == 1.2 && + event.end.y == 0.3 && + event.end.z == -4.0); + if (saw_g87_offset_exit && g87_offset_exit_index == -1) { + g87_offset_exit_index = static_cast(event_index); + } + saw_g87_clear_offset = saw_g87_clear_offset || + (event.type == CNC_SIM_EVENT_RAPID && + event.line == 4 && + event.start.x == 1.2 && + event.start.y == 0.3 && + event.start.z == -4.0 && + event.end.x == 1.2 && + event.end.y == 0.3 && + event.end.z == 1.0); + if (saw_g87_clear_offset && g87_clear_offset_index == -1) { + g87_clear_offset_index = static_cast(event_index); + } + saw_g87_clear_center = saw_g87_clear_center || + (event.type == CNC_SIM_EVENT_RAPID && + event.line == 4 && + event.start.x == 1.2 && + event.start.y == 0.3 && + event.start.z == 1.0 && + event.end.x == 1.0 && + event.end.y == 0.0 && + event.end.z == 1.0); + if (saw_g87_clear_center && g87_clear_center_index == -1) { + g87_clear_center_index = static_cast(event_index); + } + saw_g87_final_restart = saw_g87_final_restart || + (event.type == CNC_SIM_EVENT_SET_SPINDLE && + event.line == 4 && + event.spindle == 800.0 && + event.reserved == 1 && + g87_clear_center_index != -1); + if (saw_g87_final_restart && g87_final_restart_index == -1) { + g87_final_restart_index = static_cast(event_index); + } + } + ok &= expect(saw_g87_offset_at_r, "expected smoke G87 offset traverse at R plane"); + ok &= expect(saw_g87_stop_before_entry, "expected smoke G87 spindle stop before entry"); + ok &= expect(saw_g87_offset_bottom, "expected smoke G87 offset plunge to bottom"); + ok &= expect(saw_g87_center_bottom, "expected smoke G87 traverse to hole center at bottom"); + ok &= expect(saw_g87_restart_before_cut, "expected smoke G87 spindle restart before cutting feed"); + ok &= expect(saw_g87_feed_middle, "expected smoke G87 feed to middle K plane"); + ok &= expect(saw_g87_feed_bottom, "expected smoke G87 feed back to bottom"); + ok &= expect(saw_g87_stop_after_cut, "expected smoke G87 spindle stop after cutting feed"); + ok &= expect(saw_g87_offset_exit, "expected smoke G87 offset exit at bottom"); + ok &= expect(saw_g87_clear_offset, "expected smoke G87 offset retract to clear plane"); + ok &= expect(saw_g87_clear_center, "expected smoke G87 traverse back to hole center"); + ok &= expect(saw_g87_final_restart, "expected smoke G87 final spindle restart"); + ok &= expect(g87_offset_at_r_index < g87_stop_before_entry_index && + g87_stop_before_entry_index < g87_offset_bottom_index && + g87_offset_bottom_index < g87_center_bottom_index && + g87_center_bottom_index < g87_restart_before_cut_index && + g87_restart_before_cut_index < g87_feed_middle_index && + g87_feed_middle_index < g87_feed_bottom_index && + g87_feed_bottom_index < g87_stop_after_cut_index && + g87_stop_after_cut_index < g87_offset_exit_index && + g87_offset_exit_index < g87_clear_offset_index && + g87_clear_offset_index < g87_clear_center_index && + g87_clear_center_index < g87_final_restart_index, + "expected smoke G87 LinuxCNC back-boring order"); + + const char g84_program[] = + "G21 G90 G17\n" + "S600 M3\n" + "G0 X0 Y0 Z5\n" + "G98 G84 X1 Z-2 R1 P0.05 F60\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, g84_program, sizeof(g84_program) - 1) == 0, + cnc_sim_last_error(sim)); + int g84_feed_index = -1; + int g84_stop_index = -1; + int g84_reverse_index = -1; + int g84_dwell_index = -1; + int g84_feed_retract_index = -1; + int g84_restore_index = -1; + for (size_t event_index = 0; event_index < events.size(); ++event_index) { + const auto &event = events[event_index]; + saw_g84_feed = saw_g84_feed || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + event.start.x == 1.0 && + event.start.z == 1.0 && + event.end.x == 1.0 && + event.end.z == -2.0); + if (saw_g84_feed && g84_feed_index == -1) { + g84_feed_index = static_cast(event_index); + } + saw_g84_spindle_stop_at_bottom = saw_g84_spindle_stop_at_bottom || + (event.type == CNC_SIM_EVENT_SET_SPINDLE && + event.line == 4 && + event.spindle == 0.0 && + event.reserved == 0); + if (saw_g84_spindle_stop_at_bottom && g84_stop_index == -1) { + g84_stop_index = static_cast(event_index); + } + saw_g84_reverse_spindle = saw_g84_reverse_spindle || + (event.type == CNC_SIM_EVENT_SET_SPINDLE && + event.line == 4 && + event.spindle == 600.0 && + event.reserved == 2); + if (saw_g84_reverse_spindle && g84_reverse_index == -1) { + g84_reverse_index = static_cast(event_index); + } + saw_g84_dwell = saw_g84_dwell || + (event.type == CNC_SIM_EVENT_DWELL && + event.line == 4 && + event.dwell_seconds == 0.05); + if (saw_g84_dwell && g84_dwell_index == -1) { + g84_dwell_index = static_cast(event_index); + } + saw_g84_feed_retract = saw_g84_feed_retract || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + event.start.x == 1.0 && + event.start.z == -2.0 && + event.end.x == 1.0 && + event.end.z == 5.0); + if (saw_g84_feed_retract && g84_feed_retract_index == -1) { + g84_feed_retract_index = static_cast(event_index); + } + saw_g84_restore_spindle = saw_g84_restore_spindle || + (event.type == CNC_SIM_EVENT_SET_SPINDLE && + event.line == 4 && + event.spindle == 600.0 && + event.reserved == 1); + if (saw_g84_restore_spindle && g84_restore_index == -1) { + g84_restore_index = static_cast(event_index); + } + } + ok &= expect(saw_g84_feed, "expected smoke G84 feed to bottom"); + ok &= expect(saw_g84_spindle_stop_at_bottom, "expected smoke G84 spindle stop at bottom"); + ok &= expect(saw_g84_reverse_spindle, "expected smoke G84 reverse spindle before feed out"); + ok &= expect(saw_g84_dwell, "expected smoke G84 dwell after reversing spindle"); + ok &= expect(saw_g84_feed_retract, "expected smoke G84 feed retract to initial plane"); + ok &= expect(saw_g84_restore_spindle, "expected smoke G84 restore clockwise spindle"); + ok &= expect(g84_feed_index < g84_stop_index && + g84_stop_index < g84_reverse_index && + g84_reverse_index < g84_dwell_index && + g84_dwell_index < g84_feed_retract_index && + g84_feed_retract_index < g84_restore_index, + "expected smoke G84 LinuxCNC spindle and feed-out order"); + + const char g74_program[] = + "G21 G90 G17\n" + "S500 M4\n" + "G0 X0 Y0 Z5\n" + "G98 G74 X1 Z-2 R1 P0.04 F50\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, g74_program, sizeof(g74_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_g74_feed = false; + bool saw_g74_spindle_stop_at_bottom = false; + bool saw_g74_forward_spindle = false; + bool saw_g74_dwell = false; + bool saw_g74_feed_retract = false; + bool saw_g74_restore_spindle = false; + int g74_feed_index = -1; + int g74_stop_index = -1; + int g74_forward_index = -1; + int g74_dwell_index = -1; + int g74_feed_retract_index = -1; + int g74_restore_index = -1; + for (size_t event_index = 0; event_index < events.size(); ++event_index) { + const auto &event = events[event_index]; + saw_g74_feed = saw_g74_feed || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + event.start.x == 1.0 && + event.start.z == 1.0 && + event.end.x == 1.0 && + event.end.z == -2.0); + if (saw_g74_feed && g74_feed_index == -1) { + g74_feed_index = static_cast(event_index); + } + saw_g74_spindle_stop_at_bottom = saw_g74_spindle_stop_at_bottom || + (event.type == CNC_SIM_EVENT_SET_SPINDLE && + event.line == 4 && + event.spindle == 0.0 && + event.reserved == 0); + if (saw_g74_spindle_stop_at_bottom && g74_stop_index == -1) { + g74_stop_index = static_cast(event_index); + } + saw_g74_forward_spindle = saw_g74_forward_spindle || + (event.type == CNC_SIM_EVENT_SET_SPINDLE && + event.line == 4 && + event.spindle == 500.0 && + event.reserved == 1); + if (saw_g74_forward_spindle && g74_forward_index == -1) { + g74_forward_index = static_cast(event_index); + } + saw_g74_dwell = saw_g74_dwell || + (event.type == CNC_SIM_EVENT_DWELL && + event.line == 4 && + event.dwell_seconds == 0.04); + if (saw_g74_dwell && g74_dwell_index == -1) { + g74_dwell_index = static_cast(event_index); + } + saw_g74_feed_retract = saw_g74_feed_retract || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + event.start.x == 1.0 && + event.start.z == -2.0 && + event.end.x == 1.0 && + event.end.z == 5.0); + if (saw_g74_feed_retract && g74_feed_retract_index == -1) { + g74_feed_retract_index = static_cast(event_index); + } + saw_g74_restore_spindle = saw_g74_restore_spindle || + (event.type == CNC_SIM_EVENT_SET_SPINDLE && + event.line == 4 && + event.spindle == 500.0 && + event.reserved == 2); + if (saw_g74_restore_spindle && g74_restore_index == -1) { + g74_restore_index = static_cast(event_index); + } + } + ok &= expect(saw_g74_feed, "expected smoke G74 feed to bottom"); + ok &= expect(saw_g74_spindle_stop_at_bottom, "expected smoke G74 spindle stop at bottom"); + ok &= expect(saw_g74_forward_spindle, "expected smoke G74 clockwise spindle before feed out"); + ok &= expect(saw_g74_dwell, "expected smoke G74 dwell after reversing spindle"); + ok &= expect(saw_g74_feed_retract, "expected smoke G74 feed retract to initial plane"); + ok &= expect(saw_g74_restore_spindle, "expected smoke G74 restore counterclockwise spindle"); + ok &= expect(g74_feed_index < g74_stop_index && + g74_stop_index < g74_forward_index && + g74_forward_index < g74_dwell_index && + g74_dwell_index < g74_feed_retract_index && + g74_feed_retract_index < g74_restore_index, + "expected smoke G74 LinuxCNC spindle and feed-out order"); + const char sticky_p_program[] = "G21 G90 G17\n" "G0 X0 Y0 Z5\n" @@ -688,6 +2569,15 @@ int main() { ok &= expect(cnc_sim_parse_program(sim, missing_cycle_feed_program, sizeof(missing_cycle_feed_program) - 1) != 0, "expected G81 without feed rate to fail"); + const char inverse_time_cycle_program[] = + "G21 G90 G17\n" + "G93\n" + "F100\n" + "G81 X1 Z-1 R1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, inverse_time_cycle_program, sizeof(inverse_time_cycle_program) - 1) != 0, + "expected canned cycle in inverse time feed mode to fail"); + const char missing_g73_q_program[] = "G21 G90 G17\n" "G73 X1 Z-1 R1\n"; @@ -710,6 +2600,44 @@ int main() { ok &= expect(cnc_sim_parse_program(sim, stopped_g86_program, sizeof(stopped_g86_program) - 1) != 0, "expected G86 with stopped spindle to fail"); + const char stopped_g88_program[] = + "G21 G90 G17\n" + "G88 X1 Z-1 R1 P0.1 F70\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, stopped_g88_program, sizeof(stopped_g88_program) - 1) != 0, + "expected G88 with stopped spindle to fail"); + + const char stopped_g87_program[] = + "G21 G90 G17\n" + "G87 X1 Z-1 R1 I0.1 J0.1 K0 F80\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, stopped_g87_program, sizeof(stopped_g87_program) - 1) != 0, + "expected G87 with stopped spindle to fail"); + + const char missing_g87_ijk_program[] = + "G21 G90 G17\n" + "S800 M3\n" + "G87 X1 Z-1 R1 I0.1 J0.1 F80\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, missing_g87_ijk_program, sizeof(missing_g87_ijk_program) - 1) != 0, + "expected G87 without I/J/K to fail"); + + const char reverse_g84_program[] = + "G21 G90 G17\n" + "S600 M4\n" + "G84 X1 Z-1 R1 F60\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, reverse_g84_program, sizeof(reverse_g84_program) - 1) != 0, + "expected G84 with non-clockwise spindle to fail"); + + const char forward_g74_program[] = + "G21 G90 G17\n" + "S500 M3\n" + "G74 X1 Z-1 R1 F50\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, forward_g74_program, sizeof(forward_g74_program) - 1) != 0, + "expected G74 with non-counterclockwise spindle to fail"); + const char missing_g89_p_program[] = "G21 G90 G17\n" "G89 X1 Z-1 R1\n"; @@ -749,12 +2677,12 @@ int main() { ok &= expect(cnc_sim_parse_program(sim, rotary_cycle_program, sizeof(rotary_cycle_program) - 1) != 0, "expected rotary axis word in canned cycle to fail"); - const char non_xy_cycle_program[] = + const char g18_missing_depth_cycle_program[] = "G21 G90 G18\n" "G81 X1 Z-1 R1\n"; events.clear(); - ok &= expect(cnc_sim_parse_program(sim, non_xy_cycle_program, sizeof(non_xy_cycle_program) - 1) != 0, - "expected smoke canned cycle outside G17 to fail"); + ok &= expect(cnc_sim_parse_program(sim, g18_missing_depth_cycle_program, sizeof(g18_missing_depth_cycle_program) - 1) != 0, + "expected G18 canned cycle to require Y depth word"); const char uvw_cycle_program[] = "G21 G90 G17\n" @@ -770,6 +2698,13 @@ int main() { ok &= expect(cnc_sim_parse_program(sim, invalid_cycle_r_program, sizeof(invalid_cycle_r_program) - 1) != 0, "expected canned cycle R below Z to fail"); + const char incremental_g53_program[] = + "G21 G91\n" + "G53 G0 X0\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, incremental_g53_program, sizeof(incremental_g53_program) - 1) != 0, + "expected G53 in incremental distance mode to fail"); + const char subprogram_program[] = "G21 G90\n" "G0 X0 Y0 Z0\n" @@ -805,6 +2740,18 @@ int main() { ok &= expect(saw_sub_second_call, "expected smoke M98 repeated subprogram call"); ok &= expect(saw_sub_return_motion, "expected smoke M99 return to caller"); + const char noninteger_m98_p_program[] = + "G21 G90\n" + "M98 P100.5\n" + "M30\n" + "O101\n" + "M99\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + noninteger_m98_p_program, + sizeof(noninteger_m98_p_program) - 1) != 0, + "expected LinuxCNC to reject non-integer M98 P word"); + const char missing_subprogram[] = "G21\n" "M98 P999\n"; @@ -912,6 +2859,90 @@ int main() { ok &= expect(saw_oword_local_parameter, "expected O-word local parameter assignment"); ok &= expect(saw_global_parameter_after_oword, "expected global parameter value after O-word return"); + const char parallel_parameter_assignment_program[] = + "G21 G90\n" + "F100\n" + "#3 = 15\n" + "#3 = 6 G1 X#3\n" + "G1 Y#3\n" + "#4 = 2\n" + "#3 = 8 #4 = #3 G1 Z#3 A#4\n" + "G1 B#3 C#4\n" + "G1 U#3 #3 = 11 #4 = #3\n" + "G1 V#3 W#4\n" + "#3 = 13 #3 = 14 G1 X#3\n" + "G1 Y#3\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + parallel_parameter_assignment_program, + sizeof(parallel_parameter_assignment_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_parallel_assignment_old_value_motion = saw_parallel_assignment_old_value_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + event.end.x == 15.0); + saw_parallel_assignment_new_value_motion = saw_parallel_assignment_new_value_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 5 && + event.end.y == 6.0); + saw_parallel_multi_assignment_old_values_motion = saw_parallel_multi_assignment_old_values_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 7 && + event.end.z == 6.0 && + event.end.a == 2.0); + saw_parallel_multi_assignment_new_values_motion = saw_parallel_multi_assignment_new_values_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 8 && + event.end.b == 8.0 && + event.end.c == 6.0); + saw_embedded_assignment_old_values_motion = saw_embedded_assignment_old_values_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 9 && + event.end.u == 8.0); + saw_embedded_assignment_new_values_motion = saw_embedded_assignment_new_values_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 10 && + event.end.v == 11.0 && + event.end.w == 8.0); + saw_repeated_assignment_old_value_motion = saw_repeated_assignment_old_value_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 11 && + event.end.x == 11.0); + saw_repeated_assignment_last_value_motion = saw_repeated_assignment_last_value_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 12 && + event.end.y == 14.0); + } + ok &= expect(saw_parallel_assignment_old_value_motion, + "expected line-local parameter assignment to leave same-line reads on old value"); + ok &= expect(saw_parallel_assignment_new_value_motion, + "expected line-local parameter assignment to commit for the next line"); + ok &= expect(saw_parallel_multi_assignment_old_values_motion, + "expected multiple same-line parameter assignments to leave same-line reads on old values"); + ok &= expect(saw_parallel_multi_assignment_new_values_motion, + "expected multiple same-line parameter assignments to commit together for the next line"); + ok &= expect(saw_embedded_assignment_old_values_motion, + "expected embedded parameter assignments to leave same-line reads on old values"); + ok &= expect(saw_embedded_assignment_new_values_motion, + "expected embedded parameter assignments to commit together for the next line"); + ok &= expect(saw_repeated_assignment_old_value_motion, + "expected repeated same-line parameter assignment to leave same-line reads on old value"); + ok &= expect(saw_repeated_assignment_last_value_motion, + "expected repeated same-line parameter assignment to commit only the last value"); + + const char word_embedded_parameter_assignment_program[] = + "G21 G90\n" + "F100\n" + "#3 = 15\n" + "G1 X#3=6\n" + "G1 Y#3\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + word_embedded_parameter_assignment_program, + sizeof(word_embedded_parameter_assignment_program) - 1) != 0, + "expected LinuxCNC to reject parameter assignment embedded inside a word value"); + const char global_numbered_parameter_program[] = "#100 = 40\n" "#1 = 5\n" @@ -1037,6 +3068,171 @@ int main() { ok &= expect(saw_named_parameter_local, "expected local named parameter assignment in O-word call"); ok &= expect(saw_named_parameter_global_after_call, "expected global named parameter after O-word return"); + const char normalized_named_parameter_program[] = + "#

= 12\n" + "G21 G90\n" + "F100\n" + "G1 X#\n" + "O10 if [EXISTS[#

]]\n" + "G1 Y1\n" + "O10 endif\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + normalized_named_parameter_program, + sizeof(normalized_named_parameter_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_named_parameter_normalized_reference = saw_named_parameter_normalized_reference || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + event.end.x == 12.0); + saw_named_parameter_normalized_exists = saw_named_parameter_normalized_exists || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 6 && + event.end.y == 1.0); + } + ok &= expect(saw_named_parameter_normalized_reference, + "expected named parameter names to ignore case and whitespace"); + ok &= expect(saw_named_parameter_normalized_exists, + "expected EXISTS to use normalized named parameter names"); + + const char *ini_path = "/tmp/cnc_sim_smoke_named_ini.ini"; + { + std::ofstream ini(ini_path); + ini << "[SETUP]\nXPOS = 3.25\n"; + } + { + ScopedEnv ini_env("INI_FILE_NAME", ini_path); + const char ini_named_parameter_program[] = + "G21 G90\n" + "F100\n" + "G1 X#<_ini[setup]xpos>\n" + "O10 if [EXISTS[#<_ini[SETUP]XPOS>]]\n" + "G1 Y1\n" + "O10 endif\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + ini_named_parameter_program, + sizeof(ini_named_parameter_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_ini_named_parameter_motion = saw_ini_named_parameter_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 3 && + event.end.x == 3.25); + saw_ini_named_parameter_exists_motion = saw_ini_named_parameter_exists_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 5 && + event.end.y == 1.0); + } + } + ok &= expect(saw_ini_named_parameter_motion, "expected #<_ini[section]key> named parameter lookup"); + ok &= expect(saw_ini_named_parameter_exists_motion, "expected EXISTS for #<_ini[section]key>"); + + const char *hal_path = "/tmp/cnc_sim_smoke_hal_pins.txt"; + { + std::ofstream hal(hal_path); + hal << "motion-controller.time = 42\n"; + } + { + ScopedEnv hal_env("HAL_PIN_FILE", hal_path); + const char hal_named_parameter_program[] = + "G21 G90\n" + "F100\n" + "G1 X#<_hal[motion-controller.time]>\n" + "O10 if [EXISTS[#<_hal[motion-controller.time]>]]\n" + "G1 Y#<_task>\n" + "O10 endif\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + hal_named_parameter_program, + sizeof(hal_named_parameter_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_hal_named_parameter_motion = saw_hal_named_parameter_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 3 && + event.end.x == 42.0); + saw_hal_named_parameter_exists_motion = saw_hal_named_parameter_exists_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 5 && + event.end.y == 1.0); + } + } + ok &= expect(saw_hal_named_parameter_motion, "expected #<_hal[HAL item]> named parameter lookup"); + ok &= expect(saw_hal_named_parameter_exists_motion, "expected EXISTS for #<_hal[HAL item]>"); + + const char override_named_parameter_program[] = + "G21 G90\n" + "M7\n" + "M8\n" + "M48\n" + "M52 P1\n" + "M53 P1\n" + "F100\n" + "O10 if [#<_mist> EQ 1 AND #<_flood> EQ 1 AND #<_feed_override> EQ 1 AND #<_adaptive_feed> EQ 1 AND #<_feed_hold> EQ 1]\n" + "G1 X1\n" + "O10 endif\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + override_named_parameter_program, + sizeof(override_named_parameter_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_override_named_parameters = saw_override_named_parameters || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 9 && + event.end.x == 1.0); + } + ok &= expect(saw_override_named_parameters, "expected LinuxCNC override named parameters"); + + const char oword_line_named_parameter_program[] = + "G21 G90\n" + "F100\n" + "O10 if [#<_line> EQ 3]\n" + "G1 X3\n" + "O10 endif\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + oword_line_named_parameter_program, + sizeof(oword_line_named_parameter_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_oword_line_named_parameter = saw_oword_line_named_parameter || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + event.end.x == 3.0); + } + ok &= expect(saw_oword_line_named_parameter, + "expected #<_line> to hold the current line in O-word conditions"); + + const char oword_return_value_program[] = + "O sub\n" + "O return [12]\n" + "O endsub\n" + "G21 G90\n" + "F100\n" + "O call\n" + "G1 X#<_value>\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + oword_return_value_program, + sizeof(oword_return_value_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_oword_return_value = saw_oword_return_value || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 7 && + event.end.x == 12.0); + saw_oword_returned_flag = saw_oword_returned_flag || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 7 && + event.end.x == 12.0); + } + ok &= expect(saw_oword_return_value, "expected #<_value> after O-word return value"); + ok &= expect(saw_oword_returned_flag, "expected #<_value_returned> after O-word return value"); + const char nested_bracket_program[] = "#1 = 4\n" "#2 = [[#1 + 2] * [#1 - 1]]\n" @@ -1060,6 +3256,294 @@ int main() { ok &= expect(saw_nested_bracket_assignment, "expected nested bracket assignment expression"); ok &= expect(saw_nested_bracket_motion, "expected nested bracket motion expression"); + const char parenthesized_expression_program[] = + "G21 G90\n" + "F100\n" + "#1 = [(2 + 3) * 4]\n" + "G1 X#1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + parenthesized_expression_program, + sizeof(parenthesized_expression_program) - 1) != 0, + "expected LinuxCNC expression grouping to reject parentheses"); + + const char malformed_number_program[] = + "G21 G90\n" + "F100\n" + "G1 X1.2.3\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + malformed_number_program, + sizeof(malformed_number_program) - 1) != 0, + "expected LinuxCNC bad number format to reject malformed real values"); + + const char unused_p_word_program[] = + "G21 G90\n" + "F100\n" + "G1 X1 P2\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + unused_p_word_program, + sizeof(unused_p_word_program) - 1) != 0, + "expected LinuxCNC to reject P word with no code to use it"); + + const char invalid_arc_p_word_program[] = + "G21 G90 G17\n" + "F100\n" + "G2 X1 Y0 R1 P0\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + invalid_arc_p_word_program, + sizeof(invalid_arc_p_word_program) - 1) != 0, + "expected LinuxCNC to reject P value less than 1 with G2/G3"); + + const char invalid_m19_p_word_program[] = + "G21 G90\n" + "M19 R45 P3\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + invalid_m19_p_word_program, + sizeof(invalid_m19_p_word_program) - 1) != 0, + "expected LinuxCNC to reject M19 P values outside 0, 1, or 2"); + + const char unused_q_word_program[] = + "G21 G90\n" + "F100\n" + "G1 X1 Q2\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + unused_q_word_program, + sizeof(unused_q_word_program) - 1) != 0, + "expected LinuxCNC to reject Q word with no code to use it"); + + const char unused_r_word_program[] = + "G21 G90\n" + "F100\n" + "G1 X1 R2\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + unused_r_word_program, + sizeof(unused_r_word_program) - 1) != 0, + "expected LinuxCNC to reject R word with no code to use it"); + + const char unused_l_word_program[] = + "G21 G90\n" + "F100\n" + "G1 X1 L2\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + unused_l_word_program, + sizeof(unused_l_word_program) - 1) != 0, + "expected LinuxCNC to reject L word with no code to use it"); + + const char unused_i_word_program[] = + "G21 G90\n" + "F100\n" + "G1 X1 I2\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + unused_i_word_program, + sizeof(unused_i_word_program) - 1) != 0, + "expected LinuxCNC to reject I word with no code to use it"); + + const char unused_j_word_program[] = + "G21 G90\n" + "F100\n" + "G1 X1 J2\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + unused_j_word_program, + sizeof(unused_j_word_program) - 1) != 0, + "expected LinuxCNC to reject J word with no code to use it"); + + const char unused_k_word_program[] = + "G21 G90\n" + "F100\n" + "G1 X1 K2\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + unused_k_word_program, + sizeof(unused_k_word_program) - 1) != 0, + "expected LinuxCNC to reject K word with no code to use it"); + + const char invalid_m19_r_word_program[] = + "G21 G90\n" + "M19 R400\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + invalid_m19_r_word_program, + sizeof(invalid_m19_r_word_program) - 1) != 0, + "expected LinuxCNC to reject M19 R values outside 0..360"); + + const char too_many_m_codes_program[] = + "G21 G90\n" + "M3 M7 M8 M48 M50\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + too_many_m_codes_program, + sizeof(too_many_m_codes_program) - 1) != 0, + "expected LinuxCNC to reject more than four M codes on one line"); + + const char missing_g96_s_word_program[] = + "G21 G90\n" + "G96\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + missing_g96_s_word_program, + sizeof(missing_g96_s_word_program) - 1) != 0, + "expected LinuxCNC to reject G96 without an S word"); + + const char invalid_m19_q_word_program[] = + "G21 G90\n" + "M19 R45 Q0\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + invalid_m19_q_word_program, + sizeof(invalid_m19_q_word_program) - 1) != 0, + "expected LinuxCNC to reject M19 Q values less than or equal to zero"); + + const char invalid_m67_e_word_program[] = + "G21 G90\n" + "M67 E-1 Q1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + invalid_m67_e_word_program, + sizeof(invalid_m67_e_word_program) - 1) != 0, + "expected LinuxCNC to reject negative E analog index with M67"); + + const char invalid_m66_p_word_program[] = + "G21 G90\n" + "M66 P-1 L0\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + invalid_m66_p_word_program, + sizeof(invalid_m66_p_word_program) - 1) != 0, + "expected LinuxCNC to reject negative P digital input index with M66"); + + const char invalid_m62_p_word_program[] = + "G21 G90\n" + "M62 P-1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + invalid_m62_p_word_program, + sizeof(invalid_m62_p_word_program) - 1) != 0, + "expected LinuxCNC to reject negative P output index with M62"); + + const char invalid_g10_l20_r_word_program[] = + "G21 G90\n" + "G10 L20 P1 X1 R30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + invalid_g10_l20_r_word_program, + sizeof(invalid_g10_l20_r_word_program) - 1) != 0, + "expected LinuxCNC to reject R word with G10 L20"); + + const char invalid_g10_l2_i_word_program[] = + "G21 G90\n" + "G10 L2 P1 X1 I2\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + invalid_g10_l2_i_word_program, + sizeof(invalid_g10_l2_i_word_program) - 1) != 0, + "expected LinuxCNC to reject I word with G10 L2"); + + const char probe_feed_per_rev_program[] = + "G21 G90\n" + "F100\n" + "G95\n" + "G38.2 X1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + probe_feed_per_rev_program, + sizeof(probe_feed_per_rev_program) - 1) != 0, + "expected LinuxCNC to reject probing in feed-per-rev mode"); + + const char zero_length_probe_program[] = + "G21 G90\n" + "F100\n" + "G38.2 X0\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + zero_length_probe_program, + sizeof(zero_length_probe_program) - 1) != 0, + "expected LinuxCNC to reject zero-length G38.2 probe move"); + + const char missing_g4_p_word_program[] = + "G21 G90\n" + "G4\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + missing_g4_p_word_program, + sizeof(missing_g4_p_word_program) - 1) != 0, + "expected LinuxCNC to reject G4 without a P dwell word"); + + const char g4_with_arc_program[] = + "G21 G90 G17\n" + "F100\n" + "G4 P1 G2 X1 Y0 R1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g4_with_arc_program, + sizeof(g4_with_arc_program) - 1) != 0, + "expected LinuxCNC to reject G4 with G2/G3 on the same line"); + + const char noninteger_g10_p_word_program[] = + "G21 G90\n" + "G10 L2 P1.5 X1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + noninteger_g10_p_word_program, + sizeof(noninteger_g10_p_word_program) - 1) != 0, + "expected LinuxCNC to reject non-integer P word with G10"); + + const char near_noninteger_g10_p_word_program[] = + "G21 G90\n" + "G10 L2 P1.0005 X1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + near_noninteger_g10_p_word_program, + sizeof(near_noninteger_g10_p_word_program) - 1) != 0, + "expected LinuxCNC G10 P integer tolerance to reject P1.0005"); + + const char out_of_range_g10_p_word_program[] = + "G21 G90\n" + "G10 L2 P10 X1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + out_of_range_g10_p_word_program, + sizeof(out_of_range_g10_p_word_program) - 1) != 0, + "expected LinuxCNC to reject G10 L2 P values outside 0..9"); + + const char g1_without_feed_program[] = + "G21 G90\n" + "G1 X1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g1_without_feed_program, + sizeof(g1_without_feed_program) - 1) != 0, + "expected LinuxCNC to reject G1 with zero feed rate"); + + const char scientific_notation_word_program[] = + "G21 G90\n" + "F100\n" + "G1 X1e3\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + scientific_notation_word_program, + sizeof(scientific_notation_word_program) - 1) != 0, + "expected LinuxCNC real number syntax to reject exponent notation in word values"); + + const char scientific_notation_expression_program[] = + "G21 G90\n" + "F100\n" + "#1 = [1e3]\n" + "G1 X#1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + scientific_notation_expression_program, + sizeof(scientific_notation_expression_program) - 1) != 0, + "expected LinuxCNC expression syntax to reject exponent notation"); + const char math_function_program[] = "#1 = [ABS[-3] + SQRT[16]]\n" "# = [COS[0] + SIN[0] + TAN[0]]\n" @@ -1083,6 +3567,29 @@ int main() { ok &= expect(saw_math_function_assignment, "expected ABS/SQRT expression function support"); ok &= expect(saw_trig_function_motion, "expected SIN/COS/TAN expression function support"); + const char word_unary_function_program[] = + "G21 G90\n" + "F100\n" + "G1 XABS[-2]\n" + "G1 YATAN[1]/[1]\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + word_unary_function_program, + sizeof(word_unary_function_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_word_unary_function_motion = saw_word_unary_function_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 3 && + event.end.x == 2.0); + saw_word_atan_function_motion = saw_word_atan_function_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + near(event.end.y, 45.0)); + } + ok &= expect(saw_word_unary_function_motion, "expected LinuxCNC unary function as a word value"); + ok &= expect(saw_word_atan_function_motion, "expected LinuxCNC ATAN function as a word value"); + const char additional_math_function_program[] = "#1 = [ASIN[0.5] + ACOS[0.5]]\n" "#2 = [FIX[1.9] + FUP[1.1] + ROUND[-1.5]]\n" @@ -1120,6 +3627,17 @@ int main() { ok &= expect(saw_atan_function_motion, "expected ATAN expression function support"); ok &= expect(saw_exp_ln_function_motion, "expected EXP/LN expression function support"); + const char missing_function_brackets_program[] = + "G21 G90\n" + "F100\n" + "#1 = SIN30\n" + "G1 X#1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + missing_function_brackets_program, + sizeof(missing_function_brackets_program) - 1) != 0, + "expected LinuxCNC function syntax to require bracketed arguments"); + const char expression_operator_program[] = "#1 = [2 ** 3]\n" "#2 = [2 + 3 ** 2 * 4]\n" @@ -1131,6 +3649,7 @@ int main() { "#8 = [1 AND 0 OR 1]\n" "#9 = [1 OR 0 AND 0]\n" "#10 = [1.0000001 EQ 1]\n" + "#11 = [-2 ** 2]\n" "G21 G90\n" "F100\n" "G1 X#1\n" @@ -1142,54 +3661,60 @@ int main() { "G1 U#7\n" "G1 V#8\n" "G1 W#9\n" - "G1 X#10\n"; + "G1 X#10\n" + "G1 Y#11\n"; events.clear(); ok &= expect(cnc_sim_parse_program(sim, expression_operator_program, sizeof(expression_operator_program) - 1) == 0, cnc_sim_last_error(sim)); for (const auto &event : events) { saw_power_expression_motion = saw_power_expression_motion || (event.type == CNC_SIM_EVENT_LINEAR_FEED && - event.line == 13 && + event.line == 14 && event.end.x == 8.0); saw_power_precedence_motion = saw_power_precedence_motion || (event.type == CNC_SIM_EVENT_LINEAR_FEED && - event.line == 14 && + event.line == 15 && event.end.y == 38.0); saw_mod_expression_motion = saw_mod_expression_motion || (event.type == CNC_SIM_EVENT_LINEAR_FEED && - event.line == 15 && + event.line == 16 && event.end.z == 2.0); saw_negative_mod_motion = saw_negative_mod_motion || (event.type == CNC_SIM_EVENT_LINEAR_FEED && - event.line == 16 && + event.line == 17 && event.end.a == 2.0); saw_mod_left_associative_motion = saw_mod_left_associative_motion || (event.type == CNC_SIM_EVENT_LINEAR_FEED && - event.line == 17 && + event.line == 18 && event.end.b == 1.0); saw_comparison_expression_motion = saw_comparison_expression_motion || (event.type == CNC_SIM_EVENT_LINEAR_FEED && - event.line == 18 && + event.line == 19 && event.end.c == 1.0); saw_comparison_precedence_motion = saw_comparison_precedence_motion || (event.type == CNC_SIM_EVENT_LINEAR_FEED && - event.line == 19 && + event.line == 20 && event.end.u == 1.0); saw_logical_expression_motion = saw_logical_expression_motion || (event.type == CNC_SIM_EVENT_LINEAR_FEED && - event.line == 20 && + event.line == 21 && event.end.v == 1.0); saw_logical_precedence_motion = saw_logical_precedence_motion || (event.type == CNC_SIM_EVENT_LINEAR_FEED && - event.line == 21 && + event.line == 22 && event.end.w == 0.0); saw_equality_tolerance_motion = saw_equality_tolerance_motion || (event.type == CNC_SIM_EVENT_LINEAR_FEED && - event.line == 22 && + event.line == 23 && event.end.x == 1.0); + saw_signed_power_motion = saw_signed_power_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 24 && + event.end.y == 4.0); } ok &= expect(saw_power_expression_motion, "expected ** expression operator support"); ok &= expect(saw_power_precedence_motion, "expected ** precedence above multiplicative operators"); + ok &= expect(saw_signed_power_motion, "expected LinuxCNC signed real power grouping"); ok &= expect(saw_mod_expression_motion, "expected MOD expression operator support"); ok &= expect(saw_negative_mod_motion, "expected MOD to return positive remainder for negative left operand"); ok &= expect(saw_mod_left_associative_motion, "expected MOD to share multiplicative left associativity"); @@ -1199,6 +3724,121 @@ int main() { ok &= expect(saw_logical_precedence_motion, "expected logical operators to share LinuxCNC left associativity"); ok &= expect(saw_equality_tolerance_motion, "expected EQ to use LinuxCNC equality tolerance"); + const char exists_named_parameter_program[] = + "# = 7\n" + "G21 G90\n" + "F100\n" + "O10 if [EXISTS[#]]\n" + "G1 X1\n" + "O10 endif\n" + "O20 if [EXISTS[#] EQ 0]\n" + "G1 Y1\n" + "O20 endif\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + exists_named_parameter_program, + sizeof(exists_named_parameter_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_exists_present_motion = saw_exists_present_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 5 && + event.end.x == 1.0); + saw_exists_missing_motion = saw_exists_missing_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 8 && + event.end.y == 1.0); + } + ok &= expect(saw_exists_present_motion, "expected EXISTS true for defined named parameter"); + ok &= expect(saw_exists_missing_motion, "expected EXISTS false for undefined named parameter"); + + const char exists_numbered_parameter_program[] = + "#1 = 2\n" + "#2 = 99999\n" + "G21 G90\n" + "F100\n" + "O10 if [EXISTS[#1] AND EXISTS[#99999] EQ 0]\n" + "G1 X1\n" + "O10 endif\n" + "O20 if [EXISTS[##1] AND EXISTS[##2] EQ 0]\n" + "G1 Y1\n" + "O20 endif\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + exists_numbered_parameter_program, + sizeof(exists_numbered_parameter_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_exists_numbered_motion = saw_exists_numbered_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 6 && + event.end.x == 1.0); + saw_exists_indirect_numbered_motion = saw_exists_indirect_numbered_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 9 && + event.end.y == 1.0); + } + ok &= expect(saw_exists_numbered_motion, "expected EXISTS for numbered parameter range"); + ok &= expect(saw_exists_indirect_numbered_motion, "expected EXISTS for indirect numbered parameters"); + + const char invalid_exists_expression_program[] = + "#1 = 1\n" + "#2 = 2\n" + "G21 G90\n" + "O10 if [EXISTS[#1+#2]]\n" + "G1 X1\n" + "O10 endif\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + invalid_exists_expression_program, + sizeof(invalid_exists_expression_program) - 1) != 0, + "expected LinuxCNC to reject arithmetic inside EXISTS parameter reference"); + + const char missing_named_parameter_program[] = + "G21 G90\n" + "F100\n" + "G1 X#\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + missing_named_parameter_program, + sizeof(missing_named_parameter_program) - 1) != 0, + "expected undefined named parameter read to fail"); + + const char indexed_parameter_program[] = + "#2 = 3\n" + "#3 = 11\n" + "#4 = [#[1 + 2] + ##2]\n" + "#[1 + 2] = 17\n" + "G21 G90\n" + "F100\n" + "G1 X#[1 + 2]\n" + "G1 Y#4\n" + "O10 if [##2 EQ 17]\n" + "G1 Z1\n" + "O10 endif\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + indexed_parameter_program, + sizeof(indexed_parameter_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_indexed_parameter_motion = saw_indexed_parameter_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 7 && + event.end.x == 17.0); + saw_indirect_parameter_motion = saw_indirect_parameter_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 8 && + event.end.y == 22.0); + saw_indexed_parameter_condition_motion = saw_indexed_parameter_condition_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 10 && + event.end.z == 1.0); + } + ok &= expect(saw_indexed_parameter_motion, "expected #[expr] parameter index assignment/read support"); + ok &= expect(saw_indirect_parameter_motion, "expected ##n indirect parameter support"); + ok &= expect(saw_indexed_parameter_condition_motion, "expected indirect parameter support in O-word condition"); + const char oword_conditional_program[] = "G21 G90\n" "F100\n" @@ -1293,13 +3933,13 @@ int main() { "#2 = 0\n" "O300 while [#1 LT 5]\n" "#1 = [#1 + 1]\n" - "O300 if [#1 LT 3]\n" + "O301 if [#1 LT 3]\n" "O300 continue\n" - "O300 endif\n" + "O301 endif\n" "#2 = [#2 + 1]\n" - "O300 if [#1 GE 4]\n" + "O302 if [#1 GE 4]\n" "O300 break\n" - "O300 endif\n" + "O302 endif\n" "G1 X#1\n" "O300 endwhile\n" "G1 Y#1\n" @@ -1349,13 +3989,13 @@ int main() { "#4 = 0\n" "O402 repeat [5]\n" "#3 = [#3 + 1]\n" - "O402 if [#3 LT 3]\n" + "O403 if [#3 LT 3]\n" "O402 continue\n" - "O402 endif\n" + "O403 endif\n" "#4 = [#4 + 1]\n" - "O402 if [#3 GE 4]\n" + "O404 if [#3 GE 4]\n" "O402 break\n" - "O402 endif\n" + "O404 endif\n" "G1 A#3\n" "O402 endrepeat\n" "G1 B#3\n" @@ -1416,20 +4056,26 @@ int main() { "G1 Y#2\n" "O501 while [0]\n" "#3 = 0\n" - "#4 = 0\n" "O502 do\n" "#3 = [#3 + 1]\n" - "O502 if [#3 LT 3]\n" - "O502 continue\n" - "O502 endif\n" - "#4 = [#4 + 1]\n" - "O502 if [#3 GE 4]\n" + "O503 if [#3 GE 2]\n" "O502 break\n" - "O502 endif\n" + "O503 endif\n" "G1 Z#3\n" - "O502 while [#3 LT 5]\n" + "O502 while [#3 LT 4]\n" "G1 A#3\n" - "G1 B#4\n"; + "#4 = 0\n" + "#5 = 0\n" + "O504 do\n" + "#4 = [#4 + 1]\n" + "O505 if [#4 EQ 1]\n" + "O504 continue\n" + "O505 endif\n" + "#5 = [#5 + 1]\n" + "G1 U#4\n" + "O504 while [#4 LT 3]\n" + "G1 V#4\n" + "G1 W#5\n"; events.clear(); ok &= expect(cnc_sim_parse_program(sim, oword_do_while_program, sizeof(oword_do_while_program) - 1) == 0, cnc_sim_last_error(sim)); @@ -1443,23 +4089,37 @@ int main() { (event.type == CNC_SIM_EVENT_LINEAR_FEED && event.line == 11 && event.end.y == 1.0); - saw_do_continue_break_final_counter_motion = saw_do_continue_break_final_counter_motion || - (event.type == CNC_SIM_EVENT_LINEAR_FEED && - event.line == 26 && - event.end.a == 4.0); - saw_do_continue_break_body_counter_motion = saw_do_continue_break_body_counter_motion || - (event.type == CNC_SIM_EVENT_LINEAR_FEED && - event.line == 27 && - event.end.b == 2.0); + saw_do_break_body_motion = saw_do_break_body_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 19 && + event.end.z == 1.0); + saw_do_break_final_counter_motion = saw_do_break_final_counter_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 21 && + event.end.a == 2.0); + saw_do_continue_final_counter_motion = saw_do_continue_final_counter_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 32 && + event.end.v == 3.0); + saw_do_continue_body_counter_motion = saw_do_continue_body_counter_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 33 && + event.end.w == 2.0); ok &= expect(!(event.type == CNC_SIM_EVENT_LINEAR_FEED && - event.line == 24 && - event.end.z == 4.0), + event.line == 30 && + event.end.u == 1.0), + "expected O-word do continue to skip remaining loop body motion"); + ok &= expect(!(event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 19 && + event.end.z == 2.0), "expected O-word do break to skip remaining loop body motion"); } ok &= expect(do_while_motion_count == 3, "expected O-word do while body to execute three times"); ok &= expect(saw_do_while_false_once_motion, "expected O-word do while false condition to execute body once"); - ok &= expect(saw_do_continue_break_final_counter_motion, "expected O-word do break to preserve loop counter"); - ok &= expect(saw_do_continue_break_body_counter_motion, "expected O-word do continue to skip body counter updates"); + ok &= expect(saw_do_break_body_motion, "expected O-word do break setup body motion"); + ok &= expect(saw_do_break_final_counter_motion, "expected O-word do break to preserve loop counter"); + ok &= expect(saw_do_continue_final_counter_motion, "expected O-word do continue final counter motion"); + ok &= expect(saw_do_continue_body_counter_motion, "expected O-word do continue to skip body counter updates"); const char mixed_subprogram_styles[] = "G21\n" diff --git a/core/tests/linuxcnc_canon_bridge_smoke.cpp b/core/tests/linuxcnc_canon_bridge_smoke.cpp index 4023644..4464a1b 100644 --- a/core/tests/linuxcnc_canon_bridge_smoke.cpp +++ b/core/tests/linuxcnc_canon_bridge_smoke.cpp @@ -33,11 +33,61 @@ int main() { INIT_CANON(); USE_LENGTH_UNITS(CANON_UNITS_MM); SELECT_PLANE(CANON_PLANE::XY); + SET_TRAVERSE_RATE(9000.0); + const bool external_traverse_rate = GET_EXTERNAL_TRAVERSE_RATE() == 9000.0; SET_FEED_RATE(500.0); + SET_FEED_MODE(2, 1); SET_SPINDLE_SPEED(0, 1200.0); START_SPINDLE_CLOCKWISE(0, 0); START_SPINDLE_COUNTERCLOCKWISE(0, 0); STOP_SPINDLE_TURNING(0); + SET_SPINDLE_MODE(0, 2500.0); + cnc_sim_linuxcnc_set_current_line(27); + ORIENT_SPINDLE(0, 45.0, 1); + WAIT_SPINDLE_ORIENT_COMPLETE(0, 2.5); + cnc_sim_linuxcnc_set_current_line(28); + SET_MOTION_OUTPUT_BIT(1); + CLEAR_MOTION_OUTPUT_BIT(1); + SET_AUX_OUTPUT_BIT(2); + CLEAR_AUX_OUTPUT_BIT(2); + WAIT(0, DIGITAL_INPUT, WAIT_MODE_HIGH, 5.0); + WAIT(1, ANALOG_INPUT, WAIT_MODE_IMMEDIATE, 0.0); + SET_MOTION_OUTPUT_VALUE(0, 1.25); + SET_AUX_OUTPUT_VALUE(1, 2.5); + cnc_sim_linuxcnc_set_current_line(29); + ENABLE_FEED_OVERRIDE(); + const bool external_feed_override_on = GET_EXTERNAL_FEED_OVERRIDE_ENABLE() == 1; + DISABLE_FEED_OVERRIDE(); + const bool external_feed_override_off = GET_EXTERNAL_FEED_OVERRIDE_ENABLE() == 0; + ENABLE_SPEED_OVERRIDE(0); + const bool external_speed_override_on = GET_EXTERNAL_SPINDLE_OVERRIDE_ENABLE(0) == 1; + DISABLE_SPEED_OVERRIDE(0); + const bool external_speed_override_off = GET_EXTERNAL_SPINDLE_OVERRIDE_ENABLE(0) == 0; + ENABLE_ADAPTIVE_FEED(); + const bool external_adaptive_feed_on = GET_EXTERNAL_ADAPTIVE_FEED_ENABLE() == 1; + DISABLE_ADAPTIVE_FEED(); + const bool external_adaptive_feed_off = GET_EXTERNAL_ADAPTIVE_FEED_ENABLE() == 0; + ENABLE_FEED_HOLD(); + const bool external_feed_hold_on = GET_EXTERNAL_FEED_HOLD_ENABLE() == 1; + DISABLE_FEED_HOLD(); + const bool external_feed_hold_off = GET_EXTERNAL_FEED_HOLD_ENABLE() == 0; + cnc_sim_linuxcnc_set_current_line(30); + SET_MOTION_CONTROL_MODE(CANON_CONTINUOUS, 0.125); + SET_NAIVECAM_TOLERANCE(0.025); + const bool external_motion_mode = GET_EXTERNAL_MOTION_CONTROL_MODE() == CANON_CONTINUOUS; + const bool external_motion_tolerance = GET_EXTERNAL_MOTION_CONTROL_TOLERANCE() == 0.125; + const bool external_naivecam_tolerance = GET_EXTERNAL_MOTION_CONTROL_NAIVECAM_TOLERANCE() == 0.025; + SET_BLOCK_DELETE(true); + const bool external_block_delete_on = GET_BLOCK_DELETE(); + SET_BLOCK_DELETE(false); + const bool external_block_delete_off = !GET_BLOCK_DELETE(); + SET_OPTIONAL_PROGRAM_STOP(true); + const bool external_optional_stop_on = GET_OPTIONAL_PROGRAM_STOP(); + SET_OPTIONAL_PROGRAM_STOP(false); + const bool external_optional_stop_off = !GET_OPTIONAL_PROGRAM_STOP(); + cnc_sim_linuxcnc_set_current_line(0); + START_SPEED_FEED_SYNCH(0, 1.25, false); + STOP_SPEED_FEED_SYNCH(); SELECT_TOOL(7); CHANGE_TOOL(); cnc_sim_linuxcnc_set_current_line(8); @@ -45,7 +95,10 @@ int main() { 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); - STRAIGHT_PROBE(13, 10.0, 10.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0); + STRAIGHT_PROBE(13, 10.0, 10.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 3); + STRAIGHT_PROBE(14, 10.0, 10.0, -2.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1); + const bool external_probe_position = GET_EXTERNAL_PROBE_POSITION_Z() == -2.0; + const bool external_probe_tripped = GET_EXTERNAL_PROBE_TRIPPED_VALUE() == 1; 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); @@ -53,6 +106,7 @@ int main() { tool_offset.tran.z = 12.5; cnc_sim_linuxcnc_set_current_line(19); USE_TOOL_LENGTH_OFFSET(tool_offset); + const bool external_tool_length_z = GET_EXTERNAL_TOOL_LENGTH_ZOFFSET() == 12.5; cnc_sim_linuxcnc_set_current_line(20); COMMENT("bridge comment"); cnc_sim_linuxcnc_set_current_line(24); @@ -74,6 +128,7 @@ int main() { 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"); + ok &= expect(external_traverse_rate, "expected external traverse rate query to reflect SET_TRAVERSE_RATE"); bool saw_tool = false; bool saw_tool_number = false; @@ -82,6 +137,28 @@ int main() { bool saw_spindle_cw = false; bool saw_spindle_ccw = false; bool saw_spindle_stop = false; + bool saw_spindle_mode = false; + bool saw_feed_mode = false; + bool saw_spindle_orient = false; + bool saw_spindle_orient_wait = false; + bool saw_motion_digital_on = false; + bool saw_motion_digital_off = false; + bool saw_aux_digital_on = false; + bool saw_aux_digital_off = false; + bool saw_wait_digital = false; + bool saw_wait_analog = false; + bool saw_motion_analog = false; + bool saw_aux_analog = false; + bool saw_feed_override_on = false; + bool saw_feed_override_off = false; + bool saw_speed_override_on = false; + bool saw_speed_override_off = false; + bool saw_adaptive_feed_on = false; + bool saw_adaptive_feed_off = false; + bool saw_feed_hold_on = false; + bool saw_feed_hold_off = false; + bool saw_speed_feed_sync_start = false; + bool saw_speed_feed_sync_stop = false; bool saw_arc = false; bool saw_probe = false; bool saw_g5x = false; @@ -111,6 +188,110 @@ int main() { saw_spindle_stop = saw_spindle_stop || (event.type == CNC_SIM_EVENT_SET_SPINDLE && event.spindle == 0.0 && event.reserved == 0); + saw_spindle_mode = saw_spindle_mode || (event.type == CNC_SIM_EVENT_COMMENT && + event.reserved == 960 && + event.feed == 2500.0 && + event.tool == 0); + saw_feed_mode = saw_feed_mode || (event.type == CNC_SIM_EVENT_SET_FEED && + event.reserved == 1 && + event.tool == 2); + saw_spindle_orient = saw_spindle_orient || (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 27 && + event.reserved == 1901 && + event.tool == 0 && + event.feed == 45.0 && + event.arc_turns == 1); + saw_spindle_orient_wait = saw_spindle_orient_wait || (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 27 && + event.reserved == 1902 && + event.tool == 0 && + event.dwell_seconds == 2.5); + saw_motion_digital_on = saw_motion_digital_on || (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 28 && + event.reserved == 62 && + event.tool == 1 && + event.feed == 1.0); + saw_motion_digital_off = saw_motion_digital_off || (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 28 && + event.reserved == 63 && + event.tool == 1 && + event.feed == 0.0); + saw_aux_digital_on = saw_aux_digital_on || (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 28 && + event.reserved == 64 && + event.tool == 2 && + event.feed == 1.0); + saw_aux_digital_off = saw_aux_digital_off || (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 28 && + event.reserved == 65 && + event.tool == 2 && + event.feed == 0.0); + saw_wait_digital = saw_wait_digital || (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 28 && + event.reserved == 66 && + event.tool == 0 && + event.arc_turns == DIGITAL_INPUT && + event.feed == WAIT_MODE_HIGH && + event.dwell_seconds == 5.0); + saw_wait_analog = saw_wait_analog || (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 28 && + event.reserved == 66 && + event.tool == 1 && + event.arc_turns == ANALOG_INPUT && + event.feed == WAIT_MODE_IMMEDIATE && + event.dwell_seconds == 0.0); + saw_motion_analog = saw_motion_analog || (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 28 && + event.reserved == 67 && + event.tool == 0 && + event.feed == 1.25); + saw_aux_analog = saw_aux_analog || (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 28 && + event.reserved == 68 && + event.tool == 1 && + event.feed == 2.5); + saw_feed_override_on = saw_feed_override_on || (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 29 && + event.reserved == 50 && + event.feed == 1.0); + saw_feed_override_off = saw_feed_override_off || (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 29 && + event.reserved == 50 && + event.feed == 0.0); + saw_speed_override_on = saw_speed_override_on || (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 29 && + event.reserved == 51 && + event.tool == 0 && + event.feed == 1.0); + saw_speed_override_off = saw_speed_override_off || (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 29 && + event.reserved == 51 && + event.tool == 0 && + event.feed == 0.0); + saw_adaptive_feed_on = saw_adaptive_feed_on || (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 29 && + event.reserved == 52 && + event.feed == 1.0); + saw_adaptive_feed_off = saw_adaptive_feed_off || (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 29 && + event.reserved == 52 && + event.feed == 0.0); + saw_feed_hold_on = saw_feed_hold_on || (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 29 && + event.reserved == 53 && + event.feed == 1.0); + saw_feed_hold_off = saw_feed_hold_off || (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 29 && + event.reserved == 53 && + event.feed == 0.0); + saw_speed_feed_sync_start = saw_speed_feed_sync_start || (event.type == CNC_SIM_EVENT_COMMENT && + event.reserved == 3301 && + event.feed == 1.25 && + event.tool == 0); + saw_speed_feed_sync_stop = saw_speed_feed_sync_stop || (event.type == CNC_SIM_EVENT_COMMENT && + event.reserved == 3300 && + event.feed == 0.0 && + event.tool == 0); 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 && @@ -119,6 +300,7 @@ int main() { event.center.y == 5.0); saw_probe = saw_probe || (event.type == CNC_SIM_EVENT_PROBE && event.line == 13 && + event.reserved == 3 && event.start.x == 10.0 && event.start.y == 10.0 && event.end.z == -1.0); @@ -171,14 +353,54 @@ int main() { ok &= expect(saw_spindle_cw, "expected clockwise spindle state"); ok &= expect(saw_spindle_ccw, "expected counterclockwise spindle state"); ok &= expect(saw_spindle_stop, "expected stopped spindle state"); + ok &= expect(saw_spindle_mode, "expected spindle mode state"); + ok &= expect(saw_feed_mode, "expected feed mode state to preserve spindle and mode arguments"); + ok &= expect(saw_spindle_orient, "expected spindle orient state"); + ok &= expect(saw_spindle_orient_wait, "expected spindle orient wait state"); + ok &= expect(saw_motion_digital_on, "expected motion digital output on state"); + ok &= expect(saw_motion_digital_off, "expected motion digital output off state"); + ok &= expect(saw_aux_digital_on, "expected auxiliary digital output on state"); + ok &= expect(saw_aux_digital_off, "expected auxiliary digital output off state"); + ok &= expect(saw_wait_digital, "expected digital input wait state"); + ok &= expect(saw_wait_analog, "expected analog input wait state"); + ok &= expect(saw_motion_analog, "expected motion analog output state"); + ok &= expect(saw_aux_analog, "expected auxiliary analog output state"); + ok &= expect(saw_feed_override_on, "expected feed override enable state"); + ok &= expect(saw_feed_override_off, "expected feed override disable state"); + ok &= expect(saw_speed_override_on, "expected speed override enable state"); + ok &= expect(saw_speed_override_off, "expected speed override disable state"); + ok &= expect(saw_adaptive_feed_on, "expected adaptive feed enable state"); + ok &= expect(saw_adaptive_feed_off, "expected adaptive feed disable state"); + ok &= expect(saw_feed_hold_on, "expected feed hold enable state"); + ok &= expect(saw_feed_hold_off, "expected feed hold disable state"); + ok &= expect(external_feed_override_on, "expected external feed override query to reflect enable"); + ok &= expect(external_feed_override_off, "expected external feed override query to reflect disable"); + ok &= expect(external_speed_override_on, "expected external speed override query to reflect enable"); + ok &= expect(external_speed_override_off, "expected external speed override query to reflect disable"); + ok &= expect(external_adaptive_feed_on, "expected external adaptive feed query to reflect enable"); + ok &= expect(external_adaptive_feed_off, "expected external adaptive feed query to reflect disable"); + ok &= expect(external_feed_hold_on, "expected external feed hold query to reflect enable"); + ok &= expect(external_feed_hold_off, "expected external feed hold query to reflect disable"); + ok &= expect(external_motion_mode, "expected external motion-control mode query to reflect SET_MOTION_CONTROL_MODE"); + ok &= expect(external_motion_tolerance, "expected external motion-control tolerance query to reflect SET_MOTION_CONTROL_MODE"); + ok &= expect(external_naivecam_tolerance, "expected external naivecam tolerance query to reflect SET_NAIVECAM_TOLERANCE"); + ok &= expect(external_block_delete_on, "expected block-delete query to reflect enable"); + ok &= expect(external_block_delete_off, "expected block-delete query to reflect disable"); + ok &= expect(external_optional_stop_on, "expected optional-stop query to reflect enable"); + ok &= expect(external_optional_stop_off, "expected optional-stop query to reflect disable"); + ok &= expect(saw_speed_feed_sync_start, "expected speed-feed sync start state"); + ok &= expect(saw_speed_feed_sync_stop, "expected speed-feed sync stop state"); ok &= expect(saw_rapid, "expected rapid move"); ok &= expect(saw_feed, "expected linear feed"); ok &= expect(saw_arc, "expected arc feed"); ok &= expect(saw_probe, "expected probe event"); + ok &= expect(external_probe_position, "expected external probe position query to reflect last STRAIGHT_PROBE"); + ok &= expect(external_probe_tripped, "expected external probe tripped query to reflect trip-seeking STRAIGHT_PROBE"); 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_tool_length, "expected tool length offset event"); + ok &= expect(external_tool_length_z, "expected external tool length Z query to reflect USE_TOOL_LENGTH_OFFSET"); ok &= expect(saw_comment, "expected comment event line"); ok &= expect(saw_mist_on, "expected mist on state"); ok &= expect(saw_flood_on, "expected flood on state"); diff --git a/docs/linuxcnc-porting.md b/docs/linuxcnc-porting.md index 5c02727..4106327 100644 --- a/docs/linuxcnc-porting.md +++ b/docs/linuxcnc-porting.md @@ -24,15 +24,20 @@ This parser only exists to test the ABI and UI before Emscripten and LinuxCNC ar - multiple G/M words on one line - `G0`, `G1`, `G2`, `G3` - `G17`, `G18`, `G19` +- `G90.1`, `G91.1` arc IJK distance mode - `G20`, `G21`, `G70`, `G71` +- `G7`, `G8` lathe diameter/radius mode for X-axis input - `G90`, `G91` - `G4 P...` +- `G38.2`, `G38.3`, `G38.4`, `G38.5` - `F`, `S`, `T`, `M3`, `M4`, `M5`, `M6`, `M2`, `M30` +- spindle modes `G96`, `G97` - IJK and R arcs - numbered and named parameter assignment/reference such as `#1 = ...`, `# = ...`, `X#1`, and nested expressions like `X[[# + 2] * 3]` - expression functions `ABS[]`, `SQRT[]`, `EXP[]`, `LN[]`, degree-based `SIN[]`/`COS[]`/`TAN[]`/`ASIN[]`/`ACOS[]`, LinuxCNC-style `ATAN[]/[]`, and `FIX[]`/`FUP[]`/`ROUND[]` - numeric and named O-word subprograms such as `O100 call` and `O call` -- canned cycles `G73`, `G81`, `G82`, `G83`, `G85`, `G86`, `G89` with `G80`, `G98`, `G99`, `L` +- canned cycles `G73`, `G74`, `G81`, `G82`, `G83`, `G84`, `G85`, `G86`, `G87`, `G88`, `G89` with `G80`, `G98`, `G99`, `L` +- coordinate offset events for `G10 L2`, `G10 L20`, `G54`-`G59.3`, `G52`, `G92`, `G92.1`, `G92.2`, and `G92.3` It is not the production interpreter. @@ -186,17 +191,27 @@ This matrix tracks LinuxCNC feature coverage for the web/WASM simulator. A featu | Area | Status | Regression | | --- | --- | --- | -| Basic modal motion `G0/G1/G2/G3` | covered | `tests/gcode/linuxcnc_basic_motion.ngc`, `tests/gcode/basic_mill.ngc` | +| Basic modal motion `G0/G1/G2/G3` | covered, including `G18`/`G19` arc plane mapping and `G90.1`/`G91.1` IJK modes | `tests/gcode/linuxcnc_basic_motion.ngc`, `tests/gcode/basic_mill.ngc`, `tests/gcode/linuxcnc_arc_planes.ngc`, `tests/gcode/linuxcnc_arc_distance_modes.ngc` | +| Predefined position moves `G28/G28.1`, `G30/G30.1` | covered for stored position, waypoint, selected-axis return, and all-axis return | `tests/gcode/linuxcnc_predefined_positions.ngc` | +| Machine-coordinate move `G53` | covered for explicit/modal `G0`, `G1`, and incremental-mode rejection in smoke parser | `tests/gcode/linuxcnc_machine_coordinates.ngc` | | Tool select/change `T... M6` | covered with minimal native tooldata | `tests/gcode/basic_mill.ngc` | -| Tool length offset `G43/G49` | covered with nonzero tool-table offset through LinuxCNC native/source backends | `tests/gcode/linuxcnc_tool_length.ngc` | +| Tool length offset `G43/G43.1/G43.2/G49` | covered with nonzero tool-table offset, dynamic replacement, additive axis offsets, `G43.2 H...`, and clear through LinuxCNC native/source backends | `tests/gcode/linuxcnc_tool_length.ngc`, `tests/gcode/linuxcnc_dynamic_tool_length.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` | | Spindle, coolant, program stops, and current tool number `M3/M4/M5`, `M7/M8/M9`, `M0/M1/M60/M30`, `M61 Q...` | covered through LinuxCNC native/source backends | `tests/gcode/linuxcnc_spindle_direction.ngc`, `tests/gcode/linuxcnc_coolant.ngc`, `tests/gcode/linuxcnc_program_stops.ngc`, `tests/gcode/linuxcnc_tool_number.ngc` | +| Spindle speed mode `G96/G97` | covered with Canon `SET_SPINDLE_MODE` preserved as mode state events, including `G96 D...` max-RPM value | `tests/gcode/linuxcnc_spindle_modes.ngc` | +| Spindle orientation `M19 R... P... Q...` | covered with Canon `ORIENT_SPINDLE` and `WAIT_SPINDLE_ORIENT_COMPLETE` preserved as temporary mode state events | `tests/gcode/linuxcnc_spindle_orient.ngc` | +| Spindle-synchronized feed and threading `G33/G33.1/G76` | covered through smoke, LinuxCNC native, and source-linked paths with Canon `START_SPEED_FEED_SYNCH`/`STOP_SPEED_FEED_SYNCH` preserved, rigid-tap line numbers normalized, and G76 multi-pass threading smoke-covered | `tests/gcode/linuxcnc_threading_sync.ngc`, `tests/gcode/linuxcnc_threading_cycle.ngc`, smoke API regression | +| Lathe diameter/radius mode `G7/G8` | covered in smoke parser for X-axis diameter/radius scaling, including interaction with G76 threading geometry; native/source backends use LinuxCNC interpreter state directly | smoke API regression | +| Digital/analog I/O `M62`-`M68` | covered with Canon digital output, analog output, and input wait callbacks preserved as temporary state events | `tests/gcode/linuxcnc_io_controls.ngc` | +| Override controls `M48`-`M53` | covered with Canon feed override, spindle speed override, adaptive feed, and feed hold callbacks preserved as temporary state events | `tests/gcode/linuxcnc_override_controls.ngc` | +| Modal state save/restore `M70`-`M73` | covered for main-program save/invalidate/restore plus O-word subroutine autorestore in smoke parser, with native/source regressions checking restored distance mode, units, and XY plane motion | `tests/gcode/linuxcnc_modal_state.ngc`, `tests/gcode/linuxcnc_modal_autorestore.ngc` | +| LinuxCNC read-only named parameters | partially covered in smoke parser for common modal state reads used by O-word macros: `#<_absolute>`, `#<_incremental>`, `#<_metric>`, `#<_imperial>`, `#<_feed>`, `#<_rpm>`, `#<_spindle_on>`, `#<_spindle_cw>`, `#<_lathe_diameter_mode>`, and `#<_lathe_radius_mode>` | smoke API regression | | Feed and motion control modes `G93/G94/G95`, `G61/G61.1/G64` | covered through LinuxCNC native/source backends | `tests/gcode/linuxcnc_feed_modes.ngc`, `tests/gcode/linuxcnc_motion_modes.ngc` | -| Canned cycle `G81/G80` | covered for drilling expand-to-canon path | `tests/gcode/linuxcnc_canned_cycle.ngc` | -| Coordinate offset Canon events `G10 L2`, `G10 L20`, `G92/G92.1` | covered through LinuxCNC native/source backends | `tests/gcode/linuxcnc_coordinate_offsets.ngc`, `tests/gcode/linuxcnc_coordinate_l20.ngc` | +| Canned cycle `G81/G80` | covered for drilling expand-to-canon path, including `G18/G19` plane-specific depth-axis mapping for representative drilling/boring cycles | `tests/gcode/linuxcnc_canned_cycle.ngc`, `tests/gcode/linuxcnc_canned_cycle_planes.ngc` | +| Coordinate offset Canon events `G10 L2`, `G10 L20`, `G10 P0 active-system targeting`, `G54`-`G59.3` selection, `G52`, `G92/G92.1/G92.2/G92.3` | covered through LinuxCNC native/source backends, including all nine G5X indices | `tests/gcode/linuxcnc_coordinate_offsets.ngc`, `tests/gcode/linuxcnc_coordinate_l20.ngc`, `tests/gcode/linuxcnc_coordinate_p0.ngc`, `tests/gcode/linuxcnc_coordinate_select_all.ngc`, `tests/gcode/linuxcnc_g92_restore.ngc`, `tests/gcode/linuxcnc_g52_offset.ngc` | | Cutter compensation | covered for tool-table `G41/G42 D...` and explicit-radius `G41.1/G42.1/G40` through LinuxCNC native/source backends | `tests/gcode/linuxcnc_cutter_comp.ngc` | -| Probe move `G38.3` | covered as a distinct probe event through LinuxCNC native/source backends | `tests/gcode/linuxcnc_probe_no_error.ngc` | -| O-word subroutines and calls | covered for numeric `O... sub/call/return/endsub` through LinuxCNC file mode; smoke parser also covers named `O` sub/call/return/endsub | `tests/gcode/smoke_oword_subprogram.ngc` | -| Broader canned cycles `G73`, `G82`-`G89` | partially covered: `G73`, `G82`, `G83`, `G85`, `G86`, `G89` | `tests/gcode/linuxcnc_canned_cycles_extended.ngc`, smoke API regression | +| Probe moves `G38.2/G38.3/G38.4/G38.5` | covered as distinct probe events with LinuxCNC `probe_type` preserved through smoke, native, and source-linked paths | `tests/gcode/linuxcnc_probe_no_error.ngc` | +| O-word subroutines, calls, and control flow | covered for numeric and named `O... sub/call/return/endsub` plus `if`/`elseif`/`else`, `while`, `repeat`, `do`/`while`, `break`, and `continue` through LinuxCNC file mode; smoke parser also covers named `O` sub/call/return/endsub and loop-control forms | `tests/gcode/smoke_oword_subprogram.ngc`, `tests/gcode/linuxcnc_named_oword_subprogram.ngc`, `tests/gcode/linuxcnc_oword_control_flow.ngc` | +| Broader canned cycles `G73`, `G74`, `G82`-`G89` | partially covered: `G73`, `G74`, `G82`, `G83`, `G84`, `G85`, `G86`, `G87`, `G88`, `G89`; smoke parser maps supported cycles across `G17/G18/G19`, including LinuxCNC's special non-XY `G74/G84` tapping argument order and `G87` plane-specific `I/J/K` mapping; native/source regressions now check non-XY `G74/G84/G86/G87/G88` sequences | `tests/gcode/linuxcnc_canned_cycles_extended.ngc`, `tests/gcode/linuxcnc_canned_cycle_planes.ngc`, smoke API regression | | Full source-level wasm build | pending | replace Python/HAL/INI/tooldata support dependencies | diff --git a/test-linuxcnc-rs274-native.sh b/test-linuxcnc-rs274-native.sh index b9493ae..a6b9aba 100755 --- a/test-linuxcnc-rs274-native.sh +++ b/test-linuxcnc-rs274-native.sh @@ -9,6 +9,7 @@ build_dir=${TMPDIR:-/tmp}/cnc_sim_native mkdir -p "$build_dir" var_file="$build_dir/rs274ngc.var" base_var_file="$build_dir/rs274ngc-base.var" +ini_named_parameter_file="$(pwd)/tests/gcode/linuxcnc_ini_named_parameter.ini" cp "$linuxcnc_root/tests/halui/jogging/sim.var" "$base_var_file" cp "$base_var_file" "$var_file" @@ -39,22 +40,54 @@ CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linux 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_spindle_direction.ngc >/tmp/cnc_sim_linuxcnc_spindle_direction.json +CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_spindle_modes.ngc >/tmp/cnc_sim_linuxcnc_spindle_modes.json +CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_spindle_orient.ngc >/tmp/cnc_sim_linuxcnc_spindle_orient.json +CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_io_controls.ngc >/tmp/cnc_sim_linuxcnc_io_controls.json +CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_override_controls.ngc >/tmp/cnc_sim_linuxcnc_override_controls.json +CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_modal_state.ngc >/tmp/cnc_sim_linuxcnc_modal_state.json +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_modal_autorestore.ngc >/tmp/cnc_sim_linuxcnc_modal_autorestore.json +if CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_modal_invalidate.ngc >/tmp/cnc_sim_linuxcnc_modal_invalidate.json 2>/tmp/cnc_sim_linuxcnc_modal_invalidate.err; then + echo "expected linuxcnc_modal_invalidate.ngc to fail" >&2 + exit 1 +fi +CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_threading_sync.ngc >/tmp/cnc_sim_linuxcnc_threading_sync.json +CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_threading_cycle.ngc >/tmp/cnc_sim_linuxcnc_threading_cycle.json CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_tool_number.ngc >/tmp/cnc_sim_linuxcnc_tool_number.json CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_program_stops.ngc >/tmp/cnc_sim_linuxcnc_program_stops.json CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_coolant.ngc >/tmp/cnc_sim_linuxcnc_coolant.json CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_feed_modes.ngc >/tmp/cnc_sim_linuxcnc_feed_modes.json CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_motion_modes.ngc >/tmp/cnc_sim_linuxcnc_motion_modes.json CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_tool_length.ngc >/tmp/cnc_sim_linuxcnc_tool_length.json +CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_dynamic_tool_length.ngc >/tmp/cnc_sim_linuxcnc_dynamic_tool_length.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_canned_cycles_extended.ngc >/tmp/cnc_sim_linuxcnc_canned_cycles_extended.json +CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_canned_cycle_planes.ngc >/tmp/cnc_sim_linuxcnc_canned_cycle_planes.json +CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_arc_planes.ngc >/tmp/cnc_sim_linuxcnc_arc_planes.json +CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_arc_distance_modes.ngc >/tmp/cnc_sim_linuxcnc_arc_distance_modes.json +CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_predefined_positions.ngc >/tmp/cnc_sim_linuxcnc_predefined_positions.json +CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_machine_coordinates.ngc >/tmp/cnc_sim_linuxcnc_machine_coordinates.json CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_cutter_comp.ngc >/tmp/cnc_sim_linuxcnc_cutter_comp.json CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_comments.ngc >/tmp/cnc_sim_linuxcnc_comments.json CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_probe_no_error.ngc >/tmp/cnc_sim_linuxcnc_probe_no_error.json CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/smoke_oword_subprogram.ngc >/tmp/cnc_sim_linuxcnc_oword_subprogram.json +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_named_oword_subprogram.ngc >/tmp/cnc_sim_linuxcnc_named_oword_subprogram.json +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_oword_control_flow.ngc >/tmp/cnc_sim_linuxcnc_oword_control_flow.json +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_named_parameter_exists.ngc >/tmp/cnc_sim_linuxcnc_named_parameter_exists.json +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_indexed_parameters.ngc >/tmp/cnc_sim_linuxcnc_indexed_parameters.json +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_named_parameter_normalization.ngc >/tmp/cnc_sim_linuxcnc_named_parameter_normalization.json +INI_FILE_NAME="$ini_named_parameter_file" CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_ini_named_parameter.ngc >/tmp/cnc_sim_linuxcnc_ini_named_parameter.json cp "$base_var_file" "$var_file" CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_coordinate_offsets.ngc >/tmp/cnc_sim_linuxcnc_coordinate_offsets.json cp "$base_var_file" "$var_file" CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_coordinate_l20.ngc >/tmp/cnc_sim_linuxcnc_coordinate_l20.json +cp "$base_var_file" "$var_file" +CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_coordinate_p0.ngc >/tmp/cnc_sim_linuxcnc_coordinate_p0.json +cp "$base_var_file" "$var_file" +CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_coordinate_select_all.ngc >/tmp/cnc_sim_linuxcnc_coordinate_select_all.json +cp "$base_var_file" "$var_file" +CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_g92_restore.ngc >/tmp/cnc_sim_linuxcnc_g92_restore.json +cp "$base_var_file" "$var_file" +CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_g52_offset.ngc >/tmp/cnc_sim_linuxcnc_g52_offset.json python3 - <<'PY' import json from pathlib import Path @@ -95,6 +128,160 @@ if (3, 1200, 2) not in spindle_states: if (4, 0, 0) not in spindle_states: raise SystemExit("missing M5 stopped spindle state") +spindle_modes = json.loads(Path("/tmp/cnc_sim_linuxcnc_spindle_modes.json").read_text()) +spindle_mode_states = [ + (event["line"], event["reserved"], event["feed"], event["tool"]) + for event in spindle_modes + if event["type"] == "comment" and event["line"] in {2, 3, 4} and event["reserved"] in {960, 970} +] +expected_spindle_mode_states = [ + (2, 970, 0, 0), + (3, 960, 2500, 0), + (4, 970, 0, 0), +] +if spindle_mode_states != expected_spindle_mode_states: + raise SystemExit(f"unexpected G96/G97 spindle mode states: {spindle_mode_states!r}") + +spindle_orient = json.loads(Path("/tmp/cnc_sim_linuxcnc_spindle_orient.json").read_text()) +if not any( + event["type"] == "comment" and + event["line"] == 2 and + event["reserved"] == 1901 and + event["tool"] == 0 and + event["feed"] == 45 and + event["arcTurns"] == 1 + for event in spindle_orient +): + raise SystemExit("missing M19 spindle orient state") +if not any( + event["type"] == "comment" and + event["line"] == 2 and + event["reserved"] == 1902 and + event["tool"] == 0 and + event["dwellSeconds"] == 2 + for event in spindle_orient +): + raise SystemExit("missing M19 spindle orient wait state") + +io_controls = json.loads(Path("/tmp/cnc_sim_linuxcnc_io_controls.json").read_text()) +io_states = [ + (event["line"], event["reserved"], event["tool"], event["feed"], event["arcTurns"], event["dwellSeconds"]) + for event in io_controls + if event["type"] == "comment" and event["reserved"] in {62, 63, 64, 65, 66, 67, 68} +] +expected_io_states = [ + (2, 62, 1, 1, 0, 0), + (3, 63, 1, 0, 0, 0), + (4, 64, 2, 1, 0, 0), + (5, 65, 2, 0, 0, 0), + (6, 66, 0, 3, 1, 5), + (7, 66, 1, 0, 0, 0), + (8, 67, 0, 1.25, 0, 0), + (9, 68, 1, 2.5, 0, 0), +] +if io_states != expected_io_states: + raise SystemExit(f"unexpected M62-M68 I/O states: {io_states!r}") + +override_controls = json.loads(Path("/tmp/cnc_sim_linuxcnc_override_controls.json").read_text()) +override_states = [ + (event["line"], event["reserved"], event["tool"], event["feed"]) + for event in override_controls + if event["type"] == "comment" and event["reserved"] in {50, 51, 52, 53} +] +expected_override_states = [ + (2, 50, 0, 1), + (2, 51, 0, 1), + (3, 50, 0, 0), + (3, 51, 0, 0), + (4, 50, 0, 1), + (5, 50, 0, 0), + (6, 51, 0, 1), + (7, 51, 0, 0), + (8, 52, 0, 1), + (9, 52, 0, 0), + (10, 53, 0, 1), + (11, 53, 0, 0), + (12, 50, 0, 1), +] +if override_states != expected_override_states: + raise SystemExit(f"unexpected M48-M53 override states: {override_states!r}") + +modal_state = json.loads(Path("/tmp/cnc_sim_linuxcnc_modal_state.json").read_text()) +if not any( + event["type"] == "rapid" and + event["line"] == 7 and + event["end"]["x"] == 2 and + event["end"]["y"] == 0 and + event["end"]["z"] == 2 + for event in modal_state +): + raise SystemExit("missing M72 restored absolute millimeter move") +if not any( + event["type"] == "arc-feed" and + event["line"] == 8 and + event["plane"] == 17 and + event["end"]["x"] == 4 and + event["end"]["y"] == 0 + for event in modal_state +): + raise SystemExit("missing M72 restored XY plane arc") + +modal_autorestore = json.loads(Path("/tmp/cnc_sim_linuxcnc_modal_autorestore.json").read_text()) +if not any( + event["type"] == "rapid" and + event["line"] == 9 and + event["end"]["x"] == 10 + for event in modal_autorestore +): + raise SystemExit("missing M73 subroutine changed modal state move") +if not any( + event["type"] == "rapid" and + event["line"] == 4 and + event["start"]["x"] == 10 and + event["end"]["x"] == 12 + for event in modal_autorestore +): + raise SystemExit("missing M73 autorestored incremental caller move") + +threading_sync = json.loads(Path("/tmp/cnc_sim_linuxcnc_threading_sync.json").read_text()) +sync_states = [ + (event["line"], event["reserved"], event["feed"], event["tool"]) + for event in threading_sync + if event["type"] == "comment" and event["reserved"] in {3300, 3301, 3302} +] +expected_sync_states = [ + (4, 3301, 1, 0), + (4, 3300, 0, 0), + (6, 3301, 1, 0), + (6, 3300, 0, 0), +] +if sync_states != expected_sync_states: + raise SystemExit(f"unexpected G33/G33.1 sync states: {sync_states!r}") +if not any( + event["type"] == "linear-feed" and event["line"] == 6 and event["end"]["z"] == 0 + for event in threading_sync +): + raise SystemExit("missing G33.1 rigid tap motion line") + +threading_cycle = json.loads(Path("/tmp/cnc_sim_linuxcnc_threading_cycle.json").read_text()) +if not any( + event["type"] == "comment" and + event["line"] == 4 and + event["reserved"] == 3301 and + event["feed"] == 0.05 + for event in threading_cycle +): + raise SystemExit("missing G76 threading sync start") +if not any(event["type"] == "comment" and event["line"] == 4 and event["reserved"] == 3300 for event in threading_cycle): + raise SystemExit("missing G76 threading sync stop") +if not any( + event["type"] == "linear-feed" and event["line"] == 4 and event["end"]["z"] == -1 + for event in threading_cycle +): + raise SystemExit("missing G76 final threading pass") +if not any(event["type"] == "program-end" and event["line"] == 5 for event in threading_cycle): + raise SystemExit("missing G76 program end line number") + tool_number = json.loads(Path("/tmp/cnc_sim_linuxcnc_tool_number.json").read_text()) if not any( event["type"] == "tool-change" and @@ -170,6 +357,16 @@ if not any( ): raise SystemExit("missing G49 tool length clear event") +dynamic_tool_length = json.loads(Path("/tmp/cnc_sim_linuxcnc_dynamic_tool_length.json").read_text()) +dynamic_offsets = [ + (event["line"], event["start"]["x"], event["start"]["z"]) + for event in dynamic_tool_length + if event["type"] == "comment" and event["reserved"] == 430 +] +expected_dynamic_offsets = [(2, 1, 2), (3, 1.25, 2.5), (4, 1.25, 15), (5, 0, 0)] +if dynamic_offsets != expected_dynamic_offsets: + raise SystemExit(f"unexpected G43.1/G43.2 tool length offsets: {dynamic_offsets!r}") + 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 = [ @@ -183,6 +380,152 @@ actual_cycle = [(event["type"], event["end"]["x"], event["end"]["y"], event["end if actual_cycle != expected_cycle: raise SystemExit(f"unexpected G81/G80 expansion: {actual_cycle!r}") +cycle_planes = json.loads(Path("/tmp/cnc_sim_linuxcnc_canned_cycle_planes.json").read_text()) +if not any( + event["type"] == "linear-feed" and + event["line"] == 4 and + event["end"]["x"] == 1 and + event["end"]["y"] == -2 and + event["end"]["z"] == 4 + for event in cycle_planes +): + raise SystemExit("missing G18 G81 feed along Y depth axis") +if not any( + event["type"] == "rapid" and + event["line"] == 4 and + event["end"]["x"] == 1 and + event["end"]["y"] == 3 and + event["end"]["z"] == 4 + for event in cycle_planes +): + raise SystemExit("missing G18 G81 retract to Y R plane") +if not any( + event["type"] == "linear-feed" and + event["line"] == 7 and + event["end"]["x"] == -2 and + event["end"]["y"] == 3 and + event["end"]["z"] == 4 + for event in cycle_planes +): + raise SystemExit("missing G19 G82 feed along X depth axis") +if not any( + event["type"] == "dwell" and + event["line"] == 7 and + event["dwellSeconds"] == 0.2 + for event in cycle_planes +): + raise SystemExit("missing G19 G82 dwell at bottom") +if not any( + event["type"] == "rapid" and + event["line"] == 7 and + event["end"]["x"] == 2 and + event["end"]["y"] == 3 and + event["end"]["z"] == 4 + for event in cycle_planes +): + raise SystemExit("missing G19 G82 retract to X R plane") +def plane_cycle_signature(event): + if event["type"] in {"rapid", "linear-feed"}: + return (event["type"], event["end"]["x"], event["end"]["y"], event["end"]["z"]) + if event["type"] == "set-spindle": + return ("set-spindle", event["spindle"], event["reserved"]) + if event["type"] == "dwell": + return ("dwell", event["dwellSeconds"]) + if event["type"] == "program-end": + return ("program-end", event["reserved"]) + return None +plane_g84_sequence = [ + plane_cycle_signature(event) + for event in cycle_planes + if event["line"] == 11 and event["type"] in {"rapid", "linear-feed", "set-spindle", "dwell"} +] +expected_plane_g84_sequence = [ + ("rapid", 1, 5, 4), + ("rapid", 1, 3, 4), + ("linear-feed", -2, 4, 1), + ("set-spindle", 0, 0), + ("set-spindle", 600, 2), + ("dwell", 0.05), + ("linear-feed", 3, 4, 1), + ("set-spindle", 0, 0), + ("set-spindle", 600, 1), +] +if plane_g84_sequence != expected_plane_g84_sequence: + raise SystemExit(f"unexpected G18 G84 plane sequence: {plane_g84_sequence!r}") +plane_g74_sequence = [ + plane_cycle_signature(event) + for event in cycle_planes + if event["line"] == 15 and event["type"] in {"rapid", "linear-feed", "set-spindle", "dwell"} +] +expected_plane_g74_sequence = [ + ("rapid", 5, 3, 4), + ("rapid", 2, 3, 4), + ("linear-feed", 4, -2, 3), + ("set-spindle", 0, 0), + ("set-spindle", 500, 1), + ("dwell", 0.04), + ("linear-feed", 4, 2, 3), + ("set-spindle", 0, 0), + ("set-spindle", 500, 2), +] +if plane_g74_sequence != expected_plane_g74_sequence: + raise SystemExit(f"unexpected G19 G74 plane sequence: {plane_g74_sequence!r}") +plane_g86_sequence = [ + plane_cycle_signature(event) + for event in cycle_planes + if event["line"] == 19 and event["type"] in {"rapid", "linear-feed", "set-spindle", "dwell"} +] +expected_plane_g86_sequence = [ + ("rapid", 1, 5, 4), + ("rapid", 1, 3, 4), + ("linear-feed", 1, -2, 4), + ("dwell", 0.1), + ("set-spindle", 0, 0), + ("rapid", 1, 3, 4), + ("set-spindle", 900, 1), +] +if plane_g86_sequence != expected_plane_g86_sequence: + raise SystemExit(f"unexpected G18 G86 plane sequence: {plane_g86_sequence!r}") +plane_g88_sequence = [ + plane_cycle_signature(event) + for event in cycle_planes + if event["line"] == 23 and event["type"] in {"rapid", "linear-feed", "set-spindle", "dwell", "program-end"} +] +expected_plane_g88_sequence = [ + ("rapid", 5, 3, 4), + ("rapid", 2, 3, 4), + ("linear-feed", -2, 3, 4), + ("dwell", 0.07), + ("set-spindle", 0, 0), + ("program-end", 1), + ("set-spindle", 700, 1), +] +if plane_g88_sequence != expected_plane_g88_sequence: + raise SystemExit(f"unexpected G19 G88 plane sequence: {plane_g88_sequence!r}") +plane_g87_sequence = [ + plane_cycle_signature(event) + for event in cycle_planes + if event["line"] == 27 and event["type"] in {"rapid", "linear-feed", "set-spindle"} +] +expected_plane_g87_sequence = [ + ("rapid", 5, 1, 2), + ("rapid", 2, 1, 2), + ("rapid", 2, 1.2, 2.3), + ("set-spindle", 0, 0), + ("rapid", -4, 1.2, 2.3), + ("rapid", -4, 1, 2), + ("set-spindle", 800, 1), + ("linear-feed", -2, 1, 2), + ("linear-feed", -4, 1, 2), + ("set-spindle", 0, 0), + ("rapid", -4, 1.2, 2.3), + ("rapid", 2, 1.2, 2.3), + ("rapid", 2, 1, 2), + ("set-spindle", 800, 1), +] +if plane_g87_sequence != expected_plane_g87_sequence: + raise SystemExit(f"unexpected G19 G87 plane sequence: {plane_g87_sequence!r}") + extended_cycle = json.loads(Path("/tmp/cnc_sim_linuxcnc_canned_cycles_extended.json").read_text()) if not any( event["type"] == "linear-feed" and @@ -214,6 +557,178 @@ if not any( for event in extended_cycle ): raise SystemExit("missing G83 final retract to R plane") +def g84_signature(event): + if event["type"] == "linear-feed": + return ("linear-feed", event["end"]["x"], event["end"]["z"]) + if event["type"] == "set-spindle": + return ("set-spindle", event["spindle"], event["reserved"]) + if event["type"] == "dwell": + return ("dwell", event["dwellSeconds"]) + if event["type"] == "program-end": + return ("program-end", event["reserved"]) + return None +g84_sequence = [ + g84_signature(event) + for event in extended_cycle + if event["line"] == 7 and event["type"] in {"linear-feed", "set-spindle", "dwell"} +] +expected_g84_sequence = [ + ("linear-feed", 30, -2), + ("set-spindle", 0, 0), + ("set-spindle", 600, 2), + ("dwell", 0.05), + ("linear-feed", 30, 2), + ("set-spindle", 0, 0), + ("set-spindle", 600, 1), +] +if g84_sequence != expected_g84_sequence: + raise SystemExit(f"unexpected G84 tapping sequence: {g84_sequence!r}") +g74_sequence = [ + g84_signature(event) + for event in extended_cycle + if event["line"] == 9 and event["type"] in {"linear-feed", "set-spindle", "dwell"} +] +expected_g74_sequence = [ + ("linear-feed", 40, -2), + ("set-spindle", 0, 0), + ("set-spindle", 500, 1), + ("dwell", 0.04), + ("linear-feed", 40, 2), + ("set-spindle", 0, 0), + ("set-spindle", 500, 2), +] +if g74_sequence != expected_g74_sequence: + raise SystemExit(f"unexpected G74 tapping sequence: {g74_sequence!r}") +g88_sequence = [ + g84_signature(event) + for event in extended_cycle + if event["line"] == 11 and event["type"] in {"linear-feed", "set-spindle", "dwell", "program-end"} +] +expected_g88_sequence = [ + ("linear-feed", 50, -2), + ("dwell", 0.07), + ("set-spindle", 0, 0), + ("program-end", 1), + ("set-spindle", 700, 1), +] +if g88_sequence != expected_g88_sequence: + raise SystemExit(f"unexpected G88 manual-out sequence: {g88_sequence!r}") +if any( + event["type"] == "rapid" and + event["line"] == 11 and + event["start"]["x"] == 50 and + event["start"]["z"] == -2 and + event["end"]["x"] == 50 and + event["end"]["z"] == 2 + for event in extended_cycle +): + raise SystemExit("unexpected G88 automatic retract") +def g87_signature(event): + if event["type"] in {"rapid", "linear-feed"}: + return (event["type"], event["end"]["x"], event["end"]["y"], event["end"]["z"]) + if event["type"] == "set-spindle": + return ("set-spindle", event["spindle"], event["reserved"]) + return None +g87_sequence = [ + g87_signature(event) + for event in extended_cycle + if event["line"] == 14 and event["type"] in {"rapid", "linear-feed", "set-spindle"} +] +expected_g87_sequence = [ + ("rapid", 60, 0, 5), + ("rapid", 60, 0, 2), + ("rapid", 60.2, 0.3, 2), + ("set-spindle", 0, 0), + ("rapid", 60.2, 0.3, -4), + ("rapid", 60, 0, -4), + ("set-spindle", 800, 1), + ("linear-feed", 60, 0, -2), + ("linear-feed", 60, 0, -4), + ("set-spindle", 0, 0), + ("rapid", 60.2, 0.3, -4), + ("rapid", 60.2, 0.3, 2), + ("rapid", 60, 0, 2), + ("set-spindle", 800, 1), +] +if g87_sequence != expected_g87_sequence: + raise SystemExit(f"unexpected G87 back-boring sequence: {g87_sequence!r}") + +arc_planes = json.loads(Path("/tmp/cnc_sim_linuxcnc_arc_planes.json").read_text()) +if not any( + event["type"] == "arc-feed" and + event["line"] == 5 and + event["plane"] == 18 and + event["start"]["x"] == 0 and + event["start"]["z"] == 0 and + event["end"]["x"] == 10 and + event["end"]["z"] == 0 and + event["center"]["x"] == 5 and + event["center"]["z"] == 0 and + event["arcTurns"] == -1 + for event in arc_planes +): + raise SystemExit("missing G18 XZ plane arc-feed mapping") +if not any( + event["type"] == "arc-feed" and + event["line"] == 8 and + event["plane"] == 19 and + event["start"]["y"] == 0 and + event["start"]["z"] == 0 and + event["end"]["y"] == 10 and + event["end"]["z"] == 0 and + event["center"]["y"] == 5 and + event["center"]["z"] == 0 and + event["arcTurns"] == 1 + for event in arc_planes +): + raise SystemExit("missing G19 YZ plane arc-feed mapping") + +arc_distance = json.loads(Path("/tmp/cnc_sim_linuxcnc_arc_distance_modes.json").read_text()) +arc_centers = [ + (event["line"], event["center"]["x"], event["center"]["y"]) + for event in arc_distance + if event["type"] == "arc-feed" +] +expected_arc_centers = [(5, 5, 0), (7, 15, 0), (9, 25, 0)] +if arc_centers != expected_arc_centers: + raise SystemExit(f"unexpected G90.1/G91.1 arc centers: {arc_centers!r}") + +predefined = json.loads(Path("/tmp/cnc_sim_linuxcnc_predefined_positions.json").read_text()) +predefined_rapids = [ + ( + event["line"], + event["start"]["x"], event["start"]["y"], event["start"]["z"], + event["end"]["x"], event["end"]["y"], event["end"]["z"], + ) + for event in predefined + if event["type"] == "rapid" and event["line"] in {5, 9} +] +expected_predefined_rapids = [ + (5, 10, 20, 30, 10, 20, 5), + (5, 10, 20, 5, 10, 20, 3), + (9, 40, 50, 60, 40, 50, 60), + (9, 40, 50, 60, 4, 5, 6), +] +if predefined_rapids != expected_predefined_rapids: + raise SystemExit(f"unexpected G28/G30 rapid sequence: {predefined_rapids!r}") + +machine_coords = json.loads(Path("/tmp/cnc_sim_linuxcnc_machine_coordinates.json").read_text()) +machine_moves = [ + ( + event["type"], event["line"], + event["start"]["x"], event["start"]["y"], event["start"]["z"], + event["end"]["x"], event["end"]["y"], event["end"]["z"], + ) + for event in machine_coords + if event["type"] in {"rapid", "linear-feed"} and event["line"] in {3, 4, 6} +] +expected_machine_moves = [ + ("rapid", 3, 5, 6, 7, 0, 6, 1), + ("rapid", 4, 0, 6, 1, 2, 6, 1), + ("linear-feed", 6, 2, 6, 1, 2, 3, 1), +] +if machine_moves != expected_machine_moves: + raise SystemExit(f"unexpected G53 machine-coordinate moves: {machine_moves!r}") cutter_comp = json.loads(Path("/tmp/cnc_sim_linuxcnc_cutter_comp.json").read_text()) cutter_comp_motions = [ @@ -248,17 +763,29 @@ if not any(event["type"] == "comment" and event["line"] == 2 for event in commen raise SystemExit("missing LinuxCNC comment line event") probe = json.loads(Path("/tmp/cnc_sim_linuxcnc_probe_no_error.json").read_text()) +probe_events = [ + (event["line"], event["reserved"], event["start"]["z"], event["end"]["z"]) + for event in probe + if event["type"] == "probe" +] +expected_probe_events = [ + (4, 0, 5, 4), + (6, 1, 5, 3), + (8, 2, 5, 2), + (10, 3, 5, 1), + (12, 1, 5, 0), +] +if probe_events != expected_probe_events: + raise SystemExit(f"unexpected G38 probe variants: {probe_events!r}") +if any(event["type"] == "linear-feed" and event["line"] in {4, 6, 8, 10, 12} for event in probe): + raise SystemExit("G38 probe moves should not be emitted as linear-feed") if not any( - event["type"] == "probe" and - event["line"] == 4 and - event["feed"] == 100 and - event["start"]["z"] == 5 and - event["end"]["z"] == 0 + event["type"] == "linear-feed" and + event["line"] == 14 and + event["end"]["x"] == 1 for event in probe ): - raise SystemExit("missing G38.3 probe event") -if any(event["type"] == "linear-feed" and event["line"] == 4 for event in probe): - raise SystemExit("G38.3 probe should not be emitted as linear-feed") + raise SystemExit("missing #5070 probe-tripped conditional motion") oword = json.loads(Path("/tmp/cnc_sim_linuxcnc_oword_subprogram.json").read_text()) if not any( @@ -285,6 +812,161 @@ if any( ): raise SystemExit("LinuxCNC O-word return should skip remaining subprogram body") +named_oword = json.loads(Path("/tmp/cnc_sim_linuxcnc_named_oword_subprogram.json").read_text()) +if not any( + event["type"] == "linear-feed" and + event["line"] == 9 and + event["start"]["x"] == 0 and + event["end"]["x"] == 10 + for event in named_oword +): + raise SystemExit("missing LinuxCNC named O-word expression motion") +if not any( + event["type"] == "linear-feed" and + event["line"] == 10 and + event["start"]["y"] == 0 and + event["end"]["y"] == 6 + for event in named_oword +): + raise SystemExit("missing LinuxCNC named O-word #2 argument motion") +if not any( + event["type"] == "linear-feed" and + event["line"] == 5 and + event["start"]["x"] == 10 and + event["end"]["x"] == 20 + for event in named_oword +): + raise SystemExit("missing LinuxCNC named O-word return-to-caller motion") +if any( + event["type"] == "linear-feed" and + event["line"] == 12 and + event["end"]["x"] == 99 + for event in named_oword +): + raise SystemExit("LinuxCNC named O-word return should skip remaining subprogram body") + +oword_flow = json.loads(Path("/tmp/cnc_sim_linuxcnc_oword_control_flow.json").read_text()) +if not any(event["type"] == "linear-feed" and event["line"] == 7 and event["end"]["x"] == 2 for event in oword_flow): + raise SystemExit("missing LinuxCNC O-word elseif branch motion") +if any( + event["type"] == "linear-feed" and event["line"] in {5, 9} and event["end"]["x"] in {98, 99} + for event in oword_flow +): + raise SystemExit("LinuxCNC O-word conditional should skip inactive branches") +while_values = [ + event["end"]["y"] + for event in oword_flow + if event["type"] == "linear-feed" and event["line"] == 13 +] +if while_values != [0, 1, 2]: + raise SystemExit(f"unexpected LinuxCNC O-word while values: {while_values!r}") +repeat_values = [ + event["end"]["z"] + for event in oword_flow + if event["type"] == "linear-feed" and event["line"] == 19 +] +if repeat_values != [1, 2, 3]: + raise SystemExit(f"unexpected LinuxCNC O-word repeat values: {repeat_values!r}") +do_values = [ + event["end"]["a"] + for event in oword_flow + if event["type"] == "linear-feed" and event["line"] == 24 +] +if do_values != [1, 2, 3]: + raise SystemExit(f"unexpected LinuxCNC O-word do/while values: {do_values!r}") +break_values = [ + event["end"]["b"] + for event in oword_flow + if event["type"] == "linear-feed" and event["line"] == 32 +] +if break_values != [1]: + raise SystemExit(f"unexpected LinuxCNC O-word do/break body values: {break_values!r}") +if not any( + event["type"] == "linear-feed" and + event["line"] == 34 and + event["end"]["c"] == 2 + for event in oword_flow +): + raise SystemExit("missing LinuxCNC O-word do/break final loop counter motion") +continue_values = [ + event["end"]["u"] + for event in oword_flow + if event["type"] == "linear-feed" and event["line"] == 43 +] +if continue_values != [2, 3]: + raise SystemExit(f"unexpected LinuxCNC O-word do/continue body values: {continue_values!r}") +if not any( + event["type"] == "linear-feed" and + event["line"] == 45 and + event["end"]["v"] == 3 + for event in oword_flow +): + raise SystemExit("missing LinuxCNC O-word do/continue final loop counter motion") +if not any( + event["type"] == "linear-feed" and + event["line"] == 46 and + event["end"]["w"] == 2 + for event in oword_flow +): + raise SystemExit("missing LinuxCNC O-word do/continue skipped-body counter motion") + +exists = json.loads(Path("/tmp/cnc_sim_linuxcnc_named_parameter_exists.json").read_text()) +exists_moves = [ + (event["line"], event["end"]["x"], event["end"]["y"]) + for event in exists + if event["type"] == "linear-feed" +] +expected_exists_moves = [(5, 1, 0), (8, 1, 1)] +if exists_moves != expected_exists_moves: + raise SystemExit(f"unexpected LinuxCNC EXISTS named parameter branch motions: {exists_moves!r}") + +indexed = json.loads(Path("/tmp/cnc_sim_linuxcnc_indexed_parameters.json").read_text()) +indexed_moves = [ + ( + event["line"], + event["end"]["x"], event["end"]["y"], event["end"]["z"], + event["end"]["a"], event["end"]["b"], event["end"]["c"], + event["end"]["u"], event["end"]["v"], event["end"]["w"], + ) + for event in indexed + if event["type"] == "linear-feed" +] +expected_indexed_moves = [ + (7, 17, 0, 0, 0, 0, 0, 0, 0, 0), + (8, 17, 22, 0, 0, 0, 0, 0, 0, 0), + (10, 17, 22, 1, 0, 0, 0, 0, 0, 0), + (13, 17, 22, 1, 21, 0, 0, 0, 0, 0), + (14, 17, 22, 1, 21, 9, 0, 0, 0, 0), + (16, 17, 22, 1, 21, 9, 9, 2, 0, 0), + (17, 17, 22, 1, 21, 9, 9, 2, 4, 9), + (18, 4, 22, 1, 21, 9, 9, 2, 4, 9), + (19, 4, 12, 4, 21, 9, 9, 2, 4, 9), + (20, 4, 12, 4, 12, 9, 9, 2, 4, 9), + (21, 4, 12, 4, 12, 14, 9, 2, 4, 9), +] +if indexed_moves != expected_indexed_moves: + raise SystemExit(f"unexpected LinuxCNC indexed/indirect parameter motions: {indexed_moves!r}") + +normalized = json.loads(Path("/tmp/cnc_sim_linuxcnc_named_parameter_normalization.json").read_text()) +normalized_moves = [ + (event["line"], event["end"]["x"], event["end"]["y"]) + for event in normalized + if event["type"] == "linear-feed" +] +expected_normalized_moves = [(4, 12, 0), (6, 12, 1)] +if normalized_moves != expected_normalized_moves: + raise SystemExit(f"unexpected LinuxCNC normalized named parameter motions: {normalized_moves!r}") + +ini_named = json.loads(Path("/tmp/cnc_sim_linuxcnc_ini_named_parameter.json").read_text()) +ini_named_moves = [ + (event["line"], event["end"]["x"], event["end"]["y"]) + for event in ini_named + if event["type"] == "linear-feed" +] +expected_ini_named_moves = [(3, 3.25, 0), (5, 3.25, 1)] +if ini_named_moves != expected_ini_named_moves: + raise SystemExit(f"unexpected LinuxCNC INI named parameter motions: {ini_named_moves!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"] @@ -324,6 +1006,76 @@ if not any( raise SystemExit("missing G10 L20 P2 offset event after G55 selection") if not any(event["type"] == "program-end" and event["line"] == 5 for event in l20): raise SystemExit("missing G10 L20 program end line number") + +p0 = json.loads(Path("/tmp/cnc_sim_linuxcnc_coordinate_p0.json").read_text()) +if not any( + event["type"] == "set-g5x-offset" and + event["line"] == 3 and + event["tool"] == 2 and + event["start"]["x"] == 7 and + event["start"]["y"] == 8 and + event["start"]["z"] == 9 + for event in p0 +): + raise SystemExit("missing G10 L2 P0 active G55 offset event") +if not any(event["type"] == "program-end" and event["line"] == 4 for event in p0): + raise SystemExit("missing G10 P0 program end line number") + +select_all = json.loads(Path("/tmp/cnc_sim_linuxcnc_coordinate_select_all.json").read_text()) +select_events = [ + (event["line"], event["tool"], event["start"]["x"]) + for event in select_all + if event["type"] == "set-g5x-offset" and event["line"] in set(range(11, 20)) +] +expected_select_events = [ + (11, 2, 2), + (12, 1, 1), + (13, 3, 3), + (14, 4, 4), + (15, 5, 5), + (16, 6, 6), + (17, 7, 7), + (18, 8, 8), + (19, 9, 9), +] +if select_events != expected_select_events: + raise SystemExit(f"unexpected G54-G59.3 selection events: {select_events!r}") + +g92_restore = json.loads(Path("/tmp/cnc_sim_linuxcnc_g92_restore.json").read_text()) +g92_states = [ + (event["line"], event["start"]["x"], event["start"]["y"], event["start"]["z"]) + for event in g92_restore + if event["type"] == "set-g92-offset" and event["line"] > 0 +] +expected_g92_states = [ + (2, -1, -2, -3), + (3, 0, 0, 0), + (4, -1, -2, -3), + (5, 0, 0, 0), + (6, 0, 0, 0), +] +if g92_states != expected_g92_states: + raise SystemExit(f"unexpected G92.2/G92.3 restore sequence: {g92_states!r}") +if not any(event["type"] == "program-end" and event["line"] == 7 for event in g92_restore): + raise SystemExit("missing G92 restore program end line number") + +g52_offset = json.loads(Path("/tmp/cnc_sim_linuxcnc_g52_offset.json").read_text()) +g52_states = [ + (event["line"], event["start"]["x"], event["start"]["y"], event["start"]["z"]) + for event in g52_offset + if event["type"] == "set-g92-offset" and event["line"] > 0 +] +expected_g52_states = [ + (2, 1, 2, 0), + (3, 1, 3, 0), + (4, 0, 0, 0), + (5, 1, 3, 0), + (6, 0, 0, 0), +] +if g52_states != expected_g52_states: + raise SystemExit(f"unexpected G52 shared offset sequence: {g52_states!r}") +if not any(event["type"] == "program-end" and event["line"] == 7 for event in g52_offset): + raise SystemExit("missing G52 offset program end line number") PY echo "linuxcnc rs274 native smoke passed" @@ -331,17 +1083,41 @@ 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_spindle_direction.json" +echo "dumped /tmp/cnc_sim_linuxcnc_spindle_modes.json" +echo "dumped /tmp/cnc_sim_linuxcnc_spindle_orient.json" +echo "dumped /tmp/cnc_sim_linuxcnc_io_controls.json" +echo "dumped /tmp/cnc_sim_linuxcnc_override_controls.json" +echo "dumped /tmp/cnc_sim_linuxcnc_modal_state.json" +echo "dumped /tmp/cnc_sim_linuxcnc_modal_autorestore.json" +echo "dumped /tmp/cnc_sim_linuxcnc_threading_sync.json" +echo "dumped /tmp/cnc_sim_linuxcnc_threading_cycle.json" echo "dumped /tmp/cnc_sim_linuxcnc_tool_number.json" echo "dumped /tmp/cnc_sim_linuxcnc_program_stops.json" echo "dumped /tmp/cnc_sim_linuxcnc_coolant.json" echo "dumped /tmp/cnc_sim_linuxcnc_feed_modes.json" echo "dumped /tmp/cnc_sim_linuxcnc_motion_modes.json" echo "dumped /tmp/cnc_sim_linuxcnc_tool_length.json" +echo "dumped /tmp/cnc_sim_linuxcnc_dynamic_tool_length.json" echo "dumped /tmp/cnc_sim_linuxcnc_canned_cycle.json" echo "dumped /tmp/cnc_sim_linuxcnc_canned_cycles_extended.json" +echo "dumped /tmp/cnc_sim_linuxcnc_canned_cycle_planes.json" +echo "dumped /tmp/cnc_sim_linuxcnc_arc_planes.json" +echo "dumped /tmp/cnc_sim_linuxcnc_arc_distance_modes.json" +echo "dumped /tmp/cnc_sim_linuxcnc_predefined_positions.json" +echo "dumped /tmp/cnc_sim_linuxcnc_machine_coordinates.json" echo "dumped /tmp/cnc_sim_linuxcnc_cutter_comp.json" echo "dumped /tmp/cnc_sim_linuxcnc_comments.json" echo "dumped /tmp/cnc_sim_linuxcnc_probe_no_error.json" echo "dumped /tmp/cnc_sim_linuxcnc_oword_subprogram.json" +echo "dumped /tmp/cnc_sim_linuxcnc_named_oword_subprogram.json" +echo "dumped /tmp/cnc_sim_linuxcnc_oword_control_flow.json" +echo "dumped /tmp/cnc_sim_linuxcnc_named_parameter_exists.json" +echo "dumped /tmp/cnc_sim_linuxcnc_indexed_parameters.json" +echo "dumped /tmp/cnc_sim_linuxcnc_named_parameter_normalization.json" +echo "dumped /tmp/cnc_sim_linuxcnc_ini_named_parameter.json" echo "dumped /tmp/cnc_sim_linuxcnc_coordinate_offsets.json" echo "dumped /tmp/cnc_sim_linuxcnc_coordinate_l20.json" +echo "dumped /tmp/cnc_sim_linuxcnc_coordinate_p0.json" +echo "dumped /tmp/cnc_sim_linuxcnc_coordinate_select_all.json" +echo "dumped /tmp/cnc_sim_linuxcnc_g92_restore.json" +echo "dumped /tmp/cnc_sim_linuxcnc_g52_offset.json" diff --git a/test-linuxcnc-source-link.sh b/test-linuxcnc-source-link.sh index 8b57f82..0976e73 100755 --- a/test-linuxcnc-source-link.sh +++ b/test-linuxcnc-source-link.sh @@ -94,6 +94,7 @@ done var_file="$build_dir/rs274ngc-source.var" base_var_file="$build_dir/rs274ngc-source-base.var" +ini_named_parameter_file="$(pwd)/tests/gcode/linuxcnc_ini_named_parameter.ini" cp "$linuxcnc_root/tests/halui/jogging/sim.var" "$base_var_file" cp "$base_var_file" "$var_file" @@ -106,6 +107,36 @@ CNC_SIM_RS274_VAR="$var_file" \ CNC_SIM_RS274_VAR="$var_file" \ "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_spindle_direction.ngc \ >/tmp/cnc_sim_linuxcnc_source_spindle_direction.json +CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_spindle_modes.ngc \ + >/tmp/cnc_sim_linuxcnc_source_spindle_modes.json +CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_spindle_orient.ngc \ + >/tmp/cnc_sim_linuxcnc_source_spindle_orient.json +CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_io_controls.ngc \ + >/tmp/cnc_sim_linuxcnc_source_io_controls.json +CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_override_controls.ngc \ + >/tmp/cnc_sim_linuxcnc_source_override_controls.json +CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_modal_state.ngc \ + >/tmp/cnc_sim_linuxcnc_source_modal_state.json +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_modal_autorestore.ngc \ + >/tmp/cnc_sim_linuxcnc_source_modal_autorestore.json +if CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_modal_invalidate.ngc \ + >/tmp/cnc_sim_linuxcnc_source_modal_invalidate.json 2>/tmp/cnc_sim_linuxcnc_source_modal_invalidate.err; then + echo "expected linuxcnc_modal_invalidate.ngc to fail" >&2 + exit 1 +fi +CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_threading_sync.ngc \ + >/tmp/cnc_sim_linuxcnc_source_threading_sync.json +CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_threading_cycle.ngc \ + >/tmp/cnc_sim_linuxcnc_source_threading_cycle.json CNC_SIM_RS274_VAR="$var_file" \ "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_tool_number.ngc \ >/tmp/cnc_sim_linuxcnc_source_tool_number.json @@ -124,12 +155,30 @@ CNC_SIM_RS274_VAR="$var_file" \ CNC_SIM_RS274_VAR="$var_file" \ "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_tool_length.ngc \ >/tmp/cnc_sim_linuxcnc_source_tool_length.json +CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_dynamic_tool_length.ngc \ + >/tmp/cnc_sim_linuxcnc_source_dynamic_tool_length.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 CNC_SIM_RS274_VAR="$var_file" \ "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_canned_cycles_extended.ngc \ >/tmp/cnc_sim_linuxcnc_source_canned_cycles_extended.json +CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_canned_cycle_planes.ngc \ + >/tmp/cnc_sim_linuxcnc_source_canned_cycle_planes.json +CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_arc_planes.ngc \ + >/tmp/cnc_sim_linuxcnc_source_arc_planes.json +CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_arc_distance_modes.ngc \ + >/tmp/cnc_sim_linuxcnc_source_arc_distance_modes.json +CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_predefined_positions.ngc \ + >/tmp/cnc_sim_linuxcnc_source_predefined_positions.json +CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_machine_coordinates.ngc \ + >/tmp/cnc_sim_linuxcnc_source_machine_coordinates.json CNC_SIM_RS274_VAR="$var_file" \ "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_cutter_comp.ngc \ >/tmp/cnc_sim_linuxcnc_source_cutter_comp.json @@ -139,6 +188,24 @@ CNC_SIM_RS274_VAR="$var_file" \ CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/smoke_oword_subprogram.ngc \ >/tmp/cnc_sim_linuxcnc_source_oword_subprogram.json +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_named_oword_subprogram.ngc \ + >/tmp/cnc_sim_linuxcnc_source_named_oword_subprogram.json +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_oword_control_flow.ngc \ + >/tmp/cnc_sim_linuxcnc_source_oword_control_flow.json +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_named_parameter_exists.ngc \ + >/tmp/cnc_sim_linuxcnc_source_named_parameter_exists.json +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_indexed_parameters.ngc \ + >/tmp/cnc_sim_linuxcnc_source_indexed_parameters.json +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_named_parameter_normalization.ngc \ + >/tmp/cnc_sim_linuxcnc_source_named_parameter_normalization.json +INI_FILE_NAME="$ini_named_parameter_file" CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_ini_named_parameter.ngc \ + >/tmp/cnc_sim_linuxcnc_source_ini_named_parameter.json cp "$base_var_file" "$var_file" CNC_SIM_RS274_VAR="$var_file" \ "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_coordinate_offsets.ngc \ @@ -147,6 +214,22 @@ cp "$base_var_file" "$var_file" CNC_SIM_RS274_VAR="$var_file" \ "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_coordinate_l20.ngc \ >/tmp/cnc_sim_linuxcnc_source_coordinate_l20.json +cp "$base_var_file" "$var_file" +CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_coordinate_p0.ngc \ + >/tmp/cnc_sim_linuxcnc_source_coordinate_p0.json +cp "$base_var_file" "$var_file" +CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_coordinate_select_all.ngc \ + >/tmp/cnc_sim_linuxcnc_source_coordinate_select_all.json +cp "$base_var_file" "$var_file" +CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_g92_restore.ngc \ + >/tmp/cnc_sim_linuxcnc_source_g92_restore.json +cp "$base_var_file" "$var_file" +CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_g52_offset.ngc \ + >/tmp/cnc_sim_linuxcnc_source_g52_offset.json python3 - <<'PY' import json @@ -194,6 +277,160 @@ if (3, 1200, 2) not in spindle_states: if (4, 0, 0) not in spindle_states: raise SystemExit("missing source-linked M5 stopped spindle state") +spindle_modes = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_spindle_modes.json").read_text()) +spindle_mode_states = [ + (event["line"], event["reserved"], event["feed"], event["tool"]) + for event in spindle_modes + if event["type"] == "comment" and event["line"] in {2, 3, 4} and event["reserved"] in {960, 970} +] +expected_spindle_mode_states = [ + (2, 970, 0, 0), + (3, 960, 2500, 0), + (4, 970, 0, 0), +] +if spindle_mode_states != expected_spindle_mode_states: + raise SystemExit(f"unexpected source-linked G96/G97 spindle mode states: {spindle_mode_states!r}") + +spindle_orient = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_spindle_orient.json").read_text()) +if not any( + event["type"] == "comment" and + event["line"] == 2 and + event["reserved"] == 1901 and + event["tool"] == 0 and + event["feed"] == 45 and + event["arcTurns"] == 1 + for event in spindle_orient +): + raise SystemExit("missing source-linked M19 spindle orient state") +if not any( + event["type"] == "comment" and + event["line"] == 2 and + event["reserved"] == 1902 and + event["tool"] == 0 and + event["dwellSeconds"] == 2 + for event in spindle_orient +): + raise SystemExit("missing source-linked M19 spindle orient wait state") + +io_controls = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_io_controls.json").read_text()) +io_states = [ + (event["line"], event["reserved"], event["tool"], event["feed"], event["arcTurns"], event["dwellSeconds"]) + for event in io_controls + if event["type"] == "comment" and event["reserved"] in {62, 63, 64, 65, 66, 67, 68} +] +expected_io_states = [ + (2, 62, 1, 1, 0, 0), + (3, 63, 1, 0, 0, 0), + (4, 64, 2, 1, 0, 0), + (5, 65, 2, 0, 0, 0), + (6, 66, 0, 3, 1, 5), + (7, 66, 1, 0, 0, 0), + (8, 67, 0, 1.25, 0, 0), + (9, 68, 1, 2.5, 0, 0), +] +if io_states != expected_io_states: + raise SystemExit(f"unexpected source-linked M62-M68 I/O states: {io_states!r}") + +override_controls = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_override_controls.json").read_text()) +override_states = [ + (event["line"], event["reserved"], event["tool"], event["feed"]) + for event in override_controls + if event["type"] == "comment" and event["reserved"] in {50, 51, 52, 53} +] +expected_override_states = [ + (2, 50, 0, 1), + (2, 51, 0, 1), + (3, 50, 0, 0), + (3, 51, 0, 0), + (4, 50, 0, 1), + (5, 50, 0, 0), + (6, 51, 0, 1), + (7, 51, 0, 0), + (8, 52, 0, 1), + (9, 52, 0, 0), + (10, 53, 0, 1), + (11, 53, 0, 0), + (12, 50, 0, 1), +] +if override_states != expected_override_states: + raise SystemExit(f"unexpected source-linked M48-M53 override states: {override_states!r}") + +modal_state = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_modal_state.json").read_text()) +if not any( + event["type"] == "rapid" and + event["line"] == 7 and + event["end"]["x"] == 2 and + event["end"]["y"] == 0 and + event["end"]["z"] == 2 + for event in modal_state +): + raise SystemExit("missing source-linked M72 restored absolute millimeter move") +if not any( + event["type"] == "arc-feed" and + event["line"] == 8 and + event["plane"] == 17 and + event["end"]["x"] == 4 and + event["end"]["y"] == 0 + for event in modal_state +): + raise SystemExit("missing source-linked M72 restored XY plane arc") + +modal_autorestore = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_modal_autorestore.json").read_text()) +if not any( + event["type"] == "rapid" and + event["line"] == 9 and + event["end"]["x"] == 10 + for event in modal_autorestore +): + raise SystemExit("missing source-linked M73 subroutine changed modal state move") +if not any( + event["type"] == "rapid" and + event["line"] == 4 and + event["start"]["x"] == 10 and + event["end"]["x"] == 12 + for event in modal_autorestore +): + raise SystemExit("missing source-linked M73 autorestored incremental caller move") + +threading_sync = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_threading_sync.json").read_text()) +sync_states = [ + (event["line"], event["reserved"], event["feed"], event["tool"]) + for event in threading_sync + if event["type"] == "comment" and event["reserved"] in {3300, 3301, 3302} +] +expected_sync_states = [ + (4, 3301, 1, 0), + (4, 3300, 0, 0), + (6, 3301, 1, 0), + (6, 3300, 0, 0), +] +if sync_states != expected_sync_states: + raise SystemExit(f"unexpected source-linked G33/G33.1 sync states: {sync_states!r}") +if not any( + event["type"] == "linear-feed" and event["line"] == 6 and event["end"]["z"] == 0 + for event in threading_sync +): + raise SystemExit("missing source-linked G33.1 rigid tap motion line") + +threading_cycle = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_threading_cycle.json").read_text()) +if not any( + event["type"] == "comment" and + event["line"] == 4 and + event["reserved"] == 3301 and + event["feed"] == 0.05 + for event in threading_cycle +): + raise SystemExit("missing source-linked G76 threading sync start") +if not any(event["type"] == "comment" and event["line"] == 4 and event["reserved"] == 3300 for event in threading_cycle): + raise SystemExit("missing source-linked G76 threading sync stop") +if not any( + event["type"] == "linear-feed" and event["line"] == 4 and event["end"]["z"] == -1 + for event in threading_cycle +): + raise SystemExit("missing source-linked G76 final threading pass") +if not any(event["type"] == "program-end" and event["line"] == 5 for event in threading_cycle): + raise SystemExit("missing source-linked G76 program end line number") + tool_number = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_tool_number.json").read_text()) if not any( event["type"] == "tool-change" and @@ -269,6 +506,16 @@ if not any( ): raise SystemExit("missing source-linked G49 tool length clear event") +dynamic_tool_length = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_dynamic_tool_length.json").read_text()) +dynamic_offsets = [ + (event["line"], event["start"]["x"], event["start"]["z"]) + for event in dynamic_tool_length + if event["type"] == "comment" and event["reserved"] == 430 +] +expected_dynamic_offsets = [(2, 1, 2), (3, 1.25, 2.5), (4, 1.25, 15), (5, 0, 0)] +if dynamic_offsets != expected_dynamic_offsets: + raise SystemExit(f"unexpected source-linked G43.1/G43.2 tool length offsets: {dynamic_offsets!r}") + 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 = [ @@ -282,6 +529,152 @@ actual_cycle = [(event["type"], event["end"]["x"], event["end"]["y"], event["end if actual_cycle != expected_cycle: raise SystemExit(f"unexpected source-linked G81/G80 expansion: {actual_cycle!r}") +cycle_planes = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_canned_cycle_planes.json").read_text()) +if not any( + event["type"] == "linear-feed" and + event["line"] == 4 and + event["end"]["x"] == 1 and + event["end"]["y"] == -2 and + event["end"]["z"] == 4 + for event in cycle_planes +): + raise SystemExit("missing source-linked G18 G81 feed along Y depth axis") +if not any( + event["type"] == "rapid" and + event["line"] == 4 and + event["end"]["x"] == 1 and + event["end"]["y"] == 3 and + event["end"]["z"] == 4 + for event in cycle_planes +): + raise SystemExit("missing source-linked G18 G81 retract to Y R plane") +if not any( + event["type"] == "linear-feed" and + event["line"] == 7 and + event["end"]["x"] == -2 and + event["end"]["y"] == 3 and + event["end"]["z"] == 4 + for event in cycle_planes +): + raise SystemExit("missing source-linked G19 G82 feed along X depth axis") +if not any( + event["type"] == "dwell" and + event["line"] == 7 and + event["dwellSeconds"] == 0.2 + for event in cycle_planes +): + raise SystemExit("missing source-linked G19 G82 dwell at bottom") +if not any( + event["type"] == "rapid" and + event["line"] == 7 and + event["end"]["x"] == 2 and + event["end"]["y"] == 3 and + event["end"]["z"] == 4 + for event in cycle_planes +): + raise SystemExit("missing source-linked G19 G82 retract to X R plane") +def plane_cycle_signature(event): + if event["type"] in {"rapid", "linear-feed"}: + return (event["type"], event["end"]["x"], event["end"]["y"], event["end"]["z"]) + if event["type"] == "set-spindle": + return ("set-spindle", event["spindle"], event["reserved"]) + if event["type"] == "dwell": + return ("dwell", event["dwellSeconds"]) + if event["type"] == "program-end": + return ("program-end", event["reserved"]) + return None +plane_g84_sequence = [ + plane_cycle_signature(event) + for event in cycle_planes + if event["line"] == 11 and event["type"] in {"rapid", "linear-feed", "set-spindle", "dwell"} +] +expected_plane_g84_sequence = [ + ("rapid", 1, 5, 4), + ("rapid", 1, 3, 4), + ("linear-feed", -2, 4, 1), + ("set-spindle", 0, 0), + ("set-spindle", 600, 2), + ("dwell", 0.05), + ("linear-feed", 3, 4, 1), + ("set-spindle", 0, 0), + ("set-spindle", 600, 1), +] +if plane_g84_sequence != expected_plane_g84_sequence: + raise SystemExit(f"unexpected source-linked G18 G84 plane sequence: {plane_g84_sequence!r}") +plane_g74_sequence = [ + plane_cycle_signature(event) + for event in cycle_planes + if event["line"] == 15 and event["type"] in {"rapid", "linear-feed", "set-spindle", "dwell"} +] +expected_plane_g74_sequence = [ + ("rapid", 5, 3, 4), + ("rapid", 2, 3, 4), + ("linear-feed", 4, -2, 3), + ("set-spindle", 0, 0), + ("set-spindle", 500, 1), + ("dwell", 0.04), + ("linear-feed", 4, 2, 3), + ("set-spindle", 0, 0), + ("set-spindle", 500, 2), +] +if plane_g74_sequence != expected_plane_g74_sequence: + raise SystemExit(f"unexpected source-linked G19 G74 plane sequence: {plane_g74_sequence!r}") +plane_g86_sequence = [ + plane_cycle_signature(event) + for event in cycle_planes + if event["line"] == 19 and event["type"] in {"rapid", "linear-feed", "set-spindle", "dwell"} +] +expected_plane_g86_sequence = [ + ("rapid", 1, 5, 4), + ("rapid", 1, 3, 4), + ("linear-feed", 1, -2, 4), + ("dwell", 0.1), + ("set-spindle", 0, 0), + ("rapid", 1, 3, 4), + ("set-spindle", 900, 1), +] +if plane_g86_sequence != expected_plane_g86_sequence: + raise SystemExit(f"unexpected source-linked G18 G86 plane sequence: {plane_g86_sequence!r}") +plane_g88_sequence = [ + plane_cycle_signature(event) + for event in cycle_planes + if event["line"] == 23 and event["type"] in {"rapid", "linear-feed", "set-spindle", "dwell", "program-end"} +] +expected_plane_g88_sequence = [ + ("rapid", 5, 3, 4), + ("rapid", 2, 3, 4), + ("linear-feed", -2, 3, 4), + ("dwell", 0.07), + ("set-spindle", 0, 0), + ("program-end", 1), + ("set-spindle", 700, 1), +] +if plane_g88_sequence != expected_plane_g88_sequence: + raise SystemExit(f"unexpected source-linked G19 G88 plane sequence: {plane_g88_sequence!r}") +plane_g87_sequence = [ + plane_cycle_signature(event) + for event in cycle_planes + if event["line"] == 27 and event["type"] in {"rapid", "linear-feed", "set-spindle"} +] +expected_plane_g87_sequence = [ + ("rapid", 5, 1, 2), + ("rapid", 2, 1, 2), + ("rapid", 2, 1.2, 2.3), + ("set-spindle", 0, 0), + ("rapid", -4, 1.2, 2.3), + ("rapid", -4, 1, 2), + ("set-spindle", 800, 1), + ("linear-feed", -2, 1, 2), + ("linear-feed", -4, 1, 2), + ("set-spindle", 0, 0), + ("rapid", -4, 1.2, 2.3), + ("rapid", 2, 1.2, 2.3), + ("rapid", 2, 1, 2), + ("set-spindle", 800, 1), +] +if plane_g87_sequence != expected_plane_g87_sequence: + raise SystemExit(f"unexpected source-linked G19 G87 plane sequence: {plane_g87_sequence!r}") + extended_cycle = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_canned_cycles_extended.json").read_text()) if not any( event["type"] == "linear-feed" and @@ -313,6 +706,178 @@ if not any( for event in extended_cycle ): raise SystemExit("missing source-linked G83 final retract to R plane") +def g84_signature(event): + if event["type"] == "linear-feed": + return ("linear-feed", event["end"]["x"], event["end"]["z"]) + if event["type"] == "set-spindle": + return ("set-spindle", event["spindle"], event["reserved"]) + if event["type"] == "dwell": + return ("dwell", event["dwellSeconds"]) + if event["type"] == "program-end": + return ("program-end", event["reserved"]) + return None +g84_sequence = [ + g84_signature(event) + for event in extended_cycle + if event["line"] == 7 and event["type"] in {"linear-feed", "set-spindle", "dwell"} +] +expected_g84_sequence = [ + ("linear-feed", 30, -2), + ("set-spindle", 0, 0), + ("set-spindle", 600, 2), + ("dwell", 0.05), + ("linear-feed", 30, 2), + ("set-spindle", 0, 0), + ("set-spindle", 600, 1), +] +if g84_sequence != expected_g84_sequence: + raise SystemExit(f"unexpected source-linked G84 tapping sequence: {g84_sequence!r}") +g74_sequence = [ + g84_signature(event) + for event in extended_cycle + if event["line"] == 9 and event["type"] in {"linear-feed", "set-spindle", "dwell"} +] +expected_g74_sequence = [ + ("linear-feed", 40, -2), + ("set-spindle", 0, 0), + ("set-spindle", 500, 1), + ("dwell", 0.04), + ("linear-feed", 40, 2), + ("set-spindle", 0, 0), + ("set-spindle", 500, 2), +] +if g74_sequence != expected_g74_sequence: + raise SystemExit(f"unexpected source-linked G74 tapping sequence: {g74_sequence!r}") +g88_sequence = [ + g84_signature(event) + for event in extended_cycle + if event["line"] == 11 and event["type"] in {"linear-feed", "set-spindle", "dwell", "program-end"} +] +expected_g88_sequence = [ + ("linear-feed", 50, -2), + ("dwell", 0.07), + ("set-spindle", 0, 0), + ("program-end", 1), + ("set-spindle", 700, 1), +] +if g88_sequence != expected_g88_sequence: + raise SystemExit(f"unexpected source-linked G88 manual-out sequence: {g88_sequence!r}") +if any( + event["type"] == "rapid" and + event["line"] == 11 and + event["start"]["x"] == 50 and + event["start"]["z"] == -2 and + event["end"]["x"] == 50 and + event["end"]["z"] == 2 + for event in extended_cycle +): + raise SystemExit("unexpected source-linked G88 automatic retract") +def g87_signature(event): + if event["type"] in {"rapid", "linear-feed"}: + return (event["type"], event["end"]["x"], event["end"]["y"], event["end"]["z"]) + if event["type"] == "set-spindle": + return ("set-spindle", event["spindle"], event["reserved"]) + return None +g87_sequence = [ + g87_signature(event) + for event in extended_cycle + if event["line"] == 14 and event["type"] in {"rapid", "linear-feed", "set-spindle"} +] +expected_g87_sequence = [ + ("rapid", 60, 0, 5), + ("rapid", 60, 0, 2), + ("rapid", 60.2, 0.3, 2), + ("set-spindle", 0, 0), + ("rapid", 60.2, 0.3, -4), + ("rapid", 60, 0, -4), + ("set-spindle", 800, 1), + ("linear-feed", 60, 0, -2), + ("linear-feed", 60, 0, -4), + ("set-spindle", 0, 0), + ("rapid", 60.2, 0.3, -4), + ("rapid", 60.2, 0.3, 2), + ("rapid", 60, 0, 2), + ("set-spindle", 800, 1), +] +if g87_sequence != expected_g87_sequence: + raise SystemExit(f"unexpected source-linked G87 back-boring sequence: {g87_sequence!r}") + +arc_planes = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_arc_planes.json").read_text()) +if not any( + event["type"] == "arc-feed" and + event["line"] == 5 and + event["plane"] == 18 and + event["start"]["x"] == 0 and + event["start"]["z"] == 0 and + event["end"]["x"] == 10 and + event["end"]["z"] == 0 and + event["center"]["x"] == 5 and + event["center"]["z"] == 0 and + event["arcTurns"] == -1 + for event in arc_planes +): + raise SystemExit("missing source-linked G18 XZ plane arc-feed mapping") +if not any( + event["type"] == "arc-feed" and + event["line"] == 8 and + event["plane"] == 19 and + event["start"]["y"] == 0 and + event["start"]["z"] == 0 and + event["end"]["y"] == 10 and + event["end"]["z"] == 0 and + event["center"]["y"] == 5 and + event["center"]["z"] == 0 and + event["arcTurns"] == 1 + for event in arc_planes +): + raise SystemExit("missing source-linked G19 YZ plane arc-feed mapping") + +arc_distance = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_arc_distance_modes.json").read_text()) +arc_centers = [ + (event["line"], event["center"]["x"], event["center"]["y"]) + for event in arc_distance + if event["type"] == "arc-feed" +] +expected_arc_centers = [(5, 5, 0), (7, 15, 0), (9, 25, 0)] +if arc_centers != expected_arc_centers: + raise SystemExit(f"unexpected source-linked G90.1/G91.1 arc centers: {arc_centers!r}") + +predefined = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_predefined_positions.json").read_text()) +predefined_rapids = [ + ( + event["line"], + event["start"]["x"], event["start"]["y"], event["start"]["z"], + event["end"]["x"], event["end"]["y"], event["end"]["z"], + ) + for event in predefined + if event["type"] == "rapid" and event["line"] in {5, 9} +] +expected_predefined_rapids = [ + (5, 10, 20, 30, 10, 20, 5), + (5, 10, 20, 5, 10, 20, 3), + (9, 40, 50, 60, 40, 50, 60), + (9, 40, 50, 60, 4, 5, 6), +] +if predefined_rapids != expected_predefined_rapids: + raise SystemExit(f"unexpected source-linked G28/G30 rapid sequence: {predefined_rapids!r}") + +machine_coords = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_machine_coordinates.json").read_text()) +machine_moves = [ + ( + event["type"], event["line"], + event["start"]["x"], event["start"]["y"], event["start"]["z"], + event["end"]["x"], event["end"]["y"], event["end"]["z"], + ) + for event in machine_coords + if event["type"] in {"rapid", "linear-feed"} and event["line"] in {3, 4, 6} +] +expected_machine_moves = [ + ("rapid", 3, 5, 6, 7, 0, 6, 1), + ("rapid", 4, 0, 6, 1, 2, 6, 1), + ("linear-feed", 6, 2, 6, 1, 2, 3, 1), +] +if machine_moves != expected_machine_moves: + raise SystemExit(f"unexpected source-linked G53 machine-coordinate moves: {machine_moves!r}") cutter_comp = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_cutter_comp.json").read_text()) cutter_comp_motions = [ @@ -343,17 +908,29 @@ if actual_cutter_comp != expected_cutter_comp: raise SystemExit(f"unexpected source-linked cutter compensation path: {actual_cutter_comp!r}") probe = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_probe_no_error.json").read_text()) +probe_events = [ + (event["line"], event["reserved"], event["start"]["z"], event["end"]["z"]) + for event in probe + if event["type"] == "probe" +] +expected_probe_events = [ + (4, 0, 5, 4), + (6, 1, 5, 3), + (8, 2, 5, 2), + (10, 3, 5, 1), + (12, 1, 5, 0), +] +if probe_events != expected_probe_events: + raise SystemExit(f"unexpected source-linked G38 probe variants: {probe_events!r}") +if any(event["type"] == "linear-feed" and event["line"] in {4, 6, 8, 10, 12} for event in probe): + raise SystemExit("source-linked G38 probe moves should not be emitted as linear-feed") if not any( - event["type"] == "probe" and - event["line"] == 4 and - event["feed"] == 100 and - event["start"]["z"] == 5 and - event["end"]["z"] == 0 + event["type"] == "linear-feed" and + event["line"] == 14 and + event["end"]["x"] == 1 for event in probe ): - raise SystemExit("missing source-linked G38.3 probe event") -if any(event["type"] == "linear-feed" and event["line"] == 4 for event in probe): - raise SystemExit("source-linked G38.3 probe should not be emitted as linear-feed") + raise SystemExit("missing source-linked #5070 probe-tripped conditional motion") oword = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_oword_subprogram.json").read_text()) if not any( @@ -380,6 +957,161 @@ if any( ): raise SystemExit("source-linked O-word return should skip remaining subprogram body") +named_oword = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_named_oword_subprogram.json").read_text()) +if not any( + event["type"] == "linear-feed" and + event["line"] == 9 and + event["start"]["x"] == 0 and + event["end"]["x"] == 10 + for event in named_oword +): + raise SystemExit("missing source-linked named O-word expression motion") +if not any( + event["type"] == "linear-feed" and + event["line"] == 10 and + event["start"]["y"] == 0 and + event["end"]["y"] == 6 + for event in named_oword +): + raise SystemExit("missing source-linked named O-word #2 argument motion") +if not any( + event["type"] == "linear-feed" and + event["line"] == 5 and + event["start"]["x"] == 10 and + event["end"]["x"] == 20 + for event in named_oword +): + raise SystemExit("missing source-linked named O-word return-to-caller motion") +if any( + event["type"] == "linear-feed" and + event["line"] == 12 and + event["end"]["x"] == 99 + for event in named_oword +): + raise SystemExit("source-linked named O-word return should skip remaining subprogram body") + +oword_flow = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_oword_control_flow.json").read_text()) +if not any(event["type"] == "linear-feed" and event["line"] == 7 and event["end"]["x"] == 2 for event in oword_flow): + raise SystemExit("missing source-linked O-word elseif branch motion") +if any( + event["type"] == "linear-feed" and event["line"] in {5, 9} and event["end"]["x"] in {98, 99} + for event in oword_flow +): + raise SystemExit("source-linked O-word conditional should skip inactive branches") +while_values = [ + event["end"]["y"] + for event in oword_flow + if event["type"] == "linear-feed" and event["line"] == 13 +] +if while_values != [0, 1, 2]: + raise SystemExit(f"unexpected source-linked O-word while values: {while_values!r}") +repeat_values = [ + event["end"]["z"] + for event in oword_flow + if event["type"] == "linear-feed" and event["line"] == 19 +] +if repeat_values != [1, 2, 3]: + raise SystemExit(f"unexpected source-linked O-word repeat values: {repeat_values!r}") +do_values = [ + event["end"]["a"] + for event in oword_flow + if event["type"] == "linear-feed" and event["line"] == 24 +] +if do_values != [1, 2, 3]: + raise SystemExit(f"unexpected source-linked O-word do/while values: {do_values!r}") +break_values = [ + event["end"]["b"] + for event in oword_flow + if event["type"] == "linear-feed" and event["line"] == 32 +] +if break_values != [1]: + raise SystemExit(f"unexpected source-linked O-word do/break body values: {break_values!r}") +if not any( + event["type"] == "linear-feed" and + event["line"] == 34 and + event["end"]["c"] == 2 + for event in oword_flow +): + raise SystemExit("missing source-linked O-word do/break final loop counter motion") +continue_values = [ + event["end"]["u"] + for event in oword_flow + if event["type"] == "linear-feed" and event["line"] == 43 +] +if continue_values != [2, 3]: + raise SystemExit(f"unexpected source-linked O-word do/continue body values: {continue_values!r}") +if not any( + event["type"] == "linear-feed" and + event["line"] == 45 and + event["end"]["v"] == 3 + for event in oword_flow +): + raise SystemExit("missing source-linked O-word do/continue final loop counter motion") +if not any( + event["type"] == "linear-feed" and + event["line"] == 46 and + event["end"]["w"] == 2 + for event in oword_flow +): + raise SystemExit("missing source-linked O-word do/continue skipped-body counter motion") + +exists = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_named_parameter_exists.json").read_text()) +exists_moves = [ + (event["line"], event["end"]["x"], event["end"]["y"]) + for event in exists + if event["type"] == "linear-feed" +] +expected_exists_moves = [(5, 1, 0), (8, 1, 1)] +if exists_moves != expected_exists_moves: + raise SystemExit(f"unexpected source-linked EXISTS named parameter branch motions: {exists_moves!r}") + +indexed = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_indexed_parameters.json").read_text()) +indexed_moves = [ + ( + event["line"], + event["end"]["x"], event["end"]["y"], event["end"]["z"], + event["end"]["a"], event["end"]["b"], event["end"]["c"], + event["end"]["u"], event["end"]["v"], event["end"]["w"], + ) + for event in indexed + if event["type"] == "linear-feed" +] +expected_indexed_moves = [ + (7, 17, 0, 0, 0, 0, 0, 0, 0, 0), + (8, 17, 22, 0, 0, 0, 0, 0, 0, 0), + (10, 17, 22, 1, 0, 0, 0, 0, 0, 0), + (13, 17, 22, 1, 21, 0, 0, 0, 0, 0), + (14, 17, 22, 1, 21, 9, 0, 0, 0, 0), + (16, 17, 22, 1, 21, 9, 9, 2, 0, 0), + (17, 17, 22, 1, 21, 9, 9, 2, 4, 9), + (18, 4, 22, 1, 21, 9, 9, 2, 4, 9), + (19, 4, 12, 4, 21, 9, 9, 2, 4, 9), + (20, 4, 12, 4, 12, 9, 9, 2, 4, 9), + (21, 4, 12, 4, 12, 14, 9, 2, 4, 9), +] +if indexed_moves != expected_indexed_moves: + raise SystemExit(f"unexpected source-linked indexed/indirect parameter motions: {indexed_moves!r}") + +normalized = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_named_parameter_normalization.json").read_text()) +normalized_moves = [ + (event["line"], event["end"]["x"], event["end"]["y"]) + for event in normalized + if event["type"] == "linear-feed" +] +expected_normalized_moves = [(4, 12, 0), (6, 12, 1)] +if normalized_moves != expected_normalized_moves: + raise SystemExit(f"unexpected source-linked normalized named parameter motions: {normalized_moves!r}") + +ini_named = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_ini_named_parameter.json").read_text()) +ini_named_moves = [ + (event["line"], event["end"]["x"], event["end"]["y"]) + for event in ini_named + if event["type"] == "linear-feed" +] +expected_ini_named_moves = [(3, 3.25, 0), (5, 3.25, 1)] +if ini_named_moves != expected_ini_named_moves: + raise SystemExit(f"unexpected source-linked INI named parameter motions: {ini_named_moves!r}") + coords = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_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"] @@ -419,22 +1151,116 @@ if not any( raise SystemExit("missing source-linked G10 L20 P2 offset event after G55 selection") if not any(event["type"] == "program-end" and event["line"] == 5 for event in l20): raise SystemExit("missing source-linked G10 L20 program end line number") + +p0 = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_coordinate_p0.json").read_text()) +if not any( + event["type"] == "set-g5x-offset" and + event["line"] == 3 and + event["tool"] == 2 and + event["start"]["x"] == 7 and + event["start"]["y"] == 8 and + event["start"]["z"] == 9 + for event in p0 +): + raise SystemExit("missing source-linked G10 L2 P0 active G55 offset event") +if not any(event["type"] == "program-end" and event["line"] == 4 for event in p0): + raise SystemExit("missing source-linked G10 P0 program end line number") + +select_all = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_coordinate_select_all.json").read_text()) +select_events = [ + (event["line"], event["tool"], event["start"]["x"]) + for event in select_all + if event["type"] == "set-g5x-offset" and event["line"] in set(range(11, 20)) +] +expected_select_events = [ + (11, 2, 2), + (12, 1, 1), + (13, 3, 3), + (14, 4, 4), + (15, 5, 5), + (16, 6, 6), + (17, 7, 7), + (18, 8, 8), + (19, 9, 9), +] +if select_events != expected_select_events: + raise SystemExit(f"unexpected source-linked G54-G59.3 selection events: {select_events!r}") + +g92_restore = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_g92_restore.json").read_text()) +g92_states = [ + (event["line"], event["start"]["x"], event["start"]["y"], event["start"]["z"]) + for event in g92_restore + if event["type"] == "set-g92-offset" and event["line"] > 0 +] +expected_g92_states = [ + (2, -1, -2, -3), + (3, 0, 0, 0), + (4, -1, -2, -3), + (5, 0, 0, 0), + (6, 0, 0, 0), +] +if g92_states != expected_g92_states: + raise SystemExit(f"unexpected source-linked G92.2/G92.3 restore sequence: {g92_states!r}") +if not any(event["type"] == "program-end" and event["line"] == 7 for event in g92_restore): + raise SystemExit("missing source-linked G92 restore program end line number") + +g52_offset = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_g52_offset.json").read_text()) +g52_states = [ + (event["line"], event["start"]["x"], event["start"]["y"], event["start"]["z"]) + for event in g52_offset + if event["type"] == "set-g92-offset" and event["line"] > 0 +] +expected_g52_states = [ + (2, 1, 2, 0), + (3, 1, 3, 0), + (4, 0, 0, 0), + (5, 1, 3, 0), + (6, 0, 0, 0), +] +if g52_states != expected_g52_states: + raise SystemExit(f"unexpected source-linked G52 shared offset sequence: {g52_states!r}") +if not any(event["type"] == "program-end" and event["line"] == 7 for event in g52_offset): + raise SystemExit("missing source-linked G52 offset program end line number") 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_spindle_direction.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_spindle_modes.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_spindle_orient.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_io_controls.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_override_controls.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_modal_state.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_modal_autorestore.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_threading_sync.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_threading_cycle.json" echo "dumped /tmp/cnc_sim_linuxcnc_source_tool_number.json" echo "dumped /tmp/cnc_sim_linuxcnc_source_program_stops.json" echo "dumped /tmp/cnc_sim_linuxcnc_source_coolant.json" echo "dumped /tmp/cnc_sim_linuxcnc_source_feed_modes.json" echo "dumped /tmp/cnc_sim_linuxcnc_source_motion_modes.json" echo "dumped /tmp/cnc_sim_linuxcnc_source_tool_length.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_dynamic_tool_length.json" echo "dumped /tmp/cnc_sim_linuxcnc_source_canned_cycle.json" echo "dumped /tmp/cnc_sim_linuxcnc_source_canned_cycles_extended.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_canned_cycle_planes.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_arc_planes.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_arc_distance_modes.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_predefined_positions.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_machine_coordinates.json" echo "dumped /tmp/cnc_sim_linuxcnc_source_cutter_comp.json" echo "dumped /tmp/cnc_sim_linuxcnc_source_probe_no_error.json" echo "dumped /tmp/cnc_sim_linuxcnc_source_oword_subprogram.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_named_oword_subprogram.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_oword_control_flow.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_named_parameter_exists.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_indexed_parameters.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_named_parameter_normalization.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_ini_named_parameter.json" echo "dumped /tmp/cnc_sim_linuxcnc_source_coordinate_offsets.json" echo "dumped /tmp/cnc_sim_linuxcnc_source_coordinate_l20.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_coordinate_p0.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_coordinate_select_all.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_g92_restore.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_g52_offset.json" diff --git a/tests/gcode/linuxcnc_arc_distance_modes.ngc b/tests/gcode/linuxcnc_arc_distance_modes.ngc new file mode 100644 index 0000000..e42bbb7 --- /dev/null +++ b/tests/gcode/linuxcnc_arc_distance_modes.ngc @@ -0,0 +1,10 @@ +G21 G90 G17 +G0 X0 Y0 Z0 +F100 +G91.1 +G3 X10 Y0 I5 J0 +G90.1 +G3 X20 Y0 I15 J0 +G91.1 +G3 X30 Y0 I5 J0 +M30 diff --git a/tests/gcode/linuxcnc_arc_planes.ngc b/tests/gcode/linuxcnc_arc_planes.ngc new file mode 100644 index 0000000..f43c291 --- /dev/null +++ b/tests/gcode/linuxcnc_arc_planes.ngc @@ -0,0 +1,9 @@ +G21 G90 G17 +G0 X0 Y0 Z0 +F100 +G18 +G2 X10 Z0 I5 K0 +G0 X0 Y0 Z0 +G19 +G3 Y10 Z0 J5 K0 +M30 diff --git a/tests/gcode/linuxcnc_canned_cycle_planes.ngc b/tests/gcode/linuxcnc_canned_cycle_planes.ngc new file mode 100644 index 0000000..79cedfb --- /dev/null +++ b/tests/gcode/linuxcnc_canned_cycle_planes.ngc @@ -0,0 +1,28 @@ +G21 G90 +G18 +G0 X0 Y5 Z0 +G99 G81 X1 Y-2 Z4 R3 F100 +G19 +G0 X5 Y0 Z0 +G99 G82 X-2 Y3 Z4 R2 P0.2 F100 +G18 +S600 M3 +G0 X0 Y5 Z0 +G99 G84 X1 Y-2 Z4 R3 P0.05 F60 +G19 +S500 M4 +G0 X5 Y0 Z0 +G99 G74 X-2 Y3 Z4 R2 P0.04 F50 +G18 +S900 M3 +G0 X0 Y5 Z0 +G99 G86 X1 Y-2 Z4 R3 P0.1 F80 +G19 +S700 M3 +G0 X5 Y0 Z0 +G99 G88 X-2 Y3 Z4 R2 P0.07 F70 +G19 +S800 M3 +G0 X5 Y0 Z0 +G99 G87 X-4 Y1 Z2 R2 I-2 J0.2 K0.3 F80 +M30 diff --git a/tests/gcode/linuxcnc_canned_cycles_extended.ngc b/tests/gcode/linuxcnc_canned_cycles_extended.ngc index fd24e28..d6a1ded 100644 --- a/tests/gcode/linuxcnc_canned_cycles_extended.ngc +++ b/tests/gcode/linuxcnc_canned_cycles_extended.ngc @@ -3,4 +3,13 @@ G0 X0 Y0 Z10 G99 G82 X10 Y0 Z-2 R3 P0.25 F100 G83 X20 Y0 Z-3 R2 Q1 G80 +S600 M3 +G99 G84 X30 Y0 Z-2 R2 P0.05 F60 +S500 M4 +G99 G74 X40 Y0 Z-2 R2 P0.04 F50 +S700 M3 +G99 G88 X50 Y0 Z-2 R2 P0.07 F70 +G0 X0 Y0 Z5 +S800 M3 +G99 G87 X60 Y0 Z-4 R2 I0.2 J0.3 K-2 F80 M30 diff --git a/tests/gcode/linuxcnc_coordinate_p0.ngc b/tests/gcode/linuxcnc_coordinate_p0.ngc new file mode 100644 index 0000000..b9e6d9c --- /dev/null +++ b/tests/gcode/linuxcnc_coordinate_p0.ngc @@ -0,0 +1,4 @@ +G21 G90 G17 +G55 +G10 L2 P0 X7 Y8 Z9 +M30 diff --git a/tests/gcode/linuxcnc_coordinate_select_all.ngc b/tests/gcode/linuxcnc_coordinate_select_all.ngc new file mode 100644 index 0000000..dd2572e --- /dev/null +++ b/tests/gcode/linuxcnc_coordinate_select_all.ngc @@ -0,0 +1,20 @@ +G21 G90 +G10 L2 P1 X1 +G10 L2 P2 X2 +G10 L2 P3 X3 +G10 L2 P4 X4 +G10 L2 P5 X5 +G10 L2 P6 X6 +G10 L2 P7 X7 +G10 L2 P8 X8 +G10 L2 P9 X9 +G55 +G54 +G56 +G57 +G58 +G59 +G59.1 +G59.2 +G59.3 +M30 diff --git a/tests/gcode/linuxcnc_dynamic_tool_length.ngc b/tests/gcode/linuxcnc_dynamic_tool_length.ngc new file mode 100644 index 0000000..a0c4b1a --- /dev/null +++ b/tests/gcode/linuxcnc_dynamic_tool_length.ngc @@ -0,0 +1,6 @@ +G21 G90 G17 +G43.1 X1 Z2 +G43.2 X0.25 Z0.5 +G43.2 H1 +G49 +M30 diff --git a/tests/gcode/linuxcnc_g52_offset.ngc b/tests/gcode/linuxcnc_g52_offset.ngc new file mode 100644 index 0000000..93ae126 --- /dev/null +++ b/tests/gcode/linuxcnc_g52_offset.ngc @@ -0,0 +1,7 @@ +G21 G90 G17 +G52 X1 Y2 +G52 Y3 +G92.2 +G92.3 +G92.1 +M30 diff --git a/tests/gcode/linuxcnc_g92_restore.ngc b/tests/gcode/linuxcnc_g92_restore.ngc new file mode 100644 index 0000000..5013ad6 --- /dev/null +++ b/tests/gcode/linuxcnc_g92_restore.ngc @@ -0,0 +1,7 @@ +G21 G90 G17 +G92 X1 Y2 Z3 +G92.2 +G92.3 +G92.1 +G92.3 +M30 diff --git a/tests/gcode/linuxcnc_indexed_parameters.ngc b/tests/gcode/linuxcnc_indexed_parameters.ngc new file mode 100644 index 0000000..9d9b7a7 --- /dev/null +++ b/tests/gcode/linuxcnc_indexed_parameters.ngc @@ -0,0 +1,22 @@ +G21 G90 +F100 +#2 = 3 +#3 = 11 +#4 = [#[1 + 2] + ##2] +#[1 + 2] = 17 +G1 X#[1 + 2] +G1 Y#4 +O10 if [##2 EQ 17] +G1 Z1 +O10 endif +#5 = 21 +#5 = 9 G1 A#5 +G1 B#5 +#6 = 2 +#5 = 4 #6 = #5 G1 C#5 U#6 +G1 V#5 W#6 +G1 X#5 #5 = 12 #6 = #5 +G1 Y#5 Z#6 +#5 = 13 #5 = 14 G1 A#5 +G1 B#5 +M30 diff --git a/tests/gcode/linuxcnc_ini_named_parameter.ini b/tests/gcode/linuxcnc_ini_named_parameter.ini new file mode 100644 index 0000000..a344527 --- /dev/null +++ b/tests/gcode/linuxcnc_ini_named_parameter.ini @@ -0,0 +1,2 @@ +[SETUP] +XPOS = 3.25 diff --git a/tests/gcode/linuxcnc_ini_named_parameter.ngc b/tests/gcode/linuxcnc_ini_named_parameter.ngc new file mode 100644 index 0000000..5d7ab68 --- /dev/null +++ b/tests/gcode/linuxcnc_ini_named_parameter.ngc @@ -0,0 +1,7 @@ +G21 G90 +F100 +G1 X#<_ini[setup]xpos> +O10 if [EXISTS[#<_ini[SETUP]XPOS>]] +G1 Y1 +O10 endif +M30 diff --git a/tests/gcode/linuxcnc_io_controls.ngc b/tests/gcode/linuxcnc_io_controls.ngc new file mode 100644 index 0000000..2361ceb --- /dev/null +++ b/tests/gcode/linuxcnc_io_controls.ngc @@ -0,0 +1,10 @@ +G21 G90 G17 +M62 P1 +M63 P1 +M64 P2 +M65 P2 +M66 P0 L3 Q5 +M66 E1 L0 +M67 E0 Q1.25 +M68 E1 Q2.5 +M30 diff --git a/tests/gcode/linuxcnc_machine_coordinates.ngc b/tests/gcode/linuxcnc_machine_coordinates.ngc new file mode 100644 index 0000000..77e5150 --- /dev/null +++ b/tests/gcode/linuxcnc_machine_coordinates.ngc @@ -0,0 +1,7 @@ +G21 G90 G17 +G0 X5 Y6 Z7 +G53 G0 X0 Z1 +G53 X2 +G1 F100 +G53 G1 Y3 +M30 diff --git a/tests/gcode/linuxcnc_modal_autorestore.ngc b/tests/gcode/linuxcnc_modal_autorestore.ngc new file mode 100644 index 0000000..df4f3fb --- /dev/null +++ b/tests/gcode/linuxcnc_modal_autorestore.ngc @@ -0,0 +1,10 @@ +G21 G91 G17 +G0 X0 Y0 Z0 +O100 call +G0 X2 +M30 +O100 sub +M73 +G90 +G0 X10 +O100 endsub diff --git a/tests/gcode/linuxcnc_modal_invalidate.ngc b/tests/gcode/linuxcnc_modal_invalidate.ngc new file mode 100644 index 0000000..03df0e3 --- /dev/null +++ b/tests/gcode/linuxcnc_modal_invalidate.ngc @@ -0,0 +1,5 @@ +G21 G90 G17 +M70 +M71 +M72 +M30 diff --git a/tests/gcode/linuxcnc_modal_state.ngc b/tests/gcode/linuxcnc_modal_state.ngc new file mode 100644 index 0000000..3e59db0 --- /dev/null +++ b/tests/gcode/linuxcnc_modal_state.ngc @@ -0,0 +1,9 @@ +G21 G90 G17 +G0 X0 Y0 Z0 +M70 +G91 G18 G20 +G0 X1 Z1 +M72 +G0 X2 Y0 Z2 +F100 G2 X4 Y0 I1 J0 +M30 diff --git a/tests/gcode/linuxcnc_named_oword_subprogram.ngc b/tests/gcode/linuxcnc_named_oword_subprogram.ngc new file mode 100644 index 0000000..ddeae8f --- /dev/null +++ b/tests/gcode/linuxcnc_named_oword_subprogram.ngc @@ -0,0 +1,13 @@ +G21 G90 +G0 X0 Y0 Z0 +F100 +O call [4] [6] +G1 X20 +M30 +O sub +# = [#1 + #2] +G1 X# +G1 Y#2 +O return +G1 X99 +O endsub diff --git a/tests/gcode/linuxcnc_named_parameter_exists.ngc b/tests/gcode/linuxcnc_named_parameter_exists.ngc new file mode 100644 index 0000000..f6dd715 --- /dev/null +++ b/tests/gcode/linuxcnc_named_parameter_exists.ngc @@ -0,0 +1,10 @@ +G21 G90 +F100 +# = 7 +O10 if [EXISTS[#]] +G1 X1 +O10 endif +O20 if [EXISTS[#] EQ 0] +G1 Y1 +O20 endif +M30 diff --git a/tests/gcode/linuxcnc_named_parameter_normalization.ngc b/tests/gcode/linuxcnc_named_parameter_normalization.ngc new file mode 100644 index 0000000..82dde3b --- /dev/null +++ b/tests/gcode/linuxcnc_named_parameter_normalization.ngc @@ -0,0 +1,8 @@ +G21 G90 +F100 +#

= 12 +G1 X# +O10 if [EXISTS[#

]] +G1 Y1 +O10 endif +M30 diff --git a/tests/gcode/linuxcnc_override_controls.ngc b/tests/gcode/linuxcnc_override_controls.ngc new file mode 100644 index 0000000..b886d8c --- /dev/null +++ b/tests/gcode/linuxcnc_override_controls.ngc @@ -0,0 +1,12 @@ +G21 G90 G17 +M48 +M49 +M50 P1 +M50 P0 +M51 P1 +M51 P0 +M52 P1 +M52 P0 +M53 P1 +M53 P0 +M30 diff --git a/tests/gcode/linuxcnc_oword_control_flow.ngc b/tests/gcode/linuxcnc_oword_control_flow.ngc new file mode 100644 index 0000000..9f1f458 --- /dev/null +++ b/tests/gcode/linuxcnc_oword_control_flow.ngc @@ -0,0 +1,47 @@ +G21 G90 +F100 +#1 = 2 +O100 if [#1 EQ 1] +G1 X99 +O100 elseif [#1 EQ 2] +G1 X2 +O100 else +G1 X98 +O100 endif +#2 = 0 +O200 while [#2 LT 3] +G1 Y#2 +#2 = [#2 + 1] +O200 endwhile +#3 = 0 +O300 repeat [3] +#3 = [#3 + 1] +G1 Z#3 +O300 endrepeat +#4 = 0 +O400 do +#4 = [#4 + 1] +G1 A#4 +O400 while [#4 LT 3] +#5 = 0 +O410 do +#5 = [#5 + 1] +O411 if [#5 GE 2] +O410 break +O411 endif +G1 B#5 +O410 while [#5 LT 4] +G1 C#5 +#6 = 0 +#7 = 0 +O420 do +#6 = [#6 + 1] +O421 if [#6 EQ 1] +O420 continue +O421 endif +#7 = [#7 + 1] +G1 U#6 +O420 while [#6 LT 3] +G1 V#6 +G1 W#7 +M30 diff --git a/tests/gcode/linuxcnc_predefined_positions.ngc b/tests/gcode/linuxcnc_predefined_positions.ngc new file mode 100644 index 0000000..3915590 --- /dev/null +++ b/tests/gcode/linuxcnc_predefined_positions.ngc @@ -0,0 +1,10 @@ +G21 G90 G17 +G0 X1 Y2 Z3 +G28.1 +G0 X10 Y20 Z30 +G28 Z5 +G0 X4 Y5 Z6 +G30.1 +G0 X40 Y50 Z60 +G30 +M30 diff --git a/tests/gcode/linuxcnc_probe_no_error.ngc b/tests/gcode/linuxcnc_probe_no_error.ngc index 83d8476..403a720 100644 --- a/tests/gcode/linuxcnc_probe_no_error.ngc +++ b/tests/gcode/linuxcnc_probe_no_error.ngc @@ -1,5 +1,16 @@ G21 G90 G17 G0 X0 Y0 Z5 F100 +G38.2 Z4 +G0 Z5 +G38.3 Z3 +G0 Z5 +G38.4 Z2 +G0 Z5 +G38.5 Z1 +G0 Z5 G38.3 Z0 +O10 if [#5070 EQ 1] +G1 X1 +O10 endif M30 diff --git a/tests/gcode/linuxcnc_spindle_modes.ngc b/tests/gcode/linuxcnc_spindle_modes.ngc new file mode 100644 index 0000000..fe1f815 --- /dev/null +++ b/tests/gcode/linuxcnc_spindle_modes.ngc @@ -0,0 +1,5 @@ +G21 G90 G17 +G97 S1000 +G96 S120 D2500 +G97 S900 +M30 diff --git a/tests/gcode/linuxcnc_spindle_orient.ngc b/tests/gcode/linuxcnc_spindle_orient.ngc new file mode 100644 index 0000000..29d9928 --- /dev/null +++ b/tests/gcode/linuxcnc_spindle_orient.ngc @@ -0,0 +1,3 @@ +G21 G90 G17 +M19 R45 P1 Q2 +M30 diff --git a/tests/gcode/linuxcnc_threading_cycle.ngc b/tests/gcode/linuxcnc_threading_cycle.ngc new file mode 100644 index 0000000..6673655 --- /dev/null +++ b/tests/gcode/linuxcnc_threading_cycle.ngc @@ -0,0 +1,5 @@ +G20 G90 G18 +S500 M3 +G0 X1 Z0.1 +G76 P0.05 Z-1 I-.075 J0.008 K0.045 Q29.5 L2 E0.045 +M30 diff --git a/tests/gcode/linuxcnc_threading_sync.ngc b/tests/gcode/linuxcnc_threading_sync.ngc new file mode 100644 index 0000000..5f87a26 --- /dev/null +++ b/tests/gcode/linuxcnc_threading_sync.ngc @@ -0,0 +1,7 @@ +G21 G90 G17 +S500 M3 +G0 Z5 +G33 Z0 K1 +G0 Z5 +G33.1 Z0 K1 +M30