对齐 gmoccapy XYZAB 模式互锁

This commit is contained in:
2026-06-26 18:19:49 -04:00
parent 0664ea9bd7
commit 544bfd4b4b
13 changed files with 591 additions and 6 deletions

View File

@@ -218,6 +218,12 @@ function gateMode(status, requestedMode) {
if (status.taskMode === "auto" && status.interpState !== "idle" && targetMode !== "auto") {
return block(status, "mode blocked: AUTO interpreter is not idle");
}
if (status.taskState !== "on") {
return block(status, `mode blocked: machine must be on before ${targetMode.toUpperCase()}`);
}
if ((targetMode === "mdi" || targetMode === "auto") && !status.allHomed && !status.noForceHoming) {
return block(status, `mode blocked: home machine before ${targetMode.toUpperCase()}`);
}
return allow(status);
}

View File

@@ -349,14 +349,17 @@ function mdiQuickCommands(state) {
function renderSidebar(element, state, dispatch) {
const tcpKinsType = tcpKinsTypeForProfile(state.profile);
const tcpDisabled = tcpKinsType ? "" : ` disabled title="${escapeHtml(`${state.profile.id} is ${state.profile.kinematics} reference only`)}"`;
const autoGate = gateLinuxCncTaskAction(state, { type: "SET_MODE", mode: "auto" });
const manualGate = gateLinuxCncTaskAction(state, { type: "SET_MODE", mode: "manual" });
const mdiGate = gateLinuxCncTaskAction(state, { type: "SET_MODE", mode: "mdi" });
element.innerHTML = `
${iconButtonHtml({ buttonId: "tbtn_estop", action: "estop", label: "E-STOP", title: "Estop the machine / F1", className: "sidebar-button estop", active: state.machine.estopActive })}
${iconButtonHtml({ buttonId: "tbtn_on", action: "power", label: "POWER", title: "Turn the machine on/off / F2", className: "sidebar-button power", active: state.machine.powerOn })}
${iconButtonHtml({ buttonId: "tbtn_setup", action: "reset", label: "RESET", title: "Reset estop and return machine off", className: "sidebar-button", active: false })}
${iconButtonHtml({ buttonId: "rbt_auto", action: "mode-auto", label: "AUTO", title: "Enter auto mode", className: "sidebar-button", active: state.machine.mode === "auto" })}
${iconButtonHtml({ buttonId: "rbt_manual", action: "mode-manual", label: "MANUAL", title: "Enter manual mode", className: "sidebar-button", active: state.machine.mode === "manual" })}
${iconButtonHtml({ buttonId: "tbtn_user_tabs", action: "mode-jog", label: "JOG", title: "Manual jog page", className: "sidebar-button", active: state.machine.mode === "manual" })}
${iconButtonHtml({ buttonId: "rbt_mdi", action: "mode-mdi", label: "MDI", title: "Enter MDI mode", className: "sidebar-button", active: state.machine.mode === "mdi" })}
${iconButtonHtml({ buttonId: "rbt_auto", action: "mode-auto", label: "AUTO", title: autoGate.operatorMessage || "Enter auto mode", className: "sidebar-button", active: state.machine.mode === "auto", gate: autoGate })}
${iconButtonHtml({ buttonId: "rbt_manual", action: "mode-manual", label: "MANUAL", title: manualGate.operatorMessage || "Enter manual mode", className: "sidebar-button", active: state.machine.mode === "manual", gate: manualGate })}
${iconButtonHtml({ buttonId: "tbtn_user_tabs", action: "mode-jog", label: "JOG", title: manualGate.operatorMessage || "Manual jog page", className: "sidebar-button", active: state.machine.mode === "manual", gate: manualGate })}
${iconButtonHtml({ buttonId: "rbt_mdi", action: "mode-mdi", label: "MDI", title: mdiGate.operatorMessage || "Enter MDI mode", className: "sidebar-button", active: state.machine.mode === "mdi", gate: mdiGate })}
<button type="button" class="sidebar-button" data-action="kins-identity" data-active="${state.kinsType === "identity"}">IDENTITY</button>
<button type="button" class="sidebar-button" data-action="kins-tcp" data-active="${state.kinsType.startsWith("tcp-")}"${tcpDisabled}>TCP</button>
<time>13:30:31<br />20.06.2026</time>

View File

@@ -4779,3 +4779,167 @@ Outcome:
- The gmoccapy XYZAB `working4` completion commit was uploaded to the cloud repository.
- Remote `master` now points to `a81b40447de313117e2a389a394c3b8d80931001`.
- A follow-up process-log commit is required because this log entry was created after the main upload commit.
---
## 2026-06-26 18:15 EDT - working5 LinuxCNC gmoccapy_XYZAB execution-flow alignment
User request:
- Compare `/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan` against the five-axis machine execution flow documented in `/home/meswork/cnc_wams/work/working3/gmoccapy_XYZAB_execution_analysis.md`.
- Execute, test, and improve the LinuxCNC-ported CNC simulation so its simulation behavior matches the LinuxCNC source implementation.
- First create working files in `/home/meswork/cnc_wams/work/working5`:
- `01-项目功能内容`
- `02-项目程序开发详细步骤`
- `03-推进台账`
- `04-任务矩阵`
- `05-验收证据`
- `06-决策记录`
- plus README index.
Process:
- Confirmed workspace and root instructions:
- `/home/meswork/cnc_wams`
- `AGENTS.md` requires appending process logs to `web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md`.
- Read the LinuxCNC execution-flow reference:
- `/home/meswork/cnc_wams/work/working3/gmoccapy_XYZAB_execution_analysis.md`
- Key baseline: `gmoccapy_XYZAB.ini`, `trivkins coordinates=xyzab`, five joints, `NO_FORCE_HOMING = 0`, NML command/status/error, HAL loopbacks, POSTGUI after `halcomp.ready()`, and gmoccapy button sensitivity rules.
- Inspected project structure and test inventory:
- `app/src/runtime`
- `app/src/state`
- `app/src/ui`
- `app/src/profiles`
- `tests/node`
- `tests/browser`
- Created working5 control documents:
- `/home/meswork/cnc_wams/work/working5/README.md`
- `/home/meswork/cnc_wams/work/working5/01-项目功能内容.md`
- `/home/meswork/cnc_wams/work/working5/02-项目程序开发详细步骤.md`
- `/home/meswork/cnc_wams/work/working5/03-推进台账.md`
- `/home/meswork/cnc_wams/work/working5/04-任务矩阵.md`
- `/home/meswork/cnc_wams/work/working5/05-验收证据.md`
- `/home/meswork/cnc_wams/work/working5/06-决策记录.md`
- Ran initial baseline tests:
- `cd /home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app && npm run smoke:node`
- Result: passed.
- Key output included:
- `linuxcnc_kinematics_runtime_smoke=ok`
- `linuxcnc_interpreter_runtime_smoke=ok`
- `linuxcnc_ini_runtime_smoke=ok`
- `linuxcnc_task_hal_runtime_smoke=ok`
- `full_linuxcnc_5axis_source_node_smoke=ok`
- `real_linuxcnc_5axis_program_cases_smoke=ok`
- `gmoccapy_xyzab_profile_smoke=ok`
- `gmoccapy_communication_model_smoke=ok`
- `gmoccapy_hal_model_smoke=ok`
- `gmoccapy_xyzab_gates_smoke=ok`
- `linear_unit_conversion_smoke=ok`
- `cd /home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app && npm run build`
- Result: passed, output `gmoccapy_static_build=ok`.
- `cd /home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app && npm run smoke`
- Result: passed, output `gmoccapy_shell_smoke=ok` and `gmoccapy_dist_smoke=ok`.
- Reviewed relevant implementation files:
- `app/src/profiles/gmoccapy-xyzab.js`
- `app/src/state/linuxcnc-task-policy.js`
- `app/src/state/store.js`
- `app/src/ui/gmoccapy-shell.js`
- `app/src/runtime/gmoccapy-hal-model.js`
- `app/src/runtime/gmoccapy-communication-model.js`
- `tests/node/verify_gmoccapy_xyzab_gates.mjs`
- `tests/node/verify_run_preconditions.mjs`
- `tests/browser/gmoccapy_shell_smoke.html`
- Compared against LinuxCNC/gmoccapy source:
- `/home/meswork/cnc_wams/linuxcnc/src/emc/usr_intf/gmoccapy/gmoccapy.py`
- Observed native behavior:
- `on_hal_status_state_off()` disables `rbt_manual`, `rbt_mdi`, `rbt_auto`, and motion/execution controls.
- `on_hal_status_state_on()` enables Manual and base machine controls.
- With `NO_FORCE_HOMING = 0`, MDI/Auto remain unavailable until all axes are homed.
- Setup-page exit logic restores:
- estop/off: no mode buttons available
- on but not homed: only Manual available
- on and homed or no-force-homing: Manual/MDI/Auto available
- Found a Web behavior gap:
- Existing store/policy blocked `RUN` and `RUN_MDI` in invalid states.
- But mode buttons could still be clicked earlier than gmoccapy allows.
- Initial Web policy allowed mode changes that native gmoccapy would represent as insensitive buttons.
- Implemented fixes:
- In `app/src/state/linuxcnc-task-policy.js`:
- Added `SET_MODE` gate requiring `taskState === "on"` for Manual/Jog/MDI/Auto.
- Added homing gate for Auto/MDI when `NO_FORCE_HOMING = 0`.
- Preserved Auto-running protection that blocks leaving Auto while interpreter is not idle.
- In `app/src/ui/gmoccapy-shell.js`:
- Bound Manual/Jog/Auto/MDI sidebar mode buttons to `gateLinuxCncTaskAction`.
- Buttons now expose disabled state, `data-command-ready`, `aria-disabled`, and tooltip reason from the same task policy used by store dispatch.
- In `tests/node/verify_gmoccapy_xyzab_gates.mjs`:
- Added assertions that down/off state blocks Manual/MDI/Auto mode changes.
- Added assertions that power-on but unhomed state allows Manual but blocks Auto/MDI.
- Added assertions that after homing Auto mode is allowed.
- In `tests/node/verify_run_preconditions.mjs`:
- Updated expected behavior: trying Auto before homing leaves mode in Manual and run gate continues to say switch to Auto first.
- In `tests/browser/gmoccapy_shell_smoke.html`:
- Added DOM assertions for gmoccapy mode button sensitivity:
- initially powered off: Manual/Jog/Auto/MDI disabled
- powered on but unhomed: Manual/Jog enabled, Auto/MDI disabled with homing reason
- homed: Auto/MDI enabled.
- Ran targeted validation:
- `cd /home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app && node ../tests/node/verify_gmoccapy_xyzab_gates.mjs && node ../tests/node/verify_run_preconditions.mjs && bash ../tests/browser/verify_gmoccapy_shell_browser.sh`
- Result: passed.
- Output:
- `gmoccapy_xyzab_gates_smoke=ok`
- `run_preconditions_ini_profile_smoke=ok`
- `run_preconditions_kinematics_smoke=ok`
- `run_preconditions_machine_file_smoke=ok`
- `gmoccapy_shell_smoke=ok`
- Ran final full regression:
- `cd /home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app && npm run smoke:node`
- Result: passed.
- `cd /home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app && npm run build && npm run smoke`
- Result: passed.
- Output:
- `gmoccapy_static_build=ok`
- `gmoccapy_shell_smoke=ok`
- `gmoccapy_dist_smoke=ok`
- Ran quality checks:
- `git diff --check` in `/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan`
- Result: passed.
- `git diff --check` in `/home/meswork/cnc_wams`
- Result: passed.
- Updated working5 documentation:
- `03-推进台账.md` now records R1 setup and R2 mode-interlock fix.
- `04-任务矩阵.md` marks current pass complete for baseline profile/task/HAL/interpreter/UI verification.
- `05-验收证据.md` records command evidence and outputs.
- `06-决策记录.md` records why mode gating moved into `SET_MODE` and UI disabled state.
- Started a local static server:
- Command: `python3 -m http.server 4173`
- Directory: `/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app`
- Confirmed with:
- `curl -I --max-time 2 http://127.0.0.1:4173/`
- HTTP 200 OK.
- `ss -ltnp 'sport = :4173'`
- Listener: `python3`, PID `24983`.
Files changed:
- `/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app/src/state/linuxcnc-task-policy.js`
- `/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app/src/ui/gmoccapy-shell.js`
- `/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/tests/browser/gmoccapy_shell_smoke.html`
- `/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/tests/node/verify_gmoccapy_xyzab_gates.mjs`
- `/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/tests/node/verify_run_preconditions.mjs`
- `/home/meswork/cnc_wams/work/working5/README.md`
- `/home/meswork/cnc_wams/work/working5/01-项目功能内容.md`
- `/home/meswork/cnc_wams/work/working5/02-项目程序开发详细步骤.md`
- `/home/meswork/cnc_wams/work/working5/03-推进台账.md`
- `/home/meswork/cnc_wams/work/working5/04-任务矩阵.md`
- `/home/meswork/cnc_wams/work/working5/05-验收证据.md`
- `/home/meswork/cnc_wams/work/working5/06-决策记录.md`
- `/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md`
Git/worktree notes:
- `git status --short --branch` from `/home/meswork/cnc_wams` shows modified Web simulation files and new `work/working5/`.
- There is an unrelated untracked file already visible outside this task:
- `备忘/备忘3.txt`
- It was not modified.
Outcome:
- Created the requested working5 documentation set and README index.
- Executed and passed Node, build, browser, and targeted gmoccapy gate regressions.
- Fixed a real LinuxCNC/gmoccapy behavior mismatch: mode buttons now follow native `STATE_OFF`, `STATE_ON`, homing, and interpreter-idle sensitivity semantics.
- Local app server is available at `http://127.0.0.1:4173/`.

View File

@@ -303,6 +303,38 @@
if (!doc.querySelector('[data-machine-state="summary"]')?.textContent.includes("power off")) {
throw new Error("initial machine state should show power off");
}
const initialModeManualButton = doc.querySelector('[data-action="mode-manual"]');
const initialModeJogButton = doc.querySelector('[data-action="mode-jog"]');
const initialModeAutoButton = doc.querySelector('[data-action="mode-auto"]');
const initialModeMdiButton = doc.querySelector('[data-action="mode-mdi"]');
if (
!initialModeManualButton?.disabled ||
initialModeManualButton.dataset.commandReady !== "false" ||
initialModeManualButton.getAttribute("aria-disabled") !== "true"
) {
throw new Error("MANUAL mode button should be disabled before machine power on");
}
if (
!initialModeJogButton?.disabled ||
initialModeJogButton.dataset.commandReady !== "false" ||
initialModeJogButton.getAttribute("aria-disabled") !== "true"
) {
throw new Error("JOG mode button should be disabled before machine power on");
}
if (
!initialModeAutoButton?.disabled ||
initialModeAutoButton.dataset.commandReady !== "false" ||
initialModeAutoButton.getAttribute("aria-disabled") !== "true"
) {
throw new Error("AUTO mode button should be disabled before machine power on");
}
if (
!initialModeMdiButton?.disabled ||
initialModeMdiButton.dataset.commandReady !== "false" ||
initialModeMdiButton.getAttribute("aria-disabled") !== "true"
) {
throw new Error("MDI mode button should be disabled before machine power on");
}
const initiallyBlockedRunButton = doc.querySelector('[data-action="RUN"]');
if (
@@ -325,8 +357,44 @@
if (!doc.querySelector('[data-machine-state="summary"]')?.textContent.includes("power on")) {
throw new Error("machine state did not render power on");
}
const unhomedModeManualButton = doc.querySelector('[data-action="mode-manual"]');
const unhomedModeJogButton = doc.querySelector('[data-action="mode-jog"]');
const unhomedModeAutoButton = doc.querySelector('[data-action="mode-auto"]');
const unhomedModeMdiButton = doc.querySelector('[data-action="mode-mdi"]');
if (
unhomedModeManualButton?.disabled ||
unhomedModeManualButton.dataset.commandReady !== "true" ||
unhomedModeJogButton?.disabled ||
unhomedModeJogButton.dataset.commandReady !== "true"
) {
throw new Error("MANUAL and JOG mode buttons should be enabled after power on");
}
if (
!unhomedModeAutoButton?.disabled ||
unhomedModeAutoButton.dataset.commandReady !== "false" ||
!unhomedModeAutoButton.title.includes("home machine before AUTO")
) {
throw new Error("AUTO mode button should remain disabled until homed");
}
if (
!unhomedModeMdiButton?.disabled ||
unhomedModeMdiButton.dataset.commandReady !== "false" ||
!unhomedModeMdiButton.title.includes("home machine before MDI")
) {
throw new Error("MDI mode button should remain disabled until homed");
}
doc.querySelector('[data-action="HOME"]').click();
await wait(50);
const homedModeAutoButton = doc.querySelector('[data-action="mode-auto"]');
const homedModeMdiButton = doc.querySelector('[data-action="mode-mdi"]');
if (
homedModeAutoButton?.disabled ||
homedModeAutoButton.dataset.commandReady !== "true" ||
homedModeMdiButton?.disabled ||
homedModeMdiButton.dataset.commandReady !== "true"
) {
throw new Error("AUTO and MDI mode buttons should be enabled after homing");
}
doc.querySelector('[data-action="mode-auto"]').click();
await wait(50);
if (!doc.querySelector('[data-linuxcnc-task-policy="gates"]')?.textContent.includes("auto")) {

View File

@@ -15,18 +15,41 @@ let gate = gateLinuxCncTaskAction(store.getState(), { type: "RUN" });
assert.equal(gate.allowed, false);
assert.equal(gate.operatorMessage, "run blocked: machine must be on");
gate = gateLinuxCncTaskAction(store.getState(), { type: "SET_MODE", mode: "manual" });
assert.equal(gate.allowed, false);
assert.equal(gate.operatorMessage, "mode blocked: machine must be on before MANUAL");
gate = gateLinuxCncTaskAction(store.getState(), { type: "SET_MODE", mode: "mdi" });
assert.equal(gate.allowed, false);
assert.equal(gate.operatorMessage, "mode blocked: machine must be on before MDI");
gate = gateLinuxCncTaskAction(store.getState(), { type: "SET_MODE", mode: "auto" });
assert.equal(gate.allowed, false);
assert.equal(gate.operatorMessage, "mode blocked: machine must be on before AUTO");
store.dispatch({ type: "TOGGLE_POWER" });
gate = gateLinuxCncTaskAction(store.getState(), { type: "SET_MODE", mode: "manual" });
assert.equal(gate.allowed, true);
gate = gateLinuxCncTaskAction(store.getState(), { type: "RUN" });
assert.equal(gate.allowed, false);
assert.equal(gate.operatorMessage, "run blocked: switch to auto mode first");
gate = gateLinuxCncTaskAction(store.getState(), { type: "SET_MODE", mode: "auto" });
assert.equal(gate.allowed, false);
assert.equal(gate.operatorMessage, "mode blocked: home machine before AUTO");
store.dispatch({ type: "SET_MODE", mode: "auto" });
assert.equal(store.getState().machine.mode, "manual");
assert.equal(store.getState().operatorMessage, "mode blocked: home machine before 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: switch to auto mode first");
gate = gateLinuxCncTaskAction(store.getState(), { type: "SET_MODE", mode: "mdi" });
assert.equal(gate.allowed, false);
assert.equal(gate.operatorMessage, "mode blocked: home machine before MDI");
store.dispatch({ type: "SET_MODE", mode: "manual" });
store.dispatch({ type: "HOME" });
gate = gateLinuxCncTaskAction(store.getState(), { type: "SET_MODE", mode: "auto" });
assert.equal(gate.allowed, true);
store.dispatch({ type: "SET_MODE", mode: "auto" });
gate = gateLinuxCncTaskAction(store.getState(), { type: "RUN" });
assert.equal(gate.allowed, false);

View File

@@ -134,10 +134,14 @@ gate = gateLinuxCncTaskAction(store.getState(), { type: "RUN" });
assert.equal(gate.allowed, false);
assert.equal(gate.operatorMessage, "run blocked: switch to auto mode first");
gate = gateLinuxCncTaskAction(store.getState(), { type: "SET_MODE", mode: "auto" });
assert.equal(gate.allowed, false);
assert.equal(gate.operatorMessage, "mode blocked: home machine before AUTO");
store.dispatch({ type: "SET_MODE", mode: "auto" });
assert.equal(store.getState().machine.mode, "manual");
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: switch to auto mode first");
store.dispatch({ type: "HOME" });
store.dispatch({ type: "SET_MODE", mode: "manual" });