Expand LinuxCNC interpreter regression coverage
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "canon_event_sink.hh"
|
||||
#include "emc/ini/inifile.hh"
|
||||
@@ -79,7 +80,7 @@ void initialize_minimal_interp(Interp &interp)
|
||||
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;
|
||||
interp._setup.ijk_distance_mode = DISTANCE_MODE::INCREMENTAL;
|
||||
interp._setup.feed_mode = FEED_MODE::UNITS_PER_MINUTE;
|
||||
interp._setup.plane = CANON_PLANE::XY;
|
||||
interp._setup.motion_mode = G_0;
|
||||
@@ -133,6 +134,46 @@ void apply_ini_search_paths(Interp &interp, const char *ini_path)
|
||||
}
|
||||
}
|
||||
|
||||
std::filesystem::path resolve_machine_relative_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();
|
||||
}
|
||||
|
||||
int initialize_parameter_file_from_ini(Interp &interp, const char *ini_path)
|
||||
{
|
||||
standalone::reset_parameter_file_name();
|
||||
if (!ini_path || ini_path[0] == '\0') {
|
||||
return INTERP_OK;
|
||||
}
|
||||
|
||||
linuxcnc::IniFile ini(ini_path);
|
||||
if (!ini || !ini.findString("PARAMETER_FILE", "RS274NGC")) {
|
||||
return INTERP_OK;
|
||||
}
|
||||
|
||||
const int ini_rc = interp.ini_load(ini_path);
|
||||
if (ini_rc != INTERP_OK) {
|
||||
return ini_rc;
|
||||
}
|
||||
|
||||
char parameter_file_name[LINELEN] = {0};
|
||||
standalone::get_parameter_file_name(parameter_file_name, sizeof(parameter_file_name));
|
||||
const auto resolved = resolve_machine_relative_path(
|
||||
std::filesystem::path(ini_path).parent_path(), parameter_file_name);
|
||||
if (access(resolved.c_str(), F_OK) != 0) {
|
||||
standalone::reset_parameter_file_name();
|
||||
return INTERP_OK;
|
||||
}
|
||||
standalone::set_parameter_file_name(resolved.c_str());
|
||||
|
||||
return interp.init();
|
||||
}
|
||||
|
||||
std::vector<std::string> load_program(int argc, char **argv)
|
||||
{
|
||||
if (argc <= 1) {
|
||||
@@ -273,7 +314,17 @@ int main(int argc, char **argv)
|
||||
standalone::reset_canon_events();
|
||||
initialize_minimal_interp(file_interp);
|
||||
apply_ini_search_paths(file_interp, ini_path);
|
||||
file_open_rc = file_interp.open(argv[arg_offset]);
|
||||
const int init_rc = initialize_parameter_file_from_ini(file_interp, ini_path);
|
||||
if (init_rc != INTERP_OK) {
|
||||
std::cout << "file_init=" << init_rc << "\n";
|
||||
if (init_rc > INTERP_MIN_ERROR) {
|
||||
char error_buf[LINELEN] = {0};
|
||||
file_interp.error_text(init_rc, error_buf, sizeof(error_buf));
|
||||
std::cout << "file_error_text=" << error_buf << "\n";
|
||||
}
|
||||
}
|
||||
apply_ini_search_paths(file_interp, ini_path);
|
||||
file_open_rc = init_rc == INTERP_OK ? file_interp.open(argv[arg_offset]) : init_rc;
|
||||
if (file_open_rc == INTERP_OK) {
|
||||
while (true) {
|
||||
const int file_read_rc = file_interp.read();
|
||||
|
||||
@@ -22,6 +22,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;
|
||||
static std::string g_parameter_file_name;
|
||||
|
||||
void reset_canon_events()
|
||||
{
|
||||
@@ -55,6 +56,25 @@ int external_axis_mask()
|
||||
return g_external_axis_mask;
|
||||
}
|
||||
|
||||
void reset_parameter_file_name()
|
||||
{
|
||||
g_parameter_file_name.clear();
|
||||
}
|
||||
|
||||
void set_parameter_file_name(const char *filename)
|
||||
{
|
||||
g_parameter_file_name = filename ? filename : "";
|
||||
}
|
||||
|
||||
void get_parameter_file_name(char *filename, int max_size)
|
||||
{
|
||||
if (!filename || max_size <= 0) {
|
||||
return;
|
||||
}
|
||||
std::snprintf(filename, static_cast<std::size_t>(max_size), "%s",
|
||||
g_parameter_file_name.c_str());
|
||||
}
|
||||
|
||||
} // namespace standalone
|
||||
|
||||
bool GET_BLOCK_DELETE(void) { return false; }
|
||||
@@ -489,11 +509,12 @@ double GET_EXTERNAL_ORIGIN_Y() { return 0.0; }
|
||||
double GET_EXTERNAL_ORIGIN_Z() { return 0.0; }
|
||||
void GET_EXTERNAL_PARAMETER_FILE_NAME(char *filename, int max_size)
|
||||
{
|
||||
if (max_size > 0) {
|
||||
filename[0] = '\0';
|
||||
}
|
||||
standalone::get_parameter_file_name(filename, max_size);
|
||||
}
|
||||
void SET_PARAMETER_FILE_NAME(const char *filename)
|
||||
{
|
||||
standalone::set_parameter_file_name(filename);
|
||||
}
|
||||
void SET_PARAMETER_FILE_NAME(const char *) {}
|
||||
CANON_PLANE GET_EXTERNAL_PLANE() { return CANON_PLANE::XY; }
|
||||
double GET_EXTERNAL_POSITION_A() { return 0.0; }
|
||||
double GET_EXTERNAL_POSITION_B() { return 0.0; }
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include <emscripten/emscripten.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "canon_event_sink.hh"
|
||||
#include "emc/ini/inifile.hh"
|
||||
@@ -111,7 +112,7 @@ void initialize_minimal_interp(Interp &interp)
|
||||
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;
|
||||
interp._setup.ijk_distance_mode = DISTANCE_MODE::INCREMENTAL;
|
||||
interp._setup.feed_mode = FEED_MODE::UNITS_PER_MINUTE;
|
||||
interp._setup.plane = CANON_PLANE::XY;
|
||||
interp._setup.motion_mode = G_0;
|
||||
@@ -411,6 +412,17 @@ std::string remap_subroutine_path(const std::string &machine_dir, const std::str
|
||||
return machine_dir + "/" + entry;
|
||||
}
|
||||
|
||||
std::string resolve_machine_relative_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);
|
||||
@@ -452,6 +464,36 @@ bool apply_ini_search_paths(Interp &interp, const char *ini_path)
|
||||
return true;
|
||||
}
|
||||
|
||||
int initialize_parameter_file_from_ini(Interp &interp, const char *ini_path)
|
||||
{
|
||||
standalone::reset_parameter_file_name();
|
||||
if (!ini_path || ini_path[0] == '\0') {
|
||||
return INTERP_OK;
|
||||
}
|
||||
|
||||
linuxcnc::IniFile ini(ini_path);
|
||||
if (!ini || !ini.findString("PARAMETER_FILE", "RS274NGC")) {
|
||||
return INTERP_OK;
|
||||
}
|
||||
|
||||
const int ini_rc = interp.ini_load(ini_path);
|
||||
if (ini_rc != INTERP_OK) {
|
||||
return ini_rc;
|
||||
}
|
||||
|
||||
char parameter_file_name[LINELEN] = {0};
|
||||
standalone::get_parameter_file_name(parameter_file_name, sizeof(parameter_file_name));
|
||||
const std::string resolved =
|
||||
resolve_machine_relative_path(parent_path(ini_path), parameter_file_name);
|
||||
if (access(resolved.c_str(), F_OK) != 0) {
|
||||
standalone::reset_parameter_file_name();
|
||||
return INTERP_OK;
|
||||
}
|
||||
standalone::set_parameter_file_name(resolved.c_str());
|
||||
|
||||
return interp.init();
|
||||
}
|
||||
|
||||
bool parse_remaps_from_ini(Interp &interp, std::ostringstream &output, const char *ini_path)
|
||||
{
|
||||
const std::string machine_dir = parent_path(ini_path);
|
||||
@@ -569,6 +611,14 @@ char *run_file_with_ini_internal(const char *path, const char *ini_path, bool co
|
||||
|
||||
std::ostringstream output;
|
||||
apply_ini_search_paths(interp, ini_path);
|
||||
const int init_rc = initialize_parameter_file_from_ini(interp, ini_path);
|
||||
if (init_rc != INTERP_OK) {
|
||||
output << "file_init=" << init_rc << "\n";
|
||||
append_error_text(output, interp, "file_error_text=", init_rc);
|
||||
append_events_and_state(output, interp);
|
||||
return copy_result(output.str());
|
||||
}
|
||||
apply_ini_search_paths(interp, ini_path);
|
||||
|
||||
const int open_rc = interp.open(path);
|
||||
output << "file_open=" << open_rc << "\n";
|
||||
|
||||
@@ -5,5 +5,8 @@ namespace standalone {
|
||||
void reset_external_axis_mask();
|
||||
void set_external_axis_mask(int axis_mask);
|
||||
int external_axis_mask();
|
||||
void reset_parameter_file_name();
|
||||
void set_parameter_file_name(const char *filename);
|
||||
void get_parameter_file_name(char *filename, int max_size);
|
||||
|
||||
} // namespace standalone
|
||||
|
||||
@@ -25,6 +25,7 @@ Use `src/index.js` for stable imports:
|
||||
import {
|
||||
createLinuxCncIniSdk,
|
||||
createLinuxCncInterpSdk,
|
||||
planIniFileContextStaging,
|
||||
planSimConfigStaging,
|
||||
} from "./src/index.js";
|
||||
```
|
||||
@@ -86,14 +87,22 @@ 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.
|
||||
|
||||
`planSimConfigStaging()` is the companion file-staging planner for
|
||||
`planIniFileContextStaging()` is the generic file-staging planner for
|
||||
LinuxCNC INI-driven interpreter runs. Callers provide vendored source-manifest
|
||||
text, a source root, an INI file name, INI text, and a target WASM directory.
|
||||
The planner reads only INI file references and the manifest, then returns the
|
||||
INI, `[DISPLAY]OPEN_FILE`, `[EMCIO]TOOL_TABLE`,
|
||||
`[RS274NGC]PARAMETER_FILE`, `[RS274NGC]SUBROUTINE_PATH`,
|
||||
`[RS274NGC]USER_M_PATH`, and remap-NGC files that should be copied into the
|
||||
Emscripten filesystem. Each planned file includes `sourceRel`, `wasmPath`, the
|
||||
legacy alias `path`, and `executable`. It does not implement G-code, tool,
|
||||
parameter, remap, or user-M semantics.
|
||||
|
||||
`planSimConfigStaging()` is the companion convenience wrapper for
|
||||
representative LinuxCNC `configs/sim` programs. Callers provide the vendored
|
||||
source manifest text, machine relative path, INI file name, and INI text. The
|
||||
planner reads only INI file references and the manifest, then returns the INI,
|
||||
`[DISPLAY]OPEN_FILE`, `[EMCIO]TOOL_TABLE`, `[RS274NGC]PARAMETER_FILE`,
|
||||
`[RS274NGC]SUBROUTINE_PATH`, `[RS274NGC]USER_M_PATH`, and remap-NGC files that
|
||||
should be copied into the Emscripten filesystem. It does not implement G-code,
|
||||
tool, parameter, remap, or user-M semantics.
|
||||
source manifest text, machine relative path, INI file name, and INI text. It
|
||||
uses `planIniFileContextStaging()` with a `configs/sim/<machine>` source root
|
||||
and `configs/sim` upward-search boundary.
|
||||
|
||||
`runFileWithIniContinueOnError()` uses the same LinuxCNC-backed file execution
|
||||
path but keeps the runner loop going after LinuxCNC reports an error, matching
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
export { createLinuxCncIniSdk } from "./linuxcnc-ini.js";
|
||||
export { createLinuxCncInterpSdk } from "./linuxcnc-interp.js";
|
||||
export { planSimConfigStaging } from "./sim-config-staging.js";
|
||||
export {
|
||||
planIniFileContextStaging,
|
||||
planSimConfigStaging,
|
||||
} from "./sim-config-staging.js";
|
||||
|
||||
@@ -98,14 +98,14 @@ function isSubroutinePath(path) {
|
||||
return path.toLowerCase().endsWith(".ngc");
|
||||
}
|
||||
|
||||
function findUpwardByBasename(manifestSet, sourceDir, fileName) {
|
||||
function findUpwardByBasename(manifestSet, sourceDir, fileName, searchRootRel) {
|
||||
let current = sourceDir;
|
||||
while (current.startsWith("configs/sim")) {
|
||||
while (!searchRootRel || current === searchRootRel || current.startsWith(`${searchRootRel}/`)) {
|
||||
const candidate = normalizeRel(`${current}/${fileName}`);
|
||||
if (manifestSet.has(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
if (current === "configs/sim") {
|
||||
if (!current || current === searchRootRel) {
|
||||
break;
|
||||
}
|
||||
current = dirname(current);
|
||||
@@ -113,7 +113,7 @@ function findUpwardByBasename(manifestSet, sourceDir, fileName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function sourceForReference(manifestSet, sourceDir, reference) {
|
||||
function sourceForReference(manifestSet, sourceDir, reference, searchRootRel) {
|
||||
if (!reference || reference.startsWith("/")) {
|
||||
return null;
|
||||
}
|
||||
@@ -121,7 +121,7 @@ function sourceForReference(manifestSet, sourceDir, reference) {
|
||||
if (manifestSet.has(sourceRel)) {
|
||||
return sourceRel;
|
||||
}
|
||||
return findUpwardByBasename(manifestSet, sourceDir, basename(reference));
|
||||
return findUpwardByBasename(manifestSet, sourceDir, basename(reference), searchRootRel);
|
||||
}
|
||||
|
||||
function addPlannedFile(plan, manifestSet, sourceRel, targetRel, options = {}) {
|
||||
@@ -141,12 +141,13 @@ function addPlannedFile(plan, manifestSet, sourceRel, targetRel, options = {}) {
|
||||
|
||||
plan.set(sourceRel, {
|
||||
sourceRel,
|
||||
wasmPath: targetPathFor(options.wasmDir, targetRel),
|
||||
path: targetPathFor(options.wasmDir, targetRel),
|
||||
executable,
|
||||
});
|
||||
}
|
||||
|
||||
function addDirectoryFiles(plan, manifestEntries, sourceDir, targetDir, predicate, options) {
|
||||
function addDirectoryFiles(plan, manifestEntries, manifestSet, sourceDir, targetDir, predicate, options) {
|
||||
const prefix = sourceDir ? `${sourceDir}/` : "";
|
||||
for (const sourceRel of manifestEntries) {
|
||||
if (!sourceRel.startsWith(prefix)) {
|
||||
@@ -156,7 +157,7 @@ function addDirectoryFiles(plan, manifestEntries, sourceDir, targetDir, predicat
|
||||
if (childRel.includes("/") || !predicate(sourceRel)) {
|
||||
continue;
|
||||
}
|
||||
addPlannedFile(plan, new Set(manifestEntries), sourceRel, normalizeRel(`${targetDir}/${childRel}`), options);
|
||||
addPlannedFile(plan, manifestSet, sourceRel, normalizeRel(`${targetDir}/${childRel}`), options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,18 +179,45 @@ export function planSimConfigStaging({
|
||||
iniText,
|
||||
programFile = null,
|
||||
wasmDir = `/work/sim/${machineRel}`,
|
||||
}) {
|
||||
return planIniFileContextStaging({
|
||||
manifestText,
|
||||
sourceRootRel: `configs/sim/${machineRel}`,
|
||||
sourceSearchRootRel: "configs/sim",
|
||||
iniFile,
|
||||
iniText,
|
||||
programFile,
|
||||
wasmDir,
|
||||
});
|
||||
}
|
||||
|
||||
export function planIniFileContextStaging({
|
||||
manifestText,
|
||||
sourceRootRel,
|
||||
iniFile,
|
||||
iniText,
|
||||
programFile = null,
|
||||
wasmDir,
|
||||
sourceSearchRootRel = sourceRootRel,
|
||||
}) {
|
||||
const manifestEntries = cleanManifest(manifestText);
|
||||
const manifestSet = new Set(manifestEntries);
|
||||
const iniValues = parseIni(iniText);
|
||||
const plan = new Map();
|
||||
const sourceDir = sourceRelFor(machineRel, dirname(iniFile));
|
||||
const iniSourceRel = sourceRelFor(machineRel, iniFile);
|
||||
const normalizedSourceRoot = normalizeRel(sourceRootRel);
|
||||
const normalizedSearchRoot = normalizeRel(sourceSearchRootRel);
|
||||
const sourceDir = normalizeRel(`${normalizedSourceRoot}/${dirname(iniFile)}`);
|
||||
const iniSourceRel = normalizeRel(`${normalizedSourceRoot}/${iniFile}`);
|
||||
const programReference = programFile ?? firstIniValue(iniValues, "DISPLAY", "OPEN_FILE");
|
||||
|
||||
addPlannedFile(plan, manifestSet, iniSourceRel, iniFile, { wasmDir, required: true });
|
||||
if (programReference) {
|
||||
const programSourceRel = sourceForReference(manifestSet, sourceDir, programReference);
|
||||
const programSourceRel = sourceForReference(
|
||||
manifestSet,
|
||||
sourceDir,
|
||||
programReference,
|
||||
normalizedSearchRoot,
|
||||
);
|
||||
addPlannedFile(plan, manifestSet, programSourceRel, programReference, {
|
||||
wasmDir,
|
||||
required: true,
|
||||
@@ -204,39 +232,54 @@ export function planSimConfigStaging({
|
||||
if (!reference) {
|
||||
continue;
|
||||
}
|
||||
const sourceRel = sourceForReference(manifestSet, sourceDir, reference);
|
||||
const sourceRel = sourceForReference(manifestSet, sourceDir, reference, normalizedSearchRoot);
|
||||
addPlannedFile(plan, manifestSet, sourceRel, reference, { wasmDir });
|
||||
}
|
||||
|
||||
const subroutineDirs = splitSearchPath(
|
||||
firstIniValue(iniValues, "RS274NGC", "SUBROUTINE_PATH") ?? "",
|
||||
);
|
||||
const subroutineDirs = allIniValues(iniValues, "RS274NGC", "SUBROUTINE_PATH")
|
||||
.flatMap(splitSearchPath);
|
||||
for (const dirEntry of subroutineDirs) {
|
||||
if (dirEntry.startsWith("/")) {
|
||||
continue;
|
||||
}
|
||||
const sourceSubdir = normalizeRel(`${sourceDir}/${dirEntry}`);
|
||||
const targetSubdir = normalizeRel(dirEntry);
|
||||
addDirectoryFiles(plan, manifestEntries, sourceSubdir, targetSubdir, isSubroutinePath, {
|
||||
wasmDir,
|
||||
});
|
||||
addDirectoryFiles(
|
||||
plan,
|
||||
manifestEntries,
|
||||
manifestSet,
|
||||
sourceSubdir,
|
||||
targetSubdir,
|
||||
isSubroutinePath,
|
||||
{ wasmDir },
|
||||
);
|
||||
}
|
||||
|
||||
for (const dirEntry of splitSearchPath(firstIniValue(iniValues, "RS274NGC", "USER_M_PATH") ?? "")) {
|
||||
for (const dirEntry of allIniValues(iniValues, "RS274NGC", "USER_M_PATH").flatMap(splitSearchPath)) {
|
||||
if (dirEntry.startsWith("/")) {
|
||||
continue;
|
||||
}
|
||||
const sourceUserMDir = normalizeRel(`${sourceDir}/${dirEntry}`);
|
||||
const targetUserMDir = normalizeRel(dirEntry);
|
||||
addDirectoryFiles(plan, manifestEntries, sourceUserMDir, targetUserMDir, isUserMCodePath, {
|
||||
wasmDir,
|
||||
executable: true,
|
||||
});
|
||||
addDirectoryFiles(
|
||||
plan,
|
||||
manifestEntries,
|
||||
manifestSet,
|
||||
sourceUserMDir,
|
||||
targetUserMDir,
|
||||
isUserMCodePath,
|
||||
{ wasmDir, executable: true },
|
||||
);
|
||||
}
|
||||
|
||||
for (const remapName of remapNgcNames(iniValues)) {
|
||||
for (const dirEntry of subroutineDirs) {
|
||||
const sourceRel = sourceForReference(manifestSet, normalizeRel(`${sourceDir}/${dirEntry}`), remapName);
|
||||
const sourceRel = sourceForReference(
|
||||
manifestSet,
|
||||
normalizeRel(`${sourceDir}/${dirEntry}`),
|
||||
remapName,
|
||||
normalizedSearchRoot,
|
||||
);
|
||||
addPlannedFile(plan, manifestSet, sourceRel, normalizeRel(`${dirEntry}/${remapName}`), {
|
||||
wasmDir,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user