按规划继续工作

结论:补齐 LinuxCNC tooldata_common.cc 随机换刀器 load/save 验证边界,WASM/SDK/OPFS 只透传 random-toolchanger 配置;native、WASM、OPFS 和浏览器 smoke 验证已通过。
This commit is contained in:
2026-06-08 08:39:26 +08:00
parent a16f3605fa
commit 3f4eb06b47
12 changed files with 211 additions and 28 deletions

View File

@@ -243,17 +243,21 @@ restore/save behavior through the exported C ABI, including out-of-order file
rejection, missing-file success, required numeric parameter writeback, removal
of named-parameter-only lines from saved output, and the `.bak` backup produced
by vendored `save_parameters()`. It also covers LinuxCNC tool-table
load/save behavior through vendored `tooldata_common.cc`, with the SDK only
copying text into the Emscripten filesystem and calling the exported C ABI.
load/save behavior through vendored `tooldata_common.cc`, including the
non-random and random-toolchanger `tooldata_init()` branches, with the SDK
only copying text into the Emscripten filesystem and calling the exported C
ABI.
OPFS validation covers the JavaScript host-boundary adapter, the INI browser
smoke harness, the INI panel UI's machine-session load and G-code run buttons,
the raw canonical-event display fed directly by LinuxCNC interpreter WASM
output, the Node parameter/tool-table bridges that copy OPFS text through the
SDK into vendored LinuxCNC file APIs, the Node machine-session bridge that
groups INI, parameter, and tool-table loading, and a browser interpreter smoke
that uses the same session bridge before saving OPFS-backed parameter and
tool-table text through vendored LinuxCNC file APIs. Full browser coverage,
full SDK coverage, and richer machine-state validation remain future work.
groups INI, parameter, and tool-table loading, the random-toolchanger flag as
a host boundary passed through to the LinuxCNC-backed tool-table ABI, and a
browser interpreter smoke that uses the same session bridge before saving
OPFS-backed parameter and tool-table text through vendored LinuxCNC file APIs.
Full browser coverage, full SDK coverage, and richer machine-state validation
remain future work.
## WASM/Browser Fixture Matrix

View File

@@ -28,7 +28,7 @@ semantic rewrites:
| Canonical output | Canonical calls are captured as test events instead of driving hardware. |
| Feed-rate state | `SET_FEED_RATE` updates standalone canonical runtime state so vendored `convert_length_units()` can read it back through `GET_EXTERNAL_FEED_RATE()` during G20/G21 changes. |
| Tool-data reload | LinuxCNC `RELOAD_TOOLDATA` is captured as a canonical test event; browser/native tool-table reload plumbing remains a future host/runtime adapter. |
| Tool table storage | Vendored LinuxCNC `tooldata_common.cc` owns tool-table parsing and formatting; the standalone boundary only supplies the in-memory `tooldata_get()`/`tooldata_put()` callbacks and exposes load/save through the WASM C ABI. |
| Tool table storage | Vendored LinuxCNC `tooldata_common.cc` owns tool-table parsing and formatting; the standalone boundary only supplies the in-memory `tooldata_get()`/`tooldata_put()` callbacks, exposes load/save through the WASM C ABI, and forwards the host random-toolchanger flag to LinuxCNC `tooldata_init()`. |
| Tool slot status | Standalone tool adapter state supplies `GET_EXTERNAL_TOOL_SLOT()` and `GET_EXTERNAL_SELECTED_TOOL_SLOT()` for vendored `Interp::synch()`; `linuxcnc_interp_init_harness` guards current/selected slot synchronization. |
| Interpreter state tags | LinuxCNC `UPDATE_TAG` callbacks are captured from the state tags packed by vendored interpreter code; standalone code does not derive modal state. |
| Rotary indexer lock state | LinuxCNC `UNLOCK_ROTARY` and `LOCK_ROTARY` callbacks are captured from the vendored single-axis indexer path; standalone code does not implement rotary-indexing semantics. |

View File

