Expand WASM sim-config staging coverage
This commit is contained in:
@@ -158,6 +158,61 @@ std::vector<std::string> load_program(int argc, char **argv)
|
||||
return lines;
|
||||
}
|
||||
|
||||
bool is_uri_unreserved(char value)
|
||||
{
|
||||
return (value >= 'A' && value <= 'Z') ||
|
||||
(value >= 'a' && value <= 'z') ||
|
||||
(value >= '0' && value <= '9') ||
|
||||
value == '-' || value == '_' || value == '.' || value == '~';
|
||||
}
|
||||
|
||||
void append_percent_encoded_text(std::ostream &output, const char *text)
|
||||
{
|
||||
constexpr char hex[] = "0123456789ABCDEF";
|
||||
for (const char *cursor = text ? text : ""; *cursor; ++cursor) {
|
||||
char value = *cursor;
|
||||
switch (*cursor) {
|
||||
case '\r':
|
||||
case '\n':
|
||||
case '\t':
|
||||
value = ' ';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (is_uri_unreserved(value)) {
|
||||
output << value;
|
||||
} else {
|
||||
const auto byte = static_cast<unsigned char>(value);
|
||||
output << '%' << hex[(byte >> 4) & 0x0f] << hex[byte & 0x0f];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void append_run_step(std::ostream &output,
|
||||
const Interp &interp,
|
||||
const char *phase,
|
||||
int step,
|
||||
int rc)
|
||||
{
|
||||
output << "run_step phase=" << phase
|
||||
<< " step=" << step
|
||||
<< " rc=" << rc
|
||||
<< " line=" << interp._setup.sequence_number
|
||||
<< " x=" << interp._setup.current_x
|
||||
<< " y=" << interp._setup.current_y
|
||||
<< " z=" << interp._setup.current_z
|
||||
<< " a=" << interp._setup.AA_current
|
||||
<< " b=" << interp._setup.BB_current
|
||||
<< " c=" << interp._setup.CC_current
|
||||
<< " u=" << interp._setup.u_current
|
||||
<< " v=" << interp._setup.v_current
|
||||
<< " w=" << interp._setup.w_current
|
||||
<< " statement_uri=";
|
||||
append_percent_encoded_text(output, interp._setup.linetext);
|
||||
output << "\n";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char **argv)
|
||||
@@ -228,6 +283,7 @@ int main(int argc, char **argv)
|
||||
}
|
||||
++file_read_count;
|
||||
std::cout << "file_read_" << file_read_count << "=" << file_read_rc << "\n";
|
||||
append_run_step(std::cout, file_interp, "read", file_read_count, file_read_rc);
|
||||
if ((file_read_rc != INTERP_OK) && (file_read_rc != INTERP_EXECUTE_FINISH)) {
|
||||
if (file_read_rc > INTERP_MIN_ERROR) {
|
||||
char error_buf[LINELEN] = {0};
|
||||
@@ -244,6 +300,7 @@ int main(int argc, char **argv)
|
||||
const int file_execute_rc = file_interp.execute();
|
||||
++file_execute_count;
|
||||
std::cout << "file_execute_" << file_execute_count << "=" << file_execute_rc << "\n";
|
||||
append_run_step(std::cout, file_interp, "execute", file_execute_count, file_execute_rc);
|
||||
if (file_execute_rc > INTERP_MIN_ERROR) {
|
||||
char error_buf[LINELEN] = {0};
|
||||
file_interp.error_text(file_execute_rc, error_buf, sizeof(error_buf));
|
||||
|
||||
@@ -264,6 +264,61 @@ void append_tool_table_state(std::ostringstream &output)
|
||||
output << "tool_index_for_tool_2=" << standalone::find_tool_index_for_tool(2) << "\n";
|
||||
}
|
||||
|
||||
bool is_uri_unreserved(char value)
|
||||
{
|
||||
return (value >= 'A' && value <= 'Z') ||
|
||||
(value >= 'a' && value <= 'z') ||
|
||||
(value >= '0' && value <= '9') ||
|
||||
value == '-' || value == '_' || value == '.' || value == '~';
|
||||
}
|
||||
|
||||
void append_percent_encoded_text(std::ostringstream &output, const char *text)
|
||||
{
|
||||
constexpr char hex[] = "0123456789ABCDEF";
|
||||
for (const char *cursor = text ? text : ""; *cursor; ++cursor) {
|
||||
char value = *cursor;
|
||||
switch (*cursor) {
|
||||
case '\r':
|
||||
case '\n':
|
||||
case '\t':
|
||||
value = ' ';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (is_uri_unreserved(value)) {
|
||||
output << value;
|
||||
} else {
|
||||
const auto byte = static_cast<unsigned char>(value);
|
||||
output << '%' << hex[(byte >> 4) & 0x0f] << hex[byte & 0x0f];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void append_run_step(std::ostringstream &output,
|
||||
const Interp &interp,
|
||||
const char *phase,
|
||||
int step,
|
||||
int rc)
|
||||
{
|
||||
output << "run_step phase=" << phase
|
||||
<< " step=" << step
|
||||
<< " rc=" << rc
|
||||
<< " line=" << interp._setup.sequence_number
|
||||
<< " x=" << interp._setup.current_x
|
||||
<< " y=" << interp._setup.current_y
|
||||
<< " z=" << interp._setup.current_z
|
||||
<< " a=" << interp._setup.AA_current
|
||||
<< " b=" << interp._setup.BB_current
|
||||
<< " c=" << interp._setup.CC_current
|
||||
<< " u=" << interp._setup.u_current
|
||||
<< " v=" << interp._setup.v_current
|
||||
<< " w=" << interp._setup.w_current
|
||||
<< " statement_uri=";
|
||||
append_percent_encoded_text(output, interp._setup.linetext);
|
||||
output << "\n";
|
||||
}
|
||||
|
||||
void append_error_text(std::ostringstream &output, Interp &interp, const char *prefix, int rc)
|
||||
{
|
||||
if (rc > INTERP_MIN_ERROR) {
|
||||
@@ -539,6 +594,7 @@ char *run_file_with_ini_internal(const char *path, const char *ini_path, bool co
|
||||
|
||||
++file_read_count;
|
||||
output << "file_read_" << file_read_count << "=" << read_rc << "\n";
|
||||
append_run_step(output, interp, "read", file_read_count, read_rc);
|
||||
if ((read_rc != INTERP_OK) && (read_rc != INTERP_EXECUTE_FINISH)) {
|
||||
if (read_rc > INTERP_MIN_ERROR) {
|
||||
char error_buf[LINELEN] = {0};
|
||||
@@ -555,6 +611,7 @@ char *run_file_with_ini_internal(const char *path, const char *ini_path, bool co
|
||||
const int execute_rc = interp.execute();
|
||||
++file_execute_count;
|
||||
output << "file_execute_" << file_execute_count << "=" << execute_rc << "\n";
|
||||
append_run_step(output, interp, "execute", file_execute_count, execute_rc);
|
||||
if (execute_rc > INTERP_MIN_ERROR) {
|
||||
char error_buf[LINELEN] = {0};
|
||||
interp.error_text(execute_rc, error_buf, sizeof(error_buf));
|
||||
|
||||
@@ -25,6 +25,7 @@ Use `src/index.js` for stable imports:
|
||||
import {
|
||||
createLinuxCncIniSdk,
|
||||
createLinuxCncInterpSdk,
|
||||
planSimConfigStaging,
|
||||
} from "./src/index.js";
|
||||
```
|
||||
|
||||
@@ -85,6 +86,15 @@ 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
|
||||
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.
|
||||
|
||||
`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`.
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export { createLinuxCncIniSdk } from "./linuxcnc-ini.js";
|
||||
export { createLinuxCncInterpSdk } from "./linuxcnc-interp.js";
|
||||
export { planSimConfigStaging } from "./sim-config-staging.js";
|
||||
|
||||
252
wasm-port/runtime/sdk/src/sim-config-staging.js
Normal file
252
wasm-port/runtime/sdk/src/sim-config-staging.js
Normal file
@@ -0,0 +1,252 @@
|
||||
function cleanManifest(manifestText) {
|
||||
return manifestText
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line && !line.startsWith("#"));
|
||||
}
|
||||
|
||||
function normalizeRel(path) {
|
||||
const parts = [];
|
||||
for (const part of path.split("/")) {
|
||||
if (!part || part === ".") {
|
||||
continue;
|
||||
}
|
||||
if (part === "..") {
|
||||
parts.pop();
|
||||
continue;
|
||||
}
|
||||
parts.push(part);
|
||||
}
|
||||
return parts.join("/");
|
||||
}
|
||||
|
||||
function dirname(path) {
|
||||
const clean = normalizeRel(path);
|
||||
const index = clean.lastIndexOf("/");
|
||||
return index === -1 ? "" : clean.slice(0, index);
|
||||
}
|
||||
|
||||
function basename(path) {
|
||||
const clean = normalizeRel(path);
|
||||
const index = clean.lastIndexOf("/");
|
||||
return index === -1 ? clean : clean.slice(index + 1);
|
||||
}
|
||||
|
||||
function stripIniComment(line) {
|
||||
const hash = line.indexOf("#");
|
||||
const semicolon = line.indexOf(";");
|
||||
const indexes = [hash, semicolon].filter((index) => index >= 0);
|
||||
if (indexes.length === 0) {
|
||||
return line;
|
||||
}
|
||||
return line.slice(0, Math.min(...indexes));
|
||||
}
|
||||
|
||||
function parseIni(iniText) {
|
||||
const values = new Map();
|
||||
let section = "";
|
||||
for (const rawLine of iniText.split("\n")) {
|
||||
const sectionMatch = rawLine.match(/^\s*\[([^\]]+)\]/);
|
||||
if (sectionMatch) {
|
||||
section = sectionMatch[1].trim().toUpperCase();
|
||||
continue;
|
||||
}
|
||||
|
||||
const line = stripIniComment(rawLine);
|
||||
const equals = line.indexOf("=");
|
||||
if (equals === -1 || !section) {
|
||||
continue;
|
||||
}
|
||||
const key = line.slice(0, equals).trim().toUpperCase();
|
||||
const value = line.slice(equals + 1).trim();
|
||||
if (!key || !value) {
|
||||
continue;
|
||||
}
|
||||
const mapKey = `${section}.${key}`;
|
||||
const existing = values.get(mapKey) ?? [];
|
||||
existing.push(value);
|
||||
values.set(mapKey, existing);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
function firstIniValue(values, section, key) {
|
||||
return values.get(`${section.toUpperCase()}.${key.toUpperCase()}`)?.[0] ?? null;
|
||||
}
|
||||
|
||||
function allIniValues(values, section, key) {
|
||||
return values.get(`${section.toUpperCase()}.${key.toUpperCase()}`) ?? [];
|
||||
}
|
||||
|
||||
function splitSearchPath(value) {
|
||||
return value.split(":").map((entry) => entry.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
function sourceRelFor(machineRel, path) {
|
||||
return normalizeRel(`configs/sim/${machineRel}/${path}`);
|
||||
}
|
||||
|
||||
function targetPathFor(wasmDir, targetRel) {
|
||||
return `${wasmDir}/${normalizeRel(targetRel)}`;
|
||||
}
|
||||
|
||||
function isUserMCodePath(path) {
|
||||
return /^M1\d\d$/i.test(basename(path));
|
||||
}
|
||||
|
||||
function isSubroutinePath(path) {
|
||||
return path.toLowerCase().endsWith(".ngc");
|
||||
}
|
||||
|
||||
function findUpwardByBasename(manifestSet, sourceDir, fileName) {
|
||||
let current = sourceDir;
|
||||
while (current.startsWith("configs/sim")) {
|
||||
const candidate = normalizeRel(`${current}/${fileName}`);
|
||||
if (manifestSet.has(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
if (current === "configs/sim") {
|
||||
break;
|
||||
}
|
||||
current = dirname(current);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function sourceForReference(manifestSet, sourceDir, reference) {
|
||||
if (!reference || reference.startsWith("/")) {
|
||||
return null;
|
||||
}
|
||||
const sourceRel = normalizeRel(`${sourceDir}/${reference}`);
|
||||
if (manifestSet.has(sourceRel)) {
|
||||
return sourceRel;
|
||||
}
|
||||
return findUpwardByBasename(manifestSet, sourceDir, basename(reference));
|
||||
}
|
||||
|
||||
function addPlannedFile(plan, manifestSet, sourceRel, targetRel, options = {}) {
|
||||
if (!sourceRel || !manifestSet.has(sourceRel)) {
|
||||
if (options.required) {
|
||||
throw new Error(`missing vendored sim-config file: ${sourceRel}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const existing = plan.get(sourceRel);
|
||||
const executable = options.executable || isUserMCodePath(sourceRel);
|
||||
if (existing) {
|
||||
existing.executable ||= executable;
|
||||
return;
|
||||
}
|
||||
|
||||
plan.set(sourceRel, {
|
||||
sourceRel,
|
||||
path: targetPathFor(options.wasmDir, targetRel),
|
||||
executable,
|
||||
});
|
||||
}
|
||||
|
||||
function addDirectoryFiles(plan, manifestEntries, sourceDir, targetDir, predicate, options) {
|
||||
const prefix = sourceDir ? `${sourceDir}/` : "";
|
||||
for (const sourceRel of manifestEntries) {
|
||||
if (!sourceRel.startsWith(prefix)) {
|
||||
continue;
|
||||
}
|
||||
const childRel = sourceRel.slice(prefix.length);
|
||||
if (childRel.includes("/") || !predicate(sourceRel)) {
|
||||
continue;
|
||||
}
|
||||
addPlannedFile(plan, new Set(manifestEntries), sourceRel, normalizeRel(`${targetDir}/${childRel}`), options);
|
||||
}
|
||||
}
|
||||
|
||||
function remapNgcNames(iniValues) {
|
||||
const names = [];
|
||||
for (const remap of allIniValues(iniValues, "RS274NGC", "REMAP")) {
|
||||
const match = remap.match(/(?:^|\s)ngc=([^\s]+)/i);
|
||||
if (match) {
|
||||
names.push(`${match[1]}.ngc`);
|
||||
}
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
export function planSimConfigStaging({
|
||||
manifestText,
|
||||
machineRel,
|
||||
iniFile,
|
||||
iniText,
|
||||
programFile = null,
|
||||
wasmDir = `/work/sim/${machineRel}`,
|
||||
}) {
|
||||
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 programReference = programFile ?? firstIniValue(iniValues, "DISPLAY", "OPEN_FILE");
|
||||
|
||||
addPlannedFile(plan, manifestSet, iniSourceRel, iniFile, { wasmDir, required: true });
|
||||
if (programReference) {
|
||||
const programSourceRel = sourceForReference(manifestSet, sourceDir, programReference);
|
||||
addPlannedFile(plan, manifestSet, programSourceRel, programReference, {
|
||||
wasmDir,
|
||||
required: true,
|
||||
});
|
||||
}
|
||||
|
||||
for (const [section, key] of [
|
||||
["EMCIO", "TOOL_TABLE"],
|
||||
["RS274NGC", "PARAMETER_FILE"],
|
||||
]) {
|
||||
const reference = firstIniValue(iniValues, section, key);
|
||||
if (!reference) {
|
||||
continue;
|
||||
}
|
||||
const sourceRel = sourceForReference(manifestSet, sourceDir, reference);
|
||||
addPlannedFile(plan, manifestSet, sourceRel, reference, { wasmDir });
|
||||
}
|
||||
|
||||
const subroutineDirs = splitSearchPath(
|
||||
firstIniValue(iniValues, "RS274NGC", "SUBROUTINE_PATH") ?? "",
|
||||
);
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
for (const dirEntry of splitSearchPath(firstIniValue(iniValues, "RS274NGC", "USER_M_PATH") ?? "")) {
|
||||
if (dirEntry.startsWith("/")) {
|
||||
continue;
|
||||
}
|
||||
const sourceUserMDir = normalizeRel(`${sourceDir}/${dirEntry}`);
|
||||
const targetUserMDir = normalizeRel(dirEntry);
|
||||
addDirectoryFiles(plan, manifestEntries, 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);
|
||||
addPlannedFile(plan, manifestSet, sourceRel, normalizeRel(`${dirEntry}/${remapName}`), {
|
||||
wasmDir,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
wasmDir,
|
||||
iniPath: targetPathFor(wasmDir, iniFile),
|
||||
programPath: programReference ? targetPathFor(wasmDir, programReference) : null,
|
||||
files: [...plan.values()],
|
||||
};
|
||||
}
|
||||
@@ -179,6 +179,52 @@ 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);
|
||||
const motionByLine = new Map();
|
||||
|
||||
for (const line of resultText.split("\n")) {
|
||||
const motion = line.match(/^canon_event=(STRAIGHT_TRAVERSE|STRAIGHT_FEED|ARC_FEED)\b/);
|
||||
if (!motion) {
|
||||
continue;
|
||||
}
|
||||
const sourceLine = readCanonicalNumber(line, "line");
|
||||
if (Number.isFinite(sourceLine)) {
|
||||
motionByLine.set(sourceLine, motion[1]);
|
||||
}
|
||||
}
|
||||
|
||||
for (const line of resultText.split("\n")) {
|
||||
if (!line.startsWith("run_step phase=execute ")) {
|
||||
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");
|
||||
const statementIndex = line.indexOf(" statement_uri=");
|
||||
const statementValue =
|
||||
statementIndex >= 0 ? line.slice(statementIndex + " statement_uri=".length).trim() : "";
|
||||
let statement = "";
|
||||
try {
|
||||
statement = statementValue ? decodeURIComponent(statementValue) : "";
|
||||
} catch {
|
||||
statement = statementValue;
|
||||
}
|
||||
snapshots.push({
|
||||
type: motionByLine.get(sourceLine) ?? "EXECUTE",
|
||||
line: sourceLine,
|
||||
statement: statement || (Number.isFinite(sourceLine) ? (sourceLines.get(sourceLine) ?? "-") : "-"),
|
||||
axes: { ...axes },
|
||||
});
|
||||
}
|
||||
|
||||
if (snapshots.length > 0) {
|
||||
return snapshots;
|
||||
}
|
||||
|
||||
for (const line of resultText.split("\n")) {
|
||||
const motion = line.match(/^canon_event=(STRAIGHT_TRAVERSE|STRAIGHT_FEED|ARC_FEED)\b/);
|
||||
|
||||
Reference in New Issue
Block a user