#!/usr/bin/env bash set -euo pipefail cd "$(dirname "$0")" linuxcnc_root=${LINUXCNC_ROOT:-../linuxcnc} cxx=${CXX:-g++} build_dir=${TMPDIR:-/tmp}/cnc_sim_native mkdir -p "$build_dir" var_file="$build_dir/rs274ngc.var" base_var_file="$build_dir/rs274ngc-base.var" cp "$linuxcnc_root/tests/halui/jogging/sim.var" "$base_var_file" cp "$base_var_file" "$var_file" "$cxx" -std=c++17 \ $(python3.13-config --includes 2>/dev/null || python3-config --includes) \ -I core/include \ -I core/src \ -I "$linuxcnc_root/src" \ -I "$linuxcnc_root/src/emc" \ -I "$linuxcnc_root/src/emc/nml_intf" \ -I "$linuxcnc_root/src/emc/rs274ngc" \ -I "$linuxcnc_root/src/emc/motion" \ -I "$linuxcnc_root/include" \ core/src/canon_event_sink.cpp \ core/src/linuxcnc_canon_bridge.cpp \ core/src/linuxcnc_tooldata_fixture.cpp \ core/src/rtcp_kinematics.cpp \ core/src/simulator_gcode_controls.cpp \ core/tools/linuxcnc_rs274_dump.cpp \ -L "$linuxcnc_root/lib" \ -Wl,-rpath,"$linuxcnc_root/lib" \ -lrs274 \ -ltooldata \ -lpython3.13 \ -o "$build_dir/linuxcnc_rs274_dump" CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_basic_motion.ngc >/tmp/cnc_sim_linuxcnc_basic_motion.json CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/basic_mill.ngc >/tmp/cnc_sim_linuxcnc_basic_mill.json CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_rtcp_controls.ngc >/tmp/cnc_sim_linuxcnc_rtcp_controls.json CNC_SIM_RS274_VAR="$var_file" "$build_dir/linuxcnc_rs274_dump" tests/gcode/linuxcnc_spindle_direction.ngc >/tmp/cnc_sim_linuxcnc_spindle_direction.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_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_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 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 python3 - <<'PY' import json from pathlib import Path def check(path, required): events = json.loads(Path(path).read_text()) types = [event["type"] for event in events] missing = sorted(required - set(types)) if missing: raise SystemExit(f"{path}: missing events: {', '.join(missing)}") final_motion = [event for event in events if event["type"] in {"rapid", "linear-feed", "arc-feed"}][-1] if final_motion["end"]["x"] != 0 or final_motion["end"]["y"] != 0: raise SystemExit(f"{path}: unexpected final XY position") return events required_motion = {"set-units", "set-plane", "set-spindle", "rapid", "set-feed", "linear-feed", "arc-feed", "dwell", "program-end"} check("/tmp/cnc_sim_linuxcnc_basic_motion.json", required_motion) check("/tmp/cnc_sim_linuxcnc_basic_mill.json", required_motion | {"tool-change"}) controls = json.loads(Path("/tmp/cnc_sim_linuxcnc_rtcp_controls.json").read_text()) kin = [event for event in controls if event["type"] == "kinematics-switch"] if [(event["reserved"], event["feed"]) for event in kin] != [(1, 1), (0, 0), (2, 1)]: raise SystemExit("unexpected kinematics switch sequence") rtcp = [event for event in controls if event["type"] == "rtcp-state"] if [(event["line"], event["tool"], event["feed"]) for event in rtcp] != [(3, 0, 0), (4, 7, 1)]: raise SystemExit("unexpected RTCP state sequence") spindle = json.loads(Path("/tmp/cnc_sim_linuxcnc_spindle_direction.json").read_text()) spindle_states = [ (event["line"], event["spindle"], event["reserved"]) for event in spindle if event["type"] == "set-spindle" ] if (2, 1200, 1) not in spindle_states: raise SystemExit("missing M3 clockwise spindle state") if (3, 1200, 2) not in spindle_states: raise SystemExit("missing M4 counterclockwise spindle state") if (4, 0, 0) not in spindle_states: raise SystemExit("missing M5 stopped spindle state") tool_number = json.loads(Path("/tmp/cnc_sim_linuxcnc_tool_number.json").read_text()) if not any( event["type"] == "tool-change" and event["line"] == 2 and event["tool"] == 1 for event in tool_number ): raise SystemExit("missing M61 Q1 tool-number event line") stops = json.loads(Path("/tmp/cnc_sim_linuxcnc_program_stops.json").read_text()) stop_states = [ (event["line"], event["reserved"]) for event in stops if event["type"] == "program-end" ] for expected in [(2, 1), (3, 2), (4, 3), (4, 1), (5, 0)]: if expected not in stop_states: raise SystemExit(f"missing program stop state {expected!r}") coolant = json.loads(Path("/tmp/cnc_sim_linuxcnc_coolant.json").read_text()) coolant_states = [ (event["line"], event["reserved"]) for event in coolant if event["type"] == "comment" ] for expected in [(2, 11), (3, 21), (4, 10), (4, 20)]: if expected not in coolant_states: raise SystemExit(f"missing coolant state {expected!r}") feed_modes = json.loads(Path("/tmp/cnc_sim_linuxcnc_feed_modes.json").read_text()) feed_mode_comments = [ (event["line"], event["reserved"]) for event in feed_modes if event["type"] == "comment" ] for expected in [(2, 93), (3, 94), (4, 95)]: if expected not in feed_mode_comments: raise SystemExit(f"missing feed mode comment state {expected!r}") feed_mode_events = [ (event["line"], event["reserved"]) for event in feed_modes if event["type"] == "set-feed" ] for expected in [(3, 0), (4, 0)]: if expected not in feed_mode_events: raise SystemExit(f"missing feed mode event {expected!r}") motion_modes = json.loads(Path("/tmp/cnc_sim_linuxcnc_motion_modes.json").read_text()) motion_mode_states = [ (event["line"], event["reserved"], event["feed"]) for event in motion_modes if event["type"] == "comment" ] for expected in [(2, 611, 0), (3, 612, 0), (4, 640, 0.05)]: if expected not in motion_mode_states: raise SystemExit(f"missing motion mode state {expected!r}") tool_length = json.loads(Path("/tmp/cnc_sim_linuxcnc_tool_length.json").read_text()) if not any( event["type"] == "comment" and event["line"] == 2 and event["reserved"] == 430 and event["start"]["z"] == 0 for event in tool_length ): raise SystemExit("missing G43 H1 tool length offset event") if not any( event["type"] == "comment" and event["line"] == 3 and event["reserved"] == 430 and event["start"]["z"] == 0 for event in tool_length ): raise SystemExit("missing G49 tool length clear event") cycle = json.loads(Path("/tmp/cnc_sim_linuxcnc_canned_cycle.json").read_text()) motions = [event for event in cycle if event["type"] in {"rapid", "linear-feed"}] expected_cycle = [ ("rapid", 0, 0, 5), ("rapid", 20, 0, 5), ("rapid", 20, 0, 3), ("linear-feed", 20, 0, -2), ("rapid", 20, 0, 3), ] actual_cycle = [(event["type"], event["end"]["x"], event["end"]["y"], event["end"]["z"]) for event in motions] if actual_cycle != expected_cycle: raise SystemExit(f"unexpected G81/G80 expansion: {actual_cycle!r}") extended_cycle = json.loads(Path("/tmp/cnc_sim_linuxcnc_canned_cycles_extended.json").read_text()) if not any( event["type"] == "linear-feed" and event["line"] == 3 and event["end"]["x"] == 10 and event["end"]["z"] == -2 for event in extended_cycle ): raise SystemExit("missing G82 feed to bottom") if not any( event["type"] == "dwell" and event["line"] == 3 and event["dwellSeconds"] == 0.25 for event in extended_cycle ): raise SystemExit("missing G82 dwell at bottom") g83_feed_depths = [ event["end"]["z"] for event in extended_cycle if event["type"] == "linear-feed" and event["line"] == 4 ] if g83_feed_depths != [1, 0, -1, -2, -3]: raise SystemExit(f"unexpected G83 peck depths: {g83_feed_depths!r}") if not any( event["type"] == "rapid" and event["line"] == 4 and event["end"]["x"] == 20 and event["end"]["z"] == 2 for event in extended_cycle ): raise SystemExit("missing G83 final retract to R plane") cutter_comp = json.loads(Path("/tmp/cnc_sim_linuxcnc_cutter_comp.json").read_text()) cutter_comp_motions = [ event for event in cutter_comp if event["type"] in {"rapid", "linear-feed", "arc-feed"} ] expected_cutter_comp = [ ("rapid", 2, 0, 0), ("linear-feed", 6, 17, 3), ("linear-feed", 7, 17, 20), ("linear-feed", 8, 0, 0), ("linear-feed", 11, 17, -3), ("linear-feed", 12, 17, -20), ("linear-feed", 13, 0, 0), ("linear-feed", 16, 17, 3), ("linear-feed", 17, 17, 20), ("linear-feed", 18, 0, 0), ("linear-feed", 21, 17, -3), ("linear-feed", 22, 17, -20), ("linear-feed", 23, 0, 0), ] actual_cutter_comp = [ (event["type"], event["line"], event["end"]["x"], event["end"]["y"]) for event in cutter_comp_motions ] if actual_cutter_comp != expected_cutter_comp: raise SystemExit(f"unexpected cutter compensation path: {actual_cutter_comp!r}") comments = json.loads(Path("/tmp/cnc_sim_linuxcnc_comments.json").read_text()) if not any(event["type"] == "comment" and event["line"] == 2 for event in comments): raise SystemExit("missing LinuxCNC comment line event") probe = json.loads(Path("/tmp/cnc_sim_linuxcnc_probe_no_error.json").read_text()) if not any( event["type"] == "probe" and event["line"] == 4 and event["feed"] == 100 and event["start"]["z"] == 5 and event["end"]["z"] == 0 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") oword = json.loads(Path("/tmp/cnc_sim_linuxcnc_oword_subprogram.json").read_text()) if not any( event["type"] == "linear-feed" and event["line"] == 8 and event["start"]["x"] == 0 and event["end"]["x"] == 7 for event in oword ): raise SystemExit("missing LinuxCNC O-word expression motion") if not any( event["type"] == "linear-feed" and event["line"] == 9 and event["start"]["y"] == 0 and event["end"]["y"] == 2 for event in oword ): raise SystemExit("missing LinuxCNC O-word #2 argument motion") if any( event["type"] == "linear-feed" and event["line"] == 11 and event["end"]["x"] == 99 for event in oword ): raise SystemExit("LinuxCNC O-word return should skip remaining subprogram body") coords = json.loads(Path("/tmp/cnc_sim_linuxcnc_coordinate_offsets.json").read_text()) g5x = [event for event in coords if event["type"] == "set-g5x-offset"] g92 = [event for event in coords if event["type"] == "set-g92-offset"] rot = [event for event in coords if event["type"] == "set-xy-rotation"] if not any( event["line"] == 2 and event["tool"] == 1 and event["start"]["x"] == 12.5 and event["start"]["y"] == -3 and event["start"]["z"] == 4 for event in g5x ): raise SystemExit("missing G10 L2 G5X offset event") if not any(event["feed"] == 30 for event in rot): raise SystemExit("missing G10 L2 XY rotation event") if not any( event["line"] == 4 and event["start"]["x"] == 0 and event["start"]["y"] == 0 and event["start"]["z"] == 0 for event in g92 ): raise SystemExit("missing G92.1 clear offset event") if not any(event["type"] == "program-end" and event["line"] == 5 for event in coords): raise SystemExit("missing coordinate program end line number") l20 = json.loads(Path("/tmp/cnc_sim_linuxcnc_coordinate_l20.json").read_text()) if not any( event["type"] == "set-g5x-offset" and event["line"] == 4 and event["tool"] == 2 and event["start"]["x"] == 4 and event["start"]["y"] == 4 and event["start"]["z"] == 4 for event in l20 ): 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") PY echo "linuxcnc rs274 native smoke passed" echo "dumped /tmp/cnc_sim_linuxcnc_basic_motion.json" echo "dumped /tmp/cnc_sim_linuxcnc_basic_mill.json" echo "dumped /tmp/cnc_sim_linuxcnc_rtcp_controls.json" echo "dumped /tmp/cnc_sim_linuxcnc_spindle_direction.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_canned_cycle.json" echo "dumped /tmp/cnc_sim_linuxcnc_canned_cycles_extended.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_coordinate_offsets.json" echo "dumped /tmp/cnc_sim_linuxcnc_coordinate_l20.json"