@@ -47,7 +47,7 @@ Current validation is intentionally mechanical:
| RS274 interpreter state and parser | `src/emc/rs274ngc/modal_state.*`, `interp_internal.*`, `interp_read.cc`, `interp_check.cc`, `interp_execute.cc`, `interp_find.cc`, `interp_array.cc`, `interp_queue.*`, `rs274ngc*`, `units.h` | Copy unchanged | Python/remap/runtime edges are isolated in standalone wrappers and shims; parser and execution logic remain LinuxCNC source | Vendor byte sync, per-file source probes, interpreter harness fixtures |
| RS274 conversion semantics | `src/emc/rs274ngc/interp_convert.cc`, `interp_arc.cc`, `interp_inverse.cc`, `interp_cycles.cc`, `interp_g7x.cc`, `interp_o_word.cc`, `interp_write.cc` | Copy unchanged | Canonical calls are captured by standalone event sink functions; conversion behavior stays in vendored LinuxCNC files; feed-rate state is read back through the canonical runtime boundary during length-unit conversion | Vendor byte sync, per-file source probes, canonical fixture suite, no standalone `Interp::convert_g()` guard |
| Named parameters and tool slot status | `src/emc/rs274ngc/interp_namedparams.cc`, related interpreter headers | Copy unchanged | `_ini[...]` and `_hal[...]` resolve through standalone INI/HAL adapters while lookup order stays LinuxCNC-derived; current/selected tool slot reads for vendored `Interp::synch()` come from the standalone tool adapter | Vendor byte sync, source probe, `linuxcnc_namedparam_harness`, `linuxcnc_interp_init_harness` |
| Tool table parsing and formatting | `src/emc/tooldata/tooldata_common.cc` | Copy unchanged plus standalone storage callbacks | Native file parsing/formatting stays in vendored LinuxCNC source; the standalone tool adapter supplies `tooldata_get()`/`tooldata_put()` storage and index lookup, while OPFS remains a host-side persistence boundary | Vendor byte sync, `linuxcnc_tooldata_common_source_probe`, `tests/wasm/node/verify_interp_wasm.sh` load/save tool-table assertions |
| Tool table parsing and formatting | `src/emc/tooldata/tooldata_common.cc` | Copy unchanged plus standalone storage callbacks | Native file parsing/formatting stays in vendored LinuxCNC source; the standalone tool adapter supplies `tooldata_get()`/`tooldata_put()` storage and index lookup, the WASM boundary only selects the LinuxCNC non-random or random-toolchanger branch via `tooldata_init()`, and OPFS remains a host-side persistence boundary | Vendor byte sync, `linuxcnc_tooldata_common_source_probe`, `tests/wasm/node/verify_interp_wasm.sh` non-random/random load/save tool-table assertions |
| Dynamic interpreter base | `src/emc/rs274ngc/interp_base.*` | Copy unchanged | `interp_base.cc` source probe uses standalone `EMC2_HOME` compile-time path boundary for LinuxCNC dynamic interpreter lookup | Vendor byte sync, `linuxcnc_interp_base_source_probe` |
## Dependency Matrix
@@ -75,9 +75,10 @@ Current validation is intentionally mechanical:
the current interpreter-core smoke scope. The interpreter SDK is a thin
allocation, filesystem, and C ABI wrapper over vendored LinuxCNC execution
paths, including parameter-file restore/save through vendored
`rs274ngc_pre.cc` and tool-table load/save through vendored
`tooldata_common.cc`; it does not define G-code, parameter, or tool-table
semantics. Full planner/session SDK coverage remains future work.
`rs274ngc_pre.cc` and non-random/random tool-table load/save through
vendored `tooldata_common.cc`; it does not define G-code, parameter, or
tool-table semantics. Full planner/session SDK coverage remains future
work.
- OPFS persistence is connected to the INI panel through the host-side
`runtime/opfs/file-service.js` adapter. `runtime/opfs/path-model.js` now
defines paths for INI, tool table, parameter file, G-code program,

View File

