按规划持续工作

This commit is contained in:
2026-06-09 06:13:28 +08:00
parent 902c6ecaea
commit 2658ee3b72
255 changed files with 12487 additions and 150 deletions

View File

@@ -362,6 +362,12 @@ int main()
"xyzac_switchkins_test_1.ngc",
0.0,
},
{
"xyzac_switchkins_test_2",
"configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/"
"xyzac_switchkins_test_2.ngc",
0.0,
},
{
"xyzac_switchkins_test_3",
"configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/"
@@ -404,10 +410,11 @@ int main()
ok &= execute_demo_file(cases[0], demos[2]);
ok &= execute_demo_file(cases[0], demos[3]);
ok &= execute_demo_file(cases[0], demos[4]);
ok &= execute_demo_file(cases[1], demos[5]);
ok &= execute_demo_file(cases[0], demos[6]);
ok &= execute_demo_file(cases[2], demos[7]);
ok &= execute_demo_file(cases[3], demos[8]);
ok &= execute_demo_file(cases[0], demos[5]);
ok &= execute_demo_file(cases[1], demos[6]);
ok &= execute_demo_file(cases[0], demos[7]);
ok &= execute_demo_file(cases[2], demos[8]);
ok &= execute_demo_file(cases[3], demos[9]);
print_bool("fiveaxis_linuxcnc_remap_execute_path", ok);
return ok ? 0 : 1;

View File

@@ -0,0 +1,363 @@
#define private public
#include "emc/rs274ngc/rs274ngc_interp.hh"
#undef private
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <iostream>
#include <sstream>
#include <string>
#include "canon_event_sink.hh"
#include "emc/ini/inifile.hh"
#include "emc/rs274ngc/interp_internal.hh"
#include "linuxcnc_hal_adapter.hh"
#include "linuxcnc_tool_adapter.hh"
#ifndef LINUXCNC_VENDOR_DIR
#error "LINUXCNC_VENDOR_DIR must point at wasm-port/vendor/linuxcnc"
#endif
namespace {
std::filesystem::path vendor_path(const std::string &rel)
{
return std::filesystem::path(LINUXCNC_VENDOR_DIR) / rel;
}
void print_bool(const std::string &name, bool value)
{
std::cout << name << "=" << (value ? 1 : 0) << "\n";
}
void print_error_text(Interp &interp, const std::string &name, int rc)
{
char error_buf[LINELEN] = {0};
interp.error_text(rc, error_buf, sizeof(error_buf));
std::cout << name << error_buf << "\n";
}
void initialize_interp(Interp &interp)
{
standalone::reset_hal_adapter();
standalone::reset_tool_adapter();
standalone::reset_canon_events();
interp._setup.length_units = CANON_UNITS_MM;
interp._setup.distance_mode = DISTANCE_MODE::ABSOLUTE;
interp._setup.ijk_distance_mode = DISTANCE_MODE::ABSOLUTE;
interp._setup.feed_mode = FEED_MODE::UNITS_PER_MINUTE;
interp._setup.plane = CANON_PLANE::XY;
interp._setup.motion_mode = G_0;
interp._setup.percent_flag = false;
interp._setup.sequence_number = 0;
interp._setup.parameter_occurrence = 0;
interp._setup.num_spindles = 1;
interp._setup.feature_set = FEATURE_INI_VARS | FEATURE_HAL_PIN_VARS;
interp._setup.parameter_g73_peck_clearance = 1.0;
interp._setup.parameter_g83_peck_clearance = 1.0;
interp._setup.parameters[5599] = 1.0;
std::memcpy(interp._readers, Interp::default_readers, sizeof(Interp::default_readers));
interp.init_named_parameters();
}
std::filesystem::path subroutine_path(const std::filesystem::path &machine_dir,
const std::string &entry)
{
const std::filesystem::path path(entry);
if (path.is_absolute()) {
return path;
}
return machine_dir / path;
}
bool configure_remaps(Interp &interp,
const std::filesystem::path &ini_path,
const char *label,
int expected_remaps)
{
linuxcnc::IniFile ini(ini_path.string());
const bool opened = static_cast<bool>(ini);
print_bool(std::string(label) + "_ini_open", opened);
if (!opened) {
return false;
}
const auto machine_dir = ini_path.parent_path();
std::snprintf(interp._setup.program_prefix, sizeof(interp._setup.program_prefix), "%s",
machine_dir.c_str());
int subroutine_count = 0;
if (auto subroutine_path_text = ini.findString("SUBROUTINE_PATH", "RS274NGC")) {
std::stringstream paths(*subroutine_path_text);
std::string entry;
while (std::getline(paths, entry, ':') && subroutine_count < MAX_SUB_DIRS) {
const auto resolved = subroutine_path(machine_dir, entry);
interp._setup.subroutines[subroutine_count++] = strstore(resolved.c_str());
}
}
std::cout << label << "_subroutine_path_count=" << subroutine_count << "\n";
if (ini.findBoolV("OWORD_NARGS", "RS274NGC", false)) {
interp._setup.feature_set |= FEATURE_OWORD_N_ARGS;
}
bool ok = subroutine_count > 0;
int remap_count = 0;
while (auto remap = ini.findString(++remap_count, "REMAP", "RS274NGC")) {
const int line_number = ini.lineOf(remap_count, "REMAP", "RS274NGC").second;
const int rc = interp.parse_remap(remap->c_str(), line_number);
std::cout << label << "_parse_remap_" << remap_count << "=" << rc << "\n";
ok &= rc == INTERP_OK;
}
remap_count -= 1;
std::cout << label << "_parsed_remap_count=" << remap_count << "\n";
ok &= remap_count == expected_remaps;
print_bool(std::string(label) + "_remaps_ready", ok);
return ok;
}
bool execute_file(Interp &interp,
const std::filesystem::path &program_path,
const char *label,
bool continue_on_error,
bool expect_error)
{
bool ok = true;
const int open_rc = interp.open(program_path.c_str());
std::cout << label << "_file_open=" << open_rc << "\n";
if (open_rc != INTERP_OK) {
return false;
}
constexpr int max_file_steps = 20000;
int read_count = 0;
int execute_count = 0;
int finish_count = 0;
int final_rc = INTERP_OK;
bool reached_exit = false;
bool reached_endfile = false;
bool saw_error = false;
while (read_count < max_file_steps && execute_count < max_file_steps) {
const int read_rc = interp.read();
if (read_rc == INTERP_ENDFILE) {
final_rc = read_rc;
reached_endfile = true;
break;
}
++read_count;
if ((read_rc != INTERP_OK) && (read_rc != INTERP_EXECUTE_FINISH)) {
std::cout << label << "_file_read_" << read_count << "=" << read_rc << "\n";
print_error_text(interp, std::string(label) + "_file_error_text=", read_rc);
final_rc = read_rc;
saw_error = true;
if (!expect_error) {
ok = false;
}
if (!continue_on_error) {
break;
}
continue;
}
const int execute_rc = interp.execute();
++execute_count;
final_rc = execute_rc;
if (execute_rc == INTERP_EXECUTE_FINISH) {
++finish_count;
}
if ((execute_rc != INTERP_OK) &&
(execute_rc != INTERP_EXECUTE_FINISH) &&
(execute_rc != INTERP_EXIT)) {
std::cout << label << "_file_execute_" << execute_count << "=" << execute_rc << "\n";
print_error_text(interp, std::string(label) + "_file_error_text=", execute_rc);
saw_error = true;
if (!expect_error) {
ok = false;
}
if (!continue_on_error) {
break;
}
continue;
}
if (execute_rc == INTERP_EXIT) {
reached_exit = true;
break;
}
}
std::cout << label << "_file_read_count=" << read_count << "\n";
std::cout << label << "_file_execute_count=" << execute_count << "\n";
std::cout << label << "_file_finish_count=" << finish_count << "\n";
std::cout << label << "_file_final_rc=" << final_rc << "\n";
print_bool(std::string(label) + "_file_reached_exit", reached_exit);
print_bool(std::string(label) + "_file_reached_endfile", reached_endfile);
print_bool(std::string(label) + "_file_saw_error", saw_error);
std::cout << label << "_canon_event_count=" << standalone::canon_events().size() << "\n";
for (const auto &event : standalone::canon_events()) {
std::cout << label << "_canon_event=" << event << "\n";
}
ok &= read_count > 0;
ok &= execute_count > 0;
ok &= read_count < max_file_steps;
ok &= execute_count < max_file_steps;
ok &= reached_exit || reached_endfile;
ok &= expect_error ? saw_error : !saw_error;
print_bool(std::string(label) + "_linuxcnc_remap_file_execute", ok);
return ok;
}
std::string command_label(const char *command)
{
std::string value = command ? command : "";
while (!value.empty() && (value.back() == '\n' || value.back() == '\r')) {
value.pop_back();
}
return value;
}
bool execute_mdi(Interp &interp, const char *label, const char *command)
{
int rc = interp.execute(command);
std::cout << label << "_mdi_" << command_label(command) << "_execute_0=" << rc << "\n";
int finish_count = 0;
while (rc == INTERP_EXECUTE_FINISH && finish_count < 16) {
++finish_count;
rc = interp.execute();
std::cout << label << "_mdi_" << command_label(command)
<< "_execute_" << finish_count << "=" << rc << "\n";
}
std::cout << label << "_mdi_" << command_label(command)
<< "_execute_finish_count=" << finish_count << "\n";
return rc == INTERP_OK;
}
bool execute_remap_io_mdi_case()
{
const auto test_dir = vendor_path("tests/remap/remap-io");
const auto ini_path = test_dir / "test-ngc.ini";
setenv("INI_FILE_NAME", ini_path.c_str(), 1);
Interp interp;
initialize_interp(interp);
bool ok = configure_remaps(interp, ini_path, "remap_io_ngc", 7);
const struct MdiStep {
const char *command;
bool digital_input;
int digital_value;
bool analog_input;
double analog_value;
} steps[] = {
{"M62 P1\n", false, 0, false, 0.0},
{"G0 X1.0\n", false, 0, false, 0.0},
{"M63 P1\n", false, 0, false, 0.0},
{"G0 X2.0\n", false, 0, false, 0.0},
{"M62 P1\n", false, 0, false, 0.0},
{"G0 X3.0\n", false, 0, false, 0.0},
{"M63 P1\n", false, 0, false, 0.0},
{"G0 X4.0\n", false, 0, false, 0.0},
{"M64 P1\n", false, 0, false, 0.0},
{"M65 P1\n", false, 0, false, 0.0},
{"M64 P1\n", false, 0, false, 0.0},
{"M65 P1\n", false, 0, false, 0.0},
{"M66 P1\n", true, 1, false, 0.0},
{"M66 P1\n", true, 0, false, 0.0},
{"M66 E1 L0\n", false, 0, true, 42.13},
{"M66 E1 L0\n", false, 0, true, -13.42},
{"M67 E1 Q42.13\n", false, 0, false, 0.0},
{"G0 X-1.0\n", false, 0, false, 0.0},
{"M67 E1 Q-13.42\n", false, 0, false, 0.0},
{"G0 X-2.0\n", false, 0, false, 0.0},
{"M67 E1 Q0.0\n", false, 0, false, 0.0},
{"G0 X-3.0\n", false, 0, false, 0.0},
{"M68 E1 Q42.13\n", false, 0, false, 0.0},
{"M68 E1 Q-13.42\n", false, 0, false, 0.0},
{"M68 E1 Q0.0\n", false, 0, false, 0.0},
};
for (const auto &step : steps) {
if (step.digital_input) {
standalone::set_external_digital_input(0, step.digital_value);
}
if (step.analog_input) {
standalone::set_external_analog_input(0, step.analog_value);
}
ok &= execute_mdi(interp, "remap_io_ngc", step.command);
std::cout << "remap_io_ngc_parameter_5399=" << interp._setup.parameters[5399] << "\n";
std::cout << "remap_io_ngc_parameter_100=" << interp._setup.parameters[100] << "\n";
}
std::cout << "remap_io_ngc_canon_event_count=" << standalone::canon_events().size() << "\n";
for (const auto &event : standalone::canon_events()) {
std::cout << "remap_io_ngc_canon_event=" << event << "\n";
}
ok &= std::fabs(interp._setup.parameters[5399] - -13.42) < 1e-9;
ok &= std::fabs(interp._setup.parameters[100] - -13.42) < 1e-9;
print_bool("remap_io_ngc_linuxcnc_mdi_sequence", ok);
unsetenv("INI_FILE_NAME");
return ok;
}
} // namespace
int main()
{
struct RegressionCase {
const char *label;
const char *rel;
int expected_remaps;
bool continue_on_error;
bool expect_error;
};
const RegressionCase cases[] = {
{"duplicate_oword", "tests/remap/duplicate-o-word", 2, false, false},
{"fail_args_0", "tests/remap/fail/args.0", 1, false, false},
{"fail_args_1", "tests/remap/fail/args.1", 1, true, true},
{"fail_args_2", "tests/remap/fail/args.2", 1, true, true},
{"fail_body_ngc", "tests/remap/fail/body-ngc", 1, true, true},
{"m30_interaction", "tests/remap/m30-interaction", 1, false, false},
{"nested_remaps_oword", "tests/remap/nested-remaps-oword", 4, false, false},
{"posargs_0", "tests/remap/posargs.0", 1, false, false},
{"sequencing", "tests/remap/sequencing", 7, false, false},
};
bool all_ok = true;
for (const auto &test : cases) {
const auto test_dir = vendor_path(test.rel);
const auto ini_path = test_dir / "test.ini";
const auto program_path = test_dir / "test.ngc";
setenv("INI_FILE_NAME", ini_path.c_str(), 1);
Interp interp;
initialize_interp(interp);
bool ok = configure_remaps(interp, ini_path, test.label, test.expected_remaps);
ok &= execute_file(interp,
program_path,
test.label,
test.continue_on_error,
test.expect_error);
print_bool(std::string(test.label) + "_linuxcnc_regression_path", ok);
all_ok &= ok;
unsetenv("INI_FILE_NAME");
}
all_ok &= execute_remap_io_mdi_case();
print_bool("linuxcnc_upstream_remap_regression_path", all_ok);
return all_ok ? 0 : 1;
}

View File

@@ -59,6 +59,18 @@ std::unordered_map<std::string, std::vector<std::string>> &pin_aliases()
return values;
}
std::unordered_map<int, int> &digital_inputs()
{
static std::unordered_map<int, int> values;
return values;
}
std::unordered_map<int, double> &analog_inputs()
{
static std::unordered_map<int, double> values;
return values;
}
int lookup_hal_value(const char *name, std::unordered_map<std::string, HalEntry> &values,
hal_type_t *type, hal_data_u **ptr, bool *connected)
{
@@ -89,6 +101,8 @@ void reset_hal_adapter()
signals().clear();
params().clear();
pin_aliases().clear();
digital_inputs().clear();
analog_inputs().clear();
}
void set_hal_value(HalValueKind kind, const char *name, hal_type_t type, hal_data_u value,
@@ -130,6 +144,28 @@ void sync_motion_analog_output(int index, double value)
}
}
void set_external_digital_input(int index, int value)
{
digital_inputs()[index] = value ? 1 : 0;
}
void set_external_analog_input(int index, double value)
{
analog_inputs()[index] = value;
}
int external_digital_input(int index, int def)
{
auto it = digital_inputs().find(index);
return it == digital_inputs().end() ? def : it->second;
}
double external_analog_input(int index, double def)
{
auto it = analog_inputs().find(index);
return it == analog_inputs().end() ? def : it->second;
}
} // namespace standalone
int hal_init(const char *)

