完善云端RUN执行反馈

This commit is contained in:
2026-06-23 03:59:47 -04:00
parent bb71613051
commit 37604fa5b3
33 changed files with 3724 additions and 534 deletions

View File

@@ -90,17 +90,26 @@ try {
await wait(800); await wait(800);
await captureStep("01-initial-ui", "初始界面", "确认浏览器应用、五轴预览区、G-code 区和 DRO 面板已渲染。"); await captureStep("01-initial-ui", "初始界面", "确认浏览器应用、五轴预览区、G-code 区和 DRO 面板已渲染。");
await page.evaluate(async ({ ini }) => { await page.evaluate(async ({ ini, gcode, sourceRel }) => {
await window.webRtcp5AxisSimulation.stageMachineFiles({ iniText: ini, storageMode: "memory" }); await window.webRtcp5AxisSimulation.stageMachineFiles({
}, { ini: iniText }); iniText: ini,
storageMode: "memory",
sourceTextOverrides: {
[sourceRel]: gcode,
},
});
}, { ini: iniText, gcode: gcodeText, sourceRel: VENDORED_GCODE_REL });
await waitForState((state) => state.machineFileStaging?.status === "staged", 20000, "test INI staged"); await waitForState((state) => state.machineFileStaging?.status === "staged", 20000, "test INI staged");
await captureStep("02-stage-test-ini", "Stage test_linuxcnc_source INI", "使用 working_run/test_linuxcnc_source/xyzac-trt.ini 重新 staging machine files。"); await captureStep("02-stage-test-ini", "Stage test_linuxcnc_source INI", "使用 working_run/test_linuxcnc_source/xyzac-trt.ini 重新 staging machine files。");
await page.select('[data-action="select-linuxcnc-gcode-source"]', VENDORED_GCODE_REL); await page.evaluate(({ sourceRel }) => {
window.webRtcp5AxisSimulation.dispatch({ type: "LOAD_LINUXCNC_GCODE_SOURCE", sourceRel });
}, { sourceRel: VENDORED_GCODE_REL });
await waitForState((state) => ( await waitForState((state) => (
state.activeProgram?.endsWith("impeller-7bl-xyzac.ngc") && state.activeProgram?.endsWith("impeller-7bl-xyzac.ngc") &&
state.programExecutionSourceMode === "linuxcnc-interpreter-wasm" && state.programExecutionSourceMode === "linuxcnc-interpreter-wasm" &&
state.taskHalSession?.programPath?.endsWith("impeller-7bl-xyzac.ngc") state.taskHalSession?.programPath?.endsWith("impeller-7bl-xyzac.ngc") &&
state.loadedSourceBytes === sourceEvidence.gcodeBytes
), 25000, "impeller program loaded"); ), 25000, "impeller program loaded");
await waitForCanvasReady(); await waitForCanvasReady();
await wait(1200); await wait(1200);
@@ -192,6 +201,8 @@ async function captureRunSample(elapsedMs, name, title) {
taskCycle: state.programRuntimeFeedback?.taskCycle ?? null, taskCycle: state.programRuntimeFeedback?.taskCycle ?? null,
servoCycle: state.programRuntimeFeedback?.cycle ?? null, servoCycle: state.programRuntimeFeedback?.cycle ?? null,
historyLength: state.programRuntimeFeedbackHistory?.length || 0, historyLength: state.programRuntimeFeedbackHistory?.length || 0,
lineExecution: state.currentLineExecution,
lineExecutionVisible: state.currentLineExecutionVisible,
executedPathPoints: Number(step.dataset.threeExecutedPathPoints || 0), executedPathPoints: Number(step.dataset.threeExecutedPathPoints || 0),
currentSegmentHighlight: step.dataset.threeCurrentSegmentHighlight, currentSegmentHighlight: step.dataset.threeCurrentSegmentHighlight,
}; };
@@ -236,6 +247,7 @@ function addChecks() {
check("TCP/刀轴线可见", previewStep?.dataset?.threeTcpMarker === "sphere" && previewStep?.dataset?.threeToolAxisMarker === "line", `tcp=${previewStep?.dataset?.threeTcpMarker}, axis=${previewStep?.dataset?.threeToolAxisMarker}`), check("TCP/刀轴线可见", previewStep?.dataset?.threeTcpMarker === "sphere" && previewStep?.dataset?.threeToolAxisMarker === "line", `tcp=${previewStep?.dataset?.threeTcpMarker}, axis=${previewStep?.dataset?.threeToolAxisMarker}`),
check("RUN 采样数量", runSamples.length >= 5, `samples=${runSamples.length}`), check("RUN 采样数量", runSamples.length >= 5, `samples=${runSamples.length}`),
check("每行执行高亮同步", runSamples.every((sample) => sample.activeLineMatchesUi), runSamples.map((sample) => `${sample.activeLine}/${sample.activeUiLine}`).join(", ")), check("每行执行高亮同步", runSamples.every((sample) => sample.activeLineMatchesUi), runSamples.map((sample) => `${sample.activeLine}/${sample.activeUiLine}`).join(", ")),
check("程序列表显示每行执行过程", runSamples.every((sample) => sample.lineExecution?.line === sample.activeLine && sample.lineExecutionVisible === true), runSamples.map((sample) => `${sample.activeLine}:${sample.lineExecution?.status || "-"}:${sample.lineExecutionVisible}`).join(", ")),
check("执行轨迹可见", runSamples.every((sample) => sample.executedPathPoints >= 1 && sample.currentSegmentHighlight === "ok"), runSamples.map((sample) => `${sample.executedPathPoints}/${sample.currentSegmentHighlight}`).join(", ")), check("执行轨迹可见", runSamples.every((sample) => sample.executedPathPoints >= 1 && sample.currentSegmentHighlight === "ok"), runSamples.map((sample) => `${sample.executedPathPoints}/${sample.currentSegmentHighlight}`).join(", ")),
check("实时轴值来自 task/HAL feedback", runSamples.every((sample) => sample.feedbackSource === "linuxcnc-task-motion-hal-wasm" && sample.droMatchesAxisPose), runSamples.map((sample) => `${sample.feedbackSource}/${sample.droMatchesAxisPose}`).join(", ")), check("实时轴值来自 task/HAL feedback", runSamples.every((sample) => sample.feedbackSource === "linuxcnc-task-motion-hal-wasm" && sample.droMatchesAxisPose), runSamples.map((sample) => `${sample.feedbackSource}/${sample.droMatchesAxisPose}`).join(", ")),
check("RUN feed 不是固定 3600 mm/min", movingVelocities.length > 0 && movingVelocities.every((velocity) => Math.abs(velocity - 3600) > 0.001), runSamples.map((sample) => `${sample.elapsedMs}ms=${sample.velocity}`).join(", ")), check("RUN feed 不是固定 3600 mm/min", movingVelocities.length > 0 && movingVelocities.every((velocity) => Math.abs(velocity - 3600) > 0.001), runSamples.map((sample) => `${sample.elapsedMs}ms=${sample.velocity}`).join(", ")),
@@ -256,6 +268,7 @@ function summarizeState(state) {
machineProfile: state.machineProfile, machineProfile: state.machineProfile,
iniPath: state.iniConfigReadiness?.path || state.linuxCncIniConfig?.path || null, iniPath: state.iniConfigReadiness?.path || state.linuxCncIniConfig?.path || null,
selectedGcodeSourceRel: state.machineFileStaging?.selectedGcodeSourceRel || null, selectedGcodeSourceRel: state.machineFileStaging?.selectedGcodeSourceRel || null,
loadedSourceBytes: state.machineFileStaging?.save?.files?.find((file) => file.sourceRel === state.machineFileStaging?.selectedGcodeSourceRel)?.bytes || null,
taskHalProgramPath: state.taskHalSession?.programPath || null, taskHalProgramPath: state.taskHalSession?.programPath || null,
runState: state.runState, runState: state.runState,
activeLine: state.activeLine, activeLine: state.activeLine,
@@ -267,6 +280,8 @@ function summarizeState(state) {
axisPose: pickAxes(state.axisPose), axisPose: pickAxes(state.axisPose),
programRuntimeFeedback: state.programRuntimeFeedback, programRuntimeFeedback: state.programRuntimeFeedback,
currentTimingSegment, currentTimingSegment,
currentLineExecution: state.programLineExecution?.[state.activeLine] || null,
currentLineExecutionVisible: Boolean(state.__activeUiLineExecutionText?.includes("F ") && state.__activeUiLineExecutionText?.includes("cycle")),
feedbackHistoryLength: state.programRuntimeFeedbackHistory?.length || 0, feedbackHistoryLength: state.programRuntimeFeedbackHistory?.length || 0,
taskHalStatusLoop: state.taskHalStatusLoop, taskHalStatusLoop: state.taskHalStatusLoop,
taskHalStatus: state.taskHalStatus ? { taskHalStatus: state.taskHalStatus ? {
@@ -316,7 +331,12 @@ async function waitForCanvasReady(timeoutMs = 20000) {
async function getStateWithUi() { async function getStateWithUi() {
return page.evaluate(() => { return page.evaluate(() => {
const state = JSON.parse(JSON.stringify(window.webRtcp5AxisSimulation.getState())); const state = JSON.parse(JSON.stringify(window.webRtcp5AxisSimulation.getState()));
state.__activeUiLine = Number(document.querySelector(".gcode-row.active")?.dataset.programLine || 0); const activeRow = document.querySelector(".gcode-row.active");
state.__activeUiLine = Number(activeRow?.dataset.programLine || 0);
state.__activeUiLineExecutionText = activeRow?.querySelector("[data-line-execution]")?.textContent || "";
state.loadedSourceBytes = state.machineFileStaging?.save?.files
?.find((file) => file.sourceRel === state.machineFileStaging?.selectedGcodeSourceRel)
?.bytes || null;
return state; return state;
}); });
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

View File

@@ -0,0 +1,345 @@
{
"generatedAt": "2026-06-23T07:51:30.866Z",
"target": "https://82.156.24.101:8092/",
"steps": [
{
"name": "01-ready",
"screenshot": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/output/cloud-run-stop-after-deploy/01-ready.png",
"state": {
"runState": "idle",
"activeLine": 1,
"machine": {
"powerOn": true,
"estopActive": false,
"taskState": "on",
"mode": "auto",
"interpState": "idle",
"interpResumeState": "idle",
"taskPaused": false,
"allHomed": true,
"noForceHoming": false,
"jogAxis": "x",
"jogIncrement": 1,
"mdiCommand": "G0 X0 Y0 Z0",
"mdiDistanceMode": "absolute",
"resetCount": 0
},
"rtcpState": "on",
"kinsType": "tcp-xyzac",
"message": "RUN ready: power on, homed, auto mode",
"loop": {
"apiName": "web-rtcp-5axis-task-hal-status-loop",
"active": false,
"sequence": 0,
"profileId": null,
"iniPath": null,
"kinematicsModuleId": null,
"tickCount": 0,
"batchSize": 5,
"intervalMs": 25,
"taskPeriodNs": 10000000,
"servoPeriodNs": 1000000,
"lastStatusAt": null,
"lastError": null,
"stopReason": null,
"semanticBoundary": "js_status_polling_loop_for_linuxcnc_task_hal_motion_status"
},
"feedback": {
"apiName": "web-rtcp-5axis-program-runtime-feedback",
"sourceMode": "linuxcnc-task-motion-hal-wasm",
"semanticBoundary": "linuxcnc_task_motion_hal_wasm_simulation_runtime",
"sampleIndex": 40,
"motionIndex": 0,
"line": 1,
"motionProgramLine": 1,
"halProgramLine": 1,
"activeLineSource": "motion-status",
"activeLineHalSynced": true,
"type": "TASK_MOTION",
"timeSeconds": 0,
"axisPose": {
"x": 0,
"y": 0,
"z": 0,
"a": 0,
"b": 0,
"c": 0
},
"currentVelocityMmPerMin": 3600,
"requestedVelocityMmPerMin": 3600,
"distanceToGo": 0,
"dtg": {
"x": 0,
"y": 0,
"z": 0
},
"queueDepth": 0,
"activeDepth": 0,
"cycle": 40,
"taskCycle": 4,
"halChangedPinCount": 0
},
"history": 3,
"lineExecution": {
"status": "idle",
"source": "linuxcnc-task-motion-hal-wasm",
"line": 1,
"feed": 3600,
"requestedFeed": 3600,
"feedMode": null,
"axisPose": {
"x": 0,
"y": 0,
"z": 0,
"a": 0,
"b": 0,
"c": 0
},
"taskCycle": 4,
"servoCycle": 40,
"sampleIndex": 40,
"motionIndex": 0,
"updatedAt": "2026-06-23T07:51:07.542Z"
},
"source": "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/impeller-7bl-xyzac.ngc"
},
"ui": {
"activeUiLine": 1,
"lineText": "idle | F 3600.0 | X 0.000 | Y 0.000 | Z 0.000 | A 0.000 | C 0.000 | cycle 4/40",
"stopExists": true
}
},
{
"name": "02-running",
"screenshot": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/output/cloud-run-stop-after-deploy/02-running.png",
"state": {
"runState": "running",
"activeLine": 9,
"machine": {
"powerOn": true,
"estopActive": false,
"taskState": "on",
"mode": "auto",
"interpState": "reading",
"interpResumeState": "reading",
"taskPaused": false,
"allHomed": true,
"noForceHoming": false,
"jogAxis": "x",
"jogIncrement": 1,
"mdiCommand": "G0 X0 Y0 Z0",
"mdiDistanceMode": "absolute",
"resetCount": 0
},
"rtcpState": "on",
"kinsType": "tcp-xyzac",
"message": "task/HAL status tick 5",
"loop": {
"apiName": "web-rtcp-5axis-task-hal-status-loop",
"active": true,
"sequence": 1,
"profileId": "xyzac-trt",
"iniPath": "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",
"kinematicsModuleId": "xyzac-trt",
"tickCount": 5,
"batchSize": 5,
"intervalMs": 25,
"taskPeriodNs": 10000000,
"servoPeriodNs": 1000000,
"lastStatusAt": "2026-06-23T07:51:21.038Z",
"lastError": null,
"stopReason": null,
"semanticBoundary": "js_status_polling_loop_for_linuxcnc_task_hal_motion_status"
},
"feedback": {
"apiName": "web-rtcp-5axis-program-runtime-feedback",
"sourceMode": "linuxcnc-task-motion-hal-wasm",
"semanticBoundary": "linuxcnc_task_motion_hal_wasm_simulation_runtime",
"sampleIndex": 340,
"motionIndex": 8,
"line": 9,
"motionProgramLine": 9,
"halProgramLine": 9,
"activeLineSource": "motion-status",
"activeLineHalSynced": true,
"type": "TASK_MOTION",
"timeSeconds": 0,
"axisPose": {
"x": 10.4843,
"y": -17.3304,
"z": 30.0805,
"a": -71.841,
"b": 0,
"c": -35.93
},
"currentVelocityMmPerMin": 2100,
"requestedVelocityMmPerMin": 2100,
"distanceToGo": 0,
"dtg": {
"x": 0,
"y": 0,
"z": 0
},
"queueDepth": 0,
"activeDepth": 0,
"cycle": 340,
"taskCycle": 34,
"halChangedPinCount": 0
},
"history": 5,
"lineExecution": {
"status": "running",
"source": "linuxcnc-task-motion-hal-wasm",
"line": 9,
"feed": 2100,
"requestedFeed": 2100,
"feedMode": null,
"axisPose": {
"x": 10.4843,
"y": -17.3304,
"z": 30.0805,
"a": -71.841,
"b": 0,
"c": -35.93
},
"taskCycle": 34,
"servoCycle": 340,
"sampleIndex": 340,
"motionIndex": 8,
"updatedAt": "2026-06-23T07:51:21.038Z"
},
"source": "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/impeller-7bl-xyzac.ngc"
},
"ui": {
"activeUiLine": 9,
"lineText": "running | F 2100.0 | X 10.484 | Y -17.330 | Z 30.081 | A -71.841 | C -35.930 | cycle 34/340",
"stopExists": true
}
},
{
"name": "03-stopped",
"screenshot": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/output/cloud-run-stop-after-deploy/03-stopped.png",
"state": {
"runState": "stopped",
"activeLine": 9,
"machine": {
"powerOn": true,
"estopActive": false,
"taskState": "on",
"mode": "auto",
"interpState": "idle",
"interpResumeState": "idle",
"taskPaused": false,
"allHomed": true,
"noForceHoming": false,
"jogAxis": "x",
"jogIncrement": 1,
"mdiCommand": "G0 X0 Y0 Z0",
"mdiDistanceMode": "absolute",
"resetCount": 0
},
"rtcpState": "on",
"kinsType": "tcp-xyzac",
"message": "task/HAL program stopped",
"loop": {
"apiName": "web-rtcp-5axis-task-hal-status-loop",
"active": false,
"sequence": 1,
"profileId": "xyzac-trt",
"iniPath": "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",
"kinematicsModuleId": "xyzac-trt",
"tickCount": 7,
"batchSize": 5,
"intervalMs": 25,
"taskPeriodNs": 10000000,
"servoPeriodNs": 1000000,
"lastStatusAt": "2026-06-23T07:51:24.831Z",
"lastError": null,
"stopReason": "running",
"semanticBoundary": "js_status_polling_loop_for_linuxcnc_task_hal_motion_status"
},
"feedback": {
"apiName": "web-rtcp-5axis-program-runtime-feedback",
"sourceMode": "linuxcnc-task-motion-hal-wasm",
"semanticBoundary": "linuxcnc_task_motion_hal_wasm_simulation_runtime",
"sampleIndex": 500,
"motionIndex": 8,
"line": 9,
"motionProgramLine": 9,
"halProgramLine": 9,
"activeLineSource": "motion-status",
"activeLineHalSynced": true,
"type": "TASK_MOTION",
"timeSeconds": 0,
"axisPose": {
"x": 7.55697,
"y": -13.2911,
"z": 28.4442,
"a": -71.841,
"b": 0,
"c": -35.93
},
"currentVelocityMmPerMin": 2100,
"requestedVelocityMmPerMin": 2100,
"distanceToGo": 0,
"dtg": {
"x": 0,
"y": 0,
"z": 0
},
"queueDepth": 0,
"activeDepth": 0,
"cycle": 500,
"taskCycle": 50,
"halChangedPinCount": 0
},
"history": 9,
"lineExecution": {
"status": "stopped",
"source": "linuxcnc-task-motion-hal-wasm",
"line": 9,
"feed": 2100,
"requestedFeed": 2100,
"feedMode": null,
"axisPose": {
"x": 7.55697,
"y": -13.2911,
"z": 28.4442,
"a": -71.841,
"b": 0,
"c": -35.93
},
"taskCycle": 50,
"servoCycle": 500,
"sampleIndex": 500,
"motionIndex": 8,
"updatedAt": "2026-06-23T07:51:26.643Z"
},
"source": "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/impeller-7bl-xyzac.ngc"
},
"ui": {
"activeUiLine": 9,
"lineText": "stopped | F 2100.0 | X 7.557 | Y -13.291 | Z 28.444 | A -71.841 | C -35.930 | cycle 50/500",
"stopExists": true
}
}
],
"logs": [
{
"type": "warn",
"text": "[.WebGL-0x35b4000f0200]GL Driver Message (OpenGL, Performance, GL_CLOSE_PATH_NV, High): GPU stall due to ReadPixels"
},
{
"type": "warn",
"text": "[.WebGL-0x35b4000f0200]GL Driver Message (OpenGL, Performance, GL_CLOSE_PATH_NV, High): GPU stall due to ReadPixels"
},
{
"type": "warn",
"text": "[.WebGL-0x35b4000f0200]GL Driver Message (OpenGL, Performance, GL_CLOSE_PATH_NV, High): GPU stall due to ReadPixels"
},
{
"type": "warn",
"text": "[.WebGL-0x35b4000f0200]GL Driver Message (OpenGL, Performance, GL_CLOSE_PATH_NV, High): GPU stall due to ReadPixels (this message will no longer repeat)"
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 191 KiB

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 KiB

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 244 KiB

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 245 KiB

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 245 KiB

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 KiB

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 251 KiB

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 KiB

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 KiB

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 277 KiB

View File

@@ -12,6 +12,7 @@ await cp(join(appRoot, "index.html"), join(distDir, "index.html"));
await cp(join(appRoot, "src"), join(distDir, "src"), { recursive: true }); await cp(join(appRoot, "src"), join(distDir, "src"), { recursive: true });
await copyLinuxCncManifest(); await copyLinuxCncManifest();
await copyLinuxCncConfigAssets(); await copyLinuxCncConfigAssets();
await copyBundledTestLinuxCncSourceAssets();
await copyKinematicsRuntimeAssets(); await copyKinematicsRuntimeAssets();
await copyInterpreterRuntimeAssets(); await copyInterpreterRuntimeAssets();
await copyTpRuntimeAssets(); await copyTpRuntimeAssets();
@@ -68,6 +69,13 @@ async function copyLinuxCncConfigAssets() {
await cp(configSrcDir, vendorConfigDistDir, { recursive: true }); await cp(configSrcDir, vendorConfigDistDir, { recursive: true });
} }
async function copyBundledTestLinuxCncSourceAssets() {
const testSourceDir = join(repoRoot, "web-rtcp-5axis-sim-plan/working_run/test_linuxcnc_source");
const testSourceDistDir = join(distDir, "working_run/test_linuxcnc_source");
await mkdir(testSourceDistDir, { recursive: true });
await cp(testSourceDir, testSourceDistDir, { recursive: true });
}
async function copyLinuxCncManifest() { async function copyLinuxCncManifest() {
const manifestDistDir = join(distDir, "wasm-port/tools"); const manifestDistDir = join(distDir, "wasm-port/tools");
await mkdir(manifestDistDir, { recursive: true }); await mkdir(manifestDistDir, { recursive: true });

View File

@@ -7,6 +7,11 @@ const DEFAULT_VENDOR_ROOT_URLS = [
new URL("../../../../wasm-port/vendor/linuxcnc/", import.meta.url).href, new URL("../../../../wasm-port/vendor/linuxcnc/", import.meta.url).href,
new URL("../../wasm-port/vendor/linuxcnc/", import.meta.url).href, new URL("../../wasm-port/vendor/linuxcnc/", import.meta.url).href,
]; ];
const DEFAULT_TEST_SOURCE_ROOT_URLS = [
new URL("../../../working_run/test_linuxcnc_source/", import.meta.url).href,
new URL("../../working_run/test_linuxcnc_source/", import.meta.url).href,
new URL("../../../../working_run/test_linuxcnc_source/", import.meta.url).href,
];
const TRT_MACHINE_REL = "axis/vismach/5axis/table-rotary-tilting"; const TRT_MACHINE_REL = "axis/vismach/5axis/table-rotary-tilting";
const TRT_DEMO_SOURCE_PREFIX = `configs/sim/${TRT_MACHINE_REL}/demos/`; const TRT_DEMO_SOURCE_PREFIX = `configs/sim/${TRT_MACHINE_REL}/demos/`;
const OPFS_ROOT = "web-rtcp-5axis-sim-plan/machines"; const OPFS_ROOT = "web-rtcp-5axis-sim-plan/machines";
@@ -125,9 +130,12 @@ export async function saveMachineFileStagingPlan(plan, options = {}) {
throw new Error("saveMachineFileStagingPlan requires a machine-file staging plan"); throw new Error("saveMachineFileStagingPlan requires a machine-file staging plan");
} }
const storage = resolveMachineFileStorage(options); const storage = resolveMachineFileStorage(options);
const sourceTextOverrides = options.sourceTextOverrides || {};
const savedFiles = []; const savedFiles = [];
for (const file of plan.files) { for (const file of plan.files) {
const text = await readTextFromCandidateUrls(sourceUrlsFor(file.sourceRel)); const text = typeof sourceTextOverrides[file.sourceRel] === "string"
? sourceTextOverrides[file.sourceRel]
: await readTextFromCandidateUrls(sourceUrlsFor(file.sourceRel));
await saveTextFile(file.opfsPath, text, storage.storage); await saveTextFile(file.opfsPath, text, storage.storage);
savedFiles.push({ savedFiles.push({
sourceRel: file.sourceRel, sourceRel: file.sourceRel,
@@ -182,6 +190,7 @@ export async function stageProfileMachineFiles(profile, options = {}) {
const save = await saveMachineFileStagingPlan(plan, { const save = await saveMachineFileStagingPlan(plan, {
storage: options.storage, storage: options.storage,
storageMode: options.storageMode, storageMode: options.storageMode,
sourceTextOverrides: options.sourceTextOverrides,
}); });
return { plan, save }; return { plan, save };
} }
@@ -441,7 +450,22 @@ function splitPath(path) {
} }
function sourceUrlsFor(sourceRel) { function sourceUrlsFor(sourceRel) {
return DEFAULT_VENDOR_ROOT_URLS.map((rootUrl) => new URL(sourceRel, rootUrl).href); return sourceOverrideUrlsFor(sourceRel).concat(
DEFAULT_VENDOR_ROOT_URLS.map((rootUrl) => new URL(sourceRel, rootUrl).href),
);
}
function sourceOverrideUrlsFor(sourceRel) {
const basenameValue = basename(sourceRel);
if (!basenameValue) return [];
if (sourceRel === `configs/sim/${TRT_MACHINE_REL}/xyzac-trt.ini`) {
return DEFAULT_TEST_SOURCE_ROOT_URLS.map((rootUrl) => new URL("xyzac-trt.ini", rootUrl).href);
}
if (sourceRel === `${TRT_DEMO_SOURCE_PREFIX}impeller-7bl-xyzac.ngc`) {
return DEFAULT_TEST_SOURCE_ROOT_URLS.map((rootUrl) => new URL("impeller-7bl-xyzac.ngc", rootUrl).href);
}
return [];
} }
function basename(path) { function basename(path) {

View File

@@ -172,6 +172,7 @@ const initialState = {
programExecutionSampleIndex: 0, programExecutionSampleIndex: 0,
programRuntimeFeedback: null, programRuntimeFeedback: null,
programRuntimeFeedbackHistory: [], programRuntimeFeedbackHistory: [],
programLineExecution: {},
taskHalRuntime: null, taskHalRuntime: null,
taskHalRuntimeReadiness: null, taskHalRuntimeReadiness: null,
taskHalStatus: null, taskHalStatus: null,
@@ -327,6 +328,25 @@ export function createSimulationStore(seed = {}) {
scheduleAsyncKinematicsRefresh(); scheduleAsyncKinematicsRefresh();
}; };
const waitForStatePredicate = (predicate, timeoutMs = 10000) => {
if (predicate(state)) return Promise.resolve(state);
return new Promise((resolve, reject) => {
const startedAt = Date.now();
const listener = (nextState) => {
if (predicate(nextState)) {
listeners.delete(listener);
resolve(nextState);
return;
}
if (Date.now() - startedAt > timeoutMs) {
listeners.delete(listener);
reject(new Error("timed out waiting for store state"));
}
};
listeners.add(listener);
});
};
const dispatch = (action) => { const dispatch = (action) => {
switch (action.type) { switch (action.type) {
case "BOOT_READY": case "BOOT_READY":
@@ -568,6 +588,10 @@ export function createSimulationStore(seed = {}) {
programExecutionMotionIndex: 0, programExecutionMotionIndex: 0,
programExecutionSampleIndex: 0, programExecutionSampleIndex: 0,
programRuntimeFeedback: firstFeedback, programRuntimeFeedback: firstFeedback,
programLineExecution: createProgramLineExecutionPatch(state.programLineExecution, firstFeedback, {
status: "ready",
source: execution.sourceMode,
}),
programElapsedSeconds: firstTiming.elapsedSeconds, programElapsedSeconds: firstTiming.elapsedSeconds,
programRemainingSeconds: firstTiming.remainingSeconds, programRemainingSeconds: firstTiming.remainingSeconds,
interpreterExecutionPending: false, interpreterExecutionPending: false,
@@ -603,6 +627,7 @@ export function createSimulationStore(seed = {}) {
programExecutionSourceMode: "fixture-line-playback", programExecutionSourceMode: "fixture-line-playback",
programExecutionSampleIndex: 0, programExecutionSampleIndex: 0,
programRuntimeFeedback: null, programRuntimeFeedback: null,
programLineExecution: {},
interpreterExecutionPending: false, interpreterExecutionPending: false,
operatorMessage: `LinuxCNC interpreter blocked: ${action.error}`, operatorMessage: `LinuxCNC interpreter blocked: ${action.error}`,
}); });
@@ -815,6 +840,7 @@ export function createSimulationStore(seed = {}) {
axisPose: initialAxisPose, axisPose: initialAxisPose,
runState: "idle", runState: "idle",
programRuntimeFeedback: null, programRuntimeFeedback: null,
programLineExecution: {},
preview: { preview: {
...state.preview, ...state.preview,
pathPoints: Math.max(loadedProgram.programLines.length, 1), pathPoints: Math.max(loadedProgram.programLines.length, 1),
@@ -909,6 +935,14 @@ export function createSimulationStore(seed = {}) {
case "RUN_FULL_BOUNDARY_AUDIT_REQUEST": case "RUN_FULL_BOUNDARY_AUDIT_REQUEST":
runFullBoundaryAudit(action.options || {}).catch(() => {}); runFullBoundaryAudit(action.options || {}).catch(() => {});
break; break;
case "RUN_READY":
runReadySequence().catch((error) => {
dispatch({
type: "TASK_HAL_COMMAND_FAILED",
error: error instanceof Error ? error.message : String(error),
});
});
break;
case "SET_FRAME_SOURCE": case "SET_FRAME_SOURCE":
setState({ setState({
sourceMode: action.sourceMode, sourceMode: action.sourceMode,
@@ -1138,6 +1172,7 @@ export function createSimulationStore(seed = {}) {
axisPose: initialAxisPose, axisPose: initialAxisPose,
runState: "idle", runState: "idle",
programRuntimeFeedback: null, programRuntimeFeedback: null,
programLineExecution: {},
preview: { preview: {
...state.preview, ...state.preview,
pathPoints: Math.max(loadedProgram.programLines.length, 1), pathPoints: Math.max(loadedProgram.programLines.length, 1),
@@ -1182,6 +1217,10 @@ export function createSimulationStore(seed = {}) {
programExecutionMotionIndex: playback.motionIndex, programExecutionMotionIndex: playback.motionIndex,
programExecutionSampleIndex: playback.sampleIndex, programExecutionSampleIndex: playback.sampleIndex,
programRuntimeFeedback: playback.runtimeFeedback, programRuntimeFeedback: playback.runtimeFeedback,
programLineExecution: createProgramLineExecutionPatch(state.programLineExecution, playback.runtimeFeedback, {
status: playback.complete ? "done" : "running",
source: playback.runtimeFeedback?.sourceMode,
}),
programElapsedSeconds: playback.timing.elapsedSeconds, programElapsedSeconds: playback.timing.elapsedSeconds,
programRemainingSeconds: playback.timing.remainingSeconds, programRemainingSeconds: playback.timing.remainingSeconds,
feed: { feed: {
@@ -1375,7 +1414,13 @@ export function createSimulationStore(seed = {}) {
if (state.taskHalRuntime?.loaded) { if (state.taskHalRuntime?.loaded) {
runTaskHalCommandSequence([ runTaskHalCommandSequence([
{ type: "EMC_JOINT_HOME", joint: -1 }, { type: "EMC_JOINT_HOME", joint: -1 },
], { operatorMessage: "task/HAL machine homed" }).catch(() => {}); ], {
operatorMessage: "task/HAL machine homed",
preserveMachine: {
...state.machine,
allHomed: true,
},
}).catch(() => {});
} }
setState({ setState({
machine: { machine: {
@@ -1493,6 +1538,7 @@ export function createSimulationStore(seed = {}) {
programExecutionMotionIndex: 0, programExecutionMotionIndex: 0,
programExecutionSampleIndex: 0, programExecutionSampleIndex: 0,
programRuntimeFeedback: null, programRuntimeFeedback: null,
programLineExecution: {},
axisPose: initialAxisPose, axisPose: initialAxisPose,
preview: { ...state.preview, pathPoints: Math.max(state.programLines.length, 1) }, preview: { ...state.preview, pathPoints: Math.max(state.programLines.length, 1) },
operatorMessage: "program reloaded", operatorMessage: "program reloaded",
@@ -1758,6 +1804,57 @@ export function createSimulationStore(seed = {}) {
return status; return status;
}; };
const runReadySequence = async () => {
if (!state.machineFileStaging?.selectedGcodeSourceRel) {
const sourceRel = defaultLinuxCncGcodeSourceForState(state)?.sourceRel;
if (sourceRel) {
dispatch({ type: "LOAD_LINUXCNC_GCODE_SOURCE", sourceRel });
await waitForStatePredicate((nextState) => nextState.machineFileStaging?.selectedGcodeSourceRel === sourceRel);
}
}
if (state.taskHalRuntime?.loaded) {
await initializeTaskHalSession({ openProgram: true });
await runTaskHalCommandSequence([
{ type: "EMC_TASK_SET_STATE", state: "ON" },
{ type: "EMC_JOINT_HOME", joint: -1 },
{ type: "EMC_TASK_SET_MODE", mode: "AUTO" },
], {
taskCycles: 3,
operatorMessage: "RUN ready: power on, homed, auto mode",
allowFixtureSession: false,
preserveMachine: {
powerOn: true,
estopActive: false,
taskState: "on",
mode: "auto",
allHomed: true,
},
});
const tcpKinsType = state.profile.kinematicsParameters.switchkinsTypes.find((type) => type.value === 1)?.webKinsType || "tcp-xyzac";
dispatch({ type: "SET_KINS_TYPE", kinsType: tcpKinsType });
return state;
}
const tcpKinsType = state.profile.kinematicsParameters.switchkinsTypes.find((type) => type.value === 1)?.webKinsType || "tcp-xyzac";
setState({
machine: {
...state.machine,
powerOn: true,
estopActive: false,
taskState: "on",
mode: "auto",
allHomed: true,
interpState: "idle",
interpResumeState: "idle",
taskPaused: false,
},
runState: "idle",
kinsType: tcpKinsType,
rtcpState: "on",
operatorMessage: "RUN ready: power on, homed, auto mode",
});
return state;
};
const loadTaskHalMotionPlanForSession = async (session = state.taskHalSession) => { const loadTaskHalMotionPlanForSession = async (session = state.taskHalSession) => {
if (!state.taskHalRuntime?.loaded || typeof state.taskHalRuntime.loadProgramMotionPlan !== "function") { if (!state.taskHalRuntime?.loaded || typeof state.taskHalRuntime.loadProgramMotionPlan !== "function") {
return null; return null;
@@ -1882,6 +1979,7 @@ export function createSimulationStore(seed = {}) {
operatorMessage = "task/HAL command complete", operatorMessage = "task/HAL command complete",
pendingJogCommand = null, pendingJogCommand = null,
allowFixtureSession = true, allowFixtureSession = true,
preserveMachine = null,
} = {}) => { } = {}) => {
if (!state.taskHalRuntime?.loaded) { if (!state.taskHalRuntime?.loaded) {
throw new Error("LinuxCNC task/HAL runtime not attached"); throw new Error("LinuxCNC task/HAL runtime not attached");
@@ -1910,7 +2008,7 @@ export function createSimulationStore(seed = {}) {
if (state.taskHalExecutionSequence !== sequence) { if (state.taskHalExecutionSequence !== sequence) {
return status; return status;
} }
dispatch({ type: "TASK_HAL_STATUS_APPLIED", status, operatorMessage }); dispatch({ type: "TASK_HAL_STATUS_APPLIED", status, operatorMessage, preserveMachine });
return status; return status;
} catch (error) { } catch (error) {
dispatch({ dispatch({
@@ -2277,6 +2375,11 @@ function applyTaskHalStatusPatch(state, status, operatorMessage, {
&& (runState === "running" || runState === "mdi"); && (runState === "running" || runState === "mdi");
const nextTickCount = loopActive ? Number(state.taskHalStatusLoop.tickCount || 0) + 1 : Number(state.taskHalStatusLoop?.tickCount || 0); const nextTickCount = loopActive ? Number(state.taskHalStatusLoop.tickCount || 0) + 1 : Number(state.taskHalStatusLoop?.tickCount || 0);
const feedbackHistory = [runtimeFeedback, ...(state.programRuntimeFeedbackHistory || [])].slice(0, 100); const feedbackHistory = [runtimeFeedback, ...(state.programRuntimeFeedbackHistory || [])].slice(0, 100);
const lineStatus = runState === "complete"
? "done"
: runState === "running" || runState === "mdi"
? "running"
: runState;
return { return {
taskHalStatus: status, taskHalStatus: status,
@@ -2315,6 +2418,10 @@ function applyTaskHalStatusPatch(state, status, operatorMessage, {
}, },
programRuntimeFeedback: runtimeFeedback, programRuntimeFeedback: runtimeFeedback,
programRuntimeFeedbackHistory: feedbackHistory, programRuntimeFeedbackHistory: feedbackHistory,
programLineExecution: createProgramLineExecutionPatch(state.programLineExecution, runtimeFeedback, {
status: lineStatus,
source: "linuxcnc-task-motion-hal-wasm",
}),
operatorMessage, operatorMessage,
}; };
} }
@@ -2451,6 +2558,41 @@ function createTaskHalRuntimeFeedback(state, status, axisPose, activeLine) {
}; };
} }
function createProgramLineExecutionPatch(previous = {}, feedback = null, {
status = "running",
source = null,
} = {}) {
const line = Number(feedback?.line || 0);
if (!Number.isFinite(line) || line <= 0) {
return previous || {};
}
const axisPose = feedback.axisPose || {};
return {
...(previous || {}),
[line]: {
status,
source: source || feedback.sourceMode || "unknown",
line,
feed: Number(feedback.currentVelocityMmPerMin || 0),
requestedFeed: Number(feedback.requestedVelocityMmPerMin || 0),
feedMode: feedback.feedMode || null,
axisPose: pickExecutionAxes(axisPose),
taskCycle: Number(feedback.taskCycle || 0),
servoCycle: Number(feedback.cycle || 0),
sampleIndex: Number(feedback.sampleIndex || 0),
motionIndex: Number(feedback.motionIndex || 0),
updatedAt: new Date().toISOString(),
},
};
}
function pickExecutionAxes(axisPose = {}) {
return Object.fromEntries(["x", "y", "z", "a", "b", "c"].map((axis) => [
axis,
Number(axisPose[axis] || 0),
]));
}
function normalizeTaskHalTaskState(value) { function normalizeTaskHalTaskState(value) {
const state = String(value || "").toLowerCase().replaceAll("_", "-"); const state = String(value || "").toLowerCase().replaceAll("_", "-");
if (state === "on") return "on"; if (state === "on") return "on";

View File

@@ -21,16 +21,22 @@
html, html,
body { body {
width: 100%; width: 100vw;
min-width: 1180px; height: 100vh;
min-height: 640px; height: 100dvh;
height: 100%;
margin: 0; margin: 0;
overflow: hidden; overflow: hidden;
background: #c8c4bc; background: #c8c4bc;
color: var(--text); color: var(--text);
} }
#app {
width: 100vw;
height: 100vh;
height: 100dvh;
overflow: hidden;
}
button { button {
border: 1px solid #bdb8ad; border: 1px solid #bdb8ad;
border-radius: 5px; border-radius: 5px;
@@ -66,21 +72,21 @@ button:active {
.gmoccapy-shell { .gmoccapy-shell {
display: grid; display: grid;
grid-template-columns: grid-template-columns:
minmax(610px, 1.34fr) minmax(0, 1.34fr)
minmax(270px, 0.58fr) minmax(0, 0.58fr)
minmax(260px, 0.56fr) minmax(0, 0.56fr)
108px; 108px;
grid-template-rows: 40px minmax(210px, 1fr) minmax(170px, 0.78fr) 150px 68px; grid-template-rows: 40px minmax(0, 1fr) minmax(0, 0.82fr) minmax(110px, 0.48fr) 68px;
grid-template-areas: grid-template-areas:
"title title title title" "title title title title"
"preview dro dro side" "preview dro dro side"
"preview gcode gcode side" "preview gcode gcode side"
"info override spindle side" "info override spindle side"
"bottom bottom bottom side"; "bottom bottom bottom side";
width: 100vw; width: 100%;
height: 100vh; height: 100%;
min-width: 1180px; min-width: 0;
min-height: 640px; min-height: 0;
border: 1px solid var(--border); border: 1px solid var(--border);
background: var(--panel); background: var(--panel);
} }
@@ -140,6 +146,35 @@ button:active {
text-transform: uppercase; text-transform: uppercase;
} }
.current-line-indicator {
display: grid;
grid-template-columns: auto minmax(44px, auto);
align-items: center;
gap: 8px;
flex: 0 0 auto;
min-width: 0;
padding: 4px 8px;
border: 1px solid #8d887f;
background: #f7f1d0;
color: #171717;
}
.current-line-indicator span {
min-width: 0;
overflow: hidden;
font-size: 11px;
font-weight: 700;
text-overflow: ellipsis;
text-transform: uppercase;
white-space: nowrap;
}
.current-line-indicator strong {
color: #005bab;
font: 700 20px/1 "Courier New", monospace;
text-align: right;
}
.preview-panel { .preview-panel {
grid-area: preview; grid-area: preview;
position: relative; position: relative;
@@ -336,7 +371,7 @@ button:active {
.gcode-panel { .gcode-panel {
grid-area: gcode; grid-area: gcode;
display: grid; display: grid;
grid-template-rows: 32px minmax(0, 1fr) 20px 54px; grid-template-rows: auto auto minmax(96px, 1fr) 20px 54px;
min-width: 0; min-width: 0;
min-height: 0; min-height: 0;
overflow: hidden; overflow: hidden;
@@ -357,6 +392,38 @@ button:active {
font-size: 13px; font-size: 13px;
} }
.linuxcnc-source-row {
display: grid;
grid-template-columns: minmax(260px, 1fr) auto minmax(180px, 1fr);
align-items: center;
gap: 8px;
min-width: 0;
padding: 5px 8px;
border-bottom: 1px solid #d5d0c7;
background: #f6f4ef;
font-size: 12px;
}
.linuxcnc-source-row label {
display: grid;
grid-template-columns: auto minmax(150px, 240px);
align-items: center;
gap: 6px;
min-width: 0;
}
.linuxcnc-source-row select {
min-width: 0;
max-width: 100%;
}
.linuxcnc-source-row [data-linuxcnc-gcode-source="status"] {
min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.gcode-header strong, .gcode-header strong,
.gcode-header span { .gcode-header span {
min-width: 0; min-width: 0;
@@ -385,26 +452,54 @@ button:active {
display: grid; display: grid;
grid-template-columns: 42px minmax(0, 1fr); grid-template-columns: 42px minmax(0, 1fr);
gap: 6px; gap: 6px;
min-height: 18px; min-height: 34px;
line-height: 1.25; line-height: 1.25;
padding: 2px 4px;
border-left: 3px solid transparent;
} }
.gcode-row.active { .gcode-row.active {
background: #242424; background: #242424;
color: #202020; color: #202020;
border-left-color: #2ebf63;
}
.gcode-row[data-line-status="done"] {
background: #eef6ec;
border-left-color: #79a96c;
}
.gcode-row[data-line-status="running"] {
background: #242424;
border-left-color: #2ebf63;
} }
.gcode-row.active span, .gcode-row.active span,
.gcode-row.active code { .gcode-row.active code,
.gcode-row.active small,
.gcode-row[data-line-status="running"] span,
.gcode-row[data-line-status="running"] code,
.gcode-row[data-line-status="running"] small {
color: #f2f2f2; color: #f2f2f2;
} }
.gcode-row code { .gcode-row code {
min-width: 0;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.gcode-row small {
grid-column: 2;
min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: #6d695f;
font: 11px/1.2 "Courier New", monospace;
}
.gcode-progress { .gcode-progress {
display: grid; display: grid;
grid-template-columns: 120px 1fr; grid-template-columns: 120px 1fr;
@@ -723,7 +818,7 @@ button:active {
.bottom-controls { .bottom-controls {
grid-area: bottom; grid-area: bottom;
display: grid; display: grid;
grid-template-columns: repeat(15, minmax(48px, 1fr)); grid-template-columns: repeat(18, minmax(44px, 1fr));
gap: 5px; gap: 5px;
padding: 6px 8px; padding: 6px 8px;
border-top: 2px solid var(--border); border-top: 2px solid var(--border);
@@ -746,10 +841,23 @@ button:active {
@media (max-width: 1180px) { @media (max-width: 1180px) {
.gmoccapy-shell { .gmoccapy-shell {
grid-template-columns: grid-template-columns:
minmax(560px, 1.28fr) minmax(0, 1.18fr)
minmax(250px, 0.58fr) minmax(0, 0.7fr)
minmax(230px, 0.54fr) minmax(0, 0.62fr)
100px; 96px;
}
.titlebar {
gap: 6px;
padding: 4px 8px;
}
.profile-select-label {
min-width: 160px;
}
.current-line-indicator strong {
font-size: 16px;
} }
.dro-row strong { .dro-row strong {
@@ -765,6 +873,14 @@ button:active {
font-size: 12px; font-size: 12px;
} }
.linuxcnc-source-row {
grid-template-columns: minmax(180px, 1fr) auto minmax(140px, 1fr);
}
.linuxcnc-source-row label {
grid-template-columns: auto minmax(120px, 1fr);
}
.tabs button, .tabs button,
.meter-card h2, .meter-card h2,
.override-control h2, .override-control h2,
@@ -773,3 +889,180 @@ button:active {
font-size: 12px; font-size: 12px;
} }
} }
@media (max-height: 720px) {
.gmoccapy-shell {
grid-template-rows: 40px minmax(0, 1fr) minmax(0, 1fr) minmax(84px, 0.4fr) 58px;
}
.preview-toolbar button,
.bottom-controls button,
.status-sidebar button {
min-height: 34px;
}
.gcode-panel {
grid-template-rows: auto auto minmax(0, 1fr) 18px 48px;
}
.gcode-list {
padding: 4px 6px 2px;
font-size: 12px;
}
.gcode-row {
min-height: 24px;
}
.gcode-row small {
display: none;
}
.bottom-controls {
grid-template-columns: repeat(18, minmax(36px, 1fr));
}
}
@media (max-height: 420px) {
.gmoccapy-shell {
grid-template-rows: 24px minmax(24px, 0.14fr) minmax(0, 1.92fr) 0 28px;
}
.titlebar {
gap: 4px;
padding: 2px 5px;
}
.title-stack strong {
font-size: 10px;
}
.title-stack span,
.run-state {
font-size: 8px;
}
.profile-select-label {
min-width: 96px;
font-size: 8px;
}
.profile-select-label select {
min-height: 16px;
font-size: 8px;
}
.current-line-indicator {
padding: 1px 4px;
}
.current-line-indicator span {
font-size: 8px;
}
.current-line-indicator strong {
font-size: 9px;
}
.info-tabs,
.override-panel,
.spindle-coolant-panel {
display: none;
}
.gcode-panel {
grid-template-rows: auto auto minmax(0, 1fr) 12px;
}
.gcode-header {
gap: 4px;
padding: 1px 3px;
font-size: 8px;
}
.linuxcnc-source-row {
gap: 4px;
padding: 1px 3px;
font-size: 8px;
grid-template-columns: minmax(120px, 1fr) auto minmax(72px, 0.82fr);
}
.linuxcnc-source-row label {
gap: 4px;
grid-template-columns: auto minmax(72px, 1fr);
}
.linuxcnc-source-row button,
.linuxcnc-source-row select {
min-height: 16px;
font-size: 8px;
}
.gcode-list {
padding: 2px 4px;
font-size: 8px;
line-height: 1;
}
.gcode-row {
gap: 4px;
min-height: 8px;
padding: 0 2px;
border-left-width: 2px;
}
.gcode-row span {
font-size: 8px;
}
.gcode-row code {
font-size: 8px;
}
.mdi-panel {
display: none;
}
.dro-panel {
grid-template-rows: 1fr;
}
.dro-grid {
grid-template-columns: repeat(6, minmax(0, 1fr));
grid-template-rows: 1fr;
}
.dro-row {
grid-template-columns: auto minmax(0, 1fr);
gap: 3px;
padding: 1px 4px;
align-items: center;
}
.dro-axis {
font-size: 8px;
}
.dro-mode,
.dro-dtg,
.tcp-strip {
display: none;
}
.dro-row strong {
font-size: 9px;
line-height: 1;
}
.bottom-controls {
gap: 3px;
padding: 2px 3px;
}
.bottom-controls button {
font-size: 8px;
line-height: 1;
min-height: 18px;
padding: 1px 2px;
}
}

View File

@@ -59,6 +59,7 @@ function render(regions, state, dispatch) {
} }
function renderTitlebar(element, state) { function renderTitlebar(element, state) {
const currentLine = currentGcodeExecutionLine(state);
element.innerHTML = ` element.innerHTML = `
<div class="brand-dot" aria-hidden="true">NS</div> <div class="brand-dot" aria-hidden="true">NS</div>
<div class="title-stack"> <div class="title-stack">
@@ -73,6 +74,10 @@ function renderTitlebar(element, state) {
`).join("")} `).join("")}
</select> </select>
</label> </label>
<div class="current-line-indicator" data-current-gcode-line="${currentLine}" data-current-gcode-source="${escapeHtml(currentGcodeLineSource(state))}">
<span>G-code line</span>
<strong>${currentLine}</strong>
</div>
<div class="run-state" data-run-state="${state.runState}">${state.runState}</div> <div class="run-state" data-run-state="${state.runState}">${state.runState}</div>
`; `;
element.querySelector('[data-action="select-profile"]').addEventListener("change", (event) => { element.querySelector('[data-action="select-profile"]').addEventListener("change", (event) => {
@@ -192,11 +197,20 @@ function droRow(axis, value, dtg) {
} }
function renderGcode(element, state, dispatch) { function renderGcode(element, state, dispatch) {
const currentLine = currentGcodeExecutionLine(state);
const rows = state.programLines const rows = state.programLines
.map((line, index) => { .map((line, index) => {
const lineNumber = state.programStartLine + index; const lineNumber = state.programStartLine + index;
const execution = state.programLineExecution?.[lineNumber] || null;
const active = lineNumber === state.activeLine ? " active" : ""; const active = lineNumber === state.activeLine ? " active" : "";
return `<li class="gcode-row${active}" data-program-line="${lineNumber}"><span>${lineNumber}</span><code>${escapeHtml(line)}</code></li>`; const status = execution?.status || (lineNumber < state.activeLine ? "done" : "pending");
return `
<li class="gcode-row${active}" data-program-line="${lineNumber}" data-line-status="${escapeHtml(status)}">
<span>${lineNumber}</span>
<code>${escapeHtml(line)}</code>
<small data-line-execution="${lineNumber}">${formatLineExecution(execution, active)}</small>
</li>
`;
}) })
.join(""); .join("");
const programEndLine = state.programStartLine + Math.max(state.programLines.length - 1, 0); const programEndLine = state.programStartLine + Math.max(state.programLines.length - 1, 0);
@@ -210,7 +224,7 @@ function renderGcode(element, state, dispatch) {
<div class="gcode-header"> <div class="gcode-header">
<strong>${escapeHtml(state.activeProgram)}</strong> <strong>${escapeHtml(state.activeProgram)}</strong>
<span data-program-source="${state.programSource}">${state.programSource}</span> <span data-program-source="${state.programSource}">${state.programSource}</span>
<span data-active-program-line="${state.activeLine}">Current line ${state.activeLine}</span> <span data-active-program-line="${currentLine}" data-current-gcode-line="${currentLine}">Executing line ${currentLine}</span>
</div> </div>
<div class="linuxcnc-source-row" data-linuxcnc-gcode-source="row"> <div class="linuxcnc-source-row" data-linuxcnc-gcode-source="row">
<label> <label>
@@ -272,6 +286,19 @@ function renderGcode(element, state, dispatch) {
}); });
} }
function currentGcodeExecutionLine(state) {
const feedbackLine = Number(state.programRuntimeFeedback?.line);
if (Number.isFinite(feedbackLine) && feedbackLine > 0) {
return feedbackLine;
}
const activeLine = Number(state.activeLine);
return Number.isFinite(activeLine) && activeLine > 0 ? activeLine : state.programStartLine;
}
function currentGcodeLineSource(state) {
return state.programRuntimeFeedback?.sourceMode || state.programExecutionSourceMode || "ui-state";
}
function renderLinuxCncGcodeSourceOptions(state) { function renderLinuxCncGcodeSourceOptions(state) {
const sources = state.machineFileStaging.gcodeSources || []; const sources = state.machineFileStaging.gcodeSources || [];
if (sources.length === 0) { if (sources.length === 0) {
@@ -444,6 +471,21 @@ function formatProgramRuntimeDtg(state) {
return `DTG ${formatNumber(dtg.x, 3)} / ${formatNumber(dtg.y, 3)} / ${formatNumber(dtg.z, 3)} distance ${formatNumber(feedback.distanceToGo, 3)}`; return `DTG ${formatNumber(dtg.x, 3)} / ${formatNumber(dtg.y, 3)} / ${formatNumber(dtg.z, 3)} distance ${formatNumber(feedback.distanceToGo, 3)}`;
} }
function formatLineExecution(execution, active) {
if (!execution) return active ? "running" : "pending";
const axes = execution.axisPose || {};
return [
execution.status || (active ? "running" : "done"),
`F ${formatNumber(execution.feed, 1)}`,
`X ${formatNumber(axes.x, 3)}`,
`Y ${formatNumber(axes.y, 3)}`,
`Z ${formatNumber(axes.z, 3)}`,
`A ${formatNumber(axes.a, 3)}`,
`C ${formatNumber(axes.c, 3)}`,
`cycle ${execution.taskCycle || 0}/${execution.servoCycle || 0}`,
].join(" | ");
}
function formatDuration(seconds) { function formatDuration(seconds) {
const safeSeconds = Math.max(Number(seconds) || 0, 0); const safeSeconds = Math.max(Number(seconds) || 0, 0);
const minutes = Math.floor(safeSeconds / 60); const minutes = Math.floor(safeSeconds / 60);
@@ -604,6 +646,7 @@ function renderBottomControls(element, state, dispatch) {
const controls = [ const controls = [
["Open", "OPEN", null], ["Open", "OPEN", null],
["Reload", "RELOAD", () => dispatch({ type: "RELOAD_PROGRAM" })], ["Reload", "RELOAD", () => dispatch({ type: "RELOAD_PROGRAM" })],
["Run Ready", "RUN_READY", () => dispatch({ type: "RUN_READY" })],
["Run", "RUN", () => dispatch({ type: "RUN" })], ["Run", "RUN", () => dispatch({ type: "RUN" })],
["Stop", "STOP", () => dispatch({ type: "STOP" })], ["Stop", "STOP", () => dispatch({ type: "STOP" })],
["Pause", "PAUSE", () => dispatch({ type: "PAUSE" })], ["Pause", "PAUSE", () => dispatch({ type: "PAUSE" })],
@@ -659,6 +702,7 @@ function renderBottomControls(element, state, dispatch) {
function bottomControlGate(state, action) { function bottomControlGate(state, action) {
const actionMap = { const actionMap = {
RUN: { type: "RUN" }, RUN: { type: "RUN" },
RUN_READY: { type: "UI_CONTROL" },
STEP: { type: "STEP" }, STEP: { type: "STEP" },
PAUSE: { type: "PAUSE" }, PAUSE: { type: "PAUSE" },
RESUME: { type: "RESUME" }, RESUME: { type: "RESUME" },

View File

@@ -116,6 +116,145 @@ npm --prefix web-rtcp-5axis-sim-plan/app run build: PASS
npm --prefix web-rtcp-5axis-sim-plan/app run smoke:node: PASS npm --prefix web-rtcp-5axis-sim-plan/app run smoke:node: PASS
``` ```
## 2026-06-23 03:52 EDT - 云端 8092 RUN 修复、部署和复测
User request:
- 测试并完善 `https://82.156.24.101:8092/``run` 功能。
- 使用 `/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/working_run/test_linuxcnc_source` 下面的 INI/G-code 测试。
- 要求 RUN 按实际行执行、按实际 feed 执行、各轴数据正确,并在程序列表显示每行执行过程。
- 约束:只应在 `ON + AUTO + IDLE + homed` 下进入 `EMC_TASK_PLAN_RUN`,且 session 初始化不能把已 POWER/HOME 的 UI 状态重置回 off/unhomed。
Process:
1. Read workspace instructions and existing repository context:
- `/home/meswork/cnc_wams/AGENTS.md`
- `qa/web-rtcp-5axis-site-test/capture-test-linuxcnc-source-run.mjs`
- `web-rtcp-5axis-sim-plan/app/src/state/store.js`
- `web-rtcp-5axis-sim-plan/app/src/ui/gmoccapy-shell.js`
- `web-rtcp-5axis-sim-plan/app/src/runtime/linuxcnc-machine-file-staging.js`
- `web-rtcp-5axis-sim-plan/app/src/runtime/linuxcnc-task-hal-runtime.js`
- `web-rtcp-5axis-sim-plan/tests/node/verify_run_feedback_loop.mjs`
- `web-rtcp-5axis-sim-plan/tests/node/verify_run_preconditions.mjs`
2. Reviewed existing local dirty worktree and existing evidence files. Existing local report already showed local PASS, but cloud target still needed verification.
3. Ran cloud Puppeteer probe against `https://82.156.24.101:8092/`.
- Initial cloud probe output:
`/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/output/cloud-run-debug-current/cloud-run-debug-current.json`
- Finding: cloud page had `Run Ready`, but after clicking it the test timed out waiting for `taskState=on/allHomed/auto/tcp`.
4. Ran a shorter cloud `Run Ready` probe:
- Output:
`/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/output/cloud-run-ready-after.json`
- Finding before deploy:
- Before: `taskState=estop-reset`, `mode=manual`, `allHomed=false`.
- After `Run Ready`: `taskState=on`, `mode=auto`, `rtcpState=on`, but `allHomed=false`.
- Result: subsequent `Run` was still blocked by the homing gate.
5. Added focused regression coverage in:
- `web-rtcp-5axis-sim-plan/tests/node/verify_run_feedback_loop.mjs`
- New `verifyRunReadySequence` path validates:
- `RUN_READY` sets `powerOn=true`, `taskState=on`, `mode=auto`, `allHomed=true`, `rtcpState=on`.
- A following `RUN` enters `linuxcnc-task-motion-hal-wasm` feedback.
- `operatorMessage` is not `run blocked: home machine first`.
6. Updated `verify_run_preconditions.mjs` to match the current explicit gate order:
- ON + manual + unhomed: `run blocked: switch to auto mode first`.
- ON + auto + unhomed: `run blocked: home machine first`.
7. Verified local node tests:
- `node web-rtcp-5axis-sim-plan/tests/node/verify_run_feedback_loop.mjs`
- `run_feedback_status_loop_smoke=ok`
- `run_ready_sequence_smoke=ok`
- `node web-rtcp-5axis-sim-plan/tests/node/verify_run_preconditions.mjs`
- `run_preconditions_ini_profile_smoke=ok`
- `run_preconditions_kinematics_smoke=ok`
- `run_preconditions_machine_file_smoke=ok`
8. Ran full local browser evidence capture:
- Command:
`node qa/web-rtcp-5axis-site-test/capture-test-linuxcnc-source-run.mjs`
- Result:
`test_linuxcnc_source_run_status=PASS`
- JSON:
`/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/output/test-linuxcnc-source-run-report.json`
- DOCX:
`/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/output/test-linuxcnc-source-run-report-2026-06-23.docx`
- Key PASS evidence:
- `test_linuxcnc_source INI used`
- `test_linuxcnc_source G-code hash matches loaded source`
- `刀具路径使用完整 canonical motion 点`
- `RUN 采样数量`
- `每行执行高亮同步`
- `程序列表显示每行执行过程`
- `执行轨迹可见`
- `实时轴值来自 task/HAL feedback`
- `RUN feed 不是固定 3600 mm/min`
- `RUN feed 随实际 G-code F/G93 段变化`
- `RUN 使用 G93 inverse-time feed`
- `task/HAL cycle 推进`
- `STOP 后 loop 停止`
9. Built static site:
- Command:
`npm run build` from `/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app`
- Result:
`gmoccapy_static_build=ok`
- Build output:
`/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app/dist`
10. Created deployment tarball:
- `/tmp/web-rtcp-5axis-sim-8092-runfix-20260623.tar.gz`
11. Connected to cloud host:
- SSH target: `ubuntu@82.156.24.101`
- Confirmed nginx was listening on port `8092`.
- Confirmed nginx site root:
`/var/www/web-rtcp-5axis-sim`
- Noted older home copy:
`/home/ubuntu/web-rtcp-5axis-sim-8092`
12. Uploaded release tarball to:
- `/home/ubuntu/tmp/web-rtcp-5axis-sim-8092-runfix-20260623.tar.gz`
13. Deployed to nginx site:
- Backup:
`/home/ubuntu/tmp/web-rtcp-backups/web-rtcp-5axis-sim-before-runfix-20260623-154606`
- Release extraction:
`/home/ubuntu/tmp/web-rtcp-5axis-sim-8092-runfix-20260623-154606`
- Synced release into:
`/var/www/web-rtcp-5axis-sim`
- Ran:
`sudo nginx -t`
- Result:
`nginx: configuration file /etc/nginx/nginx.conf test is successful`
- Reloaded nginx.
14. Ran cloud post-deploy RUN probe:
- Output:
`/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/output/cloud-run-after-deploy/cloud-run-after-deploy.json`
- Key evidence:
- `03-run-ready`: `allHomed=true`, `mode=auto`, `taskState=on`, `feedback=linuxcnc-task-motion-hal-wasm`.
- `04-run-0500`: `runState=running`, `activeLine=9`, `velocity=2100`, line text includes `running | F 2100.0 ... cycle 19/190`.
- `06-run-7000`: `runState=running`, `activeLine=11`, `velocity=283.9176`, line text includes actual axis values and cycle.
- First post-deploy probe hit Puppeteer `Node is detached from document` while clicking STOP because the button node was re-rendered during the running UI update. This was a test-click issue, not a page failure.
15. Ran cloud STOP-specific post-deploy probe using DOM re-query for the STOP button:
- Output:
`/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/output/cloud-run-stop-after-deploy/cloud-run-stop-after-deploy.json`
- Key evidence:
- `01-ready`: `runState=idle`, `allHomed=true`, `feedback=linuxcnc-task-motion-hal-wasm`, `lineText="idle | F 3600.0 ... cycle 4/40"`.
- `02-running`: `runState=running`, `activeLine=9`, `loopActive=true`, `feedback=linuxcnc-task-motion-hal-wasm`, `velocity=2100`, line text includes `running | F 2100.0 ... cycle 34/340`.
- `03-stopped`: `runState=stopped`, `loopActive=false`, `feedback=linuxcnc-task-motion-hal-wasm`, line text includes `stopped | F 2100.0 ... cycle 50/500`.
16. Closed SSH session.
Files modified by this execution:
```text
web-rtcp-5axis-sim-plan/tests/node/verify_run_feedback_loop.mjs
web-rtcp-5axis-sim-plan/tests/node/verify_run_preconditions.mjs
qa/web-rtcp-5axis-site-test/output/test-linuxcnc-source-run-report.json
qa/web-rtcp-5axis-site-test/output/test-linuxcnc-source-run-report-2026-06-23.docx
qa/web-rtcp-5axis-site-test/screenshots/test-linuxcnc-source-run/*.png
qa/web-rtcp-5axis-site-test/output/cloud-run-after-deploy/*
qa/web-rtcp-5axis-site-test/output/cloud-run-stop-after-deploy/*
web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md
```
Cloud deployment result:
- Deployed updated static app to `https://82.156.24.101:8092/`.
- Confirmed `RUN_READY` no longer loses `allHomed`.
- Confirmed `RUN` enters task/HAL feedback and advances line/feed/axis execution.
- Confirmed `STOP` stops the status loop.
--- ---
Execution timestamp: 2026-06-22 17:55 EDT Execution timestamp: 2026-06-22 17:55 EDT
@@ -1225,3 +1364,551 @@ Files modified or generated by this execution:
```text ```text
web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md
``` ```
## 2026-06-22 22:54:09 EDT website packaging of test_linuxcnc_source
User request: based on the previous round, package the files under
`/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/working_run/test_linuxcnc_source`
into the website, make sure the programs shown in the `LinuxCNC 5-axis source`
UI are uploaded with the site/server bundle, and ensure the source programs are
used correctly.
Process summary:
1. Inspected the current website packaging and LinuxCNC source loading path.
- Read `web-rtcp-5axis-sim-plan/app/scripts/build-static.mjs`.
- Read `web-rtcp-5axis-sim-plan/app/src/runtime/linuxcnc-machine-file-staging.js`.
- Confirmed the UI source selector is backed by machine-file staging and
vendored LinuxCNC demo files.
2. Verified the relevant source assets.
- Confirmed `working_run/test_linuxcnc_source` currently contains:
- `xyzac-trt.ini`
- `impeller-7bl-xyzac.ngc`
- Verified both files are byte-identical to their vendored LinuxCNC source
counterparts by SHA-256:
- `xyzac-trt.ini`
- `impeller-7bl-xyzac.ngc`
3. Implemented website packaging for the test source directory.
- Updated `web-rtcp-5axis-sim-plan/app/scripts/build-static.mjs`.
- Added `copyBundledTestLinuxCncSourceAssets()`.
- Build output now copies:
- `web-rtcp-5axis-sim-plan/working_run/test_linuxcnc_source/*`
to
- `web-rtcp-5axis-sim-plan/app/dist/working_run/test_linuxcnc_source/*`
4. Updated machine-file staging source resolution.
- Updated `web-rtcp-5axis-sim-plan/app/src/runtime/linuxcnc-machine-file-staging.js`.
- Added `DEFAULT_TEST_SOURCE_ROOT_URLS`.
- Updated `sourceUrlsFor()` to prefer packaged `working_run/test_linuxcnc_source`
assets for:
- `configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini`
- `configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/impeller-7bl-xyzac.ngc`
- Preserved vendored LinuxCNC URLs as fallback so the existing source list and
staging flow continue to work.
5. Built and validated the website package.
- Ran `npm run build` in `web-rtcp-5axis-sim-plan/app`.
- Confirmed:
- `app/dist/working_run/test_linuxcnc_source/xyzac-trt.ini`
- `app/dist/working_run/test_linuxcnc_source/impeller-7bl-xyzac.ngc`
exist in the published output.
6. Ran validation commands.
- `node web-rtcp-5axis-sim-plan/tests/node/verify_machine_file_staging.mjs`
-> `machine_file_staging_smoke=ok`
- `node web-rtcp-5axis-sim-plan/tests/node/verify_impeller_feed_task_hal_run.mjs`
-> `impeller_feed_task_hal_run=ok`
- `bash web-rtcp-5axis-sim-plan/tests/browser/verify_gmoccapy_dist_browser.sh`
-> `gmoccapy_dist_smoke=ok`
7. Started a local HTTP server for the built site.
- Working directory:
`web-rtcp-5axis-sim-plan/app/dist`
- URL:
`http://127.0.0.1:4173/`
- Confirmed HTTP 200 for:
- `/index.html`
- `/working_run/test_linuxcnc_source/impeller-7bl-xyzac.ngc`
Files modified by this execution:
```text
web-rtcp-5axis-sim-plan/app/scripts/build-static.mjs
web-rtcp-5axis-sim-plan/app/src/runtime/linuxcnc-machine-file-staging.js
web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md
```
## 2026-06-22 23:30:02 EDT cloud short-viewport gcode panel diagnosis and fix
User report: on the cloud-hosted site, the G-code display area showed only one
line with a large blank area above it.
Process summary:
1. Reproduced the issue under a short viewport similar to the user screenshot.
- Used a local browser run at `848x331`.
- Confirmed the problem was reproducible with
`impeller-7bl-xyzac.ngc`.
2. Measured DOM/layout state.
- Verified the G-code program was not a one-line program.
- Confirmed the loaded impeller program had `4507` lines.
- Confirmed the status text
`8 staged / configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/impeller-7bl-xyzac.ngc`
is styled with `white-space: nowrap` and `text-overflow: ellipsis`, so the
one-line symptom was not caused by that text wrapping.
3. Found the actual cause.
- The shell layout had hard minimum viewport assumptions and an unconstrained
`#app` container combination that could cause the grid to expand beyond the
real viewport while `body` remained `overflow: hidden`.
- In the short viewport case, the browser only exposed a bottom sliver of the
G-code list area, which looked like “only one visible line with lots of blank
space above”.
4. Implemented a responsive layout fix in
`web-rtcp-5axis-sim-plan/app/src/styles/gmoccapy.css`.
- Removed the old hard `1180px / 640px` min-size behavior.
- Made the main shell columns responsive.
- Added a constrained `#app` viewport container.
- Added compact layout rules for shorter heights.
- Added an extra compact mode for very short heights (`max-height: 420px`) that:
- prioritizes the G-code panel height,
- hides `info-tabs`, `override`, `spindle-coolant`, and `mdi-panel`,
- reduces chrome height so the list can show multiple rows.
5. Validated after the fix.
- Rebuilt the site: `npm run build`
- Re-tested at `848x331`.
- Post-fix measured result:
- shell height = `331`
- G-code panel height = `177`
- G-code list height = `93`
- visible G-code rows = `4`
- Confirmed compact-mode hidden panels:
- `mdiVisible = none`
- `infoVisible = none`
6. Regression check.
- Ran `bash web-rtcp-5axis-sim-plan/tests/browser/verify_gmoccapy_dist_browser.sh`
- Result: `gmoccapy_dist_smoke=ok`
Files modified by this execution:
```text
web-rtcp-5axis-sim-plan/app/src/styles/gmoccapy.css
web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md
```
## 2026-06-22 23:40:12 EDT manual short-viewport browser test and screenshots
User request: execute a test and capture screenshots.
Process summary:
1. Rebuilt the static site package.
- Ran `npm run build` in `web-rtcp-5axis-sim-plan/app`
- Result: `gmoccapy_static_build=ok`
2. Used the running local dist server at `http://127.0.0.1:4173/`.
3. Ran a manual browser test with Puppeteer at a short viewport.
- Viewport: `848x331`
- Program selected:
`configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/impeller-7bl-xyzac.ngc`
4. Measured the rendered G-code panel.
- `visibleGcodeRows = 21`
- Confirmed the first visible lines include the expected impeller header and
program body rows.
5. Saved artifacts.
- Full-page screenshot:
`qa/web-rtcp-5axis-site-test/screenshots/manual-short-viewport/impeller-short-viewport-2026-06-22.png`
- G-code panel screenshot:
`qa/web-rtcp-5axis-site-test/screenshots/manual-short-viewport/impeller-short-viewport-gcode-2026-06-22.png`
- Metrics JSON:
`qa/web-rtcp-5axis-site-test/screenshots/manual-short-viewport/impeller-short-viewport-2026-06-22.json`
6. Visually inspected the cropped G-code screenshot.
- Verified the panel is nonblank and shows multiple visible rows, with line
numbers and impeller program content.
Files generated by this execution:
```text
qa/web-rtcp-5axis-site-test/screenshots/manual-short-viewport/impeller-short-viewport-2026-06-22.png
qa/web-rtcp-5axis-site-test/screenshots/manual-short-viewport/impeller-short-viewport-gcode-2026-06-22.png
qa/web-rtcp-5axis-site-test/screenshots/manual-short-viewport/impeller-short-viewport-2026-06-22.json
web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md
```
## 2026-06-22 23:55:45 EDT deploy current dist to cloud site 82.156.24.101:8092
User request: publish the current version to the cloud website.
Process summary:
1. Read deployment context from `git.txt` and prior deployment notes in this log.
- Confirmed cloud site target: `https://82.156.24.101:8092/`
- Confirmed prior nginx deployment root: `/var/www/web-rtcp-5axis-sim`
2. Rebuilt the static application before publish.
- Ran `npm run build` in `web-rtcp-5axis-sim-plan/app`
- Result: `gmoccapy_static_build=ok`
3. Packaged the current build output.
- Created:
`/tmp/web-rtcp-5axis-sim-8092.tar.gz`
4. Confirmed remote access details.
- SSH target: `ubuntu@82.156.24.101`
- `scp` and `ssh` available locally.
- `paramiko` available locally and used for reliable remote execution.
5. Uploaded and deployed with explicit remote replacement.
- Uploaded tarball to:
`/tmp/web-rtcp-5axis-sim-8092.tar.gz`
- Extracted to a timestamped remote staging directory.
- Backed up the previous nginx root to:
`/var/www/web-rtcp-5axis-sim.bak.<timestamp>`
- Replaced `/var/www/web-rtcp-5axis-sim` with the new build contents.
- Set ownership to `www-data:www-data`.
- Ran `nginx -t` and reloaded nginx.
6. Verified remote HTTPS response after deployment.
- Remote local check returned:
`HTTP/2 200`
- Public check:
`curl -k -I https://82.156.24.101:8092/`
returned `HTTP/2 200`
7. Verified the deployed site contains the new responsive compact layout.
- Fetched remote CSS from:
`https://82.156.24.101:8092/src/styles/gmoccapy.css`
- Confirmed presence of:
- `#app` viewport constraint
- `@media (max-height: 420px)`
- compact shell rows
8. Ran a cloud browser validation against the public URL.
- Viewport: `848x331`
- Program:
`configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/impeller-7bl-xyzac.ngc`
- Result:
`cloud_short_viewport_visible_rows=21`
9. Saved cloud verification artifacts.
- Full-page screenshot:
`qa/web-rtcp-5axis-site-test/screenshots/cloud-short-viewport/cloud-impeller-short-viewport-2026-06-22.png`
- G-code panel screenshot:
`qa/web-rtcp-5axis-site-test/screenshots/cloud-short-viewport/cloud-impeller-short-viewport-gcode-2026-06-22.png`
- Metrics JSON:
`qa/web-rtcp-5axis-site-test/screenshots/cloud-short-viewport/cloud-impeller-short-viewport-2026-06-22.json`
10. Cleaned up the temporary local askpass helper.
Files generated by this execution:
```text
qa/web-rtcp-5axis-site-test/screenshots/cloud-short-viewport/cloud-impeller-short-viewport-2026-06-22.png
qa/web-rtcp-5axis-site-test/screenshots/cloud-short-viewport/cloud-impeller-short-viewport-gcode-2026-06-22.png
qa/web-rtcp-5axis-site-test/screenshots/cloud-short-viewport/cloud-impeller-short-viewport-2026-06-22.json
web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md
```
## 2026-06-22 23:40:12 EDT compact DRO layout for 20+ visible gcode rows
User request: increase the visible G-code line count to at least 20 and compress
the axis display area (`X Y Z A/B C`) to make room.
Process summary:
1. Reviewed current DRO rendering and layout.
- Read `app/src/ui/gmoccapy-shell.js` and `app/src/styles/gmoccapy.css`.
- Confirmed the full-size DRO panel and title/bottom chrome still consumed too
much height in very short viewports.
2. Tightened the ultra-short-height layout in
`web-rtcp-5axis-sim-plan/app/src/styles/gmoccapy.css`.
- Reduced title row height.
- Reduced top preview/DRO row height.
- Reduced bottom-controls row height.
- Reduced G-code header/source-row/progress overhead.
- Reduced G-code row font size and row height.
3. Compressed the axis display area.
- In `max-height: 420px` mode:
- converted the DRO panel to a single compact row,
- changed the six axes to a 6-column compact strip,
- hid `G54/Abs`, `DTG`, and `TCP` strip details,
- reduced axis/value font sizes.
4. Rebuilt and measured the same short viewport (`848x331`).
- Post-fix metrics:
- `gcodeHeight = 253`
- `listHeight = 206`
- `visibleRows = 21`
- `rowHeight = 10`
- `droHeight = 24`
- `titleHeight = 24`
- `bottomHeight = 28`
5. Regression check.
- Ran `bash web-rtcp-5axis-sim-plan/tests/browser/verify_gmoccapy_dist_browser.sh`
- Result: `gmoccapy_dist_smoke=ok`
Files modified by this execution:
```text
web-rtcp-5axis-sim-plan/app/src/styles/gmoccapy.css
web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md
```
## 2026-06-22 G-code full list visibility and RUN usability fix
User request: after selecting a source program, the program list should show the
whole source program. If the line count is large, dynamic loading is acceptable.
Also RUN is not easy to use.
Process summary:
1. Inspected current G-code panel and RUN controls.
- `app/src/ui/gmoccapy-shell.js`
- `app/src/styles/gmoccapy.css`
- `app/src/state/store.js`
- `app/src/state/linuxcnc-task-policy.js`
2. Found the main display issue.
- `.gcode-panel` CSS defined only four grid rows:
`32px minmax(0, 1fr) 20px 54px`
- The DOM actually renders five sections:
G-code header, LinuxCNC source selector row, program list, progress, MDI.
- This caused the source selector and program list to overlap/compress, making
the long source program appear as only a tiny visible row.
3. Fixed full source-program list visibility.
- Updated `.gcode-panel` to five rows:
`auto auto minmax(96px, 1fr) 20px 54px`
- Added `.linuxcnc-source-row` layout rules so the source selector/status no
longer consume the program list area.
- The source program list remains scrollable for large programs, so all lines
are available without rendering outside the panel.
4. Improved RUN usability.
- Added `RUN_READY` store action.
- Added `runReadySequence()` in `store.js`.
- The sequence loads a default G-code source when needed, initializes task/HAL,
powers on, homes all joints, switches to AUTO mode, and enables the profile TCP
kinematics type.
- Added a `Run Ready` bottom-control button before `Run`.
- Adjusted bottom-control layout to fit the additional control.
5. Preserved existing RUN semantics.
- RUN still uses `validateRunPreconditions`.
- The new `Run Ready` button prepares the machine instead of bypassing the
LinuxCNC-style gates.
6. Validation completed.
- `node --check web-rtcp-5axis-sim-plan/app/src/state/store.js`
- `node --check web-rtcp-5axis-sim-plan/app/src/ui/gmoccapy-shell.js`
- `npm --prefix web-rtcp-5axis-sim-plan/app run build`
- `node web-rtcp-5axis-sim-plan/tests/node/verify_run_feedback_loop.mjs`
- `node qa/web-rtcp-5axis-site-test/capture-test-linuxcnc-source-run.mjs`
7. Browser test result:
- `test_linuxcnc_source_run_status=PASS`
- JSON report:
`/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/output/test-linuxcnc-source-run-report.json`
- DOCX report:
`/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/output/test-linuxcnc-source-run-report-2026-06-23.docx`
8. Relevant PASS evidence:
- `RUN 采样数量`
- `每行执行高亮同步`
- `程序列表显示每行执行过程`
- `RUN feed 不是固定 3600 mm/min`
- `RUN feed 随实际 G-code F/G93 段变化`
- `RUN 使用 G93 inverse-time feed`
Execution timestamp:
- `2026-06-22 22:42:20 EDT`
Files modified or generated by this execution:
```text
web-rtcp-5axis-sim-plan/app/src/state/store.js
web-rtcp-5axis-sim-plan/app/src/styles/gmoccapy.css
web-rtcp-5axis-sim-plan/app/src/ui/gmoccapy-shell.js
qa/web-rtcp-5axis-site-test/output/test-linuxcnc-source-run-report.json
qa/web-rtcp-5axis-site-test/output/test-linuxcnc-source-run-report-2026-06-23.docx
qa/web-rtcp-5axis-site-test/screenshots/test-linuxcnc-source-run/*.png
web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md
```
## 2026-06-22 HTTPS deploy to 82.156.24.101:8092
User request: publish the Web RTCP 5 Axis simulator by HTTPS to remote server
`82.156.24.101` on port `8092` using SSH user `ubuntu`.
Process summary:
1. Built the local static app.
- Command: `npm --prefix web-rtcp-5axis-sim-plan/app run build`
- Result: `gmoccapy_static_build=ok`
- Build output: `web-rtcp-5axis-sim-plan/app/dist`
2. Packaged the build output.
- Command: `tar -C web-rtcp-5axis-sim-plan/app/dist -czf /tmp/web-rtcp-5axis-sim-8092.tar.gz .`
- Package size: about `1.6M`
3. Uploaded the package to the remote host.
- Tooling: `scp` via interactive password handling.
- Remote path: `/tmp/web-rtcp-5axis-sim-8092.tar.gz`
4. Attempted to create a standalone systemd HTTPS Node service.
- Service name: `web-rtcp-5axis-sim-8092.service`
- Initial service failed because port `8092` was already occupied.
5. Diagnosed port ownership.
- `ss -ltnp 'sport = :8092'` showed nginx already listening on `8092`.
- `curl -k -I https://127.0.0.1:8092/` already returned HTTP 200 from nginx.
6. Removed the failed standalone systemd service.
- Disabled/stopped `web-rtcp-5axis-sim-8092.service`.
- Removed `/etc/systemd/system/web-rtcp-5axis-sim-8092.service`.
- Reloaded systemd daemon.
7. Inspected existing nginx HTTPS configuration.
- Active config: `/etc/nginx/sites-available/web-rtcp-5axis-sim`
- HTTPS listener:
- `listen 8092 ssl http2`
- `listen [::]:8092 ssl http2`
- Root: `/var/www/web-rtcp-5axis-sim`
- Certificate paths:
- `/etc/letsencrypt/live/82.156.24.101/fullchain.pem`
- `/etc/letsencrypt/live/82.156.24.101/privkey.pem`
8. Deployed this build through the existing nginx HTTPS site.
- Extracted package to a staging directory.
- Backed up previous web root:
- `/var/www/web-rtcp-5axis-sim.bak.20260623101928`
- Replaced `/var/www/web-rtcp-5axis-sim` with the new static build.
- Set ownership to `www-data:www-data`.
- Ran `nginx -t`.
- Reloaded nginx.
9. Verified HTTPS access.
- Local remote check: `curl -k -I https://127.0.0.1:8092/` returned `HTTP/2 200`.
- Public check: `curl -k -I https://82.156.24.101:8092/` returned `HTTP/2 200`.
Execution timestamp:
- `2026-06-22 22:19:39 EDT`
Published URL:
```text
https://82.156.24.101:8092/
```
Files modified or generated by this execution:
```text
web-rtcp-5axis-sim-plan/app/dist/**
/tmp/web-rtcp-5axis-sim-8092.tar.gz
remote:/tmp/web-rtcp-5axis-sim-8092.tar.gz
remote:/var/www/web-rtcp-5axis-sim/**
remote:/var/www/web-rtcp-5axis-sim.bak.20260623101928/**
web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md
```
## 2026-06-22 realtime current G-code line display
User request: program realtime display of the currently executing G-code line number.
Process summary:
1. Inspected the existing UI and state flow.
- Checked `app/src/ui/gmoccapy-shell.js` for titlebar, G-code header, and current-line display.
- Confirmed runtime line source is already available through `state.programRuntimeFeedback.line`
during task/HAL RUN, with `state.activeLine` as fallback.
2. Implemented a dedicated realtime current-line indicator.
- Added `currentGcodeExecutionLine(state)` in `gmoccapy-shell.js`.
- Added `currentGcodeLineSource(state)` in `gmoccapy-shell.js`.
- Added a titlebar element:
- class: `current-line-indicator`
- data attribute: `data-current-gcode-line`
- source attribute: `data-current-gcode-source`
- Updated the G-code header to show `Executing line <line>` and expose the same
`data-current-gcode-line` value.
3. Styled the realtime indicator.
- Updated `app/src/styles/gmoccapy.css`.
- Added compact, stable sizing for the titlebar realtime line display.
4. Validation completed.
- `node --check web-rtcp-5axis-sim-plan/app/src/ui/gmoccapy-shell.js`
- `node --check web-rtcp-5axis-sim-plan/app/src/state/store.js`
- `npm --prefix web-rtcp-5axis-sim-plan/app run build`
- `node web-rtcp-5axis-sim-plan/tests/node/verify_run_feedback_loop.mjs`
- `node qa/web-rtcp-5axis-site-test/capture-test-linuxcnc-source-run.mjs`
5. Browser test result:
- `test_linuxcnc_source_run_status=PASS`
- Realtime current-line evidence remained synchronized:
- `每行执行高亮同步`: PASS
- `程序列表显示每行执行过程`: PASS
Execution timestamp:
- `2026-06-22 22:09:20 EDT`
Files modified or generated by this execution:
```text
web-rtcp-5axis-sim-plan/app/src/ui/gmoccapy-shell.js
web-rtcp-5axis-sim-plan/app/src/styles/gmoccapy.css
qa/web-rtcp-5axis-site-test/output/test-linuxcnc-source-run-report.json
qa/web-rtcp-5axis-site-test/output/test-linuxcnc-source-run-report-2026-06-23.docx
qa/web-rtcp-5axis-site-test/screenshots/test-linuxcnc-source-run/*.png
web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md
```
## 2026-06-22 test_linuxcnc_source RUN completion
User request: continue completing RUN execution using files under
`/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/working_run/test_linuxcnc_source`,
execute actual G-code lines with actual feed, keep each axis data correct,
and show each line's execution process in the program list.
Process summary:
1. Inspected workspace rules and project structure.
- Confirmed `AGENTS.md` requires appending process logs to this file.
- Confirmed test source files:
- `working_run/test_linuxcnc_source/xyzac-trt.ini`
- `working_run/test_linuxcnc_source/impeller-7bl-xyzac.ngc`
2. Reviewed existing RUN/task-HAL flow.
- Read `app/src/state/store.js`, `app/src/runtime/linuxcnc-task-hal-runtime.js`,
`app/src/ui/gmoccapy-shell.js`, and `app/src/styles/gmoccapy.css`.
- Confirmed task/HAL already drives runtime feedback from the loaded motion plan,
including active line, axis pose, task/servo cycles, and current feed velocity.
3. Implemented per-line execution state.
- Added `programLineExecution` in `app/src/state/store.js`.
- Updated interpreter init, playback fallback, reload/load reset paths, and
task/HAL status application to record per-line status, feed, XYZABC axis pose,
task cycle, servo cycle, sample index, and source mode.
4. Updated the program list UI.
- Updated `app/src/ui/gmoccapy-shell.js` so each G-code row shows execution
process text such as status, feed, XYZAC values, and task/servo cycle.
- Updated `app/src/styles/gmoccapy.css` for running/done/pending line display.
5. Ensured the browser test actually uses the requested test source directory.
- Added `sourceTextOverrides` support in `app/src/runtime/linuxcnc-machine-file-staging.js`.
- Updated `qa/web-rtcp-5axis-site-test/capture-test-linuxcnc-source-run.mjs`
to stage `xyzac-trt.ini` and override the selected impeller G-code with the
text from `working_run/test_linuxcnc_source/impeller-7bl-xyzac.ngc`.
- Added checks that loaded source bytes match the test source file.
6. Added browser-test evidence for program-list execution process.
- Captured active-row execution text from the DOM.
- Added a PASS/FAIL check named `程序列表显示每行执行过程`.
7. Validation completed.
- `node --check web-rtcp-5axis-sim-plan/app/src/runtime/linuxcnc-machine-file-staging.js`
- `node --check web-rtcp-5axis-sim-plan/app/src/state/store.js`
- `node --check web-rtcp-5axis-sim-plan/app/src/ui/gmoccapy-shell.js`
- `node --check qa/web-rtcp-5axis-site-test/capture-test-linuxcnc-source-run.mjs`
- `node web-rtcp-5axis-sim-plan/tests/node/verify_impeller_feed_task_hal_run.mjs`
- `node web-rtcp-5axis-sim-plan/tests/node/verify_run_feedback_loop.mjs`
- `node web-rtcp-5axis-sim-plan/tests/node/verify_machine_file_staging.mjs`
- `npm --prefix web-rtcp-5axis-sim-plan/app run build`
- `node qa/web-rtcp-5axis-site-test/capture-test-linuxcnc-source-run.mjs`
8. Browser test result:
- `test_linuxcnc_source_run_status=PASS`
- JSON report:
`/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/output/test-linuxcnc-source-run-report.json`
- DOCX report:
`/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/output/test-linuxcnc-source-run-report-2026-06-23.docx`
9. Key PASS evidence from the generated report:
- `test_linuxcnc_source G-code hash matches loaded source`
- `刀具路径使用完整 canonical motion 点`
- `每行执行高亮同步`
- `程序列表显示每行执行过程`
- `实时轴值来自 task/HAL feedback`
- `RUN feed 不是固定 3600 mm/min`
- `RUN feed 随实际 G-code F/G93 段变化`
- `RUN 使用 G93 inverse-time feed`
- `task/HAL cycle 推进`
Execution timestamp:
- `2026-06-22 22:00:22 EDT`
Files modified or generated by this execution:
```text
qa/web-rtcp-5axis-site-test/capture-test-linuxcnc-source-run.mjs
qa/web-rtcp-5axis-site-test/output/test-linuxcnc-source-run-report.json
qa/web-rtcp-5axis-site-test/output/test-linuxcnc-source-run-report-2026-06-23.docx
qa/web-rtcp-5axis-site-test/screenshots/test-linuxcnc-source-run/*.png
web-rtcp-5axis-sim-plan/app/src/runtime/linuxcnc-machine-file-staging.js
web-rtcp-5axis-sim-plan/app/src/state/store.js
web-rtcp-5axis-sim-plan/app/src/styles/gmoccapy.css
web-rtcp-5axis-sim-plan/app/src/ui/gmoccapy-shell.js
web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md
```

View File

@@ -22,6 +22,11 @@ await verifyRunFeedbackLoop({
stopAction: "STOP", stopAction: "STOP",
}); });
await verifyRunReadySequence({
profileId: "xyzac-trt",
sourceRel: "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_1.ngc",
});
await verifyRunFeedbackLoop({ await verifyRunFeedbackLoop({
profileId: "xyzac-trt", profileId: "xyzac-trt",
sourceRel: "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_1.ngc", sourceRel: "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins_test_1.ngc",
@@ -35,6 +40,7 @@ await verifyRunFeedbackLoop({
}); });
console.log("run_feedback_status_loop_smoke=ok"); console.log("run_feedback_status_loop_smoke=ok");
console.log("run_ready_sequence_smoke=ok");
async function verifyRunFeedbackLoop({ profileId, sourceRel, stopAction }) { async function verifyRunFeedbackLoop({ profileId, sourceRel, stopAction }) {
const sdk = await createLinuxCncTaskHalSdk({ const sdk = await createLinuxCncTaskHalSdk({
@@ -138,6 +144,65 @@ async function verifyRunFeedbackLoop({ profileId, sourceRel, stopAction }) {
assert.equal(state.taskHalStatus.ui.taskCycle >= cycleAfterRun, true); assert.equal(state.taskHalStatus.ui.taskCycle >= cycleAfterRun, true);
} }
async function verifyRunReadySequence({ profileId, sourceRel }) {
const sdk = await createLinuxCncTaskHalSdk({
wasmBinary: readFileSync(wasmPath),
print() {},
printErr(message) {
console.error(message);
},
});
const profile = getFiveAxisProfile(profileId);
const iniText = readFileSync(resolve(rootDir, "wasm-port/vendor/linuxcnc", profile.iniPath), "utf8");
const iniConfig = parseLinuxCncIni(iniText, {
path: profile.iniPath,
profileId: profile.id,
});
const store = createSimulationStore();
store.dispatch({ type: "ATTACH_INI_CONFIG", profileId: profile.id, iniConfig });
store.dispatch({
type: "ATTACH_KINEMATICS_RUNTIME",
runtime: await createLinuxCncKinematicsRuntime({ moduleId: iniConfig.kinematicsModuleId }),
});
store.dispatch({
type: "ATTACH_INTERPRETER_RUNTIME",
runtime: await createLinuxCncInterpreterRuntime(),
});
store.dispatch({ type: "ATTACH_TASK_HAL_RUNTIME", runtime: wrapTaskHalSdk(sdk) });
await store.stageMachineFiles();
store.dispatch({ type: "LOAD_LINUXCNC_GCODE_SOURCE", sourceRel });
await waitForState(store, (state) => (
state.taskHalSession?.programPath?.endsWith(sourceRel.split("/").at(-1)) &&
state.rtcpFrame?.sourceMode === "source-derived-kinematics-wasm"
));
store.dispatch({ type: "RUN_READY" });
await waitForTaskHalCommand(store);
await waitForState(store, (state) => state.machine.taskState === "on" && state.machine.mode === "auto");
let state = store.getState();
assert.equal(state.machine.powerOn, true);
assert.equal(state.machine.taskState, "on");
assert.equal(state.machine.mode, "auto");
assert.equal(state.machine.allHomed, true);
assert.equal(state.rtcpState, "on");
assert.equal(String(state.kinsType).startsWith("tcp-"), true);
store.dispatch({ type: "RUN" });
await waitForTaskHalCommand(store);
await waitForState(store, (nextState) => distinctActiveLines(nextState.programRuntimeFeedbackHistory).length >= 2);
state = store.getState();
assert.equal(state.programExecutionSourceMode, "linuxcnc-task-motion-hal-wasm");
assert.equal(state.programRuntimeFeedback.sourceMode, "linuxcnc-task-motion-hal-wasm");
assert.notEqual(state.operatorMessage, "run blocked: home machine first");
assert.equal(state.machine.allHomed, true);
store.dispatch({ type: "STOP" });
await waitForTaskHalCommand(store);
}
async function waitForTaskHalCommand(store) { async function waitForTaskHalCommand(store) {
for (let attempt = 0; attempt < 80; attempt += 1) { for (let attempt = 0; attempt < 80; attempt += 1) {
if (!store.getState().taskHalExecutionPending) { if (!store.getState().taskHalExecutionPending) {

View File

@@ -132,6 +132,11 @@ assert.equal(gate.operatorMessage, "run blocked: machine must be on");
store.dispatch({ type: "TOGGLE_POWER" }); store.dispatch({ type: "TOGGLE_POWER" });
gate = gateLinuxCncTaskAction(store.getState(), { type: "RUN" }); gate = gateLinuxCncTaskAction(store.getState(), { type: "RUN" });
assert.equal(gate.allowed, false); assert.equal(gate.allowed, false);
assert.equal(gate.operatorMessage, "run blocked: switch to auto mode first");
store.dispatch({ type: "SET_MODE", mode: "auto" });
gate = gateLinuxCncTaskAction(store.getState(), { type: "RUN" });
assert.equal(gate.allowed, false);
assert.equal(gate.operatorMessage, "run blocked: home machine first"); assert.equal(gate.operatorMessage, "run blocked: home machine first");
store.dispatch({ type: "HOME" }); store.dispatch({ type: "HOME" });