@@ -172,6 +172,13 @@ void append_tool_table_state(std::ostringstream &output)
if (standalone::get_tool_entry(&tool, 2) == 0) {
append_tool_entry(output, "tool_2", tool);
}
for (int idx = 0; idx < CANON_POCKETS_MAX; ++idx) {
if (standalone::get_tool_entry(&tool, idx) == 0 && tool.toolno != -1) {
std::ostringstream prefix;
prefix << "tool_pocket_" << idx;
append_tool_entry(output, prefix.str().c_str(), tool);
}
}
output << "tool_index_for_tool_2=" << standalone::find_tool_index_for_tool(2) << "\n";
}
@@ -196,6 +203,20 @@ void apply_parameter_assignments(Interp &interp, const char *assignments)
}
}
char *load_tool_table_with_mode(const char *path, bool random_tool_changer)
{
standalone::reset_tool_adapter();
tooldata_init(random_tool_changer);
tooldata_set_db(DB_NOTUSED);
std::ostringstream output;
output << "tooldata_random_toolchanger=" << (random_tool_changer ? 1 : 0) << "\n";
const int rc = tooldata_load(path);
output << "tooldata_load=" << rc << "\n";
append_tool_table_state(output);
return copy_result(output.str());
}
} // namespace
extern "C" {
@@ -350,15 +371,13 @@ char *lcinterp_save_parameters(const char *path, const char *assignments)
EMSCRIPTEN_KEEPALIVE
char *lcinterp_load_tool_table(const char *path)
{
standalone::reset_tool_adapter();
tooldata_init(false);
tooldata_set_db(DB_NOTUSED);
return load_tool_table_with_mode(path, false);
}
std::ostringstream output;
const int rc = tooldata_load(path);
output << "tooldata_load=" << rc << "\n";
append_tool_table_state(output);
return copy_result(output.str());
EMSCRIPTEN_KEEPALIVE
char *lcinterp_load_tool_table_random(const char *path)
{
return load_tool_table_with_mode(path, true);
}
EMSCRIPTEN_KEEPALIVE

View File

@@ -45,6 +45,7 @@ export async function loadMachineSessionFromOpfs(interp, machineId, options = {}
storage: options.storage,
opfsPath: paths.toolTableOpfsPath,
wasmPath: paths.toolTableWasmPath,
randomToolChanger: options.randomToolChanger === true,
});
return {

View File

@@ -25,7 +25,9 @@ export async function loadMachineToolTableFromOpfs(interp, machineId, options =
return {
opfsPath,
wasmPath,
result: interp.loadToolTable(wasmPath),
result: interp.loadToolTable(wasmPath, {
randomToolChanger: options.randomToolChanger === true,
}),
};
}

View File

@@ -40,7 +40,11 @@ vendored LinuxCNC RS274NGC sources and exposes:
- `runFileWithIni(path, iniPath)`
- `restoreParameters(path)`
- `saveParameters(path, values)`
- `loadToolTable(path)`
- `loadToolTable(path, options)`
- `saveToolTable(path)`
- `writeTextFile(path, text)`
- `readTextFile(path)`
`loadToolTable()` accepts `{ randomToolChanger: true }` to select the
LinuxCNC random-toolchanger branch before calling vendored
`tooldata_load()`. The SDK only forwards that runtime boundary flag.

View File

@@ -85,8 +85,11 @@ export async function createLinuxCncInterpSdk(moduleOptions = {}) {
return callStringResult(mod, "lcinterp_save_parameters", path, assignments);
},
loadToolTable(path) {
return callStringResult(mod, "lcinterp_load_tool_table", path);
loadToolTable(path, options = {}) {
const functionName = options.randomToolChanger
? "lcinterp_load_tool_table_random"
: "lcinterp_load_tool_table";
return callStringResult(mod, functionName, path);
},
saveToolTable(path) {

View File

@@ -235,6 +235,72 @@
].join("\n"),
);
await saveMachineTextFiles("browser-random-toolchanger", {
ini: "[EMC]\nMACHINE = browser-random-toolchanger\n",
toolTable: [
"T2 P7 Z3.125 D1.5 I12 J34 Q4 ;browser random finish tool",
"T5 P9 X1 Y2 Z3 ;browser random rough tool",
"",
].join("\n"),
parameters: [
"5161 10.5",
"5162 20.25",
"5220 1",
"5221 2.25",
"5399 44",
"",
].join("\n"),
});
const loadedRandomSession = await loadMachineSessionFromOpfs(
interp,
"browser-random-toolchanger",
{
iniWasmPath: "/work/browser-random-machine.ini",
parameterWasmPath: "/work/browser-random-linuxcnc.var",
toolTableWasmPath: "/work/browser-random-tool.tbl",
randomToolChanger: true,
},
);
verifyExpectedOutput(
"opfs_load_random_tool_table",
loadedRandomSession.toolTable.result,
[
"tooldata_random_toolchanger=1",
"tooldata_load=0",
"tool_pocket_7.toolno=2",
"tool_pocket_7.pocketno=7",
"tool_pocket_7.z=3.125",
"tool_pocket_7.diameter=1.5",
"tool_pocket_7.frontangle=12",
"tool_pocket_7.backangle=34",
"tool_pocket_7.orientation=4",
"tool_pocket_7.comment=browser random finish tool",
"tool_pocket_9.toolno=5",
"tool_pocket_9.pocketno=9",
"tool_index_for_tool_2=7",
].join("\n"),
);
await saveMachineToolTableToOpfs(
interp,
"browser-random-toolchanger",
{ wasmPath: "/work/browser-random-tool.tbl" },
);
const savedRandomMachineFiles = await loadMachineTextFiles(
"browser-random-toolchanger",
);
verifyExpectedOutput(
"opfs_saved_random_tool_table_text",
savedRandomMachineFiles.toolTable,
[
"T2",
"P7",
";browser random finish tool",
"T5",
"P9",
";browser random rough tool",
].join("\n"),
);
status.textContent = "browser_interp_smoke=ok";
} catch (error) {
status.textContent = `browser_interp_smoke=fail ${error.stack || error.message}`;

View File

@@ -183,8 +183,13 @@ const bridgeInterp = {
bridgeFiles.set(`${path}.bak`, "5161 0.0\n5162 0.0\n");
return `save_parameters=0\nsave_path=${path}\nvalues=${Object.keys(values).join(",")}\n`;
},
loadToolTable(path) {
return `tooldata_load=0\nload_tool_path=${path}\n`;
loadToolTable(path, options = {}) {
return [
"tooldata_load=0",
`load_tool_path=${path}`,
`random_toolchanger=${options.randomToolChanger === true ? 1 : 0}`,
"",
].join("\n");
},
saveToolTable(path) {
bridgeFiles.set(path, "T2 P2 Z+1.250000 D+0.250000 ;loaded by LinuxCNC\n");
@@ -230,10 +235,22 @@ const loadedToolTable = await loadMachineToolTableFromOpfs(
assert.deepEqual(loadedToolTable, {
opfsPath: "linuxcnc/machines/xyzab-tdr/tool.tbl",
wasmPath: "/work/bridge-tool.tbl",
result: "tooldata_load=0\nload_tool_path=/work/bridge-tool.tbl\n",
result: "tooldata_load=0\nload_tool_path=/work/bridge-tool.tbl\nrandom_toolchanger=0\n",
});
assert.equal(bridgeFiles.get("/work/bridge-tool.tbl"), "T0 P0 ; no tool\nT2 P2 Z1.25 D0.25\n");
const loadedRandomToolTable = await loadMachineToolTableFromOpfs(
bridgeInterp,
"xyzab-tdr",
{ storage, wasmPath: "/work/random-bridge-tool.tbl", randomToolChanger: true },
);
assert.deepEqual(loadedRandomToolTable, {
opfsPath: "linuxcnc/machines/xyzab-tdr/tool.tbl",
wasmPath: "/work/random-bridge-tool.tbl",
result:
"tooldata_load=0\nload_tool_path=/work/random-bridge-tool.tbl\nrandom_toolchanger=1\n",
});
const savedToolTable = await saveMachineToolTableToOpfs(
bridgeInterp,
"xyzab-tdr",
@@ -271,7 +288,7 @@ assert.deepEqual(loadedSession, {
toolTable: {
opfsPath: "linuxcnc/machines/xyzab-tdr/tool.tbl",
wasmPath: "/work/session-tool.tbl",
result: "tooldata_load=0\nload_tool_path=/work/session-tool.tbl\n",
result: "tooldata_load=0\nload_tool_path=/work/session-tool.tbl\nrandom_toolchanger=0\n",
},
});
assert.equal(bridgeFiles.get("/work/session.ini"), "[EMC]\nMACHINE = xyzab-tdr\n");
@@ -281,6 +298,22 @@ assert.equal(
"T2 P2 Z+1.250000 D+0.250000 ;loaded by LinuxCNC\n",
);
const loadedRandomSession = await loadMachineSessionFromOpfs(
bridgeInterp,
"xyzab-tdr",
{
storage,
iniWasmPath: "/work/random-session.ini",
parameterWasmPath: "/work/random-session.var",
toolTableWasmPath: "/work/random-session-tool.tbl",
randomToolChanger: true,
},
);
assert.equal(
loadedRandomSession.toolTable.result,
"tooldata_load=0\nload_tool_path=/work/random-session-tool.tbl\nrandom_toolchanger=1\n",
);
await assert.rejects(
() => loadTextFile("linuxcnc/machines/missing.ini", storage),
/missing file/,

View File

@@ -256,4 +256,54 @@ assert.equal(savedToolTable.includes(";finish tool"), true);
assert.equal(savedToolTable.includes("T5"), true);
assert.equal(savedToolTable.includes("P9"), true);
assert.equal(savedToolTable.includes(";rough tool"), true);
const randomToolTablePath = "/work/random-tool.tbl";
interp.writeTextFile(
randomToolTablePath,
[
"T2 P7 Z3.125 D1.5 I12 J34 Q4 ;random finish tool",
"T5 P9 X1 Y2 Z3 ;random rough tool",
"",
].join("\n"),
);
verifyExpectedOutput(
"load_tool_table_random",
interp.loadToolTable(randomToolTablePath, { randomToolChanger: true }),
[
"tooldata_random_toolchanger=1",
"tooldata_load=0",
"tooldata_last_index=9",
"tool_1.toolno=-1",
"tool_2.toolno=-1",
"tool_pocket_7.toolno=2",
"tool_pocket_7.pocketno=7",
"tool_pocket_7.z=3.125",
"tool_pocket_7.diameter=1.5",
"tool_pocket_7.frontangle=12",
"tool_pocket_7.backangle=34",
"tool_pocket_7.orientation=4",
"tool_pocket_7.comment=random finish tool",
"tool_pocket_9.toolno=5",
"tool_pocket_9.pocketno=9",
"tool_pocket_9.z=3",
"tool_pocket_9.comment=random rough tool",
"tool_index_for_tool_2=7",
].join("\n"),
);
verifyExpectedOutput(
"save_tool_table_random",
interp.saveToolTable(randomToolTablePath),
[
"tooldata_save=0",
"tool_pocket_7.toolno=2",
"tool_pocket_9.toolno=5",
].join("\n"),
);
const savedRandomToolTable = interp.readTextFile(randomToolTablePath);
assert.equal(savedRandomToolTable.includes("T2"), true);
assert.equal(savedRandomToolTable.includes("P7"), true);
assert.equal(savedRandomToolTable.includes(";random finish tool"), true);
assert.equal(savedRandomToolTable.includes("T5"), true);
assert.equal(savedRandomToolTable.includes("P9"), true);
assert.equal(savedRandomToolTable.includes(";random rough tool"), true);
console.log("interp_wasm_node_smoke=ok");

View File

@@ -96,5 +96,5 @@ link_wasm_module \
-s STACK_SIZE=2MB \
-s NO_EXIT_RUNTIME=1 \
-s FORCE_FILESYSTEM=1 \
-s EXPORTED_FUNCTIONS='["_malloc","_free","_lcinterp_run_program","_lcinterp_run_program_with_ini","_lcinterp_run_file","_lcinterp_run_file_with_ini","_lcinterp_restore_parameters","_lcinterp_save_parameters","_lcinterp_load_tool_table","_lcinterp_save_tool_table","_lcinterp_free_string"]' \
-s EXPORTED_FUNCTIONS='["_malloc","_free","_lcinterp_run_program","_lcinterp_run_program_with_ini","_lcinterp_run_file","_lcinterp_run_file_with_ini","_lcinterp_restore_parameters","_lcinterp_save_parameters","_lcinterp_load_tool_table","_lcinterp_load_tool_table_random","_lcinterp_save_tool_table","_lcinterp_free_string"]' \
-s EXPORTED_RUNTIME_METHODS='["FS","UTF8ToString","stringToUTF8","lengthBytesUTF8"]'