View File

@@ -15,5 +15,9 @@ void set_hal_value(HalValueKind kind, const char *name, hal_type_t type, hal_dat
bool connected = true);
void link_hal_pin_alias(const char *source, const char *alias);
void sync_motion_analog_output(int index, double value);
void set_external_digital_input(int index, int value);
void set_external_analog_input(int index, double value);
int external_digital_input(int index, int def);
double external_analog_input(int index, double def);
} // namespace standalone

View File

@@ -3,13 +3,19 @@
#undef private
#include <cstring>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
#include "canon_event_sink.hh"
#include "emc/ini/inifile.hh"
#include "linuxcnc_hal_adapter.hh"
#include "linuxcnc_machine_config.hh"
#include "linuxcnc_runtime_state.hh"
#include "linuxcnc_tool_adapter.hh"
namespace {
@@ -70,6 +76,7 @@ void initialize_minimal_interp(Interp &interp)
{
seed_hal_values();
seed_tool_values();
standalone::reset_external_axis_mask();
interp._setup.length_units = CANON_UNITS_MM;
interp._setup.distance_mode = DISTANCE_MODE::ABSOLUTE;
interp._setup.ijk_distance_mode = DISTANCE_MODE::ABSOLUTE;
@@ -85,7 +92,45 @@ void initialize_minimal_interp(Interp &interp)
interp._setup.parameter_g83_peck_clearance = 1.0;
interp._setup.parameters[5599] = 1.0;
interp.load_tool_table();
std::memcpy(interp._readers, Interp::default_readers, sizeof(Interp::default_readers));
standalone::apply_axis_mask_to_interp(interp, standalone::axis_mask_from_coordinates("XYZABCUVW"));
standalone::reset_user_m_code_registry();
}
void apply_ini_search_paths(Interp &interp, const char *ini_path)
{
if (!ini_path || ini_path[0] == '\0') {
return;
}
linuxcnc::IniFile ini(ini_path);
if (!ini) {
return;
}
const auto machine_dir = std::filesystem::path(ini_path).parent_path();
standalone::apply_machine_axis_config(interp, ini);
standalone::load_tool_table_from_ini(interp, ini, machine_dir.c_str());
standalone::register_user_m_codes_from_ini(ini, machine_dir.c_str());
std::snprintf(interp._setup.program_prefix, sizeof(interp._setup.program_prefix), "%s",
machine_dir.c_str());
int subroutine_count = 0;
if (auto subroutine_path_text = ini.findString("SUBROUTINE_PATH", "RS274NGC")) {
std::stringstream paths(*subroutine_path_text);
std::string entry;
while (std::getline(paths, entry, ':') && subroutine_count < MAX_SUB_DIRS) {
std::filesystem::path resolved(entry);
if (resolved.is_relative()) {
resolved = machine_dir / resolved;
}
interp._setup.subroutines[subroutine_count++] = strstore(resolved.c_str());
}
}
if (ini.findBoolV("OWORD_NARGS", "RS274NGC", false)) {
interp._setup.feature_set |= FEATURE_OWORD_N_ARGS;
}
}
std::vector<std::string> load_program(int argc, char **argv)
@@ -117,11 +162,25 @@ std::vector<std::string> load_program(int argc, char **argv)
int main(int argc, char **argv)
{
bool continue_on_error = false;
int arg_offset = 1;
if (argc > 1 && std::strcmp(argv[1], "--continue-on-error") == 0) {
continue_on_error = true;
arg_offset = 2;
}
Interp interp;
standalone::reset_canon_events();
const char *ini_path = argc > arg_offset + 1 ? argv[arg_offset + 1] : nullptr;
if (ini_path && ini_path[0] != '\0') {
setenv("INI_FILE_NAME", ini_path, 1);
} else {
unsetenv("INI_FILE_NAME");
}
initialize_minimal_interp(interp);
apply_ini_search_paths(interp, ini_path);
const std::vector<std::string> program = load_program(argc, argv);
const std::vector<std::string> program = load_program(argc - arg_offset + 1, argv + arg_offset - 1);
if (program.empty()) {
std::cerr << "empty fixture\n";
return 2;
@@ -138,6 +197,7 @@ int main(int argc, char **argv)
const int read_rc = interp.read(program.front().c_str());
initialize_minimal_interp(interp);
apply_ini_search_paths(interp, ini_path);
for (std::size_t idx = 0; idx < program.size(); ++idx) {
interp._setup.sequence_number = static_cast<int>(idx + 1);
const int execute_rc = interp.execute(program[idx].c_str());
@@ -152,10 +212,13 @@ int main(int argc, char **argv)
int file_open_rc = INTERP_FILE_NOT_OPEN;
int file_read_count = 0;
int file_execute_count = 0;
if (argc > 1) {
bool file_saw_error = false;
if (argc > arg_offset) {
Interp file_interp;
standalone::reset_canon_events();
initialize_minimal_interp(file_interp);
file_open_rc = file_interp.open(argv[1]);
apply_ini_search_paths(file_interp, ini_path);
file_open_rc = file_interp.open(argv[arg_offset]);
if (file_open_rc == INTERP_OK) {
while (true) {
const int file_read_rc = file_interp.read();
@@ -166,7 +229,16 @@ int main(int argc, char **argv)
++file_read_count;
std::cout << "file_read_" << file_read_count << "=" << file_read_rc << "\n";
if ((file_read_rc != INTERP_OK) && (file_read_rc != INTERP_EXECUTE_FINISH)) {
break;
if (file_read_rc > INTERP_MIN_ERROR) {
char error_buf[LINELEN] = {0};
file_interp.error_text(file_read_rc, error_buf, sizeof(error_buf));
std::cout << "file_error_text=" << error_buf << "\n";
}
file_saw_error = true;
if (!continue_on_error) {
break;
}
continue;
}
const int file_execute_rc = file_interp.execute();
@@ -180,6 +252,7 @@ int main(int argc, char **argv)
if ((file_execute_rc != INTERP_OK) &&
(file_execute_rc != INTERP_EXECUTE_FINISH) &&
(file_execute_rc != INTERP_EXIT)) {
file_saw_error = true;
break;
}
if (file_execute_rc == INTERP_EXIT) {
@@ -200,6 +273,7 @@ int main(int argc, char **argv)
std::cout << "file_open=" << file_open_rc << "\n";
std::cout << "file_read_count=" << file_read_count << "\n";
std::cout << "file_execute_count=" << file_execute_count << "\n";
std::cout << "file_saw_error=" << (file_saw_error ? 1 : 0) << "\n";
std::cout << "raw_line=" << raw_line << "\n";
std::cout << "cooked_line=" << cooked_line << "\n";
std::cout << "line_length=" << length << "\n";

View File

@@ -13,6 +13,7 @@
#include "emc/rs274ngc/rs274ngc_return.hh"
#include "canon_event_sink.hh"
#include "linuxcnc_hal_adapter.hh"
#include "linuxcnc_runtime_state.hh"
#include "linuxcnc_tool_adapter.hh"
#include "nml_intf/emc.hh"
@@ -20,6 +21,7 @@ namespace standalone {
static std::vector<std::string> g_canon_events;
static double g_external_feed_rate = 0.0;
static int g_external_axis_mask = Interp::AXIS_MASK_X | Interp::AXIS_MASK_Y | Interp::AXIS_MASK_Z;
void reset_canon_events()
{
@@ -36,6 +38,23 @@ const std::vector<std::string> &canon_events()
return g_canon_events;
}
void reset_external_axis_mask()
{
g_external_axis_mask = Interp::AXIS_MASK_X | Interp::AXIS_MASK_Y | Interp::AXIS_MASK_Z;
}
void set_external_axis_mask(int axis_mask)
{
g_external_axis_mask = axis_mask == 0
? (Interp::AXIS_MASK_X | Interp::AXIS_MASK_Y | Interp::AXIS_MASK_Z)
: axis_mask;
}
int external_axis_mask()
{
return g_external_axis_mask;
}
} // namespace standalone
bool GET_BLOCK_DELETE(void) { return false; }
@@ -523,9 +542,15 @@ int GET_EXTERNAL_FEED_OVERRIDE_ENABLE() { return 1; }
int GET_EXTERNAL_SPINDLE_OVERRIDE_ENABLE(int) { return 1; }
int GET_EXTERNAL_ADAPTIVE_FEED_ENABLE() { return 1; }
int GET_EXTERNAL_FEED_HOLD_ENABLE() { return 1; }
int GET_EXTERNAL_DIGITAL_INPUT(int, int def) { return def; }
double GET_EXTERNAL_ANALOG_INPUT(int, double def) { return def; }
int GET_EXTERNAL_AXIS_MASK() { return 7; }
int GET_EXTERNAL_DIGITAL_INPUT(int index, int def)
{
return standalone::external_digital_input(index, def);
}
double GET_EXTERNAL_ANALOG_INPUT(int index, double def)
{
return standalone::external_analog_input(index, def);
}
int GET_EXTERNAL_AXIS_MASK() { return standalone::external_axis_mask(); }
int GET_EXTERNAL_OFFSET_APPLIED() { return 0; }
EmcPose GET_EXTERNAL_OFFSETS() { return {{0, 0, 0}, 0, 0, 0, 0, 0, 0}; }

View File

@@ -2,6 +2,7 @@
#include "emc/rs274ngc/rs274ngc_interp.hh"
#undef private
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <sstream>
@@ -13,6 +14,8 @@
#include "canon_event_sink.hh"
#include "emc/ini/inifile.hh"
#include "linuxcnc_hal_adapter.hh"
#include "linuxcnc_machine_config.hh"
#include "linuxcnc_runtime_state.hh"
#include "linuxcnc_tool_adapter.hh"
#include "nml_intf/emc.hh"
#include "tooldata.hh"
@@ -105,6 +108,7 @@ void initialize_minimal_interp(Interp &interp)
{
seed_hal_values();
seed_tool_values();
standalone::reset_external_axis_mask();
interp._setup.length_units = CANON_UNITS_MM;
interp._setup.distance_mode = DISTANCE_MODE::ABSOLUTE;
interp._setup.ijk_distance_mode = DISTANCE_MODE::ABSOLUTE;
@@ -120,13 +124,15 @@ void initialize_minimal_interp(Interp &interp)
interp._setup.parameter_g83_peck_clearance = 1.0;
interp._setup.parameters[5599] = 1.0;
interp.load_tool_table();
std::memcpy(interp._readers, Interp::default_readers, sizeof(Interp::default_readers));
standalone::apply_axis_mask_to_interp(interp, standalone::axis_mask_from_coordinates("XYZABCUVW"));
standalone::reset_user_m_code_registry();
}
void initialize_fiveaxis_remap_interp(Interp &interp)
{
seed_switchkins_hal();
seed_tool_values();
standalone::reset_external_axis_mask();
interp._setup.length_units = CANON_UNITS_MM;
interp._setup.distance_mode = DISTANCE_MODE::ABSOLUTE;
interp._setup.ijk_distance_mode = DISTANCE_MODE::INCREMENTAL;
@@ -142,7 +148,8 @@ void initialize_fiveaxis_remap_interp(Interp &interp)
interp._setup.parameter_g83_peck_clearance = 1.0;
interp._setup.parameters[5599] = 1.0;
interp.load_tool_table();
std::memcpy(interp._readers, Interp::default_readers, sizeof(Interp::default_readers));
standalone::apply_axis_mask_to_interp(interp, standalone::axis_mask_from_coordinates("XYZABCUVW"));
standalone::reset_user_m_code_registry();
interp.init_named_parameters();
}
@@ -274,6 +281,15 @@ void append_named_value(std::ostringstream &output, Interp &interp, const char *
output << name << ": rc=" << rc << " found=" << status << " value=" << value << "\n";
}
std::string command_label(const char *command)
{
std::string value = command ? command : "";
while (!value.empty() && (value.back() == '\n' || value.back() == '\r')) {
value.pop_back();
}
return value;
}
std::string parent_path(const char *path)
{
std::string value = path ? path : "";
@@ -329,6 +345,88 @@ bool parse_machine_remaps(Interp &interp, std::ostringstream &output, const char
return ok;
}
std::string remap_subroutine_path(const std::string &machine_dir, const std::string &entry)
{
if (!entry.empty() && entry[0] == '/') {
return entry;
}
if (machine_dir == "/") {
return machine_dir + entry;
}
return machine_dir + "/" + entry;
}
int apply_ini_search_paths(Interp &interp, const linuxcnc::IniFile &ini, const std::string &machine_dir)
{
standalone::apply_machine_axis_config(interp, ini);
standalone::load_tool_table_from_ini(interp, ini, machine_dir.c_str());
standalone::register_user_m_codes_from_ini(ini, machine_dir.c_str());
std::snprintf(interp._setup.program_prefix, sizeof(interp._setup.program_prefix), "%s",
machine_dir.c_str());
int subroutine_count = 0;
if (auto subroutine_path_text = ini.findString("SUBROUTINE_PATH", "RS274NGC")) {
std::istringstream paths(*subroutine_path_text);
std::string entry;
while (std::getline(paths, entry, ':') && subroutine_count < MAX_SUB_DIRS) {
const std::string resolved = remap_subroutine_path(machine_dir, entry);
interp._setup.subroutines[subroutine_count++] = strstore(resolved.c_str());
}
}
if (ini.findBoolV("OWORD_NARGS", "RS274NGC", false)) {
interp._setup.feature_set |= FEATURE_OWORD_N_ARGS;
}
return subroutine_count;
}
bool apply_ini_search_paths(Interp &interp, const char *ini_path)
{
if (!ini_path || ini_path[0] == '\0') {
return true;
}
linuxcnc::IniFile ini(ini_path);
if (!ini) {
return false;
}
apply_ini_search_paths(interp, ini, parent_path(ini_path));
return true;
}
bool parse_remaps_from_ini(Interp &interp, std::ostringstream &output, const char *ini_path)
{
const std::string machine_dir = parent_path(ini_path);
linuxcnc::IniFile ini(ini_path ? ini_path : "");
const bool opened = static_cast<bool>(ini);
output << "remap_ini_open=" << (opened ? 1 : 0) << "\n";
if (!opened) {
return false;
}
const int subroutine_count = apply_ini_search_paths(interp, ini, machine_dir);
output << "remap_subroutine_path_count=" << subroutine_count << "\n";
bool ok = subroutine_count > 0;
int remap_count = 0;
while (auto remap = ini.findString(++remap_count, "REMAP", "RS274NGC")) {
const int line_number = ini.lineOf(remap_count, "REMAP", "RS274NGC").second;
const int rc = interp.parse_remap(remap->c_str(), line_number);
output << "remap_parse_remap_" << remap_count << "=" << rc << "\n";
ok &= rc == INTERP_OK;
}
remap_count -= 1;
output << "remap_parsed_remap_count=" << remap_count << "\n";
ok &= remap_count > 0;
output << "remap_remaps_ready=" << (ok ? 1 : 0) << "\n";
return ok;
}
void apply_parameter_assignments(Interp &interp, const char *assignments)
{
std::istringstream input(assignments ? assignments : "");
@@ -371,6 +469,7 @@ char *lcinterp_run_program_with_ini(const char *program_text, const char *ini_pa
Interp interp;
standalone::reset_canon_events();
initialize_minimal_interp(interp);
apply_ini_search_paths(interp, ini_path);
const std::vector<std::string> program = split_program_lines(program_text);
std::ostringstream output;
@@ -401,8 +500,7 @@ char *lcinterp_run_program(const char *program_text)
return lcinterp_run_program_with_ini(program_text, nullptr);
}
EMSCRIPTEN_KEEPALIVE
char *lcinterp_run_file_with_ini(const char *path, const char *ini_path)
char *run_file_with_ini_internal(const char *path, const char *ini_path, bool continue_on_error)
{
if (ini_path && ini_path[0] != '\0') {
setenv("INI_FILE_NAME", ini_path, 1);
@@ -415,6 +513,8 @@ char *lcinterp_run_file_with_ini(const char *path, const char *ini_path)
initialize_minimal_interp(interp);
std::ostringstream output;
apply_ini_search_paths(interp, ini_path);
const int open_rc = interp.open(path);
output << "file_open=" << open_rc << "\n";
if (open_rc != INTERP_OK) {
@@ -429,6 +529,7 @@ char *lcinterp_run_file_with_ini(const char *path, const char *ini_path)
int file_read_count = 0;
int file_execute_count = 0;
bool saw_error = false;
while (true) {
const int read_rc = interp.read();
if (read_rc == INTERP_ENDFILE) {
@@ -444,7 +545,11 @@ char *lcinterp_run_file_with_ini(const char *path, const char *ini_path)
interp.error_text(read_rc, error_buf, sizeof(error_buf));
output << "file_error_text=" << error_buf << "\n";
}
break;
saw_error = true;
if (!continue_on_error) {
break;
}
continue;
}
const int execute_rc = interp.execute();
@@ -458,6 +563,7 @@ char *lcinterp_run_file_with_ini(const char *path, const char *ini_path)
if ((execute_rc != INTERP_OK) &&
(execute_rc != INTERP_EXECUTE_FINISH) &&
(execute_rc != INTERP_EXIT)) {
saw_error = true;
break;
}
if (execute_rc == INTERP_EXIT) {
@@ -467,10 +573,23 @@ char *lcinterp_run_file_with_ini(const char *path, const char *ini_path)
output << "file_read_count=" << file_read_count << "\n";
output << "file_execute_count=" << file_execute_count << "\n";
output << "file_saw_error=" << (saw_error ? 1 : 0) << "\n";
append_events_and_state(output, interp);
return copy_result(output.str());
}
EMSCRIPTEN_KEEPALIVE
char *lcinterp_run_file_with_ini(const char *path, const char *ini_path)
{
return run_file_with_ini_internal(path, ini_path, false);
}
EMSCRIPTEN_KEEPALIVE
char *lcinterp_run_file_with_ini_continue_on_error(const char *path, const char *ini_path)
{
return run_file_with_ini_internal(path, ini_path, true);
}
EMSCRIPTEN_KEEPALIVE
char *lcinterp_run_file(const char *path)
{
@@ -573,6 +692,230 @@ char *lcinterp_run_fiveaxis_remap_file(const char *path, const char *ini_path)
return copy_result(output.str());
}
char *run_remap_file_internal(const char *path,
const char *ini_path,
bool continue_on_error,
bool expect_error)
{
if (ini_path && ini_path[0] != '\0') {
setenv("INI_FILE_NAME", ini_path, 1);
} else {
unsetenv("INI_FILE_NAME");
}
Interp interp;
standalone::reset_canon_events();
initialize_minimal_interp(interp);
interp.init_named_parameters();
std::ostringstream output;
bool ok = parse_remaps_from_ini(interp, output, ini_path);
const int open_rc = interp.open(path);
output << "remap_file_open=" << open_rc << "\n";
if (open_rc != INTERP_OK) {
append_error_text(output, interp, "remap_file_error_text=", open_rc);
output << "remap_linuxcnc_file_execute=0\n";
unsetenv("INI_FILE_NAME");
return copy_result(output.str());
}
constexpr int max_file_steps = 20000;
int file_read_count = 0;
int file_execute_count = 0;
int file_finish_count = 0;
int final_rc = INTERP_OK;
bool reached_exit = false;
bool reached_endfile = false;
bool saw_error = false;
while ((file_read_count < max_file_steps) && (file_execute_count < max_file_steps)) {
const int read_rc = interp.read();
if (read_rc == INTERP_ENDFILE) {
output << "remap_file_read_eof=" << read_rc << "\n";
final_rc = read_rc;
reached_endfile = true;
break;
}
++file_read_count;
if ((read_rc != INTERP_OK) && (read_rc != INTERP_EXECUTE_FINISH)) {
output << "remap_file_read_" << file_read_count << "=" << read_rc << "\n";
append_error_text(output, interp, "remap_file_error_text=", read_rc);
final_rc = read_rc;
saw_error = true;
if (!expect_error) {
ok = false;
}
if (!continue_on_error) {
break;
}
continue;
}
const int execute_rc = interp.execute();
++file_execute_count;
final_rc = execute_rc;
if (execute_rc == INTERP_EXECUTE_FINISH) {
++file_finish_count;
}
if ((execute_rc != INTERP_OK) && (execute_rc != INTERP_EXIT) &&
(execute_rc != INTERP_EXECUTE_FINISH)) {
output << "remap_file_execute_" << file_execute_count << "=" << execute_rc << "\n";
append_error_text(output, interp, "remap_file_error_text=", execute_rc);
saw_error = true;
if (!expect_error) {
ok = false;
}
if (!continue_on_error) {
break;
}
continue;
}
if (execute_rc == INTERP_EXIT) {
reached_exit = true;
break;
}
}
output << "remap_file_read_count=" << file_read_count << "\n";
output << "remap_file_execute_count=" << file_execute_count << "\n";
output << "remap_file_finish_count=" << file_finish_count << "\n";
output << "remap_file_final_rc=" << final_rc << "\n";
output << "remap_file_reached_exit=" << (reached_exit ? 1 : 0) << "\n";
output << "remap_file_reached_endfile=" << (reached_endfile ? 1 : 0) << "\n";
output << "remap_file_saw_error=" << (saw_error ? 1 : 0) << "\n";
for (const auto &event : standalone::canon_events()) {
output << "remap_canon_event=" << event << "\n";
}
ok &= file_read_count > 0;
ok &= file_execute_count > 0;
ok &= file_read_count < max_file_steps;
ok &= file_execute_count < max_file_steps;
ok &= reached_exit || reached_endfile;
ok &= expect_error ? saw_error : !saw_error;
output << "remap_linuxcnc_file_execute=" << (ok ? 1 : 0) << "\n";
unsetenv("INI_FILE_NAME");
return copy_result(output.str());
}
} // extern "C"
bool execute_remap_mdi_command(Interp &interp, std::ostringstream &output, const char *command)
{
const std::string label = command_label(command);
int rc = interp.execute(command);
output << "remap_mdi_" << label << "_execute_0=" << rc << "\n";
int finish_count = 0;
while (rc == INTERP_EXECUTE_FINISH && finish_count < 16) {
++finish_count;
rc = interp.execute();
output << "remap_mdi_" << label << "_execute_" << finish_count << "=" << rc << "\n";
}
output << "remap_mdi_" << label << "_execute_finish_count=" << finish_count << "\n";
return rc == INTERP_OK;
}
char *run_remap_io_mdi_sequence_internal(const char *ini_path)
{
if (ini_path && ini_path[0] != '\0') {
setenv("INI_FILE_NAME", ini_path, 1);
} else {
unsetenv("INI_FILE_NAME");
}
Interp interp;
standalone::reset_canon_events();
initialize_minimal_interp(interp);
interp.init_named_parameters();
std::ostringstream output;
bool ok = parse_remaps_from_ini(interp, output, ini_path);
ok &= output.str().find("remap_parsed_remap_count=7") != std::string::npos;
const struct MdiStep {
const char *command;
bool digital_input;
int digital_value;
bool analog_input;
double analog_value;
} steps[] = {
{"M62 P1\n", false, 0, false, 0.0},
{"G0 X1.0\n", false, 0, false, 0.0},
{"M63 P1\n", false, 0, false, 0.0},
{"G0 X2.0\n", false, 0, false, 0.0},
{"M62 P1\n", false, 0, false, 0.0},
{"G0 X3.0\n", false, 0, false, 0.0},
{"M63 P1\n", false, 0, false, 0.0},
{"G0 X4.0\n", false, 0, false, 0.0},
{"M64 P1\n", false, 0, false, 0.0},
{"M65 P1\n", false, 0, false, 0.0},
{"M64 P1\n", false, 0, false, 0.0},
{"M65 P1\n", false, 0, false, 0.0},
{"M66 P1\n", true, 1, false, 0.0},
{"M66 P1\n", true, 0, false, 0.0},
{"M66 E1 L0\n", false, 0, true, 42.13},
{"M66 E1 L0\n", false, 0, true, -13.42},
{"M67 E1 Q42.13\n", false, 0, false, 0.0},
{"G0 X-1.0\n", false, 0, false, 0.0},
{"M67 E1 Q-13.42\n", false, 0, false, 0.0},
{"G0 X-2.0\n", false, 0, false, 0.0},
{"M67 E1 Q0.0\n", false, 0, false, 0.0},
{"G0 X-3.0\n", false, 0, false, 0.0},
{"M68 E1 Q42.13\n", false, 0, false, 0.0},
{"M68 E1 Q-13.42\n", false, 0, false, 0.0},
{"M68 E1 Q0.0\n", false, 0, false, 0.0},
};
for (const auto &step : steps) {
if (step.digital_input) {
standalone::set_external_digital_input(0, step.digital_value);
}
if (step.analog_input) {
standalone::set_external_analog_input(0, step.analog_value);
}
ok &= execute_remap_mdi_command(interp, output, step.command);
output << "remap_mdi_parameter_5399=" << interp._setup.parameters[5399] << "\n";
output << "remap_mdi_parameter_100=" << interp._setup.parameters[100] << "\n";
}
output << "remap_mdi_canon_event_count=" << standalone::canon_events().size() << "\n";
for (const auto &event : standalone::canon_events()) {
output << "remap_mdi_canon_event=" << event << "\n";
}
ok &= std::fabs(interp._setup.parameters[5399] - -13.42) < 1e-9;
ok &= std::fabs(interp._setup.parameters[100] - -13.42) < 1e-9;
output << "remap_io_ngc_linuxcnc_mdi_sequence=" << (ok ? 1 : 0) << "\n";
unsetenv("INI_FILE_NAME");
return copy_result(output.str());
}
extern "C" {
EMSCRIPTEN_KEEPALIVE
char *lcinterp_run_remap_file(const char *path, const char *ini_path)
{
return run_remap_file_internal(path, ini_path, false, false);
}
EMSCRIPTEN_KEEPALIVE
char *lcinterp_run_remap_file_continue_on_error(const char *path, const char *ini_path)
{
return run_remap_file_internal(path, ini_path, true, true);
}
EMSCRIPTEN_KEEPALIVE
char *lcinterp_run_remap_io_mdi_sequence(const char *ini_path)
{
return run_remap_io_mdi_sequence_internal(ini_path);
}
EMSCRIPTEN_KEEPALIVE
char *lcinterp_restore_parameters(const char *path)
{

View File

@@ -0,0 +1,280 @@
#define private public
#include "emc/rs274ngc/rs274ngc_interp.hh"
#undef private
#include <cctype>
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <sstream>
#include <string>
#include <sys/stat.h>
#include "emc/ini/inifile.hh"
#include "canon_event_sink.hh"
#include "linuxcnc_machine_config.hh"
#include "linuxcnc_runtime_state.hh"
#include "tooldata.hh"
namespace standalone {
namespace {
constexpr int kDefaultAxisMask = Interp::AXIS_MASK_X | Interp::AXIS_MASK_Y | Interp::AXIS_MASK_Z;
constexpr int kMaxUserMDirs = 6;
std::string g_user_m_paths[USER_DEFINED_FUNCTION_NUM];
bool is_executable_file(const std::filesystem::path &path)
{
struct stat st {};
return stat(path.c_str(), &st) == 0 && S_ISREG(st.st_mode) && (st.st_mode & S_IXUSR);
}
std::filesystem::path resolve_machine_path(const std::filesystem::path &machine_dir, const std::string &entry)
{
std::filesystem::path path(entry);
if (path.is_relative()) {
path = machine_dir / path;
}
return path.lexically_normal();
}
void standalone_user_m_code(int num, double arg1, double arg2)
{
FINISH();
const int code = num + 100;
std::ostringstream event;
event << "USER_M_COMMAND code=M" << code
<< " path=" << g_user_m_paths[num]
<< " p=" << arg1
<< " q=" << arg2;
push_canon_event(event.str());
}
int axis_mask_for_letter(char letter)
{
switch (std::toupper(static_cast<unsigned char>(letter))) {
case 'X':
return Interp::AXIS_MASK_X;
case 'Y':
return Interp::AXIS_MASK_Y;
case 'Z':
return Interp::AXIS_MASK_Z;
case 'A':
return Interp::AXIS_MASK_A;
case 'B':
return Interp::AXIS_MASK_B;
case 'C':
return Interp::AXIS_MASK_C;
case 'U':
return Interp::AXIS_MASK_U;
case 'V':
return Interp::AXIS_MASK_V;
case 'W':
return Interp::AXIS_MASK_W;
default:
return 0;
}
}
} // namespace
int axis_mask_from_coordinates(const char *coordinates)
{
if (!coordinates || coordinates[0] == '\0') {
return kDefaultAxisMask;
}
int axis_mask = 0;
for (const char *cursor = coordinates; *cursor; ++cursor) {
axis_mask |= axis_mask_for_letter(*cursor);
}
return axis_mask == 0 ? kDefaultAxisMask : axis_mask;
}
int axis_mask_from_ini(const char *ini_path)
{
if (!ini_path || ini_path[0] == '\0') {
return kDefaultAxisMask;
}
linuxcnc::IniFile ini(ini_path);
if (!ini) {
return kDefaultAxisMask;
}
if (auto coordinates = ini.findString("COORDINATES", "TRAJ")) {
return axis_mask_from_coordinates(coordinates->c_str());
}
return kDefaultAxisMask;
}
void apply_axis_mask_to_interp(Interp &interp, int axis_mask)
{
if (axis_mask == 0) {
axis_mask = kDefaultAxisMask;
}
set_external_axis_mask(axis_mask);
std::memcpy(interp._readers, Interp::default_readers, sizeof(Interp::default_readers));
if (!(axis_mask & Interp::AXIS_MASK_X)) {
interp._readers[static_cast<int>('x')] = nullptr;
}
if (!(axis_mask & Interp::AXIS_MASK_Y)) {
interp._readers[static_cast<int>('y')] = nullptr;
}
if (!(axis_mask & Interp::AXIS_MASK_Z)) {
interp._readers[static_cast<int>('z')] = nullptr;
}
if (!(axis_mask & Interp::AXIS_MASK_A)) {
interp._readers[static_cast<int>('a')] = nullptr;
}
if (!(axis_mask & Interp::AXIS_MASK_B)) {
interp._readers[static_cast<int>('b')] = nullptr;
}
if (!(axis_mask & Interp::AXIS_MASK_C)) {
interp._readers[static_cast<int>('c')] = nullptr;
}
if (!(axis_mask & Interp::AXIS_MASK_U)) {
interp._readers[static_cast<int>('u')] = nullptr;
}
if (!(axis_mask & Interp::AXIS_MASK_V)) {
interp._readers[static_cast<int>('v')] = nullptr;
}
if (!(axis_mask & Interp::AXIS_MASK_W)) {
interp._readers[static_cast<int>('w')] = nullptr;
}
}
void apply_machine_axis_config(Interp &interp, const linuxcnc::IniFile &ini)
{
interp._setup.orient_offset = ini.findRealV("ORIENT_OFFSET", "RS274NGC", 0.0);
interp._setup.disable_fanuc_style_sub =
ini.findBoolV("DISABLE_FANUC_STYLE_SUB", "RS274NGC", false);
if (auto coordinates = ini.findString("COORDINATES", "TRAJ")) {
apply_axis_mask_to_interp(interp, axis_mask_from_coordinates(coordinates->c_str()));
return;
}
apply_axis_mask_to_interp(interp, kDefaultAxisMask);
}
bool apply_machine_axis_config(Interp &interp, const char *ini_path)
{
if (!ini_path || ini_path[0] == '\0') {
apply_axis_mask_to_interp(interp, kDefaultAxisMask);
return true;
}
linuxcnc::IniFile ini(ini_path);
if (!ini) {
apply_axis_mask_to_interp(interp, kDefaultAxisMask);
return false;
}
apply_machine_axis_config(interp, ini);
return true;
}
int load_tool_table_from_ini(Interp &interp, const linuxcnc::IniFile &ini, const char *machine_dir)
{
if (auto tool_table = ini.findString("TOOL_TABLE", "EMCIO")) {
const std::filesystem::path base_dir = (machine_dir && machine_dir[0] != '\0')
? std::filesystem::path(machine_dir)
: std::filesystem::path();
const std::filesystem::path tool_path = resolve_machine_path(base_dir, *tool_table);
const bool random_toolchanger = ini.findBoolV("RANDOM_TOOLCHANGER", "EMCIO", false);
interp._setup.random_toolchanger = random_toolchanger;
tooldata_init(random_toolchanger);
tooldata_set_db(DB_NOTUSED);
const int rc = tooldata_load(tool_path.c_str());
if (rc != 0) {
return rc;
}
return interp.load_tool_table();
}
return INTERP_OK;
}
int load_tool_table_from_ini(Interp &interp, const char *ini_path)
{
if (!ini_path || ini_path[0] == '\0') {
return INTERP_OK;
}
linuxcnc::IniFile ini(ini_path);
if (!ini) {
return -1;
}
const std::filesystem::path machine_dir = std::filesystem::path(ini_path).parent_path();
return load_tool_table_from_ini(interp, ini, machine_dir.c_str());
}
void reset_user_m_code_registry()
{
for (int index = 0; index < USER_DEFINED_FUNCTION_NUM; ++index) {
USER_DEFINED_FUNCTION[index] = nullptr;
g_user_m_paths[index].clear();
}
}
int register_user_m_codes_from_ini(const linuxcnc::IniFile &ini, const char *machine_dir)
{
std::filesystem::path base_dir = (machine_dir && machine_dir[0] != '\0')
? std::filesystem::path(machine_dir)
: std::filesystem::current_path();
std::string program_prefix = "nc_files";
if (auto prefix = ini.findString("PROGRAM_PREFIX", "DISPLAY")) {
program_prefix = *prefix;
}
std::filesystem::path dirs[kMaxUserMDirs];
int dir_count = 0;
dirs[dir_count++] = resolve_machine_path(base_dir, program_prefix);
if (auto user_m_path = ini.findString("USER_M_PATH", "RS274NGC")) {
std::istringstream entries(*user_m_path);
std::string entry;
while (std::getline(entries, entry, ':') && dir_count < kMaxUserMDirs) {
if (!entry.empty()) {
dirs[dir_count++] = resolve_machine_path(base_dir, entry);
}
}
}
int registered = 0;
for (int num = 0; num < USER_DEFINED_FUNCTION_NUM; ++num) {
for (int dir_index = 0; dir_index < dir_count; ++dir_index) {
char file_name[8] = {0};
std::snprintf(file_name, sizeof(file_name), "M1%02d", num);
std::filesystem::path candidate = dirs[dir_index] / file_name;
if (!is_executable_file(candidate)) {
continue;
}
USER_DEFINED_FUNCTION_ADD(standalone_user_m_code, num);
g_user_m_paths[num] = candidate.string();
++registered;
break;
}
}
return registered;
}
int register_user_m_codes_from_ini(const char *ini_path)
{
if (!ini_path || ini_path[0] == '\0') {
return 0;
}
linuxcnc::IniFile ini(ini_path);
if (!ini) {
return 0;
}
const std::filesystem::path machine_dir = std::filesystem::path(ini_path).parent_path();
return register_user_m_codes_from_ini(ini, machine_dir.c_str());
}
} // namespace standalone

View File

@@ -0,0 +1,22 @@
#pragma once
class Interp;
namespace linuxcnc {
class IniFile;
}
namespace standalone {
int axis_mask_from_coordinates(const char *coordinates);
int axis_mask_from_ini(const char *ini_path);
void apply_axis_mask_to_interp(Interp &interp, int axis_mask);
void apply_machine_axis_config(Interp &interp, const linuxcnc::IniFile &ini);
bool apply_machine_axis_config(Interp &interp, const char *ini_path);
int load_tool_table_from_ini(Interp &interp, const linuxcnc::IniFile &ini, const char *machine_dir);
int load_tool_table_from_ini(Interp &interp, const char *ini_path);
void reset_user_m_code_registry();
int register_user_m_codes_from_ini(const linuxcnc::IniFile &ini, const char *machine_dir);
int register_user_m_codes_from_ini(const char *ini_path);
} // namespace standalone

View File

@@ -0,0 +1,9 @@
#pragma once
namespace standalone {
void reset_external_axis_mask();
void set_external_axis_mask(int axis_mask);
int external_axis_mask();
} // namespace standalone

View File

@@ -39,7 +39,11 @@ vendored LinuxCNC RS274NGC sources and exposes:
- `runProgramWithIni(programText, iniPath)`
- `runFile(path)`
- `runFileWithIni(path, iniPath)`
- `runFileWithIniContinueOnError(path, iniPath)`
- `runSimConfigProgram({ iniPath, programPath, files, executionMode })`
- `runFiveAxisRemapFile(path, iniPath)`
- `runRemapFile(path, iniPath)`
- `runRemapIoMdiSequence(iniPath)`
- `restoreParameters(path)`
- `saveParameters(path, values)`
- `loadToolTable(path, options)`
@@ -55,3 +59,33 @@ LinuxCNC random-toolchanger branch before calling vendored
sample-machine remap validation. Callers provide INI/remap/demo files in the
WASM filesystem; the C ABI runs vendored LinuxCNC `REMAP`, O-word, file
execution, and HAL adapter paths.
`runRemapFile()` is the generic remap-test equivalent for vendored LinuxCNC
tests such as `tests/remap/duplicate-o-word`. It reads LinuxCNC INI
`SUBROUTINE_PATH` and `REMAP` entries, then calls vendored LinuxCNC
`Interp::parse_remap()`, `open()`, `read()`, and `execute()`.
`runRemapIoMdiSequence()` is a narrow regression boundary for the NGC-only
branch of vendored LinuxCNC `tests/remap/remap-io/test-ngc.ini`. The C ABI
reads LinuxCNC `REMAP` entries, then feeds the upstream test driver's MDI
command sequence into vendored `Interp::execute()`. JavaScript only copies the
vendored INI/subroutine files into the WASM filesystem and forwards the INI
path; it does not implement M62-M68, M66 input, or remap semantics.
`runFileWithIni()` is the plain interpreter-file equivalent for upstream tests
that need LinuxCNC INI search paths, such as
`tests/interp/sub-call-from-sub`. The C ABI reads LinuxCNC
`SUBROUTINE_PATH` and then calls vendored `Interp::open()`, `read()`, and
`execute()`; the SDK only copies files and forwards paths.
`runSimConfigProgram()` is a convenience host boundary for representative
LinuxCNC `configs/sim` programs. Callers provide `{ path, text, executable }`
file entries, an INI path, and a program path. The SDK writes those text files
into the Emscripten filesystem, applies executable bits for user M-code files,
then forwards to `runFileWithIni()` by default or to `runFiveAxisRemapFile()`
when `executionMode: "fiveAxisRemap"` is explicitly requested.
`runFileWithIniContinueOnError()` uses the same LinuxCNC-backed file execution
path but keeps the runner loop going after LinuxCNC reports an error, matching
upstream `rs274 -n 0` regression tests such as `tests/interp/oword-unwind`.
It does not implement or reinterpret LinuxCNC error semantics.

View File

@@ -58,6 +58,34 @@ export async function createLinuxCncInterpSdk(moduleOptions = {}) {
return mod.FS.readFile(path, { encoding: "utf8" });
},
runSimConfigProgram({
iniPath,
programPath,
files = [],
executionMode = "fileWithIni",
}) {
for (const file of files) {
ensureParentPath(mod, file.path);
mod.FS.writeFile(file.path, file.text, { encoding: "utf8" });
if (file.executable) {
mod.FS.chmod(file.path, 0o755);
}
}
if (executionMode === "fiveAxisRemap") {
return callStringResult(
mod,
"lcinterp_run_fiveaxis_remap_file",
programPath,
iniPath,
);
}
if (executionMode === "fileWithIni") {
return callStringResult(mod, "lcinterp_run_file_with_ini", programPath, iniPath);
}
throw new Error(`unsupported sim config execution mode: ${executionMode}`);
},
runProgram(programText) {
return callStringResult(mod, "lcinterp_run_program", programText);
},
@@ -74,10 +102,31 @@ export async function createLinuxCncInterpSdk(moduleOptions = {}) {
return callStringResult(mod, "lcinterp_run_file_with_ini", path, iniPath);
},
runFileWithIniContinueOnError(path, iniPath) {
return callStringResult(
mod,
"lcinterp_run_file_with_ini_continue_on_error",
path,
iniPath,
);
},
runFiveAxisRemapFile(path, iniPath) {
return callStringResult(mod, "lcinterp_run_fiveaxis_remap_file", path, iniPath);
},
runRemapFile(path, iniPath) {
return callStringResult(mod, "lcinterp_run_remap_file", path, iniPath);
},
runRemapFileContinueOnError(path, iniPath) {
return callStringResult(mod, "lcinterp_run_remap_file_continue_on_error", path, iniPath);
},
runRemapIoMdiSequence(iniPath) {
return callStringResult(mod, "lcinterp_run_remap_io_mdi_sequence", iniPath);
},
restoreParameters(path) {
return callStringResult(mod, "lcinterp_restore_parameters", path);
},

View File

@@ -67,6 +67,22 @@ const eventCountNode = document.getElementById("event-count");
const wasmBadge = document.getElementById("wasm-badge");
const interpBadge = document.getElementById("interp-badge");
const opfsBadge = document.getElementById("opfs-badge");
const runProgressNode = document.getElementById("run-progress");
const runProgressLabelNode = document.getElementById("run-progress-label");
const runMotionNode = document.getElementById("run-motion");
const runLineNode = document.getElementById("run-line");
const runStatementNode = document.getElementById("run-statement");
const axisNodes = {
x: document.getElementById("axis-x"),
y: document.getElementById("axis-y"),
z: document.getElementById("axis-z"),
a: document.getElementById("axis-a"),
b: document.getElementById("axis-b"),
c: document.getElementById("axis-c"),
u: document.getElementById("axis-u"),
v: document.getElementById("axis-v"),
w: document.getElementById("axis-w"),
};
const fields = {
machine: document.getElementById("field-machine"),
@@ -88,6 +104,7 @@ let iniSdk = null;
let interpSdk = null;
let loadedSession = null;
let canonicalEventText = "";
let runPlaybackTimer = null;
function setBadge(node, text, className = "badge") {
node.className = className;
@@ -119,6 +136,109 @@ function setField(name, value) {
fields[name].textContent = value ?? "-";
}
function nextFrame() {
return new Promise((resolve) => requestAnimationFrame(resolve));
}
function formatAxisValue(value) {
return Number.isFinite(value) ? value.toFixed(3) : "0.000";
}
function clearRunPlayback() {
if (runPlaybackTimer) {
clearInterval(runPlaybackTimer);
runPlaybackTimer = null;
}
}
function setRunMonitor(progress, label, axes = {}, motionText = "idle", lineText = "line -", statement = "-") {
runProgressNode.value = Math.max(0, Math.min(100, progress));
runProgressLabelNode.textContent = label;
runMotionNode.textContent = motionText;
runLineNode.textContent = lineText;
runStatementNode.textContent = statement;
for (const [axis, node] of Object.entries(axisNodes)) {
node.textContent = formatAxisValue(axes[axis] ?? 0);
}
}
function readCanonicalNumber(line, name) {
const match = line.match(new RegExp(`(?:^| )${name}=([-+0-9.eE]+)`));
return match ? Number(match[1]) : null;
}
function programLineMap(programText) {
const lines = new Map();
programText.split(/\r?\n/).forEach((line, index) => {
lines.set(index + 1, line.trim() || "(blank)");
});
return lines;
}
function parseRunMotion(resultText, programText) {
const axes = { x: 0, y: 0, z: 0, a: 0, b: 0, c: 0, u: 0, v: 0, w: 0 };
const snapshots = [];
const sourceLines = programLineMap(programText);
for (const line of resultText.split("\n")) {
const motion = line.match(/^canon_event=(STRAIGHT_TRAVERSE|STRAIGHT_FEED|ARC_FEED)\b/);
if (!motion) {
continue;
}
for (const axis of Object.keys(axes)) {
const value = readCanonicalNumber(line, axis);
if (value !== null && Number.isFinite(value)) {
axes[axis] = value;
}
}
const sourceLine = readCanonicalNumber(line, "line");
snapshots.push({
type: motion[1],
line: sourceLine,
statement: Number.isFinite(sourceLine) ? (sourceLines.get(sourceLine) ?? "-") : "-",
axes: { ...axes },
});
}
return snapshots;
}
function showRunSnapshot(snapshot, index, total) {
const progress = total > 0 ? Math.round(((index + 1) / total) * 100) : 0;
const lineText = Number.isFinite(snapshot.line) ? `line ${snapshot.line}` : "line -";
setRunMonitor(
progress,
`${progress}%`,
snapshot.axes,
`${snapshot.type} ${lineText} (${index + 1}/${total})`,
lineText,
snapshot.statement,
);
}
function playRunMotion(resultText, programText) {
clearRunPlayback();
const snapshots = parseRunMotion(resultText, programText);
if (snapshots.length === 0) {
setRunMonitor(100, "100%", {}, "complete; no axis motion");
return;
}
let index = 0;
showRunSnapshot(snapshots[index], index, snapshots.length);
runPlaybackTimer = setInterval(() => {
index += 1;
if (index >= snapshots.length) {
clearRunPlayback();
showRunSnapshot(snapshots[snapshots.length - 1], snapshots.length - 1, snapshots.length);
return;
}
showRunSnapshot(snapshots[index], index, snapshots.length);
}, 220);
}
function requireIniSdk() {
if (!iniSdk) {
throw new Error("INI WASM module is not ready yet.");
@@ -287,10 +407,15 @@ document.getElementById("run-gcode").addEventListener("click", async () => {
const interp = requireInterpSdk();
const programText = await loadGcodeProgram(GCODE_FILENAME);
interp.writeTextFile(GCODE_WASM_FILE, programText);
clearRunPlayback();
setField("runStatus", "running");
setRunMonitor(5, "running", {}, "running");
await nextFrame();
const result = interp.runFileWithIni(GCODE_WASM_FILE, loadedSession.ini.wasmPath);
setField("sessionGcode", GCODE_WASM_FILE);
setField("runStatus", "ok");
setCanonicalEvents(result.trim());
playRunMotion(result.trim(), programText);
setLog(
[
"Ran G-code through LinuxCNC interpreter WASM.",
@@ -302,6 +427,8 @@ document.getElementById("run-gcode").addEventListener("click", async () => {
} catch (error) {
setField("runStatus", "failed");
setCanonicalEvents("");
clearRunPlayback();
setRunMonitor(0, "failed", {}, "failed");
setLog(`G-code run failed: ${error.message}`, true);
}
});

View File

@@ -123,6 +123,53 @@
font: 13px/1.5 "SFMono-Regular", "Consolas", monospace;
white-space: pre-wrap;
}
.run-monitor {
border: 1px solid var(--border);
border-radius: 6px;
background: #fbfdff;
padding: 12px;
display: grid;
gap: 10px;
}
.progress-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 10px;
align-items: center;
}
progress {
width: 100%;
height: 14px;
accent-color: var(--accent-2);
}
.run-readout {
font: 13px/1.4 "SFMono-Regular", "Consolas", monospace;
color: var(--muted);
white-space: nowrap;
}
.axis-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
}
.axis-cell {
border: 1px solid #edf2f6;
border-radius: 6px;
padding: 8px;
display: grid;
gap: 4px;
min-width: 0;
}
.axis-cell span:first-child {
color: var(--muted);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.04em;
}
.axis-cell span:last-child {
font: 14px/1.2 "SFMono-Regular", "Consolas", monospace;
overflow-wrap: anywhere;
}
.events {
border: 1px solid var(--border);
border-radius: 6px;
@@ -154,6 +201,11 @@
.grid { grid-template-columns: 1fr; }
textarea { min-height: 320px; }
}
@media (max-width: 560px) {
.axis-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.progress-row { grid-template-columns: 1fr; }
.run-readout { white-space: normal; }
}
</style>
</head>
<body>
@@ -226,6 +278,28 @@
<dd id="field-fiveaxis-remap">-</dd>
</dl>
<h2>Run Monitor</h2>
<div class="run-monitor">
<div class="progress-row">
<progress id="run-progress" max="100" value="0"></progress>
<span id="run-progress-label" class="run-readout">0%</span>
</div>
<div id="run-motion" class="run-readout">idle</div>
<div id="run-line" class="run-readout">line -</div>
<div id="run-statement" class="run-readout">-</div>
<div class="axis-grid" id="axis-grid">
<div class="axis-cell"><span>X</span><span id="axis-x">0.000</span></div>
<div class="axis-cell"><span>Y</span><span id="axis-y">0.000</span></div>
<div class="axis-cell"><span>Z</span><span id="axis-z">0.000</span></div>
<div class="axis-cell"><span>A</span><span id="axis-a">0.000</span></div>
<div class="axis-cell"><span>B</span><span id="axis-b">0.000</span></div>
<div class="axis-cell"><span>C</span><span id="axis-c">0.000</span></div>
<div class="axis-cell"><span>U</span><span id="axis-u">0.000</span></div>
<div class="axis-cell"><span>V</span><span id="axis-v">0.000</span></div>
<div class="axis-cell"><span>W</span><span id="axis-w">0.000</span></div>
</div>
</div>
<div class="log" id="log"></div>
<h2>Canonical Events</h2>