Compare commits

...

40 Commits

Author SHA1 Message Date
27d2316f78 推进 INI 面板 control view renderer helper 2026-06-14 22:52:21 +08:00
74617cc3fa 推进 INI 面板 control view DOM consumer 2026-06-14 22:45:45 +08:00
4133d202f0 推进 INI 面板 control view helper 2026-06-14 22:40:47 +08:00
e6f55d775f 推进 INI 面板状态 bundle helper 2026-06-14 22:34:38 +08:00
f537a8fa42 推进 INI 面板 workflow status helper 2026-06-14 22:28:44 +08:00
a1c34e454e 收敛 INI 面板状态 API namespace 2026-06-14 22:23:02 +08:00
25268404cb 推进 INI 面板状态 report JSON 导出 helper 2026-06-14 22:18:26 +08:00
404cb1344c 接入 INI 面板浏览器状态 report getter 2026-06-14 22:10:57 +08:00
5b45491e55 补充控制网页界面 UI roadmap 收敛建议 2026-06-14 22:00:11 +08:00
e80ecd4b7a 推进 INI 面板 machine session state report helper 2026-06-14 21:53:36 +08:00
7d57bce5de 推进 INI 面板 machine session state snapshot helper 2026-06-14 21:48:43 +08:00
75818c1379 推进 INI 面板 run G-code workflow helper 2026-06-14 21:38:37 +08:00
8e8a319d78 推进 INI 面板 restore/load session workflow helper 2026-06-14 21:30:19 +08:00
ddc9a4e0f3 新增 text11 实质性推进接续计划 2026-06-14 21:23:37 +08:00
4e4a9c667f 推进 INI 面板 session snapshot workflow helper 2026-06-14 21:21:17 +08:00
48a0f61b4a 收拢 browser explicit session readback path helper 2026-06-14 21:05:09 +08:00
b85444b858 推进 INI 面板状态汇总 helper 2026-06-14 20:59:50 +08:00
405cc0b011 推进 INI 面板 machine file helper 文档 2026-06-14 18:45:05 +08:00
a60826bfce 推进 INI 面板 machine file summary helper 2026-06-14 18:39:27 +08:00
083b117c21 推进 UI helper docs 索引 2026-06-14 18:33:33 +08:00
3ca213a889 推进 INI 面板 session load summary helper 2026-06-14 18:29:49 +08:00
aeb290b885 推进 INI 面板 session summary helper 2026-06-14 18:22:57 +08:00
b2b8bd6b4f 推进 UI run summary 文档一致性 guard 2026-06-14 18:10:02 +08:00
b989de23ae 推进 UI run summary helper 文档 2026-06-14 18:01:34 +08:00
56322115f9 推进 UI node smoke wrapper 2026-06-14 17:57:22 +08:00
4ac2f6fbf6 推进 INI 面板运行结果摘要 helper 2026-06-14 17:54:02 +08:00
8137cce488 推进 INI 面板运行摘要 helper 2026-06-14 17:44:20 +08:00
ccd6b67e35 推进 INI 面板运行摘要结果计数 2026-06-14 17:35:14 +08:00
f96329d67f 推进 INI 面板运行摘要状态 2026-06-14 17:31:33 +08:00
6242b415ff 推进 INI 面板运行快照上下文日志 2026-06-14 17:12:14 +08:00
0c0da13681 推进 INI 面板快照 metadata 日志 2026-06-14 16:43:06 +08:00
5cd5cb6869 推进 INI 面板快照 metadata 描述 2026-06-14 16:28:25 +08:00
69de3305c9 推进 INI 面板 UI 会话描述 helper 2026-06-14 16:18:08 +08:00
d40a6422e8 推进 OPFS G-code 程序路径 helper 2026-06-14 15:57:35 +08:00
a158f66071 推进 INI 面板 G-code 回退来源 guard 2026-06-14 15:39:18 +08:00
f204b7f113 推进 INI 面板快照 G-code 运行选择 2026-06-14 15:33:33 +08:00
633b8fd0ed 推进 INI 面板会话恢复加载流程 2026-06-14 15:24:26 +08:00
558960b2f2 推进 INI 面板会话快照工作流 2026-06-14 15:17:06 +08:00
37daf58796 推进 OPFS 机器会话快照加载入口 2026-06-14 15:09:26 +08:00
a829984300 推进 OPFS 机器会话快照保存入口 2026-06-14 15:03:33 +08:00
36 changed files with 6474 additions and 55 deletions

1634
text10.txt

File diff suppressed because it is too large Load Diff

1145
text11.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -60,6 +60,19 @@ The current OPFS host-boundary validation command is:
wasm-port/tests/opfs/node/verify_file_service.sh
```
The current UI helper Node smoke validation command is:
```bash
wasm-port/tests/ui/node/verify_ui_node_smokes.sh
```
UI helper docs:
- `wasm-port/docs/ui-run-summary-helpers.md`
- `wasm-port/docs/ui-session-summary-helpers.md`
- `wasm-port/docs/ui-machine-file-helpers.md`
- `wasm-port/docs/ui-panel-state-summary.md`
The current browser smoke validation command is:
```bash
@@ -102,6 +115,7 @@ wasm-port/tests/wasm/node/verify_sim_configs_inventory_wasm.sh
wasm-port/tests/wasm/node/verify_nc_files_wasm.sh
wasm-port/tests/wasm/node/verify_tp_wasm.sh
wasm-port/tests/opfs/node/verify_file_service.sh
wasm-port/tests/ui/node/verify_ui_node_smokes.sh
wasm-port/tests/browser/verify_ini_panel_browser.sh
wasm-port/tests/browser/verify_interp_browser.sh
wasm-port/tests/host/verify_host_smokes.sh

View File

@@ -0,0 +1,68 @@
# UI Machine File Helpers
## Purpose
`runtime/ui/ini-panel/machine-file-summary.js` contains small host-boundary
helpers for summarizing machine text file save/load actions. These helpers are
pure so browser UI panels, SDK-facing wrappers, and Node smoke tests can share
the same data shape without parsing log text.
The helpers do not implement CNC behavior. They only describe OPFS path mapping
and captured text lengths for machine files staged by the UI.
## `createMachineFileSaveSummary(paths, gcodePath)`
`paths` is the object returned by `saveMachineTextFiles(...)`.
`gcodePath` is the OPFS path returned by `saveGcodeProgram(...)`.
The returned summary has this shape:
```js
{
iniOpfsPath: "linuxcnc/machines/xyzab-tdr/machine.ini",
toolTableOpfsPath: "linuxcnc/machines/xyzab-tdr/tool.tbl",
parameterOpfsPath: "linuxcnc/machines/xyzab-tdr/linuxcnc.var",
gcodeOpfsPath: "linuxcnc/gcode/ui-session.ngc"
}
```
## `createMachineFileLoadSummary(files)`
`files` is the object returned by `loadMachineTextFiles(...)`.
The returned summary has this shape:
```js
{
iniBytes: 26,
toolTableBytes: 13,
parameterBytes: 7
}
```
## Log Helpers
- `machineFileSaveLogLines(summary)` renders the staged OPFS paths.
- `machineFileLoadLogLines(summary)` renders the loaded byte lengths.
## Boundary Rules
- Do not parse G-code text in these helpers.
- Do not infer tool, parameter, kinematics, planner, or modal semantics.
- Do not derive new CNC behavior from the staged file names or lengths.
- Keep new fields limited to host/runtime boundary facts already produced by
the LinuxCNC-owned runtime or UI staging layer.
## Validation
Run the focused Node smoke after changing these helpers:
```bash
wasm-port/tests/ui/node/verify_ini_panel_machine_file_summary.sh
```
The aggregate UI Node smoke is:
```bash
wasm-port/tests/ui/node/verify_ui_node_smokes.sh
```

View File

@@ -0,0 +1,201 @@
# UI Panel State Summary
## Purpose
`runtime/ui/ini-panel/panel-state-summary.js` contains a pure helper for
bundling the already-observed host/runtime boundary state of the INI panel into
a single machine-readable snapshot.
The helper does not implement CNC behavior. It only packages the summary
objects produced by the run/session/machine-file helpers and the visible badge
state so browser tests or future UI consumers can compare panel state without
reading ad hoc DOM text.
Upper-level UI/API consumers should read `createMachineSessionStateReport(...)`
or `window.linuxCncIniPanelApi.getMachineSessionStateReport()` rather than
scraping DOM nodes or depending on `window.linuxCncIniPanelMachineSessionState`
directly. The legacy `window.getMachineSessionStateReport()` and
`window.getMachineSessionStateReportExport()` globals remain as compatibility
shims.
For control-page polling, use
`window.linuxCncIniPanelApi.getMachineSessionWorkflowStatus()`. It returns a
smaller workflow-facing object derived from the same report data.
For a single read call, use `window.linuxCncIniPanelApi.getMachineSessionStateBundle()`.
For direct rendering, use `window.linuxCncIniPanelApi.getMachineSessionControlView()`.
The panel also exposes
`window.linuxCncIniPanelApi.renderMachineSessionControlView(container)`, which
renders the current view into a supplied DOM container.
## `createIniPanelStateSummary(input)`
`input` may contain:
```js
{
badges: { ini: "INI WASM: ready", interp: "Interpreter WASM: ready", opfs: "OPFS: ready" },
machine: { machine: "xyzab-tdr", kinematics: "trivkins" },
machineFiles: { iniOpfsPath: "...", toolTableOpfsPath: "...", parameterOpfsPath: "...", gcodeOpfsPath: "..." },
sessionSnapshot: { snapshotLabel: "...", workflow: "save-session-snapshot" },
sessionLoad: { iniOpfsPath: "...", iniWasmPath: "..." },
run: { runStatus: "ok", programSource: "snapshot" },
fiveaxisRemap: "ok"
}
```
The returned snapshot preserves those nested objects and normalizes missing
badge entries to `null`.
## `createMachineSessionStateSnapshot(input)`
This helper builds the richer state snapshot used by the INI panel and exposed
as `window.linuxCncIniPanelMachineSessionState`.
It preserves the base panel state and adds:
```js
{
selectedProgram: { source: "snapshot", opfsPath: "...", wasmPath: "..." },
fields: {
sessionIni: "/work/session-machine.ini",
sessionParameters: "/work/session-linuxcnc.var",
sessionToolTable: "/work/session-tool.tbl",
sessionGcode: "linuxcnc/gcode/ui-session.ngc",
sessionSnapshot: "ui-machine-session/ui-machine-session.json",
runStatus: "ok",
runSource: "snapshot",
runSnapshot: "ui-machine-session/ui-machine-session.json",
runWorkflow: "save-session-snapshot",
runOpfsPath: "linuxcnc/gcode/ui-session.ngc",
runWasmPath: "/work/ui-session.ngc"
},
lastAction: "run-gcode",
error: null
}
```
The INI panel updates this state after save, restore, load, run, and error
paths so browser tests and future API consumers can read one machine-readable
state object instead of collecting DOM fields and log text.
## `createMachineSessionStateReport(state)`
This helper converts the richer state snapshot into a stable consumer report
with four sections:
```js
{
readiness: { ini: "...", interp: "...", opfs: "..." },
session: { snapshotLabel: "...", workflow: "...", ini: "...", parameters: "...", toolTable: "...", gcode: "..." },
loaded: { iniWasmPath: "...", parameterWasmPath: "...", toolTableWasmPath: "..." },
program: { source: "snapshot", opfsPath: "...", wasmPath: "..." },
run: { status: "ok", canonicalEventCount: 2, motionSnapshotCount: 2, iniWasmPath: "..." },
status: { lastAction: "run-gcode", error: null, fiveaxisRemap: "ok" }
}
```
The report is intended for upper-level UI consumers or export paths that want a
stable summary shape without touching DOM nodes.
## `createMachineSessionStateReportExport(state, options)`
This helper wraps the report in a stable export payload:
```js
{
filename: "machine-session-state-report.json",
mediaType: "application/json",
report: { ... },
text: "{\n ...\n}\n"
}
```
It is a small export-layer helper for future copy/download/UI SDK wrappers.
## `createMachineSessionWorkflowStatus(state)`
This helper derives a lightweight status object for upper-level control pages:
```js
{
readiness: { ini: "...", interp: "...", opfs: "..." },
lastAction: "run-gcode",
error: null,
session: { snapshotLabel: "...", workflow: "...", gcode: "..." },
run: { status: "ok", canonicalEventCount: 2, motionSnapshotCount: 2, iniWasmPath: "..." }
}
```
It is intended for polling or status panels that need current UI workflow
state without parsing logs, DOM nodes, or transient window state.
## `createMachineSessionStateBundle(state, options)`
This helper packages the current report, export payload, and workflow status
into one read-only object:
```js
{
report: { ... },
export: { filename: "...", mediaType: "application/json", report: { ... }, text: "{...}\n" },
workflowStatus: { readiness: { ... }, lastAction: "...", error: null, session: { ... }, run: { ... } }
}
```
It is the preferred single-entry helper for control-page polling and other
upper-level UI consumers that want one stable read instead of stitching the
three views together themselves.
## `createMachineSessionControlView(bundle)`
This helper maps a bundle into UI-ready status and sections:
```js
{
status: { lastAction: "run-gcode", error: null, runStatus: "ok" },
sections: [
{ id: "readiness", title: "Readiness", rows: [{ label: "INI", value: "..." }] },
{ id: "session", title: "Session", rows: [{ label: "Workflow", value: "..." }] },
{ id: "run", title: "Run", rows: [{ label: "Canonical events", value: 2 }] }
]
}
```
It is a presentation mapping over the bundle only. It does not parse G-code,
canonical events, or LinuxCNC-owned runtime content.
A minimal read-only browser consumer can render the sections directly:
```js
window.linuxCncIniPanelApi.renderMachineSessionControlView(
document.getElementById("machine-session-control-view"),
);
```
Consumers should treat `rows` as display data and keep actions wired through
explicit workflow APIs instead of deriving behavior from labels.
For external pages that only have a view object, import the pure renderer:
```js
import { renderMachineSessionControlView } from "./control-view-renderer.js";
renderMachineSessionControlView(container, view);
```
## Boundary Rules
- Do not parse G-code text in this helper.
- Do not infer CNC semantics from the nested summaries.
- Keep the helper as a bundling layer over already-observed UI boundary state.
- Keep workflow result objects as host/runtime boundary facts; this helper must
not promote, reinterpret, or synthesize CNC behavior.
## Validation
The helper is covered by:
```bash
wasm-port/tests/ui/node/verify_ini_panel_state_summary.sh
wasm-port/tests/ui/node/verify_ui_node_smokes.sh
```

View File

@@ -0,0 +1,149 @@
# UI Run Summary Helpers
## Purpose
`runtime/ui/ini-panel/run-summary.js` contains small host-boundary helpers for
summarizing an INI panel G-code run.
`runtime/ui/ini-panel/run-workflow.js` contains the reusable UI workflow for
selecting the session/default G-code program, loading it from OPFS, staging it
into the interpreter WASM filesystem, invoking the LinuxCNC interpreter, and
returning run field/log state. These helpers are intentionally pure so browser
UI panels, SDK-facing wrappers, and Node smoke tests can share the same summary
shape without reading DOM text or parsing log output.
The helpers do not implement CNC behavior. G-code execution, canonical events,
tool handling, parameters, kinematics, and planner behavior remain owned by the
vendored LinuxCNC runtime.
## `createRunSummary(program, runtime, snapshot)`
`program` describes the host-selected program path mapping:
```js
{
source: "default" | "snapshot",
opfsPath: "linuxcnc/gcode/ui-session.ngc",
wasmPath: "/work/ui-session.ngc"
}
```
`runtime` describes already-observed runtime boundary results:
```js
{
runStatus: "ok",
snapshotLabel: "ui-machine-session/ui-machine-session.json",
iniWasmPath: "/work/session-machine.ini",
canonicalEventCount: 2,
motionSnapshotCount: 2
}
```
`snapshot` is the optional machine session snapshot loaded by the UI. The helper
reads only snapshot metadata such as `workflow` and `snapshotLabel`.
When `runtime.snapshotLabel` is present, it is used as the returned
`snapshotLabel`; otherwise the helper falls back to `snapshot.metadata.snapshotLabel`
and then `null`.
The returned summary has this shape:
```js
{
runStatus: "ok",
programSource: "snapshot",
snapshotLabel: "ui-machine-session/ui-machine-session.json",
workflow: "save-session-snapshot",
programOpfsPath: "linuxcnc/gcode/ui-session.ngc",
programWasmPath: "/work/ui-session.ngc",
iniWasmPath: "/work/session-machine.ini",
canonicalEventCount: 2,
motionSnapshotCount: 2
}
```
## `summarizeRunResult(resultText, snapshots)`
`resultText` is the raw text returned by the LinuxCNC interpreter WASM call.
`snapshots` is the UI's already-parsed motion snapshot list.
The helper returns:
```js
{
canonicalEventCount: 2,
motionSnapshotCount: 2
}
```
`canonicalEventCount` only counts result lines beginning with `canon_event=`.
`motionSnapshotCount` is only `snapshots.length`.
## `runGcodeSessionWorkflow(input)`
The workflow requires a loaded machine session and injected host/runtime
functions:
```js
{
interp,
loadedSession,
snapshot,
loadGcodeProgram,
gcodeFilenameFromProgramPath,
summarizeRun
}
```
It returns the selected program mapping, raw interpreter result text, run
summary, UI field state, log lines, and optional UI motion snapshots produced
by the injected `summarizeRun` callback:
```js
{
program: {
source: "snapshot",
opfsPath: "linuxcnc/gcode/ui-session.ngc",
wasmPath: "/work/ui-session.ngc",
text: "G0 X1\n"
},
resultText: "canon_event=...",
summary: { runStatus: "ok", programSource: "snapshot" },
fields: { runStatus: "ok", sessionGcode: "linuxcnc/gcode/ui-session.ngc" },
motionSnapshots: [{ line: 1 }],
logLines: ["Program: linuxcnc/gcode/ui-session.ngc -> /work/ui-session.ngc"]
}
```
`selectSessionGcodeProgram(input)` is the pure program-selection helper used by
the workflow. Invalid snapshot G-code paths fall back to the UI default program.
## Boundary Rules
- Do not parse G-code text in these helpers.
- Do not infer tool, parameter, kinematics, planner, or modal semantics.
- Do not derive new canonical events in JavaScript.
- Keep canonical-event and motion parsing in the existing UI display layer or
inject it as already-observed UI state; the workflow must not own CNC
semantics.
- Do not make UI helper output a promotion signal for runtime families blocked
by host dependencies.
- Keep new fields limited to host/runtime boundary facts already produced by
the LinuxCNC-owned runtime or UI staging layer.
## Validation
Run the focused Node smoke after changing these helpers:
```bash
wasm-port/tests/ui/node/verify_ini_panel_run_summary.sh
wasm-port/tests/ui/node/verify_ini_panel_run_workflow.sh
```
The aggregate UI Node smoke is:
```bash
wasm-port/tests/ui/node/verify_ui_node_smokes.sh
```
`wasm-port/tests/host/verify_host_smokes.sh` includes the UI Node smoke so these
helpers are also covered by the regular host/WASM/browser gate.

