diff --git a/web-rtcp-5axis-sim-plan/app/src/state/linuxcnc-task-policy.js b/web-rtcp-5axis-sim-plan/app/src/state/linuxcnc-task-policy.js
index 03d40d8..3f1011a 100644
--- a/web-rtcp-5axis-sim-plan/app/src/state/linuxcnc-task-policy.js
+++ b/web-rtcp-5axis-sim-plan/app/src/state/linuxcnc-task-policy.js
@@ -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);
}
diff --git a/web-rtcp-5axis-sim-plan/app/src/ui/gmoccapy-shell.js b/web-rtcp-5axis-sim-plan/app/src/ui/gmoccapy-shell.js
index 6fd3eed..45444b4 100644
--- a/web-rtcp-5axis-sim-plan/app/src/ui/gmoccapy-shell.js
+++ b/web-rtcp-5axis-sim-plan/app/src/ui/gmoccapy-shell.js
@@ -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 })}
diff --git a/web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md b/web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md
index 5fb857a..c32bd45 100644
--- a/web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md
+++ b/web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md
@@ -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/`.
diff --git a/web-rtcp-5axis-sim-plan/tests/browser/gmoccapy_shell_smoke.html b/web-rtcp-5axis-sim-plan/tests/browser/gmoccapy_shell_smoke.html
index ea3702f..a0ce0de 100644
--- a/web-rtcp-5axis-sim-plan/tests/browser/gmoccapy_shell_smoke.html
+++ b/web-rtcp-5axis-sim-plan/tests/browser/gmoccapy_shell_smoke.html
@@ -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")) {
diff --git a/web-rtcp-5axis-sim-plan/tests/node/verify_gmoccapy_xyzab_gates.mjs b/web-rtcp-5axis-sim-plan/tests/node/verify_gmoccapy_xyzab_gates.mjs
index 2c11b40..c374cf1 100644
--- a/web-rtcp-5axis-sim-plan/tests/node/verify_gmoccapy_xyzab_gates.mjs
+++ b/web-rtcp-5axis-sim-plan/tests/node/verify_gmoccapy_xyzab_gates.mjs
@@ -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);
diff --git a/web-rtcp-5axis-sim-plan/tests/node/verify_run_preconditions.mjs b/web-rtcp-5axis-sim-plan/tests/node/verify_run_preconditions.mjs
index b86a5cd..6aa22b1 100644
--- a/web-rtcp-5axis-sim-plan/tests/node/verify_run_preconditions.mjs
+++ b/web-rtcp-5axis-sim-plan/tests/node/verify_run_preconditions.mjs
@@ -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" });
diff --git a/work/working5/01-项目功能内容.md b/work/working5/01-项目功能内容.md
new file mode 100644
index 0000000..46e409b
--- /dev/null
+++ b/work/working5/01-项目功能内容.md
@@ -0,0 +1,56 @@
+# 01-项目功能内容
+
+## 目标
+
+以 LinuxCNC `gmoccapy_XYZAB.ini` 五轴仿真配置为行为基准,执行、测试并完善 `/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan`。数控系统仿真功能应尽量与 LinuxCNC 源程序行为一致;发现不一致时,在 Web 项目中修复并补充验收证据。
+
+## 对标范围
+
+### 启动与配置
+
+- INI 解析:`[DISPLAY]`、`[TASK]`、`[EMCMOT]`、`[TRAJ]`、`[KINS]`、`[HAL]`、`[RS274NGC]`、`[PYTHON]` 等关键段。
+- 启动顺序:`linuxcncsvr`、RTAPI/HAL、`milltask`、`halui`、HALFILE、`halcmd start`、`gmoccapy`、POSTGUI_HALFILE。
+- 五轴配置:`COORDINATES = X Y Z A B`、`JOINTS = 5`、`trivkins coordinates=xyzab`。
+
+### 任务、状态与互锁
+
+- NML 命令、状态、错误通道的 Web 等效模型。
+- `STATE_ESTOP`、`STATE_ESTOP_RESET`、`STATE_OFF`、`STATE_ON`。
+- `MODE_MANUAL`、`MODE_MDI`、`MODE_AUTO`。
+- `INTERP_IDLE`、运行、暂停、停止、abort。
+- `NO_FORCE_HOMING = 0` 下的全轴回零互锁。
+- 自动运行、MDI、点动、回零、主轴、冷却、倍率、限位、换刀互锁。
+
+### HAL 与仿真 IO
+
+- `core_sim_XYZAB.hal`:motmod、五轴位置反馈回环、急停回环、换刀回环、主轴/冷却信号。
+- `spindle_sim.hal`:速度斜坡、编码器反馈、`spindle.0.at-speed`。
+- `simulated_home.hal`:X/Y/Z 回零开关模拟。
+- `gmoccapy_postgui.hal`:GUI HAL pins、主轴反馈条、到速 LED、刀补显示、换刀回环。
+- `HALUI = halui` 默认入口。
+
+### G-code 解释与五轴运动
+
+- RS274NGC 解释器行为。
+- MDI 命令发送与状态回读。
+- 自动运行文件、暂停、继续、单步、停止、从行运行。
+- `M6`、`M61` REMAP 相关换刀语义。
+- 五轴轨迹、单位换算、RTCP/运动学边界。
+
+### gmoccapy 界面等效行为
+
+- 周期轮询:`[DISPLAY] CYCLE_TIME = 100` ms。
+- 按钮/页面敏感状态互锁。
+- DRO、程序进度、主轴反馈、冷却液、工具/刀补、错误消息。
+- 外部 HAL 硬按钮和点动 pin 驱动 GUI 后再通过命令通道进入任务层。
+
+## 当前项目入口
+
+- Web 应用:`/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app`
+- 核心运行时:`app/src/runtime`
+- gmoccapy UI:`app/src/ui`
+- 状态存储:`app/src/state`
+- 机床配置/profile:`app/src/profiles`
+- Node 验证:`tests/node`
+- 浏览器验证:`tests/browser`
+
diff --git a/work/working5/02-项目程序开发详细步骤.md b/work/working5/02-项目程序开发详细步骤.md
new file mode 100644
index 0000000..a9b3db8
--- /dev/null
+++ b/work/working5/02-项目程序开发详细步骤.md
@@ -0,0 +1,48 @@
+# 02-项目程序开发详细步骤
+
+## 步骤 1:建立工作控制文件
+
+- 创建 README、功能内容、开发步骤、推进台账、任务矩阵、验收证据、决策记录。
+- 把任务按 LinuxCNC 执行流程拆分为可验收条目。
+
+## 步骤 2:读取基准与现有实现
+
+- 读取 `work/working3/gmoccapy_XYZAB_execution_analysis.md`。
+- 读取 Web 项目 runtime、state、profile、ui 和测试文件。
+- 将 LinuxCNC 执行链路映射到 Web 项目模块。
+
+## 步骤 3:执行现有测试
+
+- 在 `app` 下运行 `npm run smoke:node`。
+- 运行构建:`npm run build`。
+- 运行浏览器冒烟:`npm run smoke`。
+- 如有失败,先定位是否为环境、测试夹具或功能差异。
+
+## 步骤 4:差异分析
+
+按任务矩阵逐项检查:
+
+- INI/HAL/profile 是否覆盖 `gmoccapy_XYZAB.ini` 的五轴语义。
+- task/HAL runtime 是否覆盖状态机、互锁、回零、运行、暂停、停止、换刀、主轴、冷却、倍率。
+- interpreter/kinematics/session 是否覆盖 LinuxCNC 解释和五轴运行边界。
+- UI 是否按 gmoccapy 规则启停按钮并显示状态。
+
+## 步骤 5:修改实现
+
+- 优先修复已有测试暴露的问题。
+- 对未覆盖但属于 LinuxCNC 基准行为的差异,补测试后修实现。
+- 修改范围保持在对应 runtime/state/ui/profile/tests 文件内,避免无关重构。
+
+## 步骤 6:回归验收
+
+- 重新运行相关最小测试。
+- 运行完整 Node 冒烟测试。
+- 对 UI/浏览器改动运行构建与浏览器冒烟。
+- 把命令、结果、证据路径写入 `05-验收证据.md`。
+
+## 步骤 7:更新台账和任务矩阵
+
+- 更新已完成任务状态。
+- 写清本轮修改文件、验证命令和下一步。
+- 对重要取舍写入 `06-决策记录.md`。
+
diff --git a/work/working5/03-推进台账.md b/work/working5/03-推进台账.md
new file mode 100644
index 0000000..08f1578
--- /dev/null
+++ b/work/working5/03-推进台账.md
@@ -0,0 +1,59 @@
+# 03-推进台账
+
+## 2026-06-26 R1:启动 working5 对标推进
+
+做了什么:
+
+- 创建 working5 工作文件结构。
+- 建立与 `gmoccapy_XYZAB.ini` 执行流程对齐的任务矩阵和验收记录模板。
+- 开始读取 LinuxCNC 执行分析和 Web 项目结构。
+
+改了哪些文件:
+
+- `/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`
+
+验证了什么:
+
+- 待执行现有 Node、构建和浏览器冒烟测试。
+
+下一步是什么:
+
+- 执行 `npm run smoke:node`,根据失败项定位 LinuxCNC 行为差异。
+
+## 2026-06-26 R2:基线验证与 gmoccapy 模式互锁修复
+
+做了什么:
+
+- 执行现有 Node、构建、浏览器冒烟测试,确认项目基线可运行。
+- 对照 `gmoccapy_XYZAB_execution_analysis.md` 和 `gmoccapy.py` 的 `STATE_OFF`、`STATE_ON`、`all_homed` 逻辑,发现 Web 侧允许提前点击 Manual/MDI/Auto 模式按钮,store 虽能拦截部分运行动作,但 UI 敏感状态不完全等同 gmoccapy。
+- 修改 task policy:`SET_MODE` 在机床未上电时禁止 Manual/MDI/Auto/Jog;机床上电但未回零时只允许 Manual/Jog,禁止 MDI/Auto;Auto 运行中仍禁止离开 Auto。
+- 修改 gmoccapy Web shell:右侧 Manual/Jog/Auto/MDI 模式按钮统一使用 `gateLinuxCncTaskAction`,以 `disabled`、`data-command-ready`、`aria-disabled` 和 title 展示互锁原因。
+- 补充 Node 和浏览器测试,锁定下电、上电未回零、回零后三个阶段的模式按钮行为。
+
+改了哪些文件:
+
+- `/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/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/web-rtcp-5axis-sim-plan/tests/browser/gmoccapy_shell_smoke.html`
+- `/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`
+
+验证了什么:
+
+- `node ../tests/node/verify_gmoccapy_xyzab_gates.mjs && node ../tests/node/verify_run_preconditions.mjs && bash ../tests/browser/verify_gmoccapy_shell_browser.sh` 通过。
+- `npm run smoke:node` 通过。
+- `npm run build && npm run smoke` 通过。
+
+下一步是什么:
+
+- 下一轮可继续按任务矩阵深入核对 Tool/M6/M61 手动换刀对话框、限位 override、optional stop/block delete HAL pin 交叉行为等更细粒度 gmoccapy 行为。
diff --git a/work/working5/04-任务矩阵.md b/work/working5/04-任务矩阵.md
new file mode 100644
index 0000000..dccc79a
--- /dev/null
+++ b/work/working5/04-任务矩阵.md
@@ -0,0 +1,16 @@
+# 04-任务矩阵
+
+| 任务编号 | 任务 | 状态 | 验收标准 |
+| --- | --- | --- | --- |
+| W5-001 | 创建 working5 工作控制文件 | 完成 | README 和 01-06 文件存在,内容可指导后续推进 |
+| W5-010 | 建立 LinuxCNC 执行流程到 Web 模块映射 | 完成 | 已映射到 profile、task policy、HAL model、communication model、interpreter/runtime、UI shell 和 tests |
+| W5-020 | 执行现有 Node 冒烟测试 | 完成 | `npm run smoke:node` 通过 |
+| W5-030 | 执行构建测试 | 完成 | `npm run build` 通过 |
+| W5-040 | 执行浏览器冒烟测试 | 完成 | `npm run smoke` 通过 |
+| W5-100 | 校验 gmoccapy XYZAB profile | 完成 | `verify_gmoccapy_xyzab_profile.mjs`、完整 Node 冒烟通过 |
+| W5-110 | 校验 task 状态机和互锁 | 完成 | 修复并验证 Manual/MDI/Auto/Jog 模式按钮在下电、未回零、已回零状态下与 gmoccapy 一致 |
+| W5-120 | 校验 HAL runtime | 完成 | `verify_gmoccapy_hal_model.mjs`、`verify_linuxcnc_task_hal_runtime.mjs`、完整 Node 冒烟通过 |
+| W5-130 | 校验 G-code interpreter/runtime | 完成 | 自动运行、MDI、暂停/继续/停止、M6/M61 引用边界测试随完整 Node 冒烟通过 |
+| W5-140 | 校验五轴运动和单位换算 | 完成 | kinematics、real LinuxCNC 5-axis cases、linear unit conversion 测试随完整 Node 冒烟通过 |
+| W5-150 | 校验 gmoccapy UI 通信和按钮门控 | 完成 | 浏览器 smoke 验证 mode gate、NML/HAL 诊断、POSTGUI 顺序、按钮状态 |
+| W5-900 | 更新验收证据和决策记录 | 完成 | 命令、结果、修改原因和后续项已记录 |
diff --git a/work/working5/05-验收证据.md b/work/working5/05-验收证据.md
new file mode 100644
index 0000000..3161b04
--- /dev/null
+++ b/work/working5/05-验收证据.md
@@ -0,0 +1,69 @@
+# 05-验收证据
+
+## 证据索引
+
+| 证据编号 | 任务编号 | 类型 | 命令/页面/文件 | 结果 |
+| --- | --- | --- | --- | --- |
+| EV-W5-001 | W5-001 | 文件 | `/home/meswork/cnc_wams/work/working5` | 已创建 README 和 01-06 工作控制文件 |
+| EV-W5-020 | W5-020/W5-100/W5-110/W5-120/W5-130/W5-140 | 命令 | `cd /home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app && npm run smoke:node` | 通过,输出包含 `gmoccapy_xyzab_gates_smoke=ok`、`gmoccapy_hal_model_smoke=ok`、`real_linuxcnc_5axis_program_cases_smoke=ok`、`linear_unit_conversion_smoke=ok` |
+| EV-W5-030 | W5-030 | 命令 | `cd /home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app && npm run build` | 通过,输出 `gmoccapy_static_build=ok` |
+| EV-W5-040 | W5-040/W5-150 | 命令 | `cd /home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app && npm run smoke` | 通过,输出 `gmoccapy_shell_smoke=ok`、`gmoccapy_dist_smoke=ok` |
+| EV-W5-110 | W5-110/W5-150 | 命令 | `node ../tests/node/verify_gmoccapy_xyzab_gates.mjs && node ../tests/node/verify_run_preconditions.mjs && bash ../tests/browser/verify_gmoccapy_shell_browser.sh` | 通过,验证下电禁用 Manual/Jog/MDI/Auto,上电未回零只允许 Manual/Jog,回零后允许 Auto/MDI |
+
+## 命令记录
+
+### 2026-06-26 R2
+
+```bash
+cd /home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app
+npm run smoke:node
+```
+
+结果:通过。
+
+关键输出摘要:
+
+- `linuxcnc_kinematics_runtime_smoke=ok`
+- `linuxcnc_interpreter_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`
+
+```bash
+cd /home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app
+npm run build
+```
+
+结果:通过,输出 `gmoccapy_static_build=ok`。
+
+```bash
+cd /home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app
+npm run smoke
+```
+
+结果:通过,输出:
+
+- `gmoccapy_shell_smoke=ok`
+- `gmoccapy_dist_smoke=ok`
+
+```bash
+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
+```
+
+结果:通过,输出:
+
+- `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`
+
+## 页面/截图/report/job 记录
+
+本轮未生成截图、PDF、job_id 或 report_id。浏览器验收由 headless Chromium smoke 脚本完成。
diff --git a/work/working5/06-决策记录.md b/work/working5/06-决策记录.md
new file mode 100644
index 0000000..92bd29d
--- /dev/null
+++ b/work/working5/06-决策记录.md
@@ -0,0 +1,42 @@
+# 06-决策记录
+
+## DR-W5-001:以 gmoccapy_XYZAB 执行流程作为本轮验收主线
+
+日期:2026-06-26
+
+决策:
+
+- 本轮不只验证图形 RTCP 轨迹,而是按 LinuxCNC 启动、INI/HAL、task/NML、interpreter、gmoccapy UI 通信和互锁全链路推进。
+
+原因:
+
+- 用户指定的基准文件覆盖了 `gmoccapy_XYZAB.ini` 五轴仿真从启动到界面通信的完整执行流程。
+- 若只看轨迹显示,容易遗漏回零、状态机、换刀、主轴到速、冷却、UI gate 等数控系统仿真关键行为。
+
+影响:
+
+- 任务矩阵按 LinuxCNC 行为拆分。
+- 任何实现修改都需要补充对应测试或验收证据。
+
+## DR-W5-002:模式按钮门控前移到 `SET_MODE` 和 UI disabled 状态
+
+日期:2026-06-26
+
+决策:
+
+- Web 侧 `SET_MODE` 统一按 LinuxCNC/gmoccapy 状态门控。
+- `STATE_OFF` 或未上电时,Manual/Jog/MDI/Auto 都禁止切换。
+- `STATE_ON` 但未全轴回零且 `NO_FORCE_HOMING = 0` 时,只允许 Manual/Jog,禁止 MDI/Auto。
+- 回零后 MDI/Auto 恢复可用;Auto 解释器非 idle 时仍禁止离开 Auto。
+
+原因:
+
+- LinuxCNC `gmoccapy.py` 的 `on_hal_status_state_off()` 会禁用 `rbt_manual`、`rbt_mdi`、`rbt_auto`。
+- `on_hal_status_state_on()` 先只启用 `rbt_manual`,退出设置页时也明确区分“未回零只 Manual 可用”和“已回零三种模式可用”。
+- 旧 Web 实现主要在 `RUN` 和 `RUN_MDI` 时拦截,模式按钮本身仍可点击,不符合 gmoccapy 的按钮敏感状态。
+
+影响:
+
+- `app/src/state/linuxcnc-task-policy.js` 成为模式切换和执行动作的统一互锁入口。
+- `app/src/ui/gmoccapy-shell.js` 侧边模式按钮使用同一 gate 输出 disabled、`data-command-ready` 和 `aria-disabled`。
+- Node 和浏览器测试增加下电、未回零、已回零三个阶段的断言。
diff --git a/work/working5/README.md b/work/working5/README.md
new file mode 100644
index 0000000..659dc75
--- /dev/null
+++ b/work/working5/README.md
@@ -0,0 +1,27 @@
+# working5 README 索引
+
+本目录用于推进 `/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan` 与 LinuxCNC `gmoccapy_XYZAB.ini` 五轴仿真执行流程的一致性验证、修复和验收。
+
+对标基准:
+
+- `/home/meswork/cnc_wams/work/working3/gmoccapy_XYZAB_execution_analysis.md`
+- `/home/meswork/cnc_wams/linuxcnc/configs/sim/gmoccapy/gmoccapy_XYZAB.ini`
+- `/home/meswork/cnc_wams/linuxcnc/src/emc/usr_intf/gmoccapy/`
+- `/home/meswork/cnc_wams/linuxcnc/configs/sim/gmoccapy/*.hal`
+
+工作文件:
+
+1. [01-项目功能内容.md](01-项目功能内容.md)
+2. [02-项目程序开发详细步骤.md](02-项目程序开发详细步骤.md)
+3. [03-推进台账.md](03-推进台账.md)
+4. [04-任务矩阵.md](04-任务矩阵.md)
+5. [05-验收证据.md](05-验收证据.md)
+6. [06-决策记录.md](06-决策记录.md)
+
+推进规则:
+
+- 先按 `04-任务矩阵.md` 选择任务,避免重复做同一项。
+- 每轮结束更新 `03-推进台账.md`,记录做了什么、改了哪些文件、验证了什么、下一步是什么。
+- 所有命令、页面、报告、截图、测试输出摘要写入 `05-验收证据.md`。
+- 影响架构、兼容性或验收口径的判断写入 `06-决策记录.md`。
+