diff --git a/core/src/canon_event_sink.cpp b/core/src/canon_event_sink.cpp index 1fb874a..629149f 100644 --- a/core/src/canon_event_sink.cpp +++ b/core/src/canon_event_sink.cpp @@ -1,7 +1,23 @@ #include "canon_event_sink.h" +#include + #include "rtcp_kinematics.h" +namespace { + +void rotate_xy(double *x, double *y, double degrees) { + const double radians = degrees * std::acos(-1.0) / 180.0; + const double c = std::cos(radians); + const double s = std::sin(radians); + const double nx = *x * c - *y * s; + const double ny = *x * s + *y * c; + *x = nx; + *y = ny; +} + +} // namespace + void CanonEventSink::reset() { position_ = {}; plane_ = 17; @@ -14,6 +30,7 @@ void CanonEventSink::reset() { probe_position_ = {}; probe_tripped_ = false; tool_length_offset_ = {}; + xy_rotation_degrees_ = 0.0; callback_status_ = 0; kinematics_type_ = 1; feed_override_ = false; @@ -62,6 +79,22 @@ void CanonEventSink::set_tool_length_offset(const CncSimPose &offset) { tool_length_offset_ = offset; } +void CanonEventSink::apply_tool_length_offset(const CncSimPose &offset) { + double dx = tool_length_offset_.x - offset.x; + double dy = tool_length_offset_.y - offset.y; + rotate_xy(&dx, &dy, -xy_rotation_degrees_); + position_.x += dx; + position_.y += dy; + position_.z += tool_length_offset_.z - offset.z; + position_.a += tool_length_offset_.a - offset.a; + position_.b += tool_length_offset_.b - offset.b; + position_.c += tool_length_offset_.c - offset.c; + position_.u += tool_length_offset_.u - offset.u; + position_.v += tool_length_offset_.v - offset.v; + position_.w += tool_length_offset_.w - offset.w; + 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; @@ -99,6 +132,7 @@ void CanonEventSink::set_g92_offset(const CncSimPose &offset, int line) { } void CanonEventSink::set_xy_rotation(double angle_degrees, int line) { + xy_rotation_degrees_ = angle_degrees; CncSimEvent event = base_event(CNC_SIM_EVENT_SET_XY_ROTATION, line); event.feed = angle_degrees; emit(event); @@ -138,8 +172,13 @@ void CanonEventSink::set_feed_mode(int spindle, int mode, int line) { } void CanonEventSink::set_spindle_speed(double spindle, int line) { - spindle_ = spindle; + set_spindle_speed(0, spindle, line); +} + +void CanonEventSink::set_spindle_speed(int spindle, double speed, int line) { + spindle_ = speed; CncSimEvent event = base_event(CNC_SIM_EVENT_SET_SPINDLE, line); + event.tool = spindle; event.spindle = spindle_; event.reserved = spindle_direction_; emit(event); @@ -229,16 +268,26 @@ void CanonEventSink::set_override_control(int control, bool enabled, int spindle } void CanonEventSink::start_spindle(int direction, int line) { + start_spindle(0, direction, line); +} + +void CanonEventSink::start_spindle(int spindle, int direction, int line) { spindle_direction_ = direction < 0 ? 2 : 1; CncSimEvent event = base_event(CNC_SIM_EVENT_SET_SPINDLE, line); + event.tool = spindle; event.spindle = spindle_; event.reserved = spindle_direction_; emit(event); } void CanonEventSink::stop_spindle(int line) { + stop_spindle(0, line); +} + +void CanonEventSink::stop_spindle(int spindle, int line) { spindle_direction_ = 0; CncSimEvent event = base_event(CNC_SIM_EVENT_SET_SPINDLE, line); + event.tool = spindle; event.spindle = 0.0; event.reserved = spindle_direction_; emit(event); diff --git a/core/src/canon_event_sink.h b/core/src/canon_event_sink.h index b76fa3b..bd36824 100644 --- a/core/src/canon_event_sink.h +++ b/core/src/canon_event_sink.h @@ -13,6 +13,7 @@ public: 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 apply_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); @@ -25,6 +26,7 @@ public: void set_feed_rate(double feed, int line); void set_feed_mode(int spindle, int mode, int line); void set_spindle_speed(double spindle, int line); + void set_spindle_speed(int spindle, double speed, 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); @@ -34,7 +36,9 @@ public: 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 start_spindle(int spindle, int direction, int line); void stop_spindle(int line); + void stop_spindle(int spindle, int line); void select_tool(int tool); void change_tool(int line); void straight_traverse(int line, const CncSimPose &end); @@ -92,6 +96,7 @@ private: double rtcp_tool_length_ = 0.0; int rtcp_h_code_ = 0; CncSimPose tool_length_offset_{}; + double xy_rotation_degrees_ = 0.0; int kinematics_type_ = 1; bool feed_override_ = false; bool speed_override_ = false; diff --git a/core/src/linuxcnc_canon_bridge.cpp b/core/src/linuxcnc_canon_bridge.cpp index 30a6202..7e7e40d 100644 --- a/core/src/linuxcnc_canon_bridge.cpp +++ b/core/src/linuxcnc_canon_bridge.cpp @@ -50,6 +50,12 @@ int plane_to_g_code(CANON_PLANE plane) { return 18; case CANON_PLANE::YZ: return 19; + case CANON_PLANE::UV: + return 171; + case CANON_PLANE::UW: + return 181; + case CANON_PLANE::VW: + return 191; default: return 17; } @@ -73,6 +79,12 @@ CANON_PLANE g_code_to_plane(int plane) { return CANON_PLANE::XZ; case 19: return CANON_PLANE::YZ; + case 171: + return CANON_PLANE::UV; + case 181: + return CANON_PLANE::UW; + case 191: + return CANON_PLANE::VW; case 17: default: return CANON_PLANE::XY; diff --git a/core/src/linuxcnc_tooldata_fixture.cpp b/core/src/linuxcnc_tooldata_fixture.cpp index ddab817..84d7b8b 100644 --- a/core/src/linuxcnc_tooldata_fixture.cpp +++ b/core/src/linuxcnc_tooldata_fixture.cpp @@ -2,6 +2,8 @@ #include "emc/tooldata/tooldata.hh" +#include + void cnc_sim_init_minimal_linuxcnc_tooldata() { static bool created = false; if (!created) { @@ -22,5 +24,29 @@ void cnc_sim_init_minimal_linuxcnc_tooldata() { tool.diameter = 6.0; tool.offset.tran.z = 12.5; tooldata_put(tool, 1); - tooldata_last_index_set(1); + + CANON_TOOL_TABLE thread_tool = tooldata_entry_init(); + thread_tool.toolno = 4; + thread_tool.pocketno = 4; + thread_tool.diameter = 1.0; + thread_tool.offset.tran.z = 0.7; + tooldata_put(thread_tool, 4); + + if (const char *spindle_tool = std::getenv("CNC_SIM_TOOL_IN_SPINDLE")) { + const int tool_id = std::atoi(spindle_tool); + if (tool_id == 1) { + CANON_TOOL_TABLE spindle_tooldata = tool; + spindle_tooldata.pocketno = 0; + spindle_tooldata.frontangle = 12.0; + spindle_tooldata.backangle = 34.0; + spindle_tooldata.orientation = 5; + tooldata_put(spindle_tooldata, 0); + } else if (tool_id == 4) { + CANON_TOOL_TABLE spindle_tooldata = thread_tool; + spindle_tooldata.pocketno = 0; + tooldata_put(spindle_tooldata, 0); + } + } + + tooldata_last_index_set(4); } diff --git a/core/src/smoke_gcode_parser.cpp b/core/src/smoke_gcode_parser.cpp index 13eb3c9..d2ee3ef 100644 --- a/core/src/smoke_gcode_parser.cpp +++ b/core/src/smoke_gcode_parser.cpp @@ -6,6 +6,8 @@ #include #include #include +#include +#include #include #include #include @@ -18,6 +20,16 @@ struct Word { double value; }; +struct ParsedWordList { + std::vector words; + bool dollar_flag = false; + double dollar_value = 0.0; + bool radius_flag = false; + double radius_value = 0.0; + bool theta_flag = false; + double theta_value = 0.0; +}; + struct SourceLine { int number; std::string raw; @@ -33,6 +45,7 @@ struct Subprogram { struct CallFrame { size_t return_pc; size_t sub_start; + size_t conditional_depth; int remaining; bool oword; bool has_modal_state; @@ -88,12 +101,201 @@ constexpr int kMaxNumberedParameter = 5602; constexpr int kModalMotionG33 = 33; constexpr int kModalMotionG331 = 331; constexpr int kModalMotionG76 = 76; +constexpr int kSmokeNumSpindles = 1; constexpr int kCutterCompOffEvent = 400; constexpr int kCutterCompLeftEvent = 410; constexpr int kCutterCompRightEvent = 420; constexpr int kCutterCompLeftExplicitEvent = 411; constexpr int kCutterCompRightExplicitEvent = 421; +enum class LinuxCncReaderSymbol { + None, + ParameterSetting, + Dollar, + Comment, + Semicolon, + AtSign, + Caret, + LetterWord, +}; + +const std::array &linuxcnc_reader_symbols() { + static const std::array table = [] { + std::array out{}; + out.fill(LinuxCncReaderSymbol::None); + out[static_cast('#')] = LinuxCncReaderSymbol::ParameterSetting; + out[static_cast('$')] = LinuxCncReaderSymbol::Dollar; + out[static_cast('(')] = LinuxCncReaderSymbol::Comment; + out[static_cast(';')] = LinuxCncReaderSymbol::Semicolon; + out[static_cast('@')] = LinuxCncReaderSymbol::AtSign; + out[static_cast('^')] = LinuxCncReaderSymbol::Caret; + for (unsigned char ch = 'A'; ch <= 'Z'; ++ch) { + out[ch] = LinuxCncReaderSymbol::LetterWord; + } + for (unsigned char ch = 'a'; ch <= 'z'; ++ch) { + out[ch] = LinuxCncReaderSymbol::LetterWord; + } + return out; + }(); + return table; +} + +const std::array &linuxcnc_g_modal_group_table() { + static const std::array table = [] { + std::array out{}; + out.fill(-1); + auto set = [&out](int code, int group) { + out[static_cast(code)] = group; + }; + + set(0, 1); + set(10, 1); + set(20, 1); + set(30, 1); + set(40, 0); + set(50, 1); + set(51, 1); + set(52, 1); + set(53, 0); + set(60, 1); + set(61, 1); + set(62, 1); + set(63, 0); + set(70, 15); + set(80, 15); + set(100, 0); + set(170, 2); + set(171, 2); + set(180, 2); + set(181, 2); + set(190, 2); + set(191, 2); + set(200, 6); + set(210, 6); + set(280, 0); + set(281, 0); + set(300, 0); + set(301, 0); + set(330, 1); + set(331, 1); + set(382, 1); + set(383, 1); + set(384, 1); + set(385, 1); + set(400, 7); + set(410, 7); + set(411, 7); + set(420, 7); + set(421, 7); + set(430, 8); + set(431, 8); + set(432, 8); + set(434, 8); + set(435, 8); + set(490, 8); + set(520, 0); + set(530, 0); + set(540, 12); + set(550, 12); + set(560, 12); + set(570, 12); + set(580, 12); + set(590, 12); + set(591, 12); + set(592, 12); + set(593, 12); + set(610, 13); + set(611, 13); + set(640, 13); + set(700, 1); + set(710, 1); + set(711, 1); + set(712, 1); + set(720, 1); + set(721, 1); + set(722, 1); + set(730, 1); + set(740, 1); + set(760, 1); + set(800, 1); + set(810, 1); + set(820, 1); + set(830, 1); + set(840, 1); + set(850, 1); + set(860, 1); + set(870, 1); + set(880, 1); + set(890, 1); + set(900, 3); + set(901, 4); + set(910, 3); + set(911, 4); + set(920, 0); + set(921, 16); + set(922, 16); + set(923, 16); + set(930, 5); + set(940, 5); + set(950, 5); + set(960, 14); + set(970, 14); + set(980, 10); + set(990, 10); + return out; + }(); + return table; +} + +const std::array &linuxcnc_m_modal_group_table() { + static const std::array table = [] { + std::array out{}; + out.fill(-1); + auto set = [&out](int code, int group) { + out[static_cast(code)] = group; + }; + + set(0, 4); + set(1, 4); + set(2, 4); + set(3, 7); + set(4, 7); + set(5, 7); + set(6, 6); + set(7, 8); + set(8, 8); + set(9, 8); + set(19, 7); + set(30, 4); + set(48, 9); + set(49, 9); + set(50, 9); + set(51, 9); + set(52, 9); + set(53, 9); + set(60, 4); + set(61, 6); + set(62, 5); + set(63, 5); + set(64, 5); + set(65, 5); + set(66, 5); + set(67, 5); + set(68, 5); + set(70, 7); + set(71, 7); + set(72, 7); + set(73, 7); + set(98, 10); + set(99, 4); + for (int code = 100; code <= 199; ++code) { + set(code, 10); + } + return out; + }(); + return table; +} + std::string trim(const std::string &value) { auto first = std::find_if_not(value.begin(), value.end(), [](unsigned char ch) { return std::isspace(ch); }); auto last = std::find_if_not(value.rbegin(), value.rend(), [](unsigned char ch) { return std::isspace(ch); }).base(); @@ -294,6 +496,37 @@ std::string strip_comments(const std::string &line, std::string *comment) { return out; } +bool validate_linuxcnc_comments(const std::string &line, std::string *error) { + bool in_paren = false; + for (char raw : line) { + const char ch = static_cast(std::toupper(static_cast(raw))); + if (ch == ';' && !in_paren) { + return true; + } + if (in_paren) { + if (ch == ')') { + in_paren = false; + } else if (ch == '(') { + if (error) { + *error = "Nested comment found"; + } + return false; + } + continue; + } + if (ch == '(') { + in_paren = true; + } + } + if (in_paren) { + if (error) { + *error = "Unclosed comment found"; + } + return false; + } + return true; +} + 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) { @@ -352,8 +585,95 @@ 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(); +bool skip_linuxcnc_top_level_construct(const std::string &line, size_t *index) { + const size_t i = *index; + if (i >= line.size()) { + return false; + } + const char ch = line[i]; + if (ch == '[' || ch == ']' || ch == '/') { + *index = i + 1; + return true; + } + if (ch == '#') { + *index = i + 1; + if (*index < line.size() && line[*index] == '<') { + const size_t close = line.find('>', *index + 1); + if (close != std::string::npos) { + *index = close + 1; + } + } + return true; + } + if ((ch == 'O' || ch == 'o') && i + 1 < line.size() && line[i + 1] == '<') { + const size_t close = line.find('>', i + 2); + if (close != std::string::npos) { + *index = close + 1; + return true; + } + } + return false; +} + +bool is_linuxcnc_midline_n_word(const std::string &line, size_t index) { + if (index >= line.size()) { + return false; + } + const char ch = line[index]; + if (ch != 'N' && ch != 'n') { + return false; + } + if (index == 0 || index + 1 >= line.size()) { + return false; + } + return looks_like_real_start(line[index + 1]); +} + +bool validate_linuxcnc_top_level_reader_symbols(const std::string &line, std::string *error) { + const auto &reader_symbols = linuxcnc_reader_symbols(); + for (size_t i = 0; i < line.size();) { + const unsigned char raw = static_cast(line[i]); + if (std::isspace(raw)) { + ++i; + continue; + } + if (skip_linuxcnc_top_level_construct(line, &i)) { + continue; + } + if (is_linuxcnc_midline_n_word(line, i)) { + if (error) { + *error = "Bad character 'N' used"; + } + return false; + } + const char ch = static_cast(raw); + if (reader_symbols[raw] != LinuxCncReaderSymbol::None || + looks_like_real_start(ch)) { + ++i; + continue; + } + if (error) { + if (!std::isprint(raw) || std::isspace(raw)) { + char buffer[32]; + std::snprintf(buffer, sizeof(buffer), "Bad character '\\%03o' used", raw); + *error = buffer; + } else { + *error = "Bad character '" + std::string(1, static_cast(raw)) + "' used"; + } + } + return false; + } + return true; +} + +bool parse_word_list_impl(const std::string &line, ParsedWordList *parsed, std::string *error, bool strict) { + parsed->words.clear(); + parsed->dollar_flag = false; + parsed->dollar_value = 0.0; + parsed->radius_flag = false; + parsed->radius_value = 0.0; + parsed->theta_flag = false; + parsed->theta_value = 0.0; std::array seen_letters{}; const char *text = line.c_str(); for (size_t i = 0; text[i] != '\0';) { @@ -361,14 +681,122 @@ bool parse_word_list_impl(const std::string &line, std::vector *words, std ++i; continue; } + if (text[i] == '@' || text[i] == '^') { + const bool radius_word = text[i] == '@'; + ++i; + while (text[i] == ' ' || text[i] == '\t' || text[i] == '\r') { + ++i; + } + 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; + } + 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; + } + if (strict && ((radius_word && parsed->radius_flag) || (!radius_word && parsed->theta_flag))) { + if (error) { + *error = radius_word ? "multiple @ words on one line" : "multiple ^ words on one line"; + } + return false; + } + if (radius_word) { + parsed->radius_flag = true; + parsed->radius_value = value; + } else { + parsed->theta_flag = true; + parsed->theta_value = value; + } + i = next; + continue; + } + if (text[i] == '$') { + ++i; + while (text[i] == ' ' || text[i] == '\t' || text[i] == '\r') { + ++i; + } + 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; + } + 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; + } + if (strict && parsed->dollar_flag) { + if (error) { + *error = "multiple $ words on one line"; + } + return false; + } + parsed->dollar_flag = true; + parsed->dollar_value = value; + i = next; + continue; + } char letter = static_cast(std::toupper(static_cast(text[i]))); if (!std::isalpha(static_cast(letter))) { ++i; continue; } + const size_t letter_pos = i; ++i; + if (letter_pos == 0 || + !std::isalpha(static_cast(text[letter_pos - 1]))) { + while (text[i] == ' ' || text[i] == '\t' || text[i] == '\r') { + ++i; + } + } + if (strict && letter == 'N') { + if (letter_pos > 0 && + std::isalpha(static_cast(text[letter_pos - 1]))) { + continue; + } + if (letter_pos != 0) { + if (error) { + *error = "Bad character 'N' used"; + } + return false; + } + if (!std::isdigit(static_cast(text[i]))) { + if (error) { + *error = "Bad format unsigned integer"; + } + return false; + } + } if (letter == 'O') { - if (!text[i] || text[i] == '<' || std::isalpha(static_cast(text[i]))) { + if (text[i] == '<') { + const size_t close = line.find('>', i + 1); + i = close == std::string::npos ? i : close + 1; + continue; + } + if (!text[i] || std::isalpha(static_cast(text[i]))) { continue; } } @@ -402,21 +830,24 @@ bool parse_word_list_impl(const std::string &line, std::vector *words, std } seen_letters[letter_index] = true; } - words->push_back({letter, value}); + parsed->words.push_back({letter, value}); i = next; } return true; } std::vector parse_word_list(const std::string &line) { - std::vector words; + ParsedWordList parsed; std::string error; - parse_word_list_impl(line, &words, &error, false); - return words; + parse_word_list_impl(line, &parsed, &error, false); + return parsed.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); + ParsedWordList parsed; + const bool ok = parse_word_list_impl(line, &parsed, error, true); + *words = std::move(parsed.words); + return ok; } std::unordered_map last_words_by_letter(const std::vector &word_list) { @@ -443,6 +874,35 @@ std::vector split_source_lines(const std::string &source) { return lines; } +bool linuxcnc_effective_program_lines(const std::vector &source, + std::vector *effective, + std::string *error) { + effective->clear(); + size_t first_nonblank = source.size(); + for (size_t i = 0; i < source.size(); ++i) { + if (!trim(source[i].raw).empty()) { + first_nonblank = i; + break; + } + } + if (first_nonblank == source.size() || trim(source[first_nonblank].raw) != "%") { + *effective = source; + return true; + } + + for (size_t i = first_nonblank + 1; i < source.size(); ++i) { + if (trim(source[i].raw) == "%") { + return true; + } + effective->push_back({static_cast(i - first_nonblank + 1), source[i].raw}); + } + + if (error) { + *error = "File ended with no percent sign (%)"; + } + return false; +} + bool first_o_number(const std::vector &word_list, int *number) { for (const Word &word : word_list) { if (word.letter == 'O') { @@ -459,7 +919,7 @@ std::string numeric_o_identifier(int number) { std::string first_o_identifier(const std::string &line, const std::vector &word_list) { for (size_t i = 0; i < line.size(); ++i) { - if (line[i] != 'O') { + if (std::toupper(static_cast(line[i])) != 'O') { continue; } size_t pos = i + 1; @@ -505,142 +965,92 @@ int linuxcnc_g_code_number(double value) { return code; } +bool validate_linuxcnc_g_code_value(double value, std::string *error) { + const double scaled = value * 10.0; + int code = static_cast(std::floor(scaled)); + const double fraction = scaled - static_cast(code); + if (fraction > 0.999) { + code = static_cast(std::ceil(scaled)); + } else if (fraction > 0.001) { + if (error) { + *error = "G code out of range"; + } + return false; + } + if (code > 999) { + if (error) { + *error = "G code out of range"; + } + return false; + } + if (code < 0) { + if (error) { + *error = "Negative G code used"; + } + return false; + } + return true; +} + +bool linuxcnc_g_code_is_g70_finishing_cycle(double value) { + return linuxcnc_g_code_number(value) == 700; +} + +bool linuxcnc_g_code_is_g71_g72_roughing_cycle(double value) { + const int code = linuxcnc_g_code_number(value); + return code == 710 || code == 711 || code == 712 || + code == 720 || code == 721 || code == 722; +} + +bool linuxcnc_g_code_is_g7x_cycle(double value) { + return linuxcnc_g_code_is_g70_finishing_cycle(value) || + linuxcnc_g_code_is_g71_g72_roughing_cycle(value); +} + +bool smoke_spindle_selector_valid_for_speed(double spindle) { + const int spindle_index = rounded_value(spindle); + return spindle_index >= -1 && spindle_index < kSmokeNumSpindles && + std::fabs(static_cast(spindle_index) - spindle) < 0.0001; +} + +bool smoke_spindle_selector_valid_for_mode(double spindle) { + const int spindle_index = rounded_value(spindle); + return spindle_index >= 0 && spindle_index < kSmokeNumSpindles && + std::fabs(static_cast(spindle_index) - spindle) < 0.0001; +} + +int smoke_resolved_spindle_selector(double spindle) { + const int spindle_index = rounded_value(spindle); + if (spindle_index == -1) { + return 0; + } + return spindle_index; +} + +bool smoke_tool_number_exists(int tool_number) { + return tool_number == 0 || tool_number == 1 || tool_number == 4; +} + int supported_linuxcnc_g_modal_group(double value) { const int code = linuxcnc_g_code_number(value); - switch (code) { - case 0: - case 10: - case 20: - case 30: - case 300: - case 500: - case 510: - case 520: - case 610: - case 620: - case 700: - case 710: - case 720: - case 730: - case 740: - case 760: - case 800: - case 810: - case 820: - case 830: - case 840: - case 850: - case 860: - case 870: - case 880: - case 890: - return 1; - case 170: - case 180: - case 190: - return 2; - case 900: - case 910: - return 3; - case 901: - case 911: - return 4; - case 930: - case 940: - case 950: - return 5; - case 200: - case 210: - case 7000: - case 7100: - return 6; - case 400: - case 410: - case 411: - case 420: - case 421: - return 7; - case 430: - case 431: - case 432: - case 434: - case 435: - case 490: - return 8; - case 980: - case 990: - return 10; - case 540: - case 550: - case 560: - case 570: - case 580: - case 590: - case 591: - case 592: - case 593: - return 12; - case 611: - case 640: - return 13; - case 960: - case 970: - return 14; - case 70: - case 80: - return 15; - case 922: - case 923: - return 16; - default: + if (code < 0 || code >= 1000) { return -1; } + return linuxcnc_g_modal_group_table()[static_cast(code)]; } int supported_linuxcnc_m_modal_group(int value) { - if (value >= 100 && value <= 199) { - return 10; - } - switch (value) { - case 0: - case 1: - case 2: - case 30: - case 60: - case 99: - return 4; - case 62: - case 63: - case 64: - case 65: - case 66: - return 5; - case 6: - case 61: - return 6; - case 3: - case 4: - case 5: - case 19: - case 70: - case 71: - case 72: - case 73: - return 7; - case 7: - case 8: - case 9: - return 8; - case 48: - case 49: - case 50: - case 51: - case 52: - case 53: - return 9; - default: + if (value < 0 || value >= 200) { return -1; } + return linuxcnc_m_modal_group_table()[static_cast(value)]; +} + +bool smoke_m_code_has_linuxcnc_or_simulator_use(int value) { + if (value == 428 || value == 429 || value == 430) { + return true; + } + return supported_linuxcnc_m_modal_group(value) >= 0; } bool check_linuxcnc_modal_group_conflicts(const std::vector &word_list, std::string *error) { @@ -649,9 +1059,15 @@ bool check_linuxcnc_modal_group_conflicts(const std::vector &word_list, st g_modes.fill(-1); for (const Word &word : word_list) { if (word.letter == 'G') { + if (!validate_linuxcnc_g_code_value(word.value, error)) { + return false; + } const int mode = supported_linuxcnc_g_modal_group(word.value); if (mode < 0) { - continue; + if (error) { + *error = "Unknown G code used"; + } + return false; } const int code = linuxcnc_g_code_number(word.value); const int previous_code = g_modes[static_cast(mode)]; @@ -698,7 +1114,7 @@ bool has_o_code_keyword(const std::string &line, const char *keyword) { OwordKind oword_kind(const std::string &line) { for (size_t i = 0; i < line.size(); ++i) { - if (line[i] != 'O') { + if (std::toupper(static_cast(line[i])) != 'O') { continue; } size_t pos = i + 1; @@ -918,10 +1334,38 @@ bool is_smoke_readonly_named_parameter(const std::string &name) { name == "_spindle_on" || name == "_SPINDLE_CW" || name == "_spindle_cw" || + name == "_INVERSE_TIME" || + name == "_inverse_time" || + name == "_UNITS_PER_MINUTE" || + name == "_units_per_minute" || + name == "_UNITS_PER_REV" || + name == "_units_per_rev" || + name == "_COORD_SYSTEM" || + name == "_coord_system" || + name == "_RETRACT_R_PLANE" || + name == "_retract_r_plane" || + name == "_RETRACT_OLD_Z" || + name == "_retract_old_z" || + name == "_IJK_ABSOLUTE_MODE" || + name == "_ijk_absolute_mode" || + name == "_MIST" || + name == "_mist" || + name == "_FLOOD" || + name == "_flood" || + name == "_SPEED_OVERRIDE" || + name == "_speed_override" || + name == "_FEED_OVERRIDE" || + name == "_feed_override" || + name == "_ADAPTIVE_FEED" || + name == "_adaptive_feed" || + name == "_FEED_HOLD" || + name == "_feed_hold" || name == "_MOTION_MODE" || name == "_motion_mode" || name == "_PLANE" || name == "_plane" || + name == "_CCOMP" || + name == "_ccomp" || name == "_CURRENT_TOOL" || name == "_current_tool" || name == "_CURRENT_POCKET" || @@ -930,6 +1374,20 @@ bool is_smoke_readonly_named_parameter(const std::string &name) { name == "_selected_tool" || name == "_SELECTED_POCKET" || name == "_selected_pocket" || + name == "_SPINDLE_RPM_MODE" || + name == "_spindle_rpm_mode" || + name == "_SPINDLE_CSS_MODE" || + name == "_spindle_css_mode" || + name == "_LINE" || + name == "_line" || + name == "_VALUE" || + name == "_value" || + name == "_VALUE_RETURNED" || + name == "_value_returned" || + name == "_CALL_LEVEL" || + name == "_call_level" || + name == "_REMAP_LEVEL" || + name == "_remap_level" || name == "_TOOL_OFFSET" || name == "_tool_offset" || name == "_X" || @@ -970,6 +1428,21 @@ bool is_smoke_readonly_named_parameter(const std::string &name) { name == "_abs_w"; } +bool is_smoke_readonly_numbered_parameter(int index) { + return (index >= 5061 && index <= 5070) || + (index >= 5400 && index <= 5413) || + (index >= 5420 && index <= 5428); +} + +bool is_smoke_persistent_numbered_parameter(int index) { + return (index >= 5001 && index <= 5060) || + (index >= 5161 && index <= 5399); +} + +bool is_smoke_system_persistent_numbered_parameter(int index) { + return index >= 5161 && index <= 5399; +} + class ExpressionParser { public: ExpressionParser(const std::string &text, @@ -982,7 +1455,7 @@ public: bool parse(double *value) { *value = parse_logical(); skip_space(); - return ok_ && pos_ == text_.size(); + return ok_ && pos_ == text_.size() && std::isfinite(*value); } private: @@ -1120,6 +1593,10 @@ private: if (!consume(']')) { ok_ = false; } + if (!std::isfinite(value)) { + ok_ = false; + return 0.0; + } return value; } if (consume('#')) { @@ -1139,24 +1616,22 @@ private: ok_ = false; return 0.0; } + double result = 0.0; if (name == "ABS") { - return std::fabs(argument); - } - if (name == "ACOS") { + result = std::fabs(argument); + } else if (name == "ACOS") { if (argument < -1.0 || argument > 1.0) { ok_ = false; return 0.0; } - return radians_to_degrees(std::acos(argument)); - } - if (name == "ASIN") { + result = radians_to_degrees(std::acos(argument)); + } else if (name == "ASIN") { if (argument < -1.0 || argument > 1.0) { ok_ = false; return 0.0; } - return radians_to_degrees(std::asin(argument)); - } - if (name == "ATAN") { + result = radians_to_degrees(std::asin(argument)); + } else if (name == "ATAN") { if (!consume('/') || !consume('[')) { ok_ = false; return 0.0; @@ -1166,41 +1641,42 @@ private: ok_ = false; return 0.0; } - return radians_to_degrees(std::atan2(argument, argument2)); - } - if (name == "SQRT") { - return std::sqrt(argument); - } - if (name == "EXP") { - return std::exp(argument); - } - if (name == "LN") { + result = radians_to_degrees(std::atan2(argument, argument2)); + } else if (name == "SQRT") { + if (argument < 0.0) { + ok_ = false; + return 0.0; + } + result = std::sqrt(argument); + } else if (name == "EXP") { + result = std::exp(argument); + } else if (name == "LN") { if (argument <= 0.0) { ok_ = false; return 0.0; } - return std::log(argument); + result = std::log(argument); + } else if (name == "FIX") { + result = std::floor(argument); + } else if (name == "FUP") { + result = std::ceil(argument); + } else if (name == "ROUND") { + result = round_away_from_zero(argument); + } else if (name == "SIN") { + result = std::sin(degrees_to_radians(argument)); + } else if (name == "COS") { + result = std::cos(degrees_to_radians(argument)); + } else if (name == "TAN") { + result = std::tan(degrees_to_radians(argument)); + } else { + ok_ = false; + return 0.0; } - if (name == "FIX") { - return std::floor(argument); + if (!std::isfinite(result)) { + ok_ = false; + return 0.0; } - if (name == "FUP") { - return std::ceil(argument); - } - if (name == "ROUND") { - return round_away_from_zero(argument); - } - if (name == "SIN") { - return std::sin(degrees_to_radians(argument)); - } - if (name == "COS") { - return std::cos(degrees_to_radians(argument)); - } - if (name == "TAN") { - return std::tan(degrees_to_radians(argument)); - } - ok_ = false; - return 0.0; + return result; } double value = 0.0; @@ -1282,7 +1758,19 @@ private: } return value; } - const int index = parse_integer(); + const double index_value = parse_primary(); + const int index = static_cast(std::floor(index_value)); + if (index_value - index > 0.9999) { + return parameter_reference_value(static_cast(std::ceil(index_value))); + } + if (index_value - index > 0.0001) { + ok_ = false; + return 0.0; + } + return parameter_reference_value(index); + } + + double parameter_reference_value(int index) { if (index <= 0 || index >= kMaxNumberedParameter) { ok_ = false; return 0.0; @@ -1343,6 +1831,10 @@ private: bool consume(char ch) { skip_space(); + return consume_raw(ch); + } + + bool consume_raw(char ch) { if (pos_ < text_.size() && text_[pos_] == ch) { ++pos_; return true; @@ -1499,6 +1991,81 @@ bool is_loop_end(OwordKind kind) { return kind == OwordKind::EndWhile || kind == OwordKind::EndRepeat; } +bool validate_numeric_oword_main_structure(const std::vector &lines, std::string *error) { + bool numeric_main_active = false; + bool main_terminated = false; + for (const auto &source_line : lines) { + const std::string stripped = trim(strip_comments(source_line.raw, nullptr)); + if (stripped.empty()) { + continue; + } + if (stripped == "%") { + if (numeric_main_active) { + numeric_main_active = false; + main_terminated = true; + } + continue; + } + const auto word_list = parse_word_list(stripped); + const std::string id = first_o_identifier(stripped, word_list); + const OwordKind kind = oword_kind(stripped); + if (has_m_code(word_list, 2) || has_m_code(word_list, 30)) { + numeric_main_active = false; + main_terminated = true; + continue; + } + if (!id.empty() && id.rfind("N:", 0) == 0 && + kind == OwordKind::None && + !numeric_main_active && + !main_terminated) { + numeric_main_active = true; + continue; + } + if (numeric_main_active && kind == OwordKind::Sub) { + if (error) { + *error = "numeric O-word subprogram found before main program end"; + } + return false; + } + } + if (numeric_main_active) { + if (error) { + *error = "numeric O-word main program ended without M2, M30, or %"; + } + return false; + } + return true; +} + +bool validate_no_nested_subprogram_definitions(const std::vector &lines, std::string *error) { + for (size_t i = 0; i < lines.size(); ++i) { + const std::string stripped = strip_comments(lines[i].raw, nullptr); + const auto words = parse_word_list(stripped); + const std::string id = first_o_identifier(stripped, words); + if (id.empty() || oword_kind(stripped) != OwordKind::Sub) { + continue; + } + for (size_t j = i + 1; j < lines.size(); ++j) { + const std::string nested_line = strip_comments(lines[j].raw, nullptr); + const auto nested_words = parse_word_list(nested_line); + const std::string nested_id = first_o_identifier(nested_line, nested_words); + const OwordKind nested_kind = oword_kind(nested_line); + if (nested_kind == OwordKind::EndSub && nested_id == id) { + i = j; + break; + } + if (nested_kind == OwordKind::Sub || + (!nested_id.empty() && nested_id.rfind("N:", 0) == 0 && nested_kind == OwordKind::None)) { + if (error) { + *error = "Nested subroutine definition"; + } + return false; + } + } + } + return true; +} + OwordKind matching_loop_end_kind(OwordKind kind) { if (kind == OwordKind::While) { return OwordKind::EndWhile; @@ -1638,17 +2205,23 @@ std::string number_to_string(double value) { } bool unary_function_span(const std::string &line, size_t start, size_t *end) { - static const std::array kUnaryFunctions = { + static const std::array kUnaryFunctions = { "ABS", "ACOS", "ASIN", "ATAN", "COS", "EXP", "FIX", - "FUP", "LN", "ROUND", "SIN", "SQRT", "TAN"}; + "FUP", "LN", "ROUND", "SIN", "SQRT", "TAN", "EXISTS"}; 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] != '[') { + if (upper_line.rfind(name, 0) != 0 || start + name_len >= line.size()) { continue; } - const size_t first_close = find_matching_square_bracket(line, start + name_len); + size_t first_open = start + name_len; + while (first_open < line.size() && std::isspace(static_cast(line[first_open]))) { + ++first_open; + } + if (first_open >= line.size() || line[first_open] != '[') { + continue; + } + const size_t first_close = find_matching_square_bracket(line, first_open); if (first_close == std::string::npos) { return false; } @@ -1681,6 +2254,75 @@ bool unary_function_span(const std::string &line, size_t start, size_t *end) { return false; } +bool parameter_expression_span(const std::string &line, size_t start, size_t *end) { + size_t expr_start = start; + while (expr_start < line.size() && + std::isspace(static_cast(line[expr_start]))) { + ++expr_start; + } + if (expr_start >= line.size()) { + return false; + } + if ((line[expr_start] == '+' || line[expr_start] == '-') && + expr_start + 1 < line.size() && + !std::isdigit(static_cast(line[expr_start + 1])) && + line[expr_start + 1] != '.') { + ++expr_start; + } + return unary_function_span(line, expr_start, end); +} + +bool parameter_reference_index_span(const std::string &line, size_t start, size_t *end) { + size_t expr_start = start; + while (expr_start < line.size() && + std::isspace(static_cast(line[expr_start]))) { + ++expr_start; + } + if (expr_start >= line.size()) { + return false; + } + if ((line[expr_start] == '+' || line[expr_start] == '-') && + expr_start + 1 < line.size() && + !std::isdigit(static_cast(line[expr_start + 1])) && + line[expr_start + 1] != '.') { + ++expr_start; + while (expr_start < line.size() && + std::isspace(static_cast(line[expr_start]))) { + ++expr_start; + } + } + if (expr_start < line.size() && line[expr_start] == '#') { + size_t pos = expr_start + 1; + if (pos < line.size() && line[pos] == '#') { + ++pos; + } + if (pos < line.size() && line[pos] == '[') { + const size_t close = find_matching_square_bracket(line, pos); + if (close == std::string::npos) { + return false; + } + *end = close + 1; + return true; + } + if (pos < line.size() && line[pos] == '<') { + const size_t close = line.find('>', pos + 1); + if (close == std::string::npos) { + return false; + } + *end = close + 1; + return true; + } + while (pos < line.size() && std::isdigit(static_cast(line[pos]))) { + ++pos; + } + if (pos > expr_start + 1) { + *end = pos; + return true; + } + } + return unary_function_span(line, expr_start, end); +} + std::string expand_expressions_and_parameters(const std::string &line, const std::vector &call_stack, const std::unordered_map ¶meters, @@ -1689,14 +2331,47 @@ 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()) { + if (std::isalpha(static_cast(line[i])) && i + 1 < line.size() && + (i == 0 || !std::isalpha(static_cast(line[i - 1])))) { const size_t value_start = i + 1; size_t function_start = value_start; + while (function_start < line.size() && + std::isspace(static_cast(line[function_start]))) { + ++function_start; + } if ((line[function_start] == '+' || line[function_start] == '-') && function_start + 1 < line.size() && !std::isdigit(static_cast(line[function_start + 1])) && line[function_start + 1] != '.') { ++function_start; + while (function_start < line.size() && + std::isspace(static_cast(line[function_start]))) { + ++function_start; + } + } + if (function_start < line.size() && line[function_start] == '[') { + const size_t close = find_matching_square_bracket(line, function_start); + if (close == std::string::npos) { + if (error) { + *error = "unterminated bracket expression"; + } + return {}; + } + double value = 0.0; + if (!evaluate_expression(line.substr(value_start, close - value_start + 1), + call_stack, + parameters, + named_parameters, + &value)) { + if (error) { + *error = "unsupported bracket expression"; + } + return {}; + } + out.push_back(line[i]); + out += number_to_string(value); + i = close + 1; + continue; } size_t function_end = 0; if (unary_function_span(line, function_start, &function_end)) { @@ -1837,10 +2512,34 @@ std::string expand_expressions_and_parameters(const std::string &line, continue; } if (j >= line.size() || !std::isdigit(static_cast(line[j]))) { - if (error) { - *error = "unsupported parameter expression"; + size_t expression_end = 0; + if (!parameter_reference_index_span(line, j, &expression_end)) { + if (error) { + *error = "unsupported parameter expression"; + } + return {}; } - return {}; + double index_value = 0.0; + if (!evaluate_expression(line.substr(j, expression_end - j), + call_stack, + parameters, + named_parameters, + &index_value)) { + if (error) { + *error = "unsupported 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 = expression_end; + continue; } while (j < line.size() && std::isdigit(static_cast(line[j]))) { ++j; @@ -1967,7 +2666,35 @@ bool parse_parameter_assignment(const std::string &line, if (pos >= text.size() || !std::isdigit(static_cast(text[pos]))) { if (parsed_name.empty() && parsed_index == 0) { - return false; + size_t expression_end = 0; + if (!parameter_reference_index_span(text, pos, &expression_end)) { + return false; + } + double index_value = 0.0; + if (!evaluate_expression(text.substr(pos, expression_end - pos), + 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 = expression_end; } } if (parsed_name.empty() && parsed_index == 0) { @@ -2091,6 +2818,12 @@ bool extract_parameter_assignments(const std::string &line, } return false; } + if (assigned_parameter > 0 && is_smoke_readonly_numbered_parameter(assigned_parameter)) { + if (error) { + *error = "attempt to assign read-only numbered parameter"; + } + 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"; @@ -2115,7 +2848,8 @@ std::unordered_map discover_subprograms(const std::vect continue; } - if (has_o_code_keyword(stripped, "sub")) { + const OwordKind kind = oword_kind(stripped); + if (kind == OwordKind::Sub) { for (size_t j = i + 1; j < lines.size(); ++j) { const std::string end_line = strip_comments(lines[j].raw, nullptr); const auto end_words = parse_word_list(end_line); @@ -2131,6 +2865,9 @@ std::unordered_map discover_subprograms(const std::vect } continue; } + if (kind != OwordKind::None) { + continue; + } for (size_t j = i + 1; j < lines.size(); ++j) { const std::string end_line = strip_comments(lines[j].raw, nullptr); @@ -2194,6 +2931,15 @@ CycleAxes cycle_axes_for_plane(int plane) { if (plane == 19) { return CycleAxes{'Y', 'Z', 'X', &CncSimPose::y, &CncSimPose::z, &CncSimPose::x}; } + if (plane == 171) { + return CycleAxes{'U', 'V', 'W', &CncSimPose::u, &CncSimPose::v, &CncSimPose::w}; + } + if (plane == 181) { + return CycleAxes{'W', 'U', 'V', &CncSimPose::w, &CncSimPose::u, &CncSimPose::v}; + } + if (plane == 191) { + return CycleAxes{'V', 'W', 'U', &CncSimPose::v, &CncSimPose::w, &CncSimPose::u}; + } return CycleAxes{'X', 'Y', 'Z', &CncSimPose::x, &CncSimPose::y, &CncSimPose::z}; } @@ -2210,6 +2956,15 @@ G87CycleValues g87_cycle_values_for_plane(int plane, double i, double j, double if (plane == 19) { return G87CycleValues{j, k, i}; } + if (plane == 171) { + return G87CycleValues{i, j, k}; + } + if (plane == 181) { + return G87CycleValues{k, i, j}; + } + if (plane == 191) { + return G87CycleValues{j, k, i}; + } return G87CycleValues{i, j, k}; } @@ -2225,6 +2980,18 @@ CncSimPose g74_g84_pose_for_plane(int plane, const CncSimPose &hole, const Cycle pose.x = second; pose.y = depth; pose.z = first; + } else if (plane == 171) { + pose.u = first; + pose.v = second; + pose.w = depth; + } else if (plane == 181) { + pose.u = second; + pose.v = depth; + pose.w = first; + } else if (plane == 191) { + pose.u = depth; + pose.v = first; + pose.w = second; } else { pose.x = first; pose.y = second; @@ -2309,6 +3076,65 @@ void apply_axes(CncSimPose *pose, apply_axis(pose, words, 'W', &CncSimPose::w, absolute, scale, lathe_diameter_mode); } +bool apply_polar_coordinates(CncSimPose *pose, + const ParsedWordList &parsed, + bool absolute, + std::string *error) { + if (!parsed.radius_flag && !parsed.theta_flag) { + return true; + } + const double current_x = pose->x; + const double current_y = pose->y; + if (absolute) { + if (parsed.radius_flag && parsed.theta_flag) { + pose->x = parsed.radius_value * std::cos(degrees_to_radians(parsed.theta_value)); + pose->y = parsed.radius_value * std::sin(degrees_to_radians(parsed.theta_value)); + return true; + } + if (parsed.radius_flag) { + if (current_x == 0.0 && current_y == 0.0) { + if (error) { + *error = "Must specify angle in polar coordinate if at the origin"; + } + return false; + } + const double theta = std::atan2(current_y, current_x); + pose->x = parsed.radius_value * std::cos(theta); + pose->y = parsed.radius_value * std::sin(theta); + return true; + } + const double radius = std::hypot(current_y, current_x); + pose->x = radius * std::cos(degrees_to_radians(parsed.theta_value)); + pose->y = radius * std::sin(degrees_to_radians(parsed.theta_value)); + return true; + } + if (parsed.radius_flag) { + if (current_x == 0.0 && current_y == 0.0) { + if (error) { + *error = "Incremental motion with polar coordinates is indeterminate when at the origin"; + } + return false; + } + const double theta = std::atan2(current_y, current_x); + const double radius = std::hypot(current_y, current_x) + parsed.radius_value; + pose->x = radius * std::cos(theta); + pose->y = radius * std::sin(theta); + } + if (parsed.theta_flag) { + if (current_x == 0.0 && current_y == 0.0) { + if (error) { + *error = "G91 motion with polar coordinates is indeterminate when at the origin"; + } + return false; + } + const double theta = std::atan2(current_y, current_x) + degrees_to_radians(parsed.theta_value); + const double radius = std::hypot(current_y, current_x); + pose->x = radius * std::cos(theta); + pose->y = radius * std::sin(theta); + } + return true; +} + void apply_cycle_position_axes(CncSimPose *pose, const std::unordered_map &words, const CycleAxes &axes, @@ -2501,6 +3327,8 @@ bool has_motion_g_code(const std::vector &word_list) { continue; } if (g_code_is(word.value, 33.1) || + g_code_is(word.value, 5.1) || + g_code_is(word.value, 6.1) || g_code_is(word.value, 38.2) || g_code_is(word.value, 38.3) || g_code_is(word.value, 38.4) || @@ -2520,7 +3348,8 @@ bool has_motion_g_code(const std::vector &word_list) { 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) { + modal_motion == 89 || modal_motion == 74 || modal_motion == 84 || + modal_motion == 70) { return true; } for (const Word &word : word_list) { @@ -2528,7 +3357,8 @@ bool p_word_has_linuxcnc_use(const std::vector &word_list, int modal_motio 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)) { + g_code_is(word.value, 6.2) || + linuxcnc_g_code_is_g70_finishing_cycle(word.value)) { return true; } const int g = rounded_value(word.value); @@ -2549,14 +3379,17 @@ bool p_word_has_linuxcnc_use(const std::vector &word_list, int modal_motio } bool q_word_has_linuxcnc_use(const std::vector &word_list, int modal_motion) { - if (modal_motion == 73 || modal_motion == 76 || modal_motion == 83) { + if (modal_motion == 70 || 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)) { + g_code_is(word.value, 6.2) || + linuxcnc_g_code_is_g70_finishing_cycle(word.value) || + linuxcnc_g_code_is_g71_g72_roughing_cycle(word.value)) { return true; } const int g = rounded_value(word.value); @@ -2584,7 +3417,8 @@ bool r_word_has_linuxcnc_use(const std::vector &word_list, int modal_motio 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)) { + g_code_is(word.value, 6.2) || + linuxcnc_g_code_is_g71_g72_roughing_cycle(word.value)) { return true; } const int g = rounded_value(word.value); @@ -2640,7 +3474,8 @@ bool i_word_has_linuxcnc_use(const std::vector &word_list, int modal_motio 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)) { + g_code_is(word.value, 33.1) || + linuxcnc_g_code_is_g71_g72_roughing_cycle(word.value)) { return true; } const int g = rounded_value(word.value); @@ -2750,6 +3585,215 @@ bool pose_near(const CncSimPose &lhs, const CncSimPose &rhs) { std::fabs(lhs.w - rhs.w) < kTolerance; } +CncSimPose current_position_in_active_system(const CncSimPose &position, + const std::unordered_map &g5x_offsets, + const std::unordered_map &g5x_rotations, + int active_g5x_index, + bool g92_applied, + const CncSimPose &g92_parameters) { + CncSimPose current = position; + const CncSimPose g5x_offset = g5x_offsets.count(active_g5x_index) ? g5x_offsets.at(active_g5x_index) : CncSimPose{}; + current.x -= g5x_offset.x; + current.y -= g5x_offset.y; + current.z -= g5x_offset.z; + current.a -= g5x_offset.a; + current.b -= g5x_offset.b; + current.c -= g5x_offset.c; + current.u -= g5x_offset.u; + current.v -= g5x_offset.v; + current.w -= g5x_offset.w; + const double rotation = g5x_rotations.count(active_g5x_index) ? g5x_rotations.at(active_g5x_index) : 0.0; + const double radians = -rotation * std::acos(-1.0) / 180.0; + const double c = std::cos(radians); + const double s = std::sin(radians); + const double rotated_x = current.x * c - current.y * s; + const double rotated_y = current.x * s + current.y * c; + current.x = rotated_x; + current.y = rotated_y; + if (g92_applied) { + current.x -= g92_parameters.x; + current.y -= g92_parameters.y; + current.z -= g92_parameters.z; + current.a -= g92_parameters.a; + current.b -= g92_parameters.b; + current.c -= g92_parameters.c; + current.u -= g92_parameters.u; + current.v -= g92_parameters.v; + current.w -= g92_parameters.w; + } + return current; +} + +double plane_first_component(const CncSimPose &pose, int plane) { + if (plane == 18) { + return pose.x; + } + if (plane == 19) { + return pose.y; + } + return pose.x; +} + +double plane_second_component(const CncSimPose &pose, int plane) { + if (plane == 18) { + return pose.z; + } + if (plane == 19) { + return pose.z; + } + return pose.y; +} + +double plane_vector_length(const CncSimPose &pose, int plane) { + return std::hypot(plane_first_component(pose, plane), plane_second_component(pose, plane)); +} + +double plane_unit_dot(const CncSimPose &lhs, const CncSimPose &rhs, int plane) { + const double lhs_len = plane_vector_length(lhs, plane); + const double rhs_len = plane_vector_length(rhs, plane); + if (lhs_len <= 0.0 || rhs_len <= 0.0) { + return 1.0; + } + return (plane_first_component(lhs, plane) * plane_first_component(rhs, plane) + + plane_second_component(lhs, plane) * plane_second_component(rhs, plane)) / (lhs_len * rhs_len); +} + +bool validate_arc_center_radius_consistency(const CncSimEvent &event, + int plane, + double unit_scale, + std::string *error) { + double start_first = 0.0; + double start_second = 0.0; + double end_first = 0.0; + double end_second = 0.0; + double center_first = 0.0; + double center_second = 0.0; + char axis_a = 'X'; + char axis_b = 'Y'; + if (plane == 17) { + start_first = event.start.x; + start_second = event.start.y; + end_first = event.end.x; + end_second = event.end.y; + center_first = event.center.x; + center_second = event.center.y; + axis_a = 'X'; + axis_b = 'Y'; + } else if (plane == 18) { + start_first = event.start.x; + start_second = event.start.z; + end_first = event.end.x; + end_second = event.end.z; + center_first = event.center.x; + center_second = event.center.z; + axis_a = 'X'; + axis_b = 'Z'; + } else { + start_first = event.start.y; + start_second = event.start.z; + end_first = event.end.y; + end_second = event.end.z; + center_first = event.center.y; + center_second = event.center.z; + axis_a = 'Y'; + axis_b = 'Z'; + } + + const double radius1 = std::hypot(center_first - start_first, center_second - start_second); + const double radius2 = std::hypot(center_first - end_first, center_second - end_second); + constexpr double kMmPerInch = 25.4; + constexpr double kRadiusToleranceInch = 0.00005; + constexpr double kCenterArcRadiusToleranceInch = 2.0 * 0.001 * 1.4142135623730951; + constexpr double kCenterArcRadiusToleranceMm = 2.0 * 0.01 * 1.4142135623730951; + constexpr double kSpiralRelativeTolerance = 0.001; + const double radius_tolerance = kRadiusToleranceInch * kMmPerInch; + if (radius1 < radius_tolerance || radius2 < radius_tolerance) { + if (error) { + std::ostringstream out; + out << std::fixed << std::setprecision(4); + out << "Zero-radius arc: " + << "start=(" << axis_a << start_first + << "," << axis_b << start_second + << ") center=(" << axis_a << center_first + << "," << axis_b << center_second + << ") end=(" << axis_a << end_first + << "," << axis_b << end_second + << ") r1=" << radius1 + << " r2=" << radius2; + *error = out.str(); + } + return false; + } + const double abs_err = std::fabs(radius1 - radius2); + const double larger_radius = std::max(radius1, radius2); + const double rel_err = larger_radius > 0.0 ? abs_err / larger_radius : 0.0; + const double spiral_abs_tolerance = unit_scale == 25.4 + ? kCenterArcRadiusToleranceInch * kMmPerInch + : kCenterArcRadiusToleranceMm; + if ((abs_err > spiral_abs_tolerance * 100.0) || + (rel_err > kSpiralRelativeTolerance && abs_err > spiral_abs_tolerance)) { + if (error) { + std::ostringstream out; + out << std::fixed << std::setprecision(4); + out << "Radius to end of arc differs from radius to start: " + << "start=(" << axis_a << start_first + << "," << axis_b << start_second + << ") center=(" << axis_a << center_first + << "," << axis_b << center_second + << ") end=(" << axis_a << end_first + << "," << axis_b << end_second + << ") r1=" << radius1 + << " r2=" << radius2 + << " abs_err=" << abs_err + << " rel_err=" << rel_err * 100.0 << "%"; + *error = out.str(); + } + return false; + } + return true; +} + +bool validate_r_arc_reaches_end_point(const CncSimEvent &event, int plane, double radius, std::string *error) { + double start_first = 0.0; + double start_second = 0.0; + double end_first = 0.0; + double end_second = 0.0; + if (plane == 17) { + start_first = event.start.x; + start_second = event.start.y; + end_first = event.end.x; + end_second = event.end.y; + } else if (plane == 18) { + start_first = event.start.x; + start_second = event.start.z; + end_first = event.end.x; + end_second = event.end.z; + } else { + start_first = event.start.y; + start_second = event.start.z; + end_first = event.end.y; + end_second = event.end.z; + } + const double mid_first = (start_first + end_first) * 0.5; + const double mid_second = (start_second + end_second) * 0.5; + const double half_length = std::hypot(mid_first - end_first, mid_second - end_second); + const double abs_radius = std::fabs(radius); + const double tolerance = 0.00127; + if (start_first == end_first && start_second == end_second) { + if (error) { + *error = "Current point same as end point of arc"; + } + return false; + } + if ((half_length - abs_radius) > tolerance) { + if (error) { + *error = "Arc radius too small to reach end point"; + } + return false; + } + return true; +} + bool modal_motion_is_threading(int modal_motion) { return modal_motion == kModalMotionG33 || modal_motion == kModalMotionG331; @@ -2759,6 +3803,26 @@ bool modal_motion_is_threading_cycle(int modal_motion) { return modal_motion == kModalMotionG76; } +bool modal_motion_accepts_polar_coordinates(int modal_motion) { + return 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 == 70 || + modal_motion == 710 || + modal_motion == 711 || + modal_motion == 712 || + modal_motion == 720 || + modal_motion == 721 || + modal_motion == 722; +} + +bool parsed_word_list_has_polar_coordinate(const ParsedWordList &parsed) { + return parsed.radius_flag || parsed.theta_flag; +} + } // namespace SmokeGcodeParser::SmokeGcodeParser(CanonEventSink &sink) @@ -2800,10 +3864,98 @@ CncSimPose smoke_tool_table_offset(int h_code, double unit_scale) { CncSimPose offset{}; if (h_code == 1) { offset.z = 12.5 * unit_scale; + } else if (h_code == 4) { + offset.z = 0.7 * unit_scale; } return offset; } +double smoke_tool_table_diameter(int tool_id, double unit_scale) { + if (tool_id == 1) { + return 6.0 * unit_scale; + } + if (tool_id == 4) { + return 1.0 * unit_scale; + } + return 0.0; +} + +double smoke_tool_table_frontangle(int tool_id) { + if (tool_id == 1) { + return 12.0; + } + return 0.0; +} + +double smoke_tool_table_backangle(int tool_id) { + if (tool_id == 1) { + return 34.0; + } + return 0.0; +} + +double smoke_tool_table_orientation(int tool_id) { + if (tool_id == 1) { + return 5.0; + } + return 0.0; +} + +bool load_smoke_parameter_file(const char *path, std::unordered_map *parameters) { + if (!path || !*path) { + return true; + } + std::ifstream input(path); + if (!input) { + return false; + } + std::string line; + while (std::getline(input, line)) { + std::istringstream row(line); + int index = 0; + double value = 0.0; + if ((row >> index >> value) && index > 0 && index < kMaxNumberedParameter) { + (*parameters)[index] = value; + } + } + return true; +} + +bool save_smoke_parameter_file(const char *path, const std::unordered_map ¶meters) { + if (!path || !*path) { + return true; + } + std::map merged; + { + std::ifstream input(path); + std::string line; + while (std::getline(input, line)) { + std::istringstream row(line); + int index = 0; + double value = 0.0; + if ((row >> index >> value) && index > 0 && index < kMaxNumberedParameter) { + merged[index] = value; + } + } + } + for (const auto &[index, value] : parameters) { + if (is_smoke_system_persistent_numbered_parameter(index) || + (index >= 5001 && index <= 5060 && merged.find(index) != merged.end())) { + merged[index] = value; + } + } + std::ofstream output(path, std::ios::trunc); + if (!output) { + return false; + } + output.setf(std::ios::fixed); + output << std::setprecision(6); + for (const auto &[index, value] : merged) { + output << index << '\t' << value << '\n'; + } + return true; +} + } // namespace int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string *error) { @@ -2820,9 +3972,20 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string arc_absolute_ = false; lathe_diameter_mode_ = false; cutter_comp_on_ = false; + cutter_comp_exit_pending_ = false; + cutter_comp_has_prev_vector_ = false; + cutter_comp_has_prev_prev_vector_ = false; + cutter_comp_radius_ = 0.0; + cutter_comp_prev_vector_ = {}; + cutter_comp_prev_prev_vector_ = {}; + cutter_comp_mode_ = 40; + spindle_css_mode_ = false; + spindle_tool_id_ = 0; + selected_tool_id_ = 0; feed_mode_ = 0; canned_cycle_ = 0; canned_return_to_initial_ = false; + g92_applied_ = false; canned_has_r_ = false; canned_has_z_ = false; canned_has_p_ = false; @@ -2839,16 +4002,50 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string canned_i_ = 0.0; canned_j_ = 0.0; canned_k_ = 0.0; + m66_result_ = 0.0; oword_return_value_ = 0.0; oword_value_returned_ = false; + // Match the source-linked backend's initial interpreter state publication. + sink_.use_length_units(1.0, 0); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + sink_.set_g5x_offset(1, CncSimPose{}, 0); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + sink_.set_g92_offset(CncSimPose{}, 0); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + sink_.set_xy_rotation(0.0, 0); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + const std::string source(program, program + program_len); - const std::vector lines = split_source_lines(source); + std::vector lines; + if (!linuxcnc_effective_program_lines(split_source_lines(source), &lines, error)) { + return -1; + } + for (const SourceLine &source_line : lines) { + if (!validate_linuxcnc_comments(source_line.raw, error)) { + return -1; + } + } std::unordered_map subprograms_by_header; const std::unordered_map subprograms = discover_subprograms(lines, &subprograms_by_header); + if (!validate_numeric_oword_main_structure(lines, error)) { + return -1; + } + if (!validate_no_nested_subprogram_definitions(lines, error)) { + return -1; + } std::vector call_stack; std::unordered_map parameters; std::unordered_map named_parameters; + parameters[5599] = 1.0; std::vector conditional_stack; std::unordered_map loop_iterations; std::unordered_map g5x_offsets; @@ -2869,6 +4066,49 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string int active_g5x_index = 1; size_t pc = 0; int modal_motion = -1; + int motion_control_mode_comment = 612; + double motion_control_mode_feed = 0.0; + const char *parameter_file_path = std::getenv("CNC_SIM_RS274_VAR"); + if (const char *spindle_tool = std::getenv("CNC_SIM_TOOL_IN_SPINDLE")) { + spindle_tool_id_ = std::atoi(spindle_tool); + } + + if (parameter_file_path && *parameter_file_path) { + if (!load_smoke_parameter_file(parameter_file_path, ¶meters)) { + if (error) { + *error = "failed to load parameter file"; + } + return -1; + } + auto load_pose_parameters = [&](int start_index, CncSimPose *pose) { + pose->x = parameters.count(start_index + 0) ? parameters[start_index + 0] : 0.0; + pose->y = parameters.count(start_index + 1) ? parameters[start_index + 1] : 0.0; + pose->z = parameters.count(start_index + 2) ? parameters[start_index + 2] : 0.0; + pose->a = parameters.count(start_index + 3) ? parameters[start_index + 3] : 0.0; + pose->b = parameters.count(start_index + 4) ? parameters[start_index + 4] : 0.0; + pose->c = parameters.count(start_index + 5) ? parameters[start_index + 5] : 0.0; + pose->u = parameters.count(start_index + 6) ? parameters[start_index + 6] : 0.0; + pose->v = parameters.count(start_index + 7) ? parameters[start_index + 7] : 0.0; + pose->w = parameters.count(start_index + 8) ? parameters[start_index + 8] : 0.0; + }; + load_pose_parameters(5161, &g28_home); + load_pose_parameters(5181, &g30_home); + g92_applied_ = parameters.count(5210) ? parameters[5210] != 0.0 : false; + load_pose_parameters(5211, &g92_parameters); + if (parameters.count(5220)) { + const int persisted_g5x = rounded_value(parameters[5220]); + if (persisted_g5x >= 1 && persisted_g5x <= 9) { + active_g5x_index = persisted_g5x; + } + } + for (int g5x_index = 1; g5x_index <= 9; ++g5x_index) { + const int base = 5221 + (g5x_index - 1) * 20; + CncSimPose offset{}; + load_pose_parameters(base, &offset); + g5x_offsets[g5x_index] = offset; + g5x_rotations[g5x_index] = parameters.count(base + 9) ? parameters[base + 9] : 0.0; + } + } auto current_modal_state = [&]() { return ModalState{absolute_, arc_absolute_, lathe_diameter_mode_, sink_.plane(), sink_.unit_scale()}; @@ -2908,6 +4148,17 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string frame->modal_unit_scale = state.unit_scale; }; auto refresh_readonly_named_parameters = [&](int line_number) { + auto set_pose_parameters = [&](int start_index, const CncSimPose &pose) { + parameters[start_index + 0] = pose.x; + parameters[start_index + 1] = pose.y; + parameters[start_index + 2] = pose.z; + parameters[start_index + 3] = pose.a; + parameters[start_index + 4] = pose.b; + parameters[start_index + 5] = pose.c; + parameters[start_index + 6] = pose.u; + parameters[start_index + 7] = pose.v; + parameters[start_index + 8] = pose.w; + }; 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; @@ -2960,14 +4211,20 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string 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["_ccomp"] = static_cast(cutter_comp_mode_ * 10); + named_parameters["_CCOMP"] = named_parameters["_ccomp"]; + named_parameters["_current_tool"] = static_cast(spindle_tool_id_); named_parameters["_CURRENT_TOOL"] = named_parameters["_current_tool"]; - named_parameters["_selected_tool"] = static_cast(sink_.selected_tool()); + named_parameters["_selected_tool"] = static_cast(selected_tool_id_); named_parameters["_SELECTED_TOOL"] = named_parameters["_selected_tool"]; - named_parameters["_current_pocket"] = static_cast(sink_.selected_tool()); + named_parameters["_current_pocket"] = static_cast(spindle_tool_id_); named_parameters["_CURRENT_POCKET"] = named_parameters["_current_pocket"]; - named_parameters["_selected_pocket"] = static_cast(sink_.selected_tool()); + named_parameters["_selected_pocket"] = static_cast(selected_tool_id_); named_parameters["_SELECTED_POCKET"] = named_parameters["_selected_pocket"]; + named_parameters["_spindle_rpm_mode"] = spindle_css_mode_ ? 0.0 : 1.0; + named_parameters["_SPINDLE_RPM_MODE"] = named_parameters["_spindle_rpm_mode"]; + named_parameters["_spindle_css_mode"] = spindle_css_mode_ ? 1.0 : 0.0; + named_parameters["_SPINDLE_CSS_MODE"] = named_parameters["_spindle_css_mode"]; named_parameters["_line"] = static_cast(line_number); named_parameters["_LINE"] = named_parameters["_line"]; named_parameters["_value"] = oword_return_value_; @@ -3028,8 +4285,74 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string named_parameters["_ABS_U"] = position.u; named_parameters["_ABS_V"] = position.v; named_parameters["_ABS_W"] = position.w; + + const CncSimPose &probe_position = sink_.probe_position(); + parameters[5061] = probe_position.x; + parameters[5062] = probe_position.y; + parameters[5063] = probe_position.z; + parameters[5064] = probe_position.a; + parameters[5065] = probe_position.b; + parameters[5066] = probe_position.c; + parameters[5067] = probe_position.u; + parameters[5068] = probe_position.v; + parameters[5069] = probe_position.w; + parameters[5070] = sink_.probe_tripped() ? 1.0 : 0.0; + + set_pose_parameters(5161, g28_home); + set_pose_parameters(5181, g30_home); + parameters[5210] = g92_applied_ ? 1.0 : 0.0; + set_pose_parameters(5211, g92_parameters); + parameters[5220] = static_cast(active_g5x_index); + for (int g5x_index = 1; g5x_index <= 9; ++g5x_index) { + const int base = 5221 + (g5x_index - 1) * 20; + const CncSimPose offset = g5x_offsets.count(g5x_index) ? g5x_offsets[g5x_index] : CncSimPose{}; + set_pose_parameters(base, offset); + parameters[base + 9] = g5x_rotations.count(g5x_index) ? g5x_rotations[g5x_index] : 0.0; + } + parameters[5399] = m66_result_; + + parameters[5400] = static_cast(spindle_tool_id_); + parameters[5401] = tool_length_offset.x; + parameters[5402] = tool_length_offset.y; + parameters[5403] = tool_length_offset.z; + parameters[5404] = tool_length_offset.a; + parameters[5405] = tool_length_offset.b; + parameters[5406] = tool_length_offset.c; + parameters[5407] = tool_length_offset.u; + parameters[5408] = tool_length_offset.v; + parameters[5409] = tool_length_offset.w; + parameters[5410] = smoke_tool_table_diameter(spindle_tool_id_, sink_.unit_scale()); + parameters[5411] = smoke_tool_table_frontangle(spindle_tool_id_); + parameters[5412] = smoke_tool_table_backangle(spindle_tool_id_); + parameters[5413] = smoke_tool_table_orientation(spindle_tool_id_); + + parameters[5420] = position.x; + parameters[5421] = position.y; + parameters[5422] = position.z; + parameters[5423] = position.a; + parameters[5424] = position.b; + parameters[5425] = position.c; + parameters[5426] = position.u; + parameters[5427] = position.v; + parameters[5428] = position.w; }; + if (parameter_file_path && *parameter_file_path) { + const CncSimPose active_g5x_offset = g5x_offsets.count(active_g5x_index) ? g5x_offsets[active_g5x_index] : CncSimPose{}; + sink_.set_g5x_offset(active_g5x_index, active_g5x_offset, 0); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + sink_.set_xy_rotation(g5x_rotations.count(active_g5x_index) ? g5x_rotations[active_g5x_index] : 0.0, 0); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + sink_.set_g92_offset(g92_applied_ ? g92_parameters : CncSimPose{}, 0); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + } + while (pc < lines.size()) { const size_t current_pc = pc; ++pc; @@ -3037,7 +4360,10 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string const int line_number = lines[current_pc].number; std::string comment; std::string line = strip_comments(raw_line, &comment); - if (!comment.empty()) { + const std::string trimmed_hot_comment = trim(comment); + const std::string compact_hot_comment = lower_compact(trimmed_hot_comment); + const bool debug_hot_comment = compact_hot_comment.rfind("debug,", 0) == 0; + if (!comment.empty() && !debug_hot_comment) { CncSimEvent event{}; event.version = 1; event.type = CNC_SIM_EVENT_COMMENT; @@ -3082,6 +4408,9 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string call_stack.back().params[static_cast(assignment.index)] = assignment.value; } else { parameters[assignment.index] = assignment.value; + if (assignment.index == 5399) { + m66_result_ = assignment.value; + } } } pending_assignments.clear(); @@ -3109,25 +4438,70 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string } const std::string keyword_line = line; line = expanded_line; - std::vector word_list; - if (!parse_word_list_strict(line, &word_list, error)) { + if (!validate_linuxcnc_top_level_reader_symbols(line, error)) { return -1; } + ParsedWordList parsed_word_list; + if (!parse_word_list_impl(line, &parsed_word_list, error, true)) { + return -1; + } + std::vector &word_list = parsed_word_list.words; if (!check_linuxcnc_modal_group_conflicts(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) { + const std::string trimmed_comment = trim(comment); + if (!trimmed_comment.empty()) { + const std::string compact_comment = lower_compact(trimmed_comment); + if (compact_comment.rfind("abort,", 0) == 0) { + const size_t comma = trimmed_comment.find(','); + std::string abort_text = comma == std::string::npos ? std::string{} : trim(trimmed_comment.substr(comma + 1)); + std::string abort_error; + const std::string expanded_abort = expand_expressions_and_parameters(abort_text, + call_stack, + parameters, + named_parameters, + &abort_error); + if (error) { + *error = abort_error.empty() ? expanded_abort : abort_text; + } + return -1; + } + } + if (word_list.empty() && !has_o_word && + !parsed_word_list.dollar_flag && + !parsed_word_list_has_polar_coordinate(parsed_word_list)) { continue; } auto words = last_words_by_letter(word_list); std::vector m_codes; + const int selected_spindle = parsed_word_list.dollar_flag + ? smoke_resolved_spindle_selector(parsed_word_list.dollar_value) + : 0; const int previous_canned_cycle = canned_cycle_; bool machine_coordinate_move = false; bool current_line_has_probe_motion = false; + auto clear_g5_nurbs_collection = [&]() { + nurbs_g5_collecting_ = false; + nurbs_g5_control_points_ = 0; + nurbs_g5_order_ = 3; + }; + auto clear_g6_nurbs_collection = [&]() { + nurbs_g6_collecting_ = false; + }; + const bool has_axis_words = has_axis_word(words); + const bool mode_zero_uses_axes = has_g_code(word_list, 10.0) || + has_g_code(word_list, 28.0) || + has_g_code(word_list, 30.0) || + has_g_code(word_list, 52.0) || + has_g_code(word_list, 92.0); + const bool has_non_g80_motion_g_code = has_motion_g_code(word_list) && + !std::all_of(word_list.begin(), word_list.end(), [](const Word &word) { + return word.letter != 'G' || g_code_is(word.value, 80.0); + }); int h_word_value = 0; const bool has_h_word = words.count('H') != 0; if (has_h_word) { @@ -3159,12 +4533,15 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string !has_g_code(word_list, 41.1) && !has_g_code(word_list, 42.0) && !has_g_code(word_list, 42.1) && + !std::any_of(word_list.begin(), word_list.end(), [](const Word &word) { + return word.letter == 'G' && linuxcnc_g_code_is_g7x_cycle(word.value); + }) && !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"; + *error = "D word with no G41, G41.1, G42, G42.1, G70, G71, G72, G73, G83 or G96 to use it"; } return -1; } @@ -3181,6 +4558,75 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string } return -1; } + if (parsed_word_list.dollar_flag && + !has_g_code(word_list, 33.0) && + !has_g_code(word_list, 33.1) && + !has_g_code(word_list, 76.0) && + !has_g_code(word_list, 95.0) && + !has_g_code(word_list, 96.0) && + !has_g_code(word_list, 97.0) && + !has_m_code(word_list, 3) && + !has_m_code(word_list, 4) && + !has_m_code(word_list, 5) && + !has_m_code(word_list, 19) && + !has_m_code(word_list, 51) && + !words.count('S')) { + if (error) { + *error = "$ (spindle selection) word with no M3, M4, M5, M19, M51, G33, G33.1, G76, G95, G96 or G97 to use it"; + } + return -1; + } + if (parsed_word_list_has_polar_coordinate(parsed_word_list) && + !has_motion_g_code(word_list) && + !has_g_code(word_list, 10.0) && + !has_g_code(word_list, 28.0) && + !has_g_code(word_list, 30.0) && + !has_g_code(word_list, 52.0) && + !has_g_code(word_list, 92.0) && + !has_g_code(word_list, 43.2) && + !modal_motion_accepts_polar_coordinates(modal_motion)) { + if (error) { + *error = "Cannot use axis values without a g code that uses them"; + } + return -1; + } + if (parsed_word_list_has_polar_coordinate(parsed_word_list) && has_g_code(word_list, 80.0) && + !mode_zero_uses_axes) { + if (error) { + *error = "Cannot use axis values with G80"; + } + return -1; + } + if (parsed_word_list_has_polar_coordinate(parsed_word_list) && + (has_g_code(word_list, 10.0) || + has_g_code(word_list, 43.1) || + has_g_code(word_list, 28.0) || + has_g_code(word_list, 30.0) || + has_g_code(word_list, 92.0))) { + if (error) { + *error = "Polar coordinates can only be used for motion"; + } + return -1; + } + if (parsed_word_list_has_polar_coordinate(parsed_word_list) && sink_.plane() != 17) { + if (error) { + *error = "Cannot use polar coordinate except in G17 plane"; + } + return -1; + } + if (parsed_word_list_has_polar_coordinate(parsed_word_list) && has_g_code(word_list, 92.0)) { + if (error) { + *error = "Polar coordinates can only be used for motion"; + } + return -1; + } + if (parsed_word_list_has_polar_coordinate(parsed_word_list) && + (words.count('X') || words.count('Y'))) { + if (error) { + *error = "Cannot specify X or Y words with polar coordinate"; + } + 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"; @@ -3242,11 +4688,26 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string } 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"; + if (words.count('L')) { + int l_word_value = 0; + if (!integer_word_value(words, 'L', &l_word_value)) { + if (error) { + *error = "Non-integer L word used"; + } + return -1; + } + if (l_word_value < 0) { + if (error) { + *error = "Negative L word used"; + } + return -1; + } + if (!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; } - return -1; } if (words.count('I') && !i_word_has_linuxcnc_use(word_list, modal_motion)) { if (error) { @@ -3266,6 +4727,38 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string } return -1; } + const bool has_g7x_cycle = std::any_of(word_list.begin(), word_list.end(), [](const Word &word) { + return word.letter == 'G' && linuxcnc_g_code_is_g7x_cycle(word.value); + }); + const bool has_g71_g72_roughing_cycle = std::any_of(word_list.begin(), word_list.end(), [](const Word &word) { + return word.letter == 'G' && linuxcnc_g_code_is_g71_g72_roughing_cycle(word.value); + }); + if (has_g7x_cycle) { + if (!words.count('Q')) { + if (error) { + *error = "G7x.x requires a Q word"; + } + return -1; + } + if (sink_.plane() != 18) { + if (error) { + *error = "G7x.x can only be used in XZ plane (G18)"; + } + return -1; + } + if (has_g71_g72_roughing_cycle && cutter_comp_on_) { + if (error) { + *error = "G71/G72 cannot be used with cutter compensation enabled"; + } + return -1; + } + } + if (mode_zero_uses_axes && has_non_g80_motion_g_code) { + if (error) { + *error = "Cannot use two G codes that both use axis values"; + } + 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; @@ -3536,6 +5029,12 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string bool coordinate_control = false; if (has_g_code(word_list, 10.0)) { const int l = rounded_word(words, 'L', 0); + if (l != 0 && l != 1 && l != 2 && l != 10 && l != 11 && l != 20) { + if (error) { + *error = "Line with G10 does not have L0, L1, L10, L11, L2, or L20"; + } + return -1; + } if (words.count('P') && std::fabs(static_cast(rounded_word(words, 'P', 0)) - words.at('P')) > 0.0001) { if (error) { @@ -3550,6 +5049,48 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string } return -1; } + if ((l == 1 || l == 10 || l == 11) && requested_p < 1) { + if (error) { + *error = "P value out of range with G10 L1/L10/L11"; + } + return -1; + } + if (l == 1 || l == 10 || l == 11) { + const bool has_tool_offset_word = + has_axis_word(words) || words.count('R') || words.count('Q') || + words.count('I') || words.count('J'); + if (!has_tool_offset_word) { + if (error) { + *error = "G10 L1 without offsets has no effect"; + } + return -1; + } + if (words.count('Q')) { + int orientation = 0; + if (!integer_word_value(words, 'Q', &orientation)) { + if (error) { + *error = "Q number in G10 is not an integer"; + } + return -1; + } + if (orientation > 9) { + if (error) { + *error = "Invalid tool orientation"; + } + return -1; + } + } + coordinate_control = true; + } + if (l == 0) { + if (spindle_tool_id_ > 0) { + if (error) { + *error = "G10 L0 not allowed with loaded tool"; + } + return -1; + } + coordinate_control = true; + } 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'))) { @@ -3623,7 +5164,14 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string return -1; } const int h_code = has_h_word ? h_word_value : sink_.selected_tool(); + if (has_h_word && !smoke_tool_number_exists(h_code)) { + if (error) { + *error = "Requested tool " + std::to_string(h_code) + " not found in the tool table"; + } + return -1; + } tool_length_offset = smoke_tool_table_offset(h_code, sink_.unit_scale()); + sink_.apply_tool_length_offset(tool_length_offset); emit_tool_length_offset(sink_, tool_length_offset, line_number); if (stop_if_callback_aborted(sink_, error)) { return -1; @@ -3642,6 +5190,7 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string return -1; } const bool has_tool_axis_word = has_axis_word(words); + const bool has_polar_tool_axis = parsed_word_list_has_polar_coordinate(parsed_word_list); 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"; @@ -3666,6 +5215,12 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string g_code_is(word.value, 43.2) ? pose_axis(offset, axis) + value : value); } if (g_code_is(word.value, 43.2) && has_h_word) { + if (!smoke_tool_number_exists(h_word_value)) { + if (error) { + *error = "Requested tool " + std::to_string(h_word_value) + " not found in the tool table"; + } + return -1; + } const CncSimPose table_offset = smoke_tool_table_offset(h_word_value, sink_.unit_scale()); offset.x += table_offset.x; offset.y += table_offset.y; @@ -3678,6 +5233,7 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string offset.w += table_offset.w; } tool_length_offset = offset; + sink_.apply_tool_length_offset(tool_length_offset); emit_tool_length_offset(sink_, tool_length_offset, line_number); if (stop_if_callback_aborted(sink_, error)) { return -1; @@ -3730,9 +5286,23 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string } if (cutter_comp_off) { cutter_comp_on_ = false; + cutter_comp_exit_pending_ = true; + cutter_comp_has_prev_vector_ = false; + cutter_comp_has_prev_prev_vector_ = false; + cutter_comp_radius_ = 0.0; + cutter_comp_prev_vector_ = {}; + cutter_comp_prev_prev_vector_ = {}; + cutter_comp_mode_ = 40; emit_comment_state(sink_, line_number, kCutterCompOffEvent); } else { cutter_comp_on_ = true; + cutter_comp_exit_pending_ = false; + cutter_comp_has_prev_vector_ = false; + cutter_comp_has_prev_prev_vector_ = false; + cutter_comp_radius_ = words.count('D') ? std::fabs(words.at('D')) * sink_.unit_scale() : 0.0; + cutter_comp_prev_vector_ = {}; + cutter_comp_prev_prev_vector_ = {}; + cutter_comp_mode_ = cutter_comp_left ? 41 : 42; emit_comment_state(sink_, line_number, explicit_comp ? (cutter_comp_left ? kCutterCompLeftExplicitEvent : kCutterCompRightExplicitEvent) : (cutter_comp_left ? kCutterCompLeftEvent : kCutterCompRightEvent)); @@ -3748,6 +5318,7 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string return -1; } tool_length_offset = {}; + sink_.apply_tool_length_offset(tool_length_offset); emit_tool_length_offset(sink_, tool_length_offset, line_number); if (stop_if_callback_aborted(sink_, error)) { return -1; @@ -3760,32 +5331,38 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string lathe_diameter_mode_ = true; } else if (g == 8) { lathe_diameter_mode_ = false; - } else if (g == 17 || g == 18 || g == 19) { + } else if (g == 17 || g == 18 || g == 19 || + g_code_is(word.value, 17.1) || + g_code_is(word.value, 18.1) || + g_code_is(word.value, 19.1)) { + const int selected_plane = g_code_is(word.value, 17.1) ? 171 : + g_code_is(word.value, 18.1) ? 181 : + g_code_is(word.value, 19.1) ? 191 : g; if (cutter_comp_on_ && - ((g == 17 && sink_.plane() != 17) || - (g == 18 && sink_.plane() != 18) || - (g == 19 && sink_.plane() != 19))) { + ((selected_plane == 17 && sink_.plane() != 17) || + (selected_plane == 18 && sink_.plane() != 18) || + (selected_plane == 19 && sink_.plane() != 19))) { if (error) { *error = "Cannot change planes with cutter radius compensation on"; } return -1; } - if (cutter_comp_on_ && g == 19) { + if (cutter_comp_on_ && selected_plane == 19) { if (error) { *error = "Cutter radius compensation allowed only in XY or XZ planes"; } return -1; } - sink_.select_plane(g, line_number); + sink_.select_plane(selected_plane, line_number); if (stop_if_callback_aborted(sink_, error)) { return -1; } - } else if (g == 20 || g == 70) { + } else if (g == 20) { sink_.use_length_units(25.4, line_number); if (stop_if_callback_aborted(sink_, error)) { return -1; } - } else if (g == 21 || g == 71) { + } else if (g == 21) { sink_.use_length_units(1.0, line_number); if (stop_if_callback_aborted(sink_, error)) { return -1; @@ -3814,6 +5391,12 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string } coordinate_control = true; } else if (g == 53) { + if (parsed_word_list.radius_flag || parsed_word_list.theta_flag) { + if (error) { + *error = "Cannot use polar coordinates with G53"; + } + return -1; + } machine_coordinate_move = true; } else if (g_code_is(word.value, 90.1)) { arc_absolute_ = true; @@ -3824,12 +5407,19 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string } else if (g == 91) { absolute_ = false; } else if (g == 93 || g == 94 || g == 95) { + if (parsed_word_list.dollar_flag && + !smoke_spindle_selector_valid_for_mode(parsed_word_list.dollar_value)) { + if (error) { + *error = "Invalid spindle ($) number in Spindle Feed command"; + } + return -1; + } emit_comment_state(sink_, line_number, g); if (stop_if_callback_aborted(sink_, error)) { return -1; } feed_mode_ = g == 95 ? 2 : (g == 93 ? 1 : 0); - sink_.set_feed_mode(0, g == 95 ? 1 : 0, line_number); + sink_.set_feed_mode(selected_spindle, g == 95 ? 1 : 0, line_number); if (stop_if_callback_aborted(sink_, error)) { return -1; } @@ -3840,8 +5430,16 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string } return -1; } + if (parsed_word_list.dollar_flag && + !smoke_spindle_selector_valid_for_mode(parsed_word_list.dollar_value)) { + if (error) { + *error = "Invalid spindle ($) number in Spindle Mode command"; + } + 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); + spindle_css_mode_ = (g == 96); + sink_.set_spindle_mode(selected_spindle, mode, line_number); if (stop_if_callback_aborted(sink_, error)) { return -1; } @@ -3852,7 +5450,9 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string } return -1; } - emit_comment_state(sink_, line_number, g_code_is(word.value, 61.1) ? 612 : 611); + motion_control_mode_comment = g_code_is(word.value, 61.1) ? 612 : 611; + motion_control_mode_feed = 0.0; + emit_comment_state(sink_, line_number, motion_control_mode_comment); if (stop_if_callback_aborted(sink_, error)) { return -1; } @@ -3869,6 +5469,8 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string event.line = line_number; event.feed = words.count('P') ? words.at('P') : 0.0; event.reserved = 640; + motion_control_mode_comment = 640; + motion_control_mode_feed = event.feed; sink_.emit_raw(event); if (stop_if_callback_aborted(sink_, error)) { return -1; @@ -3882,15 +5484,252 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string } modal_motion = kModalMotionG76; canned_cycle_ = 0; + clear_g5_nurbs_collection(); + } else if (g_code_is(word.value, 5.1) || g_code_is(word.value, 6.1)) { + if (cutter_comp_on_) { + if (error) { + *error = "Cannot convert spline with cutter radius compensation"; + } + return -1; + } + if (words.count('A') || words.count('B') || words.count('C')) { + if (error) { + *error = "Splines may not have motion in A, B, or C"; + } + return -1; + } + if (!words.count('I') || !words.count('J')) { + if (error) { + *error = "Must specify both I and J with G5.1 or G6.1"; + } + return -1; + } + if (feed_mode_ == 0 && sink_.feed_rate() == 0.0) { + if (error) { + *error = "Cannot make arc with zero feed rate"; + } + return -1; + } + if (feed_mode_ == 1 && !words.count('F')) { + if (error) { + *error = "F word missing with inverse time arc move"; + } + return -1; + } + const CncSimPose start = sink_.position(); + CncSimPose control = start; + CncSimPose end = start; + apply_axes(&end, words, absolute_, sink_.unit_scale(), lathe_diameter_mode_); + if (sink_.plane() == 17) { + control.x += coordinate_word_value('I', words.at('I'), sink_.unit_scale(), lathe_diameter_mode_); + control.y += coordinate_word_value('J', words.at('J'), sink_.unit_scale(), lathe_diameter_mode_); + } else if (sink_.plane() == 18) { + control.x += coordinate_word_value('I', words.at('I'), sink_.unit_scale(), lathe_diameter_mode_); + control.z += coordinate_word_value('J', words.at('J'), sink_.unit_scale(), lathe_diameter_mode_); + } else if (sink_.plane() == 19) { + control.y += coordinate_word_value('I', words.at('I'), sink_.unit_scale(), lathe_diameter_mode_); + control.z += coordinate_word_value('J', words.at('J'), sink_.unit_scale(), lathe_diameter_mode_); + } + sink_.straight_feed(line_number, start); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + sink_.straight_feed(line_number, control); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + sink_.straight_feed(line_number, end); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + modal_motion = -1; + canned_cycle_ = 0; + clear_g5_nurbs_collection(); + clear_g6_nurbs_collection(); + } else if (g_code_is(word.value, 6.2)) { + if (sink_.plane() == 171 || sink_.plane() == 181 || sink_.plane() == 191) { + if (error) { + *error = "one plane must be selected for nurbs: XY, YZ, ZX"; + } + return -1; + } + if (sink_.plane() == 17 && (words.count('X') != words.count('Y'))) { + if (error) { + *error = "You must specify both X and Y coordinates for Control Points"; + } + return -1; + } + if (sink_.plane() == 19 && (words.count('Y') != words.count('Z'))) { + if (error) { + *error = "You must specify both Y and Z coordinates for Control Points"; + } + return -1; + } + if (sink_.plane() == 18 && (words.count('X') != words.count('Z'))) { + if (error) { + *error = "You must specify both X and Z coordinates for Control Points"; + } + return -1; + } + if (!nurbs_g6_collecting_ && !words.count('P')) { + if (error) { + *error = "Program the nurbs order P in the first block of instruction"; + } + return -1; + } + if (words.count('P')) { + int p_word_value = 0; + if (!integer_word_value(words, 'P', &p_word_value)) { + if (error) { + *error = "nurbs order P number is not an integer"; + } + return -1; + } + } + if (!nurbs_g6_collecting_ && (!words.count('Q') || words.at('Q') <= 0.0)) { + if (error) { + *error = "Program the nurbs Q value (1,2,3) in the first block of nurbs instruction"; + } + return -1; + } + if (words.count('Q')) { + int q_word_value = 0; + if (words.at('Q') < 1.0 || words.at('Q') > 3.0) { + if (error) { + *error = "nurbs Q value not in range 1 to 3"; + } + return -1; + } + if (!integer_word_value(words, 'Q', &q_word_value)) { + if (error) { + *error = "nurbs Q number is not an integer"; + } + return -1; + } + } + if (nurbs_g6_collecting_ && (!words.count('R') || words.at('R') <= 0.0)) { + if (error) { + *error = "Must specify positive nurbs weight R for every Control Point"; + } + return -1; + } + if (nurbs_g6_collecting_ && !words.count('K')) { + if (error) { + *error = "Must specify nurbs K number for every Control Point"; + } + return -1; + } + if (nurbs_g6_collecting_ && words.count('K') && words.at('K') < 0.0) { + if (error) { + *error = "Must specify nurbs K number for every Control Point"; + } + return -1; + } + if (words.count('K')) { + int k_word_value = 0; + if (!integer_word_value(words, 'K', &k_word_value)) { + if (error) { + *error = "nurbs K number is not an integer"; + } + return -1; + } + } + if (feed_mode_ == 0 && sink_.feed_rate() == 0.0) { + if (error) { + *error = "Cannot make a nurbs with 0 feedrate"; + } + return -1; + } + nurbs_g6_collecting_ = true; + } else if (g_code_is(word.value, 5.2)) { + const bool missing_xy_pair = sink_.plane() == 17 && + (words.count('X') != words.count('Y')); + const bool missing_xz_pair = sink_.plane() == 18 && + (words.count('X') != words.count('Z')); + const bool missing_yz_pair = sink_.plane() == 19 && + (words.count('Y') != words.count('Z')); + if (missing_xy_pair || missing_xz_pair || missing_yz_pair) { + if (error) { + *error = "You must specify both coordinates for G5.2 control points"; + } + return -1; + } + if (nurbs_g5_collecting_ && (!words.count('P') || words.at('P') <= 0.0)) { + if (error) { + *error = "Must specify positive weight P for every Control Point"; + } + return -1; + } + const bool has_g5_control_point = (sink_.plane() == 17 && words.count('X') && words.count('Y')) || + (sink_.plane() == 18 && words.count('X') && words.count('Z')) || + (sink_.plane() == 19 && words.count('Y') && words.count('Z')); + if (nurbs_g5_collecting_ && !has_g5_control_point && words.count('P') && words.at('P') > 0.0) { + if (error) { + *error = "Can specify P without control point only for the first G5.2 point"; + } + return -1; + } + if (feed_mode_ == 0 && sink_.feed_rate() == 0.0) { + if (error) { + *error = "Cannot make a NURBS with 0 feedrate"; + } + return -1; + } + if (feed_mode_ == 1 && !words.count('F')) { + if (error) { + *error = "F word missing with inverse time arc move"; + } + return -1; + } + if (!nurbs_g5_collecting_) { + nurbs_g5_control_points_ = 1; + nurbs_g5_order_ = 3; + } + if (words.count('L')) { + const int requested_order = rounded_word(words, 'L', nurbs_g5_order_); + if (requested_order > 3) { + nurbs_g5_order_ = requested_order; + } + } + if (has_g5_control_point) { + ++nurbs_g5_control_points_; + } + nurbs_g5_collecting_ = true; + modal_motion = -1; + canned_cycle_ = 0; + clear_g6_nurbs_collection(); + } else if (g_code_is(word.value, 5.3)) { + if (!nurbs_g5_collecting_) { + if (error) { + *error = "Cannot use G5.3 without G5.2 first"; + } + return -1; + } + if (nurbs_g5_control_points_ < nurbs_g5_order_) { + if (error) { + *error = "You must specify a number of control points at least equal to the order"; + } + return -1; + } + nurbs_g5_collecting_ = false; + nurbs_g5_control_points_ = 0; + nurbs_g5_order_ = 3; + clear_g6_nurbs_collection(); } else if (g_code_is(word.value, 33.1)) { modal_motion = kModalMotionG331; canned_cycle_ = 0; + clear_g5_nurbs_collection(); + clear_g6_nurbs_collection(); } else if (g_code_is(word.value, 33.0)) { modal_motion = kModalMotionG33; canned_cycle_ = 0; + clear_g5_nurbs_collection(); + clear_g6_nurbs_collection(); } else if (g == 0 || g == 1 || g == 2 || g == 3) { modal_motion = g; canned_cycle_ = 0; + clear_g5_nurbs_collection(); + clear_g6_nurbs_collection(); } else if (probe_type_for_g_code(word.value) >= 0) { if (cutter_comp_on_) { if (error) { @@ -3902,8 +5741,10 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string modal_motion = 382 + probe_type; canned_cycle_ = 0; current_line_has_probe_motion = true; + clear_g5_nurbs_collection(); + clear_g6_nurbs_collection(); } else if (g == 4) { - if (!words.count('P')) { + if (!words.count('P') || words.at('P') <= -1.0) { if (error) { *error = "Dwell time missing with G4"; } @@ -3976,6 +5817,12 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string } return -1; } + if (!has_axis_word(words)) { + if (error) { + *error = "All axes missing with G52 or G92"; + } + return -1; + } CncSimPose offset = g92_parameters; static constexpr char axes[] = {'X', 'Y', 'Z', 'A', 'B', 'C', 'U', 'V', 'W'}; for (char axis : axes) { @@ -3985,6 +5832,7 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string set_pose_axis(&offset, axis, coordinate_word_value(words, axis, sink_.unit_scale(), lathe_diameter_mode_)); } g92_parameters = offset; + g92_applied_ = true; sink_.set_g92_offset(offset, line_number); if (stop_if_callback_aborted(sink_, error)) { return -1; @@ -4001,6 +5849,7 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string if (stop_if_callback_aborted(sink_, error)) { return -1; } + g92_applied_ = false; if (g_code_is(word.value, 92.1)) { g92_parameters = {}; } @@ -4016,6 +5865,7 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string if (stop_if_callback_aborted(sink_, error)) { return -1; } + g92_applied_ = true; coordinate_control = true; } else if (g == 92) { if (cutter_comp_on_) { @@ -4024,27 +5874,48 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string } return -1; } + if (!has_axis_word(words)) { + if (error) { + *error = "All axes missing with G52 or G92"; + } + return -1; + } CncSimPose offset = g92_parameters; + const CncSimPose current_in_active_system = current_position_in_active_system(sink_.position(), + g5x_offsets, + g5x_rotations, + active_g5x_index, + g92_applied_, + 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); + set_pose_axis(&offset, axis, pose_axis(current_in_active_system, axis) - value); } g92_parameters = offset; + g92_applied_ = true; sink_.set_g92_offset(offset, line_number); if (stop_if_callback_aborted(sink_, error)) { return -1; } coordinate_control = true; } else if (g == 80) { + if (has_axis_words && !mode_zero_uses_axes && !has_non_g80_motion_g_code) { + if (error) { + *error = "Cannot use axis values with G80"; + } + return -1; + } canned_cycle_ = 0; modal_motion = -1; + clear_g5_nurbs_collection(); } 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; + clear_g5_nurbs_collection(); } else if (g == 98) { if (cutter_comp_on_) { if (error) { @@ -4063,7 +5934,33 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string canned_return_to_initial_ = false; } } else if (word.letter == 'M') { - m_codes.push_back(rounded_value(word.value)); + int m_code = 0; + std::unordered_map single_m_word{{'M', word.value}}; + if (!integer_word_value(single_m_word, 'M', &m_code)) { + if (error) { + *error = "Non-integer M code used"; + } + return -1; + } + if (m_code < 0) { + if (error) { + *error = "Negative M code used"; + } + return -1; + } + if (m_code > 199 && m_code != 428 && m_code != 429 && m_code != 430) { + if (error) { + *error = "M code greater than 199"; + } + return -1; + } + if (!smoke_m_code_has_linuxcnc_or_simulator_use(m_code)) { + if (error) { + *error = "Unknown M code used"; + } + return -1; + } + m_codes.push_back(m_code); } } if (m_codes.size() > 4) { @@ -4080,22 +5977,63 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string return -1; } if (words.count('F')) { + if (words.at('F') < 0.0) { + if (error) { + *error = "Negative F word used"; + } + return -1; + } sink_.set_feed_rate(words['F'] * sink_.unit_scale(), line_number); if (stop_if_callback_aborted(sink_, error)) { return -1; } } if (words.count('S')) { - sink_.set_spindle_speed(words['S'], line_number); + if (words.at('S') < 0.0) { + if (error) { + *error = "Negative spindle speed used"; + } + return -1; + } + if (parsed_word_list.dollar_flag && + !smoke_spindle_selector_valid_for_speed(parsed_word_list.dollar_value)) { + if (error) { + *error = "Invalid spindle ($) number in Spindle speed command"; + } + return -1; + } + sink_.set_spindle_speed(selected_spindle, words['S'], line_number); if (stop_if_callback_aborted(sink_, error)) { return -1; } } if (words.count('T')) { - sink_.select_tool(rounded_word(words, 'T', sink_.selected_tool())); + int tool_id = 0; + if (!integer_word_value(words, 'T', &tool_id)) { + if (error) { + *error = "Non-integer value for T word"; + } + return -1; + } + if (tool_id < 0) { + if (error) { + *error = "Negative tool ID used"; + } + return -1; + } + if (!smoke_tool_number_exists(tool_id)) { + if (error) { + *error = "Requested tool " + std::to_string(tool_id) + " not found in the tool table"; + } + return -1; + } + sink_.select_tool(tool_id); + selected_tool_id_ = tool_id; + tool_selected_ = true; } - bool program_end = false; + int program_end_m_code = 0; + std::vector deferred_program_stops; bool subprogram_control = false; if (oword_call) { const auto subprogram = subprograms.find(o_identifier); @@ -4120,6 +6058,7 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string CallFrame frame{}; frame.return_pc = pc; frame.sub_start = subprogram->second.start; + frame.conditional_depth = conditional_stack.size(); frame.remaining = 1; frame.oword = true; const std::vector args = parse_oword_call_arguments(keyword_line, call_stack, parameters, named_parameters, ¶meter_error); @@ -4147,6 +6086,7 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string !restore_modal_state(frame_modal_state(frame), line_number)) { return -1; } + conditional_stack.resize(frame.conditional_depth); oword_return_value_ = 0.0; oword_value_returned_ = false; const size_t bracket = keyword_line.find('['); @@ -4184,32 +6124,33 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string if (subprogram_control) { break; } + const int spindle = selected_spindle; if (m == 3) { - sink_.start_spindle(1, line_number); + sink_.start_spindle(spindle, 1, line_number); if (stop_if_callback_aborted(sink_, error)) { return -1; } } else if (m == 4) { - sink_.start_spindle(-1, line_number); + sink_.start_spindle(spindle, -1, line_number); if (stop_if_callback_aborted(sink_, error)) { return -1; } } else if (m == 5) { - sink_.stop_spindle(line_number); + sink_.stop_spindle(spindle, line_number); if (stop_if_callback_aborted(sink_, error)) { return -1; } } else if (m == 0) { - sink_.program_stop(line_number, 1); - if (stop_if_callback_aborted(sink_, error)) { - return -1; - } + deferred_program_stops.push_back(1); } else if (m == 1) { - sink_.program_stop(line_number, 2); - if (stop_if_callback_aborted(sink_, error)) { + deferred_program_stops.push_back(2); + } else if (m == 6) { + if (!tool_selected_) { + if (error) { + *error = "Txx missing for M6"; + } return -1; } - } else if (m == 6) { if (cutter_comp_on_) { if (error) { *error = "Cannot change tools with cutter radius compensation on"; @@ -4282,6 +6223,12 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string } return -1; } + if (parsed_word_list.dollar_flag) { + if (error) { + *error = "Invalid spindle ($) number in M51 command"; + } + 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; @@ -4354,6 +6301,12 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string } return -1; } + if (!smoke_tool_number_exists(tool_number)) { + if (error) { + *error = "Requested tool " + std::to_string(tool_number) + " not found in the tool table"; + } + return -1; + } sink_.select_tool(tool_number); sink_.change_tool(line_number); if (stop_if_callback_aborted(sink_, error)) { @@ -4375,12 +6328,6 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string 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, @@ -4439,9 +6386,19 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string if (stop_if_callback_aborted(sink_, error)) { return -1; } + if (input_type == 1) { + m66_result_ = m66_result_ != 0.0 ? 1.0 : 0.0; + } } else if (m == 19) { + const int spindle = selected_spindle; + if (parsed_word_list.dollar_flag && spindle < 0) { + if (error) { + *error = "Spindle ($) number out of range in M19 Command"; + } + return -1; + } if (words.count('R') || words.count('P')) { - sink_.orient_spindle(0, + sink_.orient_spindle(spindle, words.count('R') ? words.at('R') : 0.0, rounded_word(words, 'P', 0), line_number); @@ -4450,7 +6407,7 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string } } if (words.count('Q')) { - sink_.wait_spindle_orient_complete(0, words.at('Q'), line_number); + sink_.wait_spindle_orient_complete(spindle, words.at('Q'), line_number); if (stop_if_callback_aborted(sink_, error)) { return -1; } @@ -4498,10 +6455,8 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string return -1; } } else if (m == 60) { - sink_.program_stop(line_number, 3); - if (stop_if_callback_aborted(sink_, error)) { - return -1; - } + deferred_program_stops.push_back(3); + deferred_program_stops.push_back(1); } else if (m == 98) { int sub_number = -1; if (!integer_word_value(words, 'P', &sub_number)) { @@ -4565,25 +6520,25 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string subprogram_control = true; break; } else if (m == 2 || m == 30) { - sink_.program_end(line_number); - if (stop_if_callback_aborted(sink_, error)) { - return -1; - } - program_end = true; + program_end_m_code = m; } } if (subprogram_control) { commit_pending_assignments(); continue; } - if (program_end) { - commit_pending_assignments(); - break; - } if (coordinate_control) { commit_pending_assignments(); continue; } + if (cutter_comp_exit_pending_ && + (modal_motion == 2 || modal_motion == 3) && + (words.count('I') || words.count('J') || words.count('K') || words.count('R'))) { + if (error) { + *error = "The move just after exiting cutter compensation mode must be straight, not an arc"; + } + return -1; + } if (machine_coordinate_move && !absolute_) { if (error) { *error = "Cannot use G53 incremental"; @@ -4608,6 +6563,18 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string } return -1; } + const bool current_line_requests_motion = + has_axis_words || parsed_word_list.radius_flag || parsed_word_list.theta_flag; + const bool explicit_threading_motion = + has_g_code(word_list, 33.0) || has_g_code(word_list, 33.1) || has_g_code(word_list, 76.0); + if ((current_line_requests_motion || explicit_threading_motion) && !has_axis_words && + (modal_motion_is_threading(modal_motion) || + modal_motion_is_threading_cycle(modal_motion))) { + if (error) { + *error = "All axes missing with motion code"; + } + return -1; + } if (canned_cycle_ != 0) { const CncSimPose initial = sink_.position(); @@ -4650,25 +6617,32 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string 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) { + const bool cycle_context = cycle_requested || previous_canned_cycle != 0; + if (cycle_context && sink_.feed_rate() == 0.0) { if (error) { *error = "Cannot feed with zero feed rate"; } return -1; } - if (cycle_requested && feed_mode_ == 1) { + if (cycle_context && feed_mode_ == 1) { if (error) { *error = "Cannot use inverse time feed with canned cycles"; } return -1; } - if (cycle_requested && has_rotary_word(words)) { + if (cycle_context && cutter_comp_on_) { + if (error) { + *error = "Cannot use canned cycles with cutter compensation on"; + } + return -1; + } + if (cycle_context && 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) || has_disallowed_cycle_axis_word(words, cycle_axes))) { + if (cycle_context && has_disallowed_cycle_axis_word(words, cycle_axes)) { if (error) { *error = "axis word is not allowed in the selected canned cycle plane"; } @@ -4773,8 +6747,22 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string } if (cycle_requested) { + const int saved_motion_control_mode_comment = motion_control_mode_comment; + const double saved_motion_control_mode_feed = motion_control_mode_feed; + if (motion_control_mode_comment != 611) { + motion_control_mode_comment = 611; + motion_control_mode_feed = 0.0; + emit_comment_state(sink_, line_number, 611); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + } CncSimPose hole = initial; apply_cycle_position_axes(&hole, words, cycle_axes, absolute_, sink_.unit_scale(), lathe_diameter_mode_); + if (sink_.plane() == 17 && + !apply_polar_coordinates(&hole, parsed_word_list, absolute_, error)) { + return -1; + } const double first_increment = (!absolute_ && words.count(cycle_axes.first_word)) ? words.at(cycle_axes.first_word) * sink_.unit_scale() : 0.0; @@ -4784,15 +6772,26 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string const int cycle_start_spindle_direction = sink_.spindle_direction(); auto run_cycle_at_hole = [&](const CncSimPose &target_hole) -> bool { - CncSimPose rapid_hole = target_hole; - 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; set_pose_axis(&r_plane, cycle_axes.depth, canned_r_); + if (pose_axis(sink_.position(), cycle_axes.depth) < canned_r_) { + CncSimPose raise_to_r = sink_.position(); + set_pose_axis(&raise_to_r, cycle_axes.depth, canned_r_); + sink_.straight_traverse(line_number, raise_to_r); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + } + + CncSimPose rapid_hole = target_hole; + set_pose_axis(&rapid_hole, cycle_axes.depth, pose_axis(sink_.position(), cycle_axes.depth)); + if (!pose_near(sink_.position(), rapid_hole)) { + sink_.straight_traverse(line_number, rapid_hole); + if (stop_if_callback_aborted(sink_, error)) { + return false; + } + } + if (pose_axis(sink_.position(), cycle_axes.depth) != canned_r_) { sink_.straight_traverse(line_number, r_plane); if (stop_if_callback_aborted(sink_, error)) { @@ -5024,13 +7023,32 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string if (repeat > 0 && !absolute_) { 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 (sink_.plane() == 17 && + parsed_word_list_has_polar_coordinate(parsed_word_list) && + !apply_polar_coordinates(&hole, parsed_word_list, false, error)) { + return -1; + } } if (!run_cycle_at_hole(hole)) { return -1; } } + if (motion_control_mode_comment != saved_motion_control_mode_comment) { + motion_control_mode_comment = saved_motion_control_mode_comment; + motion_control_mode_feed = saved_motion_control_mode_feed; + CncSimEvent event{}; + event.version = 1; + event.type = CNC_SIM_EVENT_COMMENT; + event.line = line_number; + event.feed = motion_control_mode_feed; + event.reserved = motion_control_mode_comment; + sink_.emit_raw(event); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + } } - } else if (has_axis_word(words) && + } else if (current_line_requests_motion && (modal_motion == 0 || modal_motion == 1 || modal_motion == 2 || @@ -5042,13 +7060,33 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string event.start = sink_.position(); event.end = sink_.position(); apply_axes(&event.end, words, absolute_, sink_.unit_scale(), lathe_diameter_mode_); + if (!apply_polar_coordinates(&event.end, parsed_word_list, absolute_, error)) { + return -1; + } 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(modal_motion) && + parsed_word_list.dollar_flag && + !smoke_spindle_selector_valid_for_mode(parsed_word_list.dollar_value)) { + if (error) { + *error = modal_motion == kModalMotionG33 + ? "Invalid spindle ($) number in G33 move" + : "Invalid spindle ($) number in G33.1 move"; + } + return -1; + } if (modal_motion_is_threading_cycle(modal_motion)) { + if (parsed_word_list.dollar_flag && + !smoke_spindle_selector_valid_for_mode(parsed_word_list.dollar_value)) { + if (error) { + *error = "Invalid D-number in G76 cycle"; + } + return -1; + } if (!words.count('P')) { if (error) { *error = "P word missing with G76"; @@ -5129,15 +7167,85 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string return -1; } if (modal_motion == 2 || modal_motion == 3) { + if (sink_.plane() == 171 || sink_.plane() == 181 || sink_.plane() == 191) { + if (error) { + *error = "Cannot do an arc in planes G17.1, G18.1, or G19.1"; + } + return -1; + } + if (cutter_comp_exit_pending_) { + if (error) { + *error = "The move just after exiting cutter compensation mode must be straight, not an arc"; + } + return -1; + } event.arc_turns = modal_motion == 2 ? -1 : 1; + if (words.count('R') && + !validate_r_arc_reaches_end_point(event, sink_.plane(), words.at('R') * sink_.unit_scale(), error)) { + return -1; + } set_arc_center(&event, words, sink_.plane(), sink_.unit_scale(), arc_absolute_); + if (!words.count('R') && + !validate_arc_center_radius_consistency(event, sink_.plane(), sink_.unit_scale(), error)) { + return -1; + } } if (modal_motion == 0) { sink_.straight_traverse(line_number, event.end); + cutter_comp_exit_pending_ = false; } else if (modal_motion == 1) { + if (cutter_comp_on_ && + cutter_comp_radius_ > 0.0 && + cutter_comp_has_prev_vector_ && + cutter_comp_has_prev_prev_vector_) { + const int plane = sink_.plane(); + CncSimPose current_vector{}; + if (plane == 18) { + current_vector.x = event.end.x - event.start.x; + current_vector.z = event.end.z - event.start.z; + } else if (plane == 19) { + current_vector.y = event.end.y - event.start.y; + current_vector.z = event.end.z - event.start.z; + } else { + current_vector.x = event.end.x - event.start.x; + current_vector.y = event.end.y - event.start.y; + } + const double previous_length = plane_vector_length(cutter_comp_prev_vector_, plane); + const double reverse_dot = plane_unit_dot(cutter_comp_prev_prev_vector_, current_vector, plane); + const double orthogonal_dot = std::fabs(plane_unit_dot(cutter_comp_prev_prev_vector_, cutter_comp_prev_vector_, plane)); + if (previous_length < cutter_comp_radius_ && + reverse_dot < -0.999 && + orthogonal_dot < 0.001) { + if (error) { + *error = "Straight feed in concave corner cannot be reached by the tool without gouging"; + } + return -1; + } + } sink_.straight_feed(line_number, event.end); + cutter_comp_exit_pending_ = false; + if (cutter_comp_on_) { + const int plane = sink_.plane(); + CncSimPose current_vector{}; + if (plane == 18) { + current_vector.x = event.end.x - event.start.x; + current_vector.z = event.end.z - event.start.z; + } else if (plane == 19) { + current_vector.y = event.end.y - event.start.y; + current_vector.z = event.end.z - event.start.z; + } else { + current_vector.x = event.end.x - event.start.x; + current_vector.y = event.end.y - event.start.y; + } + if (cutter_comp_has_prev_vector_) { + cutter_comp_prev_prev_vector_ = cutter_comp_prev_vector_; + cutter_comp_has_prev_prev_vector_ = true; + } + cutter_comp_prev_vector_ = current_vector; + cutter_comp_has_prev_vector_ = true; + } } else if (modal_motion_is_threading(modal_motion)) { - sink_.set_speed_feed_sync(0, words.at('K') * sink_.unit_scale(), false, true, line_number); + sink_.set_speed_feed_sync(selected_spindle, words.at('K') * sink_.unit_scale(), false, true, line_number); if (stop_if_callback_aborted(sink_, error)) { return -1; } @@ -5145,7 +7253,7 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string if (stop_if_callback_aborted(sink_, error)) { return -1; } - sink_.set_speed_feed_sync(0, 0.0, false, false, line_number); + sink_.set_speed_feed_sync(selected_spindle, 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(); @@ -5210,7 +7318,7 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string if (stop_if_callback_aborted(sink_, error)) { return false; } - sink_.set_speed_feed_sync(0, taper_pitch, false, true, line_number); + sink_.set_speed_feed_sync(selected_spindle, taper_pitch, false, true, line_number); if (stop_if_callback_aborted(sink_, error)) { return false; } @@ -5225,7 +7333,7 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string if (stop_if_callback_aborted(sink_, error)) { return false; } - sink_.set_speed_feed_sync(0, pitch, false, true, line_number); + sink_.set_speed_feed_sync(selected_spindle, pitch, false, true, line_number); if (stop_if_callback_aborted(sink_, error)) { return false; } @@ -5238,7 +7346,7 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string if (stop_if_callback_aborted(sink_, error)) { return false; } - sink_.set_speed_feed_sync(0, pitch, false, true, line_number); + sink_.set_speed_feed_sync(selected_spindle, pitch, false, true, line_number); if (stop_if_callback_aborted(sink_, error)) { return false; } @@ -5249,7 +7357,7 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string if (stop_if_callback_aborted(sink_, error)) { return false; } - sink_.set_speed_feed_sync(0, taper_pitch, false, true, line_number); + sink_.set_speed_feed_sync(selected_spindle, taper_pitch, false, true, line_number); if (stop_if_callback_aborted(sink_, error)) { return false; } @@ -5267,7 +7375,7 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string } } - sink_.set_speed_feed_sync(0, 0.0, false, false, line_number); + sink_.set_speed_feed_sync(selected_spindle, 0.0, false, false, line_number); if (stop_if_callback_aborted(sink_, error)) { return false; } @@ -5306,6 +7414,75 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string return -1; } } + if (program_end_m_code != 0) { + deferred_program_stops.clear(); + sink_.stop_spindle(line_number); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + emit_comment_state(sink_, line_number, 970); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + if (program_end_m_code == 30) { + sink_.program_stop(line_number, 3); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + } + active_g5x_index = 1; + { + const auto offset = g5x_offsets.find(active_g5x_index); + sink_.set_g5x_offset(active_g5x_index, + offset != g5x_offsets.end() ? offset->second : CncSimPose{}, + line_number); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + const auto rotation = g5x_rotations.find(active_g5x_index); + sink_.set_xy_rotation(rotation != g5x_rotations.end() ? rotation->second : 0.0, line_number); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + } + if (sink_.plane() != 17) { + sink_.select_plane(17, line_number); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + } + feed_mode_ = 0; + sink_.set_feed_rate(0.0, line_number); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + if (sink_.mist_on()) { + sink_.set_mist_on(false); + emit_comment_state(sink_, line_number, 10); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + } + if (sink_.flood_on()) { + sink_.set_flood_on(false); + emit_comment_state(sink_, line_number, 20); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + } + sink_.program_end(line_number); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + commit_pending_assignments(); + break; + } + for (int stop_code : deferred_program_stops) { + sink_.program_stop(line_number, stop_code); + if (stop_if_callback_aborted(sink_, error)) { + return -1; + } + } commit_pending_assignments(); } @@ -5316,5 +7493,14 @@ int SmokeGcodeParser::parse(const char *program, size_t program_len, std::string return -1; } + refresh_readonly_named_parameters(lines.empty() ? 0 : lines.back().number); + if (parameter_file_path && *parameter_file_path && + !save_smoke_parameter_file(parameter_file_path, parameters)) { + if (error) { + *error = "failed to save parameter file"; + } + return -1; + } + return 0; } diff --git a/core/src/smoke_gcode_parser.h b/core/src/smoke_gcode_parser.h index e9cc931..5dc4de1 100644 --- a/core/src/smoke_gcode_parser.h +++ b/core/src/smoke_gcode_parser.h @@ -17,9 +17,24 @@ private: bool arc_absolute_ = false; bool lathe_diameter_mode_ = false; bool cutter_comp_on_ = false; + bool cutter_comp_exit_pending_ = false; + bool cutter_comp_has_prev_vector_ = false; + bool cutter_comp_has_prev_prev_vector_ = false; + double cutter_comp_radius_ = 0.0; + int spindle_tool_id_ = 0; + CncSimPose cutter_comp_prev_vector_{}; + CncSimPose cutter_comp_prev_prev_vector_{}; + int cutter_comp_mode_ = 40; + bool spindle_css_mode_ = false; + bool tool_selected_ = false; + bool nurbs_g5_collecting_ = false; + int nurbs_g5_control_points_ = 0; + int nurbs_g5_order_ = 3; + bool nurbs_g6_collecting_ = false; int feed_mode_ = 0; int canned_cycle_ = 0; bool canned_return_to_initial_ = false; + bool g92_applied_ = false; bool canned_has_r_ = false; bool canned_has_z_ = false; bool canned_has_p_ = false; @@ -36,6 +51,8 @@ private: double canned_i_ = 0.0; double canned_j_ = 0.0; double canned_k_ = 0.0; + double m66_result_ = 0.0; double oword_return_value_ = 0.0; bool oword_value_returned_ = false; + int selected_tool_id_ = 0; }; diff --git a/core/tests/cnc_sim_api_smoke.cpp b/core/tests/cnc_sim_api_smoke.cpp index bc1c5c3..f2b275b 100644 --- a/core/tests/cnc_sim_api_smoke.cpp +++ b/core/tests/cnc_sim_api_smoke.cpp @@ -130,6 +130,7 @@ int main() { bool saw_program_stop = false; bool saw_optional_stop = false; bool saw_pallet_stop = false; + bool saw_m60_followup_stop = false; bool saw_mist_on = false; bool saw_flood_on = false; bool saw_mist_off = false; @@ -235,6 +236,12 @@ int main() { bool saw_oword_second_arg = false; bool saw_numbered_parameter = false; bool saw_parameter_expression = false; + bool saw_parameter_expression_index = false; + bool saw_parameter_unary_index_word = false; + bool saw_parameter_signed_index_word = false; + bool saw_parameter_signed_index_assignment = false; + bool saw_spaced_parameter_index_word = false; + bool saw_spaced_expression_parameter_index = false; bool saw_oword_local_parameter = false; bool saw_global_parameter_after_oword = false; bool saw_global_numbered_parameter_in_oword = false; @@ -272,11 +279,18 @@ int main() { bool saw_repeated_assignment_last_value_motion = false; bool saw_nested_bracket_assignment = false; bool saw_nested_bracket_motion = false; + bool saw_spaced_bracket_word_motion = false; + bool saw_plus_spaced_bracket_word_motion = false; + bool saw_signed_spaced_bracket_word_motion = false; bool saw_math_function_assignment = false; + bool saw_spaced_function_bracket = false; bool saw_word_unary_function_motion = false; bool saw_word_atan_function_motion = false; bool saw_signed_word_unary_function_motion = false; + bool saw_spaced_word_unary_function_motion = false; + bool saw_signed_spaced_word_unary_function_motion = false; bool saw_lowercase_motion = false; + bool saw_spaced_word_motion = false; bool saw_trig_function_motion = false; bool saw_inverse_trig_function_motion = false; bool saw_atan_function_motion = false; @@ -297,6 +311,7 @@ int main() { bool saw_exists_missing_motion = false; bool saw_exists_numbered_motion = false; bool saw_exists_indirect_numbered_motion = false; + bool saw_exists_spaced_parameter_motion = false; bool saw_indexed_parameter_motion = false; bool saw_indirect_parameter_motion = false; bool saw_indexed_parameter_condition_motion = false; @@ -482,7 +497,7 @@ int main() { const char tool_number_program[] = "G21 G90 G17\n" - "M61 Q3\n" + "M61 Q1\n" "M30\n"; events.clear(); ok &= expect(cnc_sim_parse_program(sim, tool_number_program, sizeof(tool_number_program) - 1) == 0, @@ -492,10 +507,19 @@ int main() { saw_m61_tool_number = saw_m61_tool_number || (event.type == CNC_SIM_EVENT_TOOL_CHANGE && event.line == 2 && - event.tool == 3); + event.tool == 1); } ok &= expect(saw_m61_tool_number, "expected M61 tool-number event"); + const char missing_m61_tool_program[] = + "G21 G90 G17\n" + "M61 Q3\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + missing_m61_tool_program, + sizeof(missing_m61_tool_program) - 1) != 0, + "expected LinuxCNC M61 to reject tool numbers missing from the tool table"); + const char invalid_tool_number_program[] = "G21 G90 G17\n" "M61 Q-1\n"; @@ -1134,11 +1158,11 @@ int main() { const char modal_position_named_param_program[] = "G21 G90 G17\n" - "T7 M6\n" + "T1 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" + "O10 if [[#<_selected_tool> EQ 1] AND [#<_current_tool> EQ 0] 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" @@ -1157,6 +1181,59 @@ int main() { ok &= expect(saw_modal_position_named_params, "expected modal, tool, and current-position readonly named parameters"); + const char selected_current_tool_named_param_program[] = + "G21 G90 G17\n" + "T1\n" + "O10 if [[#<_selected_tool> EQ 1] AND [#<_current_tool> EQ 0]]\n" + "G1 X1 F100\n" + "O10 endif\n" + "M6\n" + "O20 if [[#<_selected_tool> EQ 1] AND [#<_current_tool> EQ 0]]\n" + "G1 Y1\n" + "O20 endif\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + selected_current_tool_named_param_program, + sizeof(selected_current_tool_named_param_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_selected_tool_before_change = false; + bool saw_current_tool_after_change = false; + for (const auto &event : events) { + saw_selected_tool_before_change = saw_selected_tool_before_change || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + near(event.end.x, 1.0)); + saw_current_tool_after_change = saw_current_tool_after_change || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 8 && + near(event.end.y, 1.0)); + } + ok &= expect(saw_selected_tool_before_change, + "expected LinuxCNC T word to update selected tool before current tool"); + ok &= expect(saw_current_tool_after_change, + "expected LinuxCNC M6 to keep current tool tied to #5400 in the nonrandom fixture"); + + const char m61_tool_named_param_program[] = + "G21 G90 G17\n" + "M61 Q1\n" + "O10 if [[#<_selected_tool> EQ 0] AND [#<_current_tool> EQ 0]]\n" + "G1 X1 F100\n" + "O10 endif\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + m61_tool_named_param_program, + sizeof(m61_tool_named_param_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_m61_tool_named_params = false; + for (const auto &event : events) { + saw_m61_tool_named_params = saw_m61_tool_named_params || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + near(event.end.x, 1.0)); + } + ok &= expect(saw_m61_tool_named_params, + "expected LinuxCNC M61 to leave selected/current tool named parameters unchanged in nonrandom fixture"); + const char dynamic_tool_length_program[] = "G21 G90 G17\n" "G43.1 X1 Z2\n" @@ -1199,6 +1276,76 @@ int main() { 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 immediate_g431_named_position_program[] = + "G21 G90 G17\n" + "G43.1 X1 Z2\n" + "O10 if [[#<_x> EQ -1] AND [#<_z> EQ -2] AND [#<_tool_offset> EQ 1]]\n" + "G1 X1 F100\n" + "O10 endif\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + immediate_g431_named_position_program, + sizeof(immediate_g431_named_position_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_immediate_g431_named_position = false; + for (const auto &event : events) { + saw_immediate_g431_named_position = saw_immediate_g431_named_position || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + near(event.end.x, 1.0) && + near(event.end.z, -2.0)); + } + ok &= expect(saw_immediate_g431_named_position, + "expected G43.1 to update readonly position named parameters before the next motion"); + + const char immediate_g432_named_position_program[] = + "G21 G90 G17\n" + "G43.2 X1 Z2\n" + "O10 if [[#<_x> EQ -1] AND [#<_z> EQ -2] AND [#<_tool_offset> EQ 1]]\n" + "G1 X1 F100\n" + "O10 endif\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + immediate_g432_named_position_program, + sizeof(immediate_g432_named_position_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_immediate_g432_named_position = false; + for (const auto &event : events) { + saw_immediate_g432_named_position = saw_immediate_g432_named_position || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + near(event.end.x, 1.0) && + near(event.end.z, -2.0)); + } + ok &= expect(saw_immediate_g432_named_position, + "expected G43.2 to update readonly position named parameters before the next motion"); + + const char immediate_g49_named_restore_program[] = + "G21 G90 G17\n" + "G43.1 X1 Z2\n" + "G49\n" + "O10 if [[#<_x> EQ 0] AND [#<_z> EQ 0] AND [#<_tool_offset> EQ 0]]\n" + "G1 X1 F100\n" + "O10 endif\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + immediate_g49_named_restore_program, + sizeof(immediate_g49_named_restore_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_immediate_g49_named_restore = false; + for (const auto &event : events) { + saw_immediate_g49_named_restore = saw_immediate_g49_named_restore || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 5 && + near(event.end.x, 1.0) && + near(event.end.z, 0.0)); + } + ok &= expect(saw_immediate_g49_named_restore, + "expected G49 to restore readonly position named parameters before the next motion"); + const char g43_tool_length_program[] = "G21 G90 G17\n" "G43 H1\n" @@ -1227,6 +1374,38 @@ int main() { 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 immediate_g43_named_position_program[] = + "G21 G90 G17\n" + "G43 H1\n" + "O10 if [#<_z> EQ -12.5]\n" + "G1 X1 F100\n" + "O10 endif\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + immediate_g43_named_position_program, + sizeof(immediate_g43_named_position_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_immediate_g43_named_position = false; + for (const auto &event : events) { + saw_immediate_g43_named_position = saw_immediate_g43_named_position || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + near(event.end.x, 1.0) && + near(event.end.z, -12.5)); + } + ok &= expect(saw_immediate_g43_named_position, + "expected G43 to update readonly position named parameters before the next motion"); + + const char missing_g43_h_tool_program[] = + "G21 G90 G17\n" + "G43 H3\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + missing_g43_h_tool_program, + sizeof(missing_g43_h_tool_program) - 1) != 0, + "expected LinuxCNC G43 H to reject tool numbers missing from the tool table"); + const char invalid_h_word_program[] = "G21 G90 G17\n" "H1\n"; @@ -1267,7 +1446,7 @@ int main() { "G21 G90 G17\n" "G0 X0 Y0 Z0\n" "F100\n" - "G41 D1\n" + "G41.1 D0.25\n" "G1 X20 Y0\n" "G40\n" "G1 X0 Y0\n"; @@ -1277,10 +1456,10 @@ int main() { 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_g41 = saw_g41 || (event.type == CNC_SIM_EVENT_COMMENT && event.reserved == 411); 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_g41, "expected G41.1 cutter compensation state event"); ok &= expect(saw_g40, "expected G40 cutter compensation clear event"); const char invalid_cutter_plane_program[] = @@ -1403,6 +1582,15 @@ int main() { sizeof(invalid_cutter_tool_change_program) - 1) != 0, "expected M6 to reject cutter compensation"); + const char missing_t_for_m6_program[] = + "G21 G90 G17\n" + "M6\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + missing_t_for_m6_program, + sizeof(missing_t_for_m6_program) - 1) != 0, + "expected M6 to reject missing T word"); + const char invalid_cutter_wait_input_program[] = "G21 G90 G17\n" "G41 D1\n" @@ -1431,6 +1619,15 @@ int main() { sizeof(mixed_g432_h_axis_program) - 1) != 0, "expected G43.2 to reject mixed H and axis words"); + const char missing_g432_h_tool_program[] = + "G21 G90 G17\n" + "G43.2 H3\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + missing_g432_h_tool_program, + sizeof(missing_g432_h_tool_program) - 1) != 0, + "expected LinuxCNC G43.2 H to reject tool numbers missing from the tool table"); + const char invalid_g432_program[] = "G21 G90 G17\n" "G43.2\n"; @@ -1507,10 +1704,271 @@ int main() { (event.type == CNC_SIM_EVENT_PROGRAM_END && event.line == 4 && event.reserved == 3); + saw_m60_followup_stop = saw_m60_followup_stop || + (event.type == CNC_SIM_EVENT_PROGRAM_END && + event.line == 4 && + event.reserved == 1); } ok &= expect(saw_program_stop, "expected M0 program stop event"); ok &= expect(saw_optional_stop, "expected M1 optional stop event"); ok &= expect(saw_pallet_stop, "expected M60 pallet shuttle event"); + ok &= expect(saw_m60_followup_stop, "expected LinuxCNC M60 follow-up program stop event"); + + const char stop_same_line_motion_program[] = + "G0 X1\n" + "M0 G0 Y2\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + stop_same_line_motion_program, + sizeof(stop_same_line_motion_program) - 1) == 0, + cnc_sim_last_error(sim)); + int stop_same_line_motion_index = -1; + int stop_same_line_stop_index = -1; + for (size_t event_index = 0; event_index < events.size(); ++event_index) { + const auto &event = events[event_index]; + if (event.type == CNC_SIM_EVENT_RAPID && + event.line == 2 && + event.end.y == 2.0) { + stop_same_line_motion_index = static_cast(event_index); + } + if (event.type == CNC_SIM_EVENT_PROGRAM_END && + event.line == 2 && + event.reserved == 1) { + stop_same_line_stop_index = static_cast(event_index); + } + } + ok &= expect(stop_same_line_motion_index >= 0 && + stop_same_line_stop_index > stop_same_line_motion_index, + "expected LinuxCNC to execute same-line motion before M0 stop"); + + const char m30_stop_program[] = + "G21 G90\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, m30_stop_program, sizeof(m30_stop_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_m30_end_stop = false; + bool saw_m30_final_end = false; + for (const auto &event : events) { + saw_m30_end_stop = saw_m30_end_stop || + (event.type == CNC_SIM_EVENT_PROGRAM_END && + event.line == 2 && + event.reserved == 3); + saw_m30_final_end = saw_m30_final_end || + (event.type == CNC_SIM_EVENT_PROGRAM_END && + event.line == 2 && + event.reserved == 0); + } + ok &= expect(saw_m30_end_stop, "expected M30 end-stop event"); + ok &= expect(saw_m30_final_end, "expected M30 final program end event"); + + const char m2_same_line_motion_program[] = + "G0 X1\n" + "M2 G0 Y2\n" + "G0 Z3\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + m2_same_line_motion_program, + sizeof(m2_same_line_motion_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_m2_same_line_motion = false; + bool saw_after_m2_motion = false; + bool saw_m2_same_line_end = false; + for (const auto &event : events) { + saw_m2_same_line_motion = saw_m2_same_line_motion || + (event.type == CNC_SIM_EVENT_RAPID && + event.line == 2 && + event.end.y == 2.0); + saw_after_m2_motion = saw_after_m2_motion || + (event.type == CNC_SIM_EVENT_RAPID && + event.line == 3); + saw_m2_same_line_end = saw_m2_same_line_end || + (event.type == CNC_SIM_EVENT_PROGRAM_END && + event.line == 2 && + event.reserved == 0); + } + ok &= expect(saw_m2_same_line_motion, "expected LinuxCNC to execute same-line motion before M2 end"); + ok &= expect(!saw_after_m2_motion, "expected LinuxCNC to stop reading after M2 line"); + ok &= expect(saw_m2_same_line_end, "expected LinuxCNC M2 same-line program end"); + + const char m2_feed_reset_program[] = + "F123\n" + "M2\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + m2_feed_reset_program, + sizeof(m2_feed_reset_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_m2_feed_reset = false; + for (const auto &event : events) { + saw_m2_feed_reset = saw_m2_feed_reset || + (event.type == CNC_SIM_EVENT_SET_FEED && + event.line == 2 && + event.feed == 0.0); + } + ok &= expect(saw_m2_feed_reset, "expected LinuxCNC M2 to reset feed rate to zero"); + + const char m2_plane_reset_program[] = + "G18\n" + "M2\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + m2_plane_reset_program, + sizeof(m2_plane_reset_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_m2_plane_reset = false; + for (const auto &event : events) { + saw_m2_plane_reset = saw_m2_plane_reset || + (event.type == CNC_SIM_EVENT_SET_PLANE && + event.line == 2 && + event.plane == 17); + } + ok &= expect(saw_m2_plane_reset, "expected LinuxCNC M2 to reset selected plane to G17"); + + const char m2_coolant_reset_program[] = + "M7\n" + "M8\n" + "M2\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + m2_coolant_reset_program, + sizeof(m2_coolant_reset_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_m2_mist_off = false; + bool saw_m2_flood_off = false; + for (const auto &event : events) { + saw_m2_mist_off = saw_m2_mist_off || + (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 3 && + event.reserved == 10); + saw_m2_flood_off = saw_m2_flood_off || + (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 3 && + event.reserved == 20); + } + ok &= expect(saw_m2_mist_off, "expected LinuxCNC M2 to turn mist coolant off"); + ok &= expect(saw_m2_flood_off, "expected LinuxCNC M2 to turn flood coolant off"); + + const char m2_g54_reset_program[] = + "G55\n" + "M2\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + m2_g54_reset_program, + sizeof(m2_g54_reset_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_m2_g54_reset = false; + for (const auto &event : events) { + saw_m2_g54_reset = saw_m2_g54_reset || + (event.type == CNC_SIM_EVENT_SET_G5X_OFFSET && + event.line == 2 && + event.tool == 1); + } + ok &= expect(saw_m2_g54_reset, "expected LinuxCNC M2 to reset coordinate system to G54"); + + const char percent_delimited_program[] = + "%\n" + "G21 G90\n" + "G0 X1\n" + "%\n" + "G0 Y2\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + percent_delimited_program, + sizeof(percent_delimited_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_percent_program_motion = false; + bool saw_after_percent_motion = false; + for (const auto &event : events) { + saw_percent_program_motion = saw_percent_program_motion || + (event.type == CNC_SIM_EVENT_RAPID && + event.line == 3 && + event.end.x == 1.0); + saw_after_percent_motion = saw_after_percent_motion || + (event.type == CNC_SIM_EVENT_RAPID && + event.line == 5); + } + ok &= expect(saw_percent_program_motion, "expected LinuxCNC percent-delimited program body to execute"); + ok &= expect(!saw_after_percent_motion, "expected LinuxCNC to stop at closing percent line"); + + const char leading_blank_percent_program[] = + "\n" + "\n" + "%\n" + "G0 X1\n" + "%\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + leading_blank_percent_program, + sizeof(leading_blank_percent_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_leading_blank_percent_motion = false; + for (const auto &event : events) { + saw_leading_blank_percent_motion = saw_leading_blank_percent_motion || + (event.type == CNC_SIM_EVENT_RAPID && + event.line == 2 && + event.end.x == 1.0); + } + ok &= expect(saw_leading_blank_percent_motion, + "expected LinuxCNC percent-delimited line numbers to ignore leading blanks before opening percent"); + + const char unterminated_percent_program[] = + "%\n" + "G0 X1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + unterminated_percent_program, + sizeof(unterminated_percent_program) - 1) != 0, + "expected LinuxCNC to require closing percent line"); + + const char commented_percent_program[] = + "% (not a delimiter)\n" + "G0 X1\n" + "%\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + commented_percent_program, + sizeof(commented_percent_program) - 1) != 0, + "expected LinuxCNC to reject percent lines with trailing comments"); + + const char mid_program_percent_program[] = + "G0 X1\n" + "%\n" + "G0 Y2\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + mid_program_percent_program, + sizeof(mid_program_percent_program) - 1) != 0, + "expected LinuxCNC to reject percent line without opening percent delimiter"); + + const char unclosed_comment_program[] = + "G21 G90\n" + "G0 X1 (missing close\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + unclosed_comment_program, + sizeof(unclosed_comment_program) - 1) != 0, + "expected LinuxCNC to reject unclosed parenthesis comments"); + + const char nested_comment_program[] = + "G21 G90\n" + "G0 X1 (outer (inner))\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + nested_comment_program, + sizeof(nested_comment_program) - 1) != 0, + "expected LinuxCNC to reject nested parenthesis comments"); + + const char uncalled_subprogram_bad_comment_program[] = + "O sub\n" + "G0 X1 (missing close\n" + "O endsub\n" + "M2\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + uncalled_subprogram_bad_comment_program, + sizeof(uncalled_subprogram_bad_comment_program) - 1) != 0, + "expected LinuxCNC to reject bad comments inside uncalled subprogram definitions"); const char coolant_program[] = "G21\n" @@ -1627,9 +2085,9 @@ int main() { 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); + near(event.start.x, -10.3253175473055) && + near(event.start.y, 6.84807621135332) && + near(event.start.z, -7.0)); saw_smoke_g921 = saw_smoke_g921 || (event.type == CNC_SIM_EVENT_SET_G92_OFFSET && event.line == 6 && @@ -1645,9 +2103,9 @@ int main() { 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); + near(event.start.x, -10.3253175473055) && + near(event.start.y, 6.84807621135332) && + near(event.start.z, -7.0)); saw_smoke_g923_after_g921 = saw_smoke_g923_after_g921 || (event.type == CNC_SIM_EVENT_SET_G92_OFFSET && event.line == 7 && @@ -1866,6 +2324,91 @@ 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_r_plane_order_program[] = + "G21 G90 G17\n" + "F100\n" + "G81 X1 Y0 Z-1 R1\n" + "G80\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + canned_cycle_r_plane_order_program, + sizeof(canned_cycle_r_plane_order_program) - 1) == 0, + cnc_sim_last_error(sim)); + int first_rapid_index = -1; + int second_rapid_index = -1; + int exact_path_index = -1; + int exact_stop_restore_index = -1; + for (size_t i = 0; i < events.size(); ++i) { + const auto &event = events[i]; + if (exact_path_index == -1 && + event.type == CNC_SIM_EVENT_COMMENT && + event.line == 3 && + event.reserved == 611) { + exact_path_index = static_cast(i); + continue; + } + if (exact_stop_restore_index == -1 && + event.type == CNC_SIM_EVENT_COMMENT && + event.line == 3 && + event.reserved == 612) { + exact_stop_restore_index = static_cast(i); + continue; + } + if (event.type != CNC_SIM_EVENT_RAPID || event.line != 3) { + continue; + } + if (first_rapid_index == -1 && + near(event.end.x, 0.0) && + near(event.end.y, 0.0) && + near(event.end.z, 1.0)) { + first_rapid_index = static_cast(i); + continue; + } + if (second_rapid_index == -1 && + near(event.end.x, 1.0) && + near(event.end.y, 0.0) && + near(event.end.z, 1.0)) { + second_rapid_index = static_cast(i); + } + } + ok &= expect(exact_path_index >= 0, "expected canned cycle to enter exact-path motion mode"); + ok &= expect(first_rapid_index >= 0, "expected canned cycle to raise to R plane before XY move"); + ok &= expect(second_rapid_index > first_rapid_index, "expected canned cycle XY move after R-plane raise"); + ok &= expect(exact_stop_restore_index > second_rapid_index, "expected canned cycle to restore prior motion mode after cycle"); + + const char canned_cycle_g64_restore_program[] = + "G21 G90 G17\n" + "G64 P0.1\n" + "F100\n" + "G81 X1 Y0 Z-1 R1\n" + "G80\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + canned_cycle_g64_restore_program, + sizeof(canned_cycle_g64_restore_program) - 1) == 0, + cnc_sim_last_error(sim)); + int g64_enter_exact_path_index = -1; + int g64_restore_continuous_index = -1; + for (size_t i = 0; i < events.size(); ++i) { + const auto &event = events[i]; + if (g64_enter_exact_path_index == -1 && + event.type == CNC_SIM_EVENT_COMMENT && + event.line == 4 && + event.reserved == 611) { + g64_enter_exact_path_index = static_cast(i); + continue; + } + if (g64_restore_continuous_index == -1 && + event.type == CNC_SIM_EVENT_COMMENT && + event.line == 4 && + event.reserved == 640 && + near(event.feed, 0.1)) { + g64_restore_continuous_index = static_cast(i); + } + } + ok &= expect(g64_enter_exact_path_index >= 0, "expected canned cycle to switch to exact-path under G64"); + ok &= expect(g64_restore_continuous_index > g64_enter_exact_path_index, "expected canned cycle to restore G64 after motion"); + const char canned_cycle_plane_program[] = "G21 G90\n" "G18\n" @@ -1918,6 +2461,259 @@ int main() { 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 uv_canned_cycle_program[] = + "G21 G90 G17.1\n" + "F100\n" + "G81 U1 V2 W-1 R1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, uv_canned_cycle_program, sizeof(uv_canned_cycle_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_g171_g81_feed = false; + bool saw_g171_g81_retract = false; + for (const auto &event : events) { + saw_g171_g81_feed = saw_g171_g81_feed || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 3 && + near(event.end.u, 1.0) && + near(event.end.v, 2.0) && + near(event.end.w, -1.0)); + saw_g171_g81_retract = saw_g171_g81_retract || + (event.type == CNC_SIM_EVENT_RAPID && + event.line == 3 && + near(event.end.u, 1.0) && + near(event.end.v, 2.0) && + near(event.end.w, 1.0)); + } + ok &= expect(saw_g171_g81_feed, "expected LinuxCNC G17.1 G81 feed along W depth"); + ok &= expect(saw_g171_g81_retract, "expected LinuxCNC G17.1 G81 retract to W R plane"); + + const char cutter_comp_uv_plane_program[] = + "G21 G90 G17\n" + "F100\n" + "G41.1 D0.25\n" + "G17.1\n" + "G40\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + cutter_comp_uv_plane_program, + sizeof(cutter_comp_uv_plane_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_cutter_comp_uv_plane = false; + for (const auto &event : events) { + saw_cutter_comp_uv_plane = saw_cutter_comp_uv_plane || + (event.type == CNC_SIM_EVENT_SET_PLANE && + event.line == 4 && + event.plane == 171); + } + ok &= expect(saw_cutter_comp_uv_plane, + "expected LinuxCNC to allow G17.1 plane selection while cutter compensation is active"); + + const char uv_arc_program[] = + "G21 G90 G17.1\n" + "F100\n" + "G2 U1 V0 R1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, uv_arc_program, sizeof(uv_arc_program) - 1) != 0, + "expected LinuxCNC to reject arcs in G17.1/G18.1/G19.1 planes"); + ok &= expect(std::string(cnc_sim_last_error(sim)).find("Cannot do an arc in planes G17.1, G18.1, or G19.1") != std::string::npos, + cnc_sim_last_error(sim)); + + const char uv_g62_nurbs_program[] = + "G21 G90 G17.1\n" + "F100\n" + "G6.2 U1 V1 P3 Q1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + uv_g62_nurbs_program, + sizeof(uv_g62_nurbs_program) - 1) != 0, + "expected LinuxCNC to reject G6.2 NURBS in G17.1/G18.1/G19.1 planes"); + ok &= expect(std::string(cnc_sim_last_error(sim)).find("one plane must be selected for nurbs: XY, YZ, ZX") != std::string::npos, + cnc_sim_last_error(sim)); + + const char g62_missing_xy_pair_program[] = + "G21 G90 G17\n" + "F100\n" + "G6.2 X1 P3 Q1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g62_missing_xy_pair_program, + sizeof(g62_missing_xy_pair_program) - 1) != 0, + "expected LinuxCNC to reject G6.2 control point with X but no Y in XY plane"); + ok &= expect(std::string(cnc_sim_last_error(sim)).find("You must specify both X and Y coordinates for Control Points") != std::string::npos, + cnc_sim_last_error(sim)); + + const char g62_missing_p_program[] = + "G21 G90 G17\n" + "F100\n" + "G6.2 X1 Y1 Q1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g62_missing_p_program, + sizeof(g62_missing_p_program) - 1) != 0, + "expected LinuxCNC to reject first G6.2 NURBS block without P order"); + ok &= expect(std::string(cnc_sim_last_error(sim)).find("Program the nurbs order P in the first block of instruction") != std::string::npos, + cnc_sim_last_error(sim)); + + const char g62_noninteger_p_program[] = + "G21 G90 G17\n" + "F100\n" + "G6.2 X1 Y1 P3.5 Q1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g62_noninteger_p_program, + sizeof(g62_noninteger_p_program) - 1) != 0, + "expected LinuxCNC to reject non-integer G6.2 P order"); + ok &= expect(std::string(cnc_sim_last_error(sim)).find("nurbs order P number is not an integer") != std::string::npos, + cnc_sim_last_error(sim)); + + const char g62_missing_q_program[] = + "G21 G90 G17\n" + "F100\n" + "G6.2 X1 Y1 P3\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g62_missing_q_program, + sizeof(g62_missing_q_program) - 1) != 0, + "expected LinuxCNC to reject first G6.2 NURBS block without Q mode"); + ok &= expect(std::string(cnc_sim_last_error(sim)).find("Program the nurbs Q value (1,2,3) in the first block of nurbs instruction") != std::string::npos, + cnc_sim_last_error(sim)); + + const char g62_q_out_of_range_program[] = + "G21 G90 G17\n" + "F100\n" + "G6.2 X1 Y1 P3 Q4\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g62_q_out_of_range_program, + sizeof(g62_q_out_of_range_program) - 1) != 0, + "expected LinuxCNC to reject G6.2 Q outside 1..3"); + ok &= expect(std::string(cnc_sim_last_error(sim)).find("nurbs Q value not in range 1 to 3") != std::string::npos, + cnc_sim_last_error(sim)); + + const char g62_noninteger_q_program[] = + "G21 G90 G17\n" + "F100\n" + "G6.2 X1 Y1 P3 Q1.5\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g62_noninteger_q_program, + sizeof(g62_noninteger_q_program) - 1) != 0, + "expected LinuxCNC to reject non-integer G6.2 Q"); + ok &= expect(std::string(cnc_sim_last_error(sim)).find("nurbs Q number is not an integer") != std::string::npos, + cnc_sim_last_error(sim)); + + const char g62_followup_missing_r_program[] = + "G21 G90 G17\n" + "F100\n" + "G6.2 X1 Y1 P3 Q1 R1 K0\n" + "G6.2 X2 Y2 K1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g62_followup_missing_r_program, + sizeof(g62_followup_missing_r_program) - 1) != 0, + "expected LinuxCNC to reject follow-up G6.2 control point without positive R weight"); + ok &= expect(std::string(cnc_sim_last_error(sim)).find("Must specify positive nurbs weight R for every Control Point") != std::string::npos, + cnc_sim_last_error(sim)); + + const char g62_followup_missing_k_program[] = + "G21 G90 G17\n" + "F100\n" + "G6.2 X1 Y1 P3 Q1 R1 K0\n" + "G6.2 X2 Y2 R1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g62_followup_missing_k_program, + sizeof(g62_followup_missing_k_program) - 1) != 0, + "expected LinuxCNC to reject follow-up G6.2 control point without K knot"); + ok &= expect(std::string(cnc_sim_last_error(sim)).find("Must specify nurbs K number for every Control Point") != std::string::npos, + cnc_sim_last_error(sim)); + + const char g62_followup_negative_k_program[] = + "G21 G90 G17\n" + "F100\n" + "G6.2 X1 Y1 P3 Q1 R1 K0\n" + "G6.2 X2 Y2 R1 K-1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g62_followup_negative_k_program, + sizeof(g62_followup_negative_k_program) - 1) != 0, + "expected LinuxCNC to reject follow-up G6.2 control point with negative K knot"); + ok &= expect(std::string(cnc_sim_last_error(sim)).find("Must specify nurbs K number for every Control Point") != std::string::npos, + cnc_sim_last_error(sim)); + + const char g62_without_feed_program[] = + "G21 G90 G17\n" + "G6.2 X1 Y1 P3 Q1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g62_without_feed_program, + sizeof(g62_without_feed_program) - 1) != 0, + "expected LinuxCNC to reject G6.2 with zero feed rate"); + ok &= expect(std::string(cnc_sim_last_error(sim)).find("Cannot make a nurbs with 0 feedrate") != std::string::npos, + cnc_sim_last_error(sim)); + + const char g51_missing_j_program[] = + "G21 G90 G17\n" + "F100\n" + "G5.1 X1 Y1 I1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g51_missing_j_program, + sizeof(g51_missing_j_program) - 1) != 0, + "expected LinuxCNC to reject G5.1 without both I and J"); + ok &= expect(std::string(cnc_sim_last_error(sim)).find("Must specify both I and J with G5.1 or G6.1") != std::string::npos, + cnc_sim_last_error(sim)); + + const char g61_missing_i_program[] = + "G21 G90 G17\n" + "F100\n" + "G6.1 X1 Y1 J1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g61_missing_i_program, + sizeof(g61_missing_i_program) - 1) != 0, + "expected LinuxCNC to reject G6.1 without both I and J"); + ok &= expect(std::string(cnc_sim_last_error(sim)).find("Must specify both I and J with G5.1 or G6.1") != std::string::npos, + cnc_sim_last_error(sim)); + + const char g51_spline_program[] = + "G21 G90 G17\n" + "F100\n" + "G5.1 X2 Y0 I1 J1\n"; + bool saw_g51_control_point = false; + bool saw_g51_end_point = false; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g51_spline_program, + sizeof(g51_spline_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_g51_control_point = saw_g51_control_point || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 3 && + near(event.end.x, 1.0) && + near(event.end.y, 1.0)); + saw_g51_end_point = saw_g51_end_point || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 3 && + near(event.end.x, 2.0) && + near(event.end.y, 0.0)); + } + ok &= expect(saw_g51_control_point, "expected LinuxCNC G5.1 spline control point feed"); + ok &= expect(saw_g51_end_point, "expected LinuxCNC G5.1 spline endpoint feed"); + + const char g51_with_rotary_axis_program[] = + "G21 G90 G17\n" + "F100\n" + "G5.1 X2 Y0 A1 I1 J1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g51_with_rotary_axis_program, + sizeof(g51_with_rotary_axis_program) - 1) != 0, + "expected LinuxCNC to reject rotary axis motion in G5.1"); + ok &= expect(std::string(cnc_sim_last_error(sim)).find("Splines may not have motion in A, B, or C") != std::string::npos, + cnc_sim_last_error(sim)); + const char repeated_cycle_program[] = "G21 G90 G17\n" "G0 X0 Y0 Z0\n" @@ -2582,6 +3378,15 @@ int main() { 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 cutter_comp_cycle_program[] = + "G21 G90 G17\n" + "F100\n" + "G41.1 D1\n" + "G81 X1 Z-1 R1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, cutter_comp_cycle_program, sizeof(cutter_comp_cycle_program) - 1) != 0, + "expected canned cycle with cutter compensation on to fail"); + const char missing_g73_q_program[] = "G21 G90 G17\n" "G73 X1 Z-1 R1\n"; @@ -2863,6 +3668,123 @@ 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 parameter_expression_index_program[] = + "#1 = 5\n" + "#6 = [#ABS[-1] + 2]\n" + "G21 G90\n" + "F100\n" + "G1 X#6\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + parameter_expression_index_program, + sizeof(parameter_expression_index_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_parameter_expression_index = saw_parameter_expression_index || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 5 && + event.end.x == 7.0); + } + ok &= expect(saw_parameter_expression_index, + "expected LinuxCNC parameter reference index to accept a real-value expression"); + + const char parameter_unary_index_word_program[] = + "#1 = 9\n" + "G21 G90\n" + "F100\n" + "G1 X#ABS[-1]\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + parameter_unary_index_word_program, + sizeof(parameter_unary_index_word_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_parameter_unary_index_word = saw_parameter_unary_index_word || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + event.end.x == 9.0); + } + ok &= expect(saw_parameter_unary_index_word, + "expected LinuxCNC word parameter index to accept an unbracketed unary expression"); + + const char parameter_signed_index_word_program[] = + "#1 = 8\n" + "#2 = -1\n" + "G21 G90\n" + "F100\n" + "G1 X#-#2\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + parameter_signed_index_word_program, + sizeof(parameter_signed_index_word_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_parameter_signed_index_word = saw_parameter_signed_index_word || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 5 && + event.end.x == 8.0); + } + ok &= expect(saw_parameter_signed_index_word, + "expected LinuxCNC word parameter index to accept a signed parameter expression"); + + const char parameter_signed_index_assignment_program[] = + "#2 = -1\n" + "#-#2 = 6\n" + "G21 G90\n" + "F100\n" + "G1 X#1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + parameter_signed_index_assignment_program, + sizeof(parameter_signed_index_assignment_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_parameter_signed_index_assignment = saw_parameter_signed_index_assignment || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 5 && + event.end.x == 6.0); + } + ok &= expect(saw_parameter_signed_index_assignment, + "expected LinuxCNC parameter assignment index to accept a signed parameter expression"); + + const char spaced_parameter_index_program[] = + "#1 = 8\n" + "G21 G90\n" + "F100\n" + "G1 X# ABS[-1]\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + spaced_parameter_index_program, + sizeof(spaced_parameter_index_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_spaced_parameter_index_word = saw_spaced_parameter_index_word || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + event.end.x == 8.0); + } + ok &= expect(saw_spaced_parameter_index_word, + "expected LinuxCNC to ignore whitespace after # in a parameter index"); + + const char spaced_expression_parameter_index_program[] = + "#1 = 8\n" + "G21 G90\n" + "F100\n" + "G1 X[# ABS[-1]]\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + spaced_expression_parameter_index_program, + sizeof(spaced_expression_parameter_index_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_spaced_expression_parameter_index = saw_spaced_expression_parameter_index || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + event.end.x == 8.0); + } + ok &= expect(saw_spaced_expression_parameter_index, + "expected LinuxCNC to ignore whitespace after # inside an expression parameter index"); + const char parallel_parameter_assignment_program[] = "G21 G90\n" "F100\n" @@ -3068,6 +3990,41 @@ int main() { ok &= expect(saw_lowercase_named_oword_call, "expected lowercase named O-word subprogram call"); ok &= expect(saw_lowercase_named_oword_return, "expected lowercase named O-word return to caller"); + const char lowercase_numeric_oword_program[] = + "o100 sub\n" + "o10 if [#1 GT 0]\n" + "o10 return [#1 * 2]\n" + "o10 endif\n" + "o100 endsub [7]\n" + "g21 g90\n" + "f100\n" + "o100 call [3]\n" + "g1 x#<_value>\n" + "o100 call [0]\n" + "g1 y#<_value>\n" + "m30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + lowercase_numeric_oword_program, + sizeof(lowercase_numeric_oword_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_lowercase_numeric_oword_return = false; + bool saw_lowercase_numeric_oword_endsub = false; + for (const auto &event : events) { + saw_lowercase_numeric_oword_return = saw_lowercase_numeric_oword_return || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 9 && + near(event.end.x, 6.0)); + saw_lowercase_numeric_oword_endsub = saw_lowercase_numeric_oword_endsub || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 11 && + near(event.end.y, 7.0)); + } + ok &= expect(saw_lowercase_numeric_oword_return, + "expected lowercase numeric O-word return[value] to set #<_value>"); + ok &= expect(saw_lowercase_numeric_oword_endsub, + "expected lowercase numeric O-word endsub[value] to set #<_value>"); + const char named_parameter_program[] = "# = 5\n" "# = [# + 2]\n" @@ -3107,6 +4064,977 @@ 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 parameter_expression_assignment_program[] = + "G20\n" + "# = 2\n" + "#atan[1]/[1] = 3\n" + "#3 = 4\n" + "G0 X# Y#45 Z#3\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + parameter_expression_assignment_program, + sizeof(parameter_expression_assignment_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_parameter_expression_assignment_motion = false; + for (const auto &event : events) { + saw_parameter_expression_assignment_motion = saw_parameter_expression_assignment_motion || + (event.type == CNC_SIM_EVENT_RAPID && + event.line == 5 && + near(event.end.x, 50.8) && + near(event.end.y, 76.2) && + near(event.end.z, 101.6)); + } + ok &= expect(saw_parameter_expression_assignment_motion, + "expected LinuxCNC parameter expression assignment without outer brackets"); + + const char m98_mixed_styles_program[] = + "#1 = 42.01\n" + "#30 = 42.30\n" + "#31 = 42.31\n" + "M98 P1\n" + "G1 X#1 Y#30 Z#31 F100\n" + "#1 = 42.01\n" + "#30 = 42.30\n" + "#31 = 42.31\n" + "O2 call\n" + "G1 A#1 B#30 C#31\n" + "M30\n" + "O1\n" + "#1 = 13.01\n" + "#30 = 13.30\n" + "#31 = 13.31\n" + "M99\n" + "O2 sub\n" + "#1 = 13.01\n" + "#30 = 13.30\n" + "#31 = 13.31\n" + "O2 endsub\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + m98_mixed_styles_program, + sizeof(m98_mixed_styles_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_m98_global_parameter_motion = false; + bool saw_oword_local_parameter_motion = false; + for (const auto &event : events) { + saw_m98_global_parameter_motion = saw_m98_global_parameter_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 5 && + near(event.end.x, 13.01) && + near(event.end.y, 13.30) && + near(event.end.z, 13.31)); + saw_oword_local_parameter_motion = saw_oword_local_parameter_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 10 && + near(event.end.a, 42.01) && + near(event.end.b, 42.30) && + near(event.end.c, 13.31)); + } + ok &= expect(saw_m98_global_parameter_motion, + "expected M98 subprogram to remain discoverable when O-word calls share the file"); + ok &= expect(saw_oword_local_parameter_motion, + "expected O-word local parameter scope to coexist with M98 subprogram discovery"); + + const char numeric_oword_main_eof_program[] = + "O2 sub\n" + "G0 Z3\n" + "O2 endsub\n" + "\n" + "O1\n" + "G0 X1 Y2\n" + "O2 call\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + numeric_oword_main_eof_program, + sizeof(numeric_oword_main_eof_program) - 1) != 0, + "expected LinuxCNC to reject numeric O-word main program ending at EOF"); + + const char numeric_oword_sub_illegal_location_program[] = + "O1\n" + "G0 X1 Y2\n" + "O2 call\n" + "\n" + "O2 sub\n" + "G0 Z3\n" + "O2 endsub\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + numeric_oword_sub_illegal_location_program, + sizeof(numeric_oword_sub_illegal_location_program) - 1) != 0, + "expected LinuxCNC to reject numeric O-word subprogram before main program end"); + + const char canned_cycle_rotary_followup_program[] = + "G81 X0 Y0 Z-1 R0 F12\n" + "A90\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + canned_cycle_rotary_followup_program, + sizeof(canned_cycle_rotary_followup_program) - 1) != 0, + "expected LinuxCNC to reject rotary axis word during active canned cycle"); + + const char nested_subroutine_definition_program[] = + "O sub\n" + "O100 sub\n" + "O100 endsub\n" + "O endsub\n" + "M2\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + nested_subroutine_definition_program, + sizeof(nested_subroutine_definition_program) - 1) != 0, + "expected LinuxCNC to reject nested subroutine definition"); + + const char cutter_comp_arc_exit_program[] = + "G0 X0 Y0 Z0\n" + "G41.1 D.25\n" + "G1 X1 F1\n" + "G1 X2\n" + "G40\n" + "G2 I1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + cutter_comp_arc_exit_program, + sizeof(cutter_comp_arc_exit_program) - 1) != 0, + "expected LinuxCNC to reject arc move immediately after exiting cutter compensation"); + + const char abort_hot_comment_program[] = + "#42 = 0\n" + "# = 4711\n" + "O100 while [#42 LT 100]\n" + "O200 if [#42 EQ 20]\n" + " (abort, MixedCase param42=#42 named=#)\n" + "O200 endif\n" + "#42 = [#42 + 1]\n" + "O100 endwhile\n" + "M2\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + abort_hot_comment_program, + sizeof(abort_hot_comment_program) - 1) != 0, + "expected LinuxCNC abort hot comment to stop execution"); + + const char cutter_comp_gouging_program[] = + "G0 X0 Y0 Z0\n" + "G41.1 D.25\n" + "G1 X1 F1\n" + "G1 X2\n" + "G1 Y.1\n" + "G1 X1\n" + "G40\n" + "G1 X2\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + cutter_comp_gouging_program, + sizeof(cutter_comp_gouging_program) - 1) != 0, + "expected LinuxCNC to reject straight-feed concave cutter-comp gouging"); + + const char readonly_numbered_parameter_program[] = + "G21 G90 G17\n" + "T1 M6\n" + "G43 H1\n" + "F100\n" + "O10 if [[#5400 EQ 0] AND [#5403 EQ 12.5] AND [#5410 EQ 0] AND [#5411 EQ 0] AND [#5412 EQ 0] AND [#5413 EQ 0] AND [#5422 EQ -12.5]]\n" + "G1 X1\n" + "O10 endif\n" + "G38.2 X5 Y0 Z-1\n" + "O20 if [[#5061 EQ 5] AND [#5062 EQ 0] AND [#5063 EQ -1] AND [#5064 EQ 0] AND [#5065 EQ 0] AND [#5066 EQ 0] AND [#5067 EQ 0] AND [#5068 EQ 0] AND [#5069 EQ 0] AND [#5070 EQ 1]]\n" + "G1 Y1\n" + "O20 endif\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + readonly_numbered_parameter_program, + sizeof(readonly_numbered_parameter_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_numbered_tool_and_position_params = false; + bool saw_numbered_probe_params = false; + for (const auto &event : events) { + saw_numbered_tool_and_position_params = saw_numbered_tool_and_position_params || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 6 && + near(event.end.x, 1.0)); + saw_numbered_probe_params = saw_numbered_probe_params || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 10 && + near(event.end.y, 1.0)); + } + ok &= expect(saw_numbered_tool_and_position_params, + "expected LinuxCNC numbered tool/position parameters from parameter table"); + ok &= expect(saw_numbered_probe_params, + "expected LinuxCNC numbered probe-result parameters from parameter table"); + + const char rotary_probe_numbered_parameter_program[] = + "G21 G90 G17\n" + "F100\n" + "G38.2 A5\n" + "O10 if [[#5064 EQ 5] AND [#5070 EQ 1]]\n" + "G1 X1\n" + "O10 endif\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + rotary_probe_numbered_parameter_program, + sizeof(rotary_probe_numbered_parameter_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_rotary_probe_numbered_params = false; + for (const auto &event : events) { + saw_rotary_probe_numbered_params = saw_rotary_probe_numbered_params || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 5 && + near(event.end.x, 1.0) && + near(event.end.a, 5.0)); + } + ok &= expect(saw_rotary_probe_numbered_params, + "expected LinuxCNC numbered rotary probe-result parameters from parameter table"); + + const char multi_rotary_probe_numbered_parameter_program[] = + "G21 G90 G17\n" + "F100\n" + "G38.2 A5 B6 C7 U8 V9 W10\n" + "O10 if [[#5064 EQ 5] AND [#5065 EQ 6] AND [#5066 EQ 7] AND [#5067 EQ 8] AND [#5068 EQ 9] AND [#5069 EQ 10] AND [#5070 EQ 1]]\n" + "G1 X1\n" + "O10 endif\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + multi_rotary_probe_numbered_parameter_program, + sizeof(multi_rotary_probe_numbered_parameter_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_multi_rotary_probe_numbered_params = false; + for (const auto &event : events) { + saw_multi_rotary_probe_numbered_params = saw_multi_rotary_probe_numbered_params || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 5 && + near(event.end.x, 1.0) && + near(event.end.a, 5.0) && + near(event.end.b, 6.0) && + near(event.end.c, 7.0) && + near(event.end.u, 8.0) && + near(event.end.v, 9.0) && + near(event.end.w, 10.0)); + } + ok &= expect(saw_multi_rotary_probe_numbered_params, + "expected LinuxCNC numbered multi-axis rotary probe-result parameters from parameter table"); + + const char tool_info_zero_parameter_program[] = + "G21 G90 G17\n" + "M61 Q1\n" + "F100\n" + "O10 if [[#5400 EQ 0] AND [#5410 EQ 0] AND [#5411 EQ 0] AND [#5412 EQ 0] AND [#5413 EQ 0]]\n" + "G1 X1\n" + "O10 endif\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + tool_info_zero_parameter_program, + sizeof(tool_info_zero_parameter_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_tool_info_zero_parameter = false; + for (const auto &event : events) { + saw_tool_info_zero_parameter = saw_tool_info_zero_parameter || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 5 && + near(event.end.x, 1.0)); + } + ok &= expect(saw_tool_info_zero_parameter, + "expected LinuxCNC numbered tool info parameters to remain zero in nonrandom toolchanger fixture"); + + { + ScopedEnv spindle_tool_env("CNC_SIM_TOOL_IN_SPINDLE", "1"); + const char spindle_tool_info_parameter_program[] = + "G21 G90 G17\n" + "F100\n" + "O10 if [[#5400 EQ 1] AND [#5410 EQ 6] AND [#5411 EQ 12] AND [#5412 EQ 34] AND [#5413 EQ 5]]\n" + "G1 X1\n" + "O10 endif\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + spindle_tool_info_parameter_program, + sizeof(spindle_tool_info_parameter_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_spindle_tool_info_parameter = false; + for (const auto &event : events) { + saw_spindle_tool_info_parameter = saw_spindle_tool_info_parameter || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + near(event.end.x, 1.0)); + } + ok &= expect(saw_spindle_tool_info_parameter, + "expected LinuxCNC numbered tool info parameters for preloaded spindle tool"); + } + + const char g28_g30_numbered_parameter_program[] = + "G21 G90 G17\n" + "G0 X1 Y2 Z3\n" + "G28.1\n" + "G0 X4 Y5 Z6\n" + "G30.1\n" + "O10 if [[#5161 EQ 1] AND [#5162 EQ 2] AND [#5163 EQ 3] AND [#5181 EQ 4] AND [#5182 EQ 5] AND [#5183 EQ 6]]\n" + "G1 X10 F100\n" + "O10 endif\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g28_g30_numbered_parameter_program, + sizeof(g28_g30_numbered_parameter_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_g28_g30_numbered_params = false; + for (const auto &event : events) { + saw_g28_g30_numbered_params = saw_g28_g30_numbered_params || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 7 && + near(event.end.x, 10.0)); + } + ok &= expect(saw_g28_g30_numbered_params, + "expected LinuxCNC numbered G28/G30 home parameters from parameter table"); + + const char rotary_g28_g30_numbered_parameter_program[] = + "G21 G90 G17\n" + "G0 A1 B2 C3 U4 V5 W6\n" + "G28.1\n" + "G0 A7 B8 C9 U10 V11 W12\n" + "G30.1\n" + "O10 if [[#5164 EQ 1] AND [#5165 EQ 2] AND [#5166 EQ 3] AND [#5167 EQ 4] AND [#5168 EQ 5] AND [#5169 EQ 6] AND [#5184 EQ 7] AND [#5185 EQ 8] AND [#5186 EQ 9] AND [#5187 EQ 10] AND [#5188 EQ 11] AND [#5189 EQ 12]]\n" + "G1 X1 F100\n" + "O10 endif\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + rotary_g28_g30_numbered_parameter_program, + sizeof(rotary_g28_g30_numbered_parameter_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_rotary_g28_g30_numbered_params = false; + for (const auto &event : events) { + saw_rotary_g28_g30_numbered_params = saw_rotary_g28_g30_numbered_params || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 7 && + near(event.end.x, 1.0) && + near(event.end.a, 7.0) && + near(event.end.b, 8.0) && + near(event.end.c, 9.0) && + near(event.end.u, 10.0) && + near(event.end.v, 11.0) && + near(event.end.w, 12.0)); + } + ok &= expect(saw_rotary_g28_g30_numbered_params, + "expected LinuxCNC numbered rotary G28/G30 home parameters from parameter table"); + + const char g92_numbered_parameter_program[] = + "G21 G90 G17\n" + "G92 X1 Y2 Z3\n" + "O10 if [[#5210 EQ 1] AND [#5211 EQ -1] AND [#5212 EQ -2] AND [#5213 EQ -3]]\n" + "G1 X1 F100\n" + "O10 endif\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g92_numbered_parameter_program, + sizeof(g92_numbered_parameter_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_g92_numbered_params = false; + for (const auto &event : events) { + saw_g92_numbered_params = saw_g92_numbered_params || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + near(event.end.x, 1.0)); + } + ok &= expect(saw_g92_numbered_params, + "expected LinuxCNC numbered G92 parameters from parameter table"); + + const char rotary_g92_numbered_parameter_program[] = + "G21 G90 G17\n" + "G92 A1 B2 C3 U4 V5 W6\n" + "O10 if [[#5214 EQ -1] AND [#5215 EQ -2] AND [#5216 EQ -3] AND [#5217 EQ -4] AND [#5218 EQ -5] AND [#5219 EQ -6]]\n" + "G1 X1 F100\n" + "O10 endif\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + rotary_g92_numbered_parameter_program, + sizeof(rotary_g92_numbered_parameter_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_rotary_g92_numbered_params = false; + for (const auto &event : events) { + saw_rotary_g92_numbered_params = saw_rotary_g92_numbered_params || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + near(event.end.x, 1.0)); + } + ok &= expect(saw_rotary_g92_numbered_params, + "expected LinuxCNC numbered rotary G92 parameters from parameter table"); + + const char g52_g92_numbered_interaction_program[] = + "G20\n" + "F100\n" + "G52 X2 Y3\n" + "G52 X0 Y0\n" + "O10 if [[#5210 EQ 1] AND [#5211 EQ 0] AND [#5212 EQ 0]]\n" + "G1 X1\n" + "O10 endif\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g52_g92_numbered_interaction_program, + sizeof(g52_g92_numbered_interaction_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_g52_zero_keeps_5210 = false; + for (const auto &event : events) { + saw_g52_zero_keeps_5210 = saw_g52_zero_keeps_5210 || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 6 && + near(event.end.x, 25.4)); + } + ok &= expect(saw_g52_zero_keeps_5210, + "expected LinuxCNC #5210 to remain set after G52 X0 Y0 while #5211/#5212 clear"); + + const char g5x_numbered_parameter_program[] = + "G21 G90 G17\n" + "G10 L2 P2 X12.5 Y-3 Z4 R30\n" + "G55\n" + "O10 if [[#5220 EQ 2] AND [#5241 EQ 12.5] AND [#5242 EQ -3] AND [#5243 EQ 4] AND [#5250 EQ 30]]\n" + "G1 X1 F100\n" + "O10 endif\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g5x_numbered_parameter_program, + sizeof(g5x_numbered_parameter_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_g5x_numbered_params = false; + for (const auto &event : events) { + saw_g5x_numbered_params = saw_g5x_numbered_params || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 5 && + near(event.end.x, 1.0)); + } + ok &= expect(saw_g5x_numbered_params, + "expected LinuxCNC numbered G5X parameters from parameter table"); + + const char g593_numbered_parameter_program[] = + "G21 G90 G17\n" + "G10 L2 P9 X7 Y8 Z9 R15\n" + "G59.3\n" + "O10 if [[#5220 EQ 9] AND [#5381 EQ 7] AND [#5382 EQ 8] AND [#5383 EQ 9] AND [#5390 EQ 15]]\n" + "G1 X1 F100\n" + "O10 endif\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g593_numbered_parameter_program, + sizeof(g593_numbered_parameter_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_g593_numbered_params = false; + for (const auto &event : events) { + saw_g593_numbered_params = saw_g593_numbered_params || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 5 && + near(event.end.x, 1.0)); + } + ok &= expect(saw_g593_numbered_params, + "expected LinuxCNC numbered G59.3 parameters from parameter table"); + + const char g593_rotary_numbered_parameter_program[] = + "G21 G90 G17\n" + "G10 L2 P9 A1 B2 C3 U4 V5 W6 R15\n" + "G59.3\n" + "O10 if [[#5384 EQ 1] AND [#5385 EQ 2] AND [#5386 EQ 3] AND [#5387 EQ 4] AND [#5388 EQ 5] AND [#5389 EQ 6] AND [#5390 EQ 15]]\n" + "G1 X1 F100\n" + "O10 endif\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g593_rotary_numbered_parameter_program, + sizeof(g593_rotary_numbered_parameter_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_g593_rotary_numbered_params = false; + for (const auto &event : events) { + saw_g593_rotary_numbered_params = saw_g593_rotary_numbered_params || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 5 && + near(event.end.x, 1.0)); + } + ok &= expect(saw_g593_rotary_numbered_params, + "expected LinuxCNC numbered rotary G59.3 parameters from parameter table"); + + const char g5x_rotary_numbered_parameter_program[] = + "G21 G90 G17\n" + "G10 L2 P2 A1 B2 C3 U4 V5 W6 R30\n" + "G55\n" + "O10 if [[#5244 EQ 1] AND [#5245 EQ 2] AND [#5246 EQ 3] AND [#5247 EQ 4] AND [#5248 EQ 5] AND [#5249 EQ 6] AND [#5250 EQ 30]]\n" + "G1 X1 F100\n" + "O10 endif\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g5x_rotary_numbered_parameter_program, + sizeof(g5x_rotary_numbered_parameter_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_g5x_rotary_numbered_params = false; + for (const auto &event : events) { + saw_g5x_rotary_numbered_params = saw_g5x_rotary_numbered_params || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 5 && + near(event.end.x, 1.0)); + } + ok &= expect(saw_g5x_rotary_numbered_params, + "expected LinuxCNC numbered rotary G5X parameters from parameter table"); + + const char g54_rotary_numbered_parameter_program[] = + "G21 G90 G17\n" + "G10 L2 P1 X7 Y8 Z9 A1 B2 C3 U4 V5 W6 R20\n" + "O10 if [[#5220 EQ 1] AND [#5221 EQ 7] AND [#5222 EQ 8] AND [#5223 EQ 9] AND [#5224 EQ 1] AND [#5225 EQ 2] AND [#5226 EQ 3] AND [#5227 EQ 4] AND [#5228 EQ 5] AND [#5229 EQ 6] AND [#5230 EQ 20]]\n" + "G1 X1 F100\n" + "O10 endif\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g54_rotary_numbered_parameter_program, + sizeof(g54_rotary_numbered_parameter_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_g54_rotary_numbered_params = false; + for (const auto &event : events) { + saw_g54_rotary_numbered_params = saw_g54_rotary_numbered_params || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + near(event.end.x, 1.0)); + } + ok &= expect(saw_g54_rotary_numbered_params, + "expected LinuxCNC numbered rotary G54 parameters from parameter table"); + + const char m66_result_parameter_program[] = + "#5399 = 1\n" + "M66 P0 L0\n" + "O10 if [#5399 EQ 1]\n" + "G1 X1 F100\n" + "O10 endif\n" + "#5399 = 42.13\n" + "M66 E0 L0\n" + "O20 if [#5399 EQ 42.13]\n" + "G1 Y1\n" + "O20 endif\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + m66_result_parameter_program, + sizeof(m66_result_parameter_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_m66_result_digital = false; + bool saw_m66_result_analog = false; + for (const auto &event : events) { + saw_m66_result_digital = saw_m66_result_digital || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + near(event.end.x, 1.0)); + saw_m66_result_analog = saw_m66_result_analog || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 9 && + near(event.end.y, 1.0)); + } + ok &= expect(saw_m66_result_digital, "expected LinuxCNC #5399 digital M66 immediate result"); + ok &= expect(saw_m66_result_analog, "expected LinuxCNC #5399 analog M66 immediate result"); + + const char debug_level_parameter_program[] = + "#5599 = 1\n" + "O10 if [#5599 EQ 1]\n" + "G1 X1 F100\n" + "O10 endif\n" + "(DEBUG,one)\n" + "#5599 = 0\n" + "O20 if [#5599 EQ 0]\n" + "G1 Y1\n" + "O20 endif\n" + "(DEBUG,two)\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + debug_level_parameter_program, + sizeof(debug_level_parameter_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_debug_level_enabled = false; + bool saw_debug_level_disabled = false; + bool saw_debug_comment_event = false; + for (const auto &event : events) { + saw_debug_level_enabled = saw_debug_level_enabled || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 3 && + near(event.end.x, 1.0)); + saw_debug_level_disabled = saw_debug_level_disabled || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 8 && + near(event.end.y, 1.0)); + saw_debug_comment_event = saw_debug_comment_event || + (event.type == CNC_SIM_EVENT_COMMENT && + (event.line == 5 || event.line == 10)); + } + ok &= expect(saw_debug_level_enabled, "expected LinuxCNC #5599 default-enabled debug level parameter"); + ok &= expect(saw_debug_level_disabled, "expected LinuxCNC #5599 writable debug level parameter"); + ok &= expect(!saw_debug_comment_event, "expected LinuxCNC DEBUG hot comments to avoid generic comment events"); + + const char tool_offset_numbered_parameter_program[] = + "G21 G90 G17\n" + "G43.1 X1 Y0.5 Z2\n" + "O10 if [[#5401 EQ 1] AND [#5402 EQ 0.5] AND [#5403 EQ 2] AND [#5420 EQ -1] AND [#5421 EQ -0.5] AND [#5422 EQ -2]]\n" + "G1 Y1 F100\n" + "O10 endif\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + tool_offset_numbered_parameter_program, + sizeof(tool_offset_numbered_parameter_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_tool_offset_numbered_params = false; + for (const auto &event : events) { + saw_tool_offset_numbered_params = saw_tool_offset_numbered_params || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + near(event.end.x, -1.0) && + near(event.end.y, 1.0) && + near(event.end.z, -2.0)); + } + ok &= expect(saw_tool_offset_numbered_params, + "expected LinuxCNC numbered tool-offset and current-position parameters from parameter table"); + + const char rotary_tool_offset_numbered_parameter_program[] = + "G21 G90 G17\n" + "G43.1 A1 B2 C3 U4 V5 W6\n" + "O10 if [[#5404 EQ 1] AND [#5405 EQ 2] AND [#5406 EQ 3] AND [#5407 EQ 4] AND [#5408 EQ 5] AND [#5409 EQ 6] AND [#5423 EQ -1] AND [#5424 EQ -2] AND [#5425 EQ -3] AND [#5426 EQ -4] AND [#5427 EQ -5] AND [#5428 EQ -6]]\n" + "G1 X1 F100\n" + "O10 endif\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + rotary_tool_offset_numbered_parameter_program, + sizeof(rotary_tool_offset_numbered_parameter_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_rotary_tool_offset_numbered_params = false; + for (const auto &event : events) { + saw_rotary_tool_offset_numbered_params = saw_rotary_tool_offset_numbered_params || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + near(event.end.x, 1.0) && + near(event.end.a, -1.0) && + near(event.end.b, -2.0) && + near(event.end.c, -3.0) && + near(event.end.u, -4.0) && + near(event.end.v, -5.0) && + near(event.end.w, -6.0)); + } + ok &= expect(saw_rotary_tool_offset_numbered_params, + "expected LinuxCNC numbered rotary tool-offset and current-position parameters from parameter table"); + + const char current_position_numbered_parameter_program[] = + "G21 G90 G17\n" + "F100\n" + "G1 X1 Y2 Z3 A4 B5 C6 U7 V8 W9\n" + "O10 if [[#5420 EQ 1] AND [#5421 EQ 2] AND [#5422 EQ 3] AND [#5423 EQ 4] AND [#5424 EQ 5] AND [#5425 EQ 6] AND [#5426 EQ 7] AND [#5427 EQ 8] AND [#5428 EQ 9]]\n" + "G1 X10\n" + "O10 endif\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + current_position_numbered_parameter_program, + sizeof(current_position_numbered_parameter_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_current_position_numbered_params = false; + for (const auto &event : events) { + saw_current_position_numbered_params = saw_current_position_numbered_params || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 5 && + near(event.end.x, 10.0) && + near(event.end.y, 2.0) && + near(event.end.z, 3.0) && + near(event.end.a, 4.0) && + near(event.end.b, 5.0) && + near(event.end.c, 6.0) && + near(event.end.u, 7.0) && + near(event.end.v, 8.0) && + near(event.end.w, 9.0)); + } + ok &= expect(saw_current_position_numbered_params, + "expected LinuxCNC numbered current-position parameters from parameter table"); + + const char *persistent_var_path = "/tmp/cnc_sim_smoke_persistent.var"; + { + std::ofstream var_file(persistent_var_path); + } + { + ScopedEnv var_env("CNC_SIM_RS274_VAR", persistent_var_path); + const char persistent_parameters_set_program[] = + "G21 G90 G17\n" + "#5001 = 23\n" + "G0 X1 Y2 Z3\n" + "G28.1\n" + "G10 L2 P2 X7 Y8 Z9 R30\n" + "G55\n" + "G92 X4 Y5 Z6\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + persistent_parameters_set_program, + sizeof(persistent_parameters_set_program) - 1) == 0, + cnc_sim_last_error(sim)); + const char persistent_parameters_check_program[] = + "G21 G90 G17\n" + "F100\n" + "O10 if [[#5161 EQ 1] AND [#5162 EQ 2] AND [#5163 EQ 3] AND [#5210 EQ 1] AND [#5211 LT -12.1] AND [#5211 GT -12.3] AND [#5212 LT -7.1] AND [#5212 GT -7.3] AND [#5213 EQ -12] AND [#5220 EQ 1]]\n" + "O11 if [[#5241 EQ 7] AND [#5242 EQ 8] AND [#5243 EQ 9] AND [#5250 EQ 30]]\n" + "G1 X1\n" + "O11 endif\n" + "O10 endif\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + persistent_parameters_check_program, + sizeof(persistent_parameters_check_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_persistent_parameter_restore = false; + for (const auto &event : events) { + saw_persistent_parameter_restore = saw_persistent_parameter_restore || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 5 && + near(event.end.x, 1.0)); + } + ok &= expect(saw_persistent_parameter_restore, + "expected LinuxCNC persistent numbered parameters to reload from parameter file"); + } + + const char *g54_persistent_var_path = "/tmp/cnc_sim_smoke_g54_persistent.var"; + { + std::ofstream var_file(g54_persistent_var_path); + } + { + ScopedEnv var_env("CNC_SIM_RS274_VAR", g54_persistent_var_path); + const char g54_persistent_set_program[] = + "G21 G90 G17\n" + "G10 L2 P1 X1 Y2 Z3 A4 B5 C6 U7 V8 W9 R11\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g54_persistent_set_program, + sizeof(g54_persistent_set_program) - 1) == 0, + cnc_sim_last_error(sim)); + const char g54_persistent_check_program[] = + "G21 G90 G17\n" + "F100\n" + "O10 if [[#5220 EQ 1] AND [#5221 EQ 1] AND [#5222 EQ 2] AND [#5223 EQ 3] AND [#5224 EQ 4] AND [#5225 EQ 5] AND [#5226 EQ 6] AND [#5227 EQ 7] AND [#5228 EQ 8] AND [#5229 EQ 9] AND [#5230 EQ 11]]\n" + "G1 X1\n" + "O10 endif\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g54_persistent_check_program, + sizeof(g54_persistent_check_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_g54_persistent_restore = false; + for (const auto &event : events) { + saw_g54_persistent_restore = saw_g54_persistent_restore || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + near(event.end.x, 1.0)); + } + ok &= expect(saw_g54_persistent_restore, + "expected LinuxCNC G54 persistent numbered parameters to reload from parameter file"); + } + + const char *user_persistent_var_path = "/tmp/cnc_sim_smoke_user_persistent.var"; + { + std::ofstream var_file(user_persistent_var_path); + } + { + ScopedEnv var_env("CNC_SIM_RS274_VAR", user_persistent_var_path); + const char user_persistent_set_program[] = + "#5001 = 23\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + user_persistent_set_program, + sizeof(user_persistent_set_program) - 1) == 0, + cnc_sim_last_error(sim)); + const char user_persistent_check_program[] = + "F100\n" + "O10 if [#5001 EQ 0]\n" + "G1 X1\n" + "O10 endif\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + user_persistent_check_program, + sizeof(user_persistent_check_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_user_persistent_not_saved = false; + for (const auto &event : events) { + saw_user_persistent_not_saved = saw_user_persistent_not_saved || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 3 && + near(event.end.x, 1.0)); + } + ok &= expect(saw_user_persistent_not_saved, + "expected LinuxCNC #5001 to remain absent from an empty parameter file"); + } + + const char *user_existing_persistent_var_path = "/tmp/cnc_sim_smoke_user_existing_persistent.var"; + { + std::ofstream var_file(user_existing_persistent_var_path); + var_file << "5001\t17.000000\n"; + } + { + ScopedEnv var_env("CNC_SIM_RS274_VAR", user_existing_persistent_var_path); + const char user_existing_persistent_set_program[] = + "#5001 = 23\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + user_existing_persistent_set_program, + sizeof(user_existing_persistent_set_program) - 1) == 0, + cnc_sim_last_error(sim)); + const char user_existing_persistent_check_program[] = + "F100\n" + "O10 if [#5001 EQ 23]\n" + "G1 X1\n" + "O10 endif\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + user_existing_persistent_check_program, + sizeof(user_existing_persistent_check_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_user_existing_persistent_saved = false; + for (const auto &event : events) { + saw_user_existing_persistent_saved = saw_user_existing_persistent_saved || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 3 && + near(event.end.x, 1.0)); + } + ok &= expect(saw_user_existing_persistent_saved, + "expected LinuxCNC #5001 to persist when the parameter already exists in the file"); + } + + const char *user_boundary_persistent_var_path = "/tmp/cnc_sim_smoke_user_boundary_persistent.var"; + { + std::ofstream var_file(user_boundary_persistent_var_path); + var_file << "5001\t17.000000\n5030\t18.000000\n5060\t19.000000\n"; + } + { + ScopedEnv var_env("CNC_SIM_RS274_VAR", user_boundary_persistent_var_path); + const char user_boundary_persistent_set_program[] = + "#5000 = 11\n" + "#5001 = 22\n" + "#5030 = 32\n" + "#5060 = 33\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + user_boundary_persistent_set_program, + sizeof(user_boundary_persistent_set_program) - 1) == 0, + cnc_sim_last_error(sim)); + const char user_boundary_persistent_check_program[] = + "F100\n" + "O10 if [[#5000 EQ 0] AND [#5001 EQ 22] AND [#5030 EQ 32] AND [#5060 EQ 33]]\n" + "G1 X1\n" + "O10 endif\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + user_boundary_persistent_check_program, + sizeof(user_boundary_persistent_check_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_user_persistent_boundary = false; + for (const auto &event : events) { + saw_user_persistent_boundary = saw_user_persistent_boundary || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 3 && + near(event.end.x, 1.0)); + } + ok &= expect(saw_user_persistent_boundary, + "expected LinuxCNC persistent-user parameter boundary behavior for #5000/#5001/#5060"); + } + + const char *rotary_persistent_var_path = "/tmp/cnc_sim_smoke_rotary_persistent.var"; + { + std::ofstream var_file(rotary_persistent_var_path); + } + { + ScopedEnv var_env("CNC_SIM_RS274_VAR", rotary_persistent_var_path); + const char rotary_persistent_set_program[] = + "G21 G90 G17\n" + "G0 A1 B2 C3 U4 V5 W6\n" + "G28.1\n" + "G0 A7 B8 C9 U10 V11 W12\n" + "G30.1\n" + "G10 L2 P9 A13 B14 C15 U16 V17 W18 R19\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + rotary_persistent_set_program, + sizeof(rotary_persistent_set_program) - 1) == 0, + cnc_sim_last_error(sim)); + const char rotary_persistent_check_program[] = + "G21 G90 G17\n" + "F100\n" + "O10 if [[#5164 EQ 1] AND [#5165 EQ 2] AND [#5166 EQ 3] AND [#5167 EQ 4] AND [#5168 EQ 5] AND [#5169 EQ 6] AND [#5184 EQ 7] AND [#5185 EQ 8] AND [#5186 EQ 9] AND [#5187 EQ 10] AND [#5188 EQ 11] AND [#5189 EQ 12]]\n" + "O11 if [[#5384 EQ 13] AND [#5385 EQ 14] AND [#5386 EQ 15] AND [#5387 EQ 16] AND [#5388 EQ 17] AND [#5389 EQ 18] AND [#5390 EQ 19]]\n" + "G1 X1\n" + "O11 endif\n" + "O10 endif\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + rotary_persistent_check_program, + sizeof(rotary_persistent_check_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_rotary_persistent_restore = false; + for (const auto &event : events) { + saw_rotary_persistent_restore = saw_rotary_persistent_restore || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 5 && + near(event.end.x, 1.0)); + } + ok &= expect(saw_rotary_persistent_restore, + "expected LinuxCNC rotary/UVW persistent numbered parameters to reload from parameter file"); + } + + const char volatile_global_set_program[] = + "#31 = 7\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + volatile_global_set_program, + sizeof(volatile_global_set_program) - 1) == 0, + cnc_sim_last_error(sim)); + const char volatile_global_check_program[] = + "F100\n" + "O10 if [#31 EQ 0]\n" + "G1 X1\n" + "O10 endif\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + volatile_global_check_program, + sizeof(volatile_global_check_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_volatile_global_reset = false; + for (const auto &event : events) { + saw_volatile_global_reset = saw_volatile_global_reset || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 3 && + near(event.end.x, 1.0)); + } + ok &= expect(saw_volatile_global_reset, + "expected LinuxCNC volatile global parameter #31 to reset between parses"); + + const char readonly_numbered_parameter_assignment_program[] = + "#5400 = 1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + readonly_numbered_parameter_assignment_program, + sizeof(readonly_numbered_parameter_assignment_program) - 1) != 0, + "expected LinuxCNC to reject assignment to read-only numbered parameter"); + + const char arc_center_tolerance_good_imperial_program[] = + "G20\n" + "F1\n" + "G0 X0 Y0 Z0\n" + "G2 X5.000 Y0 I[2.500 - [0.00099 * SQRT[2]]]\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + arc_center_tolerance_good_imperial_program, + sizeof(arc_center_tolerance_good_imperial_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_arc_center_tolerance_good_motion = false; + for (const auto &event : events) { + saw_arc_center_tolerance_good_motion = saw_arc_center_tolerance_good_motion || + (event.type == CNC_SIM_EVENT_ARC_FEED && + event.line == 4 && + near(event.end.x, 127.0)); + } + ok &= expect(saw_arc_center_tolerance_good_motion, + "expected LinuxCNC to accept small imperial center-format arc within tolerance"); + + const char arc_center_tolerance_bad_metric_program[] = + "G21\n" + "F1\n" + "G0 X0 Y0 Z0\n" + "G2 X50.00 Y0 I[25.00 - [0.0101 * SQRT[2]]]\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + arc_center_tolerance_bad_metric_program, + sizeof(arc_center_tolerance_bad_metric_program) - 1) != 0, + "expected LinuxCNC to reject center-format arc outside tolerance"); + const char normalized_named_parameter_program[] = "#

= 12\n" "G21 G90\n" @@ -3225,6 +5153,67 @@ int main() { } ok &= expect(saw_override_named_parameters, "expected LinuxCNC override named parameters"); + const char modal_named_parameter_program[] = + "G21 G90 G17\n" + "G97 S1000\n" + "O10 if [#<_spindle_rpm_mode> EQ 1 AND #<_spindle_css_mode> EQ 0 AND #<_ccomp> EQ 400]\n" + "G1 X1 F100\n" + "O10 endif\n" + "G41 D1\n" + "O20 if [#<_ccomp> EQ 410]\n" + "G1 Y1\n" + "O20 endif\n" + "G40\n" + "G42.1 D0.25\n" + "O30 if [#<_ccomp> EQ 420]\n" + "G1 X2\n" + "O30 endif\n" + "G40\n" + "G96 S120 D2500\n" + "O40 if [#<_spindle_rpm_mode> EQ 0 AND #<_spindle_css_mode> EQ 1]\n" + "G1 A1\n" + "O40 endif\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + modal_named_parameter_program, + sizeof(modal_named_parameter_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_spindle_rpm_mode_named_param = false; + bool saw_ccomp_left_named_param = false; + bool saw_ccomp_right_named_param = false; + bool saw_spindle_css_mode_named_param = false; + for (const auto &event : events) { + saw_spindle_rpm_mode_named_param = saw_spindle_rpm_mode_named_param || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + near(event.end.x, 1.0)); + saw_ccomp_left_named_param = saw_ccomp_left_named_param || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 8 && + near(event.end.y, 1.0)); + saw_ccomp_right_named_param = saw_ccomp_right_named_param || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 13 && + near(event.end.x, 2.0) && + near(event.end.y, 1.0)); + saw_spindle_css_mode_named_param = saw_spindle_css_mode_named_param || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 18 && + near(event.end.a, 1.0)); + } + ok &= expect(saw_spindle_rpm_mode_named_param, "expected #<_spindle_rpm_mode> readonly named parameter"); + ok &= expect(saw_ccomp_left_named_param, "expected #<_ccomp> readonly named parameter for G41"); + ok &= expect(saw_ccomp_right_named_param, "expected #<_ccomp> readonly named parameter for G42"); + ok &= expect(saw_spindle_css_mode_named_param, "expected #<_spindle_css_mode> readonly named parameter"); + + const char readonly_named_parameter_assignment_program[] = + "#<_line> = 7\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + readonly_named_parameter_assignment_program, + sizeof(readonly_named_parameter_assignment_program) - 1) != 0, + "expected LinuxCNC to reject assignment to read-only named parameter"); + const char oword_line_named_parameter_program[] = "G21 G90\n" "F100\n" @@ -3272,6 +5261,106 @@ int main() { 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 value_returned_program[] = + "O sub\n" + "O endsub [4711]\n" + "O sub\n" + "O100 return [4712]\n" + "O endsub\n" + "O sub\n" + "O endsub\n" + "G21 G90\n" + "F100\n" + "O10 if [#<_value> EQ 0 AND #<_value_returned> EQ 0]\n" + "G1 X1\n" + "O10 endif\n" + "O call\n" + "O20 if [#<_value> EQ 4711 AND #<_value_returned> EQ 1]\n" + "G1 Y1\n" + "O20 endif\n" + "O call\n" + "O30 if [#<_value> EQ 0 AND #<_value_returned> EQ 0]\n" + "G1 Z1\n" + "O30 endif\n" + "O call\n" + "O40 if [#<_value> EQ 4712 AND #<_value_returned> EQ 1]\n" + "G1 A1\n" + "O40 endif\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + value_returned_program, + sizeof(value_returned_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_value_returned_initial = false; + bool saw_value_returned_endsub = false; + bool saw_value_returned_cleared = false; + bool saw_value_returned_return = false; + for (const auto &event : events) { + saw_value_returned_initial = saw_value_returned_initial || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 11 && + near(event.end.x, 1.0)); + saw_value_returned_endsub = saw_value_returned_endsub || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 15 && + near(event.end.y, 1.0)); + saw_value_returned_cleared = saw_value_returned_cleared || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 19 && + near(event.end.z, 1.0)); + saw_value_returned_return = saw_value_returned_return || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 23 && + near(event.end.a, 1.0)); + } + ok &= expect(saw_value_returned_initial, "expected #<_value_returned> clear before any O-word call"); + ok &= expect(saw_value_returned_endsub, "expected #<_value_returned> set after endsub [value]"); + ok &= expect(saw_value_returned_cleared, "expected #<_value_returned> cleared after O-word procedure call"); + ok &= expect(saw_value_returned_return, "expected #<_value_returned> set after return [value]"); + + const char call_level_program[] = + "O sub\n" + "O10 if [#<_call_level> EQ 2 AND #<_remap_level> EQ 0]\n" + "G1 Z1\n" + "O10 endif\n" + "O endsub\n" + "O sub\n" + "O20 if [#<_call_level> EQ 1 AND #<_remap_level> EQ 0]\n" + "G1 Y1\n" + "O20 endif\n" + "O call\n" + "O endsub\n" + "G21 G90\n" + "F100\n" + "O30 if [#<_call_level> EQ 0 AND #<_remap_level> EQ 0]\n" + "G1 X1\n" + "O30 endif\n" + "O call\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + call_level_program, + sizeof(call_level_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_call_level_named_parameter = saw_call_level_named_parameter || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 8 && + near(event.end.y, 1.0)); + saw_remap_level_named_parameter = saw_remap_level_named_parameter || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 3 && + near(event.end.z, 1.0)); + saw_inner_line_named_parameter = saw_inner_line_named_parameter || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 15 && + near(event.end.x, 1.0)); + } + ok &= expect(saw_call_level_named_parameter, "expected #<_call_level> readonly named parameter in nested O-word call"); + ok &= expect(saw_remap_level_named_parameter, "expected #<_remap_level> readonly named parameter"); + ok &= expect(saw_inner_line_named_parameter, "expected #<_call_level> to report zero in main program"); + const char nested_bracket_program[] = "#1 = 4\n" "#2 = [[#1 + 2] * [#1 - 1]]\n" @@ -3295,6 +5384,38 @@ 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 signed_spaced_bracket_word_program[] = + "G21 G90\n" + "F100\n" + "G1 X [1 + 2]\n" + "G1 Y + [1 + 2]\n" + "G1 X- [1 + 2]\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + signed_spaced_bracket_word_program, + sizeof(signed_spaced_bracket_word_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_spaced_bracket_word_motion = saw_spaced_bracket_word_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 3 && + event.end.x == 3.0); + saw_plus_spaced_bracket_word_motion = saw_plus_spaced_bracket_word_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + event.end.y == 3.0); + saw_signed_spaced_bracket_word_motion = saw_signed_spaced_bracket_word_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 5 && + event.end.x == -3.0); + } + ok &= expect(saw_spaced_bracket_word_motion, + "expected LinuxCNC word bracket expressions to allow whitespace before brackets"); + ok &= expect(saw_plus_spaced_bracket_word_motion, + "expected LinuxCNC word bracket expressions to allow whitespace after a plus sign"); + ok &= expect(saw_signed_spaced_bracket_word_motion, + "expected LinuxCNC word bracket expressions to allow whitespace after a minus sign"); + const char parenthesized_expression_program[] = "G21 G90\n" "F100\n" @@ -3336,6 +5457,92 @@ int main() { sizeof(invalid_arc_p_word_program) - 1) != 0, "expected LinuxCNC to reject P value less than 1 with G2/G3"); + const char r_arc_too_small_program[] = + "G21 G90 G17\n" + "F100\n" + "G2 X1 Y0 R0.1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + r_arc_too_small_program, + sizeof(r_arc_too_small_program) - 1) != 0 && + std::string(cnc_sim_last_error(sim)).find("Arc radius too small to reach end point") != std::string::npos, + "expected LinuxCNC to reject R arc radius too small to reach end point"); + + const char r_arc_same_point_program[] = + "G21 G90 G17\n" + "F100\n" + "G2 X0 Y0 R1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + r_arc_same_point_program, + sizeof(r_arc_same_point_program) - 1) != 0 && + std::string(cnc_sim_last_error(sim)).find("Current point same as end point of arc") != std::string::npos, + "expected LinuxCNC to reject R arc with identical start and end points"); + + const char negative_r_arc_program[] = + "G21 G90 G17\n" + "F100\n" + "G2 X0 Y1 R-1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + negative_r_arc_program, + sizeof(negative_r_arc_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_negative_r_arc = false; + for (const auto &event : events) { + saw_negative_r_arc = saw_negative_r_arc || + (event.type == CNC_SIM_EVENT_ARC_FEED && + event.line == 3 && + near(event.end.x, 0.0) && + near(event.end.y, 1.0) && + near(event.center.x, -0.8660254037844386) && + near(event.center.y, 0.5)); + } + ok &= expect(saw_negative_r_arc, "expected LinuxCNC clockwise negative-R arc center selection"); + + const char negative_r_arc_ccw_program[] = + "G21 G90 G17\n" + "F100\n" + "G3 X0 Y1 R-1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + negative_r_arc_ccw_program, + sizeof(negative_r_arc_ccw_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_negative_r_arc_ccw = false; + for (const auto &event : events) { + saw_negative_r_arc_ccw = saw_negative_r_arc_ccw || + (event.type == CNC_SIM_EVENT_ARC_FEED && + event.line == 3 && + near(event.end.x, 0.0) && + near(event.end.y, 1.0) && + near(event.center.x, 0.8660254037844386) && + near(event.center.y, 0.5)); + } + ok &= expect(saw_negative_r_arc_ccw, "expected LinuxCNC counterclockwise negative-R arc center selection"); + + const char zero_radius_arc_program[] = + "G21 G90 G17\n" + "F100\n" + "G2 X0 Y1 I0 J1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + zero_radius_arc_program, + sizeof(zero_radius_arc_program) - 1) != 0 && + std::string(cnc_sim_last_error(sim)).find("Zero-radius arc:") != std::string::npos, + "expected LinuxCNC to reject zero-radius IJK arc"); + + const char mismatched_radius_arc_program[] = + "G21 G90 G17\n" + "F100\n" + "G2 X0 Y1 I1 J0\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + mismatched_radius_arc_program, + sizeof(mismatched_radius_arc_program) - 1) != 0 && + std::string(cnc_sim_last_error(sim)).find("Radius to end of arc differs from radius to start:") != std::string::npos, + "expected LinuxCNC to reject mismatched-radius IJK arc"); + const char invalid_m19_p_word_program[] = "G21 G90\n" "M19 R45 P3\n"; @@ -3375,6 +5582,129 @@ int main() { sizeof(unused_l_word_program) - 1) != 0, "expected LinuxCNC to reject L word with no code to use it"); + const char g71_word_use_program[] = + "G21 G90 G18\n" + "G71 D1 I0.5 Q2 R0.1 X1 Z-1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g71_word_use_program, + sizeof(g71_word_use_program) - 1) == 0, + cnc_sim_last_error(sim)); + + const char g70_is_not_units_program[] = + "G21 G90\n" + "G70\n" + "F100\n" + "G1 X25.4\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g70_is_not_units_program, + sizeof(g70_is_not_units_program) - 1) != 0, + "expected LinuxCNC to treat G70 as a lathe cycle, not inch units"); + + const char g71_requires_g18_program[] = + "G21 G90\n" + "G71 Q1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g71_requires_g18_program, + sizeof(g71_requires_g18_program) - 1) != 0, + "expected LinuxCNC to require G18 for G71/G72 lathe cycles"); + + const char g71_rejects_cutter_comp_program[] = + "G21 G90 G18\n" + "G41.1 D1\n" + "G71 Q1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g71_rejects_cutter_comp_program, + sizeof(g71_rejects_cutter_comp_program) - 1) != 0, + "expected LinuxCNC to reject G71/G72 with cutter compensation enabled"); + + const char negative_l_word_program[] = + "G21 G90\n" + "G10 L-1 P1 X0\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + negative_l_word_program, + sizeof(negative_l_word_program) - 1) != 0, + "expected LinuxCNC to reject negative L word"); + + const char noninteger_l_word_program[] = + "G21 G90\n" + "G10 L1.5 P1 X0\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + noninteger_l_word_program, + sizeof(noninteger_l_word_program) - 1) != 0, + "expected LinuxCNC to reject non-integer L word"); + + const char invalid_g10_l_word_program[] = + "G21 G90\n" + "G10 L3 P1 X0\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + invalid_g10_l_word_program, + sizeof(invalid_g10_l_word_program) - 1) != 0, + "expected LinuxCNC to reject unsupported G10 L word"); + + const char g10_l0_program[] = + "G21 G90\n" + "G10 L0\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g10_l0_program, + sizeof(g10_l0_program) - 1) == 0, + cnc_sim_last_error(sim)); + + const char g10_l0_loaded_tool_program[] = + "G21 G90\n" + "G10 L0\n"; + { + ScopedEnv spindle_tool_env("CNC_SIM_TOOL_IN_SPINDLE", "1"); + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g10_l0_loaded_tool_program, + sizeof(g10_l0_loaded_tool_program) - 1) != 0, + "expected LinuxCNC to reject G10 L0 with a loaded tool"); + } + + const char out_of_range_g10_l1_p_word_program[] = + "G21 G90\n" + "G10 L1 P0 X0\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + out_of_range_g10_l1_p_word_program, + sizeof(out_of_range_g10_l1_p_word_program) - 1) != 0, + "expected LinuxCNC to reject G10 L1 P values less than 1"); + + const char g10_l1_without_offsets_program[] = + "G21 G90\n" + "G10 L1 P1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g10_l1_without_offsets_program, + sizeof(g10_l1_without_offsets_program) - 1) != 0, + "expected LinuxCNC to reject G10 L1 without offsets"); + + const char noninteger_g10_tool_q_program[] = + "G21 G90\n" + "G10 L1 P1 Q1.5\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + noninteger_g10_tool_q_program, + sizeof(noninteger_g10_tool_q_program) - 1) != 0, + "expected LinuxCNC to reject non-integer G10 tool Q word"); + + const char out_of_range_g10_tool_q_program[] = + "G21 G90\n" + "G10 L1 P1 Q10\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + out_of_range_g10_tool_q_program, + sizeof(out_of_range_g10_tool_q_program) - 1) != 0, + "expected LinuxCNC to reject G10 tool orientation greater than 9"); + const char unused_i_word_program[] = "G21 G90\n" "F100\n" @@ -3459,14 +5789,24 @@ int main() { 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[] = + const char negative_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"); + negative_m62_p_word_program, + sizeof(negative_m62_p_word_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_negative_m62_output = false; + for (const auto &event : events) { + saw_negative_m62_output = saw_negative_m62_output || + (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 2 && + event.reserved == 62 && + event.tool == -1); + } + ok &= expect(saw_negative_m62_output, + "expected LinuxCNC to allow M62 P-1 and pass the rounded output index"); const char invalid_g10_l20_r_word_program[] = "G21 G90\n" @@ -3516,6 +5856,15 @@ int main() { sizeof(missing_g4_p_word_program) - 1) != 0, "expected LinuxCNC to reject G4 without a P dwell word"); + const char negative_g4_p_word_program[] = + "G21 G90\n" + "G4 P-1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + negative_g4_p_word_program, + sizeof(negative_g4_p_word_program) - 1) != 0, + "expected LinuxCNC to reject G4 P-1 as a missing dwell time"); + const char g4_with_arc_program[] = "G21 G90 G17\n" "F100\n" @@ -3562,6 +5911,173 @@ int main() { sizeof(g1_without_feed_program) - 1) != 0, "expected LinuxCNC to reject G1 with zero feed rate"); + const char negative_feed_word_program[] = + "G21 G90\n" + "F-1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + negative_feed_word_program, + sizeof(negative_feed_word_program) - 1) != 0, + "expected LinuxCNC to reject negative F word"); + + const char negative_spindle_speed_program[] = + "G21 G90\n" + "S-100\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + negative_spindle_speed_program, + sizeof(negative_spindle_speed_program) - 1) != 0, + "expected LinuxCNC to reject negative S word"); + + const char negative_tool_id_program[] = + "G21 G90\n" + "T-1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + negative_tool_id_program, + sizeof(negative_tool_id_program) - 1) != 0, + "expected LinuxCNC to reject negative T word"); + + const char missing_tool_id_program[] = + "G21 G90\n" + "T3\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + missing_tool_id_program, + sizeof(missing_tool_id_program) - 1) != 0, + "expected LinuxCNC to reject T words missing from the tool table"); + + const char noninteger_tool_id_program[] = + "G21 G90\n" + "T1.5\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + noninteger_tool_id_program, + sizeof(noninteger_tool_id_program) - 1) != 0, + "expected LinuxCNC to reject non-integer T word"); + + const char negative_m_code_program[] = + "G21 G90\n" + "M-1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + negative_m_code_program, + sizeof(negative_m_code_program) - 1) != 0, + "expected LinuxCNC to reject negative M code"); + + const char noninteger_m_code_program[] = + "G21 G90\n" + "M3.5\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + noninteger_m_code_program, + sizeof(noninteger_m_code_program) - 1) != 0, + "expected LinuxCNC to reject non-integer M code"); + + const char upper_tolerance_noninteger_m_code_program[] = + "G21 G90\n" + "M3.9999\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + upper_tolerance_noninteger_m_code_program, + sizeof(upper_tolerance_noninteger_m_code_program) - 1) != 0, + "expected LinuxCNC integer tolerance to reject M3.9999"); + + const char unknown_m_code_program[] = + "G21 G90\n" + "M54\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + unknown_m_code_program, + sizeof(unknown_m_code_program) - 1) != 0, + "expected LinuxCNC to reject unknown M code"); + + const char out_of_range_m_code_program[] = + "G21 G90\n" + "M200\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + out_of_range_m_code_program, + sizeof(out_of_range_m_code_program) - 1) != 0, + "expected LinuxCNC to reject M code greater than 199"); + + const char g33_with_f_word_program[] = + "G21 G90\n" + "S100 M3\n" + "G33 Z0 K1 F100\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g33_with_f_word_program, + sizeof(g33_with_f_word_program) - 1) != 0, + "expected LinuxCNC to reject F word with G33"); + + const char g33_without_k_word_program[] = + "G21 G90\n" + "S100 M3\n" + "G33 Z0\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g33_without_k_word_program, + sizeof(g33_without_k_word_program) - 1) != 0, + "expected LinuxCNC to reject G33 without K word"); + + const char g331_without_k_word_program[] = + "G21 G90\n" + "S100 M3\n" + "G33.1 Z0\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g331_without_k_word_program, + sizeof(g331_without_k_word_program) - 1) != 0, + "expected LinuxCNC to reject G33.1 without K word"); + + const char g33_without_axis_program[] = + "G21 G90\n" + "S100 M3\n" + "G33 K1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g33_without_axis_program, + sizeof(g33_without_axis_program) - 1) != 0, + "expected LinuxCNC to reject G33 without axis words"); + + const char g52_without_axis_program[] = + "G21 G90\n" + "G52\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g52_without_axis_program, + sizeof(g52_without_axis_program) - 1) != 0, + "expected LinuxCNC to reject G52 without axis words"); + + const char g92_without_axis_program[] = + "G21 G90\n" + "G92\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g92_without_axis_program, + sizeof(g92_without_axis_program) - 1) != 0, + "expected LinuxCNC to reject G92 without axis words"); + + const char g80_with_axis_program[] = + "G21 G90\n" + "G80 X1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g80_with_axis_program, + sizeof(g80_with_axis_program) - 1) != 0, + "expected LinuxCNC to reject axis words with G80"); + + const char group0_and_motion_axis_program[] = + "G21 G90\n" + "F100\n" + "G92 X0 G1 Y1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + group0_and_motion_axis_program, + sizeof(group0_and_motion_axis_program) - 1) != 0, + "expected LinuxCNC to reject group 0 and motion G codes that both use axis words"); + const char duplicate_axis_word_program[] = "G21 G90\n" "F100\n" @@ -3582,6 +6098,90 @@ int main() { sizeof(same_modal_group_g_program) - 1) != 0, "expected LinuxCNC to reject two G codes from the same modal group"); + const char same_modal_group_uv_plane_program[] = + "G21 G90\n" + "G17.1 G18.1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + same_modal_group_uv_plane_program, + sizeof(same_modal_group_uv_plane_program) - 1) != 0, + "expected LinuxCNC to reject G17.1 and G18.1 from the same modal group"); + + const char same_modal_group_predefined_position_program[] = + "G21 G90\n" + "G28 G30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + same_modal_group_predefined_position_program, + sizeof(same_modal_group_predefined_position_program) - 1) != 0, + "expected LinuxCNC to reject G28 and G30 from the same modal group"); + + const char same_modal_group_g52_g92_program[] = + "G21 G90\n" + "G52 X1 G92 Y0\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + same_modal_group_g52_g92_program, + sizeof(same_modal_group_g52_g92_program) - 1) != 0, + "expected LinuxCNC to reject G52 and G92 from the same modal group"); + + const char same_modal_group_g10_g92_program[] = + "G21 G90\n" + "G10 L2 P1 X1 G92 Y0\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + same_modal_group_g10_g92_program, + sizeof(same_modal_group_g10_g92_program) - 1) != 0, + "expected LinuxCNC to reject G10 and G92 from the same modal group"); + + const char same_modal_group_control_mode_program[] = + "G21 G90\n" + "G61 G64\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + same_modal_group_control_mode_program, + sizeof(same_modal_group_control_mode_program) - 1) != 0, + "expected LinuxCNC to reject G61 and G64 from the same modal group"); + + const char same_modal_group_g4_g53_program[] = + "G21 G90\n" + "G4 P0 G53 G0 X0\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + same_modal_group_g4_g53_program, + sizeof(same_modal_group_g4_g53_program) - 1) != 0, + "expected LinuxCNC to reject G4 and G53 from the same modal group"); + + const char same_modal_group_g921_g922_program[] = + "G21 G90\n" + "G92.1 G92.2\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + same_modal_group_g921_g922_program, + sizeof(same_modal_group_g921_g922_program) - 1) != 0, + "expected LinuxCNC to reject G92.1 and G92.2 from the same modal group"); + + const char same_modal_group_threading_motion_program[] = + "G21 G90\n" + "F100\n" + "S100 M3\n" + "G33 Z0 K1 G33.1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + same_modal_group_threading_motion_program, + sizeof(same_modal_group_threading_motion_program) - 1) != 0, + "expected LinuxCNC to reject G33 and G33.1 from the same modal group"); + + const char same_modal_group_probe_motion_program[] = + "G21 G90\n" + "F100\n" + "G38.2 Z1 G38.3\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + same_modal_group_probe_motion_program, + sizeof(same_modal_group_probe_motion_program) - 1) != 0, + "expected LinuxCNC to reject G38.2 and G38.3 from the same modal group"); + const char g80_before_motion_program[] = "G21 G90\n" "F100\n" @@ -3611,6 +6211,918 @@ int main() { sizeof(same_modal_group_m_program) - 1) != 0, "expected LinuxCNC to reject two M codes from the same modal group"); + const char same_modal_group_output_m_program[] = + "G21 G90\n" + "M62 P0 M67 E0 Q1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + same_modal_group_output_m_program, + sizeof(same_modal_group_output_m_program) - 1) != 0, + "expected LinuxCNC to reject M62 and M67 from the same modal group"); + + const char negative_g_code_program[] = + "G21 G90\n" + "G-1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + negative_g_code_program, + sizeof(negative_g_code_program) - 1) != 0, + "expected LinuxCNC to reject negative G code"); + + const char out_of_range_g_code_program[] = + "G21 G90\n" + "G100\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + out_of_range_g_code_program, + sizeof(out_of_range_g_code_program) - 1) != 0, + "expected LinuxCNC to reject G code greater than 99.9"); + + const char noninitial_line_number_program[] = + "G21 G90\n" + "G1 X1 N10\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + noninitial_line_number_program, + sizeof(noninitial_line_number_program) - 1) != 0, + "expected LinuxCNC to reject non-initial N word"); + + const char negative_line_number_program[] = + "G21 G90\n" + "N-1 G0 X0\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + negative_line_number_program, + sizeof(negative_line_number_program) - 1) != 0, + "expected LinuxCNC to reject negative N line numbers"); + + const char missing_line_number_program[] = + "G21 G90\n" + "N G0 X0\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + missing_line_number_program, + sizeof(missing_line_number_program) - 1) != 0, + "expected LinuxCNC to reject N line numbers without digits"); + + const char nonnumeric_line_number_program[] = + "G21 G90\n" + "NG0 X0\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + nonnumeric_line_number_program, + sizeof(nonnumeric_line_number_program) - 1) != 0, + "expected LinuxCNC to reject N line numbers followed by a non-digit"); + + const char bare_midline_n_program[] = + "G21 G90\n" + "G0 N X0\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + bare_midline_n_program, + sizeof(bare_midline_n_program) - 1) != 0, + "expected LinuxCNC to reject bare mid-line N words"); + + const char unknown_g_code_program[] = + "G21 G90\n" + "G11\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + unknown_g_code_program, + sizeof(unknown_g_code_program) - 1) != 0, + "expected LinuxCNC to reject unknown G code"); + + const char invalid_g50_code_program[] = + "G21 G90\n" + "G50\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + invalid_g50_code_program, + sizeof(invalid_g50_code_program) - 1) != 0, + "expected LinuxCNC to reject G50 as an unknown G code"); + + const char invalid_g62_code_program[] = + "G21 G90\n" + "G62\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + invalid_g62_code_program, + sizeof(invalid_g62_code_program) - 1) != 0, + "expected LinuxCNC to reject G62 as an unknown G code"); + + const char invalid_character_program[] = + "G21 G90\n" + "?\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + invalid_character_program, + sizeof(invalid_character_program) - 1) != 0, + "expected LinuxCNC to reject unsupported top-level characters"); + + const char unused_dollar_word_program[] = + "G21 G90\n" + "$1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + unused_dollar_word_program, + sizeof(unused_dollar_word_program) - 1) != 0, + "expected LinuxCNC to reject $ word with no code to use it"); + + const char unused_atsign_word_program[] = + "G21 G90\n" + "@1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + unused_atsign_word_program, + sizeof(unused_atsign_word_program) - 1) != 0, + "expected LinuxCNC to reject @ word with no code to use it"); + + const char unused_carat_word_program[] = + "G21 G90\n" + "^45\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + unused_carat_word_program, + sizeof(unused_carat_word_program) - 1) != 0, + "expected LinuxCNC to reject ^ word with no code to use it"); + + const char m19_dollar_word_program[] = + "G21 G90\n" + "M19 R45 $0 Q2\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + m19_dollar_word_program, + sizeof(m19_dollar_word_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_m19_dollar_orient = false; + bool saw_m19_dollar_wait = false; + for (const auto &event : events) { + saw_m19_dollar_orient = saw_m19_dollar_orient || + (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 2 && + event.reserved == 1901 && + event.tool == 0 && + near(event.feed, 45.0)); + saw_m19_dollar_wait = saw_m19_dollar_wait || + (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 2 && + event.reserved == 1902 && + event.tool == 0 && + near(event.dwell_seconds, 2.0)); + } + ok &= expect(saw_m19_dollar_orient, "expected LinuxCNC M19 $ spindle orient state"); + ok &= expect(saw_m19_dollar_wait, "expected LinuxCNC M19 $ spindle orient wait state"); + + const char spindle_dollar_program[] = + "G21\n" + "S1200 M3 $0\n" + "M4 $0\n" + "M5 $0\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, spindle_dollar_program, sizeof(spindle_dollar_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_spindle_dollar_cw = false; + bool saw_spindle_dollar_ccw = false; + bool saw_spindle_dollar_stop = false; + for (const auto &event : events) { + saw_spindle_dollar_cw = saw_spindle_dollar_cw || + (event.type == CNC_SIM_EVENT_SET_SPINDLE && + event.line == 2 && + event.tool == 0 && + event.spindle == 1200.0 && + event.reserved == 1); + saw_spindle_dollar_ccw = saw_spindle_dollar_ccw || + (event.type == CNC_SIM_EVENT_SET_SPINDLE && + event.line == 3 && + event.tool == 0 && + event.spindle == 1200.0 && + event.reserved == 2); + saw_spindle_dollar_stop = saw_spindle_dollar_stop || + (event.type == CNC_SIM_EVENT_SET_SPINDLE && + event.line == 4 && + event.tool == 0 && + event.spindle == 0.0 && + event.reserved == 0); + } + ok &= expect(saw_spindle_dollar_cw, "expected LinuxCNC M3 $ spindle state"); + ok &= expect(saw_spindle_dollar_ccw, "expected LinuxCNC M4 $ spindle state"); + ok &= expect(saw_spindle_dollar_stop, "expected LinuxCNC M5 $ spindle state"); + + const char spindle_all_program[] = + "G21\n" + "S1200 M3 $-1\n" + "M5 $-1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, spindle_all_program, sizeof(spindle_all_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_spindle_all_cw = false; + bool saw_spindle_all_stop = false; + for (const auto &event : events) { + saw_spindle_all_cw = saw_spindle_all_cw || + (event.type == CNC_SIM_EVENT_SET_SPINDLE && + event.line == 2 && + event.tool == 0 && + event.spindle == 1200.0 && + event.reserved == 1); + saw_spindle_all_stop = saw_spindle_all_stop || + (event.type == CNC_SIM_EVENT_SET_SPINDLE && + event.line == 3 && + event.tool == 0 && + event.spindle == 0.0 && + event.reserved == 0); + } + ok &= expect(saw_spindle_all_cw, "expected LinuxCNC M3 $-1 spindle state"); + ok &= expect(saw_spindle_all_stop, "expected LinuxCNC M5 $-1 spindle state"); + + const char spindle_mode_dollar_program[] = + "G21\n" + "G97 S1000 $0\n" + "G96 S120 D2500 $0\n" + "G95 $0\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + spindle_mode_dollar_program, + sizeof(spindle_mode_dollar_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_g97_dollar_mode = false; + bool saw_g96_dollar_mode = false; + bool saw_g95_dollar_feed_mode = false; + for (const auto &event : events) { + saw_g97_dollar_mode = saw_g97_dollar_mode || + (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 2 && + event.reserved == 970 && + event.tool == 0); + saw_g96_dollar_mode = saw_g96_dollar_mode || + (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 3 && + event.reserved == 960 && + event.tool == 0 && + near(event.feed, 2500.0)); + saw_g95_dollar_feed_mode = saw_g95_dollar_feed_mode || + (event.type == CNC_SIM_EVENT_SET_FEED && + event.line == 4 && + event.reserved == 1 && + event.tool == 0); + } + ok &= expect(saw_g97_dollar_mode, "expected LinuxCNC G97 $ spindle mode state"); + ok &= expect(saw_g96_dollar_mode, "expected LinuxCNC G96 $ spindle mode state"); + ok &= expect(saw_g95_dollar_feed_mode, "expected LinuxCNC G95 $ feed mode state"); + + const char spindle_speed_dollar_program[] = + "G21 G90\n" + "S1200 $0\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + spindle_speed_dollar_program, + sizeof(spindle_speed_dollar_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_spindle_speed_dollar = false; + for (const auto &event : events) { + saw_spindle_speed_dollar = saw_spindle_speed_dollar || + (event.type == CNC_SIM_EVENT_SET_SPINDLE && + event.line == 2 && + event.tool == 0 && + event.spindle == 1200.0); + } + ok &= expect(saw_spindle_speed_dollar, "expected LinuxCNC S $ spindle speed state"); + + const char spindle_speed_all_program[] = + "G21 G90\n" + "S1200 $-1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + spindle_speed_all_program, + sizeof(spindle_speed_all_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_spindle_speed_all = false; + for (const auto &event : events) { + saw_spindle_speed_all = saw_spindle_speed_all || + (event.type == CNC_SIM_EVENT_SET_SPINDLE && + event.line == 2 && + event.tool == 0 && + event.spindle == 1200.0); + } + ok &= expect(saw_spindle_speed_all, "expected LinuxCNC S $-1 spindle speed state"); + + const char invalid_spindle_speed_selector_program[] = + "G21 G90\n" + "S1200 $1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + invalid_spindle_speed_selector_program, + sizeof(invalid_spindle_speed_selector_program) - 1) != 0, + "expected LinuxCNC to reject out-of-range $ selector with S word"); + + const char invalid_m51_spindle_selector_program[] = + "G21 G90\n" + "M51 P1 $0\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + invalid_m51_spindle_selector_program, + sizeof(invalid_m51_spindle_selector_program) - 1) != 0, + "expected LinuxCNC to reject $ selector with M51 in single-spindle mode"); + + const char threading_dollar_program[] = + "G21 G90 G17\n" + "S500 M3\n" + "G0 Z5\n" + "G33 Z0 K1 $0\n" + "G0 Z5\n" + "G33.1 Z0 K1 $0\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + threading_dollar_program, + sizeof(threading_dollar_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_g33_dollar_sync_start = false; + bool saw_g33_dollar_sync_stop = false; + bool saw_g331_dollar_sync_start = false; + bool saw_g331_dollar_sync_stop = false; + for (const auto &event : events) { + saw_g33_dollar_sync_start = saw_g33_dollar_sync_start || + (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 4 && + event.reserved == 3301 && + event.tool == 0 && + near(event.feed, 1.0)); + saw_g33_dollar_sync_stop = saw_g33_dollar_sync_stop || + (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 4 && + event.reserved == 3300 && + event.tool == 0); + saw_g331_dollar_sync_start = saw_g331_dollar_sync_start || + (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 6 && + event.reserved == 3301 && + event.tool == 0 && + near(event.feed, 1.0)); + saw_g331_dollar_sync_stop = saw_g331_dollar_sync_stop || + (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 6 && + event.reserved == 3300 && + event.tool == 0); + } + ok &= expect(saw_g33_dollar_sync_start, "expected LinuxCNC G33 $ sync start"); + ok &= expect(saw_g33_dollar_sync_stop, "expected LinuxCNC G33 $ sync stop"); + ok &= expect(saw_g331_dollar_sync_start, "expected LinuxCNC G33.1 $ sync start"); + ok &= expect(saw_g331_dollar_sync_stop, "expected LinuxCNC G33.1 $ sync stop"); + + const char invalid_g33_spindle_selector_program[] = + "G21 G90\n" + "S100 M3\n" + "G33 Z0 K1 $1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + invalid_g33_spindle_selector_program, + sizeof(invalid_g33_spindle_selector_program) - 1) != 0, + "expected LinuxCNC to reject out-of-range $ selector with G33"); + + const char invalid_g331_spindle_selector_program[] = + "G21 G90\n" + "S100 M3\n" + "G33.1 Z0 K1 $1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + invalid_g331_spindle_selector_program, + sizeof(invalid_g331_spindle_selector_program) - 1) != 0, + "expected LinuxCNC to reject out-of-range $ selector with G33.1"); + + const char invalid_g95_spindle_selector_program[] = + "G21 G90\n" + "G95 $1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + invalid_g95_spindle_selector_program, + sizeof(invalid_g95_spindle_selector_program) - 1) != 0, + "expected LinuxCNC to reject out-of-range $ selector with G95"); + + const char invalid_g96_spindle_selector_program[] = + "G21 G90\n" + "G96 S120 D2500 $1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + invalid_g96_spindle_selector_program, + sizeof(invalid_g96_spindle_selector_program) - 1) != 0, + "expected LinuxCNC to reject out-of-range $ selector with G96/G97"); + + const char threading_cycle_dollar_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 $0\n" + "M30\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + threading_cycle_dollar_program, + sizeof(threading_cycle_dollar_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_g76_dollar_sync_start = false; + bool saw_g76_dollar_sync_stop = false; + for (const auto &event : events) { + saw_g76_dollar_sync_start = saw_g76_dollar_sync_start || + (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 4 && + event.reserved == 3301 && + event.tool == 0 && + near(event.feed, 0.05 * 25.4)); + saw_g76_dollar_sync_stop = saw_g76_dollar_sync_stop || + (event.type == CNC_SIM_EVENT_COMMENT && + event.line == 4 && + event.reserved == 3300 && + event.tool == 0); + } + ok &= expect(saw_g76_dollar_sync_start, "expected LinuxCNC G76 $ sync start"); + ok &= expect(saw_g76_dollar_sync_stop, "expected LinuxCNC G76 $ sync stop"); + + const char invalid_g76_spindle_selector_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 $1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + invalid_g76_spindle_selector_program, + sizeof(invalid_g76_spindle_selector_program) - 1) != 0, + "expected LinuxCNC to reject out-of-range $ selector with G76"); + + const char polar_absolute_program[] = + "G21 G90 G17\n" + "F100\n" + "G1 @1 ^90\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + polar_absolute_program, + sizeof(polar_absolute_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_polar_absolute_motion = false; + for (const auto &event : events) { + saw_polar_absolute_motion = saw_polar_absolute_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 3 && + near(event.end.x, 0.0) && + near(event.end.y, 1.0)); + } + ok &= expect(saw_polar_absolute_motion, "expected LinuxCNC polar absolute XY motion"); + + const char polar_theta_absolute_program[] = + "G21 G90 G17\n" + "F100\n" + "G1 X1\n" + "G1 ^90\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + polar_theta_absolute_program, + sizeof(polar_theta_absolute_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_polar_theta_absolute_motion = false; + for (const auto &event : events) { + saw_polar_theta_absolute_motion = saw_polar_theta_absolute_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + near(event.end.x, 0.0) && + near(event.end.y, 1.0)); + } + ok &= expect(saw_polar_theta_absolute_motion, "expected LinuxCNC polar absolute theta-only motion"); + + const char polar_theta_modal_program[] = + "G21 G90 G17\n" + "F100\n" + "G1 X1\n" + "^90\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + polar_theta_modal_program, + sizeof(polar_theta_modal_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_polar_theta_modal_motion = false; + for (const auto &event : events) { + saw_polar_theta_modal_motion = saw_polar_theta_modal_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + near(event.end.x, 0.0) && + near(event.end.y, 1.0)); + } + ok &= expect(saw_polar_theta_modal_motion, "expected LinuxCNC polar theta-only modal motion"); + + const char polar_radius_origin_program[] = + "G21 G90 G17\n" + "F100\n" + "G1 @1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + polar_radius_origin_program, + sizeof(polar_radius_origin_program) - 1) != 0, + "expected LinuxCNC to require angle for polar radius at origin"); + + const char polar_non_g17_program[] = + "G21 G90 G18\n" + "F100\n" + "G1 @1 ^90\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + polar_non_g17_program, + sizeof(polar_non_g17_program) - 1) != 0, + "expected LinuxCNC to reject polar coordinates outside G17"); + + const char polar_incremental_radius_program[] = + "G21 G91 G17\n" + "F100\n" + "G1 X1\n" + "G1 @1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + polar_incremental_radius_program, + sizeof(polar_incremental_radius_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_polar_incremental_radius_motion = false; + for (const auto &event : events) { + saw_polar_incremental_radius_motion = saw_polar_incremental_radius_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + near(event.end.x, 2.0) && + near(event.end.y, 0.0)); + } + ok &= expect(saw_polar_incremental_radius_motion, "expected LinuxCNC polar incremental radius motion"); + + const char polar_radius_modal_program[] = + "G21 G90 G17\n" + "F100\n" + "G1 X1\n" + "@2\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + polar_radius_modal_program, + sizeof(polar_radius_modal_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_polar_radius_modal_motion = false; + for (const auto &event : events) { + saw_polar_radius_modal_motion = saw_polar_radius_modal_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + near(event.end.x, 2.0) && + near(event.end.y, 0.0)); + } + ok &= expect(saw_polar_radius_modal_motion, "expected LinuxCNC polar radius-only modal motion"); + + const char polar_incremental_theta_program[] = + "G21 G91 G17\n" + "F100\n" + "G1 X1\n" + "G1 ^90\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + polar_incremental_theta_program, + sizeof(polar_incremental_theta_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_polar_incremental_theta_motion = false; + for (const auto &event : events) { + saw_polar_incremental_theta_motion = saw_polar_incremental_theta_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + near(event.end.x, 0.0) && + near(event.end.y, 1.0)); + } + ok &= expect(saw_polar_incremental_theta_motion, "expected LinuxCNC polar incremental theta motion"); + + const char polar_xy_conflict_program[] = + "G21 G90 G17\n" + "F100\n" + "G1 X1 Y0 @1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + polar_xy_conflict_program, + sizeof(polar_xy_conflict_program) - 1) != 0, + "expected LinuxCNC to reject X/Y words mixed with polar coordinate"); + + const char polar_arc_radius_mismatch_program[] = + "G21 G90 G17\n" + "F100\n" + "G2 @1 ^90 I1 J0\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + polar_arc_radius_mismatch_program, + sizeof(polar_arc_radius_mismatch_program) - 1) != 0, + "expected LinuxCNC to reject polar arc with inconsistent center radius"); + + const char polar_arc_r_program[] = + "G21 G90 G17\n" + "F100\n" + "G0 X1 Y0\n" + "G2 @1 ^90 R1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + polar_arc_r_program, + sizeof(polar_arc_r_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_polar_arc_r_motion = false; + for (const auto &event : events) { + saw_polar_arc_r_motion = saw_polar_arc_r_motion || + (event.type == CNC_SIM_EVENT_ARC_FEED && + event.line == 4 && + near(event.end.x, 0.0) && + near(event.end.y, 1.0) && + near(event.center.x, 1.0) && + near(event.center.y, 1.0)); + } + ok &= expect(saw_polar_arc_r_motion, "expected LinuxCNC polar arc with R word"); + + const char polar_arc_ijk_program[] = + "G21 G90 G17\n" + "F100\n" + "G0 X1 Y0\n" + "G3 @1 ^90 I-1 J0\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + polar_arc_ijk_program, + sizeof(polar_arc_ijk_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_polar_arc_ijk_motion = false; + for (const auto &event : events) { + saw_polar_arc_ijk_motion = saw_polar_arc_ijk_motion || + (event.type == CNC_SIM_EVENT_ARC_FEED && + event.line == 4 && + near(event.end.x, 0.0) && + near(event.end.y, 1.0) && + near(event.center.x, 0.0) && + near(event.center.y, 0.0)); + } + ok &= expect(saw_polar_arc_ijk_motion, "expected LinuxCNC polar arc with IJK center"); + + const char polar_arc_zero_radius_program[] = + "G21 G90 G17\n" + "F100\n" + "G2 @1 ^90 I0 J1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + polar_arc_zero_radius_program, + sizeof(polar_arc_zero_radius_program) - 1) != 0 && + std::string(cnc_sim_last_error(sim)).find("Zero-radius arc:") != std::string::npos, + "expected LinuxCNC to reject zero-radius polar arc"); + + const char polar_canned_cycle_program[] = + "G21 G90 G17\n" + "F100\n" + "G81 @1 ^90 Z-1 R1\n" + "G80\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + polar_canned_cycle_program, + sizeof(polar_canned_cycle_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_polar_cycle_xy_position = false; + for (const auto &event : events) { + saw_polar_cycle_xy_position = saw_polar_cycle_xy_position || + ((event.type == CNC_SIM_EVENT_RAPID || event.type == CNC_SIM_EVENT_LINEAR_FEED) && + event.line == 3 && + near(event.end.x, 0.0) && + near(event.end.y, 1.0)); + } + ok &= expect(saw_polar_cycle_xy_position, "expected LinuxCNC polar canned cycle XY position"); + + const char polar_canned_cycle_incremental_radius_program[] = + "G21 G91 G17\n" + "F100\n" + "G0 X1\n" + "G81 @1 Z-1 R1 L2\n" + "G80\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + polar_canned_cycle_incremental_radius_program, + sizeof(polar_canned_cycle_incremental_radius_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_polar_cycle_incremental_radius_first = false; + bool saw_polar_cycle_incremental_radius_second = false; + for (const auto &event : events) { + saw_polar_cycle_incremental_radius_first = saw_polar_cycle_incremental_radius_first || + ((event.type == CNC_SIM_EVENT_RAPID || event.type == CNC_SIM_EVENT_LINEAR_FEED) && + event.line == 4 && + near(event.end.x, 2.0) && + near(event.end.y, 0.0)); + saw_polar_cycle_incremental_radius_second = saw_polar_cycle_incremental_radius_second || + ((event.type == CNC_SIM_EVENT_RAPID || event.type == CNC_SIM_EVENT_LINEAR_FEED) && + event.line == 4 && + near(event.end.x, 3.0) && + near(event.end.y, 0.0)); + } + ok &= expect(saw_polar_cycle_incremental_radius_first, "expected LinuxCNC polar canned cycle incremental radius first repeat"); + ok &= expect(saw_polar_cycle_incremental_radius_second, "expected LinuxCNC polar canned cycle incremental radius second repeat"); + + const char polar_canned_cycle_incremental_theta_program[] = + "G21 G91 G17\n" + "F100\n" + "G0 X1\n" + "G81 ^90 Z-1 R1 L2\n" + "G80\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + polar_canned_cycle_incremental_theta_program, + sizeof(polar_canned_cycle_incremental_theta_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_polar_cycle_incremental_theta_first = false; + bool saw_polar_cycle_incremental_theta_second = false; + for (const auto &event : events) { + saw_polar_cycle_incremental_theta_first = saw_polar_cycle_incremental_theta_first || + ((event.type == CNC_SIM_EVENT_RAPID || event.type == CNC_SIM_EVENT_LINEAR_FEED) && + event.line == 4 && + near(event.end.x, 0.0) && + near(event.end.y, 1.0)); + saw_polar_cycle_incremental_theta_second = saw_polar_cycle_incremental_theta_second || + ((event.type == CNC_SIM_EVENT_RAPID || event.type == CNC_SIM_EVENT_LINEAR_FEED) && + event.line == 4 && + near(event.end.x, -1.0) && + near(event.end.y, 0.0)); + } + ok &= expect(saw_polar_cycle_incremental_theta_first, "expected LinuxCNC polar canned cycle incremental theta first repeat"); + ok &= expect(saw_polar_cycle_incremental_theta_second, "expected LinuxCNC polar canned cycle incremental theta second repeat"); + + const char polar_g80_program[] = + "G21 G90 G17\n" + "G80 @1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + polar_g80_program, + sizeof(polar_g80_program) - 1) != 0, + "expected LinuxCNC to reject polar coordinates with G80"); + + const char polar_g92_program[] = + "G21 G90 G17\n" + "G92 @1 ^90\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + polar_g92_program, + sizeof(polar_g92_program) - 1) != 0, + "expected LinuxCNC to reject polar coordinates with G92"); + + const char polar_g10_program[] = + "G21 G90 G17\n" + "G10 L2 P1 @1 ^90\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + polar_g10_program, + sizeof(polar_g10_program) - 1) != 0, + "expected LinuxCNC to reject polar coordinates with G10"); + + const char polar_g431_program[] = + "G21 G90 G17\n" + "G43.1 @1 ^90\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + polar_g431_program, + sizeof(polar_g431_program) - 1) != 0, + "expected LinuxCNC to reject polar coordinates with G43.1"); + + const char polar_g432_h_program[] = + "G21 G90 G17\n" + "G43.2 H1 @1 ^90\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + polar_g432_h_program, + sizeof(polar_g432_h_program) - 1) == 0, + cnc_sim_last_error(sim)); + + const char polar_g432_program[] = + "G21 G90 G17\n" + "G43.2 @1 ^90\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + polar_g432_program, + sizeof(polar_g432_program) - 1) != 0, + "expected LinuxCNC to reject polar coordinates with G43.2 when H is missing"); + + const char polar_g28_program[] = + "G21 G90 G17\n" + "G28 @1 ^90\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + polar_g28_program, + sizeof(polar_g28_program) - 1) != 0, + "expected LinuxCNC to reject polar coordinates with G28"); + + const char polar_g30_program[] = + "G21 G90 G17\n" + "G30 @1 ^90\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + polar_g30_program, + sizeof(polar_g30_program) - 1) != 0, + "expected LinuxCNC to reject polar coordinates with G30"); + + const char polar_g53_program[] = + "G21 G90 G17\n" + "G53 G0 @1 ^90\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + polar_g53_program, + sizeof(polar_g53_program) - 1) != 0, + "expected LinuxCNC to reject polar coordinates with G53"); + + const char g53_without_g52_program[] = + "G21 G90\n" + "G5.3\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g53_without_g52_program, + sizeof(g53_without_g52_program) - 1) != 0, + "expected LinuxCNC to reject G5.3 without G5.2 first"); + + const char g53_with_too_few_g52_points_program[] = + "G21 G90 G17\n" + "F100\n" + "G5.2 X1 Y1 P1\n" + "G5.3\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g53_with_too_few_g52_points_program, + sizeof(g53_with_too_few_g52_points_program) - 1) != 0, + "expected LinuxCNC to reject G5.3 with fewer control points than the NURBS order"); + + const char g52_missing_y_control_point_program[] = + "G21 G90 G17\n" + "F100\n" + "G5.2 X1 P1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g52_missing_y_control_point_program, + sizeof(g52_missing_y_control_point_program) - 1) != 0, + "expected LinuxCNC to reject G5.2 control point with X but no Y in XY plane"); + + const char g52_nonpositive_subsequent_weight_program[] = + "G21 G90 G17\n" + "F100\n" + "G5.2 X1 Y1 P1\n" + "G5.2 X2 Y2 P0\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g52_nonpositive_subsequent_weight_program, + sizeof(g52_nonpositive_subsequent_weight_program) - 1) != 0, + "expected LinuxCNC to reject non-positive P weight after the first G5.2 control point"); + + const char g52_missing_subsequent_weight_program[] = + "G21 G90 G17\n" + "F100\n" + "G5.2 X1 Y1 P1\n" + "G5.2 X2 Y2\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g52_missing_subsequent_weight_program, + sizeof(g52_missing_subsequent_weight_program) - 1) != 0, + "expected LinuxCNC to reject missing P weight after the first G5.2 control point"); + + const char g52_weight_without_subsequent_control_point_program[] = + "G21 G90 G17\n" + "F100\n" + "G5.2 X1 Y1 P1\n" + "G5.2 P1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g52_weight_without_subsequent_control_point_program, + sizeof(g52_weight_without_subsequent_control_point_program) - 1) != 0, + "expected LinuxCNC to reject G5.2 P without control point after NURBS collection starts"); + + const char g52_without_feed_program[] = + "G21 G90 G17\n" + "G5.2 X1 Y1 P1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g52_without_feed_program, + sizeof(g52_without_feed_program) - 1) != 0, + "expected LinuxCNC to reject G5.2 with zero feed rate"); + + const char inverse_time_g52_without_f_program[] = + "G21 G90 G17\n" + "G93\n" + "G5.2 X1 Y1 P1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + inverse_time_g52_without_f_program, + sizeof(inverse_time_g52_without_f_program) - 1) != 0, + "expected LinuxCNC to reject inverse-time G5.2 without F word"); + + const char g53_after_intervening_motion_program[] = + "G21 G90 G17\n" + "F100\n" + "G5.2 X1 Y1 P1\n" + "G5.2 X2 Y2 P1\n" + "G1 X3\n" + "G5.3\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + g53_after_intervening_motion_program, + sizeof(g53_after_intervening_motion_program) - 1) != 0, + "expected LinuxCNC to reject G5.3 after another motion mode interrupts G5.2"); + + const char bad_precision_g_code_program[] = + "G21 G90\n" + "F100\n" + "G1.234 X1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + bad_precision_g_code_program, + sizeof(bad_precision_g_code_program) - 1) != 0, + "expected LinuxCNC to reject G code not on a tenth boundary"); + const char scientific_notation_word_program[] = "G21 G90\n" "F100\n" @@ -3684,6 +7196,42 @@ int main() { ok &= expect(saw_word_atan_function_motion, "expected LinuxCNC ATAN function as a word value"); ok &= expect(saw_signed_word_unary_function_motion, "expected signed LinuxCNC unary function as a word value"); + const char spaced_word_unary_function_program[] = + "G21 G90\n" + "F100\n" + "G1 X SIN [30]\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + spaced_word_unary_function_program, + sizeof(spaced_word_unary_function_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_spaced_word_unary_function_motion = saw_spaced_word_unary_function_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 3 && + near(event.end.x, 0.5)); + } + ok &= expect(saw_spaced_word_unary_function_motion, + "expected LinuxCNC word unary functions to allow whitespace before argument brackets"); + + const char signed_spaced_word_unary_function_program[] = + "G21 G90\n" + "F100\n" + "G1 X- SIN [30]\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + signed_spaced_word_unary_function_program, + sizeof(signed_spaced_word_unary_function_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_signed_spaced_word_unary_function_motion = saw_signed_spaced_word_unary_function_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 3 && + near(event.end.x, -0.5)); + } + ok &= expect(saw_signed_spaced_word_unary_function_motion, + "expected LinuxCNC signed word unary functions to allow whitespace after the sign"); + const char lowercase_word_program[] = "g21 g90\n" "f100\n" @@ -3701,6 +7249,24 @@ int main() { } ok &= expect(saw_lowercase_motion, "expected LinuxCNC lowercase word parsing"); + const char spaced_word_program[] = + "G 21 G 90\n" + "F 100\n" + "G 1 X 3\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + spaced_word_program, + sizeof(spaced_word_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_spaced_word_motion = saw_spaced_word_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 3 && + event.end.x == 3.0); + } + ok &= expect(saw_spaced_word_motion, + "expected LinuxCNC to ignore whitespace between a word letter and 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" @@ -3749,6 +7315,63 @@ int main() { sizeof(missing_function_brackets_program) - 1) != 0, "expected LinuxCNC function syntax to require bracketed arguments"); + const char negative_sqrt_program[] = + "#1 = SQRT[-1]\n" + "G1 X#1 F100\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + negative_sqrt_program, + sizeof(negative_sqrt_program) - 1) != 0, + "expected LinuxCNC to reject negative SQRT arguments"); + + const char infinite_expression_program[] = + "#1 = EXP[1000]\n" + "G1 X#1 F100\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + infinite_expression_program, + sizeof(infinite_expression_program) - 1) != 0, + "expected LinuxCNC to reject infinite expression results"); + + const char hidden_infinite_expression_program[] = + "O10 if [EXP[1000] EQ EXP[1000]]\n" + "G1 X1 F100\n" + "O10 endif\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + hidden_infinite_expression_program, + sizeof(hidden_infinite_expression_program) - 1) != 0, + "expected LinuxCNC to reject infinite unary results before comparison"); + + const char hidden_infinite_bracket_expression_program[] = + "O10 if [[EXP[709] * EXP[709]] GE [EXP[709] * EXP[709]]]\n" + "G1 X1 F100\n" + "O10 endif\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + hidden_infinite_bracket_expression_program, + sizeof(hidden_infinite_bracket_expression_program) - 1) != 0, + "expected LinuxCNC to reject infinite bracket expression values before comparison"); + + const char spaced_function_bracket_program[] = + "G21 G90\n" + "F100\n" + "#1 = [SIN [30]]\n" + "G1 X#1\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + spaced_function_bracket_program, + sizeof(spaced_function_bracket_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_spaced_function_bracket = saw_spaced_function_bracket || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 4 && + near(event.end.x, 0.5)); + } + ok &= expect(saw_spaced_function_bracket, + "expected LinuxCNC to ignore whitespace before a function argument bracket"); + const char expression_operator_program[] = "#1 = [2 ** 3]\n" "#2 = [2 + 3 ** 2 * 4]\n" @@ -3863,6 +7486,37 @@ int main() { 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_word_value_program[] = + "# = 999\n" + "G0 X EXISTS[#1] Y EXISTS[#99999] Z EXISTS[#] A EXISTS[#]\n" + "#1 = 2\n" + "#2 = 99999\n" + "G0 X EXISTS[##1] Y EXISTS[##2] Z EXISTS[###1]\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + exists_word_value_program, + sizeof(exists_word_value_program) - 1) == 0, + cnc_sim_last_error(sim)); + bool saw_exists_word_value_direct = false; + bool saw_exists_word_value_indirect = false; + for (const auto &event : events) { + saw_exists_word_value_direct = saw_exists_word_value_direct || + (event.type == CNC_SIM_EVENT_RAPID && + event.line == 2 && + near(event.end.x, 1.0) && + near(event.end.y, 0.0) && + near(event.end.z, 1.0) && + near(event.end.a, 0.0)); + saw_exists_word_value_indirect = saw_exists_word_value_indirect || + (event.type == CNC_SIM_EVENT_RAPID && + event.line == 5 && + near(event.end.x, 1.0) && + near(event.end.y, 0.0) && + near(event.end.z, 0.0)); + } + ok &= expect(saw_exists_word_value_direct, "expected LinuxCNC EXISTS function as a direct word value"); + ok &= expect(saw_exists_word_value_indirect, "expected LinuxCNC EXISTS function with indirect word-value parameters"); + const char exists_numbered_parameter_program[] = "#1 = 2\n" "#2 = 99999\n" @@ -3905,6 +7559,27 @@ int main() { sizeof(invalid_exists_expression_program) - 1) != 0, "expected LinuxCNC to reject arithmetic inside EXISTS parameter reference"); + const char exists_spaced_parameter_program[] = + "#1 = 1\n" + "G21 G90\n" + "F100\n" + "O10 if [EXISTS[# 1]]\n" + "G1 X1\n" + "O10 endif\n"; + events.clear(); + ok &= expect(cnc_sim_parse_program(sim, + exists_spaced_parameter_program, + sizeof(exists_spaced_parameter_program) - 1) == 0, + cnc_sim_last_error(sim)); + for (const auto &event : events) { + saw_exists_spaced_parameter_motion = saw_exists_spaced_parameter_motion || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 5 && + event.end.x == 1.0); + } + ok &= expect(saw_exists_spaced_parameter_motion, + "expected LinuxCNC EXISTS to ignore whitespace after # in a parameter reference"); + const char missing_named_parameter_program[] = "G21 G90\n" "F100\n" diff --git a/test-linuxcnc-source-link.sh b/test-linuxcnc-source-link.sh index 0976e73..8e08ce5 100755 --- a/test-linuxcnc-source-link.sh +++ b/test-linuxcnc-source-link.sh @@ -167,6 +167,9 @@ CNC_SIM_RS274_VAR="$var_file" \ 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_extended_plane_cycle.ngc \ + >/tmp/cnc_sim_linuxcnc_source_extended_plane_cycle.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 @@ -206,6 +209,245 @@ CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ 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 +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_readonly_named_parameters_extra.ngc \ + >/tmp/cnc_sim_linuxcnc_source_readonly_named_parameters_extra.json +if CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_readonly_named_parameter_assign_error.ngc \ + >/tmp/cnc_sim_linuxcnc_source_readonly_named_parameter_assign_error.json 2>/tmp/cnc_sim_linuxcnc_source_readonly_named_parameter_assign_error.err; then + echo "expected linuxcnc_readonly_named_parameter_assign_error.ngc to fail" >&2 + exit 1 +fi +if CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_negative_sqrt_error.ngc \ + >/tmp/cnc_sim_linuxcnc_source_negative_sqrt_error.json 2>/tmp/cnc_sim_linuxcnc_source_negative_sqrt_error.err; then + echo "expected linuxcnc_negative_sqrt_error.ngc to fail" >&2 + exit 1 +fi +if CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_infinite_expression_error.ngc \ + >/tmp/cnc_sim_linuxcnc_source_infinite_expression_error.json 2>/tmp/cnc_sim_linuxcnc_source_infinite_expression_error.err; then + echo "expected linuxcnc_infinite_expression_error.ngc to fail" >&2 + exit 1 +fi +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_value_returned.ngc \ + >/tmp/cnc_sim_linuxcnc_source_value_returned.json +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_call_level.ngc \ + >/tmp/cnc_sim_linuxcnc_source_call_level.json +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_exists_word_value.ngc \ + >/tmp/cnc_sim_linuxcnc_source_exists_word_value.json +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_lowercase_numeric_oword.ngc \ + >/tmp/cnc_sim_linuxcnc_source_lowercase_numeric_oword.json +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_parameter_expression_assignment.ngc \ + >/tmp/cnc_sim_linuxcnc_source_parameter_expression_assignment.json +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_word_bracket_whitespace.ngc \ + >/tmp/cnc_sim_linuxcnc_source_word_bracket_whitespace.json +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_m98_mixed_styles_variables.ngc \ + >/tmp/cnc_sim_linuxcnc_source_m98_mixed_styles_variables.json +if CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_numeric_oword_main_eof_error.ngc \ + >/tmp/cnc_sim_linuxcnc_source_numeric_oword_main_eof_error.json 2>/tmp/cnc_sim_linuxcnc_source_numeric_oword_main_eof_error.err; then + echo "expected linuxcnc_numeric_oword_main_eof_error.ngc to fail" >&2 + exit 1 +fi +if CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_numeric_oword_sub_illegal_location.ngc \ + >/tmp/cnc_sim_linuxcnc_source_numeric_oword_sub_illegal_location.json 2>/tmp/cnc_sim_linuxcnc_source_numeric_oword_sub_illegal_location.err; then + echo "expected linuxcnc_numeric_oword_sub_illegal_location.ngc to fail" >&2 + exit 1 +fi +if CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_canned_cycle_reject_rotary_followup.ngc \ + >/tmp/cnc_sim_linuxcnc_source_canned_cycle_reject_rotary_followup.json 2>/tmp/cnc_sim_linuxcnc_source_canned_cycle_reject_rotary_followup.err; then + echo "expected linuxcnc_canned_cycle_reject_rotary_followup.ngc to fail" >&2 + exit 1 +fi +if CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_nested_subroutine_definition_error.ngc \ + >/tmp/cnc_sim_linuxcnc_source_nested_subroutine_definition_error.json 2>/tmp/cnc_sim_linuxcnc_source_nested_subroutine_definition_error.err; then + echo "expected linuxcnc_nested_subroutine_definition_error.ngc to fail" >&2 + exit 1 +fi +if CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_ccomp_arcexit_error.ngc \ + >/tmp/cnc_sim_linuxcnc_source_ccomp_arcexit_error.json 2>/tmp/cnc_sim_linuxcnc_source_ccomp_arcexit_error.err; then + echo "expected linuxcnc_ccomp_arcexit_error.ngc to fail" >&2 + exit 1 +fi +if CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_abort_hot_comment.ngc \ + >/tmp/cnc_sim_linuxcnc_source_abort_hot_comment.json 2>/tmp/cnc_sim_linuxcnc_source_abort_hot_comment.err; then + echo "expected linuxcnc_abort_hot_comment.ngc to fail" >&2 + exit 1 +fi +if CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_ccomp_gouging_error.ngc \ + >/tmp/cnc_sim_linuxcnc_source_ccomp_gouging_error.json 2>/tmp/cnc_sim_linuxcnc_source_ccomp_gouging_error.err; then + echo "expected linuxcnc_ccomp_gouging_error.ngc to fail" >&2 + exit 1 +fi +cp "$base_var_file" "$var_file" +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_numbered_parameters.ngc \ + >/tmp/cnc_sim_linuxcnc_source_numbered_parameters.json +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_rotary_probe_numbered_parameters.ngc \ + >/tmp/cnc_sim_linuxcnc_source_rotary_probe_numbered_parameters.json +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_multi_rotary_probe_numbered_parameters.ngc \ + >/tmp/cnc_sim_linuxcnc_source_multi_rotary_probe_numbered_parameters.json +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_tool_info_zero_parameter.ngc \ + >/tmp/cnc_sim_linuxcnc_source_tool_info_zero_parameter.json +CNC_SIM_TOOL_IN_SPINDLE=1 CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_spindle_tool_info_parameter.ngc \ + >/tmp/cnc_sim_linuxcnc_source_spindle_tool_info_parameter.json +if CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_readonly_numbered_parameter_assign_error.ngc \ + >/tmp/cnc_sim_linuxcnc_source_readonly_numbered_parameter_assign_error.json 2>/tmp/cnc_sim_linuxcnc_source_readonly_numbered_parameter_assign_error.err; then + echo "expected linuxcnc_readonly_numbered_parameter_assign_error.ngc to fail" >&2 + exit 1 +fi +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_g28_g30_numbered_parameters.ngc \ + >/tmp/cnc_sim_linuxcnc_source_g28_g30_numbered_parameters.json +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_rotary_g28_g30_numbered_parameters.ngc \ + >/tmp/cnc_sim_linuxcnc_source_rotary_g28_g30_numbered_parameters.json +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_g92_numbered_parameters.ngc \ + >/tmp/cnc_sim_linuxcnc_source_g92_numbered_parameters.json +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_rotary_g92_numbered_parameters.ngc \ + >/tmp/cnc_sim_linuxcnc_source_rotary_g92_numbered_parameters.json +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_g52_g92_numbered_interaction.ngc \ + >/tmp/cnc_sim_linuxcnc_source_g52_g92_numbered_interaction.json +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_g5x_numbered_parameters.ngc \ + >/tmp/cnc_sim_linuxcnc_source_g5x_numbered_parameters.json +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_g59_3_numbered_parameters.ngc \ + >/tmp/cnc_sim_linuxcnc_source_g59_3_numbered_parameters.json +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_g59_3_rotary_numbered_parameters.ngc \ + >/tmp/cnc_sim_linuxcnc_source_g59_3_rotary_numbered_parameters.json +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_g5x_rotary_numbered_parameters.ngc \ + >/tmp/cnc_sim_linuxcnc_source_g5x_rotary_numbered_parameters.json +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_g54_rotary_numbered_parameters.ngc \ + >/tmp/cnc_sim_linuxcnc_source_g54_rotary_numbered_parameters.json +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_debug_level_parameter.ngc \ + >/tmp/cnc_sim_linuxcnc_source_debug_level_parameter.json +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_m66_result_parameter.ngc \ + >/tmp/cnc_sim_linuxcnc_source_m66_result_parameter.json +cp "$base_var_file" "$var_file" +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_tool_offset_numbered_parameters.ngc \ + >/tmp/cnc_sim_linuxcnc_source_tool_offset_numbered_parameters.json +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_rotary_tool_offset_numbered_parameters.ngc \ + >/tmp/cnc_sim_linuxcnc_source_rotary_tool_offset_numbered_parameters.json +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_current_position_numbered_parameters.ngc \ + >/tmp/cnc_sim_linuxcnc_source_current_position_numbered_parameters.json +cp "$base_var_file" "$var_file" +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_persistent_parameters_set.ngc \ + >/tmp/cnc_sim_linuxcnc_source_persistent_parameters_set.json +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_persistent_parameters_check.ngc \ + >/tmp/cnc_sim_linuxcnc_source_persistent_parameters_check.json +rotary_persistent_var_file="$build_dir/rs274ngc-rotary-persistent.var" +: > "$rotary_persistent_var_file" +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$rotary_persistent_var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_rotary_persistent_parameters_set.ngc \ + >/tmp/cnc_sim_linuxcnc_source_rotary_persistent_parameters_set.json +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$rotary_persistent_var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_rotary_persistent_parameters_check.ngc \ + >/tmp/cnc_sim_linuxcnc_source_rotary_persistent_parameters_check.json +g54_persistent_var_file="$build_dir/rs274ngc-g54-persistent.var" +: > "$g54_persistent_var_file" +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$g54_persistent_var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_g54_persistent_parameters_set.ngc \ + >/tmp/cnc_sim_linuxcnc_source_g54_persistent_parameters_set.json +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$g54_persistent_var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_g54_persistent_parameters_check.ngc \ + >/tmp/cnc_sim_linuxcnc_source_g54_persistent_parameters_check.json +empty_var_file="$build_dir/rs274ngc-empty.var" +: > "$empty_var_file" +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$empty_var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_user_persistent_parameter_set.ngc \ + >/tmp/cnc_sim_linuxcnc_source_user_persistent_parameter_set.json +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$empty_var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_user_persistent_parameter_check.ngc \ + >/tmp/cnc_sim_linuxcnc_source_user_persistent_parameter_check.json +existing_user_var_file="$build_dir/rs274ngc-existing-user.var" +printf '5001\t17.000000\n' > "$existing_user_var_file" +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$existing_user_var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_user_persistent_parameter_set.ngc \ + >/tmp/cnc_sim_linuxcnc_source_user_existing_persistent_parameter_set.json +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$existing_user_var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_user_persistent_parameter_existing_check.ngc \ + >/tmp/cnc_sim_linuxcnc_source_user_existing_persistent_parameter_check.json +boundary_user_var_file="$build_dir/rs274ngc-boundary-user.var" +printf '5001\t17.000000\n5030\t18.000000\n5060\t19.000000\n' > "$boundary_user_var_file" +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$boundary_user_var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_user_persistent_parameter_boundary_set.ngc \ + >/tmp/cnc_sim_linuxcnc_source_user_persistent_parameter_boundary_set.json +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$boundary_user_var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_user_persistent_parameter_boundary_check.ngc \ + >/tmp/cnc_sim_linuxcnc_source_user_persistent_parameter_boundary_check.json +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_volatile_global_parameter_set.ngc \ + >/tmp/cnc_sim_linuxcnc_source_volatile_global_parameter_set.json +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_volatile_global_parameter_check.ngc \ + >/tmp/cnc_sim_linuxcnc_source_volatile_global_parameter_check.json +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_arc_center_tolerance_good_imperial.ngc \ + >/tmp/cnc_sim_linuxcnc_source_arc_center_tolerance_good_imperial.json +if CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_arc_center_tolerance_bad_metric.ngc \ + >/tmp/cnc_sim_linuxcnc_source_arc_center_tolerance_bad_metric.json 2>/tmp/cnc_sim_linuxcnc_source_arc_center_tolerance_bad_metric.err; then + echo "expected linuxcnc_arc_center_tolerance_bad_metric.ngc to fail" >&2 + exit 1 +fi +if CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_extended_plane_arc_error.ngc \ + >/tmp/cnc_sim_linuxcnc_source_extended_plane_arc_error.json 2>/tmp/cnc_sim_linuxcnc_source_extended_plane_arc_error.err; then + echo "expected linuxcnc_extended_plane_arc_error.ngc to fail" >&2 + exit 1 +fi +if ! grep -q "Cannot do an arc in planes G17.1, G18.1, or G19.1" /tmp/cnc_sim_linuxcnc_source_extended_plane_arc_error.err; then + echo "unexpected linuxcnc_extended_plane_arc_error.ngc error" >&2 + cat /tmp/cnc_sim_linuxcnc_source_extended_plane_arc_error.err >&2 + exit 1 +fi +if CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_g62_extended_plane_error.ngc \ + >/tmp/cnc_sim_linuxcnc_source_g62_extended_plane_error.json 2>/tmp/cnc_sim_linuxcnc_source_g62_extended_plane_error.err; then + echo "expected linuxcnc_g62_extended_plane_error.ngc to fail" >&2 + exit 1 +fi +if ! grep -q "one plane must be selected for nurbs: XY, YZ, ZX" /tmp/cnc_sim_linuxcnc_source_g62_extended_plane_error.err; then + echo "unexpected linuxcnc_g62_extended_plane_error.ngc error" >&2 + cat /tmp/cnc_sim_linuxcnc_source_g62_extended_plane_error.err >&2 + exit 1 +fi +CNC_SIM_RS274_FILE_MODE=1 CNC_SIM_RS274_VAR="$var_file" \ + "$build_dir/linuxcnc_rs274_source_dump" tests/gcode/linuxcnc_g76only.ngc \ + >/tmp/cnc_sim_linuxcnc_source_g76only.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 \ @@ -675,6 +917,31 @@ expected_plane_g87_sequence = [ if plane_g87_sequence != expected_plane_g87_sequence: raise SystemExit(f"unexpected source-linked G19 G87 plane sequence: {plane_g87_sequence!r}") +extended_plane_cycle = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_extended_plane_cycle.json").read_text()) +if not any( + event["type"] == "linear-feed" and + event["line"] == 3 and + event["end"]["u"] == 1 and + event["end"]["v"] == 2 and + event["end"]["w"] == -1 + for event in extended_plane_cycle +): + raise SystemExit("missing source-linked G17.1 G81 feed along W depth axis") +if not any( + event["type"] == "rapid" and + event["line"] == 3 and + event["end"]["u"] == 1 and + event["end"]["v"] == 2 and + event["end"]["w"] == 1 + for event in extended_plane_cycle +): + raise SystemExit("missing source-linked G17.1 G81 retract to W R plane") +if not any( + event["type"] == "set-plane" and event["line"] == 7 and event["plane"] == 171 + for event in extended_plane_cycle +): + raise SystemExit("missing source-linked cutter-comp-active G17.1 plane selection") + extended_cycle = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_canned_cycles_extended.json").read_text()) if not any( event["type"] == "linear-feed" and @@ -1112,6 +1379,544 @@ 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}") +readonly_named = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_readonly_named_parameters_extra.json").read_text()) +if not any( + event["type"] == "linear-feed" and + event["line"] == 4 and + event["end"]["x"] == 1 + for event in readonly_named +): + raise SystemExit("missing source-linked #<_spindle_rpm_mode> / #<_spindle_css_mode> G97 branch motion") +if not any( + event["type"] == "linear-feed" and + event["line"] == 8 and + event["end"]["y"] == 1 + for event in readonly_named +): + raise SystemExit("missing source-linked #<_ccomp> left-compensation branch motion") +if not any( + event["type"] == "linear-feed" and + event["line"] == 13 and + event["end"]["x"] == 2 + for event in readonly_named +): + raise SystemExit("missing source-linked #<_ccomp> right-compensation branch motion") +if not any( + event["type"] == "linear-feed" and + event["line"] == 18 and + event["end"]["a"] == 1 + for event in readonly_named +): + raise SystemExit("missing source-linked #<_spindle_rpm_mode> / #<_spindle_css_mode> G96 branch motion") + +value_returned = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_value_returned.json").read_text()) +if not any( + event["type"] == "linear-feed" and + event["line"] == 11 and + event["end"]["x"] == 1 + for event in value_returned +): + raise SystemExit("missing source-linked initial #<_value_returned> clear branch motion") +if not any( + event["type"] == "linear-feed" and + event["line"] == 15 and + event["end"]["y"] == 1 + for event in value_returned +): + raise SystemExit("missing source-linked #<_value_returned> endsub[value] branch motion") +if not any( + event["type"] == "linear-feed" and + event["line"] == 19 and + event["end"]["z"] == 1 + for event in value_returned +): + raise SystemExit("missing source-linked #<_value_returned> cleared-procedure branch motion") +if not any( + event["type"] == "linear-feed" and + event["line"] == 23 and + event["end"]["a"] == 1 + for event in value_returned +): + raise SystemExit("missing source-linked #<_value_returned> return[value] branch motion") + +call_level = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_call_level.json").read_text()) +if not any( + event["type"] == "linear-feed" and + event["line"] == 15 and + event["end"]["x"] == 1 + for event in call_level +): + raise SystemExit("missing source-linked main-program #<_call_level> branch motion") +if not any( + event["type"] == "linear-feed" and + event["line"] == 8 and + event["end"]["y"] == 1 + for event in call_level +): + raise SystemExit("missing source-linked outer O-word #<_call_level> branch motion") +if not any( + event["type"] == "linear-feed" and + event["line"] == 3 and + event["end"]["z"] == 1 + for event in call_level +): + raise SystemExit("missing source-linked inner O-word #<_call_level>/#<_remap_level> branch motion") + +exists_word_value = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_exists_word_value.json").read_text()) +if not any( + event["type"] == "rapid" and + event["line"] == 2 and + event["end"]["x"] == 1 and + event["end"]["y"] == 0 and + event["end"]["z"] == 1 and + event["end"]["a"] == 0 + for event in exists_word_value +): + raise SystemExit("missing source-linked direct EXISTS word-value motion") +if not any( + event["type"] == "rapid" and + event["line"] == 5 and + event["end"]["x"] == 1 and + event["end"]["y"] == 0 and + event["end"]["z"] == 0 + for event in exists_word_value +): + raise SystemExit("missing source-linked indirect EXISTS word-value motion") + +lowercase_numeric_oword = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_lowercase_numeric_oword.json").read_text()) +if not any( + event["type"] == "linear-feed" and + event["line"] == 9 and + event["end"]["x"] == 6 + for event in lowercase_numeric_oword +): + raise SystemExit("missing source-linked lowercase numeric O-word return[value] motion") +if not any( + event["type"] == "linear-feed" and + event["line"] == 11 and + event["end"]["y"] == 7 + for event in lowercase_numeric_oword +): + raise SystemExit("missing source-linked lowercase numeric O-word endsub[value] motion") + +parameter_expression_assignment = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_parameter_expression_assignment.json").read_text()) +if not any( + event["type"] == "rapid" and + event["line"] == 5 and + event["end"]["x"] == 2 and + event["end"]["y"] == 3 and + event["end"]["z"] == 4 + for event in parameter_expression_assignment +): + raise SystemExit("missing source-linked parameter expression assignment motion") +if not any( + event["type"] == "rapid" and + event["line"] == 8 and + event["end"]["a"] == 7 + for event in parameter_expression_assignment +): + raise SystemExit("missing source-linked parameter reference expression-index motion") +if not any( + event["type"] == "rapid" and + event["line"] == 9 and + event["end"]["b"] == 5 + for event in parameter_expression_assignment +): + raise SystemExit("missing source-linked word parameter unary expression-index motion") +if not any( + event["type"] == "rapid" and + event["line"] == 11 and + event["end"]["c"] == 5 + for event in parameter_expression_assignment +): + raise SystemExit("missing source-linked word parameter signed expression-index motion") +if not any( + event["type"] == "rapid" and + event["line"] == 13 and + event["end"]["u"] == 6 + for event in parameter_expression_assignment +): + raise SystemExit("missing source-linked parameter assignment signed expression-index motion") +if not any( + event["type"] == "rapid" and + event["line"] == 14 and + event["end"]["v"] == 6 + for event in parameter_expression_assignment +): + raise SystemExit("missing source-linked whitespace-insensitive parameter expression-index motion") +if not any( + event["type"] == "rapid" and + event["line"] == 15 and + event["end"]["w"] == 3 + for event in parameter_expression_assignment +): + raise SystemExit("missing source-linked whitespace-insensitive word value motion") + +word_bracket_whitespace = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_word_bracket_whitespace.json").read_text()) +if not any( + event["type"] == "linear-feed" and + event["line"] == 3 and + event["end"]["x"] == 3 + for event in word_bracket_whitespace +): + raise SystemExit("missing source-linked spaced bracket word X motion") +if not any( + event["type"] == "linear-feed" and + event["line"] == 4 and + event["end"]["y"] == 3 + for event in word_bracket_whitespace +): + raise SystemExit("missing source-linked plus-spaced bracket word Y motion") +if not any( + event["type"] == "linear-feed" and + event["line"] == 5 and + event["end"]["x"] == -3 + for event in word_bracket_whitespace +): + raise SystemExit("missing source-linked minus-spaced bracket word X motion") + +m98_mixed_styles = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_m98_mixed_styles_variables.json").read_text()) +if not any( + event["type"] == "linear-feed" and + event["line"] == 5 and + event["end"]["x"] == 13.01 and + event["end"]["y"] == 13.3 and + event["end"]["z"] == 13.31 + for event in m98_mixed_styles +): + raise SystemExit("missing source-linked M98 mixed-style global-parameter motion") +if not any( + event["type"] == "linear-feed" and + event["line"] == 10 and + event["end"]["a"] == 42.01 and + event["end"]["b"] == 42.3 and + event["end"]["c"] == 13.31 + for event in m98_mixed_styles +): + raise SystemExit("missing source-linked O-word mixed-style local-parameter motion") + +arc_center_tolerance_good_imperial = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_arc_center_tolerance_good_imperial.json").read_text()) +if not any( + event["type"] == "arc-feed" and + event["line"] == 4 and + event["end"]["x"] == 5 + for event in arc_center_tolerance_good_imperial +): + raise SystemExit("missing source-linked small imperial center-format arc within tolerance") + +g76only = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_g76only.json").read_text()) +if not any( + event["type"] == "comment" and + event["line"] == 8 and + event["reserved"] == 3301 + for event in g76only +): + raise SystemExit("missing source-linked G76 speed-feed sync start in g76only fixture") +if not any( + event["type"] == "program-end" and + event["line"] == 13 + for event in g76only +): + raise SystemExit("missing source-linked G76-only program end") + +numbered_parameters = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_numbered_parameters.json").read_text()) +if not any( + event["type"] == "linear-feed" and + event["line"] == 6 and + event["end"]["x"] == 1 + for event in numbered_parameters +): + raise SystemExit("missing source-linked numbered tool/position parameter branch motion") +if not any( + event["type"] == "linear-feed" and + event["line"] == 10 and + event["end"]["y"] == 1 + for event in numbered_parameters +): + raise SystemExit("missing source-linked numbered probe parameter branch motion") + +rotary_probe_numbered = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_rotary_probe_numbered_parameters.json").read_text()) +if not any( + event["type"] == "linear-feed" and + event["line"] == 5 and + event["end"]["x"] == 1 and + event["end"]["a"] == 5 + for event in rotary_probe_numbered +): + raise SystemExit("missing source-linked numbered rotary probe parameter branch motion") + +multi_rotary_probe_numbered = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_multi_rotary_probe_numbered_parameters.json").read_text()) +if not any( + event["type"] == "linear-feed" and + event["line"] == 5 and + event["end"]["x"] == 1 and + event["end"]["a"] == 5 and + event["end"]["b"] == 6 and + event["end"]["c"] == 7 and + event["end"]["u"] == 8 and + event["end"]["v"] == 9 and + event["end"]["w"] == 10 + for event in multi_rotary_probe_numbered +): + raise SystemExit("missing source-linked numbered multi-axis rotary probe parameter branch motion") + +tool_info_zero_parameter = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_tool_info_zero_parameter.json").read_text()) +if not any( + event["type"] == "linear-feed" and + event["line"] == 5 and + event["end"]["x"] == 1 + for event in tool_info_zero_parameter +): + raise SystemExit("missing source-linked zeroed #5400/#5410/#5411/#5412/#5413 branch motion") + +spindle_tool_info_parameter = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_spindle_tool_info_parameter.json").read_text()) +if not any( + event["type"] == "linear-feed" and + event["line"] == 4 and + event["end"]["x"] == 1 + for event in spindle_tool_info_parameter +): + raise SystemExit("missing source-linked preloaded spindle-tool #5400/#5410/#5411/#5412/#5413 branch motion") + +g28_g30_numbered = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_g28_g30_numbered_parameters.json").read_text()) +if not any( + event["type"] == "linear-feed" and + event["line"] == 7 and + event["end"]["x"] == 10 + for event in g28_g30_numbered +): + raise SystemExit("missing source-linked G28/G30 numbered parameter branch motion") + +rotary_g28_g30_numbered = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_rotary_g28_g30_numbered_parameters.json").read_text()) +if not any( + event["type"] == "linear-feed" and + event["line"] == 7 and + event["end"]["x"] == 1 and + event["end"]["a"] == 7 and + event["end"]["b"] == 8 and + event["end"]["c"] == 9 and + event["end"]["u"] == 10 and + event["end"]["v"] == 11 and + event["end"]["w"] == 12 + for event in rotary_g28_g30_numbered +): + raise SystemExit("missing source-linked rotary G28/G30 numbered parameter branch motion") + +g92_numbered = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_g92_numbered_parameters.json").read_text()) +if not any( + event["type"] == "linear-feed" and + event["line"] == 4 and + event["end"]["x"] == 1 + for event in g92_numbered +): + raise SystemExit("missing source-linked G92 numbered parameter branch motion") + +rotary_g92_numbered = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_rotary_g92_numbered_parameters.json").read_text()) +if not any( + event["type"] == "linear-feed" and + event["line"] == 4 and + event["end"]["x"] == 1 + for event in rotary_g92_numbered +): + raise SystemExit("missing source-linked rotary G92 numbered parameter branch motion") + +g52_g92_numbered = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_g52_g92_numbered_interaction.json").read_text()) +if not any( + event["type"] == "linear-feed" and + event["line"] == 6 and + event["end"]["x"] == 1 + for event in g52_g92_numbered +): + raise SystemExit("missing source-linked G52 X0 Y0 #5210/#5211/#5212 branch motion") + +g5x_numbered = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_g5x_numbered_parameters.json").read_text()) +if not any( + event["type"] == "linear-feed" and + event["line"] == 5 and + event["end"]["x"] == 1 + for event in g5x_numbered +): + raise SystemExit("missing source-linked G5X numbered parameter branch motion") + +g593_numbered = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_g59_3_numbered_parameters.json").read_text()) +if not any( + event["type"] == "linear-feed" and + event["line"] == 5 and + event["end"]["x"] == 1 + for event in g593_numbered +): + raise SystemExit("missing source-linked G59.3 numbered parameter branch motion") + +g593_rotary_numbered = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_g59_3_rotary_numbered_parameters.json").read_text()) +if not any( + event["type"] == "linear-feed" and + event["line"] == 5 and + event["end"]["x"] == 1 + for event in g593_rotary_numbered +): + raise SystemExit("missing source-linked rotary G59.3 numbered parameter branch motion") + +g5x_rotary_numbered = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_g5x_rotary_numbered_parameters.json").read_text()) +if not any( + event["type"] == "linear-feed" and + event["line"] == 5 and + event["end"]["x"] == 1 + for event in g5x_rotary_numbered +): + raise SystemExit("missing source-linked rotary G5X numbered parameter branch motion") + +g54_rotary_numbered = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_g54_rotary_numbered_parameters.json").read_text()) +if not any( + event["type"] == "linear-feed" and + event["line"] == 4 and + event["end"]["x"] == 1 + for event in g54_rotary_numbered +): + raise SystemExit("missing source-linked rotary G54 numbered parameter branch motion") + +debug_level_parameter = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_debug_level_parameter.json").read_text()) +if not any( + event["type"] == "linear-feed" and + event["line"] == 3 and + event["end"]["x"] == 1 + for event in debug_level_parameter +): + raise SystemExit("missing source-linked #5599 enabled debug-level branch motion") +if not any( + event["type"] == "linear-feed" and + event["line"] == 8 and + event["end"]["y"] == 1 + for event in debug_level_parameter +): + raise SystemExit("missing source-linked #5599 disabled debug-level branch motion") +if any( + event["type"] == "comment" and event["line"] in {5, 10} + for event in debug_level_parameter +): + raise SystemExit("unexpected source-linked generic comment event for DEBUG hot comment") + +m66_result_parameter = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_m66_result_parameter.json").read_text()) +if not any( + event["type"] == "linear-feed" and + event["line"] == 4 and + event["end"]["x"] == 1 + for event in m66_result_parameter +): + raise SystemExit("missing source-linked #5399 digital M66 result branch motion") +if not any( + event["type"] == "linear-feed" and + event["line"] == 9 and + event["end"]["y"] == 1 + for event in m66_result_parameter +): + raise SystemExit("missing source-linked #5399 analog M66 result branch motion") + +tool_offset_numbered = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_tool_offset_numbered_parameters.json").read_text()) +if not any( + event["type"] == "linear-feed" and + event["line"] == 4 and + event["end"]["x"] == -1 and + event["end"]["y"] == 1 and + event["end"]["z"] == -2 + for event in tool_offset_numbered +): + raise SystemExit("missing source-linked numbered tool-offset/current-position parameter branch motion") + +rotary_tool_offset_numbered = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_rotary_tool_offset_numbered_parameters.json").read_text()) +if not any( + event["type"] == "linear-feed" and + event["line"] == 4 and + event["end"]["x"] == 1 and + event["end"]["a"] == -1 and + event["end"]["b"] == -2 and + event["end"]["c"] == -3 and + event["end"]["u"] == -4 and + event["end"]["v"] == -5 and + event["end"]["w"] == -6 + for event in rotary_tool_offset_numbered +): + raise SystemExit("missing source-linked numbered rotary tool-offset/current-position parameter branch motion") + +current_position_numbered = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_current_position_numbered_parameters.json").read_text()) +if not any( + event["type"] == "linear-feed" and + event["line"] == 5 and + event["end"]["x"] == 10 and + event["end"]["y"] == 2 and + event["end"]["z"] == 3 and + event["end"]["a"] == 4 and + event["end"]["b"] == 5 and + event["end"]["c"] == 6 and + event["end"]["u"] == 7 and + event["end"]["v"] == 8 and + event["end"]["w"] == 9 + for event in current_position_numbered +): + raise SystemExit("missing source-linked numbered current-position parameter branch motion") + +persistent_parameters_check = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_persistent_parameters_check.json").read_text()) +if not any( + event["type"] == "linear-feed" and + event["line"] == 5 and + event["end"]["x"] == 1 + for event in persistent_parameters_check +): + raise SystemExit("missing source-linked persistent parameter reload branch motion") + +rotary_persistent_parameters_check = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_rotary_persistent_parameters_check.json").read_text()) +if not any( + event["type"] == "linear-feed" and + event["line"] == 5 and + event["end"]["x"] == 1 + for event in rotary_persistent_parameters_check +): + raise SystemExit("missing source-linked rotary persistent parameter reload branch motion") + +g54_persistent_parameters_check = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_g54_persistent_parameters_check.json").read_text()) +if not any( + event["type"] == "linear-feed" and + event["line"] == 4 and + event["end"]["x"] == 1 + for event in g54_persistent_parameters_check +): + raise SystemExit("missing source-linked G54 persistent parameter reload branch motion") + +user_persistent_parameter_check = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_user_persistent_parameter_check.json").read_text()) +if not any( + event["type"] == "linear-feed" and + event["line"] == 3 and + event["end"]["x"] == 1 + for event in user_persistent_parameter_check +): + raise SystemExit("missing source-linked empty-var #5001 non-persistence branch motion") + +user_existing_persistent_parameter_check = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_user_existing_persistent_parameter_check.json").read_text()) +if not any( + event["type"] == "linear-feed" and + event["line"] == 3 and + event["end"]["x"] == 1 + for event in user_existing_persistent_parameter_check +): + raise SystemExit("missing source-linked existing-var #5001 persistence branch motion") + +user_persistent_parameter_boundary_check = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_user_persistent_parameter_boundary_check.json").read_text()) +if not any( + event["type"] == "linear-feed" and + event["line"] == 3 and + event["end"]["x"] == 1 + for event in user_persistent_parameter_boundary_check +): + raise SystemExit("missing source-linked #5000/#5001/#5030/#5060 persistence boundary branch motion") + +volatile_global_parameter_check = json.loads(Path("/tmp/cnc_sim_linuxcnc_source_volatile_global_parameter_check.json").read_text()) +if not any( + event["type"] == "linear-feed" and + event["line"] == 3 and + event["end"]["x"] == 1 + for event in volatile_global_parameter_check +): + raise SystemExit("missing source-linked volatile global #31 reset branch motion") + 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"] @@ -1245,6 +2050,7 @@ 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_extended_plane_cycle.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" @@ -1258,6 +2064,48 @@ 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_readonly_named_parameters_extra.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_value_returned.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_call_level.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_exists_word_value.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_lowercase_numeric_oword.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_parameter_expression_assignment.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_word_bracket_whitespace.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_m98_mixed_styles_variables.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_arc_center_tolerance_good_imperial.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_g76only.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_numbered_parameters.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_rotary_probe_numbered_parameters.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_multi_rotary_probe_numbered_parameters.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_tool_info_zero_parameter.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_spindle_tool_info_parameter.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_g28_g30_numbered_parameters.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_rotary_g28_g30_numbered_parameters.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_g92_numbered_parameters.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_rotary_g92_numbered_parameters.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_g52_g92_numbered_interaction.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_g5x_numbered_parameters.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_g59_3_numbered_parameters.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_g59_3_rotary_numbered_parameters.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_g5x_rotary_numbered_parameters.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_g54_rotary_numbered_parameters.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_debug_level_parameter.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_m66_result_parameter.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_tool_offset_numbered_parameters.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_rotary_tool_offset_numbered_parameters.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_current_position_numbered_parameters.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_persistent_parameters_set.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_persistent_parameters_check.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_rotary_persistent_parameters_set.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_rotary_persistent_parameters_check.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_g54_persistent_parameters_set.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_g54_persistent_parameters_check.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_user_persistent_parameter_set.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_user_persistent_parameter_check.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_user_existing_persistent_parameter_set.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_user_existing_persistent_parameter_check.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_user_persistent_parameter_boundary_set.json" +echo "dumped /tmp/cnc_sim_linuxcnc_source_user_persistent_parameter_boundary_check.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" diff --git a/tests/gcode/linuxcnc_abort_hot_comment.ngc b/tests/gcode/linuxcnc_abort_hot_comment.ngc new file mode 100644 index 0000000..49b30a9 --- /dev/null +++ b/tests/gcode/linuxcnc_abort_hot_comment.ngc @@ -0,0 +1,9 @@ +#42 = 0 +# = 4711 +O100 while [#42 LT 100] +O200 if [#42 EQ 20] + (abort, MixedCase param42=#42 named=#) +O200 endif +#42 = [#42 + 1] +O100 endwhile +M2 diff --git a/tests/gcode/linuxcnc_arc_center_tolerance_bad_metric.ngc b/tests/gcode/linuxcnc_arc_center_tolerance_bad_metric.ngc new file mode 100644 index 0000000..46a8d5b --- /dev/null +++ b/tests/gcode/linuxcnc_arc_center_tolerance_bad_metric.ngc @@ -0,0 +1,4 @@ +G21 +F1 +G0 X0 Y0 Z0 +G2 X50.00 Y0 I[25.00 - [0.0101 * SQRT[2]]] diff --git a/tests/gcode/linuxcnc_arc_center_tolerance_good_imperial.ngc b/tests/gcode/linuxcnc_arc_center_tolerance_good_imperial.ngc new file mode 100644 index 0000000..e428f3f --- /dev/null +++ b/tests/gcode/linuxcnc_arc_center_tolerance_good_imperial.ngc @@ -0,0 +1,5 @@ +G20 +F1 +G0 X0 Y0 Z0 +G2 X5.000 Y0 I[2.500 - [0.00099 * SQRT[2]]] +M2 diff --git a/tests/gcode/linuxcnc_call_level.ngc b/tests/gcode/linuxcnc_call_level.ngc new file mode 100644 index 0000000..1e110af --- /dev/null +++ b/tests/gcode/linuxcnc_call_level.ngc @@ -0,0 +1,18 @@ +O sub +O10 if [#<_call_level> EQ 2 AND #<_remap_level> EQ 0] +G1 Z1 +O10 endif +O endsub +O sub +O20 if [#<_call_level> EQ 1 AND #<_remap_level> EQ 0] +G1 Y1 +O20 endif +O call +O endsub +G21 G90 +F100 +O30 if [#<_call_level> EQ 0 AND #<_remap_level> EQ 0] +G1 X1 +O30 endif +O call +M30 diff --git a/tests/gcode/linuxcnc_canned_cycle_reject_rotary_followup.ngc b/tests/gcode/linuxcnc_canned_cycle_reject_rotary_followup.ngc new file mode 100644 index 0000000..c84273a --- /dev/null +++ b/tests/gcode/linuxcnc_canned_cycle_reject_rotary_followup.ngc @@ -0,0 +1,2 @@ +G81 X0 Y0 Z-1 R0 F12 +A90 diff --git a/tests/gcode/linuxcnc_ccomp_arcexit_error.ngc b/tests/gcode/linuxcnc_ccomp_arcexit_error.ngc new file mode 100644 index 0000000..307b3a7 --- /dev/null +++ b/tests/gcode/linuxcnc_ccomp_arcexit_error.ngc @@ -0,0 +1,6 @@ +G0 X0 Y0 Z0 +G41.1 D.25 +G1 X1 F1 +G1 X2 +G40 +G2 I1 diff --git a/tests/gcode/linuxcnc_ccomp_gouging_error.ngc b/tests/gcode/linuxcnc_ccomp_gouging_error.ngc new file mode 100644 index 0000000..94de6f8 --- /dev/null +++ b/tests/gcode/linuxcnc_ccomp_gouging_error.ngc @@ -0,0 +1,8 @@ +G0 X0 Y0 Z0 +G41.1 D.25 +G1 X1 F1 +G1 X2 +G1 Y.1 +G1 X1 +G40 +G1 X2 diff --git a/tests/gcode/linuxcnc_current_position_numbered_parameters.ngc b/tests/gcode/linuxcnc_current_position_numbered_parameters.ngc new file mode 100644 index 0000000..7d37251 --- /dev/null +++ b/tests/gcode/linuxcnc_current_position_numbered_parameters.ngc @@ -0,0 +1,7 @@ +G21 G90 G17 +F100 +G1 X1 Y2 Z3 A4 B5 C6 U7 V8 W9 +O10 if [[#5420 EQ 1] AND [#5421 EQ 2] AND [#5422 EQ 3] AND [#5423 EQ 4] AND [#5424 EQ 5] AND [#5425 EQ 6] AND [#5426 EQ 7] AND [#5427 EQ 8] AND [#5428 EQ 9]] +G1 X10 +O10 endif +M30 diff --git a/tests/gcode/linuxcnc_debug_level_parameter.ngc b/tests/gcode/linuxcnc_debug_level_parameter.ngc new file mode 100644 index 0000000..a10e298 --- /dev/null +++ b/tests/gcode/linuxcnc_debug_level_parameter.ngc @@ -0,0 +1,11 @@ +#5599 = 1 +O10 if [#5599 EQ 1] +G1 X1 F100 +O10 endif +(DEBUG,one) +#5599 = 0 +O20 if [#5599 EQ 0] +G1 Y1 +O20 endif +(DEBUG,two) +M30 diff --git a/tests/gcode/linuxcnc_exists_word_value.ngc b/tests/gcode/linuxcnc_exists_word_value.ngc new file mode 100644 index 0000000..ec26c80 --- /dev/null +++ b/tests/gcode/linuxcnc_exists_word_value.ngc @@ -0,0 +1,6 @@ +# = 999 +G0 X EXISTS[#1] Y EXISTS[#99999] Z EXISTS[#] A EXISTS[#] +#1 = 2 +#2 = 99999 +G0 X EXISTS[##1] Y EXISTS[##2] Z EXISTS[###1] +M30 diff --git a/tests/gcode/linuxcnc_extended_plane_arc_error.ngc b/tests/gcode/linuxcnc_extended_plane_arc_error.ngc new file mode 100644 index 0000000..dbe4806 --- /dev/null +++ b/tests/gcode/linuxcnc_extended_plane_arc_error.ngc @@ -0,0 +1,3 @@ +G21 G90 G17.1 +F100 +G2 U1 V0 R1 diff --git a/tests/gcode/linuxcnc_extended_plane_cycle.ngc b/tests/gcode/linuxcnc_extended_plane_cycle.ngc new file mode 100644 index 0000000..66b962e --- /dev/null +++ b/tests/gcode/linuxcnc_extended_plane_cycle.ngc @@ -0,0 +1,9 @@ +G21 G90 G17.1 +F100 +G81 U1 V2 W-1 R1 +G80 +G17 +G41.1 D0.25 +G17.1 +G40 +M2 diff --git a/tests/gcode/linuxcnc_g28_g30_numbered_parameters.ngc b/tests/gcode/linuxcnc_g28_g30_numbered_parameters.ngc new file mode 100644 index 0000000..2fa020b --- /dev/null +++ b/tests/gcode/linuxcnc_g28_g30_numbered_parameters.ngc @@ -0,0 +1,9 @@ +G21 G90 G17 +G0 X1 Y2 Z3 +G28.1 +G0 X4 Y5 Z6 +G30.1 +O10 if [[#5161 EQ 1] AND [#5162 EQ 2] AND [#5163 EQ 3] AND [#5181 EQ 4] AND [#5182 EQ 5] AND [#5183 EQ 6]] +G1 X10 F100 +O10 endif +M30 diff --git a/tests/gcode/linuxcnc_g52_g92_numbered_interaction.ngc b/tests/gcode/linuxcnc_g52_g92_numbered_interaction.ngc new file mode 100644 index 0000000..17dbe8c --- /dev/null +++ b/tests/gcode/linuxcnc_g52_g92_numbered_interaction.ngc @@ -0,0 +1,8 @@ +G20 +F100 +G52 X2 Y3 +G52 X0 Y0 +O10 if [[#5210 EQ 1] AND [#5211 EQ 0] AND [#5212 EQ 0]] +G1 X1 +O10 endif +M30 diff --git a/tests/gcode/linuxcnc_g54_persistent_parameters_check.ngc b/tests/gcode/linuxcnc_g54_persistent_parameters_check.ngc new file mode 100644 index 0000000..3b8fe97 --- /dev/null +++ b/tests/gcode/linuxcnc_g54_persistent_parameters_check.ngc @@ -0,0 +1,6 @@ +G21 G90 G17 +F100 +O10 if [[#5220 EQ 1] AND [#5221 EQ 1] AND [#5222 EQ 2] AND [#5223 EQ 3] AND [#5224 EQ 4] AND [#5225 EQ 5] AND [#5226 EQ 6] AND [#5227 EQ 7] AND [#5228 EQ 8] AND [#5229 EQ 9] AND [#5230 EQ 11]] +G1 X1 +O10 endif +M30 diff --git a/tests/gcode/linuxcnc_g54_persistent_parameters_set.ngc b/tests/gcode/linuxcnc_g54_persistent_parameters_set.ngc new file mode 100644 index 0000000..13bea7a --- /dev/null +++ b/tests/gcode/linuxcnc_g54_persistent_parameters_set.ngc @@ -0,0 +1,3 @@ +G21 G90 G17 +G10 L2 P1 X1 Y2 Z3 A4 B5 C6 U7 V8 W9 R11 +M30 diff --git a/tests/gcode/linuxcnc_g54_rotary_numbered_parameters.ngc b/tests/gcode/linuxcnc_g54_rotary_numbered_parameters.ngc new file mode 100644 index 0000000..2c32898 --- /dev/null +++ b/tests/gcode/linuxcnc_g54_rotary_numbered_parameters.ngc @@ -0,0 +1,6 @@ +G21 G90 G17 +G10 L2 P1 X7 Y8 Z9 A1 B2 C3 U4 V5 W6 R20 +O10 if [[#5220 EQ 1] AND [#5221 EQ 7] AND [#5222 EQ 8] AND [#5223 EQ 9] AND [#5224 EQ 1] AND [#5225 EQ 2] AND [#5226 EQ 3] AND [#5227 EQ 4] AND [#5228 EQ 5] AND [#5229 EQ 6] AND [#5230 EQ 20]] +G1 X1 F100 +O10 endif +M30 diff --git a/tests/gcode/linuxcnc_g59_3_numbered_parameters.ngc b/tests/gcode/linuxcnc_g59_3_numbered_parameters.ngc new file mode 100644 index 0000000..669b1b9 --- /dev/null +++ b/tests/gcode/linuxcnc_g59_3_numbered_parameters.ngc @@ -0,0 +1,7 @@ +G21 G90 G17 +G10 L2 P9 X7 Y8 Z9 R15 +G59.3 +O10 if [[#5220 EQ 9] AND [#5381 EQ 7] AND [#5382 EQ 8] AND [#5383 EQ 9] AND [#5390 EQ 15]] +G1 X1 F100 +O10 endif +M30 diff --git a/tests/gcode/linuxcnc_g59_3_rotary_numbered_parameters.ngc b/tests/gcode/linuxcnc_g59_3_rotary_numbered_parameters.ngc new file mode 100644 index 0000000..ca023ab --- /dev/null +++ b/tests/gcode/linuxcnc_g59_3_rotary_numbered_parameters.ngc @@ -0,0 +1,7 @@ +G21 G90 G17 +G10 L2 P9 A1 B2 C3 U4 V5 W6 R15 +G59.3 +O10 if [[#5384 EQ 1] AND [#5385 EQ 2] AND [#5386 EQ 3] AND [#5387 EQ 4] AND [#5388 EQ 5] AND [#5389 EQ 6] AND [#5390 EQ 15]] +G1 X1 F100 +O10 endif +M30 diff --git a/tests/gcode/linuxcnc_g5x_numbered_parameters.ngc b/tests/gcode/linuxcnc_g5x_numbered_parameters.ngc new file mode 100644 index 0000000..4ea6fad --- /dev/null +++ b/tests/gcode/linuxcnc_g5x_numbered_parameters.ngc @@ -0,0 +1,7 @@ +G21 G90 G17 +G10 L2 P2 X12.5 Y-3 Z4 R30 +G55 +O10 if [[#5220 EQ 2] AND [#5241 EQ 12.5] AND [#5242 EQ -3] AND [#5243 EQ 4] AND [#5250 EQ 30]] +G1 X1 F100 +O10 endif +M30 diff --git a/tests/gcode/linuxcnc_g5x_rotary_numbered_parameters.ngc b/tests/gcode/linuxcnc_g5x_rotary_numbered_parameters.ngc new file mode 100644 index 0000000..98f557f --- /dev/null +++ b/tests/gcode/linuxcnc_g5x_rotary_numbered_parameters.ngc @@ -0,0 +1,7 @@ +G21 G90 G17 +G10 L2 P2 A1 B2 C3 U4 V5 W6 R30 +G55 +O10 if [[#5244 EQ 1] AND [#5245 EQ 2] AND [#5246 EQ 3] AND [#5247 EQ 4] AND [#5248 EQ 5] AND [#5249 EQ 6] AND [#5250 EQ 30]] +G1 X1 F100 +O10 endif +M30 diff --git a/tests/gcode/linuxcnc_g62_extended_plane_error.ngc b/tests/gcode/linuxcnc_g62_extended_plane_error.ngc new file mode 100644 index 0000000..3090aae --- /dev/null +++ b/tests/gcode/linuxcnc_g62_extended_plane_error.ngc @@ -0,0 +1,3 @@ +G21 G90 G17.1 +F100 +G6.2 U1 V1 P3 Q1 diff --git a/tests/gcode/linuxcnc_g76only.ngc b/tests/gcode/linuxcnc_g76only.ngc new file mode 100644 index 0000000..5f5e0fb --- /dev/null +++ b/tests/gcode/linuxcnc_g76only.ngc @@ -0,0 +1,13 @@ +G20 G64 G18 + +T4 M6 +G43 +S800 M3 +G4 P1 +G0 Z.2 X.2 +G76 P.05 Z-.5 I-.075 J.008 K.045 H3 R2.0 Q29.5 E.05 L2 +G0 X.5 +G0 Z0 +M5 + +M2 diff --git a/tests/gcode/linuxcnc_g92_numbered_parameters.ngc b/tests/gcode/linuxcnc_g92_numbered_parameters.ngc new file mode 100644 index 0000000..3284b5c --- /dev/null +++ b/tests/gcode/linuxcnc_g92_numbered_parameters.ngc @@ -0,0 +1,6 @@ +G21 G90 G17 +G92 X1 Y2 Z3 +O10 if [[#5210 EQ 1] AND [#5211 EQ -1] AND [#5212 EQ -2] AND [#5213 EQ -3]] +G1 X1 F100 +O10 endif +M30 diff --git a/tests/gcode/linuxcnc_infinite_expression_error.ngc b/tests/gcode/linuxcnc_infinite_expression_error.ngc new file mode 100644 index 0000000..5541e39 --- /dev/null +++ b/tests/gcode/linuxcnc_infinite_expression_error.ngc @@ -0,0 +1,2 @@ +#1 = EXP[1000] +G1 X#1 F100 diff --git a/tests/gcode/linuxcnc_lowercase_numeric_oword.ngc b/tests/gcode/linuxcnc_lowercase_numeric_oword.ngc new file mode 100644 index 0000000..f54cc43 --- /dev/null +++ b/tests/gcode/linuxcnc_lowercase_numeric_oword.ngc @@ -0,0 +1,12 @@ +o100 sub +o10 if [#1 GT 0] +o10 return [#1 * 2] +o10 endif +o100 endsub [7] +g21 g90 +f100 +o100 call [3] +g1 x#<_value> +o100 call [0] +g1 y#<_value> +m30 diff --git a/tests/gcode/linuxcnc_m66_result_parameter.ngc b/tests/gcode/linuxcnc_m66_result_parameter.ngc new file mode 100644 index 0000000..8e1372e --- /dev/null +++ b/tests/gcode/linuxcnc_m66_result_parameter.ngc @@ -0,0 +1,11 @@ +#5399 = 1 +M66 P0 L0 +O10 if [#5399 EQ 1] +G1 X1 F100 +O10 endif +#5399 = 42.13 +M66 E0 L0 +O20 if [#5399 EQ 42.13] +G1 Y1 +O20 endif +M30 diff --git a/tests/gcode/linuxcnc_m98_mixed_styles_variables.ngc b/tests/gcode/linuxcnc_m98_mixed_styles_variables.ngc new file mode 100644 index 0000000..e95711e --- /dev/null +++ b/tests/gcode/linuxcnc_m98_mixed_styles_variables.ngc @@ -0,0 +1,23 @@ +#1 = 42.01 +#30 = 42.30 +#31 = 42.31 +M98 P1 +G1 X#1 Y#30 Z#31 F100 +#1 = 42.01 +#30 = 42.30 +#31 = 42.31 +O2 call +G1 A#1 B#30 C#31 +M30 + +O1 +#1 = 13.01 +#30 = 13.30 +#31 = 13.31 +M99 + +O2 sub +#1 = 13.01 +#30 = 13.30 +#31 = 13.31 +O2 endsub diff --git a/tests/gcode/linuxcnc_multi_rotary_probe_numbered_parameters.ngc b/tests/gcode/linuxcnc_multi_rotary_probe_numbered_parameters.ngc new file mode 100644 index 0000000..b10ed2a --- /dev/null +++ b/tests/gcode/linuxcnc_multi_rotary_probe_numbered_parameters.ngc @@ -0,0 +1,7 @@ +G21 G90 G17 +F100 +G38.2 A5 B6 C7 U8 V9 W10 +O10 if [[#5064 EQ 5] AND [#5065 EQ 6] AND [#5066 EQ 7] AND [#5067 EQ 8] AND [#5068 EQ 9] AND [#5069 EQ 10] AND [#5070 EQ 1]] +G1 X1 +O10 endif +M30 diff --git a/tests/gcode/linuxcnc_negative_sqrt_error.ngc b/tests/gcode/linuxcnc_negative_sqrt_error.ngc new file mode 100644 index 0000000..903bdc8 --- /dev/null +++ b/tests/gcode/linuxcnc_negative_sqrt_error.ngc @@ -0,0 +1,2 @@ +#1 = SQRT[-1] +G1 X#1 F100 diff --git a/tests/gcode/linuxcnc_nested_subroutine_definition_error.ngc b/tests/gcode/linuxcnc_nested_subroutine_definition_error.ngc new file mode 100644 index 0000000..a432adb --- /dev/null +++ b/tests/gcode/linuxcnc_nested_subroutine_definition_error.ngc @@ -0,0 +1,5 @@ +O sub +O100 sub +O100 endsub +O endsub +M2 diff --git a/tests/gcode/linuxcnc_numbered_parameters.ngc b/tests/gcode/linuxcnc_numbered_parameters.ngc new file mode 100644 index 0000000..6d99de8 --- /dev/null +++ b/tests/gcode/linuxcnc_numbered_parameters.ngc @@ -0,0 +1,12 @@ +G21 G90 G17 +T1 M6 +G43 H1 +F100 +O10 if [[#5400 EQ 0] AND [#5403 EQ 12.5] AND [#5410 EQ 0] AND [#5411 EQ 0] AND [#5412 EQ 0] AND [#5413 EQ 0] AND [#5422 EQ -12.5]] +G1 X1 +O10 endif +G38.2 X5 Y0 Z-1 +O20 if [[#5061 EQ 5] AND [#5062 EQ 0] AND [#5063 EQ -1] AND [#5064 EQ 0] AND [#5065 EQ 0] AND [#5066 EQ 0] AND [#5067 EQ 0] AND [#5068 EQ 0] AND [#5069 EQ 0] AND [#5070 EQ 1]] +G1 Y1 +O20 endif +M30 diff --git a/tests/gcode/linuxcnc_numeric_oword_main_eof_error.ngc b/tests/gcode/linuxcnc_numeric_oword_main_eof_error.ngc new file mode 100644 index 0000000..a4e0eca --- /dev/null +++ b/tests/gcode/linuxcnc_numeric_oword_main_eof_error.ngc @@ -0,0 +1,7 @@ +O2 sub +G0 Z3 +O2 endsub + +O1 +G0 X1 Y2 +O2 call diff --git a/tests/gcode/linuxcnc_numeric_oword_sub_illegal_location.ngc b/tests/gcode/linuxcnc_numeric_oword_sub_illegal_location.ngc new file mode 100644 index 0000000..5e7d990 --- /dev/null +++ b/tests/gcode/linuxcnc_numeric_oword_sub_illegal_location.ngc @@ -0,0 +1,7 @@ +O1 +G0 X1 Y2 +O2 call + +O2 sub +G0 Z3 +O2 endsub diff --git a/tests/gcode/linuxcnc_parameter_expression_assignment.ngc b/tests/gcode/linuxcnc_parameter_expression_assignment.ngc new file mode 100644 index 0000000..1b3aaa0 --- /dev/null +++ b/tests/gcode/linuxcnc_parameter_expression_assignment.ngc @@ -0,0 +1,16 @@ +G20 +# = 2 +#atan[1]/[1] = 3 +#3 = 4 +G0 X# Y#45 Z#3 +#1 = 5 +#6 = [#ABS[-1] + 2] +G0 A#6 +G0 B#ABS[-1] +#2 = -1 +G0 C#-#2 +#-#2 = 6 +G0 U#1 +G0 V# ABS[-1] +G 0 W 3 +M2 diff --git a/tests/gcode/linuxcnc_persistent_parameters_check.ngc b/tests/gcode/linuxcnc_persistent_parameters_check.ngc new file mode 100644 index 0000000..3519e3b --- /dev/null +++ b/tests/gcode/linuxcnc_persistent_parameters_check.ngc @@ -0,0 +1,8 @@ +G21 G90 G17 +F100 +O10 if [[#5161 EQ 1] AND [#5162 EQ 2] AND [#5163 EQ 3] AND [#5210 EQ 1] AND [#5211 LT -12.1] AND [#5211 GT -12.3] AND [#5212 LT -7.1] AND [#5212 GT -7.3] AND [#5213 EQ -12] AND [#5220 EQ 1]] +O11 if [[#5241 EQ 7] AND [#5242 EQ 8] AND [#5243 EQ 9] AND [#5250 EQ 30]] +G1 X1 +O11 endif +O10 endif +M30 diff --git a/tests/gcode/linuxcnc_persistent_parameters_set.ngc b/tests/gcode/linuxcnc_persistent_parameters_set.ngc new file mode 100644 index 0000000..34f8705 --- /dev/null +++ b/tests/gcode/linuxcnc_persistent_parameters_set.ngc @@ -0,0 +1,8 @@ +G21 G90 G17 +#5001 = 23 +G0 X1 Y2 Z3 +G28.1 +G10 L2 P2 X7 Y8 Z9 R30 +G55 +G92 X4 Y5 Z6 +M30 diff --git a/tests/gcode/linuxcnc_readonly_named_parameter_assign_error.ngc b/tests/gcode/linuxcnc_readonly_named_parameter_assign_error.ngc new file mode 100644 index 0000000..feadf26 --- /dev/null +++ b/tests/gcode/linuxcnc_readonly_named_parameter_assign_error.ngc @@ -0,0 +1 @@ +#<_line> = 7 diff --git a/tests/gcode/linuxcnc_readonly_named_parameters_extra.ngc b/tests/gcode/linuxcnc_readonly_named_parameters_extra.ngc new file mode 100644 index 0000000..8927181 --- /dev/null +++ b/tests/gcode/linuxcnc_readonly_named_parameters_extra.ngc @@ -0,0 +1,20 @@ +G21 G90 G17 +G97 S1000 +O10 if [#<_spindle_rpm_mode> EQ 1 AND #<_spindle_css_mode> EQ 0 AND #<_ccomp> EQ 400] +G1 X1 F100 +O10 endif +G41.1 D0.25 +O20 if [#<_ccomp> EQ 410] +G1 Y1 +O20 endif +G40 +G42.1 D0.25 +O30 if [#<_ccomp> EQ 420] +G1 X2 +O30 endif +G40 +G96 S120 D2500 +O40 if [#<_spindle_rpm_mode> EQ 0 AND #<_spindle_css_mode> EQ 1] +G1 A1 +O40 endif +M30 diff --git a/tests/gcode/linuxcnc_readonly_numbered_parameter_assign_error.ngc b/tests/gcode/linuxcnc_readonly_numbered_parameter_assign_error.ngc new file mode 100644 index 0000000..ad35129 --- /dev/null +++ b/tests/gcode/linuxcnc_readonly_numbered_parameter_assign_error.ngc @@ -0,0 +1 @@ +#5400 = 1 diff --git a/tests/gcode/linuxcnc_rotary_g28_g30_numbered_parameters.ngc b/tests/gcode/linuxcnc_rotary_g28_g30_numbered_parameters.ngc new file mode 100644 index 0000000..76a8dac --- /dev/null +++ b/tests/gcode/linuxcnc_rotary_g28_g30_numbered_parameters.ngc @@ -0,0 +1,9 @@ +G21 G90 G17 +G0 A1 B2 C3 U4 V5 W6 +G28.1 +G0 A7 B8 C9 U10 V11 W12 +G30.1 +O10 if [[#5164 EQ 1] AND [#5165 EQ 2] AND [#5166 EQ 3] AND [#5167 EQ 4] AND [#5168 EQ 5] AND [#5169 EQ 6] AND [#5184 EQ 7] AND [#5185 EQ 8] AND [#5186 EQ 9] AND [#5187 EQ 10] AND [#5188 EQ 11] AND [#5189 EQ 12]] +G1 X1 F100 +O10 endif +M30 diff --git a/tests/gcode/linuxcnc_rotary_g92_numbered_parameters.ngc b/tests/gcode/linuxcnc_rotary_g92_numbered_parameters.ngc new file mode 100644 index 0000000..d4c30a1 --- /dev/null +++ b/tests/gcode/linuxcnc_rotary_g92_numbered_parameters.ngc @@ -0,0 +1,6 @@ +G21 G90 G17 +G92 A1 B2 C3 U4 V5 W6 +O10 if [[#5214 EQ -1] AND [#5215 EQ -2] AND [#5216 EQ -3] AND [#5217 EQ -4] AND [#5218 EQ -5] AND [#5219 EQ -6]] +G1 X1 F100 +O10 endif +M30 diff --git a/tests/gcode/linuxcnc_rotary_persistent_parameters_check.ngc b/tests/gcode/linuxcnc_rotary_persistent_parameters_check.ngc new file mode 100644 index 0000000..863477d --- /dev/null +++ b/tests/gcode/linuxcnc_rotary_persistent_parameters_check.ngc @@ -0,0 +1,8 @@ +G21 G90 G17 +F100 +O10 if [[#5164 EQ 1] AND [#5165 EQ 2] AND [#5166 EQ 3] AND [#5167 EQ 4] AND [#5168 EQ 5] AND [#5169 EQ 6] AND [#5184 EQ 7] AND [#5185 EQ 8] AND [#5186 EQ 9] AND [#5187 EQ 10] AND [#5188 EQ 11] AND [#5189 EQ 12]] +O11 if [[#5384 EQ 13] AND [#5385 EQ 14] AND [#5386 EQ 15] AND [#5387 EQ 16] AND [#5388 EQ 17] AND [#5389 EQ 18] AND [#5390 EQ 19]] +G1 X1 +O11 endif +O10 endif +M30 diff --git a/tests/gcode/linuxcnc_rotary_persistent_parameters_set.ngc b/tests/gcode/linuxcnc_rotary_persistent_parameters_set.ngc new file mode 100644 index 0000000..68f3230 --- /dev/null +++ b/tests/gcode/linuxcnc_rotary_persistent_parameters_set.ngc @@ -0,0 +1,7 @@ +G21 G90 G17 +G0 A1 B2 C3 U4 V5 W6 +G28.1 +G0 A7 B8 C9 U10 V11 W12 +G30.1 +G10 L2 P9 A13 B14 C15 U16 V17 W18 R19 +M30 diff --git a/tests/gcode/linuxcnc_rotary_probe_numbered_parameters.ngc b/tests/gcode/linuxcnc_rotary_probe_numbered_parameters.ngc new file mode 100644 index 0000000..af54fa6 --- /dev/null +++ b/tests/gcode/linuxcnc_rotary_probe_numbered_parameters.ngc @@ -0,0 +1,7 @@ +G21 G90 G17 +F100 +G38.2 A5 +O10 if [[#5064 EQ 5] AND [#5070 EQ 1]] +G1 X1 +O10 endif +M30 diff --git a/tests/gcode/linuxcnc_rotary_tool_offset_numbered_parameters.ngc b/tests/gcode/linuxcnc_rotary_tool_offset_numbered_parameters.ngc new file mode 100644 index 0000000..a53ddb2 --- /dev/null +++ b/tests/gcode/linuxcnc_rotary_tool_offset_numbered_parameters.ngc @@ -0,0 +1,6 @@ +G21 G90 G17 +G43.1 A1 B2 C3 U4 V5 W6 +O10 if [[#5404 EQ 1] AND [#5405 EQ 2] AND [#5406 EQ 3] AND [#5407 EQ 4] AND [#5408 EQ 5] AND [#5409 EQ 6] AND [#5423 EQ -1] AND [#5424 EQ -2] AND [#5425 EQ -3] AND [#5426 EQ -4] AND [#5427 EQ -5] AND [#5428 EQ -6]] +G1 X1 F100 +O10 endif +M30 diff --git a/tests/gcode/linuxcnc_spindle_tool_info_parameter.ngc b/tests/gcode/linuxcnc_spindle_tool_info_parameter.ngc new file mode 100644 index 0000000..ff13cd3 --- /dev/null +++ b/tests/gcode/linuxcnc_spindle_tool_info_parameter.ngc @@ -0,0 +1,6 @@ +G21 G90 G17 +F100 +O10 if [[#5400 EQ 1] AND [#5410 EQ 6] AND [#5411 EQ 12] AND [#5412 EQ 34] AND [#5413 EQ 5]] +G1 X1 +O10 endif +M30 diff --git a/tests/gcode/linuxcnc_tool_info_zero_parameter.ngc b/tests/gcode/linuxcnc_tool_info_zero_parameter.ngc new file mode 100644 index 0000000..25acf79 --- /dev/null +++ b/tests/gcode/linuxcnc_tool_info_zero_parameter.ngc @@ -0,0 +1,7 @@ +G21 G90 G17 +M61 Q1 +F100 +O10 if [[#5400 EQ 0] AND [#5410 EQ 0] AND [#5411 EQ 0] AND [#5412 EQ 0] AND [#5413 EQ 0]] +G1 X1 +O10 endif +M30 diff --git a/tests/gcode/linuxcnc_tool_offset_numbered_parameters.ngc b/tests/gcode/linuxcnc_tool_offset_numbered_parameters.ngc new file mode 100644 index 0000000..42b2932 --- /dev/null +++ b/tests/gcode/linuxcnc_tool_offset_numbered_parameters.ngc @@ -0,0 +1,6 @@ +G21 G90 G17 +G43.1 X1 Y0.5 Z2 +O10 if [[#5401 EQ 1] AND [#5402 EQ 0.5] AND [#5403 EQ 2] AND [#5420 EQ -1] AND [#5421 EQ -0.5] AND [#5422 EQ -2]] +G1 Y1 F100 +O10 endif +M30 diff --git a/tests/gcode/linuxcnc_user_persistent_parameter_boundary_check.ngc b/tests/gcode/linuxcnc_user_persistent_parameter_boundary_check.ngc new file mode 100644 index 0000000..8cfd8cf --- /dev/null +++ b/tests/gcode/linuxcnc_user_persistent_parameter_boundary_check.ngc @@ -0,0 +1,5 @@ +F100 +O10 if [[#5000 EQ 0] AND [#5001 EQ 22] AND [#5030 EQ 32] AND [#5060 EQ 33]] +G1 X1 +O10 endif +M30 diff --git a/tests/gcode/linuxcnc_user_persistent_parameter_boundary_set.ngc b/tests/gcode/linuxcnc_user_persistent_parameter_boundary_set.ngc new file mode 100644 index 0000000..c54bd75 --- /dev/null +++ b/tests/gcode/linuxcnc_user_persistent_parameter_boundary_set.ngc @@ -0,0 +1,5 @@ +#5000 = 11 +#5001 = 22 +#5030 = 32 +#5060 = 33 +M30 diff --git a/tests/gcode/linuxcnc_user_persistent_parameter_check.ngc b/tests/gcode/linuxcnc_user_persistent_parameter_check.ngc new file mode 100644 index 0000000..735ec4e --- /dev/null +++ b/tests/gcode/linuxcnc_user_persistent_parameter_check.ngc @@ -0,0 +1,5 @@ +F100 +O10 if [#5001 EQ 0] +G1 X1 +O10 endif +M30 diff --git a/tests/gcode/linuxcnc_user_persistent_parameter_existing_check.ngc b/tests/gcode/linuxcnc_user_persistent_parameter_existing_check.ngc new file mode 100644 index 0000000..5076ba7 --- /dev/null +++ b/tests/gcode/linuxcnc_user_persistent_parameter_existing_check.ngc @@ -0,0 +1,5 @@ +F100 +O10 if [#5001 EQ 23] +G1 X1 +O10 endif +M30 diff --git a/tests/gcode/linuxcnc_user_persistent_parameter_set.ngc b/tests/gcode/linuxcnc_user_persistent_parameter_set.ngc new file mode 100644 index 0000000..8fc274b --- /dev/null +++ b/tests/gcode/linuxcnc_user_persistent_parameter_set.ngc @@ -0,0 +1,2 @@ +#5001 = 23 +M30 diff --git a/tests/gcode/linuxcnc_value_returned.ngc b/tests/gcode/linuxcnc_value_returned.ngc new file mode 100644 index 0000000..dba253f --- /dev/null +++ b/tests/gcode/linuxcnc_value_returned.ngc @@ -0,0 +1,25 @@ +O sub +O endsub [4711] +O sub +O100 return [4712] +O endsub +O sub +O endsub +G21 G90 +F100 +O10 if [#<_value> EQ 0 AND #<_value_returned> EQ 0] +G1 X1 +O10 endif +O call +O20 if [#<_value> EQ 4711 AND #<_value_returned> EQ 1] +G1 Y1 +O20 endif +O call +O30 if [#<_value> EQ 0 AND #<_value_returned> EQ 0] +G1 Z1 +O30 endif +O call +O40 if [#<_value> EQ 4712 AND #<_value_returned> EQ 1] +G1 A1 +O40 endif +M30 diff --git a/tests/gcode/linuxcnc_volatile_global_parameter_check.ngc b/tests/gcode/linuxcnc_volatile_global_parameter_check.ngc new file mode 100644 index 0000000..f86ee39 --- /dev/null +++ b/tests/gcode/linuxcnc_volatile_global_parameter_check.ngc @@ -0,0 +1,5 @@ +F100 +O10 if [#31 EQ 0] +G1 X1 +O10 endif +M30 diff --git a/tests/gcode/linuxcnc_volatile_global_parameter_set.ngc b/tests/gcode/linuxcnc_volatile_global_parameter_set.ngc new file mode 100644 index 0000000..8f03e74 --- /dev/null +++ b/tests/gcode/linuxcnc_volatile_global_parameter_set.ngc @@ -0,0 +1,2 @@ +#31 = 7 +M30 diff --git a/tests/gcode/linuxcnc_word_bracket_whitespace.ngc b/tests/gcode/linuxcnc_word_bracket_whitespace.ngc new file mode 100644 index 0000000..c621e49 --- /dev/null +++ b/tests/gcode/linuxcnc_word_bracket_whitespace.ngc @@ -0,0 +1,6 @@ +G21 G90 +F100 +G1 X [1 + 2] +G1 Y + [1 + 2] +G1 X- [1 + 2] +M2