diff --git a/core/src/cnc_sim_api.cpp b/core/src/cnc_sim_api.cpp index 3d707dd..afbfdc0 100644 --- a/core/src/cnc_sim_api.cpp +++ b/core/src/cnc_sim_api.cpp @@ -11,6 +11,7 @@ struct CncSimHandle { CncSimDialect dialect = CNC_SIM_DIALECT_LINUXCNC; GcodeBackendKind backend = GcodeBackendKind::LinuxCncRs274; + GcodeBackendOptions backend_options; std::string last_error; CanonEventSink sink; }; @@ -96,6 +97,72 @@ bool contains_string_value(const std::string &compact, const char *key, const ch return false; } +bool find_json_string_value(const std::string &json, const char *key, std::string *value) { + const std::string needle = std::string("\"") + key + "\""; + size_t pos = 0; + while ((pos = json.find(needle, pos)) != std::string::npos) { + pos += needle.size(); + while (pos < json.size() && std::isspace(static_cast(json[pos]))) { + ++pos; + } + if (pos >= json.size() || json[pos] != ':') { + continue; + } + ++pos; + while (pos < json.size() && std::isspace(static_cast(json[pos]))) { + ++pos; + } + if (pos >= json.size() || json[pos] != '"') { + continue; + } + ++pos; + std::string parsed; + bool escaped = false; + for (; pos < json.size(); ++pos) { + const char ch = json[pos]; + if (escaped) { + switch (ch) { + case '"': + case '\\': + case '/': + parsed.push_back(ch); + break; + case 'n': + parsed.push_back('\n'); + break; + case 'r': + parsed.push_back('\r'); + break; + case 't': + parsed.push_back('\t'); + break; + default: + parsed.push_back(ch); + break; + } + escaped = false; + } else if (ch == '\\') { + escaped = true; + } else if (ch == '"') { + *value = parsed; + return true; + } else { + parsed.push_back(ch); + } + } + } + return false; +} + +bool find_json_string_any(const std::string &json, std::string *value, const char *first) { + return find_json_string_value(json, first, value); +} + +template +bool find_json_string_any(const std::string &json, std::string *value, const char *first, Keys... rest) { + return find_json_string_value(json, first, value) || find_json_string_any(json, value, rest...); +} + bool contains_switchkins_config_alias(const std::string &compact, const char *alias) { return contains_string_value(compact, "switchkins", alias) || contains_string_value(compact, "remap", alias) || @@ -300,6 +367,20 @@ int apply_config(CncSimHandle *handle, const std::string &json) { return -1; } + std::string ini_file_name; + if (find_json_string_any(json, + &ini_file_name, + "ini", + "iniFile", + "inifile", + "iniFileName", + "inifilename", + "ini_file", + "ini_file_name", + "INI_FILE_NAME")) { + handle->backend_options.ini_file_name = ini_file_name; + } + bool rtcp_enabled = false; double tool_length = 0.0; const bool has_rtcp_enabled = contains_bool_value(compact, "rtcp", &rtcp_enabled) || @@ -383,7 +464,12 @@ int cnc_sim_parse_program(CncSimHandle *handle, const char *program, size_t prog } handle->last_error.clear(); - const int rc = parse_gcode_with_backend(handle->backend, handle->sink, program, program_len, &handle->last_error); + const int rc = parse_gcode_with_backend(handle->backend, + handle->sink, + program, + program_len, + handle->backend_options, + &handle->last_error); if (rc != 0 && handle->last_error.empty()) { handle->last_error = "program parse failed"; } diff --git a/core/src/gcode_backend.cpp b/core/src/gcode_backend.cpp index 9b0b2c0..bbd4276 100644 --- a/core/src/gcode_backend.cpp +++ b/core/src/gcode_backend.cpp @@ -22,6 +22,7 @@ int parse_gcode_with_backend(GcodeBackendKind backend, CanonEventSink &sink, const char *program, size_t program_len, + const GcodeBackendOptions &options, std::string *error) { switch (backend) { case GcodeBackendKind::Smoke: { @@ -37,7 +38,7 @@ int parse_gcode_with_backend(GcodeBackendKind backend, } case GcodeBackendKind::LinuxCncRs274: #ifdef CNC_SIM_ENABLE_LINUXCNC_RS274_BACKEND - return parse_linuxcnc_rs274_backend(sink, program, program_len, error); + return parse_linuxcnc_rs274_backend(sink, program, program_len, options, error); #else if (error) { *error = "linuxcnc-rs274 backend is not compiled into this build"; diff --git a/core/src/gcode_backend.h b/core/src/gcode_backend.h index 09d2dd9..ca03c27 100644 --- a/core/src/gcode_backend.h +++ b/core/src/gcode_backend.h @@ -10,11 +10,15 @@ enum class GcodeBackendKind { LinuxCncRs274, }; +struct GcodeBackendOptions { + std::string ini_file_name; +}; + const char *gcode_backend_name(GcodeBackendKind backend); int parse_gcode_with_backend(GcodeBackendKind backend, CanonEventSink &sink, const char *program, size_t program_len, + const GcodeBackendOptions &options, std::string *error); - diff --git a/core/src/linuxcnc_rs274_backend.cpp b/core/src/linuxcnc_rs274_backend.cpp index 47fd35c..4a7769c 100644 --- a/core/src/linuxcnc_rs274_backend.cpp +++ b/core/src/linuxcnc_rs274_backend.cpp @@ -175,6 +175,7 @@ int execute_file_mode(InterpBase *interp, int parse_linuxcnc_rs274_backend(CanonEventSink &sink, const char *program, size_t program_len, + const GcodeBackendOptions &options, std::string *error) { if (!program && program_len != 0) { if (error) { @@ -197,6 +198,22 @@ int parse_linuxcnc_rs274_backend(CanonEventSink &sink, cnc_sim_linuxcnc_set_canon_sink(&sink); InterpBase *interp = makeInterp(); + if (!options.ini_file_name.empty()) { + // LinuxCNC source basis: emctask.cc calls Interp::ini_load() before + // Interp::init(), and rs274ngc_pre.cc ini_load() maps + // [RS274NGC] PARAMETER_FILE through SET_PARAMETER_FILE_NAME(). + if (interp->ini_load(options.ini_file_name.c_str()) != INTERP_OK) { + if (error) { + *error = "ini_load failed: " + options.ini_file_name; + } + delete interp; + cnc_sim_linuxcnc_set_canon_sink(nullptr); + return -1; + } + if (const char *parameter_file = std::getenv("CNC_SIM_RS274_VAR")) { + SET_PARAMETER_FILE_NAME(parameter_file); + } + } int status = interp->init(); if (status != INTERP_OK) { if (error) { diff --git a/core/src/linuxcnc_rs274_backend.h b/core/src/linuxcnc_rs274_backend.h index 2951dd3..9059d65 100644 --- a/core/src/linuxcnc_rs274_backend.h +++ b/core/src/linuxcnc_rs274_backend.h @@ -1,6 +1,7 @@ #pragma once #include "canon_event_sink.h" +#include "gcode_backend.h" #include #include @@ -8,5 +9,5 @@ int parse_linuxcnc_rs274_backend(CanonEventSink &sink, const char *program, size_t program_len, + const GcodeBackendOptions &options, std::string *error); - diff --git a/core/tests/cnc_sim_api_linuxcnc_rs274_smoke.cpp b/core/tests/cnc_sim_api_linuxcnc_rs274_smoke.cpp index 4d7a801..c33733f 100644 --- a/core/tests/cnc_sim_api_linuxcnc_rs274_smoke.cpp +++ b/core/tests/cnc_sim_api_linuxcnc_rs274_smoke.cpp @@ -2,8 +2,11 @@ #include #include +#include +#include #include #include +#include #include namespace { @@ -61,6 +64,34 @@ private: std::string old_; }; +class ScopedTempDir { +public: + explicit ScopedTempDir(const char *prefix) { + const char *tmpdir = std::getenv("TMPDIR"); + std::string tmpl = std::string(tmpdir ? tmpdir : "/tmp") + "/" + prefix + ".XXXXXX"; + std::vector buffer(tmpl.begin(), tmpl.end()); + buffer.push_back('\0'); + char *dir = mkdtemp(buffer.data()); + if (!dir) { + std::perror("mkdtemp"); + std::exit(1); + } + path_ = dir; + } + + ~ScopedTempDir() { + std::error_code error; + std::filesystem::remove_all(path_, error); + } + + std::string file(const char *name) const { + return path_ + "/" + name; + } + +private: + std::string path_; +}; + bool saw_comment_state(const std::vector &events, int line, int reserved, @@ -168,6 +199,8 @@ bool expect_switchkins_remap_config(const std::string &json, } // namespace int main() { + ScopedTempDir temp_dir("cnc_sim_api_linuxcnc_rs274_smoke"); + { std::vector default_events; CncSimHandle *default_sim = cnc_sim_create(); @@ -196,6 +229,55 @@ int main() { } } + { + const std::string ini_parameter_var_path = temp_dir.file("ini_parameter.var"); + { + std::ofstream var_file(ini_parameter_var_path); + var_file << "5001\t44.000000\n"; + } + const std::string rs274_ini_path = temp_dir.file("rs274_parameter.ini"); + { + std::ofstream ini_file(rs274_ini_path); + ini_file << "[RS274NGC]\n"; + ini_file << "PARAMETER_FILE = " << ini_parameter_var_path << "\n"; + } + std::vector ini_events; + CncSimHandle *ini_sim = cnc_sim_create(); + cnc_sim_set_event_callback(ini_sim, collect_event, &ini_events); + const std::string ini_config = + std::string("{\"backend\":\"linuxcnc-rs274\",\"iniFileName\":\"") + + rs274_ini_path + + "\"}"; + bool ini_ok = true; + ini_ok &= expect(cnc_sim_load_config_json(ini_sim, ini_config.c_str(), ini_config.size()) == 0, + cnc_sim_last_error(ini_sim)); + ScopedEnv file_mode_env("CNC_SIM_RS274_FILE_MODE", "1"); + const char ini_parameter_check_program[] = + "G21 G90 G17\n" + "F100\n" + "G1 X#5001\n"; + ini_ok &= expect(cnc_sim_parse_program(ini_sim, + ini_parameter_check_program, + sizeof(ini_parameter_check_program) - 1) == 0, + cnc_sim_last_error(ini_sim)); + bool saw_ini_parameter_restore = false; + for (const auto &event : ini_events) { + saw_ini_parameter_restore = saw_ini_parameter_restore || + (event.type == CNC_SIM_EVENT_LINEAR_FEED && + event.line == 3 && + near(event.end.x, 44.0)); + } + // LinuxCNC source basis: emctask.cc calls interp.ini_load(), and + // rs274ngc_pre.cc ini_load() maps [RS274NGC] PARAMETER_FILE through + // SET_PARAMETER_FILE_NAME before rs274_ngc_init() restores parameters. + ini_ok &= expect(saw_ini_parameter_restore, + "expected LinuxCNC INI PARAMETER_FILE to restore numbered parameters"); + cnc_sim_destroy(ini_sim); + if (!ini_ok) { + return 1; + } + } + const char config[] = "{\"backend\":\"linuxcnc-rs274\",\"rtcp\":{\"enabled\":true,\"toolLength\":100,\"toolLengths\":{\"7\":125}}}"; const char program[] = diff --git a/test-native.sh b/test-native.sh index 8f2892d..babbf77 100755 --- a/test-native.sh +++ b/test-native.sh @@ -81,6 +81,8 @@ grep -F 'metadata:src/emc/sai/dummyemcstat.cc:source basis for wasm-safe emcStat grep -F 'blocked:src/emc/rs274ngc/gcodemodule.cc:Python gcode module binding, tracked for full rs274ngc source coverage' linuxcnc-rs274-wasm-source-files.txt >/dev/null grep -F 'expected LinuxCNC parameter env override not to leak into default parses' core/tests/cnc_sim_api_smoke.cpp >/dev/null grep -F 'SET_PARAMETER_FILE_NAME("rs274ngc.var")' core/src/linuxcnc_rs274_backend.cpp >/dev/null +grep -F 'expected LinuxCNC INI PARAMETER_FILE to restore numbered parameters' core/tests/cnc_sim_api_linuxcnc_rs274_smoke.cpp >/dev/null +grep -F 'interp->ini_load(options.ini_file_name.c_str())' core/src/linuxcnc_rs274_backend.cpp >/dev/null grep -F 'metadata:src/emc/nml_intf/emcops.cc:source basis for native source-link EMC status constructor support object' linuxcnc-rs274-source-files.txt >/dev/null grep -F 'metadata:src/emc/sai/dummyemcstat.cc:source basis for native source-link emcStatus singleton support object' linuxcnc-rs274-source-files.txt >/dev/null grep -F 'metadata:src/emc/sai/Submakefile:LinuxCNC SAI build metadata defining dummyemcstat support object' linuxcnc-rs274-source-files.txt >/dev/null