View File

@@ -0,0 +1,163 @@
# UI Session Summary Helpers
## Purpose
`runtime/ui/ini-panel/session-summary.js` contains small host-boundary helpers
for summarizing machine session snapshot state.
`runtime/ui/ini-panel/session-load-summary.js` contains the matching helpers
for machine-session load state. These helpers are pure so browser UI panels,
SDK-facing wrappers, and Node smoke tests can share the same data shape without
parsing log text.
`runtime/ui/ini-panel/session-workflow.js` contains the reusable UI workflows
for saving, restoring, and restore-loading a machine session snapshot as
host-boundary operations.
The helpers do not implement CNC behavior. G-code execution, canonical events,
tool handling, parameters, kinematics, and planner behavior remain owned by the
vendored LinuxCNC runtime.
## `createSessionSnapshotSummary(snapshot, fallbackLabel)`
`snapshot` is the saved machine session snapshot loaded by the UI.
`fallbackLabel` is the UI-selected label to prefer when present.
The returned summary has this shape:
```js
{
snapshotLabel: "ui-machine-session/ui-machine-session.json",
workflow: "save-session-snapshot",
defaultGcodeOpfsPath: "linuxcnc/gcode/ui-session.ngc",
iniOpfsPath: "linuxcnc/machines/xyzab-tdr/machine.ini",
parameterOpfsPath: "linuxcnc/machines/xyzab-tdr/linuxcnc.var",
toolTableOpfsPath: "linuxcnc/machines/xyzab-tdr/tool.tbl",
gcodeOpfsPath: "linuxcnc/gcode/ui-session.ngc"
}
```
## `createMachineSessionLoadSummary(session)`
`session` is the loaded interpreter/session bridge result. The helper extracts
the OPFS and WASM path mapping plus the captured parameter/tool-table result
texts.
The returned summary has this shape:
```js
{
iniOpfsPath: "linuxcnc/machines/xyzab-tdr/machine.ini",
iniWasmPath: "/work/session-machine.ini",
parameterOpfsPath: "linuxcnc/machines/xyzab-tdr/linuxcnc.var",
parameterWasmPath: "/work/session-linuxcnc.var",
parameterResult: "restore_parameters=0",
toolTableOpfsPath: "linuxcnc/machines/xyzab-tdr/tool.tbl",
toolTableWasmPath: "/work/session-tool.tbl",
toolTableResult: "tooldata_load=0"
}
```
## `saveMachineSessionSnapshotWorkflow(input)`
The workflow writes the current INI/tool/parameter text files, writes the
default G-code program, saves the machine session snapshot, and returns the
snapshot summary, field state, and log lines for UI consumers.
The persistence functions are injected by the caller:
```js
{
saveMachineTextFiles,
saveGcodeProgram,
saveMachineSessionSnapshot
}
```
## `restoreMachineSessionSnapshotWorkflow(input)`
The workflow loads the machine session snapshot, loads the persisted machine
text files so the editor can be restored, and optionally loads the machine
session into the interpreter WASM filesystem.
When `loadSession` is omitted, the returned `fields` contain the OPFS snapshot
paths. When `loadSession` is supplied, the returned `fields` contain the loaded
WASM paths for INI/parameter/tool-table files and the snapshot G-code OPFS
path.
The returned object has this shape:
```js
{
snapshot: { ... },
editorIniText: "[EMC]\n...",
summary: { snapshotLabel: "...", workflow: "save-session-snapshot" },
fields: {
sessionIni: "/work/session-machine.ini",
sessionParameters: "/work/session-linuxcnc.var",
sessionToolTable: "/work/session-tool.tbl",
sessionGcode: "linuxcnc/gcode/ui-session.ngc",
sessionSnapshot: "ui-machine-session/ui-machine-session.json"
},
loadSummary: { iniWasmPath: "/work/session-machine.ini" },
loadedSession: { ... },
logLines: [
"Snapshot: ui-machine-session/ui-machine-session.json",
"Workflow: save-session-snapshot"
]
}
```
The returned object has this shape:
```js
{
snapshot: { ... },
summary: { snapshotLabel: "...", workflow: "save-session-snapshot" },
fields: {
sessionIni: "linuxcnc/machines/xyzab-tdr/machine.ini",
sessionParameters: "linuxcnc/machines/xyzab-tdr/linuxcnc.var",
sessionToolTable: "linuxcnc/machines/xyzab-tdr/tool.tbl",
sessionGcode: "linuxcnc/gcode/ui-session.ngc",
sessionSnapshot: "ui-machine-session/ui-machine-session.json"
},
logLines: [
"Snapshot: ui-machine-session/ui-machine-session.json",
"Workflow: save-session-snapshot"
]
}
```
## Log Helpers
- `sessionSnapshotSummaryLogLines(summary)` renders snapshot label, workflow,
and default G-code path.
- `sessionSnapshotMetadataLogLines(summary)` renders the metadata subset used in
save/restore log summaries.
- `machineSessionLoadLogLines(summary)` renders the interpreter/session
OPFS-to-WASM mapping and captured result lines.
- `sessionSnapshotSaveLogLines(summary)` renders the save-workflow snapshot
paths used by the INI panel.
- `sessionSnapshotRestoreLogLines(summary)` renders the restore-workflow
snapshot paths used by the INI panel.
## Boundary Rules
- Do not parse G-code text in these helpers.
- Do not infer tool, parameter, kinematics, planner, or modal semantics.
- Do not derive new CNC behavior from the load result strings.
- Keep new fields limited to host/runtime boundary facts already produced by the
LinuxCNC-owned runtime or UI staging layer.
## Validation
Run the focused Node smoke after changing these helpers:
```bash
wasm-port/tests/ui/node/verify_ini_panel_session_summary.sh
wasm-port/tests/ui/node/verify_ini_panel_session_workflow.sh
```
The aggregate UI Node smoke is:
```bash
wasm-port/tests/ui/node/verify_ui_node_smokes.sh
```

View File

@@ -3,9 +3,12 @@ import {
gcodeProgramPath,
machineIniPath,
parameterFilePath,
splitOpfsPath,
toolTablePath,
} from "./path-model.js";
const GCODE_PROGRAM_ROOT = ["linuxcnc", "gcode"];
const MACHINE_FILE_PATHS = {
ini: machineIniPath,
toolTable: toolTablePath,
@@ -43,6 +46,18 @@ export async function loadMachineTextFiles(machineId, options = {}) {
};
}
export function gcodeFilenameFromProgramPath(path) {
const parts = splitOpfsPath(path);
if (
parts.length !== 3 ||
parts[0] !== GCODE_PROGRAM_ROOT[0] ||
parts[1] !== GCODE_PROGRAM_ROOT[1]
) {
throw new Error(`Invalid G-code program OPFS path: ${path}`);
}
return parts[2];
}
export async function saveGcodeProgram(filename, text, options = {}) {
const path = gcodeProgramPath(filename);
await saveTextFile(path, text, options.storage);

View File

@@ -27,6 +27,27 @@ function optionalOpfsPath(value, label) {
return normalizeOpfsPath(value);
}
function requiredSnapshotFilePath(files, key, label) {
const value = files[key];
if (typeof value !== "string") {
throw new Error(`${label} must be a string.`);
}
return normalizeOpfsPath(value);
}
function validateMachineSessionSnapshotPayload(payload, machineId) {
assertPlainObject(payload, "machine session snapshot payload");
if (payload.machineId !== machineId) {
throw new Error(`Machine session snapshot id mismatch: ${payload.machineId}`);
}
assertPlainObject(payload.files, "machine session snapshot files");
requiredSnapshotFilePath(payload.files, "ini", "machine session INI OPFS path");
requiredSnapshotFilePath(payload.files, "parameters", "machine session parameter OPFS path");
requiredSnapshotFilePath(payload.files, "toolTable", "machine session tool table OPFS path");
optionalOpfsPath(payload.files.gcode, "machine session G-code OPFS path");
return payload;
}
export function createMachineSessionSnapshotPayload(machineId, options = {}) {
assertPlainObject(options, "machine session snapshot options");
const ini = optionalOpfsPath(options.iniOpfsPath, "INI OPFS path") ??
@@ -89,6 +110,18 @@ export async function saveSessionSnapshot(sessionId, payload, options = {}) {
return snapshot;
}
export async function saveMachineSessionSnapshot(sessionId, machineId, options = {}) {
assertPlainObject(options, "machine session snapshot save options");
const payload = createMachineSessionSnapshotPayload(machineId, options);
return saveSessionSnapshot(sessionId, payload, options);
}
export async function loadMachineSessionSnapshot(sessionId, machineId, options = {}) {
const snapshot = await loadSessionSnapshot(sessionId, options);
validateMachineSessionSnapshotPayload(snapshot.payload, machineId);
return snapshot;
}
export async function loadSessionSnapshot(sessionId, options = {}) {
const path = sessionSnapshotPath(sessionId, options.filename);
const text = await loadTextFile(path, options.storage);

View File

@@ -8,6 +8,7 @@ import {
saveTextFile,
} from "../../opfs/file-service.js";
import {
gcodeFilenameFromProgramPath,
loadGcodeProgram,
loadMachineTextFiles,
machineFilePaths,
@@ -17,6 +18,44 @@ import {
import {
loadMachineSessionFromOpfs,
} from "../../opfs/linuxcnc-machine-session-bridge.js";
import {
loadMachineSessionSnapshot,
saveMachineSessionSnapshot,
} from "../../opfs/snapshot-store.js";
import {
createMachineFileLoadSummary,
createMachineFileSaveSummary,
machineFileLoadLogLines,
machineFileSaveLogLines,
} from "./machine-file-summary.js";
import {
summarizeRunResult,
} from "./run-summary.js";
import {
runGcodeSessionWorkflow,
} from "./run-workflow.js";
import {
createMachineSessionLoadSummary,
machineSessionLoadLogLines,
} from "./session-load-summary.js";
import {
sessionSnapshotSummaryLogLines,
} from "./session-summary.js";
import {
restoreMachineSessionSnapshotWorkflow,
saveMachineSessionSnapshotWorkflow,
} from "./session-workflow.js";
import {
createMachineSessionControlView,
createMachineSessionStateSnapshot,
createMachineSessionStateBundle,
createMachineSessionStateReport,
createMachineSessionStateReportExport,
createMachineSessionWorkflowStatus,
} from "./panel-state-summary.js";
import {
renderMachineSessionControlView,
} from "./control-view-renderer.js";
const SAMPLE_PATH =
"../../../vendor/linuxcnc/configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini";
@@ -44,6 +83,12 @@ const SESSION_WASM_FILES = {
};
const GCODE_FILENAME = "ui-session.ngc";
const GCODE_WASM_FILE = "/work/ui-session.ngc";
const UI_SESSION = {
snapshotId: "ui-machine-session",
snapshotFilename: "ui-machine-session.json",
defaultGcodeFilename: GCODE_FILENAME,
gcodeWasmPath: GCODE_WASM_FILE,
};
const DEFAULT_TOOL_TABLE = "T2 P7 Z3.125 D1.5 I12 J34 Q4 ;ui session tool\n";
const DEFAULT_PARAMETERS = [
"5161 0.0",
@@ -72,6 +117,7 @@ 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 controlViewNode = document.getElementById("machine-session-control-view");
const axisNodes = {
x: document.getElementById("axis-x"),
y: document.getElementById("axis-y"),
@@ -96,15 +142,78 @@ const fields = {
sessionParameters: document.getElementById("field-session-parameters"),
sessionToolTable: document.getElementById("field-session-tool-table"),
sessionGcode: document.getElementById("field-session-gcode"),
sessionSnapshot: document.getElementById("field-session-snapshot"),
runStatus: document.getElementById("field-run-status"),
runSource: document.getElementById("field-run-source"),
runSnapshot: document.getElementById("field-run-snapshot"),
runWorkflow: document.getElementById("field-run-workflow"),
runOpfsPath: document.getElementById("field-run-opfs-path"),
runWasmPath: document.getElementById("field-run-wasm-path"),
fiveaxisRemap: document.getElementById("field-fiveaxis-remap"),
};
let iniSdk = null;
let interpSdk = null;
let loadedSession = null;
let restoredSessionSnapshot = null;
let canonicalEventText = "";
let runPlaybackTimer = null;
let panelMachineSessionState = createMachineSessionStateSnapshot();
function getMachineSessionStateReport() {
return createMachineSessionStateReport(window.linuxCncIniPanelMachineSessionState);
}
function getMachineSessionStateReportExport() {
return createMachineSessionStateReportExport(window.linuxCncIniPanelMachineSessionState);
}
function getMachineSessionWorkflowStatus() {
return createMachineSessionWorkflowStatus(window.linuxCncIniPanelMachineSessionState);
}
function getMachineSessionStateBundle() {
return createMachineSessionStateBundle(window.linuxCncIniPanelMachineSessionState);
}
function getMachineSessionControlView() {
return createMachineSessionControlView(getMachineSessionStateBundle());
}
function renderCurrentMachineSessionControlView(container = controlViewNode) {
return renderMachineSessionControlView(container, getMachineSessionControlView());
}
function updatePanelMachineSessionState(patch = {}) {
panelMachineSessionState = createMachineSessionStateSnapshot({
...panelMachineSessionState,
...patch,
fields: {
...panelMachineSessionState.fields,
...(patch.fields ?? {}),
},
});
window.linuxCncIniPanelMachineSessionState = panelMachineSessionState;
renderCurrentMachineSessionControlView();
return panelMachineSessionState;
}
window.linuxCncIniPanelApi = {
getMachineSessionControlView,
getMachineSessionStateBundle,
getMachineSessionStateReport,
getMachineSessionStateReportExport,
getMachineSessionWorkflowStatus,
renderMachineSessionControlView: renderCurrentMachineSessionControlView,
};
window.getMachineSessionControlView = getMachineSessionControlView;
window.getMachineSessionStateBundle = getMachineSessionStateBundle;
window.getMachineSessionStateReport = getMachineSessionStateReport;
window.getMachineSessionStateReportExport = getMachineSessionStateReportExport;
window.getMachineSessionWorkflowStatus = getMachineSessionWorkflowStatus;
window.renderMachineSessionControlView = renderCurrentMachineSessionControlView;
window.linuxCncIniPanelMachineSessionState = panelMachineSessionState;
renderCurrentMachineSessionControlView();
function setBadge(node, text, className = "badge") {
node.className = className;
@@ -264,9 +373,8 @@ function showRunSnapshot(snapshot, index, total) {
);
}
function playRunMotion(resultText, programText) {
function playRunMotion(resultText, programText, snapshots = parseRunMotion(resultText, programText)) {
clearRunPlayback();
const snapshots = parseRunMotion(resultText, programText);
if (snapshots.length === 0) {
setRunMonitor(100, "100%", {}, "complete; no axis motion");
return;
@@ -303,6 +411,86 @@ function syncEditorToWasmFs() {
requireIniSdk().writeTextFile(WASM_FILE, editor.value);
}
function sessionSnapshotLabel(snapshot = UI_SESSION) {
const sessionId = typeof snapshot === "string" ? snapshot : snapshot.snapshotId ?? snapshot.sessionId;
return `${sessionId}/${UI_SESSION.snapshotFilename}`;
}
function defaultGcodeOpfsPath() {
return `linuxcnc/gcode/${UI_SESSION.defaultGcodeFilename}`;
}
function sessionSnapshotMetadata() {
return {
source: "ini-panel",
workflow: "save-session-snapshot",
snapshotLabel: sessionSnapshotLabel(UI_SESSION),
defaultGcodeOpfsPath: defaultGcodeOpfsPath(),
};
}
function clearLastRunSummary() {
window.linuxCncIniPanelLastRunSummary = null;
setField("runSource", "-");
setField("runSnapshot", "-");
setField("runWorkflow", "-");
setField("runOpfsPath", "-");
setField("runWasmPath", "-");
}
function setLastRunSummary(summary) {
window.linuxCncIniPanelLastRunSummary = summary;
setField("runSource", summary.programSource);
setField("runSnapshot", summary.snapshotLabel ?? "-");
setField("runWorkflow", summary.workflow ?? "-");
setField("runOpfsPath", summary.programOpfsPath);
setField("runWasmPath", summary.programWasmPath);
updatePanelMachineSessionState({
run: summary,
fields: {
runStatus: summary.runStatus,
runSource: summary.programSource,
runSnapshot: summary.snapshotLabel ?? "-",
runWorkflow: summary.workflow ?? "-",
runOpfsPath: summary.programOpfsPath,
runWasmPath: summary.programWasmPath,
},
});
return summary;
}
async function restoreSessionSnapshotToEditor() {
const snapshot = await loadMachineSessionSnapshot(
UI_SESSION.snapshotId,
MACHINE_ID,
{ filename: UI_SESSION.snapshotFilename },
);
const files = await loadMachineTextFiles(MACHINE_ID);
editor.value = files.ini;
setField("sessionIni", snapshot.payload.files.ini);
setField("sessionParameters", snapshot.payload.files.parameters);
setField("sessionToolTable", snapshot.payload.files.toolTable);
setField("sessionGcode", snapshot.payload.files.gcode);
setField("sessionSnapshot", sessionSnapshotLabel(snapshot.sessionId));
restoredSessionSnapshot = snapshot;
return snapshot;
}
async function loadMachineSessionIntoWasm() {
loadedSession = await loadMachineSessionFromOpfs(
requireInterpSdk(),
MACHINE_ID,
{
...SESSION_WASM_FILES,
iniSdk: requireIniSdk(),
},
);
setField("sessionIni", loadedSession.ini.wasmPath);
setField("sessionParameters", loadedSession.parameters.wasmPath);
setField("sessionToolTable", loadedSession.toolTable.wasmPath);
return loadedSession;
}
async function loadSample() {
setLog(`Fetching ${SAMPLE_PATH}`);
const response = await fetch(SAMPLE_PATH);
@@ -354,27 +542,45 @@ async function boot() {
try {
iniSdk = await createLinuxCncIniSdk();
setBadge(wasmBadge, "INI WASM: ready");
updatePanelMachineSessionState({ badges: { ...panelMachineSessionState.badges, ini: "INI WASM: ready" } });
} catch (error) {
setBadge(wasmBadge, "INI WASM: failed", "badge danger");
setLog(`INI WASM init failed: ${error.message}`, true);
updatePanelMachineSessionState({
badges: { ...panelMachineSessionState.badges, ini: "INI WASM: failed" },
lastAction: "boot",
error: error.message,
});
throw error;
}
try {
interpSdk = await createLinuxCncInterpSdk();
setBadge(interpBadge, "Interpreter WASM: ready");
updatePanelMachineSessionState({ badges: { ...panelMachineSessionState.badges, interp: "Interpreter WASM: ready" } });
} catch (error) {
setBadge(interpBadge, "Interpreter WASM: failed", "badge danger");
setLog(`Interpreter WASM init failed: ${error.message}`, true);
updatePanelMachineSessionState({
badges: { ...panelMachineSessionState.badges, interp: "Interpreter WASM: failed" },
lastAction: "boot",
error: error.message,
});
throw error;
}
try {
await getOpfsRoot();
setBadge(opfsBadge, "OPFS: ready");
updatePanelMachineSessionState({ badges: { ...panelMachineSessionState.badges, opfs: "OPFS: ready" } });
} catch (error) {
setBadge(opfsBadge, "OPFS: unavailable", "badge danger");
setLog(`OPFS check failed: ${error.message}`, true);
updatePanelMachineSessionState({
badges: { ...panelMachineSessionState.badges, opfs: "OPFS: unavailable" },
lastAction: "boot",
error: error.message,
});
}
}
@@ -402,14 +608,17 @@ document.getElementById("save-machine-files").addEventListener("click", async ()
toolTable: DEFAULT_TOOL_TABLE,
parameters: DEFAULT_PARAMETERS,
});
const gcodePath = await saveGcodeProgram(GCODE_FILENAME, DEFAULT_GCODE);
const gcodePath = await saveGcodeProgram(UI_SESSION.defaultGcodeFilename, DEFAULT_GCODE);
const summary = createMachineFileSaveSummary(paths, gcodePath);
updatePanelMachineSessionState({
machineFiles: summary,
lastAction: "save-machine-files",
error: null,
});
setLog(
[
"Saved machine text files to OPFS.",
`INI: ${paths.ini}`,
`Tool table: ${paths.toolTable}`,
`Parameters: ${paths.parameters}`,
`G-code: ${gcodePath}`,
...machineFileSaveLogLines(summary),
].join("\n"),
);
} catch (error) {
@@ -417,64 +626,227 @@ document.getElementById("save-machine-files").addEventListener("click", async ()
}
});
document.getElementById("load-session").addEventListener("click", async () => {
document.getElementById("save-session-snapshot").addEventListener("click", async () => {
try {
loadedSession = await loadMachineSessionFromOpfs(
requireInterpSdk(),
MACHINE_ID,
{
...SESSION_WASM_FILES,
iniSdk: requireIniSdk(),
},
);
setField("sessionIni", loadedSession.ini.wasmPath);
setField("sessionParameters", loadedSession.parameters.wasmPath);
setField("sessionToolTable", loadedSession.toolTable.wasmPath);
const workflow = await saveMachineSessionSnapshotWorkflow({
machineId: MACHINE_ID,
snapshotId: UI_SESSION.snapshotId,
snapshotFilename: UI_SESSION.snapshotFilename,
snapshotLabel: sessionSnapshotLabel(UI_SESSION),
iniText: editor.value,
toolTableText: DEFAULT_TOOL_TABLE,
parameterText: DEFAULT_PARAMETERS,
gcodeFilename: UI_SESSION.defaultGcodeFilename,
gcodeText: DEFAULT_GCODE,
metadata: sessionSnapshotMetadata(),
saveMachineTextFiles,
saveGcodeProgram,
saveMachineSessionSnapshot,
});
for (const [name, value] of Object.entries(workflow.fields)) {
setField(name, value);
}
restoredSessionSnapshot = workflow.snapshot;
updatePanelMachineSessionState({
sessionSnapshot: workflow.summary,
sessionLoad: null,
selectedProgram: null,
fields: workflow.fields,
lastAction: "save-session-snapshot",
error: null,
});
setLog(
[
"Loaded machine session into the interpreter WASM filesystem.",
`INI: ${loadedSession.ini.opfsPath} -> ${loadedSession.ini.wasmPath}`,
`Parameter file: ${loadedSession.parameters.opfsPath} -> ${loadedSession.parameters.wasmPath}`,
`Parameters: ${loadedSession.parameters.result.trim()}`,
`Tool table file: ${loadedSession.toolTable.opfsPath} -> ${loadedSession.toolTable.wasmPath}`,
`Tool table: ${loadedSession.toolTable.result.trim()}`,
"Saved machine session snapshot to OPFS.",
...workflow.logLines,
].join("\n"),
);
} catch (error) {
updatePanelMachineSessionState({
lastAction: "save-session-snapshot",
error: error.message,
});
setLog(`Machine session snapshot save failed: ${error.message}`, true);
}
});
document.getElementById("restore-session-snapshot").addEventListener("click", async () => {
try {
const workflow = await restoreMachineSessionSnapshotWorkflow({
machineId: MACHINE_ID,
snapshotId: UI_SESSION.snapshotId,
snapshotFilename: UI_SESSION.snapshotFilename,
snapshotLabel: sessionSnapshotLabel(UI_SESSION),
loadMachineSessionSnapshot,
loadMachineTextFiles,
});
editor.value = workflow.editorIniText;
setField("sessionIni", workflow.fields.sessionIni);
setField("sessionParameters", workflow.fields.sessionParameters);
setField("sessionToolTable", workflow.fields.sessionToolTable);
setField("sessionGcode", workflow.fields.sessionGcode);
setField("sessionSnapshot", workflow.fields.sessionSnapshot);
restoredSessionSnapshot = workflow.snapshot;
updatePanelMachineSessionState({
sessionSnapshot: workflow.summary,
sessionLoad: null,
selectedProgram: null,
fields: workflow.fields,
lastAction: "restore-session-snapshot",
error: null,
});
setLog(
[
"Restored machine session snapshot from OPFS.",
...workflow.logLines,
].join("\n"),
);
} catch (error) {
updatePanelMachineSessionState({
lastAction: "restore-session-snapshot",
error: error.message,
});
setLog(`Machine session snapshot restore failed: ${error.message}`, true);
}
});
document.getElementById("load-session").addEventListener("click", async () => {
try {
await loadMachineSessionIntoWasm();
const loadSummary = createMachineSessionLoadSummary(loadedSession);
updatePanelMachineSessionState({
sessionLoad: loadSummary,
fields: {
sessionIni: loadSummary.iniWasmPath,
sessionParameters: loadSummary.parameterWasmPath,
sessionToolTable: loadSummary.toolTableWasmPath,
},
lastAction: "load-session",
error: null,
});
setLog(
[
"Loaded machine session into the interpreter WASM filesystem.",
...machineSessionLoadLogLines(loadSummary),
].join("\n"),
);
} catch (error) {
updatePanelMachineSessionState({
lastAction: "load-session",
error: error.message,
});
setLog(`Machine session load failed: ${error.message}`, true);
}
});
document.getElementById("restore-load-session").addEventListener("click", async () => {
try {
const workflow = await restoreMachineSessionSnapshotWorkflow({
machineId: MACHINE_ID,
snapshotId: UI_SESSION.snapshotId,
snapshotFilename: UI_SESSION.snapshotFilename,
snapshotLabel: sessionSnapshotLabel(UI_SESSION),
loadMachineSessionSnapshot,
loadMachineTextFiles,
loadSession: () => loadMachineSessionIntoWasm(),
});
editor.value = workflow.editorIniText;
setField("sessionIni", workflow.fields.sessionIni);
setField("sessionParameters", workflow.fields.sessionParameters);
setField("sessionToolTable", workflow.fields.sessionToolTable);
setField("sessionGcode", workflow.fields.sessionGcode);
setField("sessionSnapshot", workflow.fields.sessionSnapshot);
restoredSessionSnapshot = workflow.snapshot;
if (workflow.loadedSession) {
loadedSession = workflow.loadedSession;
}
updatePanelMachineSessionState({
sessionSnapshot: workflow.summary,
sessionLoad: workflow.loadSummary,
selectedProgram: null,
fields: workflow.fields,
lastAction: "restore-load-session",
error: null,
});
setLog(
[
"Restored and loaded machine session snapshot.",
...workflow.logLines,
].join("\n"),
);
} catch (error) {
updatePanelMachineSessionState({
lastAction: "restore-load-session",
error: error.message,
});
setLog(`Machine session snapshot restore/load failed: ${error.message}`, true);
}
});
document.getElementById("run-gcode").addEventListener("click", async () => {
try {
if (!loadedSession) {
throw new Error("Load a machine session before running G-code.");
}
const interp = requireInterpSdk();
const programText = await loadGcodeProgram(GCODE_FILENAME);
interp.writeTextFile(GCODE_WASM_FILE, programText);
clearRunPlayback();
setField("runStatus", "running");
clearLastRunSummary();
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);
const workflow = await runGcodeSessionWorkflow({
interp,
loadedSession,
snapshot: restoredSessionSnapshot,
defaultFilename: UI_SESSION.defaultGcodeFilename,
defaultOpfsPath: defaultGcodeOpfsPath(),
wasmPath: UI_SESSION.gcodeWasmPath,
gcodeFilenameFromProgramPath,
loadGcodeProgram,
snapshotLabel: restoredSessionSnapshot ? sessionSnapshotLabel(restoredSessionSnapshot.sessionId) : null,
summarizeRun: (resultText, programText) => {
const motionSnapshots = parseRunMotion(resultText, programText);
return {
...summarizeRunResult(resultText, motionSnapshots),
motionSnapshots,
};
},
});
setField("sessionGcode", workflow.fields.sessionGcode);
setField("runStatus", workflow.fields.runStatus);
setLastRunSummary(workflow.summary);
updatePanelMachineSessionState({
selectedProgram: {
source: workflow.program.source,
opfsPath: workflow.program.opfsPath,
wasmPath: workflow.program.wasmPath,
},
run: workflow.summary,
fields: workflow.fields,
lastAction: "run-gcode",
error: null,
});
setCanonicalEvents(workflow.resultText);
playRunMotion(workflow.resultText, workflow.program.text, workflow.motionSnapshots);
setLog(
[
"Ran G-code through LinuxCNC interpreter WASM.",
`Program: ${GCODE_WASM_FILE}`,
`INI: ${loadedSession.ini.wasmPath}`,
result.trim(),
...workflow.logLines,
].join("\n"),
);
} catch (error) {
setField("runStatus", "failed");
setCanonicalEvents("");
clearRunPlayback();
clearLastRunSummary();
setRunMonitor(0, "failed", {}, "failed");
updatePanelMachineSessionState({
fields: {
runStatus: "failed",
},
lastAction: "run-gcode",
error: error.message,
});
setLog(`G-code run failed: ${error.message}`, true);
}
});
@@ -548,12 +920,11 @@ document.getElementById("load-machine-files").addEventListener("click", async ()
try {
const files = await loadMachineTextFiles(MACHINE_ID);
editor.value = files.ini;
const summary = createMachineFileLoadSummary(files);
setLog(
[
"Loaded machine text files from OPFS.",
`INI bytes: ${files.ini.length}`,
`Tool table bytes: ${files.toolTable.length}`,
`Parameter bytes: ${files.parameters.length}`,
...machineFileLoadLogLines(summary),
].join("\n"),
);
} catch (error) {

View File

@@ -0,0 +1,60 @@
function displayValue(value) {
return String(value ?? "-");
}
function statusText(status = {}) {
return [
status.lastAction ?? "-",
status.runStatus ?? "-",
status.error ?? "-",
].join(" | ");
}
export function renderMachineSessionControlView(container, view = {}) {
if (!container) {
throw new Error("A control view container is required.");
}
const document = container.ownerDocument;
container.textContent = "";
const statusNode = document.createElement("div");
statusNode.dataset.controlViewStatus = "";
statusNode.textContent = statusText(view.status);
container.appendChild(statusNode);
const sectionsNode = document.createElement("div");
sectionsNode.dataset.controlViewSections = "";
container.appendChild(sectionsNode);
const sectionNodes = [];
for (const section of view.sections ?? []) {
const sectionNode = document.createElement("section");
sectionNode.dataset.sectionId = section.id;
sectionNode.dataset.controlViewSectionId = section.id;
const heading = document.createElement("h3");
heading.textContent = section.title;
sectionNode.appendChild(heading);
const list = document.createElement("dl");
for (const row of section.rows ?? []) {
const term = document.createElement("dt");
term.textContent = row.label;
const def = document.createElement("dd");
def.dataset.controlViewRow = row.label;
def.textContent = displayValue(row.value);
list.append(term, def);
}
sectionNode.appendChild(list);
sectionsNode.appendChild(sectionNode);
sectionNodes.push(sectionNode);
}
return {
statusNode,
sectionsNode,
sectionNodes,
};
}

View File

@@ -85,6 +85,47 @@
background: #fbfdff;
}
.status { display: grid; gap: 8px; }
.control-view {
border-top: 1px solid #edf2f6;
padding-top: 12px;
display: grid;
gap: 12px;
}
.control-view [data-control-view-status] {
color: var(--muted);
font: 13px/1.4 "SFMono-Regular", "Consolas", monospace;
overflow-wrap: anywhere;
}
.control-view [data-control-view-sections] {
display: grid;
gap: 10px;
}
.control-view section {
border-top: 1px solid #edf2f6;
padding-top: 10px;
display: grid;
gap: 8px;
}
.control-view h3 {
margin: 0;
font-size: 13px;
font-weight: 700;
}
.control-view dl {
display: grid;
grid-template-columns: 132px 1fr;
gap: 6px 10px;
margin: 0;
}
.control-view dt {
color: var(--muted);
font-size: 12px;
}
.control-view dd {
margin: 0;
font: 12px/1.4 "SFMono-Regular", "Consolas", monospace;
overflow-wrap: anywhere;
}
.badge {
display: inline-block;
padding: 4px 8px;
@@ -228,6 +269,9 @@
<button id="load-opfs">Load From OPFS</button>
<button id="save-machine-files">Save Machine Files</button>
<button id="load-machine-files">Load Machine Files</button>
<button id="save-session-snapshot">Save Session Snapshot</button>
<button id="restore-session-snapshot">Restore Session Snapshot</button>
<button id="restore-load-session" class="secondary">Restore And Load Session</button>
<button id="sync-wasm">Sync To WASM FS</button>
<button id="query" class="secondary">Query LinuxCNC Fields</button>
<button id="load-session" class="secondary">Load Machine Session</button>
@@ -244,6 +288,7 @@
<div><span id="interp-badge" class="badge">Interpreter WASM: loading</span></div>
<div><span id="opfs-badge" class="badge">OPFS: checking</span></div>
</div>
<div id="machine-session-control-view" class="control-view"></div>
<dl class="kv">
<dt>Machine</dt>
@@ -272,8 +317,20 @@
<dd id="field-session-tool-table">-</dd>
<dt>Session G-code</dt>
<dd id="field-session-gcode">-</dd>
<dt>Session Snapshot</dt>
<dd id="field-session-snapshot">-</dd>
<dt>Run Status</dt>
<dd id="field-run-status">-</dd>
<dt>Run Source</dt>
<dd id="field-run-source">-</dd>
<dt>Run Snapshot</dt>
<dd id="field-run-snapshot">-</dd>
<dt>Run Workflow</dt>
<dd id="field-run-workflow">-</dd>
<dt>Run OPFS Path</dt>
<dd id="field-run-opfs-path">-</dd>
<dt>Run WASM Path</dt>
<dd id="field-run-wasm-path">-</dd>
<dt>5-Axis Remap</dt>
<dd id="field-fiveaxis-remap">-</dd>
</dl>

View File

@@ -0,0 +1,33 @@
export function createMachineFileSaveSummary(paths, gcodePath = null) {
return {
iniOpfsPath: paths?.ini ?? null,
toolTableOpfsPath: paths?.toolTable ?? null,
parameterOpfsPath: paths?.parameters ?? null,
gcodeOpfsPath: gcodePath ?? null,
};
}
export function machineFileSaveLogLines(summary) {
return [
`INI: ${summary.iniOpfsPath ?? "-"}`,
`Tool table: ${summary.toolTableOpfsPath ?? "-"}`,
`Parameters: ${summary.parameterOpfsPath ?? "-"}`,
`G-code: ${summary.gcodeOpfsPath ?? "-"}`,
];
}
export function createMachineFileLoadSummary(files) {
return {
iniBytes: files?.ini?.length ?? 0,
toolTableBytes: files?.toolTable?.length ?? 0,
parameterBytes: files?.parameters?.length ?? 0,
};
}
export function machineFileLoadLogLines(summary) {
return [
`INI bytes: ${summary.iniBytes}`,
`Tool table bytes: ${summary.toolTableBytes}`,
`Parameter bytes: ${summary.parameterBytes}`,
];
}

View File

@@ -0,0 +1,187 @@
export function createIniPanelStateSummary({
badges = {},
machine = null,
machineFiles = null,
sessionSnapshot = null,
sessionLoad = null,
run = null,
fiveaxisRemap = null,
} = {}) {
return {
badges: {
ini: badges.ini ?? null,
interp: badges.interp ?? null,
opfs: badges.opfs ?? null,
},
machine,
machineFiles,
sessionSnapshot,
sessionLoad,
run,
fiveaxisRemap: fiveaxisRemap ?? null,
};
}
export function createMachineSessionStateSnapshot({
badges = {},
machine = null,
machineFiles = null,
sessionSnapshot = null,
sessionLoad = null,
selectedProgram = null,
run = null,
fields = {},
lastAction = null,
error = null,
fiveaxisRemap = null,
} = {}) {
return {
...createIniPanelStateSummary({
badges,
machine,
machineFiles,
sessionSnapshot,
sessionLoad,
run,
fiveaxisRemap,
}),
selectedProgram,
fields: {
sessionIni: fields.sessionIni ?? null,
sessionParameters: fields.sessionParameters ?? null,
sessionToolTable: fields.sessionToolTable ?? null,
sessionGcode: fields.sessionGcode ?? null,
sessionSnapshot: fields.sessionSnapshot ?? null,
runStatus: fields.runStatus ?? null,
runSource: fields.runSource ?? null,
runSnapshot: fields.runSnapshot ?? null,
runWorkflow: fields.runWorkflow ?? null,
runOpfsPath: fields.runOpfsPath ?? null,
runWasmPath: fields.runWasmPath ?? null,
},
lastAction: lastAction ?? null,
error: error ?? null,
};
}
export function createMachineSessionStateReport(state = {}) {
const badges = state.badges ?? {};
const fields = state.fields ?? {};
const sessionSnapshot = state.sessionSnapshot ?? {};
const sessionLoad = state.sessionLoad ?? {};
const selectedProgram = state.selectedProgram ?? {};
const run = state.run ?? {};
return {
readiness: {
ini: badges.ini ?? null,
interp: badges.interp ?? null,
opfs: badges.opfs ?? null,
},
session: {
snapshotLabel: sessionSnapshot.snapshotLabel ?? fields.sessionSnapshot ?? null,
workflow: sessionSnapshot.workflow ?? fields.runWorkflow ?? null,
ini: fields.sessionIni ?? sessionSnapshot.iniOpfsPath ?? sessionLoad.iniWasmPath ?? null,
parameters: fields.sessionParameters ?? sessionSnapshot.parameterOpfsPath ?? sessionLoad.parameterWasmPath ?? null,
toolTable: fields.sessionToolTable ?? sessionSnapshot.toolTableOpfsPath ?? sessionLoad.toolTableWasmPath ?? null,
gcode: fields.sessionGcode ?? sessionSnapshot.gcodeOpfsPath ?? null,
},
loaded: {
iniWasmPath: sessionLoad.iniWasmPath ?? null,
parameterWasmPath: sessionLoad.parameterWasmPath ?? null,
toolTableWasmPath: sessionLoad.toolTableWasmPath ?? null,
},
program: {
source: selectedProgram.source ?? run.programSource ?? fields.runSource ?? null,
opfsPath: selectedProgram.opfsPath ?? run.programOpfsPath ?? fields.runOpfsPath ?? null,
wasmPath: selectedProgram.wasmPath ?? run.programWasmPath ?? fields.runWasmPath ?? null,
},
run: {
status: run.runStatus ?? fields.runStatus ?? null,
canonicalEventCount: run.canonicalEventCount ?? 0,
motionSnapshotCount: run.motionSnapshotCount ?? 0,
iniWasmPath: run.iniWasmPath ?? null,
},
status: {
lastAction: state.lastAction ?? null,
error: state.error ?? null,
fiveaxisRemap: state.fiveaxisRemap ?? null,
},
};
}
export function createMachineSessionStateReportExport(state = {}, options = {}) {
const report = createMachineSessionStateReport(state);
return {
filename: options.filename ?? "machine-session-state-report.json",
mediaType: "application/json",
report,
text: `${JSON.stringify(report, null, 2)}\n`,
};
}
export function createMachineSessionWorkflowStatus(state = {}) {
const report = createMachineSessionStateReport(state);
return {
readiness: report.readiness,
lastAction: report.status.lastAction,
error: report.status.error,
session: report.session,
run: report.run,
};
}
export function createMachineSessionStateBundle(state = {}, options = {}) {
const report = createMachineSessionStateReport(state);
const exportPayload = createMachineSessionStateReportExport(state, options);
const workflowStatus = createMachineSessionWorkflowStatus(state);
return {
report,
export: exportPayload,
workflowStatus,
};
}
export function createMachineSessionControlView(bundle = {}) {
const report = bundle.report ?? {};
const workflowStatus = bundle.workflowStatus ?? {};
const session = workflowStatus.session ?? report.session ?? {};
const run = workflowStatus.run ?? report.run ?? {};
return {
status: {
lastAction: workflowStatus.lastAction ?? report.status?.lastAction ?? null,
error: workflowStatus.error ?? report.status?.error ?? null,
runStatus: run.status ?? null,
},
sections: [
{
id: "readiness",
title: "Readiness",
rows: [
{ label: "INI", value: workflowStatus.readiness?.ini ?? report.readiness?.ini ?? null },
{ label: "Interpreter", value: workflowStatus.readiness?.interp ?? report.readiness?.interp ?? null },
{ label: "OPFS", value: workflowStatus.readiness?.opfs ?? report.readiness?.opfs ?? null },
],
},
{
id: "session",
title: "Session",
rows: [
{ label: "Snapshot", value: session.snapshotLabel ?? null },
{ label: "Workflow", value: session.workflow ?? null },
{ label: "INI", value: session.ini ?? null },
{ label: "G-code", value: session.gcode ?? null },
],
},
{
id: "run",
title: "Run",
rows: [
{ label: "Status", value: run.status ?? null },
{ label: "Canonical events", value: run.canonicalEventCount ?? 0 },
{ label: "Motion snapshots", value: run.motionSnapshotCount ?? 0 },
{ label: "INI WASM", value: run.iniWasmPath ?? null },
],
},
],
};
}

View File

@@ -0,0 +1,21 @@
export function summarizeRunResult(resultText, snapshots = []) {
return {
canonicalEventCount: resultText.split("\n").filter((line) => line.startsWith("canon_event=")).length,
motionSnapshotCount: snapshots.length,
};
}
export function createRunSummary(program, runtime = {}, snapshot = null) {
const metadata = snapshot?.metadata ?? {};
return {
runStatus: runtime.runStatus ?? "ok",
programSource: program.source,
snapshotLabel: runtime.snapshotLabel ?? metadata.snapshotLabel ?? null,
workflow: metadata.workflow ?? null,
programOpfsPath: program.opfsPath,
programWasmPath: program.wasmPath,
iniWasmPath: runtime.iniWasmPath ?? null,
canonicalEventCount: runtime.canonicalEventCount ?? 0,
motionSnapshotCount: runtime.motionSnapshotCount ?? 0,
};
}

View File

@@ -0,0 +1,109 @@
import {
createRunSummary,
} from "./run-summary.js";
import {
createSessionSnapshotSummary,
sessionSnapshotMetadataLogLines,
} from "./session-summary.js";
export function selectSessionGcodeProgram({
snapshot = null,
defaultFilename,
defaultOpfsPath,
wasmPath,
gcodeFilenameFromProgramPath,
}) {
const snapshotOpfsPath = snapshot?.payload?.files?.gcode;
if (typeof snapshotOpfsPath === "string") {
try {
return {
filename: gcodeFilenameFromProgramPath(snapshotOpfsPath),
opfsPath: snapshotOpfsPath,
source: "snapshot",
wasmPath,
};
} catch {
// Invalid snapshot program paths fall back to the UI default program.
}
}
return {
filename: defaultFilename,
opfsPath: defaultOpfsPath,
source: "default",
wasmPath,
};
}
export function runSessionContextLogLines(program, snapshot = null, snapshotLabel = null) {
const summary = createSessionSnapshotSummary({
metadata: snapshot?.metadata ?? {},
});
return [
`Program: ${program.opfsPath} -> ${program.wasmPath}`,
`Program source: ${program.source}`,
`Snapshot: ${snapshot ? (snapshotLabel ?? snapshot?.metadata?.snapshotLabel ?? snapshot?.sessionId) : "-"}`,
...sessionSnapshotMetadataLogLines(summary),
];
}
export async function runGcodeSessionWorkflow({
interp,
loadedSession,
snapshot = null,
defaultFilename,
defaultOpfsPath,
wasmPath,
gcodeFilenameFromProgramPath,
loadGcodeProgram,
summarizeRun = () => ({}),
snapshotLabel = null,
}) {
if (!loadedSession) {
throw new Error("Load a machine session before running G-code.");
}
const program = selectSessionGcodeProgram({
snapshot,
defaultFilename,
defaultOpfsPath,
wasmPath,
gcodeFilenameFromProgramPath,
});
const programText = await loadGcodeProgram(program.filename);
interp.writeTextFile(program.wasmPath, programText);
const resultText = interp.runFileWithIni(program.wasmPath, loadedSession.ini.wasmPath).trim();
const runResultSummary = summarizeRun(resultText, programText);
const { motionSnapshots = [], ...runCounts } = runResultSummary;
const summary = createRunSummary(
program,
{
runStatus: "ok",
iniWasmPath: loadedSession.ini.wasmPath,
snapshotLabel: snapshot ? snapshotLabel : null,
...runCounts,
},
snapshot,
);
return {
program: {
...program,
text: programText,
},
resultText,
motionSnapshots,
summary,
fields: {
sessionGcode: program.opfsPath,
runStatus: summary.runStatus,
runSource: summary.programSource,
runSnapshot: summary.snapshotLabel ?? "-",
runWorkflow: summary.workflow ?? "-",
runOpfsPath: summary.programOpfsPath,
runWasmPath: summary.programWasmPath,
},
logLines: [
...runSessionContextLogLines(program, snapshot, summary.snapshotLabel),
`INI: ${loadedSession.ini.wasmPath}`,
resultText,
],
};
}

View File

@@ -0,0 +1,22 @@
export function createMachineSessionLoadSummary(session) {
return {
iniOpfsPath: session?.ini?.opfsPath ?? null,
iniWasmPath: session?.ini?.wasmPath ?? null,
parameterOpfsPath: session?.parameters?.opfsPath ?? null,
parameterWasmPath: session?.parameters?.wasmPath ?? null,
parameterResult: session?.parameters?.result ?? "",
toolTableOpfsPath: session?.toolTable?.opfsPath ?? null,
toolTableWasmPath: session?.toolTable?.wasmPath ?? null,
toolTableResult: session?.toolTable?.result ?? "",
};
}
export function machineSessionLoadLogLines(summary) {
return [
`INI: ${summary.iniOpfsPath ?? "-"} -> ${summary.iniWasmPath ?? "-"}`,
`Parameter file: ${summary.parameterOpfsPath ?? "-"} -> ${summary.parameterWasmPath ?? "-"}`,
`Parameters: ${summary.parameterResult.trim()}`,
`Tool table file: ${summary.toolTableOpfsPath ?? "-"} -> ${summary.toolTableWasmPath ?? "-"}`,
`Tool table: ${summary.toolTableResult.trim()}`,
];
}

View File

@@ -0,0 +1,28 @@
export function createSessionSnapshotSummary(snapshot, fallbackLabel = null) {
const metadata = snapshot?.metadata ?? {};
const files = snapshot?.payload?.files ?? {};
return {
snapshotLabel: fallbackLabel ?? metadata.snapshotLabel ?? null,
workflow: metadata.workflow ?? null,
defaultGcodeOpfsPath: metadata.defaultGcodeOpfsPath ?? null,
iniOpfsPath: files.ini ?? null,
parameterOpfsPath: files.parameters ?? null,
toolTableOpfsPath: files.toolTable ?? null,
gcodeOpfsPath: files.gcode ?? null,
};
}
export function sessionSnapshotSummaryLogLines(summary) {
return [
`Snapshot: ${summary.snapshotLabel ?? "-"}`,
`Workflow: ${summary.workflow ?? "-"}`,
`Default G-code: ${summary.defaultGcodeOpfsPath ?? "-"}`,
];
}
export function sessionSnapshotMetadataLogLines(summary) {
return [
`Workflow: ${summary.workflow ?? "-"}`,
`Default G-code: ${summary.defaultGcodeOpfsPath ?? "-"}`,
];
}

View File

@@ -0,0 +1,130 @@
import {
createMachineSessionLoadSummary,
machineSessionLoadLogLines,
} from "./session-load-summary.js";
import {
createSessionSnapshotSummary,
sessionSnapshotSummaryLogLines,
} from "./session-summary.js";
export function createSessionSnapshotFieldState(summary) {
return {
sessionIni: summary.iniOpfsPath,
sessionParameters: summary.parameterOpfsPath,
sessionToolTable: summary.toolTableOpfsPath,
sessionGcode: summary.gcodeOpfsPath,
sessionSnapshot: summary.snapshotLabel,
};
}
export function sessionSnapshotSaveLogLines(summary) {
return [
...sessionSnapshotSummaryLogLines(summary),
`INI: ${summary.iniOpfsPath}`,
`Parameter file: ${summary.parameterOpfsPath}`,
`Tool table file: ${summary.toolTableOpfsPath}`,
`G-code: ${summary.gcodeOpfsPath}`,
];
}
export function sessionSnapshotRestoreLogLines(summary) {
return [
...sessionSnapshotSummaryLogLines(summary),
`INI: ${summary.iniOpfsPath}`,
`Parameter file: ${summary.parameterOpfsPath}`,
`Tool table file: ${summary.toolTableOpfsPath}`,
`G-code: ${summary.gcodeOpfsPath ?? "-"}`,
];
}
export async function saveMachineSessionSnapshotWorkflow({
machineId,
snapshotId,
snapshotFilename,
snapshotLabel,
iniText,
toolTableText,
parameterText,
gcodeFilename,
gcodeText,
metadata,
saveMachineTextFiles,
saveGcodeProgram,
saveMachineSessionSnapshot,
}) {
await Promise.all([
saveMachineTextFiles(machineId, {
ini: iniText,
toolTable: toolTableText,
parameters: parameterText,
}),
saveGcodeProgram(gcodeFilename, gcodeText),
]);
const snapshot = await saveMachineSessionSnapshot(
snapshotId,
machineId,
{
filename: snapshotFilename,
gcodeFilename,
metadata,
},
);
const summary = createSessionSnapshotSummary(snapshot, snapshotLabel);
return {
snapshot,
summary,
fields: createSessionSnapshotFieldState(summary),
logLines: sessionSnapshotSaveLogLines(summary),
};
}
export async function restoreMachineSessionSnapshotWorkflow({
machineId,
snapshotId,
snapshotFilename,
snapshotLabel,
loadMachineSessionSnapshot,
loadMachineTextFiles,
loadSession = null,
}) {
const snapshot = await loadMachineSessionSnapshot(
snapshotId,
machineId,
{ filename: snapshotFilename },
);
const files = await loadMachineTextFiles(machineId);
const summary = createSessionSnapshotSummary(snapshot, snapshotLabel ?? snapshotLabelFromSession(snapshot));
const fields = createSessionSnapshotFieldState(summary);
const result = {
snapshot,
editorIniText: files.ini,
summary,
fields,
loadSummary: null,
loadedSession: null,
logLines: sessionSnapshotRestoreLogLines(summary),
};
if (loadSession) {
const loadedSession = await loadSession();
const loadSummary = createMachineSessionLoadSummary(loadedSession);
result.loadedSession = loadedSession;
result.loadSummary = loadSummary;
result.fields = {
...fields,
sessionIni: loadSummary.iniWasmPath,
sessionParameters: loadSummary.parameterWasmPath,
sessionToolTable: loadSummary.toolTableWasmPath,
sessionGcode: summary.gcodeOpfsPath,
};
result.logLines = [
...sessionSnapshotSummaryLogLines(summary),
...machineSessionLoadLogLines(loadSummary),
`G-code: ${summary.gcodeOpfsPath ?? "-"}`,
];
}
return result;
}
function snapshotLabelFromSession(snapshot) {
return snapshot?.metadata?.snapshotLabel ?? snapshot?.sessionId ?? null;
}

View File

@@ -6,8 +6,10 @@
</head>
<body>
<pre id="status">running</pre>
<div id="control-view"></div>
<script type="module">
import { createLinuxCncIniSdk } from "../../runtime/sdk/src/index.js";
import { renderMachineSessionControlView } from "../../runtime/ui/ini-panel/control-view-renderer.js";
import { loadTextFile, saveTextFile } from "../../runtime/opfs/file-service.js";
import {
gcodeProgramPath,
@@ -16,10 +18,13 @@
} from "../../runtime/opfs/path-model.js";
import {
createMachineSessionSnapshotPayload,
loadMachineSessionSnapshot,
loadSessionSnapshot,
saveMachineSessionSnapshot,
saveSessionSnapshot,
} from "../../runtime/opfs/snapshot-store.js";
import {
gcodeFilenameFromProgramPath,
loadGcodeProgram,
loadMachineTextFiles,
saveGcodeProgram,
@@ -204,6 +209,61 @@ TOOL_TABLE = browser-tool.tbl
customSnapshot.metadata.source,
"custom snapshot metadata",
);
const machineSessionSnapshot = await saveMachineSessionSnapshot(
"browser-machine-session",
"browser-smoke",
{
filename: "browser-machine-snapshot.json",
parameterFilename: "browser-linuxcnc.var",
toolTableFilename: "browser-tool.tbl",
gcodeFilename: "browser-smoke.ngc",
createdAt: "2026-06-08T00:00:00.000Z",
metadata: { source: "browser-machine-session" },
},
);
const loadedMachineSessionSnapshot = await loadSessionSnapshot(
"browser-machine-session",
{ filename: "browser-machine-snapshot.json" },
);
const loadedCheckedMachineSessionSnapshot = await loadMachineSessionSnapshot(
"browser-machine-session",
"browser-smoke",
{ filename: "browser-machine-snapshot.json" },
);
assertEqual(
machineSessionSnapshot.payload.files.parameters,
"linuxcnc/machines/browser-smoke/browser-linuxcnc.var",
"machine session snapshot parameter file",
);
assertEqual(
machineSessionSnapshot.payload.files.toolTable,
"linuxcnc/machines/browser-smoke/browser-tool.tbl",
"machine session snapshot tool table",
);
assertEqual(
loadedMachineSessionSnapshot.payload.files.gcode,
"linuxcnc/gcode/browser-smoke.ngc",
"machine session snapshot G-code",
);
assertEqual(
loadedMachineSessionSnapshot.metadata.source,
"browser-machine-session",
"machine session snapshot metadata",
);
assertEqual(
loadedCheckedMachineSessionSnapshot.payload.machineId,
"browser-smoke",
"checked machine session snapshot id",
);
await assertRejects(
"browser machine snapshot wrong machine",
() => loadMachineSessionSnapshot(
"browser-machine-session",
"other-browser-machine",
{ filename: "browser-machine-snapshot.json" },
),
/Machine session snapshot id mismatch/,
);
await assertRejects(
"browser snapshot invalid filename save",
() => saveSessionSnapshot("browser-session", {}, {
@@ -271,6 +331,16 @@ TOOL_TABLE = browser-tool.tbl
"linuxcnc/gcode/browser-custom.ngc",
"custom G-code path",
);
assertEqual(
gcodeFilenameFromProgramPath("linuxcnc/gcode/browser-custom.ngc"),
"browser-custom.ngc",
"custom G-code filename from OPFS path",
);
await assertRejects(
"browser G-code invalid program OPFS path",
() => Promise.resolve(gcodeFilenameFromProgramPath("linuxcnc/machines/browser-custom.ngc")),
/Invalid G-code program OPFS path/,
);
await saveGcodeProgram("browser-smoke.ngc", "G0 X0 Y0\nM2\n");
assertEqual(await loadGcodeProgram("browser-smoke.ngc"), "G0 X0 Y0\nM2\n", "gcode text");
await saveGcodeProgram("browser-custom.ngc", "G1 X1 F10\nM2\n");
@@ -299,6 +369,17 @@ TOOL_TABLE = browser-tool.tbl
() => uiDocument.getElementById("opfs-badge")?.textContent.includes("ready"),
"INI panel OPFS readiness",
);
const uiApi = uiDocument.defaultView.linuxCncIniPanelApi;
if (
!uiApi?.getMachineSessionControlView ||
!uiApi?.getMachineSessionStateBundle ||
!uiApi?.getMachineSessionStateReport ||
!uiApi?.getMachineSessionStateReportExport ||
!uiApi?.getMachineSessionWorkflowStatus ||
!uiApi?.renderMachineSessionControlView
) {
throw new Error("INI panel API namespace did not expose state getters");
}
uiDocument.getElementById("ini-editor").value = iniText;
uiDocument.getElementById("query").click();
@@ -325,7 +406,168 @@ TOOL_TABLE = browser-tool.tbl
uiDocument.getElementById("load-session").click();
await waitFor(
() => uiDocument.getElementById("log")?.textContent.includes("Loaded machine session"),
"machine session load",
"fallback machine session load",
);
uiDocument.getElementById("run-gcode").click();
await waitFor(
() => uiDocument.getElementById("log")?.textContent.includes("Ran G-code"),
"fallback G-code run",
);
const fallbackRunLog = uiDocument.getElementById("log").textContent;
if (
!fallbackRunLog.includes("Program: linuxcnc/gcode/ui-session.ngc -> /work/ui-session.ngc") ||
!fallbackRunLog.includes("Program source: default") ||
!fallbackRunLog.includes("Snapshot: -") ||
!fallbackRunLog.includes("Workflow: -") ||
!fallbackRunLog.includes("Default G-code: -") ||
!fallbackRunLog.includes("canon_event=STRAIGHT_FEED")
) {
throw new Error(`UI fallback G-code run did not use default OPFS program path: ${fallbackRunLog}`);
}
assertEqual(
uiDocument.getElementById("field-run-source").textContent,
"default",
"UI fallback run source field",
);
assertEqual(
uiDocument.getElementById("field-run-snapshot").textContent,
"-",
"UI fallback run snapshot field",
);
assertEqual(
uiDocument.getElementById("field-run-workflow").textContent,
"-",
"UI fallback run workflow field",
);
assertEqual(
uiDocument.getElementById("field-run-opfs-path").textContent,
"linuxcnc/gcode/ui-session.ngc",
"UI fallback run OPFS field",
);
assertEqual(
uiDocument.getElementById("field-run-wasm-path").textContent,
"/work/ui-session.ngc",
"UI fallback run WASM field",
);
const fallbackRunSummary = uiDocument.defaultView.linuxCncIniPanelLastRunSummary;
assertEqual(fallbackRunSummary.runStatus, "ok", "UI fallback run summary status");
assertEqual(fallbackRunSummary.programSource, "default", "UI fallback run summary source");
assertEqual(fallbackRunSummary.snapshotLabel, null, "UI fallback run summary snapshot");
assertEqual(fallbackRunSummary.workflow, null, "UI fallback run summary workflow");
assertEqual(
fallbackRunSummary.programOpfsPath,
"linuxcnc/gcode/ui-session.ngc",
"UI fallback run summary OPFS path",
);
assertEqual(
fallbackRunSummary.programWasmPath,
"/work/ui-session.ngc",
"UI fallback run summary WASM path",
);
assertEqual(
fallbackRunSummary.iniWasmPath,
"/work/session-machine.ini",
"UI fallback run summary INI WASM path",
);
assertEqual(
fallbackRunSummary.canonicalEventCount,
2,
"UI fallback run summary canonical event count",
);
assertEqual(
fallbackRunSummary.motionSnapshotCount,
2,
"UI fallback run summary motion snapshot count",
);
uiDocument.getElementById("save-session-snapshot").click();
await waitFor(
() => uiDocument.getElementById("log")?.textContent.includes("Saved machine session snapshot"),
"machine session snapshot save",
);
assertEqual(
uiDocument.getElementById("field-session-snapshot").textContent,
"ui-machine-session/ui-machine-session.json",
"UI session snapshot path",
);
assertEqual(
uiDocument.getElementById("field-session-ini").textContent,
"linuxcnc/machines/xyzab-tdr/machine.ini",
"UI snapshot INI OPFS path",
);
assertEqual(
uiDocument.getElementById("field-session-gcode").textContent,
"linuxcnc/gcode/ui-session.ngc",
"UI snapshot G-code OPFS path",
);
const savedSnapshotLog = uiDocument.getElementById("log").textContent;
if (
!savedSnapshotLog.includes("Workflow: save-session-snapshot") ||
!savedSnapshotLog.includes("Default G-code: linuxcnc/gcode/ui-session.ngc")
) {
throw new Error(`UI saved snapshot log missing metadata summary: ${savedSnapshotLog}`);
}
const savedMachineSessionStateReport = uiApi.getMachineSessionStateReport();
assertEqual(
savedMachineSessionStateReport.session.snapshotLabel,
"ui-machine-session/ui-machine-session.json",
"UI saved machine session state report snapshot label",
);
assertEqual(
savedMachineSessionStateReport.session.gcode,
"linuxcnc/gcode/ui-session.ngc",
"UI saved machine session state report G-code path",
);
assertEqual(
savedMachineSessionStateReport.status.lastAction,
"save-session-snapshot",
"UI saved machine session state report last action",
);
const savedUiSnapshot = await loadMachineSessionSnapshot(
"ui-machine-session",
"xyzab-tdr",
{ filename: "ui-machine-session.json" },
);
assertEqual(savedUiSnapshot.metadata.source, "ini-panel", "UI snapshot metadata source");
assertEqual(
savedUiSnapshot.metadata.workflow,
"save-session-snapshot",
"UI snapshot metadata workflow",
);
assertEqual(
savedUiSnapshot.metadata.snapshotLabel,
"ui-machine-session/ui-machine-session.json",
"UI snapshot metadata label",
);
assertEqual(
savedUiSnapshot.metadata.defaultGcodeOpfsPath,
"linuxcnc/gcode/ui-session.ngc",
"UI snapshot metadata default G-code path",
);
uiDocument.getElementById("ini-editor").value = "";
uiDocument.getElementById("restore-session-snapshot").click();
await waitFor(
() => uiDocument.getElementById("log")?.textContent.includes("Restored machine session snapshot"),
"machine session snapshot restore",
);
assertEqual(
uiDocument.getElementById("field-session-snapshot").textContent,
"ui-machine-session/ui-machine-session.json",
"UI restored session snapshot path",
);
const restoredSnapshotLog = uiDocument.getElementById("log").textContent;
if (
!restoredSnapshotLog.includes("Workflow: save-session-snapshot") ||
!restoredSnapshotLog.includes("Default G-code: linuxcnc/gcode/ui-session.ngc")
) {
throw new Error(`UI restored snapshot log missing metadata summary: ${restoredSnapshotLog}`);
}
if (!uiDocument.getElementById("ini-editor").value.includes("MACHINE = browser-smoke")) {
throw new Error("UI restored session snapshot did not reload INI text");
}
uiDocument.getElementById("restore-load-session").click();
await waitFor(
() => uiDocument.getElementById("log")?.textContent.includes("Restored and loaded machine session"),
"restore and load machine session",
);
assertEqual(
uiDocument.getElementById("field-session-ini").textContent,
@@ -344,9 +586,12 @@ TOOL_TABLE = browser-tool.tbl
);
const uiLog = uiDocument.getElementById("log").textContent;
if (!uiLog.includes("restore_parameters=0") || !uiLog.includes("tooldata_load=0")) {
throw new Error(`UI session log missing LinuxCNC load result: ${uiLog}`);
throw new Error(`UI restore/load session log missing LinuxCNC load result: ${uiLog}`);
}
if (
!uiLog.includes("Snapshot: ui-machine-session/ui-machine-session.json") ||
!uiLog.includes("Workflow: save-session-snapshot") ||
!uiLog.includes("Default G-code: linuxcnc/gcode/ui-session.ngc") ||
!uiLog.includes(
"Parameter file: linuxcnc/machines/xyzab-tdr/linuxcnc.var -> /work/session-linuxcnc.var",
) ||
@@ -354,8 +599,29 @@ TOOL_TABLE = browser-tool.tbl
"Tool table file: linuxcnc/machines/xyzab-tdr/tool.tbl -> /work/session-tool.tbl",
)
) {
throw new Error(`UI session log missing OPFS default file mapping: ${uiLog}`);
throw new Error(`UI restore/load session log missing OPFS default file mapping: ${uiLog}`);
}
const restoredLoadedMachineSessionStateReport = uiApi.getMachineSessionStateReport();
assertEqual(
restoredLoadedMachineSessionStateReport.session.snapshotLabel,
"ui-machine-session/ui-machine-session.json",
"UI restored loaded machine session state report snapshot label",
);
assertEqual(
restoredLoadedMachineSessionStateReport.loaded.iniWasmPath,
"/work/session-machine.ini",
"UI restored loaded machine session state report loaded INI WASM path",
);
assertEqual(
restoredLoadedMachineSessionStateReport.loaded.parameterWasmPath,
"/work/session-linuxcnc.var",
"UI restored loaded machine session state report loaded parameter WASM path",
);
assertEqual(
restoredLoadedMachineSessionStateReport.status.lastAction,
"restore-load-session",
"UI restored loaded machine session state report last action",
);
uiDocument.getElementById("run-gcode").click();
await waitFor(
() => uiDocument.getElementById("log")?.textContent.includes("Ran G-code"),
@@ -363,8 +629,8 @@ TOOL_TABLE = browser-tool.tbl
);
assertEqual(
uiDocument.getElementById("field-session-gcode").textContent,
"/work/ui-session.ngc",
"UI session G-code path",
"linuxcnc/gcode/ui-session.ngc",
"UI session snapshot G-code path",
);
assertEqual(
uiDocument.getElementById("field-run-status").textContent,
@@ -372,6 +638,270 @@ TOOL_TABLE = browser-tool.tbl
"UI G-code run status",
);
const runLog = uiDocument.getElementById("log").textContent;
if (
!runLog.includes("Program: linuxcnc/gcode/ui-session.ngc -> /work/ui-session.ngc") ||
!runLog.includes("Program source: snapshot") ||
!runLog.includes("Snapshot: ui-machine-session/ui-machine-session.json") ||
!runLog.includes("Workflow: save-session-snapshot") ||
!runLog.includes("Default G-code: linuxcnc/gcode/ui-session.ngc")
) {
throw new Error(`UI G-code run missing snapshot session context: ${runLog}`);
}
assertEqual(
uiDocument.getElementById("field-run-source").textContent,
"snapshot",
"UI snapshot run source field",
);
assertEqual(
uiDocument.getElementById("field-run-snapshot").textContent,
"ui-machine-session/ui-machine-session.json",
"UI snapshot run snapshot field",
);
assertEqual(
uiDocument.getElementById("field-run-workflow").textContent,
"save-session-snapshot",
"UI snapshot run workflow field",
);
assertEqual(
uiDocument.getElementById("field-run-opfs-path").textContent,
"linuxcnc/gcode/ui-session.ngc",
"UI snapshot run OPFS field",
);
assertEqual(
uiDocument.getElementById("field-run-wasm-path").textContent,
"/work/ui-session.ngc",
"UI snapshot run WASM field",
);
const snapshotRunSummary = uiDocument.defaultView.linuxCncIniPanelLastRunSummary;
assertEqual(snapshotRunSummary.runStatus, "ok", "UI snapshot run summary status");
assertEqual(snapshotRunSummary.programSource, "snapshot", "UI snapshot run summary source");
assertEqual(
snapshotRunSummary.snapshotLabel,
"ui-machine-session/ui-machine-session.json",
"UI snapshot run summary snapshot",
);
assertEqual(
snapshotRunSummary.workflow,
"save-session-snapshot",
"UI snapshot run summary workflow",
);
assertEqual(
snapshotRunSummary.programOpfsPath,
"linuxcnc/gcode/ui-session.ngc",
"UI snapshot run summary OPFS path",
);
assertEqual(
snapshotRunSummary.programWasmPath,
"/work/ui-session.ngc",
"UI snapshot run summary WASM path",
);
assertEqual(
snapshotRunSummary.iniWasmPath,
"/work/session-machine.ini",
"UI snapshot run summary INI WASM path",
);
assertEqual(
snapshotRunSummary.canonicalEventCount,
2,
"UI snapshot run summary canonical event count",
);
assertEqual(
snapshotRunSummary.motionSnapshotCount,
2,
"UI snapshot run summary motion snapshot count",
);
const machineSessionStateReport = uiApi.getMachineSessionStateReport();
assertEqual(
machineSessionStateReport.status.lastAction,
"run-gcode",
"UI machine session state report last action",
);
assertEqual(
machineSessionStateReport.program.opfsPath,
"linuxcnc/gcode/ui-session.ngc",
"UI machine session state report selected program OPFS path",
);
assertEqual(
machineSessionStateReport.loaded.iniWasmPath,
"/work/session-machine.ini",
"UI machine session state report loaded INI WASM path",
);
assertEqual(
machineSessionStateReport.program.source,
"snapshot",
"UI machine session state report run source",
);
assertEqual(
machineSessionStateReport.session.workflow,
"save-session-snapshot",
"UI machine session state report workflow",
);
assertEqual(
machineSessionStateReport.status.error,
null,
"UI machine session state report error",
);
assertEqual(
machineSessionStateReport.run.status,
"ok",
"UI machine session state report run status",
);
assertEqual(
machineSessionStateReport.run.canonicalEventCount,
2,
"UI machine session state report canonical event count",
);
assertEqual(
machineSessionStateReport.run.motionSnapshotCount,
2,
"UI machine session state report motion snapshot count",
);
const machineSessionStateReportExport = uiApi.getMachineSessionStateReportExport();
assertEqual(
machineSessionStateReportExport.filename,
"machine-session-state-report.json",
"UI machine session state report export filename",
);
assertEqual(
machineSessionStateReportExport.mediaType,
"application/json",
"UI machine session state report export media type",
);
assertEqual(
machineSessionStateReportExport.text.endsWith("\n"),
true,
"UI machine session state report export newline",
);
assertEqual(
JSON.parse(machineSessionStateReportExport.text).run.canonicalEventCount,
2,
"UI machine session state report export canonical event count",
);
assertEqual(
JSON.parse(machineSessionStateReportExport.text).program.source,
"snapshot",
"UI machine session state report export program source",
);
const machineSessionWorkflowStatus = uiApi.getMachineSessionWorkflowStatus();
assertEqual(
machineSessionWorkflowStatus.lastAction,
"run-gcode",
"UI machine session workflow status last action",
);
assertEqual(
machineSessionWorkflowStatus.error,
null,
"UI machine session workflow status error",
);
assertEqual(
machineSessionWorkflowStatus.readiness.opfs,
"OPFS: ready",
"UI machine session workflow status OPFS readiness",
);
assertEqual(
machineSessionWorkflowStatus.session.workflow,
"save-session-snapshot",
"UI machine session workflow status session workflow",
);
assertEqual(
machineSessionWorkflowStatus.run.status,
"ok",
"UI machine session workflow status run status",
);
assertEqual(
machineSessionWorkflowStatus.run.canonicalEventCount,
2,
"UI machine session workflow status canonical event count",
);
const machineSessionStateBundle = uiApi.getMachineSessionStateBundle();
assertEqual(
machineSessionStateBundle.report.status.lastAction,
machineSessionStateReport.status.lastAction,
"UI machine session state bundle report last action",
);
assertEqual(
machineSessionStateBundle.export.text,
machineSessionStateReportExport.text,
"UI machine session state bundle export text",
);
assertEqual(
machineSessionStateBundle.workflowStatus.run.canonicalEventCount,
machineSessionWorkflowStatus.run.canonicalEventCount,
"UI machine session state bundle workflow status canonical event count",
);
assertEqual(
uiDocument.defaultView.getMachineSessionStateBundle().workflowStatus.lastAction,
machineSessionStateBundle.workflowStatus.lastAction,
"UI legacy machine session state bundle getter matches API namespace",
);
const machineSessionControlView = uiApi.getMachineSessionControlView();
const renderedControlView = renderMachineSessionControlView(
document.getElementById("control-view"),
machineSessionControlView,
);
assertEqual(
machineSessionControlView.status.runStatus,
"ok",
"UI machine session control view run status",
);
assertEqual(
machineSessionControlView.sections[0].id,
"readiness",
"UI machine session control view readiness section",
);
assertEqual(
machineSessionControlView.sections[1].rows[1].value,
"save-session-snapshot",
"UI machine session control view session workflow row",
);
assertEqual(
machineSessionControlView.sections[2].rows[1].value,
2,
"UI machine session control view canonical event row",
);
assertEqual(
renderedControlView.statusNode.textContent,
"run-gcode | ok | -",
"UI machine session control view DOM status",
);
assertEqual(
renderedControlView.sectionsNode.querySelectorAll("section").length,
3,
"UI machine session control view DOM section count",
);
assertEqual(
renderedControlView.sectionsNode.querySelector('section[data-section-id=\"run\"] dd:nth-of-type(2)').textContent,
"2",
"UI machine session control view DOM canonical event row",
);
const uiRenderedControlView = uiApi.renderMachineSessionControlView(
uiDocument.getElementById("machine-session-control-view"),
);
assertEqual(
uiRenderedControlView.statusNode.textContent,
"run-gcode | ok | -",
"UI machine session built-in control view DOM status",
);
assertEqual(
uiRenderedControlView.sectionsNode.querySelector('section[data-section-id=\"run\"] dd:nth-of-type(2)').textContent,
"2",
"UI machine session built-in control view canonical event row",
);
assertEqual(
uiDocument.defaultView.getMachineSessionControlView().status.lastAction,
machineSessionControlView.status.lastAction,
"UI legacy machine session control view getter matches API namespace",
);
assertEqual(
uiDocument.defaultView.getMachineSessionStateReport().status.lastAction,
machineSessionStateReport.status.lastAction,
"UI legacy machine session state report getter matches API namespace",
);
assertEqual(
uiDocument.defaultView.getMachineSessionStateReportExport().text,
machineSessionStateReportExport.text,
"UI legacy machine session state report export getter matches API namespace",
);
if (
!runLog.includes("canon_event=STRAIGHT_TRAVERSE") ||
!runLog.includes("canon_event=STRAIGHT_FEED")

View File

@@ -4839,6 +4839,14 @@
await verifyOpfsSavedText(label, () => loadTextFile(opfsPath), expectedLines);
}
function opfsSessionSavedReadbackPaths(session, savedParameters) {
return {
parameters: session.parameters.opfsPath,
parameterBackup: savedParameters.backupOpfsPath,
toolTable: session.toolTable.opfsPath,
};
}
async function verifyOpfsMachineFileReadback(label, machineId, fileKey, expectedLines) {
await verifyOpfsSavedText(label, async () => {
const machineFiles = await loadMachineTextFiles(machineId);
@@ -9881,20 +9889,22 @@
"\t",
iniNamedParameterReadbackValues,
);
const iniNamedSavedParameterTextPath = loadedIniFileSession.parameters.opfsPath;
const iniNamedSavedReadbackPaths = opfsSessionSavedReadbackPaths(
loadedIniFileSession,
savedIniNamedParameters,
);
await verifyOpfsReadback(
"opfs_saved_ini_named_parameter_text",
iniNamedSavedParameterTextPath,
iniNamedSavedReadbackPaths.parameters,
iniNamedSavedParameterTextLines,
);
const iniNamedSavedParameterBackupTextLines = opfsSavedParameterPlainTextLinesForValues(
" ",
iniNamedInitialParameterValues,
);
const iniNamedSavedParameterBackupPath = savedIniNamedParameters.backupOpfsPath;
await verifyOpfsReadback(
"opfs_saved_ini_named_parameter_backup_text",
iniNamedSavedParameterBackupPath,
iniNamedSavedReadbackPaths.parameterBackup,
iniNamedSavedParameterBackupTextLines,
);
const iniNamedLoadToolTableResultLines = opfsToolTableResultLinesForTool(
@@ -9937,10 +9947,9 @@
iniNamedSaveToolTableResultLines,
);
const iniNamedSavedToolTableTextLines = opfsToolTableTextLinesForTool(iniNamedToolTable);
const iniNamedSavedToolTableTextPath = loadedIniFileSession.toolTable.opfsPath;
await verifyOpfsReadback(
"opfs_saved_ini_named_tool_table_text",
iniNamedSavedToolTableTextPath,
iniNamedSavedReadbackPaths.toolTable,
iniNamedSavedToolTableTextLines,
);
@@ -10073,20 +10082,22 @@
"\t",
explicitParameterReadbackValues,
);
const explicitSavedParameterTextPath = loadedIniOverrideSession.parameters.opfsPath;
const explicitSavedReadbackPaths = opfsSessionSavedReadbackPaths(
loadedIniOverrideSession,
savedExplicitParameters,
);
await verifyOpfsReadback(
"opfs_saved_explicit_parameter_text",
explicitSavedParameterTextPath,
explicitSavedReadbackPaths.parameters,
explicitSavedParameterTextLines,
);
const explicitSavedParameterBackupTextLines = opfsSavedParameterPlainTextLinesForValues(
" ",
explicitInitialParameterValues,
);
const explicitSavedParameterBackupPath = savedExplicitParameters.backupOpfsPath;
await verifyOpfsReadback(
"opfs_saved_explicit_parameter_backup_text",
explicitSavedParameterBackupPath,
explicitSavedReadbackPaths.parameterBackup,
explicitSavedParameterBackupTextLines,
);
const savedExplicitToolTable = await saveMachineToolTableToOpfs(
@@ -10115,10 +10126,9 @@
explicitSaveToolTableResultLines,
);
const explicitSavedToolTableTextLines = opfsToolTableTextLinesForTool(explicitToolTable);
const explicitSavedToolTableTextPath = loadedIniOverrideSession.toolTable.opfsPath;
await verifyOpfsReadback(
"opfs_saved_explicit_tool_table_text",
explicitSavedToolTableTextPath,
explicitSavedReadbackPaths.toolTable,
explicitSavedToolTableTextLines,
);

View File

@@ -13,6 +13,7 @@ SKIP_INTERP_BUILD=1 "$ROOT_DIR/tests/wasm/node/verify_sim_configs_inventory_wasm
SKIP_INTERP_BUILD=1 "$ROOT_DIR/tests/wasm/node/verify_nc_files_wasm.sh"
SKIP_TP_BUILD=1 "$ROOT_DIR/tests/wasm/node/verify_tp_wasm.sh"
"$ROOT_DIR/tests/opfs/node/verify_file_service.sh"
"$ROOT_DIR/tests/ui/node/verify_ui_node_smokes.sh"
SKIP_INI_BUILD=1 SKIP_INTERP_BUILD=1 "$ROOT_DIR/tests/browser/verify_ini_panel_browser.sh"
SKIP_INI_BUILD=1 SKIP_INTERP_BUILD=1 "$ROOT_DIR/tests/browser/verify_interp_browser.sh"

View File

@@ -18,11 +18,14 @@ import {
import {
createMachineSessionSnapshotPayload,
createSessionSnapshot,
loadMachineSessionSnapshot,
loadSessionSnapshot,
saveMachineSessionSnapshot,
saveSessionSnapshot,
validateSessionSnapshot,
} from "../../../runtime/opfs/snapshot-store.js";
import {
gcodeFilenameFromProgramPath,
loadGcodeProgram,
loadMachineTextFiles,
machineFilePaths,
@@ -199,6 +202,18 @@ assertGcodeProgramStoragePath(
"custom-fixture.ngc",
gcodeProgramPath("custom-fixture.ngc"),
);
assert.equal(
gcodeFilenameFromProgramPath("linuxcnc/gcode/fixture.ngc"),
"fixture.ngc",
);
assert.throws(
() => gcodeFilenameFromProgramPath("linuxcnc/machines/xyzab-tdr/fixture.ngc"),
/Invalid G-code program OPFS path/,
);
assert.throws(
() => gcodeFilenameFromProgramPath("linuxcnc/gcode/nested/fixture.ngc"),
/Invalid G-code program OPFS path/,
);
assertPreviewCacheStoragePath("fixture", previewCachePath("fixture"));
assertPreviewCacheStoragePath(
"fixture",
@@ -325,6 +340,64 @@ assert.deepEqual(
}),
customSnapshot,
);
const machineSessionSnapshot = await saveMachineSessionSnapshot(
"machine-session-1",
"xyzab-tdr",
{
storage,
filename: "machine-session-snapshot.json",
gcodeFilename: "fixture.ngc",
createdAt: "2026-06-08T00:00:00.000Z",
metadata: { source: "node-machine-session" },
},
);
assertSessionSnapshotStoragePath(
"machine-session-1",
sessionSnapshotPath("machine-session-1", "machine-session-snapshot.json"),
"machine-session-snapshot.json",
);
assert.deepEqual(machineSessionSnapshot.payload, snapshotPayload);
assert.deepEqual(
await loadSessionSnapshot("machine-session-1", {
storage,
filename: "machine-session-snapshot.json",
}),
machineSessionSnapshot,
);
assert.deepEqual(
await loadMachineSessionSnapshot("machine-session-1", "xyzab-tdr", {
storage,
filename: "machine-session-snapshot.json",
}),
machineSessionSnapshot,
);
await assert.rejects(
() => loadMachineSessionSnapshot("machine-session-1", "other-machine", {
storage,
filename: "machine-session-snapshot.json",
}),
/Machine session snapshot id mismatch/,
);
await saveTextFile(
sessionSnapshotPath("machine-session-bad-files"),
JSON.stringify({
...machineSessionSnapshot,
sessionId: "machine-session-bad-files",
payload: {
machineId: "xyzab-tdr",
files: {
ini: "linuxcnc/machines/xyzab-tdr/machine.ini",
parameters: "../linuxcnc.var",
toolTable: "linuxcnc/machines/xyzab-tdr/tool.tbl",
},
},
}),
storage,
);
await assert.rejects(
() => loadMachineSessionSnapshot("machine-session-bad-files", "xyzab-tdr", { storage }),
/Invalid OPFS path/,
);
await saveMachineTextFiles("xyzab-tdr", {
ini: "[EMC]\nMACHINE = xyzab-tdr\n",

View File

@@ -0,0 +1,73 @@
import assert from "node:assert/strict";
import {
createMachineFileLoadSummary,
createMachineFileSaveSummary,
machineFileLoadLogLines,
machineFileSaveLogLines,
} from "../../../runtime/ui/ini-panel/machine-file-summary.js";
const saveSummary = createMachineFileSaveSummary({
ini: "linuxcnc/machines/xyzab-tdr/machine.ini",
toolTable: "linuxcnc/machines/xyzab-tdr/tool.tbl",
parameters: "linuxcnc/machines/xyzab-tdr/linuxcnc.var",
}, "linuxcnc/gcode/ui-session.ngc");
assert.deepEqual(saveSummary, {
iniOpfsPath: "linuxcnc/machines/xyzab-tdr/machine.ini",
toolTableOpfsPath: "linuxcnc/machines/xyzab-tdr/tool.tbl",
parameterOpfsPath: "linuxcnc/machines/xyzab-tdr/linuxcnc.var",
gcodeOpfsPath: "linuxcnc/gcode/ui-session.ngc",
});
assert.deepEqual(
machineFileSaveLogLines(saveSummary),
[
"INI: linuxcnc/machines/xyzab-tdr/machine.ini",
"Tool table: linuxcnc/machines/xyzab-tdr/tool.tbl",
"Parameters: linuxcnc/machines/xyzab-tdr/linuxcnc.var",
"G-code: linuxcnc/gcode/ui-session.ngc",
],
);
const loadSummary = createMachineFileLoadSummary({
ini: "[EMC]\nMACHINE = xyzab-tdr\n",
toolTable: "T2 P7 Z3.125\n",
parameters: "5220 1\n",
});
assert.deepEqual(loadSummary, {
iniBytes: 26,
toolTableBytes: 13,
parameterBytes: 7,
});
assert.deepEqual(
machineFileLoadLogLines(loadSummary),
[
"INI bytes: 26",
"Tool table bytes: 13",
"Parameter bytes: 7",
],
);
assert.deepEqual(
createMachineFileSaveSummary(null),
{
iniOpfsPath: null,
toolTableOpfsPath: null,
parameterOpfsPath: null,
gcodeOpfsPath: null,
},
);
assert.deepEqual(
createMachineFileLoadSummary(null),
{
iniBytes: 0,
toolTableBytes: 0,
parameterBytes: 0,
},
);
console.log("ini_panel_machine_file_summary_node_smoke=ok");

View File

@@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)"
node "$ROOT_DIR/tests/ui/node/verify_ini_panel_machine_file_summary.mjs"

View File

@@ -0,0 +1,99 @@
import assert from "node:assert/strict";
import {
createRunSummary,
summarizeRunResult,
} from "../../../runtime/ui/ini-panel/run-summary.js";
const program = {
source: "snapshot",
opfsPath: "linuxcnc/gcode/ui-session.ngc",
wasmPath: "/work/ui-session.ngc",
};
const snapshot = {
sessionId: "ui-machine-session",
metadata: {
workflow: "save-session-snapshot",
snapshotLabel: "ui-machine-session/ui-machine-session.json",
},
};
assert.deepEqual(
createRunSummary(program, {
runStatus: "ok",
iniWasmPath: "/work/session-machine.ini",
canonicalEventCount: 2,
motionSnapshotCount: 2,
}, snapshot),
{
runStatus: "ok",
programSource: "snapshot",
snapshotLabel: "ui-machine-session/ui-machine-session.json",
workflow: "save-session-snapshot",
programOpfsPath: "linuxcnc/gcode/ui-session.ngc",
programWasmPath: "/work/ui-session.ngc",
iniWasmPath: "/work/session-machine.ini",
canonicalEventCount: 2,
motionSnapshotCount: 2,
},
);
assert.deepEqual(
createRunSummary({
source: "default",
opfsPath: "linuxcnc/gcode/ui-session.ngc",
wasmPath: "/work/ui-session.ngc",
}),
{
runStatus: "ok",
programSource: "default",
snapshotLabel: null,
workflow: null,
programOpfsPath: "linuxcnc/gcode/ui-session.ngc",
programWasmPath: "/work/ui-session.ngc",
iniWasmPath: null,
canonicalEventCount: 0,
motionSnapshotCount: 0,
},
);
assert.deepEqual(
createRunSummary(program, {
snapshotLabel: "override-session/override.json",
}, snapshot),
{
runStatus: "ok",
programSource: "snapshot",
snapshotLabel: "override-session/override.json",
workflow: "save-session-snapshot",
programOpfsPath: "linuxcnc/gcode/ui-session.ngc",
programWasmPath: "/work/ui-session.ngc",
iniWasmPath: null,
canonicalEventCount: 0,
motionSnapshotCount: 0,
},
);
assert.deepEqual(
summarizeRunResult([
"canon_event=STRAIGHT_TRAVERSE line=1 x=1 y=2",
"run_step phase=execute line=1",
"canon_event=STRAIGHT_FEED line=2 x=3 y=4",
"message=canon_event=not-a-canonical-prefix",
"",
].join("\n"), [{}, {}]),
{
canonicalEventCount: 2,
motionSnapshotCount: 2,
},
);
assert.deepEqual(
summarizeRunResult("", []),
{
canonicalEventCount: 0,
motionSnapshotCount: 0,
},
);
console.log("ini_panel_run_summary_node_smoke=ok");

View File

@@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)"
node "$ROOT_DIR/tests/ui/node/verify_ini_panel_run_summary.mjs"

View File

@@ -0,0 +1,149 @@
import assert from "node:assert/strict";
import {
runGcodeSessionWorkflow,
selectSessionGcodeProgram,
} from "../../../runtime/ui/ini-panel/run-workflow.js";
const defaultProgram = selectSessionGcodeProgram({
defaultFilename: "ui-session.ngc",
defaultOpfsPath: "linuxcnc/gcode/ui-session.ngc",
wasmPath: "/work/ui-session.ngc",
gcodeFilenameFromProgramPath: () => {
throw new Error("not used");
},
});
assert.deepEqual(defaultProgram, {
filename: "ui-session.ngc",
opfsPath: "linuxcnc/gcode/ui-session.ngc",
source: "default",
wasmPath: "/work/ui-session.ngc",
});
const snapshotProgram = selectSessionGcodeProgram({
snapshot: {
payload: {
files: {
gcode: "linuxcnc/gcode/snapshot-session.ngc",
},
},
},
defaultFilename: "ui-session.ngc",
defaultOpfsPath: "linuxcnc/gcode/ui-session.ngc",
wasmPath: "/work/ui-session.ngc",
gcodeFilenameFromProgramPath: (path) => path.split("/").at(-1),
});
assert.deepEqual(snapshotProgram, {
filename: "snapshot-session.ngc",
opfsPath: "linuxcnc/gcode/snapshot-session.ngc",
source: "snapshot",
wasmPath: "/work/ui-session.ngc",
});
const calls = [];
const workflow = await runGcodeSessionWorkflow({
interp: {
writeTextFile: (wasmPath, text) => {
calls.push(["writeTextFile", wasmPath, text]);
},
runFileWithIni: (programWasmPath, iniWasmPath) => {
calls.push(["runFileWithIni", programWasmPath, iniWasmPath]);
return [
"canon_event=STRAIGHT_FEED line=1 x=1 y=2",
"run_step phase=execute line=1 x=1 y=2 statement_uri=G1%20X1%20Y2",
"",
].join("\n");
},
},
loadedSession: {
ini: {
wasmPath: "/work/session-machine.ini",
},
},
snapshot: {
sessionId: "ui-machine-session",
metadata: {
workflow: "save-session-snapshot",
snapshotLabel: "ui-machine-session/ui-machine-session.json",
defaultGcodeOpfsPath: "linuxcnc/gcode/ui-session.ngc",
},
payload: {
files: {
gcode: "linuxcnc/gcode/snapshot-session.ngc",
},
},
},
defaultFilename: "ui-session.ngc",
defaultOpfsPath: "linuxcnc/gcode/ui-session.ngc",
wasmPath: "/work/ui-session.ngc",
gcodeFilenameFromProgramPath: (path) => path.split("/").at(-1),
loadGcodeProgram: async (filename) => {
calls.push(["loadGcodeProgram", filename]);
return "G1 X1 Y2\n";
},
snapshotLabel: "ui-machine-session/ui-machine-session.json",
summarizeRun: (resultText, programText) => {
calls.push(["summarizeRun", resultText, programText]);
return {
canonicalEventCount: 1,
motionSnapshotCount: 1,
motionSnapshots: [{ line: 1 }],
};
},
});
assert.deepEqual(calls, [
["loadGcodeProgram", "snapshot-session.ngc"],
["writeTextFile", "/work/ui-session.ngc", "G1 X1 Y2\n"],
["runFileWithIni", "/work/ui-session.ngc", "/work/session-machine.ini"],
[
"summarizeRun",
"canon_event=STRAIGHT_FEED line=1 x=1 y=2\nrun_step phase=execute line=1 x=1 y=2 statement_uri=G1%20X1%20Y2",
"G1 X1 Y2\n",
],
]);
assert.deepEqual(workflow.summary, {
runStatus: "ok",
programSource: "snapshot",
snapshotLabel: "ui-machine-session/ui-machine-session.json",
workflow: "save-session-snapshot",
programOpfsPath: "linuxcnc/gcode/snapshot-session.ngc",
programWasmPath: "/work/ui-session.ngc",
iniWasmPath: "/work/session-machine.ini",
canonicalEventCount: 1,
motionSnapshotCount: 1,
});
assert.deepEqual(workflow.fields, {
sessionGcode: "linuxcnc/gcode/snapshot-session.ngc",
runStatus: "ok",
runSource: "snapshot",
runSnapshot: "ui-machine-session/ui-machine-session.json",
runWorkflow: "save-session-snapshot",
runOpfsPath: "linuxcnc/gcode/snapshot-session.ngc",
runWasmPath: "/work/ui-session.ngc",
});
assert.deepEqual(workflow.motionSnapshots, [{ line: 1 }]);
assert.deepEqual(workflow.logLines, [
"Program: linuxcnc/gcode/snapshot-session.ngc -> /work/ui-session.ngc",
"Program source: snapshot",
"Snapshot: ui-machine-session/ui-machine-session.json",
"Workflow: save-session-snapshot",
"Default G-code: linuxcnc/gcode/ui-session.ngc",
"INI: /work/session-machine.ini",
"canon_event=STRAIGHT_FEED line=1 x=1 y=2\nrun_step phase=execute line=1 x=1 y=2 statement_uri=G1%20X1%20Y2",
]);
await assert.rejects(
() => runGcodeSessionWorkflow({
loadedSession: null,
}),
/Load a machine session before running G-code/,
);
console.log("ini_panel_run_workflow_node_smoke=ok");

View File

@@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)"
node "$ROOT_DIR/tests/ui/node/verify_ini_panel_run_workflow.mjs"

View File

@@ -0,0 +1,130 @@
import assert from "node:assert/strict";
import {
createMachineSessionLoadSummary,
machineSessionLoadLogLines,
} from "../../../runtime/ui/ini-panel/session-load-summary.js";
import {
createSessionSnapshotSummary,
sessionSnapshotMetadataLogLines,
sessionSnapshotSummaryLogLines,
} from "../../../runtime/ui/ini-panel/session-summary.js";
const snapshot = {
metadata: {
workflow: "save-session-snapshot",
snapshotLabel: "ui-machine-session/ui-machine-session.json",
defaultGcodeOpfsPath: "linuxcnc/gcode/ui-session.ngc",
},
payload: {
files: {
ini: "linuxcnc/machines/xyzab-tdr/machine.ini",
parameters: "linuxcnc/machines/xyzab-tdr/linuxcnc.var",
toolTable: "linuxcnc/machines/xyzab-tdr/tool.tbl",
gcode: "linuxcnc/gcode/ui-session.ngc",
},
},
};
const summary = createSessionSnapshotSummary(snapshot);
assert.deepEqual(summary, {
snapshotLabel: "ui-machine-session/ui-machine-session.json",
workflow: "save-session-snapshot",
defaultGcodeOpfsPath: "linuxcnc/gcode/ui-session.ngc",
iniOpfsPath: "linuxcnc/machines/xyzab-tdr/machine.ini",
parameterOpfsPath: "linuxcnc/machines/xyzab-tdr/linuxcnc.var",
toolTableOpfsPath: "linuxcnc/machines/xyzab-tdr/tool.tbl",
gcodeOpfsPath: "linuxcnc/gcode/ui-session.ngc",
});
assert.deepEqual(
createSessionSnapshotSummary(snapshot, "override-session/override.json"),
{
...summary,
snapshotLabel: "override-session/override.json",
},
);
assert.deepEqual(
sessionSnapshotSummaryLogLines(summary),
[
"Snapshot: ui-machine-session/ui-machine-session.json",
"Workflow: save-session-snapshot",
"Default G-code: linuxcnc/gcode/ui-session.ngc",
],
);
assert.deepEqual(
sessionSnapshotMetadataLogLines(summary),
[
"Workflow: save-session-snapshot",
"Default G-code: linuxcnc/gcode/ui-session.ngc",
],
);
assert.deepEqual(
createSessionSnapshotSummary(null),
{
snapshotLabel: null,
workflow: null,
defaultGcodeOpfsPath: null,
iniOpfsPath: null,
parameterOpfsPath: null,
toolTableOpfsPath: null,
gcodeOpfsPath: null,
},
);
const loadSummary = createMachineSessionLoadSummary({
ini: {
opfsPath: "linuxcnc/machines/xyzab-tdr/machine.ini",
wasmPath: "/work/session-machine.ini",
},
parameters: {
opfsPath: "linuxcnc/machines/xyzab-tdr/linuxcnc.var",
wasmPath: "/work/session-linuxcnc.var",
result: "restore_parameters=0",
},
toolTable: {
opfsPath: "linuxcnc/machines/xyzab-tdr/tool.tbl",
wasmPath: "/work/session-tool.tbl",
result: "tooldata_load=0",
},
});
assert.deepEqual(loadSummary, {
iniOpfsPath: "linuxcnc/machines/xyzab-tdr/machine.ini",
iniWasmPath: "/work/session-machine.ini",
parameterOpfsPath: "linuxcnc/machines/xyzab-tdr/linuxcnc.var",
parameterWasmPath: "/work/session-linuxcnc.var",
parameterResult: "restore_parameters=0",
toolTableOpfsPath: "linuxcnc/machines/xyzab-tdr/tool.tbl",
toolTableWasmPath: "/work/session-tool.tbl",
toolTableResult: "tooldata_load=0",
});
assert.deepEqual(
machineSessionLoadLogLines(loadSummary),
[
"INI: linuxcnc/machines/xyzab-tdr/machine.ini -> /work/session-machine.ini",
"Parameter file: linuxcnc/machines/xyzab-tdr/linuxcnc.var -> /work/session-linuxcnc.var",
"Parameters: restore_parameters=0",
"Tool table file: linuxcnc/machines/xyzab-tdr/tool.tbl -> /work/session-tool.tbl",
"Tool table: tooldata_load=0",
],
);
assert.deepEqual(
createMachineSessionLoadSummary(null),
{
iniOpfsPath: null,
iniWasmPath: null,
parameterOpfsPath: null,
parameterWasmPath: null,
parameterResult: "",
toolTableOpfsPath: null,
toolTableWasmPath: null,
toolTableResult: "",
},
);
console.log("ini_panel_session_summary_node_smoke=ok");

View File

@@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)"
node "$ROOT_DIR/tests/ui/node/verify_ini_panel_session_summary.mjs"

View File

@@ -0,0 +1,252 @@
import assert from "node:assert/strict";
import {
restoreMachineSessionSnapshotWorkflow,
saveMachineSessionSnapshotWorkflow,
} from "../../../runtime/ui/ini-panel/session-workflow.js";
const calls = [];
const saveWorkflow = await saveMachineSessionSnapshotWorkflow({
machineId: "xyzab-tdr",
snapshotId: "ui-machine-session",
snapshotFilename: "ui-machine-session.json",
snapshotLabel: "ui-machine-session/ui-machine-session.json",
iniText: "[EMC]\nMACHINE = xyzab-tdr\n",
toolTableText: "T2 P7 Z3.125 D1.5 I12 J34 Q4 ;ui session tool\n",
parameterText: "5161 0.0\n",
gcodeFilename: "ui-session.ngc",
gcodeText: "G0 X1.0 Y2.0 (Comment)\nG1 X3.0 Y4.0 F120.0\n",
metadata: {
source: "ini-panel",
workflow: "save-session-snapshot",
snapshotLabel: "ui-machine-session/ui-machine-session.json",
defaultGcodeOpfsPath: "linuxcnc/gcode/ui-session.ngc",
},
saveMachineTextFiles: async (machineId, files) => {
calls.push(["saveMachineTextFiles", machineId, files]);
},
saveGcodeProgram: async (filename, text) => {
calls.push(["saveGcodeProgram", filename, text]);
},
saveMachineSessionSnapshot: async (snapshotId, machineId, options) => {
calls.push(["saveMachineSessionSnapshot", snapshotId, machineId, options]);
return {
sessionId: snapshotId,
metadata: options.metadata,
payload: {
files: {
ini: "linuxcnc/machines/xyzab-tdr/machine.ini",
parameters: "linuxcnc/machines/xyzab-tdr/linuxcnc.var",
toolTable: "linuxcnc/machines/xyzab-tdr/tool.tbl",
gcode: "linuxcnc/gcode/ui-session.ngc",
},
},
};
},
});
assert.deepEqual(calls, [
[
"saveMachineTextFiles",
"xyzab-tdr",
{
ini: "[EMC]\nMACHINE = xyzab-tdr\n",
toolTable: "T2 P7 Z3.125 D1.5 I12 J34 Q4 ;ui session tool\n",
parameters: "5161 0.0\n",
},
],
[
"saveGcodeProgram",
"ui-session.ngc",
"G0 X1.0 Y2.0 (Comment)\nG1 X3.0 Y4.0 F120.0\n",
],
[
"saveMachineSessionSnapshot",
"ui-machine-session",
"xyzab-tdr",
{
filename: "ui-machine-session.json",
gcodeFilename: "ui-session.ngc",
metadata: {
source: "ini-panel",
workflow: "save-session-snapshot",
snapshotLabel: "ui-machine-session/ui-machine-session.json",
defaultGcodeOpfsPath: "linuxcnc/gcode/ui-session.ngc",
},
},
],
]);
assert.deepEqual(saveWorkflow.summary, {
snapshotLabel: "ui-machine-session/ui-machine-session.json",
workflow: "save-session-snapshot",
defaultGcodeOpfsPath: "linuxcnc/gcode/ui-session.ngc",
iniOpfsPath: "linuxcnc/machines/xyzab-tdr/machine.ini",
parameterOpfsPath: "linuxcnc/machines/xyzab-tdr/linuxcnc.var",
toolTableOpfsPath: "linuxcnc/machines/xyzab-tdr/tool.tbl",
gcodeOpfsPath: "linuxcnc/gcode/ui-session.ngc",
});
assert.deepEqual(saveWorkflow.fields, {
sessionIni: "linuxcnc/machines/xyzab-tdr/machine.ini",
sessionParameters: "linuxcnc/machines/xyzab-tdr/linuxcnc.var",
sessionToolTable: "linuxcnc/machines/xyzab-tdr/tool.tbl",
sessionGcode: "linuxcnc/gcode/ui-session.ngc",
sessionSnapshot: "ui-machine-session/ui-machine-session.json",
});
assert.deepEqual(saveWorkflow.logLines, [
"Snapshot: ui-machine-session/ui-machine-session.json",
"Workflow: save-session-snapshot",
"Default G-code: linuxcnc/gcode/ui-session.ngc",
"INI: linuxcnc/machines/xyzab-tdr/machine.ini",
"Parameter file: linuxcnc/machines/xyzab-tdr/linuxcnc.var",
"Tool table file: linuxcnc/machines/xyzab-tdr/tool.tbl",
"G-code: linuxcnc/gcode/ui-session.ngc",
]);
const restoreCalls = [];
const restoreWorkflow = await restoreMachineSessionSnapshotWorkflow({
machineId: "xyzab-tdr",
snapshotId: "ui-machine-session",
snapshotFilename: "ui-machine-session.json",
snapshotLabel: "ui-machine-session/ui-machine-session.json",
loadMachineSessionSnapshot: async (snapshotId, machineId, options) => {
restoreCalls.push(["loadMachineSessionSnapshot", snapshotId, machineId, options]);
return {
sessionId: snapshotId,
metadata: {
workflow: "save-session-snapshot",
snapshotLabel: "ui-machine-session/ui-machine-session.json",
defaultGcodeOpfsPath: "linuxcnc/gcode/ui-session.ngc",
},
payload: {
files: {
ini: "linuxcnc/machines/xyzab-tdr/machine.ini",
parameters: "linuxcnc/machines/xyzab-tdr/linuxcnc.var",
toolTable: "linuxcnc/machines/xyzab-tdr/tool.tbl",
gcode: "linuxcnc/gcode/ui-session.ngc",
},
},
};
},
loadMachineTextFiles: async (machineId) => {
restoreCalls.push(["loadMachineTextFiles", machineId]);
return {
ini: "[EMC]\nMACHINE = xyzab-tdr\n",
};
},
});
assert.deepEqual(restoreCalls, [
[
"loadMachineSessionSnapshot",
"ui-machine-session",
"xyzab-tdr",
{ filename: "ui-machine-session.json" },
],
["loadMachineTextFiles", "xyzab-tdr"],
]);
assert.deepEqual(restoreWorkflow.fields, {
sessionIni: "linuxcnc/machines/xyzab-tdr/machine.ini",
sessionParameters: "linuxcnc/machines/xyzab-tdr/linuxcnc.var",
sessionToolTable: "linuxcnc/machines/xyzab-tdr/tool.tbl",
sessionGcode: "linuxcnc/gcode/ui-session.ngc",
sessionSnapshot: "ui-machine-session/ui-machine-session.json",
});
assert.deepEqual(restoreWorkflow.logLines, [
"Snapshot: ui-machine-session/ui-machine-session.json",
"Workflow: save-session-snapshot",
"Default G-code: linuxcnc/gcode/ui-session.ngc",
"INI: linuxcnc/machines/xyzab-tdr/machine.ini",
"Parameter file: linuxcnc/machines/xyzab-tdr/linuxcnc.var",
"Tool table file: linuxcnc/machines/xyzab-tdr/tool.tbl",
"G-code: linuxcnc/gcode/ui-session.ngc",
]);
const restoreLoadCalls = [];
const restoreLoadWorkflow = await restoreMachineSessionSnapshotWorkflow({
machineId: "xyzab-tdr",
snapshotId: "ui-machine-session",
snapshotFilename: "ui-machine-session.json",
snapshotLabel: "ui-machine-session/ui-machine-session.json",
loadMachineSessionSnapshot: async (snapshotId, machineId, options) => {
restoreLoadCalls.push(["loadMachineSessionSnapshot", snapshotId, machineId, options]);
return {
sessionId: snapshotId,
metadata: {
workflow: "save-session-snapshot",
snapshotLabel: "ui-machine-session/ui-machine-session.json",
defaultGcodeOpfsPath: "linuxcnc/gcode/ui-session.ngc",
},
payload: {
files: {
ini: "linuxcnc/machines/xyzab-tdr/machine.ini",
parameters: "linuxcnc/machines/xyzab-tdr/linuxcnc.var",
toolTable: "linuxcnc/machines/xyzab-tdr/tool.tbl",
gcode: "linuxcnc/gcode/ui-session.ngc",
},
},
};
},
loadMachineTextFiles: async (machineId) => {
restoreLoadCalls.push(["loadMachineTextFiles", machineId]);
return {
ini: "[EMC]\nMACHINE = xyzab-tdr\n",
};
},
loadSession: async () => {
restoreLoadCalls.push(["loadSession"]);
return {
ini: {
opfsPath: "linuxcnc/machines/xyzab-tdr/machine.ini",
wasmPath: "/work/session-machine.ini",
},
parameters: {
opfsPath: "linuxcnc/machines/xyzab-tdr/linuxcnc.var",
wasmPath: "/work/session-linuxcnc.var",
result: "restore_parameters=0",
},
toolTable: {
opfsPath: "linuxcnc/machines/xyzab-tdr/tool.tbl",
wasmPath: "/work/session-tool.tbl",
result: "tooldata_load=0",
},
};
},
});
assert.deepEqual(restoreLoadCalls, [
[
"loadMachineSessionSnapshot",
"ui-machine-session",
"xyzab-tdr",
{ filename: "ui-machine-session.json" },
],
["loadMachineTextFiles", "xyzab-tdr"],
["loadSession"],
]);
assert.deepEqual(restoreLoadWorkflow.fields, {
sessionIni: "/work/session-machine.ini",
sessionParameters: "/work/session-linuxcnc.var",
sessionToolTable: "/work/session-tool.tbl",
sessionGcode: "linuxcnc/gcode/ui-session.ngc",
sessionSnapshot: "ui-machine-session/ui-machine-session.json",
});
assert.deepEqual(restoreLoadWorkflow.logLines, [
"Snapshot: ui-machine-session/ui-machine-session.json",
"Workflow: save-session-snapshot",
"Default G-code: linuxcnc/gcode/ui-session.ngc",
"INI: linuxcnc/machines/xyzab-tdr/machine.ini -> /work/session-machine.ini",
"Parameter file: linuxcnc/machines/xyzab-tdr/linuxcnc.var -> /work/session-linuxcnc.var",
"Parameters: restore_parameters=0",
"Tool table file: linuxcnc/machines/xyzab-tdr/tool.tbl -> /work/session-tool.tbl",
"Tool table: tooldata_load=0",
"G-code: linuxcnc/gcode/ui-session.ngc",
]);
console.log("ini_panel_session_workflow_node_smoke=ok");

View File

@@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)"
node "$ROOT_DIR/tests/ui/node/verify_ini_panel_session_workflow.mjs"

View File

@@ -0,0 +1,613 @@
import assert from "node:assert/strict";
import {
createIniPanelStateSummary,
createMachineSessionControlView,
createMachineSessionStateSnapshot,
createMachineSessionStateBundle,
createMachineSessionStateReport,
createMachineSessionStateReportExport,
createMachineSessionWorkflowStatus,
} from "../../../runtime/ui/ini-panel/panel-state-summary.js";
import {
renderMachineSessionControlView,
} from "../../../runtime/ui/ini-panel/control-view-renderer.js";
const summary = createIniPanelStateSummary({
badges: {
ini: "INI WASM: ready",
interp: "Interpreter WASM: ready",
opfs: "OPFS: ready",
},
machine: {
machine: "xyzab-tdr",
kinematics: "trivkins",
},
machineFiles: {
iniOpfsPath: "linuxcnc/machines/xyzab-tdr/machine.ini",
toolTableOpfsPath: "linuxcnc/machines/xyzab-tdr/tool.tbl",
parameterOpfsPath: "linuxcnc/machines/xyzab-tdr/linuxcnc.var",
gcodeOpfsPath: "linuxcnc/gcode/ui-session.ngc",
},
sessionSnapshot: {
snapshotLabel: "ui-machine-session/ui-machine-session.json",
workflow: "save-session-snapshot",
},
sessionLoad: {
iniOpfsPath: "linuxcnc/machines/xyzab-tdr/machine.ini",
iniWasmPath: "/work/session-machine.ini",
},
run: {
runStatus: "ok",
programSource: "snapshot",
},
fiveaxisRemap: "ok",
});
assert.deepEqual(summary, {
badges: {
ini: "INI WASM: ready",
interp: "Interpreter WASM: ready",
opfs: "OPFS: ready",
},
machine: {
machine: "xyzab-tdr",
kinematics: "trivkins",
},
machineFiles: {
iniOpfsPath: "linuxcnc/machines/xyzab-tdr/machine.ini",
toolTableOpfsPath: "linuxcnc/machines/xyzab-tdr/tool.tbl",
parameterOpfsPath: "linuxcnc/machines/xyzab-tdr/linuxcnc.var",
gcodeOpfsPath: "linuxcnc/gcode/ui-session.ngc",
},
sessionSnapshot: {
snapshotLabel: "ui-machine-session/ui-machine-session.json",
workflow: "save-session-snapshot",
},
sessionLoad: {
iniOpfsPath: "linuxcnc/machines/xyzab-tdr/machine.ini",
iniWasmPath: "/work/session-machine.ini",
},
run: {
runStatus: "ok",
programSource: "snapshot",
},
fiveaxisRemap: "ok",
});
assert.deepEqual(
createIniPanelStateSummary(),
{
badges: {
ini: null,
interp: null,
opfs: null,
},
machine: null,
machineFiles: null,
sessionSnapshot: null,
sessionLoad: null,
run: null,
fiveaxisRemap: null,
},
);
assert.deepEqual(
createMachineSessionStateSnapshot({
badges: {
ini: "INI WASM: ready",
interp: "Interpreter WASM: ready",
opfs: "OPFS: ready",
},
machine: {
machine: "xyzab-tdr",
},
sessionSnapshot: {
snapshotLabel: "ui-machine-session/ui-machine-session.json",
},
sessionLoad: {
iniWasmPath: "/work/session-machine.ini",
},
selectedProgram: {
source: "snapshot",
opfsPath: "linuxcnc/gcode/ui-session.ngc",
wasmPath: "/work/ui-session.ngc",
},
run: {
runStatus: "ok",
programSource: "snapshot",
},
fields: {
sessionIni: "/work/session-machine.ini",
sessionParameters: "/work/session-linuxcnc.var",
sessionToolTable: "/work/session-tool.tbl",
sessionGcode: "linuxcnc/gcode/ui-session.ngc",
sessionSnapshot: "ui-machine-session/ui-machine-session.json",
runStatus: "ok",
runSource: "snapshot",
runSnapshot: "ui-machine-session/ui-machine-session.json",
runWorkflow: "save-session-snapshot",
runOpfsPath: "linuxcnc/gcode/ui-session.ngc",
runWasmPath: "/work/ui-session.ngc",
},
lastAction: "run-gcode",
fiveaxisRemap: "ok",
}),
{
badges: {
ini: "INI WASM: ready",
interp: "Interpreter WASM: ready",
opfs: "OPFS: ready",
},
machine: {
machine: "xyzab-tdr",
},
machineFiles: null,
sessionSnapshot: {
snapshotLabel: "ui-machine-session/ui-machine-session.json",
},
sessionLoad: {
iniWasmPath: "/work/session-machine.ini",
},
run: {
runStatus: "ok",
programSource: "snapshot",
},
fiveaxisRemap: "ok",
selectedProgram: {
source: "snapshot",
opfsPath: "linuxcnc/gcode/ui-session.ngc",
wasmPath: "/work/ui-session.ngc",
},
fields: {
sessionIni: "/work/session-machine.ini",
sessionParameters: "/work/session-linuxcnc.var",
sessionToolTable: "/work/session-tool.tbl",
sessionGcode: "linuxcnc/gcode/ui-session.ngc",
sessionSnapshot: "ui-machine-session/ui-machine-session.json",
runStatus: "ok",
runSource: "snapshot",
runSnapshot: "ui-machine-session/ui-machine-session.json",
runWorkflow: "save-session-snapshot",
runOpfsPath: "linuxcnc/gcode/ui-session.ngc",
runWasmPath: "/work/ui-session.ngc",
},
lastAction: "run-gcode",
error: null,
},
);
assert.deepEqual(
createMachineSessionStateSnapshot().fields,
{
sessionIni: null,
sessionParameters: null,
sessionToolTable: null,
sessionGcode: null,
sessionSnapshot: null,
runStatus: null,
runSource: null,
runSnapshot: null,
runWorkflow: null,
runOpfsPath: null,
runWasmPath: null,
},
);
assert.deepEqual(
createMachineSessionStateReport({
badges: {
ini: "INI WASM: ready",
interp: "Interpreter WASM: ready",
opfs: "OPFS: ready",
},
fields: {
sessionSnapshot: "ui-machine-session/ui-machine-session.json",
sessionIni: "/work/session-machine.ini",
sessionParameters: "/work/session-linuxcnc.var",
sessionToolTable: "/work/session-tool.tbl",
sessionGcode: "linuxcnc/gcode/ui-session.ngc",
runStatus: "ok",
runSource: "snapshot",
runSnapshot: "ui-machine-session/ui-machine-session.json",
runWorkflow: "save-session-snapshot",
runOpfsPath: "linuxcnc/gcode/ui-session.ngc",
runWasmPath: "/work/ui-session.ngc",
},
sessionLoad: {
iniWasmPath: "/work/session-machine.ini",
parameterWasmPath: "/work/session-linuxcnc.var",
toolTableWasmPath: "/work/session-tool.tbl",
},
selectedProgram: {
source: "snapshot",
opfsPath: "linuxcnc/gcode/ui-session.ngc",
wasmPath: "/work/ui-session.ngc",
},
run: {
runStatus: "ok",
programSource: "snapshot",
programOpfsPath: "linuxcnc/gcode/ui-session.ngc",
programWasmPath: "/work/ui-session.ngc",
iniWasmPath: "/work/session-machine.ini",
canonicalEventCount: 2,
motionSnapshotCount: 2,
},
lastAction: "run-gcode",
error: null,
fiveaxisRemap: "ok",
}),
{
readiness: {
ini: "INI WASM: ready",
interp: "Interpreter WASM: ready",
opfs: "OPFS: ready",
},
session: {
snapshotLabel: "ui-machine-session/ui-machine-session.json",
workflow: "save-session-snapshot",
ini: "/work/session-machine.ini",
parameters: "/work/session-linuxcnc.var",
toolTable: "/work/session-tool.tbl",
gcode: "linuxcnc/gcode/ui-session.ngc",
},
loaded: {
iniWasmPath: "/work/session-machine.ini",
parameterWasmPath: "/work/session-linuxcnc.var",
toolTableWasmPath: "/work/session-tool.tbl",
},
program: {
source: "snapshot",
opfsPath: "linuxcnc/gcode/ui-session.ngc",
wasmPath: "/work/ui-session.ngc",
},
run: {
status: "ok",
canonicalEventCount: 2,
motionSnapshotCount: 2,
iniWasmPath: "/work/session-machine.ini",
},
status: {
lastAction: "run-gcode",
error: null,
fiveaxisRemap: "ok",
},
},
);
const expectedExportReport = {
readiness: {
ini: "INI WASM: ready",
interp: "Interpreter WASM: ready",
opfs: "OPFS: ready",
},
session: {
snapshotLabel: "ui-machine-session/ui-machine-session.json",
workflow: null,
ini: "/work/session-machine.ini",
parameters: null,
toolTable: null,
gcode: null,
},
loaded: {
iniWasmPath: "/work/session-machine.ini",
parameterWasmPath: null,
toolTableWasmPath: null,
},
program: {
source: null,
opfsPath: null,
wasmPath: null,
},
run: {
status: null,
canonicalEventCount: 0,
motionSnapshotCount: 0,
iniWasmPath: null,
},
status: {
lastAction: "save-session-snapshot",
error: null,
fiveaxisRemap: null,
},
};
assert.deepEqual(
createMachineSessionStateReportExport({
badges: {
ini: "INI WASM: ready",
interp: "Interpreter WASM: ready",
opfs: "OPFS: ready",
},
fields: {
sessionSnapshot: "ui-machine-session/ui-machine-session.json",
sessionIni: "/work/session-machine.ini",
},
sessionLoad: {
iniWasmPath: "/work/session-machine.ini",
},
lastAction: "save-session-snapshot",
}),
{
filename: "machine-session-state-report.json",
mediaType: "application/json",
report: expectedExportReport,
text: `${JSON.stringify(expectedExportReport, null, 2)}\n`,
},
);
assert.deepEqual(
createMachineSessionWorkflowStatus({
badges: {
ini: "INI WASM: ready",
interp: "Interpreter WASM: ready",
opfs: "OPFS: ready",
},
fields: {
sessionSnapshot: "ui-machine-session/ui-machine-session.json",
sessionIni: "/work/session-machine.ini",
sessionGcode: "linuxcnc/gcode/ui-session.ngc",
runStatus: "ok",
runWorkflow: "save-session-snapshot",
},
run: {
runStatus: "ok",
canonicalEventCount: 2,
motionSnapshotCount: 2,
},
lastAction: "run-gcode",
error: null,
}),
{
readiness: {
ini: "INI WASM: ready",
interp: "Interpreter WASM: ready",
opfs: "OPFS: ready",
},
lastAction: "run-gcode",
error: null,
session: {
snapshotLabel: "ui-machine-session/ui-machine-session.json",
workflow: "save-session-snapshot",
ini: "/work/session-machine.ini",
parameters: null,
toolTable: null,
gcode: "linuxcnc/gcode/ui-session.ngc",
},
run: {
status: "ok",
canonicalEventCount: 2,
motionSnapshotCount: 2,
iniWasmPath: null,
},
},
);
const expectedBundleReport = {
readiness: {
ini: "INI WASM: ready",
interp: "Interpreter WASM: ready",
opfs: "OPFS: ready",
},
session: {
snapshotLabel: "ui-machine-session/ui-machine-session.json",
workflow: "save-session-snapshot",
ini: "/work/session-machine.ini",
parameters: null,
toolTable: null,
gcode: "linuxcnc/gcode/ui-session.ngc",
},
loaded: {
iniWasmPath: null,
parameterWasmPath: null,
toolTableWasmPath: null,
},
program: {
source: null,
opfsPath: null,
wasmPath: null,
},
run: {
status: "ok",
canonicalEventCount: 2,
motionSnapshotCount: 2,
iniWasmPath: null,
},
status: {
lastAction: "run-gcode",
error: null,
fiveaxisRemap: null,
},
};
assert.deepEqual(
createMachineSessionStateBundle({
badges: {
ini: "INI WASM: ready",
interp: "Interpreter WASM: ready",
opfs: "OPFS: ready",
},
fields: {
sessionSnapshot: "ui-machine-session/ui-machine-session.json",
sessionIni: "/work/session-machine.ini",
sessionGcode: "linuxcnc/gcode/ui-session.ngc",
runStatus: "ok",
runWorkflow: "save-session-snapshot",
},
run: {
runStatus: "ok",
canonicalEventCount: 2,
motionSnapshotCount: 2,
},
lastAction: "run-gcode",
error: null,
}),
{
report: expectedBundleReport,
export: {
filename: "machine-session-state-report.json",
mediaType: "application/json",
report: expectedBundleReport,
text: `${JSON.stringify(expectedBundleReport, null, 2)}\n`,
},
workflowStatus: {
readiness: {
ini: "INI WASM: ready",
interp: "Interpreter WASM: ready",
opfs: "OPFS: ready",
},
lastAction: "run-gcode",
error: null,
session: {
snapshotLabel: "ui-machine-session/ui-machine-session.json",
workflow: "save-session-snapshot",
ini: "/work/session-machine.ini",
parameters: null,
toolTable: null,
gcode: "linuxcnc/gcode/ui-session.ngc",
},
run: {
status: "ok",
canonicalEventCount: 2,
motionSnapshotCount: 2,
iniWasmPath: null,
},
},
},
);
assert.deepEqual(
createMachineSessionControlView({
report: expectedBundleReport,
export: {
filename: "machine-session-state-report.json",
mediaType: "application/json",
report: expectedBundleReport,
text: `${JSON.stringify(expectedBundleReport, null, 2)}\n`,
},
workflowStatus: {
readiness: {
ini: "INI WASM: ready",
interp: "Interpreter WASM: ready",
opfs: "OPFS: ready",
},
lastAction: "run-gcode",
error: null,
session: {
snapshotLabel: "ui-machine-session/ui-machine-session.json",
workflow: "save-session-snapshot",
ini: "/work/session-machine.ini",
parameters: null,
toolTable: null,
gcode: "linuxcnc/gcode/ui-session.ngc",
},
run: {
status: "ok",
canonicalEventCount: 2,
motionSnapshotCount: 2,
iniWasmPath: null,
},
},
}),
{
status: {
lastAction: "run-gcode",
error: null,
runStatus: "ok",
},
sections: [
{
id: "readiness",
title: "Readiness",
rows: [
{ label: "INI", value: "INI WASM: ready" },
{ label: "Interpreter", value: "Interpreter WASM: ready" },
{ label: "OPFS", value: "OPFS: ready" },
],
},
{
id: "session",
title: "Session",
rows: [
{ label: "Snapshot", value: "ui-machine-session/ui-machine-session.json" },
{ label: "Workflow", value: "save-session-snapshot" },
{ label: "INI", value: "/work/session-machine.ini" },
{ label: "G-code", value: "linuxcnc/gcode/ui-session.ngc" },
],
},
{
id: "run",
title: "Run",
rows: [
{ label: "Status", value: "ok" },
{ label: "Canonical events", value: 2 },
{ label: "Motion snapshots", value: 2 },
{ label: "INI WASM", value: null },
],
},
],
},
);
function createElement(tagName) {
const node = {
tagName,
children: [],
dataset: {},
textContent: "",
ownerDocument: null,
append(...children) {
this.children.push(...children);
},
appendChild(child) {
this.children.push(child);
return child;
},
};
node.ownerDocument = { createElement };
return node;
}
const controlViewContainer = createElement("div");
const renderedControlView = renderMachineSessionControlView(
controlViewContainer,
createMachineSessionControlView({
report: expectedBundleReport,
workflowStatus: {
readiness: expectedBundleReport.readiness,
lastAction: "run-gcode",
error: null,
session: expectedBundleReport.session,
run: expectedBundleReport.run,
},
}),
);
assert.equal(renderedControlView.statusNode.textContent, "run-gcode | ok | -");
assert.equal(renderedControlView.sectionNodes.length, 3);
assert.equal(renderedControlView.sectionNodes[2].dataset.sectionId, "run");
assert.equal(
renderedControlView.sectionNodes[2].children[1].children[3].textContent,
"2",
);
assert.deepEqual(
{
getMachineSessionControlView: typeof globalThis.window?.linuxCncIniPanelApi?.getMachineSessionControlView,
renderMachineSessionControlView: typeof globalThis.window?.linuxCncIniPanelApi?.renderMachineSessionControlView,
getMachineSessionStateBundle: typeof globalThis.window?.linuxCncIniPanelApi?.getMachineSessionStateBundle,
getMachineSessionStateReport: typeof globalThis.window?.linuxCncIniPanelApi?.getMachineSessionStateReport,
getMachineSessionStateReportExport: typeof globalThis.window?.linuxCncIniPanelApi?.getMachineSessionStateReportExport,
getMachineSessionWorkflowStatus: typeof globalThis.window?.linuxCncIniPanelApi?.getMachineSessionWorkflowStatus,
},
{
getMachineSessionControlView: "undefined",
renderMachineSessionControlView: "undefined",
getMachineSessionStateBundle: "undefined",
getMachineSessionStateReport: "undefined",
getMachineSessionStateReportExport: "undefined",
getMachineSessionWorkflowStatus: "undefined",
},
);
console.log("ini_panel_state_summary_node_smoke=ok");

View File

@@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)"
node "$ROOT_DIR/tests/ui/node/verify_ini_panel_state_summary.mjs"

View File

@@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)"
"$ROOT_DIR/tests/ui/node/verify_ini_panel_run_summary.sh"
"$ROOT_DIR/tests/ui/node/verify_ini_panel_run_workflow.sh"
"$ROOT_DIR/tests/ui/node/verify_ini_panel_session_summary.sh"
"$ROOT_DIR/tests/ui/node/verify_ini_panel_session_workflow.sh"
"$ROOT_DIR/tests/ui/node/verify_ini_panel_machine_file_summary.sh"
"$ROOT_DIR/tests/ui/node/verify_ini_panel_state_summary.sh"
echo "ui_node_smokes=ok"