diff --git a/AGENTS.md b/AGENTS.md
index c940252..b14d402 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -11,3 +11,5 @@ After every GPT/Codex execution completes, append the full execution process
log to:
`/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md`
+
+The log content written to this file must be recorded in Chinese.
diff --git a/linuxcnc.zip b/linuxcnc.zip
deleted file mode 100644
index 947b12a..0000000
Binary files a/linuxcnc.zip and /dev/null differ
diff --git a/web-rtcp-5axis-sim-plan/app/src/profiles/gmoccapy-xyzab.js b/web-rtcp-5axis-sim-plan/app/src/profiles/gmoccapy-xyzab.js
index 048b333..a27d86a 100644
--- a/web-rtcp-5axis-sim-plan/app/src/profiles/gmoccapy-xyzab.js
+++ b/web-rtcp-5axis-sim-plan/app/src/profiles/gmoccapy-xyzab.js
@@ -2,6 +2,15 @@ import { getSourceReferencesForProfile } from "./source-reference-map.js";
const sourceReferenceObjects = getSourceReferencesForProfile("gmoccapy-xyzab");
+const gmoccapyXyzabJogAxisPins = ["x", "y", "z", "a", "b"].flatMap((axis) => [
+ `gmoccapy.jog.axis.jog-${axis}-plus`,
+ `gmoccapy.jog.axis.jog-${axis}-minus`,
+]);
+
+const gmoccapyXyzabJogIncrementPins = Array.from({ length: 6 }, (_, index) => (
+ `gmoccapy.jog.jog-inc-${index}`
+));
+
export const gmoccapyXyzabPanelSchema = {
id: "gmoccapy-xyzab-hal-component",
profileId: "gmoccapy-xyzab",
@@ -20,8 +29,8 @@ export const gmoccapyXyzabPanelSchema = {
id: "tbtn-estop",
type: "button",
text: "ESTOP",
- halpin: "gmoccapy.h-button.button-0",
- halNet: "gmoccapy-h-button-0",
+ halpin: "gmoccapy.v-button.button-0",
+ halNet: "gmoccapy-v-button-0",
target: "gmoccapy.command.STATE_ESTOP",
nativeCommand: "STATE_ESTOP / STATE_ESTOP_RESET",
webAction: { type: "ESTOP" },
@@ -30,8 +39,8 @@ export const gmoccapyXyzabPanelSchema = {
id: "tbtn-on",
type: "button",
text: "POWER",
- halpin: "gmoccapy.h-button.button-1",
- halNet: "gmoccapy-h-button-1",
+ halpin: "gmoccapy.v-button.button-1",
+ halNet: "gmoccapy-v-button-1",
target: "gmoccapy.command.STATE_ON",
nativeCommand: "STATE_ON / STATE_OFF",
webAction: { type: "TOGGLE_POWER" },
@@ -40,8 +49,8 @@ export const gmoccapyXyzabPanelSchema = {
id: "rbt-manual",
type: "button",
text: "MANUAL",
- halpin: "gmoccapy.v-button.button-0",
- halNet: "gmoccapy-v-button-0",
+ halpin: "gmoccapy.v-button.button-2",
+ halNet: "gmoccapy-v-button-2",
target: "gmoccapy.command.MODE_MANUAL",
nativeCommand: "MODE_MANUAL",
webAction: { type: "SET_MODE", mode: "manual" },
@@ -50,8 +59,8 @@ export const gmoccapyXyzabPanelSchema = {
id: "rbt-mdi",
type: "button",
text: "MDI",
- halpin: "gmoccapy.v-button.button-1",
- halNet: "gmoccapy-v-button-1",
+ halpin: "gmoccapy.v-button.button-3",
+ halNet: "gmoccapy-v-button-3",
target: "gmoccapy.command.MODE_MDI",
nativeCommand: "MODE_MDI",
webAction: { type: "SET_MODE", mode: "mdi" },
@@ -60,8 +69,8 @@ export const gmoccapyXyzabPanelSchema = {
id: "rbt-auto",
type: "button",
text: "AUTO",
- halpin: "gmoccapy.v-button.button-2",
- halNet: "gmoccapy-v-button-2",
+ halpin: "gmoccapy.v-button.button-4",
+ halNet: "gmoccapy-v-button-4",
target: "gmoccapy.command.MODE_AUTO",
nativeCommand: "MODE_AUTO",
webAction: { type: "SET_MODE", mode: "auto" },
@@ -215,6 +224,26 @@ export const gmoccapyXyzabProfile = {
{ signal: "spindle-at-speed", source: "near_speed.out", target: "spindle.0.at-speed" },
{ signal: "spindle-abs", source: "abs_spindle_feedback.out", target: "gmoccapy.spindle_feedback_bar" },
],
+ toolChange: {
+ strategy: "iocontrol-loopback",
+ manualGmoccapyPinsConnected: false,
+ postguiUnlinks: [
+ "iocontrol.0.tool-change",
+ "iocontrol.0.tool-changed",
+ "iocontrol.0.tool-prep-number",
+ ],
+ commentedManualGuiNets: [
+ "gmoccapy.toolchange-change <= iocontrol.0.tool-change",
+ "gmoccapy.toolchange-changed => iocontrol.0.tool-changed",
+ "gmoccapy.toolchange-number <= iocontrol.0.tool-prep-number",
+ ],
+ activeLoop: {
+ signal: "tool-change-loop",
+ source: "iocontrol.0.tool-change",
+ target: "iocontrol.0.tool-changed",
+ stage: "gmoccapy_postgui.hal",
+ },
+ },
},
},
nativeRuntime: {
@@ -269,11 +298,32 @@ export const gmoccapyXyzabProfile = {
halPins: [
"gmoccapy.h-button.button-0",
"gmoccapy.v-button.button-0",
- "gmoccapy.jog.axis.jog-x-plus",
- "gmoccapy.jog.axis.jog-b-minus",
+ ...gmoccapyXyzabJogAxisPins,
+ ...gmoccapyXyzabJogIncrementPins,
+ "gmoccapy.jog.jog-increment",
+ "gmoccapy.jog.turtle-jog",
"gmoccapy.feed.feed-override.counts",
+ "gmoccapy.feed.feed-override.count-enable",
+ "gmoccapy.feed.feed-override.analog-enable",
+ "gmoccapy.feed.feed-override.direct-value",
+ "gmoccapy.feed.reset-feed-override",
"gmoccapy.spindle.spindle-override.counts",
+ "gmoccapy.spindle.spindle-override.count-enable",
+ "gmoccapy.spindle.spindle-override.analog-enable",
+ "gmoccapy.spindle.spindle-override.direct-value",
+ "gmoccapy.spindle.reset-spindle-override",
+ "gmoccapy.rapid.rapid-override.counts",
+ "gmoccapy.rapid.rapid-override.count-enable",
+ "gmoccapy.rapid.rapid-override.analog-enable",
"gmoccapy.rapid.rapid-override.direct-value",
+ "gmoccapy.rapid.reset-rapid-override",
+ "gmoccapy.jog.jog-velocity.counts",
+ "gmoccapy.jog.jog-velocity.count-enable",
+ "gmoccapy.jog.jog-velocity.analog-enable",
+ "gmoccapy.jog.jog-velocity.direct-value",
+ "gmoccapy.ignore-limits",
+ "gmoccapy.optional-stop",
+ "gmoccapy.blockdelete",
"gmoccapy.tooloffset-x",
"gmoccapy.tooloffset-z",
"gmoccapy.toolchange-change",
@@ -284,6 +334,8 @@ export const gmoccapyXyzabProfile = {
"gmoccapy.spindle_feedback_bar",
"gmoccapy.spindle_at_speed_led",
"gmoccapy.error",
+ "gmoccapy.delete-message",
+ "gmoccapy.warning-confirm",
],
offsets: {
toolOffsetX: 0,
diff --git a/web-rtcp-5axis-sim-plan/app/src/runtime/gmoccapy-communication-model.js b/web-rtcp-5axis-sim-plan/app/src/runtime/gmoccapy-communication-model.js
index a183d76..7de50fa 100644
--- a/web-rtcp-5axis-sim-plan/app/src/runtime/gmoccapy-communication-model.js
+++ b/web-rtcp-5axis-sim-plan/app/src/runtime/gmoccapy-communication-model.js
@@ -20,6 +20,16 @@ export const gmoccapyCommunicationModel = {
path: ["gmoccapy.* pins", "HAL shared memory", "halui/iocontrol/motmod/spindle_sim"],
notes: "postgui HAL connects gmoccapy pins after halcomp.ready().",
},
+ hardwareButtons: {
+ label: "HAL hardware buttons",
+ path: ["external HAL bit", "gmoccapy.h-button/v-button pin", "_button_pin_changed", "visible sensitive GTK button", "linuxcnc.command()"],
+ notes: "gmoccapy maps rising-edge hard-button pins to the current visible software button; insensitive or hidden targets are ignored.",
+ },
+ halPins: {
+ label: "HAL input pins",
+ path: ["external HAL value", "gmoccapy.* input pin", "hal_glib.GPin value_changed", "gmoccapy callback", "linuxcnc.command() or GTK widget state"],
+ notes: "jog pins, jog increment pins, ignore-limits, optional-stop/blockdelete, override counts/direct-value, reset and message pins enter gmoccapy through HAL callbacks.",
+ },
remap: {
label: "Interpreter REMAP",
path: ["milltask", "RS274NGC", "Python remap prolog/ngc/epilog"],
@@ -148,6 +158,34 @@ export const gmoccapyCommunicationModel = {
nativeStatus: "iocontrol.0.coolant-flood/mist",
gate: "blocked in estop/off",
},
+ {
+ actionId: "hardware-button",
+ webAction: "explicit data-gmoccapy-hal-pin button mapping",
+ nativeCommand: "_button_pin_changed -> GtkButton emit clicked/pressed/toggled",
+ nativeStatus: "same status as the target software button",
+ gate: "rising-edge pin ignored when target button is hidden or insensitive",
+ },
+ {
+ actionId: "hal-pin-input",
+ webAction: "GMOCAPY_HAL_PIN",
+ nativeCommand: "_ignore_limits/_optional_blocks/_blockdelete/_on_counts_changed/_on_analog_value_changed/_reset_override/_del_message_changed",
+ nativeStatus: "ignore-limits, block delete, optional stop, feed/rapid/spindle/jog override widget state and operator message state",
+ gate: "counts require count-enable, direct-value requires analog-enable, reset/delete-message pins are rising-edge only",
+ },
+ {
+ actionId: "hal-jog-pin",
+ webAction: "GMOCAPY_HAL_PIN jog.axis/jog-inc",
+ nativeCommand: "_on_pin_jog_changed/_on_pin_incr_changed",
+ nativeStatus: "manual jog command, active jog increment and gmoccapy.jog.jog-increment HAL OUT",
+ gate: "axis pins use press/release level, task must be on and manual; increment pins are rising-edge only",
+ },
+ {
+ actionId: "hal-warning-confirm",
+ webAction: "GMOCAPY_HAL_PIN warning-confirm",
+ nativeCommand: "dialogs.warning_dialog confirm_pin polling",
+ nativeStatus: "active warning dialog response",
+ gate: "level-polled while warning dialog is active",
+ },
],
};
@@ -163,6 +201,8 @@ export function createGmoccapyCommunicationSummary(model = gmoccapyCommunication
model.startupStages.findIndex((stage) => stage.id === "gmoccapy"),
hasNativeNmlCommandPath: model.nativePaths.command.path.includes("NML emcCommand"),
hasNativeHalPath: model.nativePaths.hal.path.includes("HAL shared memory"),
+ hasNativeHardwareButtonPath: model.nativePaths.hardwareButtons.path.includes("_button_pin_changed"),
+ hasNativeHalPinInputPath: model.nativePaths.halPins.path.includes("hal_glib.GPin value_changed"),
hasWebStorePath: model.webPath.path.includes("store.dispatch"),
semanticBoundary: model.semanticBoundary,
};
diff --git a/web-rtcp-5axis-sim-plan/app/src/runtime/gmoccapy-hal-model.js b/web-rtcp-5axis-sim-plan/app/src/runtime/gmoccapy-hal-model.js
index 7136eef..1461fc0 100644
--- a/web-rtcp-5axis-sim-plan/app/src/runtime/gmoccapy-hal-model.js
+++ b/web-rtcp-5axis-sim-plan/app/src/runtime/gmoccapy-hal-model.js
@@ -68,7 +68,13 @@ export const gmoccapyHalModel = {
"gmoccapy.jog.axis.jog-b-plus",
"gmoccapy.jog.axis.jog-b-minus",
"gmoccapy.jog.jog-inc-0",
+ "gmoccapy.jog.jog-inc-1",
+ "gmoccapy.jog.jog-inc-2",
+ "gmoccapy.jog.jog-inc-3",
+ "gmoccapy.jog.jog-inc-4",
+ "gmoccapy.jog.jog-inc-5",
"gmoccapy.jog.jog-increment",
+ "gmoccapy.jog.turtle-jog",
],
webSource: "JOG dispatch and machine.jogIncrement",
semanticBoundary: "native_jog_pin_reference_web_task_policy_gate",
@@ -77,17 +83,38 @@ export const gmoccapyHalModel = {
group: "override",
pins: [
"gmoccapy.feed.feed-override.counts",
+ "gmoccapy.feed.feed-override.count-enable",
+ "gmoccapy.feed.feed-override.analog-enable",
"gmoccapy.feed.feed-override.direct-value",
+ "gmoccapy.feed.reset-feed-override",
"gmoccapy.spindle.spindle-override.counts",
+ "gmoccapy.spindle.spindle-override.count-enable",
+ "gmoccapy.spindle.spindle-override.analog-enable",
"gmoccapy.spindle.spindle-override.direct-value",
+ "gmoccapy.spindle.reset-spindle-override",
"gmoccapy.jog.jog-velocity.counts",
+ "gmoccapy.jog.jog-velocity.count-enable",
+ "gmoccapy.jog.jog-velocity.analog-enable",
"gmoccapy.jog.jog-velocity.direct-value",
"gmoccapy.rapid.rapid-override.counts",
+ "gmoccapy.rapid.rapid-override.count-enable",
+ "gmoccapy.rapid.rapid-override.analog-enable",
"gmoccapy.rapid.rapid-override.direct-value",
+ "gmoccapy.rapid.reset-rapid-override",
],
webSource: "feed/spindle override state",
semanticBoundary: "native_override_pin_reference_web_state_mapping",
},
+ {
+ group: "operator-inputs",
+ pins: [
+ "gmoccapy.ignore-limits",
+ "gmoccapy.optional-stop",
+ "gmoccapy.blockdelete",
+ ],
+ webSource: "gmoccapyGui ignoreLimits, optionalBlocks, optionalStop",
+ semanticBoundary: "native_ignore_limits_optional_stop_blockdelete_pin_reference_web_state_mapping",
+ },
{
group: "tool",
pins: [
@@ -98,7 +125,7 @@ export const gmoccapyHalModel = {
"gmoccapy.toolchange-changed",
"gmoccapy.toolchange-number",
],
- webSource: "toolPreview and tool-change diagnostics",
+ webSource: "toolPreview and disconnected manual tool-change diagnostics",
semanticBoundary: "native_tool_pin_reference_simulated_tool_loop",
},
{
@@ -257,12 +284,319 @@ export const gmoccapyHalModel = {
role: "automatic simulated tool-change completion",
},
],
+ hardwareButtons: {
+ source: {
+ path: "linuxcnc/src/emc/usr_intf/gmoccapy/gmoccapy.py",
+ methods: ["_button_pin_changed", "_get_child_button", "_make_hal_pins"],
+ },
+ behavior: {
+ edge: "rising-edge-only",
+ hButtonContainer: "current ntb_button page visible children",
+ vButtonContainer: "vbtb_main visible children",
+ insensitiveTarget: "ignored",
+ hiddenLabelsSkipped: true,
+ semanticBoundary: "native_gmoccapy_hardware_button_pin_to_visible_sensitive_button",
+ },
+ verticalMain: [
+ {
+ index: 0,
+ pin: "gmoccapy.v-button.button-0",
+ nativeWidget: "tbtn_estop",
+ webAction: "ESTOP",
+ webDispatch: { type: "ESTOP" },
+ webSelector: '[data-action="estop"]',
+ gate: "always clickable safety action",
+ },
+ {
+ index: 1,
+ pin: "gmoccapy.v-button.button-1",
+ nativeWidget: "tbtn_on",
+ webAction: "TOGGLE_POWER",
+ webDispatch: { type: "TOGGLE_POWER" },
+ webSelector: '[data-action="power"]',
+ gate: "blocked while STATE_ESTOP",
+ },
+ {
+ index: 2,
+ pin: "gmoccapy.v-button.button-2",
+ nativeWidget: "rbt_manual",
+ webAction: "SET_MODE manual",
+ webDispatch: { type: "SET_MODE", mode: "manual" },
+ webSelector: '[data-action="mode-manual"]',
+ gate: "machine on and AUTO interpreter idle",
+ },
+ {
+ index: 3,
+ pin: "gmoccapy.v-button.button-3",
+ nativeWidget: "rbt_mdi",
+ webAction: "SET_MODE mdi",
+ webDispatch: { type: "SET_MODE", mode: "mdi" },
+ webSelector: '[data-action="mode-mdi"]',
+ gate: "machine on and homed when NO_FORCE_HOMING=0",
+ },
+ {
+ index: 4,
+ pin: "gmoccapy.v-button.button-4",
+ nativeWidget: "rbt_auto",
+ webAction: "SET_MODE auto",
+ webDispatch: { type: "SET_MODE", mode: "auto" },
+ webSelector: '[data-action="mode-auto"]',
+ gate: "machine on and homed when NO_FORCE_HOMING=0",
+ },
+ {
+ index: 5,
+ pin: "gmoccapy.v-button.button-5",
+ nativeWidget: "tbtn_user_tabs",
+ webAction: "SET_MODE manual",
+ webDispatch: { type: "SET_MODE", mode: "jog" },
+ webSelector: '[data-action="mode-jog"]',
+ gate: "machine on and manual page available",
+ },
+ {
+ index: 6,
+ pin: "gmoccapy.v-button.button-6",
+ nativeWidget: "tbtn_setup",
+ webAction: "UI_CONTROL",
+ webSelector: null,
+ gate: "native setup page is diagnostic-only in the Web shell",
+ },
+ ],
+ bottomMain: [
+ {
+ index: 0,
+ pin: "gmoccapy.h-button.button-0",
+ nativeWidget: "btn_homing",
+ webAction: "HOME",
+ webDispatch: { type: "HOME" },
+ webSelector: '[data-action="HOME"]',
+ gate: "machine on and manual mode",
+ },
+ {
+ index: 1,
+ pin: "gmoccapy.h-button.button-1",
+ nativeWidget: "btn_touch",
+ webAction: "UI_CONTROL",
+ webSelector: null,
+ gate: "touch-off page not implemented in Web shell",
+ },
+ {
+ index: 3,
+ pin: "gmoccapy.h-button.button-3",
+ nativeWidget: "btn_tool",
+ webAction: "UI_CONTROL",
+ webSelector: null,
+ gate: "tool editor page not implemented in Web shell",
+ },
+ {
+ index: 6,
+ pin: "gmoccapy.h-button.button-6",
+ nativeWidget: "tbtn_switch_mode",
+ webAction: "SET_KINS_TYPE identity",
+ webSelector: null,
+ gate: "native joint/world switch page position is diagnostic-only in the Web shell",
+ },
+ ],
+ },
+ halPinActions: {
+ source: {
+ path: "linuxcnc/src/emc/usr_intf/gmoccapy/gmoccapy.py",
+ methods: [
+ "_ignore_limits",
+ "_optional_blocks",
+ "_blockdelete",
+ "_on_pin_jog_changed",
+ "_on_pin_incr_changed",
+ "_on_counts_changed",
+ "_on_analog_enable_changed",
+ "_on_analog_value_changed",
+ "_reset_override",
+ "_del_message_changed",
+ ],
+ prefPath: "linuxcnc/configs/sim/gmoccapy/gmoccapy_XYZAB.pref",
+ },
+ jogPins: {
+ axes: [
+ { axis: "x", plus: "gmoccapy.jog.axis.jog-x-plus", minus: "gmoccapy.jog.axis.jog-x-minus" },
+ { axis: "y", plus: "gmoccapy.jog.axis.jog-y-plus", minus: "gmoccapy.jog.axis.jog-y-minus" },
+ { axis: "z", plus: "gmoccapy.jog.axis.jog-z-plus", minus: "gmoccapy.jog.axis.jog-z-minus" },
+ { axis: "a", plus: "gmoccapy.jog.axis.jog-a-plus", minus: "gmoccapy.jog.axis.jog-a-minus" },
+ { axis: "b", plus: "gmoccapy.jog.axis.jog-b-plus", minus: "gmoccapy.jog.axis.jog-b-minus" },
+ ],
+ increments: [
+ { index: 0, pin: "gmoccapy.jog.jog-inc-0", label: "Continuous", distance: 0 },
+ { index: 1, pin: "gmoccapy.jog.jog-inc-1", label: "1.000 mm", distance: 1 },
+ { index: 2, pin: "gmoccapy.jog.jog-inc-2", label: "0.100 mm", distance: 0.1 },
+ { index: 3, pin: "gmoccapy.jog.jog-inc-3", label: "0.010 mm", distance: 0.01 },
+ { index: 4, pin: "gmoccapy.jog.jog-inc-4", label: "0.001 mm", distance: 0.001 },
+ { index: 5, pin: "gmoccapy.jog.jog-inc-5", label: "1.2345 in", distance: 31.3563 },
+ ],
+ outputPin: "gmoccapy.jog.jog-increment",
+ turtlePin: "gmoccapy.jog.turtle-jog",
+ nativeCallbacks: {
+ axis: "_on_pin_jog_changed -> _on_btn_jog_pressed/_on_btn_jog_released",
+ increment: "_on_pin_incr_changed -> _jog_increment_changed",
+ turtle: "_on_pin_turtle_jog -> tbtn_turtle_jog.set_active",
+ },
+ },
+ operatorPins: [
+ {
+ pin: "gmoccapy.ignore-limits",
+ nativeCallback: "_ignore_limits -> chk_ignore_limits.set_active",
+ nativeCommand: "on_chk_ignore_limits_toggled -> command.override_limits() when active",
+ webDispatch: { type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.ignore-limits" },
+ webState: "gmoccapyGui.ignoreLimits",
+ },
+ {
+ pin: "gmoccapy.optional-stop",
+ nativeCallback: "_optional_blocks",
+ nativeCommand: "tbtn_optional_blocks -> command.set_block_delete(state)",
+ webDispatch: { type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.optional-stop" },
+ webState: "gmoccapyGui.optionalBlocks",
+ },
+ {
+ pin: "gmoccapy.blockdelete",
+ nativeCallback: "_blockdelete",
+ nativeCommand: "command.set_optional_stop(state)",
+ webDispatch: { type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.blockdelete" },
+ webState: "gmoccapyGui.optionalStop",
+ },
+ ],
+ overridePins: [
+ {
+ target: "feed",
+ counts: "gmoccapy.feed.feed-override.counts",
+ countEnable: "gmoccapy.feed.feed-override.count-enable",
+ analogEnable: "gmoccapy.feed.feed-override.analog-enable",
+ directValue: "gmoccapy.feed.feed-override.direct-value",
+ reset: "gmoccapy.feed.reset-feed-override",
+ nativeWidget: "spc_feed",
+ scalePref: "scale_feed_override=1",
+ webState: "feed.feedOverride",
+ },
+ {
+ target: "rapid",
+ counts: "gmoccapy.rapid.rapid-override.counts",
+ countEnable: "gmoccapy.rapid.rapid-override.count-enable",
+ analogEnable: "gmoccapy.rapid.rapid-override.analog-enable",
+ directValue: "gmoccapy.rapid.rapid-override.direct-value",
+ reset: "gmoccapy.rapid.reset-rapid-override",
+ nativeWidget: "spc_rapid",
+ scalePref: "scale_rapid_override=1",
+ webState: "feed.rapidOverride",
+ },
+ {
+ target: "spindle",
+ counts: "gmoccapy.spindle.spindle-override.counts",
+ countEnable: "gmoccapy.spindle.spindle-override.count-enable",
+ analogEnable: "gmoccapy.spindle.spindle-override.analog-enable",
+ directValue: "gmoccapy.spindle.spindle-override.direct-value",
+ reset: "gmoccapy.spindle.reset-spindle-override",
+ nativeWidget: "spc_spindle",
+ scalePref: "scale_spindle_override=1",
+ webState: "spindle.override",
+ },
+ {
+ target: "jogVelocity",
+ counts: "gmoccapy.jog.jog-velocity.counts",
+ countEnable: "gmoccapy.jog.jog-velocity.count-enable",
+ analogEnable: "gmoccapy.jog.jog-velocity.analog-enable",
+ directValue: "gmoccapy.jog.jog-velocity.direct-value",
+ reset: null,
+ nativeWidget: "spc_lin_jog_vel",
+ scalePref: "scale_jog_vel=140.4",
+ webState: "gmoccapyGui.jogVelocity",
+ },
+ ],
+ messagePins: [
+ {
+ pin: "gmoccapy.delete-message",
+ nativeCallback: "_del_message_changed",
+ nativeCommand: "pin true deletes first alert message when gmoccapy.error is set, otherwise del_last()",
+ webState: "operatorMessage",
+ },
+ {
+ pin: "gmoccapy.warning-confirm",
+ nativeCallback: "dialogs.warning_dialog periodic confirm_pin poll",
+ nativeCommand: "pin true responds OK to active warning dialog",
+ webState: "gmoccapyGui.warningConfirm",
+ },
+ {
+ pin: "gmoccapy.error",
+ nativeCallback: "_show_error/_on_message_deleted",
+ nativeCommand: "HAL OUT bit tracks alert messages",
+ webState: "gmoccapyGui.error",
+ },
+ ],
+ behavior: {
+ optionalStopPinCrossesToBlockDelete: true,
+ blockdeletePinCrossesToOptionalStop: true,
+ jogAxisPinsPressRelease: true,
+ jogPinsUseTaskPolicyGate: true,
+ jogIncrementPinsRisingEdgeOnly: true,
+ jogIncrementZeroIsContinuous: true,
+ turtleJogLevelDriven: true,
+ countInputRequiresEnable: true,
+ directValueRequiresAnalogEnable: true,
+ resetPinsRisingEdgeOnly: true,
+ deleteMessageRisingEdgeOnly: true,
+ warningConfirmLevelPolled: true,
+ directValueRange: "0..1",
+ semanticBoundary: "native_gmoccapy_hal_pin_callbacks_to_web_store_actions",
+ },
+ },
+ toolChange: {
+ strategy: "iocontrol-loopback",
+ manualGmoccapyPinsConnected: false,
+ remapCodes: ["M6", "M61"],
+ postguiUnlinks: [
+ {
+ pin: "iocontrol.0.tool-change",
+ reason: "gmoccapy_postgui.hal removes the core_sim connection before applying the final loopback",
+ },
+ {
+ pin: "iocontrol.0.tool-changed",
+ reason: "gmoccapy_postgui.hal removes the core_sim connection before applying the final loopback",
+ },
+ {
+ pin: "iocontrol.0.tool-prep-number",
+ reason: "manual gmoccapy tool-change number net is not active in this simulation",
+ },
+ ],
+ commentedManualGuiNets: [
+ {
+ signal: "tool-change",
+ source: "iocontrol.0.tool-change",
+ target: "gmoccapy.toolchange-change",
+ },
+ {
+ signal: "tool-changed",
+ source: "gmoccapy.toolchange-changed",
+ target: "iocontrol.0.tool-changed",
+ },
+ {
+ signal: "tool-prep-number",
+ source: "iocontrol.0.tool-prep-number",
+ target: "gmoccapy.toolchange-number",
+ },
+ ],
+ activeLoop: {
+ signal: "tool-change-loop",
+ source: "iocontrol.0.tool-change",
+ target: "iocontrol.0.tool-changed",
+ stage: "POSTGUI_HALFILE",
+ },
+ semanticBoundary: "gmoccapy_xyzab_iocontrol_tool_change_loop_no_manual_gui_dialog",
+ },
semanticBoundary: "gmoccapy_hal_pin_postgui_reference_web_diagnostic_only",
};
export function createGmoccapyHalSummary(model = gmoccapyHalModel) {
const pinCount = model.nativePins.reduce((total, group) => total + group.pins.length, 0);
const postguiFile = model.halFiles.find((file) => file.stage === "POSTGUI_HALFILE");
+ const mappedHardwareButtonCount = [
+ ...(model.hardwareButtons?.verticalMain || []),
+ ...(model.hardwareButtons?.bottomMain || []),
+ ].filter((button) => button.webSelector).length;
return {
apiName: `${model.apiName}-summary`,
profileId: model.profileId,
@@ -275,6 +609,48 @@ export function createGmoccapyHalSummary(model = gmoccapyHalModel) {
postguiRequiresHalcompReady: Boolean(postguiFile?.requiresHalcompReady) &&
model.postguiNets.every((net) => net.requiresHalcompReady),
hasToolChangeSimulationLoop: model.postguiNets.some((net) => net.simulationLoop),
+ toolChangeStrategy: model.toolChange.strategy,
+ manualToolChangeGuiConnected: model.toolChange.manualGmoccapyPinsConnected,
+ postguiToolUnlinkCount: model.toolChange.postguiUnlinks.length,
+ commentedManualToolChangeNetCount: model.toolChange.commentedManualGuiNets.length,
+ hardwareButtonVerticalCount: model.hardwareButtons.verticalMain.length,
+ hardwareButtonBottomMainCount: model.hardwareButtons.bottomMain.length,
+ mappedHardwareButtonCount,
+ hardwareButtonEdge: model.hardwareButtons.behavior.edge,
+ hardwareButtonInsensitiveTarget: model.hardwareButtons.behavior.insensitiveTarget,
+ operatorInputPinCount: model.halPinActions.operatorPins.length,
+ overridePinTargetCount: model.halPinActions.overridePins.length,
+ jogAxisPinCount: (model.halPinActions.jogPins?.axes || []).length * 2,
+ jogIncrementPinCount: model.halPinActions.jogPins?.increments?.length || 0,
+ messageInputPinCount: model.halPinActions.messagePins.length,
+ optionalStopPinCrossesToBlockDelete: model.halPinActions.behavior.optionalStopPinCrossesToBlockDelete,
+ blockdeletePinCrossesToOptionalStop: model.halPinActions.behavior.blockdeletePinCrossesToOptionalStop,
+ jogAxisPinsPressRelease: model.halPinActions.behavior.jogAxisPinsPressRelease,
+ jogPinsUseTaskPolicyGate: model.halPinActions.behavior.jogPinsUseTaskPolicyGate,
+ jogIncrementPinsRisingEdgeOnly: model.halPinActions.behavior.jogIncrementPinsRisingEdgeOnly,
+ jogIncrementZeroIsContinuous: model.halPinActions.behavior.jogIncrementZeroIsContinuous,
+ turtleJogLevelDriven: model.halPinActions.behavior.turtleJogLevelDriven,
+ countInputRequiresEnable: model.halPinActions.behavior.countInputRequiresEnable,
+ directValueRequiresAnalogEnable: model.halPinActions.behavior.directValueRequiresAnalogEnable,
+ resetPinsRisingEdgeOnly: model.halPinActions.behavior.resetPinsRisingEdgeOnly,
+ deleteMessageRisingEdgeOnly: model.halPinActions.behavior.deleteMessageRisingEdgeOnly,
+ warningConfirmLevelPolled: model.halPinActions.behavior.warningConfirmLevelPolled,
semanticBoundary: model.semanticBoundary,
};
}
+
+export function resolveGmoccapyHardwareButton(input, model = gmoccapyHalModel) {
+ const pin = typeof input === "string" ? input : input?.pin;
+ const location = typeof input === "object" ? input?.location || input?.panel : null;
+ const index = typeof input === "object" && input?.index !== undefined ? Number(input.index) : null;
+ const allButtons = [
+ ...(model.hardwareButtons?.verticalMain || []).map((button) => ({ ...button, panel: "right", location: "v" })),
+ ...(model.hardwareButtons?.bottomMain || []).map((button) => ({ ...button, panel: "bottom", location: "h" })),
+ ];
+ if (pin) return allButtons.find((button) => button.pin === pin) || null;
+ if (!Number.isFinite(index)) return null;
+ const normalizedLocation = location === "right" || location === "v" || location === "vertical" ? "v"
+ : location === "bottom" || location === "h" || location === "horizontal" ? "h"
+ : null;
+ return allButtons.find((button) => button.location === normalizedLocation && button.index === index) || null;
+}
diff --git a/web-rtcp-5axis-sim-plan/app/src/state/store.js b/web-rtcp-5axis-sim-plan/app/src/state/store.js
index 6b62ded..d968e58 100644
--- a/web-rtcp-5axis-sim-plan/app/src/state/store.js
+++ b/web-rtcp-5axis-sim-plan/app/src/state/store.js
@@ -20,6 +20,7 @@ import {
buildTaskHalProgramMotionPlan,
buildTaskHalSessionFromMachineFiles,
} from "../runtime/linuxcnc-task-hal-runtime.js";
+import { gmoccapyHalModel, resolveGmoccapyHardwareButton } from "../runtime/gmoccapy-hal-model.js";
import {
createLinuxCncTaskPolicyStatus,
gateLinuxCncTaskAction,
@@ -230,6 +231,35 @@ const initialState = {
flood: true,
mist: false,
},
+ gmoccapyGui: {
+ ignoreLimits: false,
+ optionalBlocks: false,
+ optionalStop: false,
+ feedOverrideCounts: 0,
+ rapidOverrideCounts: 0,
+ spindleOverrideCounts: 0,
+ jogVelocityCounts: 0,
+ feedOverrideCountEnabled: false,
+ rapidOverrideCountEnabled: false,
+ spindleOverrideCountEnabled: false,
+ jogVelocityCountEnabled: false,
+ feedOverrideAnalogEnabled: false,
+ rapidOverrideAnalogEnabled: false,
+ spindleOverrideAnalogEnabled: false,
+ jogVelocityAnalogEnabled: false,
+ jogVelocity: 100,
+ jogIncrementIndex: 0,
+ jogIncrementLabel: "Continuous",
+ jogIncrementOutput: 0,
+ turtleJog: false,
+ activeJogPin: null,
+ warningConfirm: false,
+ error: false,
+ deletedMessageCount: 0,
+ lastHalPin: null,
+ lastHalPinValue: null,
+ lastHalPinEffect: null,
+ },
preview: {
pathPoints: 64,
selectedView: "iso",
@@ -964,6 +994,34 @@ export function createSimulationStore(seed = {}) {
case "REFRESH_KINEMATICS_FRAME":
return refreshAsyncKinematicsFrame({ operatorMessage: "LinuxCNC kinematics frame refreshed" });
break;
+ case "GMOCAPY_HARDWARE_BUTTON":
+ {
+ if (action.value === false || action.risingEdge === false) {
+ setState({ operatorMessage: "gmoccapy hardware button falling edge ignored" });
+ break;
+ }
+ const button = resolveGmoccapyHardwareButton(action);
+ if (!button) {
+ setState({ operatorMessage: `gmoccapy hardware button unmapped: ${action.pin || `${action.location}:${action.index}`}` });
+ break;
+ }
+ if (!button.webDispatch) {
+ setState({ operatorMessage: `gmoccapy hardware button diagnostic-only: ${button.pin}` });
+ break;
+ }
+ dispatch(button.webDispatch);
+ }
+ break;
+ case "GMOCAPY_HAL_PIN":
+ {
+ const result = applyGmoccapyHalPinPatch(state, action, gmoccapyHalModel);
+ if (!result.applied) {
+ setState({ operatorMessage: result.operatorMessage });
+ break;
+ }
+ setState(result.patch);
+ }
+ break;
case "TOGGLE_POWER":
{
const gate = gateLinuxCncTaskAction(state, action);
@@ -1585,6 +1643,12 @@ export function createSimulationStore(seed = {}) {
200,
),
},
+ gmoccapyGui: {
+ ...state.gmoccapyGui,
+ lastHalPin: null,
+ lastHalPinValue: null,
+ lastHalPinEffect: `${action.target} override manual step`,
+ },
operatorMessage: `${action.target} override adjusted`,
});
}
@@ -1601,10 +1665,55 @@ export function createSimulationStore(seed = {}) {
...state.spindle,
override: clampPercent(state.spindle.override + action.delta, 0, 150),
},
+ gmoccapyGui: {
+ ...state.gmoccapyGui,
+ lastHalPin: null,
+ lastHalPinValue: null,
+ lastHalPinEffect: "spindle override manual step",
+ },
operatorMessage: "spindle override adjusted",
});
}
break;
+ case "RESET_OVERRIDE":
+ setState(resetOverridePatchForTarget(state, action.target));
+ break;
+ case "SET_BLOCK_DELETE":
+ setState({
+ gmoccapyGui: {
+ ...state.gmoccapyGui,
+ optionalBlocks: Boolean(action.enabled),
+ lastHalPin: null,
+ lastHalPinValue: null,
+ lastHalPinEffect: "block delete set from Web optional blocks button",
+ },
+ operatorMessage: `block delete ${action.enabled ? "on" : "off"}`,
+ });
+ break;
+ case "SET_OPTIONAL_STOP":
+ setState({
+ gmoccapyGui: {
+ ...state.gmoccapyGui,
+ optionalStop: Boolean(action.enabled),
+ lastHalPin: null,
+ lastHalPinValue: null,
+ lastHalPinEffect: "optional stop set from Web control",
+ },
+ operatorMessage: `optional stop ${action.enabled ? "on" : "off"}`,
+ });
+ break;
+ case "SET_IGNORE_LIMITS":
+ setState({
+ gmoccapyGui: {
+ ...state.gmoccapyGui,
+ ignoreLimits: Boolean(action.enabled),
+ lastHalPin: null,
+ lastHalPinValue: null,
+ lastHalPinEffect: "ignore limits set from Web checkbox",
+ },
+ operatorMessage: action.enabled ? "limit override requested" : "limit override clear",
+ });
+ break;
case "TOGGLE_COOLANT":
{
const gate = gateLinuxCncTaskAction(state, action);
@@ -3269,6 +3378,535 @@ function clampPercent(value, min, max) {
return Math.min(Math.max(value, min), max);
}
+const GMOCAPY_OVERRIDE_TARGETS = {
+ feed: {
+ countPin: "gmoccapy.feed.feed-override.counts",
+ countEnablePin: "gmoccapy.feed.feed-override.count-enable",
+ analogEnablePin: "gmoccapy.feed.feed-override.analog-enable",
+ directValuePin: "gmoccapy.feed.feed-override.direct-value",
+ resetPin: "gmoccapy.feed.reset-feed-override",
+ countStateKey: "feedOverrideCounts",
+ countEnableStateKey: "feedOverrideCountEnabled",
+ analogEnableStateKey: "feedOverrideAnalogEnabled",
+ stateDomain: "feed",
+ stateKey: "feedOverride",
+ min: 0,
+ max: 200,
+ scale: 1,
+ },
+ rapid: {
+ countPin: "gmoccapy.rapid.rapid-override.counts",
+ countEnablePin: "gmoccapy.rapid.rapid-override.count-enable",
+ analogEnablePin: "gmoccapy.rapid.rapid-override.analog-enable",
+ directValuePin: "gmoccapy.rapid.rapid-override.direct-value",
+ resetPin: "gmoccapy.rapid.reset-rapid-override",
+ countStateKey: "rapidOverrideCounts",
+ countEnableStateKey: "rapidOverrideCountEnabled",
+ analogEnableStateKey: "rapidOverrideAnalogEnabled",
+ stateDomain: "feed",
+ stateKey: "rapidOverride",
+ min: 0,
+ max: 200,
+ scale: 1,
+ },
+ spindle: {
+ countPin: "gmoccapy.spindle.spindle-override.counts",
+ countEnablePin: "gmoccapy.spindle.spindle-override.count-enable",
+ analogEnablePin: "gmoccapy.spindle.spindle-override.analog-enable",
+ directValuePin: "gmoccapy.spindle.spindle-override.direct-value",
+ resetPin: "gmoccapy.spindle.reset-spindle-override",
+ countStateKey: "spindleOverrideCounts",
+ countEnableStateKey: "spindleOverrideCountEnabled",
+ analogEnableStateKey: "spindleOverrideAnalogEnabled",
+ stateDomain: "spindle",
+ stateKey: "override",
+ min: 0,
+ max: 150,
+ scale: 1,
+ },
+ jogVelocity: {
+ countPin: "gmoccapy.jog.jog-velocity.counts",
+ countEnablePin: "gmoccapy.jog.jog-velocity.count-enable",
+ analogEnablePin: "gmoccapy.jog.jog-velocity.analog-enable",
+ directValuePin: "gmoccapy.jog.jog-velocity.direct-value",
+ countStateKey: "jogVelocityCounts",
+ countEnableStateKey: "jogVelocityCountEnabled",
+ analogEnableStateKey: "jogVelocityAnalogEnabled",
+ stateDomain: "gmoccapyGui",
+ stateKey: "jogVelocity",
+ min: 0,
+ max: 14040,
+ scale: 140.4,
+ },
+};
+
+function applyGmoccapyHalPinPatch(state, action, model = gmoccapyHalModel) {
+ const pin = normalizeGmoccapyHalPin(action.pin);
+ const value = action.value;
+ if (!pin) {
+ return { applied: false, operatorMessage: "gmoccapy HAL pin blocked: missing pin" };
+ }
+ if (!isKnownGmoccapyHalPin(pin, model)) {
+ return { applied: false, operatorMessage: `gmoccapy HAL pin unmapped: ${pin}` };
+ }
+
+ if (pin === "gmoccapy.ignore-limits") {
+ const enabled = Boolean(value);
+ return {
+ applied: true,
+ patch: {
+ gmoccapyGui: halGuiPatch(state, pin, value, {
+ ignoreLimits: enabled,
+ lastHalPinEffect: enabled
+ ? "chk_ignore_limits set active; command.override_limits() requested"
+ : "chk_ignore_limits cleared",
+ }),
+ operatorMessage: enabled ? "gmoccapy HAL ignore-limits requested" : "gmoccapy HAL ignore-limits cleared",
+ },
+ };
+ }
+
+ if (pin === "gmoccapy.optional-stop") {
+ const enabled = Boolean(value);
+ return {
+ applied: true,
+ patch: {
+ gmoccapyGui: halGuiPatch(state, pin, value, {
+ optionalBlocks: enabled,
+ lastHalPinEffect: "optional-stop pin drives tbtn_optional_blocks -> set_block_delete",
+ }),
+ operatorMessage: `gmoccapy HAL optional-stop -> block delete ${enabled ? "on" : "off"}`,
+ },
+ };
+ }
+
+ if (pin === "gmoccapy.blockdelete") {
+ const enabled = Boolean(value);
+ return {
+ applied: true,
+ patch: {
+ gmoccapyGui: halGuiPatch(state, pin, value, {
+ optionalStop: enabled,
+ lastHalPinEffect: "blockdelete pin drives command.set_optional_stop",
+ }),
+ operatorMessage: `gmoccapy HAL blockdelete -> optional stop ${enabled ? "on" : "off"}`,
+ },
+ };
+ }
+
+ const jogAxisPin = resolveGmoccapyJogAxisPin(pin, model);
+ if (jogAxisPin) {
+ return applyGmoccapyJogAxisHalPatch(state, pin, value, jogAxisPin);
+ }
+
+ const jogIncrement = resolveGmoccapyJogIncrementPin(pin, model);
+ if (jogIncrement) {
+ return applyGmoccapyJogIncrementHalPatch(state, pin, value, jogIncrement);
+ }
+
+ if (pin === "gmoccapy.jog.turtle-jog") {
+ const enabled = Boolean(value);
+ return {
+ applied: true,
+ patch: {
+ gmoccapyGui: halGuiPatch(state, pin, value, {
+ turtleJog: enabled,
+ lastHalPinEffect: `turtle jog ${enabled ? "enabled" : "disabled"} by level-driven pin`,
+ }),
+ operatorMessage: `gmoccapy HAL turtle jog ${enabled ? "on" : "off"}`,
+ },
+ };
+ }
+
+ if (pin === "gmoccapy.delete-message") {
+ if (!value) {
+ return {
+ applied: true,
+ patch: {
+ gmoccapyGui: halGuiPatch(state, pin, value, {
+ lastHalPinEffect: "delete-message falling edge ignored",
+ }),
+ operatorMessage: "gmoccapy HAL delete-message falling edge ignored",
+ },
+ };
+ }
+ const hadError = state.gmoccapyGui?.error === true || /error|blocked|warning/i.test(state.operatorMessage || "");
+ return {
+ applied: true,
+ patch: {
+ gmoccapyGui: halGuiPatch(state, pin, value, {
+ error: false,
+ deletedMessageCount: Number(state.gmoccapyGui?.deletedMessageCount || 0) + 1,
+ lastHalPinEffect: hadError
+ ? "delete-message removed first alert and cleared gmoccapy.error"
+ : "delete-message deleted last notification",
+ }),
+ operatorMessage: hadError
+ ? "gmoccapy HAL delete-message cleared alert"
+ : "gmoccapy HAL delete-message deleted last message",
+ },
+ };
+ }
+
+ if (pin === "gmoccapy.warning-confirm") {
+ const enabled = Boolean(value);
+ return {
+ applied: true,
+ patch: {
+ gmoccapyGui: halGuiPatch(state, pin, value, {
+ warningConfirm: enabled,
+ lastHalPinEffect: enabled
+ ? "warning-confirm level would accept active warning dialog"
+ : "warning-confirm level cleared",
+ }),
+ operatorMessage: enabled
+ ? "gmoccapy HAL warning-confirm asserted"
+ : "gmoccapy HAL warning-confirm cleared",
+ },
+ };
+ }
+
+ const overrideTarget = findOverrideTargetForPin(pin);
+ if (overrideTarget) {
+ return applyGmoccapyOverrideHalPatch(state, pin, value, overrideTarget);
+ }
+
+ return {
+ applied: true,
+ patch: {
+ gmoccapyGui: halGuiPatch(state, pin, value, {
+ lastHalPinEffect: "diagnostic-only gmoccapy HAL pin",
+ }),
+ operatorMessage: `gmoccapy HAL diagnostic-only: ${pin}`,
+ },
+ };
+}
+
+function normalizeGmoccapyHalPin(pin) {
+ const value = String(pin || "").trim();
+ if (!value) return "";
+ return value.startsWith("gmoccapy.") ? value : `gmoccapy.${value}`;
+}
+
+function isKnownGmoccapyHalPin(pin, model = gmoccapyHalModel) {
+ return model.nativePins.some((group) => group.pins.includes(pin));
+}
+
+function halGuiPatch(state, pin, value, patch = {}) {
+ return {
+ ...state.gmoccapyGui,
+ ...patch,
+ lastHalPin: pin,
+ lastHalPinValue: value,
+ };
+}
+
+function findOverrideTargetForPin(pin) {
+ return Object.entries(GMOCAPY_OVERRIDE_TARGETS)
+ .find(([, target]) => [
+ target.countPin,
+ target.countEnablePin,
+ target.analogEnablePin,
+ target.directValuePin,
+ target.resetPin,
+ ].includes(pin)) || null;
+}
+
+function resolveGmoccapyJogAxisPin(pin, model = gmoccapyHalModel) {
+ for (const entry of model.halPinActions?.jogPins?.axes || []) {
+ if (pin === entry.plus) return { axis: entry.axis, direction: 1 };
+ if (pin === entry.minus) return { axis: entry.axis, direction: -1 };
+ }
+ return null;
+}
+
+function resolveGmoccapyJogIncrementPin(pin, model = gmoccapyHalModel) {
+ return (model.halPinActions?.jogPins?.increments || []).find((entry) => entry.pin === pin) || null;
+}
+
+function applyGmoccapyJogAxisHalPatch(state, pin, value, jogAxisPin) {
+ const axis = String(jogAxisPin.axis || "").toLowerCase();
+ const direction = Number(jogAxisPin.direction || 1);
+ const axisLabel = axis.toUpperCase();
+ const sign = direction > 0 ? "+" : "-";
+ if (!value) {
+ const isActivePin = state.gmoccapyGui?.activeJogPin === pin;
+ return {
+ applied: true,
+ patch: {
+ runState: isActivePin && state.runState === "jogging" ? "idle" : state.runState,
+ gmoccapyGui: halGuiPatch(state, pin, value, {
+ activeJogPin: isActivePin ? null : state.gmoccapyGui?.activeJogPin || null,
+ lastHalPinEffect: `jog ${axisLabel}${sign} released`,
+ }),
+ operatorMessage: `gmoccapy HAL jog ${axisLabel}${sign} released`,
+ },
+ };
+ }
+
+ const gate = gateLinuxCncTaskAction(state, { type: "JOG", axis, direction });
+ if (!gate.allowed) {
+ return {
+ applied: true,
+ patch: {
+ gmoccapyGui: halGuiPatch(state, pin, value, {
+ lastHalPinEffect: `jog ${axisLabel}${sign} ignored: ${gate.operatorMessage}`,
+ }),
+ operatorMessage: `gmoccapy HAL jog blocked: ${gate.operatorMessage}`,
+ },
+ };
+ }
+
+ const increment = Number(state.gmoccapyGui?.jogIncrementOutput ?? state.machine?.jogIncrement ?? 0);
+ const continuous = Math.abs(increment) <= 0;
+ const nextAxisPose = continuous
+ ? state.axisPose
+ : {
+ ...state.axisPose,
+ [axis]: Number(state.axisPose?.[axis] || 0) + direction * increment,
+ };
+ return {
+ applied: true,
+ patch: {
+ machine: {
+ ...state.machine,
+ mode: "manual",
+ jogAxis: axis,
+ jogIncrement: increment,
+ },
+ axisPose: nextAxisPose,
+ runState: "jogging",
+ gmoccapyGui: halGuiPatch(state, pin, value, {
+ activeJogPin: pin,
+ lastHalPinEffect: continuous
+ ? `jog ${axisLabel}${sign} continuous press`
+ : `jog ${axisLabel}${sign} incremental ${increment}`,
+ }),
+ operatorMessage: continuous
+ ? `gmoccapy HAL jog ${axisLabel}${sign} continuous`
+ : `gmoccapy HAL jog ${axisLabel}${sign} ${increment}`,
+ },
+ };
+}
+
+function applyGmoccapyJogIncrementHalPatch(state, pin, value, jogIncrement) {
+ if (!value) {
+ return {
+ applied: true,
+ patch: {
+ gmoccapyGui: halGuiPatch(state, pin, value, {
+ lastHalPinEffect: `jog increment ${jogIncrement.index} falling edge ignored`,
+ }),
+ operatorMessage: "gmoccapy HAL jog increment falling edge ignored",
+ },
+ };
+ }
+
+ const distance = Number(jogIncrement.distance || 0);
+ const label = String(jogIncrement.label || `increment ${jogIncrement.index}`);
+ return {
+ applied: true,
+ patch: {
+ machine: {
+ ...state.machine,
+ jogIncrement: distance,
+ },
+ runState: state.machine.mode === "manual" && state.runState === "jogging" ? "idle" : state.runState,
+ gmoccapyGui: halGuiPatch(state, pin, value, {
+ jogIncrementIndex: Number(jogIncrement.index),
+ jogIncrementLabel: label,
+ jogIncrementOutput: distance,
+ activeJogPin: null,
+ lastHalPinEffect: `jog increment selected ${label}; gmoccapy.jog.jog-increment=${distance}`,
+ }),
+ operatorMessage: `gmoccapy HAL jog increment ${label}`,
+ },
+ };
+}
+
+function applyGmoccapyOverrideHalPatch(state, pin, value, [targetName, target]) {
+ if (pin === target.countEnablePin || pin === target.analogEnablePin) {
+ const enabled = Boolean(value);
+ const key = pin === target.countEnablePin ? target.countEnableStateKey : target.analogEnableStateKey;
+ return {
+ applied: true,
+ patch: {
+ gmoccapyGui: halGuiPatch(state, pin, value, {
+ [key]: enabled,
+ lastHalPinEffect: `${targetName} ${pin === target.countEnablePin ? "count" : "analog"} input ${enabled ? "enabled" : "disabled"}`,
+ }),
+ operatorMessage: `gmoccapy HAL ${targetName} ${pin === target.countEnablePin ? "counts" : "analog"} ${enabled ? "enabled" : "disabled"}`,
+ },
+ };
+ }
+
+ if (pin === target.resetPin) {
+ if (!value) {
+ return {
+ applied: true,
+ patch: {
+ gmoccapyGui: halGuiPatch(state, pin, value, {
+ lastHalPinEffect: `${targetName} reset falling edge ignored`,
+ }),
+ operatorMessage: `gmoccapy HAL ${targetName} reset falling edge ignored`,
+ },
+ };
+ }
+ return {
+ applied: true,
+ patch: resetOverridePatchForTarget(state, targetName, { pin, value }),
+ };
+ }
+
+ if (pin === target.directValuePin) {
+ if (!state.gmoccapyGui?.[target.analogEnableStateKey]) {
+ return {
+ applied: true,
+ patch: {
+ gmoccapyGui: halGuiPatch(state, pin, value, {
+ lastHalPinEffect: `${targetName} direct-value ignored until analog-enable is true`,
+ }),
+ operatorMessage: `gmoccapy HAL ${targetName} direct-value ignored: analog disabled`,
+ },
+ };
+ }
+ const normalized = clampNumber(Number(value), 0, 1);
+ const nextValue = target.min + (target.max - target.min) * normalized;
+ return overrideValuePatch(state, targetName, target, nextValue, {
+ pin,
+ value,
+ effect: `${targetName} direct-value ${normalized}`,
+ operatorMessage: `gmoccapy HAL ${targetName} direct-value ${Math.round(nextValue)}`,
+ });
+ }
+
+ if (pin === target.countPin) {
+ const counts = Number(value);
+ if (!Number.isFinite(counts)) {
+ return { applied: false, operatorMessage: `gmoccapy HAL ${targetName} counts invalid: ${value}` };
+ }
+ if (!state.gmoccapyGui?.[target.countEnableStateKey]) {
+ return {
+ applied: true,
+ patch: {
+ gmoccapyGui: halGuiPatch(state, pin, value, {
+ [target.countStateKey]: counts,
+ lastHalPinEffect: `${targetName} counts synchronized while count-enable is false`,
+ }),
+ operatorMessage: `gmoccapy HAL ${targetName} counts synchronized`,
+ },
+ };
+ }
+ const previousCounts = Number(state.gmoccapyGui?.[target.countStateKey] || 0);
+ const delta = (counts - previousCounts) * target.scale;
+ const currentValue = currentOverrideValue(state, target);
+ return overrideValuePatch(state, targetName, target, currentValue + delta, {
+ pin,
+ value,
+ countPatch: { [target.countStateKey]: counts },
+ effect: `${targetName} counts delta ${delta}`,
+ operatorMessage: `gmoccapy HAL ${targetName} counts adjusted`,
+ });
+ }
+
+ return { applied: false, operatorMessage: `gmoccapy HAL override pin unmapped: ${pin}` };
+}
+
+function overrideValuePatch(state, targetName, target, rawValue, {
+ pin,
+ value,
+ countPatch = {},
+ effect,
+ operatorMessage,
+} = {}) {
+ const nextValue = clampPercent(rawValue, target.min, target.max);
+ const gmoccapyGui = halGuiPatch(state, pin, value, {
+ ...countPatch,
+ lastHalPinEffect: effect,
+ });
+ if (target.stateDomain === "feed") {
+ return {
+ applied: true,
+ patch: {
+ feed: {
+ ...state.feed,
+ [target.stateKey]: Math.round(nextValue),
+ },
+ gmoccapyGui,
+ operatorMessage,
+ },
+ };
+ }
+ if (target.stateDomain === "spindle") {
+ return {
+ applied: true,
+ patch: {
+ spindle: {
+ ...state.spindle,
+ [target.stateKey]: Math.round(nextValue),
+ },
+ gmoccapyGui,
+ operatorMessage,
+ },
+ };
+ }
+ return {
+ applied: true,
+ patch: {
+ gmoccapyGui: {
+ ...gmoccapyGui,
+ [target.stateKey]: Number(nextValue.toFixed(3)),
+ },
+ operatorMessage,
+ },
+ };
+}
+
+function currentOverrideValue(state, target) {
+ if (target.stateDomain === "feed") return Number(state.feed?.[target.stateKey] || 0);
+ if (target.stateDomain === "spindle") return Number(state.spindle?.[target.stateKey] || 0);
+ return Number(state.gmoccapyGui?.[target.stateKey] || 0);
+}
+
+function resetOverridePatchForTarget(state, targetName, hal = {}) {
+ const target = GMOCAPY_OVERRIDE_TARGETS[targetName];
+ if (!target || targetName === "jogVelocity") {
+ return {
+ gmoccapyGui: halGuiPatch(state, hal.pin || null, hal.value ?? null, {
+ lastHalPinEffect: `reset unsupported for ${targetName}`,
+ }),
+ operatorMessage: `override reset unsupported: ${targetName}`,
+ };
+ }
+ const effect = `${targetName} override reset to 100`;
+ const gmoccapyGui = halGuiPatch(state, hal.pin || null, hal.value ?? null, {
+ lastHalPinEffect: hal.pin ? `HAL ${effect}` : effect,
+ });
+ if (target.stateDomain === "spindle") {
+ return {
+ spindle: {
+ ...state.spindle,
+ [target.stateKey]: 100,
+ },
+ gmoccapyGui,
+ operatorMessage: hal.pin ? `gmoccapy HAL ${targetName} reset to 100` : `${targetName} override reset`,
+ };
+ }
+ return {
+ feed: {
+ ...state.feed,
+ [target.stateKey]: 100,
+ },
+ gmoccapyGui,
+ operatorMessage: hal.pin ? `gmoccapy HAL ${targetName} reset to 100` : `${targetName} override reset`,
+ };
+}
+
+function clampNumber(value, min, max) {
+ if (!Number.isFinite(value)) return min;
+ return Math.min(Math.max(value, min), max);
+}
+
function buildDroFromFrame(frame, runtimeFeedback = null) {
const dtg = runtimeFeedback?.dtg || null;
return {
diff --git a/web-rtcp-5axis-sim-plan/app/src/styles/gmoccapy.css b/web-rtcp-5axis-sim-plan/app/src/styles/gmoccapy.css
index 902ac1c..a72e239 100644
--- a/web-rtcp-5axis-sim-plan/app/src/styles/gmoccapy.css
+++ b/web-rtcp-5axis-sim-plan/app/src/styles/gmoccapy.css
@@ -735,7 +735,7 @@ button[data-icon-name] span {
grid-area: override;
display: grid;
grid-template-columns: 1fr;
- grid-template-rows: repeat(3, minmax(0, 1fr));
+ grid-template-rows: repeat(4, minmax(0, 1fr));
gap: 6px;
padding: 6px;
border-top: 2px solid var(--border);
@@ -806,7 +806,7 @@ button[data-icon-name] span {
.stepper {
display: grid;
- grid-template-columns: 30px minmax(54px, 1fr) 30px;
+ grid-template-columns: 30px minmax(48px, 1fr) 30px 36px;
gap: 2px;
align-items: center;
margin-top: 1px;
@@ -827,6 +827,42 @@ button[data-icon-name] span {
min-width: 0;
min-height: 26px;
padding: 2px 4px;
+ font-size: 12px;
+}
+
+.toggle-row {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 3px;
+ min-width: 0;
+}
+
+.toggle-row button {
+ min-width: 0;
+ min-height: 24px;
+ padding: 2px 3px;
+ overflow: hidden;
+ font-size: 11px;
+ font-weight: 700;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.toggle-row button[data-active="true"] {
+ background: #ffd25f;
+ border-color: #a76e00;
+}
+
+.status-chip {
+ min-width: 0;
+ overflow: hidden;
+ padding: 2px 4px;
+ background: #ebe6dc;
+ border: 1px solid #c6bfb3;
+ color: #4f4a42;
+ font: 10px/1.2 "Courier New", monospace;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
.spindle-coolant-panel {
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 45444b4..e4879a4 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
@@ -353,13 +353,13 @@ function renderSidebar(element, state, dispatch) {
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_estop", action: "estop", label: "E-STOP", title: "Estop the machine / F1", className: "sidebar-button estop", active: state.machine.estopActive, attrs: nativeButtonAttrs("v", 0) })}
+ ${iconButtonHtml({ buttonId: "tbtn_on", action: "power", label: "POWER", title: "Turn the machine on/off / F2", className: "sidebar-button power", active: state.machine.powerOn, attrs: nativeButtonAttrs("v", 1) })}
${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: 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 })}
+ ${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, attrs: nativeButtonAttrs("v", 4) })}
+ ${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, attrs: nativeButtonAttrs("v", 2) })}
+ ${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, attrs: nativeButtonAttrs("v", 5) })}
+ ${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, attrs: nativeButtonAttrs("v", 3) })}
@@ -449,8 +449,14 @@ function renderInfoTabs(element, state) {
HAL model:${gmoccapyHalModel.semanticBoundary}
HAL files:${halSummary.halFileCount} files / POSTGUI ${halSummary.postguiRequiresHalcompReady ? "after halcomp.ready" : "pending"}
HAL pins:${halSummary.nativePinGroupCount} groups / ${halSummary.nativePinCount} pins
+ Hard buttons:${formatGmoccapyHardButtons(halSummary)}
+ HAL jog:${formatGmoccapyJogInputs(halSummary, state)}
+ HAL inputs:${formatGmoccapyHalInputs(halSummary, state)}
+ HAL overrides:${formatGmoccapyOverrideInputs(halSummary, state)}
+ HAL messages:${formatGmoccapyMessageInputs(halSummary, state)}
postgui nets:${gmoccapyHalModel.postguiNets.map((net) => net.signal).join(" / ")}
Tool loop:${halSummary.hasToolChangeSimulationLoop ? "simulated tool-change loop" : "pending"}
+ Tool change:${formatGmoccapyToolChange(halSummary)}
Operator:${escapeHtml(state.operatorMessage)}
`;
@@ -597,6 +603,15 @@ function renderOverride(element, state, dispatch) {
${overrideControl("Rapid Override", "rapid", state.feed.rapidOverride)}
${overrideControl("Feed Rate", "feed", state.feed.feedOverride, `F ${state.feed.feedRate}`)}
+
+ HAL Inputs
+
+
+
+
+
+ ${escapeHtml(formatGmoccapyHalLastInput(state))}
+
`;
for (const target of ["rapid", "feed"]) {
@@ -606,7 +621,22 @@ function renderOverride(element, state, dispatch) {
element.querySelector(`[data-action="${target}-override-up"]`).addEventListener("click", () => {
dispatch({ type: "ADJUST_OVERRIDE", target, delta: 10 });
});
+ const reset = element.querySelector(`[data-action="${target}-override-reset"]`);
+ if (reset) {
+ reset.addEventListener("click", () => {
+ dispatch({ type: "RESET_OVERRIDE", target });
+ });
+ }
}
+ element.querySelector('[data-action="ignore-limits"]').addEventListener("click", () => {
+ dispatch({ type: "SET_IGNORE_LIMITS", enabled: !state.gmoccapyGui.ignoreLimits });
+ });
+ element.querySelector('[data-action="block-delete"]').addEventListener("click", () => {
+ dispatch({ type: "SET_BLOCK_DELETE", enabled: !state.gmoccapyGui.optionalBlocks });
+ });
+ element.querySelector('[data-action="optional-stop"]').addEventListener("click", () => {
+ dispatch({ type: "SET_OPTIONAL_STOP", enabled: !state.gmoccapyGui.optionalStop });
+ });
}
function overrideControl(label, target, value, prefix = "") {
@@ -618,6 +648,7 @@ function overrideControl(label, target, value, prefix = "") {
${value} %
+
`;
@@ -647,6 +678,7 @@ function renderSpindleCoolant(element, state, dispatch) {
${state.spindle.override} %
+
@@ -664,6 +696,9 @@ function renderSpindleCoolant(element, state, dispatch) {
element.querySelector('[data-action="spindle-override-up"]').addEventListener("click", () => {
dispatch({ type: "ADJUST_SPINDLE_OVERRIDE", delta: 10 });
});
+ element.querySelector('[data-action="spindle-override-reset"]').addEventListener("click", () => {
+ dispatch({ type: "RESET_OVERRIDE", target: "spindle" });
+ });
element.querySelector('[data-action="spindle-forward"]').addEventListener("click", () => {
dispatch({ type: "SET_SPINDLE_DIRECTION", direction: "forward" });
});
@@ -716,7 +751,7 @@ function renderBottomControls(element, state, dispatch) {
title: gate.allowed ? label : gate.operatorMessage || "blocked",
active: bottomControlActive(state, action),
disabled: Boolean(disabled),
- attrs: blockedAttrs,
+ attrs: `${blockedAttrs}${bottomNativeButtonAttrs(action)}`,
});
})
.join("")}
@@ -785,6 +820,17 @@ function bottomIconButtonId(action) {
}[action] || "btn_load";
}
+function bottomNativeButtonAttrs(action) {
+ if (action === "HOME") return nativeButtonAttrs("h", 0);
+ return "";
+}
+
+function nativeButtonAttrs(location, index) {
+ const prefix = location === "v" ? "v-button" : "h-button";
+ const panel = location === "v" ? "right" : "bottom";
+ return ` data-gmoccapy-hal-pin="gmoccapy.${prefix}.button-${index}" data-gmoccapy-native-panel="${panel}" data-gmoccapy-native-button-index="${index}"`;
+}
+
function bottomControlActive(state, action) {
if (action === "PAUSE") return state.runState === "paused";
if (action === "RUN" || action === "RESUME") return state.runState === "running";
@@ -855,6 +901,79 @@ function formatGmoccapyActionMapping(actionId) {
return `${mapping.nativeCommand} / ${mapping.gate}`;
}
+function formatGmoccapyToolChange(halSummary) {
+ const manualPins = halSummary.manualToolChangeGuiConnected ? "manual GUI pins connected" : "manual GUI pins disconnected";
+ return [
+ halSummary.toolChangeStrategy,
+ manualPins,
+ `${halSummary.postguiToolUnlinkCount} postgui unlinkp`,
+ `${halSummary.commentedManualToolChangeNetCount} commented manual nets`,
+ ].join(" / ");
+}
+
+function formatGmoccapyHalInputs(halSummary, state) {
+ return [
+ `${halSummary.operatorInputPinCount} operator pins`,
+ halSummary.optionalStopPinCrossesToBlockDelete ? "optional-stop -> block delete" : "optional-stop direct",
+ halSummary.blockdeletePinCrossesToOptionalStop ? "blockdelete -> optional stop" : "blockdelete direct",
+ `ignore-limits ${state.gmoccapyGui.ignoreLimits ? "on" : "off"}`,
+ `block ${state.gmoccapyGui.optionalBlocks ? "on" : "off"}`,
+ `M1 ${state.gmoccapyGui.optionalStop ? "on" : "off"}`,
+ ].join(" / ");
+}
+
+function formatGmoccapyJogInputs(halSummary, state) {
+ return [
+ `${halSummary.jogAxisPinCount} axis jog pins`,
+ `${halSummary.jogIncrementPinCount} jog-inc pins`,
+ halSummary.jogAxisPinsPressRelease ? "press/release level" : "edge jog",
+ halSummary.jogIncrementPinsRisingEdgeOnly ? "jog-inc rising-edge-only" : "jog-inc level",
+ halSummary.jogIncrementZeroIsContinuous ? "jog-inc-0 continuous" : "jog-inc-0 incremental",
+ halSummary.turtleJogLevelDriven ? "turtle level-driven" : "turtle pending",
+ `active ${state.gmoccapyGui.activeJogPin || "none"}`,
+ `increment ${state.gmoccapyGui.jogIncrementLabel}=${state.gmoccapyGui.jogIncrementOutput}`,
+ ].join(" / ");
+}
+
+function formatGmoccapyOverrideInputs(halSummary, state) {
+ return [
+ `${halSummary.overridePinTargetCount} override targets`,
+ halSummary.countInputRequiresEnable ? "counts require enable" : "counts direct",
+ halSummary.directValueRequiresAnalogEnable ? "direct-value requires analog-enable" : "direct-value direct",
+ halSummary.resetPinsRisingEdgeOnly ? "reset rising-edge-only" : "reset level",
+ `last ${formatGmoccapyHalLastInput(state)}`,
+ ].join(" / ");
+}
+
+function formatGmoccapyMessageInputs(halSummary, state) {
+ return [
+ `${halSummary.messageInputPinCount} message pins`,
+ halSummary.deleteMessageRisingEdgeOnly ? "delete-message rising-edge-only" : "delete-message level",
+ halSummary.warningConfirmLevelPolled ? "warning-confirm level-polled" : "warning-confirm edge",
+ `error ${state.gmoccapyGui.error ? "on" : "off"}`,
+ `deleted ${state.gmoccapyGui.deletedMessageCount}`,
+ `confirm ${state.gmoccapyGui.warningConfirm ? "on" : "off"}`,
+ ].join(" / ");
+}
+
+function formatGmoccapyHalLastInput(state) {
+ if (!state.gmoccapyGui?.lastHalPin && !state.gmoccapyGui?.lastHalPinEffect) return "none";
+ return [
+ state.gmoccapyGui.lastHalPin || "web-control",
+ state.gmoccapyGui.lastHalPinEffect || "-",
+ ].join(" ");
+}
+
+function formatGmoccapyHardButtons(halSummary) {
+ return [
+ `${halSummary.hardwareButtonVerticalCount} right v-button pins`,
+ `${halSummary.hardwareButtonBottomMainCount} bottom h-button main refs`,
+ `${halSummary.mappedHardwareButtonCount} mapped Web buttons`,
+ halSummary.hardwareButtonEdge,
+ `insensitive ${halSummary.hardwareButtonInsensitiveTarget}`,
+ ].join(" / ");
+}
+
function formatNumber(value, digits = 3) {
return Number(value).toFixed(digits);
}
diff --git a/web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md b/web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md
index 13ab4f9..5769564 100644
--- a/web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md
+++ b/web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md
@@ -5103,3 +5103,582 @@ Final push and verification:
Outcome:
- Backup-directory file `备忘/备忘3.txt` is now committed and pushed to cloud repository.
- This final verification log entry was created after the push and requires one final log-only sync commit.
+
+---
+
+## 2026-06-26 18:33 EDT - working5 gmoccapy XYZAB tool-change HAL semantics
+
+User request:
+- Start work according to `/home/meswork/cnc_wams/work/working5`.
+
+Initial context and status:
+- Read `work/working5/README.md`, `01-项目功能内容.md`, `02-项目程序开发详细步骤.md`, `03-推进台账.md`, `04-任务矩阵.md`, `05-验收证据.md`, and `06-决策记录.md`.
+- Read `work/working3/gmoccapy_XYZAB_execution_analysis.md`.
+- Checked repository state:
+ - existing unrelated deletion remained present: `D linuxcnc.zip`.
+ - this deletion was not reverted, staged, or otherwise modified.
+
+Baseline/source inspection:
+- Inspected Web project files:
+ - `app/src/state/linuxcnc-task-policy.js`
+ - `app/src/state/store.js`
+ - `app/src/runtime/gmoccapy-hal-model.js`
+ - `app/src/runtime/gmoccapy-communication-model.js`
+ - `app/src/profiles/gmoccapy-xyzab.js`
+ - `app/src/ui/gmoccapy-shell.js`
+ - related Node and browser smoke tests.
+- Inspected native LinuxCNC reference files:
+ - `linuxcnc/configs/sim/gmoccapy/gmoccapy_XYZAB.ini`
+ - `linuxcnc/configs/sim/gmoccapy/core_sim_XYZAB.hal`
+ - `linuxcnc/configs/sim/gmoccapy/gmoccapy_postgui.hal`
+- Key finding:
+ - `gmoccapy_postgui.hal` executes `unlinkp iocontrol.0.tool-change`, `unlinkp iocontrol.0.tool-changed`, and `unlinkp iocontrol.0.tool-prep-number`.
+ - the manual gmoccapy tool-change GUI nets for `gmoccapy.toolchange-change`, `gmoccapy.toolchange-changed`, and `gmoccapy.toolchange-number` are commented out.
+ - the active simulated tool-change completion path is `net tool-change-loop iocontrol.0.tool-change => iocontrol.0.tool-changed`.
+
+Implementation changes:
+- Updated `web-rtcp-5axis-sim-plan/app/src/runtime/gmoccapy-hal-model.js`:
+ - added structured `toolChange` metadata.
+ - recorded strategy `iocontrol-loopback`.
+ - recorded `manualGmoccapyPinsConnected: false`.
+ - recorded M6/M61 remap references.
+ - recorded the three POSTGUI `unlinkp` pins.
+ - recorded commented manual GUI nets and the active postgui loopback.
+ - exposed tool-change summary fields through `createGmoccapyHalSummary()`.
+- Updated `web-rtcp-5axis-sim-plan/app/src/profiles/gmoccapy-xyzab.js`:
+ - added matching `hal.halcmd.toolChange` metadata to the profile.
+- Updated `web-rtcp-5axis-sim-plan/app/src/ui/gmoccapy-shell.js`:
+ - added a `Tool change` diagnostic row under gmoccapy HAL diagnostics.
+ - displays strategy, manual GUI pin connection state, POSTGUI unlink count, and commented manual net count.
+- Updated tests:
+ - `tests/node/verify_gmoccapy_hal_model.mjs`
+ - `tests/node/verify_gmoccapy_xyzab_profile.mjs`
+ - `tests/node/verify_profile_boundary.mjs`
+ - `tests/browser/gmoccapy_shell_smoke.html`
+- Updated working5 records:
+ - `work/working5/03-推进台账.md`
+ - `work/working5/04-任务矩阵.md`
+ - `work/working5/05-验收证据.md`
+ - `work/working5/06-决策记录.md`
+
+Validation commands and results:
+- Targeted Node tests:
+ - command:
+ - `cd /home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app`
+ - `node ../tests/node/verify_gmoccapy_hal_model.mjs && node ../tests/node/verify_gmoccapy_xyzab_profile.mjs && node ../tests/node/verify_profile_boundary.mjs`
+ - result:
+ - `gmoccapy_hal_model_smoke=ok`
+ - `gmoccapy_xyzab_profile_smoke=ok`
+ - `profile_boundary_smoke=ok`
+- Full Node smoke:
+ - command:
+ - `npm run smoke:node`
+ - result:
+ - passed.
+ - included `linuxcnc_task_hal_runtime_smoke=ok`, `profile_boundary_smoke=ok`, `gmoccapy_xyzab_profile_smoke=ok`, `gmoccapy_hal_model_smoke=ok`, `gmoccapy_xyzab_gates_smoke=ok`, and `linear_unit_conversion_smoke=ok`.
+- Build and browser smoke:
+ - command:
+ - `npm run build && npm run smoke`
+ - result:
+ - `gmoccapy_static_build=ok`
+ - `gmoccapy_shell_smoke=ok`
+ - `gmoccapy_dist_smoke=ok`
+- Diff check:
+ - command:
+ - `git diff --check -- web-rtcp-5axis-sim-plan work/working5`
+ - result:
+ - passed with no output.
+
+Working tree after implementation:
+- Modified intended files:
+ - `web-rtcp-5axis-sim-plan/app/src/profiles/gmoccapy-xyzab.js`
+ - `web-rtcp-5axis-sim-plan/app/src/runtime/gmoccapy-hal-model.js`
+ - `web-rtcp-5axis-sim-plan/app/src/ui/gmoccapy-shell.js`
+ - `web-rtcp-5axis-sim-plan/tests/browser/gmoccapy_shell_smoke.html`
+ - `web-rtcp-5axis-sim-plan/tests/node/verify_gmoccapy_hal_model.mjs`
+ - `web-rtcp-5axis-sim-plan/tests/node/verify_gmoccapy_xyzab_profile.mjs`
+ - `web-rtcp-5axis-sim-plan/tests/node/verify_profile_boundary.mjs`
+ - `work/working5/03-推进台账.md`
+ - `work/working5/04-任务矩阵.md`
+ - `work/working5/05-验收证据.md`
+ - `work/working5/06-决策记录.md`
+ - `web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md`
+- Existing unrelated item left untouched:
+ - `D linuxcnc.zip`
+
+Decision recorded:
+- `DR-W5-003`: XYZAB tool change is modeled as POSTGUI `iocontrol` loopback, not as a manual gmoccapy tool-change dialog, because the native `gmoccapy_postgui.hal` disconnects the manual GUI pins and leaves those nets commented.
+
+Outcome:
+- W5-160 added and marked complete.
+- gmoccapy XYZAB profile, HAL model, UI diagnostics, Node tests, browser tests, and working5 records now explicitly capture the LinuxCNC XYZAB tool-change behavior.
+- No commit or push was requested or performed.
+
+## 2026-06-26 20:15 EDT - ToDesk Ubuntu crash dialog handling
+
+User request:
+- The desktop showed an Ubuntu crash report dialog saying "The application ToDesk has closed unexpectedly"; resolve it.
+
+Process log:
+- Checked running ToDesk processes with `ps -ef | rg -i 'todesk|ToDesk'`.
+ - Found `/opt/todesk/bin/ToDesk_Service` running as root.
+ - The ToDesk foreground GUI process was not running initially.
+- Checked systemd services.
+ - `todeskd.service` was loaded, enabled, and active/running.
+ - No `todesk.service` user or system unit existed.
+- Checked installed package and files.
+ - `todesk 4.8.6.2` was installed.
+ - Binaries existed under `/opt/todesk/bin`.
+- Checked crash files under `/var/crash`.
+ - Found `/var/crash/_opt_todesk_bin_ToDesk.1000.crash`.
+ - Other unrelated old crash reports for WeChat and LibreOffice were present and left untouched.
+- Read the ToDesk crash report.
+ - Crash time: Fri Jun 26 20:06:38 2026.
+ - Executable: `/opt/todesk/bin/ToDesk`.
+ - Session tag included `wayland-session`.
+ - Signal: `SIGSEGV`.
+ - Crash title: `ToDesk crashed with SIGSEGV in html::element::determine_style()`.
+ - Stack top showed ToDesk internal UI/rendering functions:
+ - `html::element::determine_style(...)`
+ - `html::element::get_style() const`
+ - `html::element::attr_role() const`
+ - `html::behavior::window_frame_ctl::on(...)`
+- Checked desktop/session state.
+ - Active session type was `wayland`.
+ - `DISPLAY=:0`, `WAYLAND_DISPLAY=wayland-0`, and user DBus was available.
+- Checked ToDesk autostart entry.
+ - `/home/meswork/.config/autostart/todesk.desktop` launches ToDesk with:
+ - `LIBVA_DRIVER_NAME=iHD`
+ - `LIBVA_DRIVERS_PATH=/opt/todesk/bin`
+ - `GDK_BACKEND=x11`
+- Confirmed Ubuntu crash dialog process.
+ - `apport-gtk` and `update-notifier-crash` were running.
+- Attempted a combined cleanup/start command.
+ - The `pkill -f '/usr/share/apport/apport-gtk'` pattern also matched the active shell command, so that combined command exited early with code 143 before moving the crash file or relaunching ToDesk.
+ - Rechecked state afterward to avoid assuming success.
+- Closed the Ubuntu crash dialog process.
+ - After the earlier `pkill`, `apport-gtk` was no longer running.
+- Moved the ToDesk crash report out of `/var/crash`.
+ - Created `/home/meswork/.local/share/todesk/crash-backup`.
+ - Moved `_opt_todesk_bin_ToDesk.1000.crash` there with a timestamped filename.
+ - This prevents the same Ubuntu crash report dialog from reappearing for that crash.
+- Relaunched ToDesk foreground GUI without restarting the daemon:
+ - `setsid env LIBVA_DRIVER_NAME=iHD LIBVA_DRIVERS_PATH=/opt/todesk/bin GDK_BACKEND=x11 DISPLAY=:0 XDG_RUNTIME_DIR=/run/user/1000 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus /opt/todesk/bin/ToDesk > /tmp/todesk-launch.log 2>&1 < /dev/null &`
+- Observed relaunch.
+ - `/opt/todesk/bin/ToDesk` appeared as user `meswork`.
+ - `/opt/todesk/bin/ToDesk_Service` remained running as root.
+ - `/var/crash` no longer contained a ToDesk crash file.
+ - Launch log showed ToDesk extracting icon/image resources and then only `mkdir: cannot create directory '/tmp/fuse': File exists`; no fatal error appeared.
+- Waited and rechecked stability.
+ - After about 20 seconds, both `ToDesk` and `ToDesk_Service` were still running.
+ - `todeskd.service` remained `active/running` with `NRestarts=0`.
+ - No new ToDesk crash file appeared in `/var/crash`.
+ - `apport-gtk` and `update-notifier-crash` were no longer running.
+
+Outcome:
+- The Ubuntu crash report dialog was closed.
+- The stale ToDesk crash report was moved out of `/var/crash`.
+- ToDesk foreground GUI was relaunched successfully.
+- ToDesk daemon was not restarted, reducing risk of disrupting the active remote session.
+- Final observed state:
+ - `root ... /opt/todesk/bin/ToDesk_Service`
+ - `meswork ... /opt/todesk/bin/ToDesk`
+ - `/var/crash` contained no ToDesk crash report.
+
+## 2026-06-26 20:17 EDT - 将 GPT/Codex 过程日志语言规则写入 AGENTS.md
+
+用户请求:
+- `web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md` 这个文件使用中文记录。
+- 将该要求写入 `AGENTS.md`。
+
+执行过程:
+- 读取 `/home/meswork/cnc_wams/AGENTS.md`,确认当前已有工作区范围和日志路径规则:
+ - 每次 GPT/Codex 执行完成后,将完整执行过程日志追加到 `/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md`。
+- 检查目标日志文件和 `AGENTS.md` 文件存在:
+ - `/home/meswork/cnc_wams/AGENTS.md`
+ - `/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md`
+- 修改 `/home/meswork/cnc_wams/AGENTS.md`,在日志规则下追加:
+ - `The log content written to this file must be recorded in Chinese.`
+- 按新的日志语言要求,将本次执行过程用中文追加到 `gpdlog.md`。
+
+结果:
+- 已将 `gpdlog.md` 使用中文记录的要求写入 `AGENTS.md`。
+- 从本次记录开始,日志内容按中文记录。
+
+---
+
+## 2026-06-26 R4 - working5 continuation: gmoccapy HAL hard-button pin mapping
+
+User request:
+- Continue from `/home/meswork/cnc_wams/work/working5`.
+
+Context gathered:
+- Read `work/working5/README.md`, `01-项目功能内容.md`, `02-项目程序开发详细步骤.md`, `03-推进台账.md`, `04-任务矩阵.md`, `05-验收证据.md`, and `06-决策记录.md`.
+- Confirmed previous completed work through R3: mode gate fixes and XYZAB POSTGUI tool-change loopback semantics.
+- Inspected LinuxCNC native references:
+ - `linuxcnc/src/emc/usr_intf/gmoccapy/gmoccapy.py` `_make_hal_pins()`, `_button_pin_changed()`, `_get_child_button()`.
+ - `linuxcnc/share/gmoccapy/gmoccapy.glade` `vbtb_main` order.
+ - `linuxcnc/configs/sim/gmoccapy/h_buttonlist.hal`, `v_buttonlist.hal`, and related glade button list examples.
+- Inspected Web implementation and tests:
+ - `app/src/profiles/gmoccapy-xyzab.js`
+ - `app/src/runtime/gmoccapy-hal-model.js`
+ - `app/src/runtime/gmoccapy-communication-model.js`
+ - `app/src/state/store.js`
+ - `app/src/ui/gmoccapy-shell.js`
+ - `tests/node/verify_gmoccapy_hal_model.mjs`
+ - `tests/node/verify_gmoccapy_communication_model.mjs`
+ - `tests/node/verify_gmoccapy_xyzab_profile.mjs`
+ - `tests/node/verify_gmoccapy_xyzab_gates.mjs`
+ - `tests/browser/gmoccapy_shell_smoke.html`
+
+Findings:
+- Native gmoccapy hard-button pins are created as `gmoccapy.h-button.button-0..9` and `gmoccapy.v-button.button-0..6`.
+- `_button_pin_changed()` ignores falling edges and only handles `pin.get() == True`.
+- `_get_child_button()` maps button numbers to current visible non-label GTK children, and ignores insensitive targets.
+- Native `vbtb_main` order is `tbtn_estop`, `tbtn_on`, `rbt_manual`, `rbt_mdi`, `rbt_auto`, `tbtn_user_tabs`, `tbtn_setup`.
+- Existing Web profile panel schema incorrectly listed Estop/Power as h-button pins and shifted Manual/MDI/Auto v-button indexes.
+
+Implementation changes:
+- Updated `web-rtcp-5axis-sim-plan/app/src/profiles/gmoccapy-xyzab.js`:
+ - corrected task/mode panel schema pin order to `gmoccapy.v-button.button-0..4` for Estop, Power, Manual, MDI, Auto.
+- Updated `web-rtcp-5axis-sim-plan/app/src/runtime/gmoccapy-hal-model.js`:
+ - added structured `hardwareButtons` metadata.
+ - recorded native source methods `_button_pin_changed`, `_get_child_button`, `_make_hal_pins`.
+ - recorded rising-edge-only behavior, current visible child mapping, hidden-label skip behavior, and insensitive-target ignored behavior.
+ - mapped right-side v-button pins and selected bottom h-button references.
+ - added `resolveGmoccapyHardwareButton()`.
+ - extended `createGmoccapyHalSummary()` with hard-button counts and behavior fields.
+- Updated `web-rtcp-5axis-sim-plan/app/src/runtime/gmoccapy-communication-model.js`:
+ - added hardware-button HAL path through `_button_pin_changed`.
+ - added `hardware-button` action mapping.
+ - added summary flag `hasNativeHardwareButtonPath`.
+- Updated `web-rtcp-5axis-sim-plan/app/src/state/store.js`:
+ - added `GMOCAPY_HARDWARE_BUTTON` action.
+ - falling edges are ignored.
+ - mapped pins dispatch existing Web actions and reuse existing task policy gates.
+ - unmapped and diagnostic-only pins produce explicit operator messages.
+- Updated `web-rtcp-5axis-sim-plan/app/src/ui/gmoccapy-shell.js`:
+ - added `data-gmoccapy-hal-pin`, `data-gmoccapy-native-panel`, and `data-gmoccapy-native-button-index` to mapped Web buttons.
+ - added `Hard buttons` gmoccapy HAL diagnostic row.
+- Updated tests:
+ - `tests/node/verify_gmoccapy_hal_model.mjs` validates hard-button model and resolver.
+ - `tests/node/verify_gmoccapy_communication_model.mjs` validates hardware-button native path/action/summary.
+ - `tests/node/verify_gmoccapy_xyzab_profile.mjs` validates corrected profile panel pin order.
+ - `tests/node/verify_gmoccapy_xyzab_gates.mjs` validates `GMOCAPY_HARDWARE_BUTTON` falling edge, rising edge, gate, diagnostic-only, and unmapped behavior.
+ - `tests/browser/gmoccapy_shell_smoke.html` validates DOM pin attributes and hard-button diagnostic text.
+- Updated working5 records:
+ - `work/working5/03-推进台账.md`
+ - `work/working5/04-任务矩阵.md`
+ - `work/working5/05-验收证据.md`
+ - `work/working5/06-决策记录.md`
+
+Validation commands and results:
+- Targeted Node tests:
+ - command:
+ - `cd /home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app`
+ - `node ../tests/node/verify_gmoccapy_hal_model.mjs && node ../tests/node/verify_gmoccapy_communication_model.mjs && node ../tests/node/verify_gmoccapy_xyzab_profile.mjs && node ../tests/node/verify_gmoccapy_xyzab_gates.mjs`
+ - result:
+ - `gmoccapy_hal_model_smoke=ok`
+ - `gmoccapy_communication_model_smoke=ok`
+ - `gmoccapy_xyzab_profile_smoke=ok`
+ - `gmoccapy_xyzab_gates_smoke=ok`
+- Full Node smoke:
+ - command:
+ - `npm run smoke:node`
+ - result:
+ - passed.
+ - included `gmoccapy_xyzab_profile_smoke=ok`, `gmoccapy_communication_model_smoke=ok`, `gmoccapy_hal_model_smoke=ok`, `gmoccapy_xyzab_gates_smoke=ok`, and `linear_unit_conversion_smoke=ok`.
+- Build and browser smoke:
+ - command:
+ - `npm run build && npm run smoke`
+ - result:
+ - `gmoccapy_static_build=ok`
+ - `gmoccapy_shell_smoke=ok`
+ - `gmoccapy_dist_smoke=ok`
+- Diff check:
+ - command:
+ - `git diff --check -- web-rtcp-5axis-sim-plan work/working5`
+ - result:
+ - passed with no output.
+
+Working tree notes:
+- Modified intended R4 files:
+ - `web-rtcp-5axis-sim-plan/app/src/runtime/gmoccapy-communication-model.js`
+ - `web-rtcp-5axis-sim-plan/app/src/runtime/gmoccapy-hal-model.js`
+ - `web-rtcp-5axis-sim-plan/app/src/state/store.js`
+ - `web-rtcp-5axis-sim-plan/tests/node/verify_gmoccapy_communication_model.mjs`
+ - `web-rtcp-5axis-sim-plan/tests/node/verify_gmoccapy_hal_model.mjs`
+ - `web-rtcp-5axis-sim-plan/tests/node/verify_gmoccapy_xyzab_gates.mjs`
+ - `web-rtcp-5axis-sim-plan/tests/node/verify_gmoccapy_xyzab_profile.mjs`
+ - `web-rtcp-5axis-sim-plan/tests/browser/gmoccapy_shell_smoke.html`
+ - `work/working5/03-推进台账.md`
+ - `work/working5/04-任务矩阵.md`
+ - `work/working5/05-验收证据.md`
+ - `work/working5/06-决策记录.md`
+ - `web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md`
+- Also present from earlier R3 or pre-existing work:
+ - `web-rtcp-5axis-sim-plan/app/src/profiles/gmoccapy-xyzab.js` includes both R3 tool-change metadata and R4 corrected v-button pin order.
+ - `web-rtcp-5axis-sim-plan/app/src/ui/gmoccapy-shell.js` includes both R3 tool-change diagnostics and R4 hard-button DOM/diagnostics.
+ - `web-rtcp-5axis-sim-plan/tests/node/verify_profile_boundary.mjs` contains R3 tool-change boundary assertions.
+ - `D linuxcnc.zip` remains an unrelated pre-existing deletion and was not touched.
+
+Decision recorded:
+- `DR-W5-004`: external HAL hard-button pins are represented through explicit pin-to-action mappings and `GMOCAPY_HARDWARE_BUTTON`, not by relying on Web DOM order. Native-only buttons not implemented in Web are diagnostic-only.
+
+Outcome:
+- W5-170 added and marked complete.
+- gmoccapy XYZAB external HAL hard-button pin behavior is now modeled, testable, visible in UI diagnostics, and covered by Node/build/browser smoke.
+- No commit or push was requested or performed.
+
+## 2026-06-26 20:26 EDT - 取消旧日志中文转换
+
+用户请求:
+- 先要求将 `web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md` 以前的英文记录也转成中文。
+- 随后用户明确说明“不用转了”。
+
+执行过程:
+- 检查了 `gpdlog.md` 的规模和内容结构,确认文件约 5431 行,包含大量英文过程记录、命令、路径、测试输出和已存在的中文记录。
+- 原计划是保护命令、路径、代码块、URL 和测试输出,只转换叙述性日志文本。
+- 曾尝试安装本地离线翻译工具 `argostranslate`,第一次因下载超时失败;第二次安装进程因用户中断仍在后台运行。
+- 收到“不用转了”后,停止继续转换旧日志。
+- 检查后台进程,发现 `python3 -m pip install --user --timeout 120 --retries 5 --quiet argostranslate` 仍在运行。
+- 终止该后台安装进程,并确认没有剩余 `pip install` / `argostranslate` 相关进程。
+
+结果:
+- 未转换旧的 `gpdlog.md` 内容。
+- 未批量修改历史日志正文。
+- 已停止残留的离线翻译工具安装进程。
+- 仅按当前 `AGENTS.md` 规则,用中文追加本次取消转换的执行过程记录。
+
+## 2026-06-27 - working5 R5:gmoccapy HAL 输入 pin 与倍率 pin 语义补齐
+
+用户请求:
+- 用户要求按 `/home/meswork/cnc_wams/work/working5` 开始工作。
+
+执行过程:
+- 读取 `/home/meswork/cnc_wams/AGENTS.md`,确认每轮执行结束后需要将完整过程日志用中文追加到 `/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md`。
+- 读取 `work/working5/README.md`、`01-项目功能内容.md`、`02-项目程序开发详细步骤.md`、`03-推进台账.md`、`04-任务矩阵.md`、`05-验收证据.md`、`06-决策记录.md`。
+- 确认 working5 已完成 R1 到 R4,下一步明确指向 `gmoccapy.ignore-limits`、`gmoccapy.optional-stop`、`gmoccapy.blockdelete`、override analog/count/reset pins 与 Web 状态的交叉行为。
+- 检查工作树状态,发现已有未提交改动和无关的 `linuxcnc.zip` 删除;按规则没有回退任何既有改动。
+- 使用 `rg` 搜索 Web 项目和 LinuxCNC 源码中与 `optional`、`blockdelete`、`ignore-limits`、`override`、`feed`、`rapid`、`spindle` 相关的实现。
+- 读取 `app/src/state/store.js`、`app/src/profiles/gmoccapy-xyzab.js`、`app/src/runtime/gmoccapy-hal-model.js`、`app/src/runtime/gmoccapy-communication-model.js`、`app/src/ui/gmoccapy-shell.js`、相关 Node/browser 测试。
+- 对照 `/home/meswork/cnc_wams/linuxcnc/src/emc/usr_intf/gmoccapy/gmoccapy.py`,确认:
+ - `_ignore_limits()` 由 `gmoccapy.ignore-limits` 驱动 `chk_ignore_limits`,active 时通过 `on_chk_ignore_limits_toggled()` 调用 `command.override_limits()`。
+ - `gmoccapy.optional-stop` 连接 `_optional_blocks()`,实际驱动 `tbtn_optional_blocks`,再调用 `command.set_block_delete()`。
+ - `gmoccapy.blockdelete` 连接 `_blockdelete()`,实际调用 `command.set_optional_stop()`。
+ - `_on_counts_changed()` 只有对应 `*.count-enable` 为 true 时才调整 feed/rapid/spindle/jog velocity 滑块,否则只同步 counts 基线。
+ - `_on_analog_value_changed()` 只有对应 `*.analog-enable` 为 true 时才接受 `*.direct-value`。
+ - `_reset_override()` 只在 pin 为 true 时复位 feed/rapid/spindle override。
+- 对照 `/home/meswork/cnc_wams/linuxcnc/configs/sim/gmoccapy/gmoccapy_XYZAB.pref`,确认本配置默认 scale:`scale_feed_override = 1`、`scale_rapid_override = 1`、`scale_spindle_override = 1`、`scale_jog_vel = 140.4`。
+
+实现修改:
+- 修改 `app/src/state/store.js`:
+ - 引入 `gmoccapyHalModel`。
+ - 新增 `gmoccapyGui` 状态,记录 `ignoreLimits`、`optionalBlocks`、`optionalStop`、override counts/enable/analog-enable、`jogVelocity` 和最后一次 HAL pin 效果。
+ - 新增 `GMOCAPY_HAL_PIN` action。
+ - 新增 HAL pin 解析、已知 pin 校验、operator input patch、override counts/direct-value/reset patch、100% reset 等 helper。
+ - 实现 `ignore-limits`、`optional-stop -> block delete`、`blockdelete -> optional stop`、override count-enable、analog-enable、direct-value、reset rising-edge 语义。
+ - 新增 `RESET_OVERRIDE`、`SET_BLOCK_DELETE`、`SET_OPTIONAL_STOP`、`SET_IGNORE_LIMITS` 等 Web action。
+- 修改 `app/src/profiles/gmoccapy-xyzab.js`:
+ - 在 `halPins` 中补充 feed/rapid/spindle/jog override counts、count-enable、analog-enable、direct-value、reset pins。
+ - 补充 `gmoccapy.ignore-limits`、`gmoccapy.optional-stop`、`gmoccapy.blockdelete`。
+- 修改 `app/src/runtime/gmoccapy-hal-model.js`:
+ - 扩展 `override` pin 组。
+ - 新增 `operator-inputs` pin 组。
+ - 新增 `halPinActions`,记录 `_ignore_limits`、`_optional_blocks`、`_blockdelete`、`_on_counts_changed`、`_on_analog_value_changed`、`_reset_override` 等源码方法与 Web 映射。
+ - 在 summary 中新增 operator input 数量、override target 数量、optional/blockdelete 交叉标志、count/direct/reset enable 规则。
+- 修改 `app/src/runtime/gmoccapy-communication-model.js`:
+ - 新增 `nativePaths.halPins`。
+ - 新增 `hal-pin-input` action mapping,说明 `GMOCAPY_HAL_PIN` 与 gmoccapy HAL callback 的关系。
+ - summary 新增 `hasNativeHalPinInputPath`。
+- 修改 `app/src/ui/gmoccapy-shell.js`:
+ - 在 info diagnostics 中新增 `data-gmoccapy-hal="operator-inputs"` 和 `data-gmoccapy-hal="override-inputs"` 行。
+ - 在 override 面板新增 ignore limits、block delete、optional stop 和最后一次 HAL input 状态显示。
+ - 为 feed/rapid/spindle override 增加 100% reset 控件。
+ - 新增 HAL input/override 格式化函数。
+- 修改 `app/src/styles/gmoccapy.css`:
+ - 将 override 面板调整为 4 行。
+ - 将 `.stepper` 扩展为 4 列以容纳 100% reset 按钮。
+ - 新增 `.toggle-row` 和 `.status-chip` 样式,避免新增文本挤压或溢出。
+- 修改 Node 测试:
+ - `verify_gmoccapy_xyzab_gates.mjs` 覆盖 `GMOCAPY_HAL_PIN` 的 ignore-limits、optional-stop/blockdelete 交叉、counts/direct-value enable、reset rising-edge、jog velocity scale、unknown pin。
+ - `verify_gmoccapy_hal_model.mjs` 覆盖 operator input 组、override pin 组和 summary 字段。
+ - `verify_gmoccapy_communication_model.mjs` 覆盖 `halPins` path 和 `hal-pin-input` action mapping。
+ - `verify_gmoccapy_xyzab_profile.mjs` 覆盖新增 profile halPins。
+- 修改浏览器测试 `tests/browser/gmoccapy_shell_smoke.html`:
+ - 覆盖 operator input/override diagnostic 文本。
+ - 覆盖 feed/spindle 100% reset 按钮。
+ - 覆盖 ignore limits、block delete、optional stop Web 控件状态。
+ - 通过 `window.webRtcp5AxisSimulation.dispatch({ type: "GMOCAPY_HAL_PIN", ... })` 覆盖 optional/blockdelete 交叉 pin。
+- 更新 working5 文档:
+ - `03-推进台账.md` 追加 R5。
+ - `04-任务矩阵.md` 新增并完成 W5-180。
+ - `05-验收证据.md` 新增 EV-W5-180、EV-W5-181 和 R5 命令/断言摘要。
+ - `06-决策记录.md` 新增 DR-W5-005。
+
+验证命令与结果:
+- 最小 Node 与浏览器回归:
+ - 命令:
+ - `cd /home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app`
+ - `node ../tests/node/verify_gmoccapy_hal_model.mjs && node ../tests/node/verify_gmoccapy_communication_model.mjs && node ../tests/node/verify_gmoccapy_xyzab_profile.mjs && node ../tests/node/verify_gmoccapy_xyzab_gates.mjs && bash ../tests/browser/verify_gmoccapy_shell_browser.sh`
+ - 结果:
+ - `gmoccapy_hal_model_smoke=ok`
+ - `gmoccapy_communication_model_smoke=ok`
+ - `gmoccapy_xyzab_profile_smoke=ok`
+ - `gmoccapy_xyzab_gates_smoke=ok`
+ - `gmoccapy_shell_smoke=ok`
+- 完整 Node smoke:
+ - 命令:
+ - `npm run smoke:node`
+ - 结果:
+ - 通过。
+ - 关键输出包含 `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`。
+- 构建与浏览器 smoke:
+ - 命令:
+ - `npm run build && npm run smoke`
+ - 结果:
+ - `gmoccapy_static_build=ok`
+ - `gmoccapy_shell_smoke=ok`
+ - `gmoccapy_dist_smoke=ok`
+- 空白检查:
+ - 命令:
+ - `git diff --check -- web-rtcp-5axis-sim-plan work/working5`
+ - 结果:
+ - 通过,无输出。
+
+工作树备注:
+- 本轮直接修改的实现和测试文件:
+ - `web-rtcp-5axis-sim-plan/app/src/profiles/gmoccapy-xyzab.js`
+ - `web-rtcp-5axis-sim-plan/app/src/runtime/gmoccapy-communication-model.js`
+ - `web-rtcp-5axis-sim-plan/app/src/runtime/gmoccapy-hal-model.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/tests/browser/gmoccapy_shell_smoke.html`
+ - `web-rtcp-5axis-sim-plan/tests/node/verify_gmoccapy_communication_model.mjs`
+ - `web-rtcp-5axis-sim-plan/tests/node/verify_gmoccapy_hal_model.mjs`
+ - `web-rtcp-5axis-sim-plan/tests/node/verify_gmoccapy_xyzab_gates.mjs`
+ - `web-rtcp-5axis-sim-plan/tests/node/verify_gmoccapy_xyzab_profile.mjs`
+- 本轮更新的工作记录文件:
+ - `work/working5/03-推进台账.md`
+ - `work/working5/04-任务矩阵.md`
+ - `work/working5/05-验收证据.md`
+ - `work/working5/06-决策记录.md`
+ - `web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md`
+- 工作树中仍有先前存在或其他轮次留下的改动,例如 `AGENTS.md`、`linuxcnc.zip` 删除、`verify_profile_boundary.mjs` 等;本轮没有回退这些改动。
+
+结果:
+- W5-180 已完成。
+- Web 项目现在能按 gmoccapy 源码语义模拟 `ignore-limits`、`optional-stop`、`blockdelete` 和 override counts/direct-value/reset HAL input pins。
+- 新增行为已在 profile、HAL model、communication model、store、UI 和 Node/browser 测试中形成可追踪证据。
+- 没有执行 commit 或 push。
+
+## 2026-06-27 R6 过程日志:继续完成 working5 jog 与消息 HAL pin 对标
+
+用户请求:
+- 接续上一轮,继续完成 `/home/meswork/cnc_wams/work/working5` 的工作。
+
+现场恢复:
+- 查看 `work/working5` 的 README、推进台账、任务矩阵、验收证据和决策记录。
+- 确认 R5 已完成 `ignore-limits`、`optional-stop`、`blockdelete` 与 override HAL input pin 语义。
+- 确认下一步为 `gmoccapy.jog.axis.*`、`gmoccapy.jog.jog-inc-*`、`gmoccapy.delete-message`、`gmoccapy.warning-confirm` 等剩余 HAL pin 与 Web 状态/消息系统的映射。
+- 查看工作树,注意到已有未提交改动包括 `AGENTS.md`、`linuxcnc.zip` 删除、`web-rtcp-5axis-sim-plan` 多个源码/测试文件和 `working5` 文档;本轮未回退这些已有改动。
+
+源码对标:
+- 读取 `linuxcnc/src/emc/usr_intf/gmoccapy/gmoccapy.py` 中 `_make_hal_pins()`、`_on_pin_jog_changed()`、`_on_pin_incr_changed()`、`_on_pin_turtle_jog()`、`_del_message_changed()` 等实现。
+- 读取 `linuxcnc/src/emc/usr_intf/gmoccapy/dialogs.py` 中 `warning_dialog()`,确认其每 100ms 轮询 `warning-confirm` confirm pin。
+- 读取 `linuxcnc/configs/sim/gmoccapy/gmoccapy_XYZAB.ini`,确认 `[DISPLAY] INCREMENTS = 1.000 mm, 0.100 mm, 0.010 mm, 0.001 mm ,1.2345 in`。
+- 确认 gmoccapy 会在 INI 增量前插入 continuous `rbt_0`,因此 XYZAB 的 jog increment HAL input 应覆盖 `jog-inc-0..5`,其中 `jog-inc-0` 为连续点动、距离 0,`jog-inc-5` 的 `1.2345 in` 在 mm 机床下为 31.3563 mm。
+- 确认 jog axis pin 与 hard-button pin 不同:hard-button 是 rising-edge-only,jog axis pin 是 true press、false release 的 level 行为。
+
+实现修改:
+- 修改 `app/src/profiles/gmoccapy-xyzab.js`:
+ - 新增 `gmoccapyXyzabJogAxisPins` 和 `gmoccapyXyzabJogIncrementPins`。
+ - profile `halPins` 补齐 10 个 XYZAB axis jog pins、6 个 jog increment pins、`gmoccapy.jog.jog-increment`、`gmoccapy.jog.turtle-jog`、`gmoccapy.delete-message`、`gmoccapy.warning-confirm`。
+- 修改 `app/src/runtime/gmoccapy-hal-model.js`:
+ - jog pin group 增加 `jog-inc-1..5` 和 `jog.turtle-jog`。
+ - `halPinActions.source.methods` 增加 `_on_pin_jog_changed`、`_on_pin_incr_changed`、`_del_message_changed`。
+ - 新增 `jogPins` 结构,记录 5 个轴、10 个 axis jog pins、6 个增量 pin、output pin、turtle pin 和 native callback 语义。
+ - 新增 `messagePins` 结构,记录 `delete-message`、`warning-confirm`、`error` 的 native callback 和 Web 状态。
+ - summary 增加 `jogAxisPinCount`、`jogIncrementPinCount`、`messageInputPinCount`、`jogAxisPinsPressRelease`、`jogIncrementPinsRisingEdgeOnly`、`jogIncrementZeroIsContinuous`、`turtleJogLevelDriven`、`deleteMessageRisingEdgeOnly`、`warningConfirmLevelPolled`。
+- 修改 `app/src/runtime/gmoccapy-communication-model.js`:
+ - HAL input path 说明扩展到 jog、jog increment、message pins。
+ - `hal-pin-input` action mapping 加入 `_del_message_changed` 和 delete-message rising-edge 规则。
+ - 新增 `hal-jog-pin` action mapping,对应 `_on_pin_jog_changed/_on_pin_incr_changed`。
+ - 新增 `hal-warning-confirm` action mapping,对应 `dialogs.warning_dialog` confirm pin polling。
+- 修改 `app/src/state/store.js`:
+ - `gmoccapyGui` 新增 `jogIncrementIndex`、`jogIncrementLabel`、`jogIncrementOutput`、`turtleJog`、`activeJogPin`、`warningConfirm`、`error`、`deletedMessageCount`。
+ - 扩展 `GMOCAPY_HAL_PIN` 分发,处理 jog axis press/release、jog increment rising-edge、turtle jog level、delete-message true/falling edge、warning-confirm level。
+ - 新增解析函数 `resolveGmoccapyJogAxisPin()`、`resolveGmoccapyJogIncrementPin()`。
+ - 新增 `applyGmoccapyJogAxisHalPatch()` 和 `applyGmoccapyJogIncrementHalPatch()`。
+ - jog axis HAL pin 使用 `gmoccapyGui.jogIncrementOutput` 作为 gmoccapy 的当前增量输出,避免把 Web 普通按钮默认 1mm 点动误用于 HAL continuous 默认状态。
+- 修改 `app/src/ui/gmoccapy-shell.js`:
+ - info diagnostics 新增 `data-gmoccapy-hal="jog-inputs"` 和 `data-gmoccapy-hal="message-inputs"`。
+ - 新增 `formatGmoccapyJogInputs()` 和 `formatGmoccapyMessageInputs()`,显示 axis jog pin 数量、jog-inc 数量、press/release level、rising-edge-only、continuous、turtle、delete-message、warning-confirm 等语义。
+
+测试修改:
+- 修改 `tests/node/verify_gmoccapy_hal_model.mjs`:
+ - 覆盖 jog pin group、jogPins 结构、messagePins 结构和 summary 新字段。
+- 修改 `tests/node/verify_gmoccapy_xyzab_profile.mjs`:
+ - 覆盖新增 profile halPins:axis jog、jog-inc、jog-increment、turtle-jog、delete-message、warning-confirm。
+- 修改 `tests/node/verify_gmoccapy_communication_model.mjs`:
+ - 覆盖新增 `hal-jog-pin`、`hal-warning-confirm` action mappings。
+ - summary action count 更新为 19。
+- 修改 `tests/node/verify_gmoccapy_xyzab_gates.mjs`:
+ - 覆盖 HAL jog 未上电 gate、continuous jog press/release、`jog-inc-2` falling edge 忽略和 rising edge 选择、`jog-inc-5` 英寸换算、turtle jog level。
+ - 覆盖 `delete-message` falling edge 忽略、true 删除 alert/消息、`warning-confirm` true/false 状态。
+- 修改 `tests/browser/gmoccapy_shell_smoke.html`:
+ - 覆盖 `data-gmoccapy-hal="jog-inputs"` 和 `data-gmoccapy-hal="message-inputs"` 文本。
+ - 通过公开 `window.webRtcp5AxisSimulation.dispatch({ type: "GMOCAPY_HAL_PIN", ... })` 验证浏览器内 jog-inc、axis jog press/release、turtle jog、delete-message、warning-confirm。
+ - 在相关测试段显式 RESET 后重新 POWER 并切回 Manual,避免依赖前序状态。
+
+工作文档更新:
+- 修改 `work/working5/04-任务矩阵.md`:新增并完成 W5-190。
+- 修改 `work/working5/03-推进台账.md`:追加 2026-06-27 R6 记录。
+- 修改 `work/working5/05-验收证据.md`:新增 EV-W5-190、EV-W5-191 和 R6 命令/断言摘要。
+- 修改 `work/working5/06-决策记录.md`:新增 DR-W5-006,记录 jog/message HAL pin 按 gmoccapy 回调边界建模的原因和影响。
+
+验证命令与结果:
+- 最小模型/通信测试:
+ - 命令:`cd /home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app && node ../tests/node/verify_gmoccapy_hal_model.mjs && node ../tests/node/verify_gmoccapy_communication_model.mjs`
+ - 结果:通过,输出 `gmoccapy_hal_model_smoke=ok`、`gmoccapy_communication_model_smoke=ok`。
+- profile/store 行为测试:
+ - 命令:`cd /home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app && node ../tests/node/verify_gmoccapy_xyzab_profile.mjs && node ../tests/node/verify_gmoccapy_xyzab_gates.mjs`
+ - 结果:通过,输出 `gmoccapy_xyzab_profile_smoke=ok`、`gmoccapy_xyzab_gates_smoke=ok`。
+- 浏览器 shell smoke:
+ - 命令:`cd /home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app && bash ../tests/browser/verify_gmoccapy_shell_browser.sh`
+ - 结果:通过,输出 `gmoccapy_shell_smoke=ok`。
+- 完整 Node smoke:
+ - 命令:`cd /home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app && npm run smoke:node`
+ - 结果:通过,关键输出包含 `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`。
+- 构建和浏览器/dist smoke:
+ - 命令:`cd /home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app && npm run build && npm run smoke`
+ - 结果:通过,输出 `gmoccapy_static_build=ok`、`gmoccapy_shell_smoke=ok`、`gmoccapy_dist_smoke=ok`。
+- 空白检查:
+ - 命令:`git diff --check -- web-rtcp-5axis-sim-plan work/working5`
+ - 结果:通过,无输出。
+
+工作树备注:
+- 本轮直接修改的实现/测试文件:
+ - `web-rtcp-5axis-sim-plan/app/src/profiles/gmoccapy-xyzab.js`
+ - `web-rtcp-5axis-sim-plan/app/src/runtime/gmoccapy-communication-model.js`
+ - `web-rtcp-5axis-sim-plan/app/src/runtime/gmoccapy-hal-model.js`
+ - `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/tests/browser/gmoccapy_shell_smoke.html`
+ - `web-rtcp-5axis-sim-plan/tests/node/verify_gmoccapy_communication_model.mjs`
+ - `web-rtcp-5axis-sim-plan/tests/node/verify_gmoccapy_hal_model.mjs`
+ - `web-rtcp-5axis-sim-plan/tests/node/verify_gmoccapy_xyzab_gates.mjs`
+ - `web-rtcp-5axis-sim-plan/tests/node/verify_gmoccapy_xyzab_profile.mjs`
+- 本轮更新的 working5 文档:
+ - `work/working5/03-推进台账.md`
+ - `work/working5/04-任务矩阵.md`
+ - `work/working5/05-验收证据.md`
+ - `work/working5/06-决策记录.md`
+- 工作树中仍有此前存在或其他轮次留下的改动,例如 `AGENTS.md`、`linuxcnc.zip` 删除、`web-rtcp-5axis-sim-plan/app/src/styles/gmoccapy.css`、`verify_profile_boundary.mjs`、`备忘/备忘3.txt` 等;本轮没有回退这些改动。
+
+结果:
+- W5-190 已完成。
+- Web 项目现在能按 gmoccapy 源码语义模拟 jog axis、jog increment、turtle jog、delete-message 和 warning-confirm HAL input pins。
+- 新增行为已在 profile、HAL model、communication model、store、UI 和 Node/browser 测试中形成可追踪证据。
+- 没有执行 commit 或 push。
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 a0ce0de..7452224 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
@@ -282,6 +282,25 @@
throw new Error(`missing gmoccapy icon ${selector} expected ${iconName}, got ${button?.dataset.iconName}`);
}
}
+ for (const [selector, pin, panel, index] of [
+ ['[data-action="estop"]', "gmoccapy.v-button.button-0", "right", "0"],
+ ['[data-action="power"]', "gmoccapy.v-button.button-1", "right", "1"],
+ ['[data-action="mode-manual"]', "gmoccapy.v-button.button-2", "right", "2"],
+ ['[data-action="mode-mdi"]', "gmoccapy.v-button.button-3", "right", "3"],
+ ['[data-action="mode-auto"]', "gmoccapy.v-button.button-4", "right", "4"],
+ ['[data-action="mode-jog"]', "gmoccapy.v-button.button-5", "right", "5"],
+ ['[data-action="HOME"]', "gmoccapy.h-button.button-0", "bottom", "0"],
+ ]) {
+ const button = doc.querySelector(selector);
+ if (
+ !button ||
+ button.dataset.gmoccapyHalPin !== pin ||
+ button.dataset.gmoccapyNativePanel !== panel ||
+ button.dataset.gmoccapyNativeButtonIndex !== index
+ ) {
+ throw new Error(`gmoccapy hard-button mapping mismatch ${selector}: ${JSON.stringify(button?.dataset)}`);
+ }
+ }
if (!doc.querySelector('[data-gmoccapy-comm="boundary"]')?.textContent.includes("native_gmoccapy_nml_hal_reference")) {
throw new Error("missing gmoccapy communication boundary diagnostic");
}
@@ -294,6 +313,60 @@
if (!doc.querySelector('[data-gmoccapy-hal="postgui"]')?.textContent.includes("tool-change-loop")) {
throw new Error("missing gmoccapy HAL postgui diagnostic");
}
+ const halInputDiagnostic = doc.querySelector('[data-gmoccapy-hal="operator-inputs"]')?.textContent || "";
+ if (
+ !halInputDiagnostic.includes("3 operator pins") ||
+ !halInputDiagnostic.includes("optional-stop -> block delete") ||
+ !halInputDiagnostic.includes("blockdelete -> optional stop") ||
+ !halInputDiagnostic.includes("ignore-limits off")
+ ) {
+ throw new Error(`missing gmoccapy HAL operator input diagnostic: ${halInputDiagnostic}`);
+ }
+ const overrideInputDiagnostic = doc.querySelector('[data-gmoccapy-hal="override-inputs"]')?.textContent || "";
+ if (
+ !overrideInputDiagnostic.includes("4 override targets") ||
+ !overrideInputDiagnostic.includes("counts require enable") ||
+ !overrideInputDiagnostic.includes("direct-value requires analog-enable") ||
+ !overrideInputDiagnostic.includes("reset rising-edge-only")
+ ) {
+ throw new Error(`missing gmoccapy HAL override diagnostic: ${overrideInputDiagnostic}`);
+ }
+ const jogInputDiagnostic = doc.querySelector('[data-gmoccapy-hal="jog-inputs"]')?.textContent || "";
+ if (
+ !jogInputDiagnostic.includes("10 axis jog pins") ||
+ !jogInputDiagnostic.includes("6 jog-inc pins") ||
+ !jogInputDiagnostic.includes("press/release level") ||
+ !jogInputDiagnostic.includes("jog-inc rising-edge-only") ||
+ !jogInputDiagnostic.includes("jog-inc-0 continuous")
+ ) {
+ throw new Error(`missing gmoccapy HAL jog diagnostic: ${jogInputDiagnostic}`);
+ }
+ const messageInputDiagnostic = doc.querySelector('[data-gmoccapy-hal="message-inputs"]')?.textContent || "";
+ if (
+ !messageInputDiagnostic.includes("3 message pins") ||
+ !messageInputDiagnostic.includes("delete-message rising-edge-only") ||
+ !messageInputDiagnostic.includes("warning-confirm level-polled")
+ ) {
+ throw new Error(`missing gmoccapy HAL message diagnostic: ${messageInputDiagnostic}`);
+ }
+ const hardButtonDiagnostic = doc.querySelector('[data-gmoccapy-hal="hard-buttons"]')?.textContent || "";
+ if (
+ !hardButtonDiagnostic.includes("7 right v-button pins") ||
+ !hardButtonDiagnostic.includes("4 bottom h-button main refs") ||
+ !hardButtonDiagnostic.includes("7 mapped Web buttons") ||
+ !hardButtonDiagnostic.includes("rising-edge-only") ||
+ !hardButtonDiagnostic.includes("insensitive ignored")
+ ) {
+ throw new Error(`missing gmoccapy hard-button diagnostic: ${hardButtonDiagnostic}`);
+ }
+ const toolChangeDiagnostic = doc.querySelector('[data-gmoccapy-hal="tool-change"]')?.textContent || "";
+ if (
+ !toolChangeDiagnostic.includes("iocontrol-loopback") ||
+ !toolChangeDiagnostic.includes("manual GUI pins disconnected") ||
+ !toolChangeDiagnostic.includes("3 postgui unlinkp")
+ ) {
+ throw new Error(`missing gmoccapy HAL tool-change diagnostic: ${toolChangeDiagnostic}`);
+ }
if (!doc.querySelector('[data-tool-preview="summary"]')?.textContent.includes("T1")) {
throw new Error("missing tool preview summary");
}
@@ -877,6 +950,16 @@
if (win.webRtcp5AxisSimulation.getState().runState !== "idle") {
throw new Error("RESET did not return to idle");
}
+ doc.querySelector('[data-action="power"]').click();
+ await wait(50);
+ if (win.webRtcp5AxisSimulation.getState().machine.powerOn !== true) {
+ throw new Error("POWER did not re-enable machine before override tests");
+ }
+ doc.querySelector('[data-action="mode-manual"]').click();
+ await wait(50);
+ if (win.webRtcp5AxisSimulation.getState().machine.mode !== "manual") {
+ throw new Error("MANUAL mode did not re-enable before HAL jog tests");
+ }
doc.querySelector('[data-action="feed-override-down"]').click();
await wait(50);
if (win.webRtcp5AxisSimulation.getState().feed.feedOverride !== 90) {
@@ -885,6 +968,11 @@
if (doc.querySelector('[data-value="feed-override"]')?.textContent.trim() !== "90 %") {
throw new Error("feed override DOM did not update");
}
+ doc.querySelector('[data-action="feed-override-reset"]').click();
+ await wait(50);
+ if (win.webRtcp5AxisSimulation.getState().feed.feedOverride !== 100) {
+ throw new Error("feed override reset button did not update state");
+ }
doc.querySelector('[data-action="rapid-override-up"]').click();
await wait(50);
if (win.webRtcp5AxisSimulation.getState().feed.rapidOverride !== 110) {
@@ -895,6 +983,121 @@
if (win.webRtcp5AxisSimulation.getState().spindle.override !== 110) {
throw new Error("spindle override button did not update state");
}
+ doc.querySelector('[data-action="spindle-override-reset"]').click();
+ await wait(50);
+ if (win.webRtcp5AxisSimulation.getState().spindle.override !== 100) {
+ throw new Error("spindle override reset button did not update state");
+ }
+ doc.querySelector('[data-action="ignore-limits"]').click();
+ doc.querySelector('[data-action="block-delete"]').click();
+ doc.querySelector('[data-action="optional-stop"]').click();
+ await wait(50);
+ const guiState = win.webRtcp5AxisSimulation.getState().gmoccapyGui;
+ if (guiState.ignoreLimits !== true || guiState.optionalBlocks !== true || guiState.optionalStop !== true) {
+ throw new Error(`gmoccapy HAL input buttons did not update state: ${JSON.stringify(guiState)}`);
+ }
+ if (!doc.querySelector('[data-value="gmoccapy-hal-last"]')?.textContent.includes("optional stop set from Web control")) {
+ throw new Error("gmoccapy HAL last input DOM did not update");
+ }
+ win.webRtcp5AxisSimulation.dispatch({
+ type: "GMOCAPY_HAL_PIN",
+ pin: "gmoccapy.optional-stop",
+ value: false,
+ });
+ win.webRtcp5AxisSimulation.dispatch({
+ type: "GMOCAPY_HAL_PIN",
+ pin: "gmoccapy.blockdelete",
+ value: false,
+ });
+ await wait(50);
+ const crossedGuiState = win.webRtcp5AxisSimulation.getState().gmoccapyGui;
+ if (crossedGuiState.optionalBlocks !== false || crossedGuiState.optionalStop !== false) {
+ throw new Error(`gmoccapy HAL crossed optional/blockdelete pins did not update state: ${JSON.stringify(crossedGuiState)}`);
+ }
+ win.webRtcp5AxisSimulation.dispatch({
+ type: "GMOCAPY_HAL_PIN",
+ pin: "gmoccapy.jog.jog-inc-2",
+ value: false,
+ });
+ await wait(25);
+ if (win.webRtcp5AxisSimulation.getState().machine.jogIncrement !== 1) {
+ throw new Error("gmoccapy HAL jog increment falling edge should not change Web machine jog increment");
+ }
+ win.webRtcp5AxisSimulation.dispatch({
+ type: "GMOCAPY_HAL_PIN",
+ pin: "gmoccapy.jog.jog-inc-2",
+ value: true,
+ });
+ await wait(25);
+ let halJogGuiState = win.webRtcp5AxisSimulation.getState().gmoccapyGui;
+ if (win.webRtcp5AxisSimulation.getState().machine.jogIncrement !== 0.1 || halJogGuiState.jogIncrementOutput !== 0.1) {
+ throw new Error(`gmoccapy HAL jog-inc pin did not select 0.100 mm: ${JSON.stringify(halJogGuiState)}`);
+ }
+ win.webRtcp5AxisSimulation.dispatch({
+ type: "GMOCAPY_HAL_PIN",
+ pin: "gmoccapy.jog.axis.jog-x-plus",
+ value: true,
+ });
+ await wait(50);
+ if (
+ win.webRtcp5AxisSimulation.getState().runState !== "jogging" ||
+ win.webRtcp5AxisSimulation.getState().gmoccapyGui.activeJogPin !== "gmoccapy.jog.axis.jog-x-plus"
+ ) {
+ throw new Error(`gmoccapy HAL jog axis pin did not enter jogging state: ${JSON.stringify(win.webRtcp5AxisSimulation.getState().gmoccapyGui)}`);
+ }
+ win.webRtcp5AxisSimulation.dispatch({
+ type: "GMOCAPY_HAL_PIN",
+ pin: "gmoccapy.jog.axis.jog-x-plus",
+ value: false,
+ });
+ await wait(50);
+ if (
+ win.webRtcp5AxisSimulation.getState().runState !== "idle" ||
+ win.webRtcp5AxisSimulation.getState().gmoccapyGui.activeJogPin !== null
+ ) {
+ throw new Error("gmoccapy HAL jog release did not clear active jog pin");
+ }
+ win.webRtcp5AxisSimulation.dispatch({
+ type: "GMOCAPY_HAL_PIN",
+ pin: "gmoccapy.jog.turtle-jog",
+ value: true,
+ });
+ await wait(25);
+ if (win.webRtcp5AxisSimulation.getState().gmoccapyGui.turtleJog !== true) {
+ throw new Error("gmoccapy HAL turtle-jog pin did not update state");
+ }
+ win.webRtcp5AxisSimulation.dispatch({
+ type: "GMOCAPY_HAL_PIN",
+ pin: "gmoccapy.delete-message",
+ value: false,
+ });
+ await wait(25);
+ if (win.webRtcp5AxisSimulation.getState().gmoccapyGui.deletedMessageCount !== 0) {
+ throw new Error("gmoccapy HAL delete-message falling edge should be ignored");
+ }
+ win.webRtcp5AxisSimulation.dispatch({
+ type: "GMOCAPY_HAL_PIN",
+ pin: "gmoccapy.delete-message",
+ value: true,
+ });
+ win.webRtcp5AxisSimulation.dispatch({
+ type: "GMOCAPY_HAL_PIN",
+ pin: "gmoccapy.warning-confirm",
+ value: true,
+ });
+ await wait(50);
+ halJogGuiState = win.webRtcp5AxisSimulation.getState().gmoccapyGui;
+ if (halJogGuiState.deletedMessageCount !== 1 || halJogGuiState.warningConfirm !== true) {
+ throw new Error(`gmoccapy HAL message pins did not update state: ${JSON.stringify(halJogGuiState)}`);
+ }
+ const updatedJogDiagnostic = doc.querySelector('[data-gmoccapy-hal="jog-inputs"]')?.textContent || "";
+ const updatedMessageDiagnostic = doc.querySelector('[data-gmoccapy-hal="message-inputs"]')?.textContent || "";
+ if (!updatedJogDiagnostic.includes("turtle level-driven") || !updatedJogDiagnostic.includes("increment 0.100 mm=0.1")) {
+ throw new Error(`gmoccapy HAL jog diagnostic did not refresh: ${updatedJogDiagnostic}`);
+ }
+ if (!updatedMessageDiagnostic.includes("deleted 1") || !updatedMessageDiagnostic.includes("confirm on")) {
+ throw new Error(`gmoccapy HAL message diagnostic did not refresh: ${updatedMessageDiagnostic}`);
+ }
doc.querySelector('[data-action="toggle-flood"]').click();
doc.querySelector('[data-action="toggle-mist"]').click();
await wait(50);
diff --git a/web-rtcp-5axis-sim-plan/tests/node/verify_gmoccapy_communication_model.mjs b/web-rtcp-5axis-sim-plan/tests/node/verify_gmoccapy_communication_model.mjs
index a52c785..41c6030 100644
--- a/web-rtcp-5axis-sim-plan/tests/node/verify_gmoccapy_communication_model.mjs
+++ b/web-rtcp-5axis-sim-plan/tests/node/verify_gmoccapy_communication_model.mjs
@@ -17,6 +17,8 @@ assert.deepEqual(model.nativePaths.command.path, ["gmoccapy", "linuxcnc.command(
assert.equal(model.nativePaths.status.path.includes("linuxcnc.stat()"), true);
assert.equal(model.nativePaths.status.path.includes("linuxcnc.error_channel()"), true);
assert.equal(model.nativePaths.hal.path.includes("HAL shared memory"), true);
+assert.equal(model.nativePaths.hardwareButtons.path.includes("_button_pin_changed"), true);
+assert.equal(model.nativePaths.halPins.path.includes("hal_glib.GPin value_changed"), true);
assert.equal(model.nativePaths.remap.path.includes("Python remap prolog/ngc/epilog"), true);
assert.deepEqual(model.webPath.path, ["button", "store.dispatch", "linuxcnc-task-policy", "runtime/status snapshot", "UI/Three.js"]);
@@ -42,6 +44,10 @@ for (const actionId of [
"abort",
"spindle",
"coolant",
+ "hardware-button",
+ "hal-pin-input",
+ "hal-jog-pin",
+ "hal-warning-confirm",
]) {
assert.equal(actionIds.includes(actionId), true, `${actionId} missing`);
}
@@ -60,15 +66,42 @@ const spindleMapping = model.actionMappings.find((mapping) => mapping.actionId =
assert.equal(spindleMapping.nativeStatus.includes("spindle.0.forward"), true);
assert.equal(spindleMapping.gate.includes("restricted during interpreter"), true);
+const hardwareButtonMapping = model.actionMappings.find((mapping) => mapping.actionId === "hardware-button");
+assert.equal(hardwareButtonMapping.nativeCommand.includes("_button_pin_changed"), true);
+assert.equal(hardwareButtonMapping.gate.includes("rising-edge"), true);
+assert.equal(hardwareButtonMapping.gate.includes("insensitive"), true);
+
+const halPinMapping = model.actionMappings.find((mapping) => mapping.actionId === "hal-pin-input");
+assert.equal(halPinMapping.webAction, "GMOCAPY_HAL_PIN");
+assert.equal(halPinMapping.nativeCommand.includes("_optional_blocks"), true);
+assert.equal(halPinMapping.nativeCommand.includes("_on_counts_changed"), true);
+assert.equal(halPinMapping.nativeCommand.includes("_del_message_changed"), true);
+assert.equal(halPinMapping.gate.includes("counts require count-enable"), true);
+assert.equal(halPinMapping.gate.includes("direct-value requires analog-enable"), true);
+assert.equal(halPinMapping.gate.includes("delete-message pins are rising-edge only"), true);
+
+const halJogMapping = model.actionMappings.find((mapping) => mapping.actionId === "hal-jog-pin");
+assert.equal(halJogMapping.webAction, "GMOCAPY_HAL_PIN jog.axis/jog-inc");
+assert.equal(halJogMapping.nativeCommand.includes("_on_pin_jog_changed"), true);
+assert.equal(halJogMapping.nativeCommand.includes("_on_pin_incr_changed"), true);
+assert.equal(halJogMapping.gate.includes("press/release level"), true);
+assert.equal(halJogMapping.gate.includes("increment pins are rising-edge only"), true);
+
+const warningConfirmMapping = model.actionMappings.find((mapping) => mapping.actionId === "hal-warning-confirm");
+assert.equal(warningConfirmMapping.nativeCommand.includes("confirm_pin"), true);
+assert.equal(warningConfirmMapping.gate, "level-polled while warning dialog is active");
+
const summary = createGmoccapyCommunicationSummary(model);
assert.equal(summary.apiName, "web-rtcp-5axis-gmoccapy-communication-model-summary");
assert.equal(summary.profileId, "gmoccapy-xyzab");
-assert.equal(summary.nativePathCount, 4);
+assert.equal(summary.nativePathCount, 6);
assert.equal(summary.startupStageCount, 9);
-assert.equal(summary.actionCount, 15);
+assert.equal(summary.actionCount, 19);
assert.equal(summary.postguiAfterHalcompReady, true);
assert.equal(summary.hasNativeNmlCommandPath, true);
assert.equal(summary.hasNativeHalPath, true);
+assert.equal(summary.hasNativeHardwareButtonPath, true);
+assert.equal(summary.hasNativeHalPinInputPath, true);
assert.equal(summary.hasWebStorePath, true);
assert.equal(summary.semanticBoundary, model.semanticBoundary);
diff --git a/web-rtcp-5axis-sim-plan/tests/node/verify_gmoccapy_hal_model.mjs b/web-rtcp-5axis-sim-plan/tests/node/verify_gmoccapy_hal_model.mjs
index 4589abf..4329110 100644
--- a/web-rtcp-5axis-sim-plan/tests/node/verify_gmoccapy_hal_model.mjs
+++ b/web-rtcp-5axis-sim-plan/tests/node/verify_gmoccapy_hal_model.mjs
@@ -3,6 +3,7 @@ import assert from "node:assert/strict";
import {
createGmoccapyHalSummary,
gmoccapyHalModel,
+ resolveGmoccapyHardwareButton,
} from "../../app/src/runtime/gmoccapy-hal-model.js";
const model = gmoccapyHalModel;
@@ -23,6 +24,7 @@ for (const groupName of [
"hard-buttons",
"jog",
"override",
+ "operator-inputs",
"tool",
"program",
"message",
@@ -34,7 +36,14 @@ for (const groupName of [
assert.equal(groups.get("hard-buttons").pins.includes("gmoccapy.h-button.button-0"), true);
assert.equal(groups.get("hard-buttons").pins.includes("gmoccapy.v-button.button-6"), true);
assert.equal(groups.get("jog").pins.includes("gmoccapy.jog.axis.jog-b-minus"), true);
+assert.equal(groups.get("jog").pins.includes("gmoccapy.jog.jog-inc-5"), true);
+assert.equal(groups.get("jog").pins.includes("gmoccapy.jog.turtle-jog"), true);
assert.equal(groups.get("override").pins.includes("gmoccapy.rapid.rapid-override.direct-value"), true);
+assert.equal(groups.get("override").pins.includes("gmoccapy.feed.feed-override.count-enable"), true);
+assert.equal(groups.get("override").pins.includes("gmoccapy.spindle.reset-spindle-override"), true);
+assert.equal(groups.get("operator-inputs").pins.includes("gmoccapy.ignore-limits"), true);
+assert.equal(groups.get("operator-inputs").pins.includes("gmoccapy.optional-stop"), true);
+assert.equal(groups.get("operator-inputs").pins.includes("gmoccapy.blockdelete"), true);
assert.equal(groups.get("tool").pins.includes("gmoccapy.toolchange-changed"), true);
assert.equal(groups.get("program").pins.includes("gmoccapy.program.current-line"), true);
assert.equal(groups.get("message").pins.includes("gmoccapy.error"), true);
@@ -56,17 +65,113 @@ assert.equal(model.postguiNets.some((net) => net.target === "gmoccapy.spindle_fe
assert.equal(model.postguiNets.some((net) => net.target === "gmoccapy.tooloffset-x"), true);
assert.equal(model.postguiNets.some((net) => net.simulationLoop && net.signal === "tool-change-loop"), true);
+assert.equal(model.hardwareButtons.source.methods.includes("_button_pin_changed"), true);
+assert.equal(model.hardwareButtons.behavior.edge, "rising-edge-only");
+assert.equal(model.hardwareButtons.behavior.insensitiveTarget, "ignored");
+assert.deepEqual(model.hardwareButtons.verticalMain.map((button) => button.nativeWidget), [
+ "tbtn_estop",
+ "tbtn_on",
+ "rbt_manual",
+ "rbt_mdi",
+ "rbt_auto",
+ "tbtn_user_tabs",
+ "tbtn_setup",
+]);
+assert.deepEqual(model.hardwareButtons.verticalMain.map((button) => button.pin), [
+ "gmoccapy.v-button.button-0",
+ "gmoccapy.v-button.button-1",
+ "gmoccapy.v-button.button-2",
+ "gmoccapy.v-button.button-3",
+ "gmoccapy.v-button.button-4",
+ "gmoccapy.v-button.button-5",
+ "gmoccapy.v-button.button-6",
+]);
+assert.equal(model.hardwareButtons.bottomMain.find((button) => button.index === 0).nativeWidget, "btn_homing");
+assert.equal(model.hardwareButtons.bottomMain.find((button) => button.index === 0).webDispatch.type, "HOME");
+assert.equal(resolveGmoccapyHardwareButton("gmoccapy.v-button.button-4", model).webDispatch.mode, "auto");
+assert.equal(resolveGmoccapyHardwareButton({ location: "bottom", index: 0 }, model).webDispatch.type, "HOME");
+assert.equal(resolveGmoccapyHardwareButton({ location: "right", index: 6 }, model).webDispatch, undefined);
+assert.equal(resolveGmoccapyHardwareButton("gmoccapy.h-button.button-9", model), null);
+
+assert.equal(model.halPinActions.source.methods.includes("_optional_blocks"), true);
+assert.equal(model.halPinActions.source.methods.includes("_blockdelete"), true);
+assert.equal(model.halPinActions.source.methods.includes("_on_pin_jog_changed"), true);
+assert.equal(model.halPinActions.source.methods.includes("_on_pin_incr_changed"), true);
+assert.equal(model.halPinActions.source.methods.includes("_del_message_changed"), true);
+assert.equal(model.halPinActions.jogPins.axes.length, 5);
+assert.equal(model.halPinActions.jogPins.axes.find((entry) => entry.axis === "b").minus, "gmoccapy.jog.axis.jog-b-minus");
+assert.equal(model.halPinActions.jogPins.increments.length, 6);
+assert.equal(model.halPinActions.jogPins.increments[0].distance, 0);
+assert.equal(model.halPinActions.jogPins.increments[5].pin, "gmoccapy.jog.jog-inc-5");
+assert.equal(model.halPinActions.jogPins.increments[5].distance, 31.3563);
+assert.equal(model.halPinActions.jogPins.turtlePin, "gmoccapy.jog.turtle-jog");
+assert.equal(model.halPinActions.operatorPins.find((pin) => pin.pin === "gmoccapy.optional-stop").webState, "gmoccapyGui.optionalBlocks");
+assert.equal(model.halPinActions.operatorPins.find((pin) => pin.pin === "gmoccapy.blockdelete").webState, "gmoccapyGui.optionalStop");
+assert.equal(model.halPinActions.overridePins.find((pin) => pin.target === "feed").countEnable, "gmoccapy.feed.feed-override.count-enable");
+assert.equal(model.halPinActions.overridePins.find((pin) => pin.target === "spindle").reset, "gmoccapy.spindle.reset-spindle-override");
+assert.equal(model.halPinActions.overridePins.find((pin) => pin.target === "jogVelocity").scalePref, "scale_jog_vel=140.4");
+assert.equal(model.halPinActions.behavior.optionalStopPinCrossesToBlockDelete, true);
+assert.equal(model.halPinActions.behavior.blockdeletePinCrossesToOptionalStop, true);
+assert.equal(model.halPinActions.behavior.jogAxisPinsPressRelease, true);
+assert.equal(model.halPinActions.behavior.jogPinsUseTaskPolicyGate, true);
+assert.equal(model.halPinActions.behavior.jogIncrementPinsRisingEdgeOnly, true);
+assert.equal(model.halPinActions.behavior.jogIncrementZeroIsContinuous, true);
+assert.equal(model.halPinActions.behavior.turtleJogLevelDriven, true);
+assert.equal(model.halPinActions.behavior.countInputRequiresEnable, true);
+assert.equal(model.halPinActions.behavior.directValueRequiresAnalogEnable, true);
+assert.equal(model.halPinActions.behavior.resetPinsRisingEdgeOnly, true);
+assert.equal(model.halPinActions.messagePins.find((pin) => pin.pin === "gmoccapy.delete-message").nativeCallback, "_del_message_changed");
+assert.equal(model.halPinActions.messagePins.find((pin) => pin.pin === "gmoccapy.warning-confirm").nativeCallback, "dialogs.warning_dialog periodic confirm_pin poll");
+assert.equal(model.halPinActions.behavior.deleteMessageRisingEdgeOnly, true);
+assert.equal(model.halPinActions.behavior.warningConfirmLevelPolled, true);
+
+assert.equal(model.toolChange.strategy, "iocontrol-loopback");
+assert.equal(model.toolChange.manualGmoccapyPinsConnected, false);
+assert.deepEqual(model.toolChange.remapCodes, ["M6", "M61"]);
+assert.equal(model.toolChange.postguiUnlinks.map((entry) => entry.pin).join(","), "iocontrol.0.tool-change,iocontrol.0.tool-changed,iocontrol.0.tool-prep-number");
+assert.equal(model.toolChange.commentedManualGuiNets.some((net) => net.target === "gmoccapy.toolchange-change"), true);
+assert.equal(model.toolChange.commentedManualGuiNets.some((net) => net.source === "gmoccapy.toolchange-changed"), true);
+assert.equal(model.toolChange.activeLoop.signal, "tool-change-loop");
+assert.equal(model.toolChange.activeLoop.stage, "POSTGUI_HALFILE");
+assert.equal(model.toolChange.semanticBoundary, "gmoccapy_xyzab_iocontrol_tool_change_loop_no_manual_gui_dialog");
+
const summary = createGmoccapyHalSummary(model);
assert.equal(summary.apiName, "web-rtcp-5axis-gmoccapy-hal-model-summary");
assert.equal(summary.profileId, "gmoccapy-xyzab");
assert.equal(summary.halFileCount, 4);
-assert.equal(summary.nativePinGroupCount, 7);
+assert.equal(summary.nativePinGroupCount, 8);
assert.equal(summary.nativePinCount >= 50, true);
assert.equal(summary.coreNetCount, 10);
assert.equal(summary.spindleNetCount, 4);
assert.equal(summary.postguiNetCount, 5);
assert.equal(summary.postguiRequiresHalcompReady, true);
assert.equal(summary.hasToolChangeSimulationLoop, true);
+assert.equal(summary.toolChangeStrategy, "iocontrol-loopback");
+assert.equal(summary.manualToolChangeGuiConnected, false);
+assert.equal(summary.postguiToolUnlinkCount, 3);
+assert.equal(summary.commentedManualToolChangeNetCount, 3);
+assert.equal(summary.hardwareButtonVerticalCount, 7);
+assert.equal(summary.hardwareButtonBottomMainCount, 4);
+assert.equal(summary.mappedHardwareButtonCount, 7);
+assert.equal(summary.hardwareButtonEdge, "rising-edge-only");
+assert.equal(summary.hardwareButtonInsensitiveTarget, "ignored");
+assert.equal(summary.operatorInputPinCount, 3);
+assert.equal(summary.overridePinTargetCount, 4);
+assert.equal(summary.jogAxisPinCount, 10);
+assert.equal(summary.jogIncrementPinCount, 6);
+assert.equal(summary.messageInputPinCount, 3);
+assert.equal(summary.optionalStopPinCrossesToBlockDelete, true);
+assert.equal(summary.blockdeletePinCrossesToOptionalStop, true);
+assert.equal(summary.jogAxisPinsPressRelease, true);
+assert.equal(summary.jogPinsUseTaskPolicyGate, true);
+assert.equal(summary.jogIncrementPinsRisingEdgeOnly, true);
+assert.equal(summary.jogIncrementZeroIsContinuous, true);
+assert.equal(summary.turtleJogLevelDriven, true);
+assert.equal(summary.countInputRequiresEnable, true);
+assert.equal(summary.directValueRequiresAnalogEnable, true);
+assert.equal(summary.resetPinsRisingEdgeOnly, true);
+assert.equal(summary.deleteMessageRisingEdgeOnly, true);
+assert.equal(summary.warningConfirmLevelPolled, true);
assert.equal(summary.semanticBoundary, model.semanticBoundary);
console.log("gmoccapy_hal_model_smoke=ok");
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 c374cf1..c909600 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
@@ -200,4 +200,116 @@ assert.equal(controlStore.getState().spindle.direction, "stop");
controlStore.dispatch({ type: "TOGGLE_COOLANT", kind: "flood" });
assert.equal(controlStore.getState().coolant.flood, true);
+const hardButtonStore = createSimulationStore();
+hardButtonStore.dispatch({ type: "SET_PROFILE", profileId: "gmoccapy-xyzab" });
+hardButtonStore.dispatch({ type: "GMOCAPY_HARDWARE_BUTTON", pin: "gmoccapy.v-button.button-1", value: false });
+assert.equal(hardButtonStore.getState().machine.powerOn, false);
+assert.equal(hardButtonStore.getState().operatorMessage, "gmoccapy hardware button falling edge ignored");
+hardButtonStore.dispatch({ type: "GMOCAPY_HARDWARE_BUTTON", pin: "gmoccapy.v-button.button-1", value: true });
+assert.equal(hardButtonStore.getState().machine.powerOn, true);
+hardButtonStore.dispatch({ type: "GMOCAPY_HARDWARE_BUTTON", pin: "gmoccapy.v-button.button-4", value: true });
+assert.equal(hardButtonStore.getState().machine.mode, "manual");
+assert.equal(hardButtonStore.getState().operatorMessage, "mode blocked: home machine before AUTO");
+hardButtonStore.dispatch({ type: "GMOCAPY_HARDWARE_BUTTON", location: "bottom", index: 0, value: true });
+assert.equal(hardButtonStore.getState().machine.allHomed, true);
+hardButtonStore.dispatch({ type: "GMOCAPY_HARDWARE_BUTTON", pin: "gmoccapy.v-button.button-4", value: true });
+assert.equal(hardButtonStore.getState().machine.mode, "auto");
+hardButtonStore.dispatch({ type: "GMOCAPY_HARDWARE_BUTTON", pin: "gmoccapy.v-button.button-6", value: true });
+assert.equal(hardButtonStore.getState().operatorMessage, "gmoccapy hardware button diagnostic-only: gmoccapy.v-button.button-6");
+hardButtonStore.dispatch({ type: "GMOCAPY_HARDWARE_BUTTON", pin: "gmoccapy.h-button.button-9", value: true });
+assert.equal(hardButtonStore.getState().operatorMessage, "gmoccapy hardware button unmapped: gmoccapy.h-button.button-9");
+
+const halPinStore = createSimulationStore();
+halPinStore.dispatch({ type: "SET_PROFILE", profileId: "gmoccapy-xyzab" });
+halPinStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.ignore-limits", value: true });
+assert.equal(halPinStore.getState().gmoccapyGui.ignoreLimits, true);
+assert.equal(halPinStore.getState().operatorMessage, "gmoccapy HAL ignore-limits requested");
+halPinStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.optional-stop", value: true });
+assert.equal(halPinStore.getState().gmoccapyGui.optionalBlocks, true);
+assert.equal(halPinStore.getState().gmoccapyGui.optionalStop, false);
+assert.equal(halPinStore.getState().operatorMessage, "gmoccapy HAL optional-stop -> block delete on");
+halPinStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.blockdelete", value: true });
+assert.equal(halPinStore.getState().gmoccapyGui.optionalStop, true);
+assert.equal(halPinStore.getState().operatorMessage, "gmoccapy HAL blockdelete -> optional stop on");
+
+halPinStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.feed.feed-override.counts", value: 12 });
+assert.equal(halPinStore.getState().feed.feedOverride, 100);
+assert.equal(halPinStore.getState().gmoccapyGui.feedOverrideCounts, 12);
+assert.equal(halPinStore.getState().operatorMessage, "gmoccapy HAL feed counts synchronized");
+halPinStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.feed.feed-override.count-enable", value: true });
+halPinStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.feed.feed-override.counts", value: 17 });
+assert.equal(halPinStore.getState().feed.feedOverride, 105);
+assert.equal(halPinStore.getState().operatorMessage, "gmoccapy HAL feed counts adjusted");
+halPinStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.rapid.rapid-override.analog-enable", value: true });
+halPinStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.rapid.rapid-override.direct-value", value: 0.25 });
+assert.equal(halPinStore.getState().feed.rapidOverride, 50);
+halPinStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.spindle.spindle-override.direct-value", value: 0.5 });
+assert.equal(halPinStore.getState().spindle.override, 100);
+assert.equal(halPinStore.getState().operatorMessage, "gmoccapy HAL spindle direct-value ignored: analog disabled");
+halPinStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.spindle.spindle-override.analog-enable", value: true });
+halPinStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.spindle.spindle-override.direct-value", value: 0.5 });
+assert.equal(halPinStore.getState().spindle.override, 75);
+halPinStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.spindle.reset-spindle-override", value: false });
+assert.equal(halPinStore.getState().spindle.override, 75);
+assert.equal(halPinStore.getState().operatorMessage, "gmoccapy HAL spindle reset falling edge ignored");
+halPinStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.spindle.reset-spindle-override", value: true });
+assert.equal(halPinStore.getState().spindle.override, 100);
+assert.equal(halPinStore.getState().operatorMessage, "gmoccapy HAL spindle reset to 100");
+halPinStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.jog.jog-velocity.count-enable", value: true });
+halPinStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.jog.jog-velocity.counts", value: 2 });
+assert.equal(halPinStore.getState().gmoccapyGui.jogVelocity, 380.8);
+halPinStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.unknown", value: true });
+assert.equal(halPinStore.getState().operatorMessage, "gmoccapy HAL pin unmapped: gmoccapy.unknown");
+
+const halJogStore = createSimulationStore();
+halJogStore.dispatch({ type: "SET_PROFILE", profileId: "gmoccapy-xyzab" });
+halJogStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.jog.axis.jog-x-plus", value: true });
+assert.equal(halJogStore.getState().runState, "idle");
+assert.equal(halJogStore.getState().operatorMessage, "gmoccapy HAL jog blocked: jog blocked: machine must be on");
+halJogStore.dispatch({ type: "TOGGLE_POWER" });
+halJogStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.jog.axis.jog-x-plus", value: true });
+assert.equal(halJogStore.getState().runState, "jogging");
+assert.equal(halJogStore.getState().axisPose.x, 43);
+assert.equal(halJogStore.getState().machine.jogIncrement, 0);
+assert.equal(halJogStore.getState().gmoccapyGui.activeJogPin, "gmoccapy.jog.axis.jog-x-plus");
+assert.equal(halJogStore.getState().operatorMessage, "gmoccapy HAL jog X+ continuous");
+halJogStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.jog.axis.jog-x-plus", value: false });
+assert.equal(halJogStore.getState().runState, "idle");
+assert.equal(halJogStore.getState().gmoccapyGui.activeJogPin, null);
+assert.equal(halJogStore.getState().operatorMessage, "gmoccapy HAL jog X+ released");
+halJogStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.jog.jog-inc-2", value: false });
+assert.equal(halJogStore.getState().machine.jogIncrement, 0);
+assert.equal(halJogStore.getState().operatorMessage, "gmoccapy HAL jog increment falling edge ignored");
+halJogStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.jog.jog-inc-2", value: true });
+assert.equal(halJogStore.getState().machine.jogIncrement, 0.1);
+assert.equal(halJogStore.getState().gmoccapyGui.jogIncrementIndex, 2);
+assert.equal(halJogStore.getState().gmoccapyGui.jogIncrementOutput, 0.1);
+halJogStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.jog.axis.jog-x-plus", value: true });
+assert.equal(halJogStore.getState().axisPose.x, 43.1);
+assert.equal(halJogStore.getState().operatorMessage, "gmoccapy HAL jog X+ 0.1");
+halJogStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.jog.jog-inc-5", value: true });
+assert.equal(halJogStore.getState().machine.jogIncrement, 31.3563);
+assert.equal(halJogStore.getState().gmoccapyGui.jogIncrementLabel, "1.2345 in");
+halJogStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.jog.turtle-jog", value: true });
+assert.equal(halJogStore.getState().gmoccapyGui.turtleJog, true);
+assert.equal(halJogStore.getState().operatorMessage, "gmoccapy HAL turtle jog on");
+
+const halMessageStore = createSimulationStore();
+halMessageStore.dispatch({ type: "SET_PROFILE", profileId: "gmoccapy-xyzab" });
+halMessageStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.delete-message", value: false });
+assert.equal(halMessageStore.getState().gmoccapyGui.deletedMessageCount, 0);
+assert.equal(halMessageStore.getState().operatorMessage, "gmoccapy HAL delete-message falling edge ignored");
+halMessageStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.jog.axis.jog-y-minus", value: true });
+assert.equal(halMessageStore.getState().operatorMessage, "gmoccapy HAL jog blocked: jog blocked: machine must be on");
+halMessageStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.delete-message", value: true });
+assert.equal(halMessageStore.getState().gmoccapyGui.error, false);
+assert.equal(halMessageStore.getState().gmoccapyGui.deletedMessageCount, 1);
+assert.equal(halMessageStore.getState().operatorMessage, "gmoccapy HAL delete-message cleared alert");
+halMessageStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.warning-confirm", value: true });
+assert.equal(halMessageStore.getState().gmoccapyGui.warningConfirm, true);
+assert.equal(halMessageStore.getState().operatorMessage, "gmoccapy HAL warning-confirm asserted");
+halMessageStore.dispatch({ type: "GMOCAPY_HAL_PIN", pin: "gmoccapy.warning-confirm", value: false });
+assert.equal(halMessageStore.getState().gmoccapyGui.warningConfirm, false);
+assert.equal(halMessageStore.getState().operatorMessage, "gmoccapy HAL warning-confirm cleared");
+
console.log("gmoccapy_xyzab_gates_smoke=ok");
diff --git a/web-rtcp-5axis-sim-plan/tests/node/verify_gmoccapy_xyzab_profile.mjs b/web-rtcp-5axis-sim-plan/tests/node/verify_gmoccapy_xyzab_profile.mjs
index 9b7c4ae..c66efe5 100644
--- a/web-rtcp-5axis-sim-plan/tests/node/verify_gmoccapy_xyzab_profile.mjs
+++ b/web-rtcp-5axis-sim-plan/tests/node/verify_gmoccapy_xyzab_profile.mjs
@@ -41,13 +41,46 @@ assert.equal(profile.jointConfig[4].homeOffset, 45);
assert.equal(profile.hal.halcmd.feedbackNets.length, 5);
assert.equal(profile.hal.halcmd.offsetNets.some((net) => net.target === "gmoccapy.tooloffset-z"), true);
assert.equal(profile.hal.halcmd.simulationLoops.some((net) => net.signal === "tool-change-loop"), true);
+assert.equal(profile.hal.halcmd.toolChange.strategy, "iocontrol-loopback");
+assert.equal(profile.hal.halcmd.toolChange.manualGmoccapyPinsConnected, false);
+assert.deepEqual(profile.hal.halcmd.toolChange.postguiUnlinks, [
+ "iocontrol.0.tool-change",
+ "iocontrol.0.tool-changed",
+ "iocontrol.0.tool-prep-number",
+]);
+assert.equal(profile.hal.halcmd.toolChange.commentedManualGuiNets.length, 3);
+assert.equal(profile.hal.halcmd.toolChange.activeLoop.signal, "tool-change-loop");
assert.equal(profile.halPins.includes("gmoccapy.program.current-line"), true);
assert.equal(profile.halPins.includes("gmoccapy.spindle_at_speed_led"), true);
+assert.equal(profile.halPins.includes("gmoccapy.jog.axis.jog-x-plus"), true);
+assert.equal(profile.halPins.includes("gmoccapy.jog.axis.jog-y-minus"), true);
+assert.equal(profile.halPins.includes("gmoccapy.jog.axis.jog-b-minus"), true);
+assert.equal(profile.halPins.includes("gmoccapy.jog.jog-inc-0"), true);
+assert.equal(profile.halPins.includes("gmoccapy.jog.jog-inc-5"), true);
+assert.equal(profile.halPins.includes("gmoccapy.jog.jog-increment"), true);
+assert.equal(profile.halPins.includes("gmoccapy.jog.turtle-jog"), true);
+assert.equal(profile.halPins.includes("gmoccapy.ignore-limits"), true);
+assert.equal(profile.halPins.includes("gmoccapy.optional-stop"), true);
+assert.equal(profile.halPins.includes("gmoccapy.blockdelete"), true);
+assert.equal(profile.halPins.includes("gmoccapy.feed.feed-override.count-enable"), true);
+assert.equal(profile.halPins.includes("gmoccapy.feed.reset-feed-override"), true);
+assert.equal(profile.halPins.includes("gmoccapy.spindle.reset-spindle-override"), true);
+assert.equal(profile.halPins.includes("gmoccapy.jog.jog-velocity.direct-value"), true);
+assert.equal(profile.halPins.includes("gmoccapy.delete-message"), true);
+assert.equal(profile.halPins.includes("gmoccapy.warning-confirm"), true);
assert.equal(profile.toolTable.toolCount, 17);
assert.equal(profile.macros.includes("go_to_position X-pos Y-pos Z-pos"), true);
assert.equal(profile.panelSchema.id, "gmoccapy-xyzab-hal-component");
assert.equal(profile.panelSchema.boundary, "gmoccapy_hal_component_reference_only");
assert.equal(profile.panelSchema.groups.length, 2);
+const taskModeControls = profile.panelSchema.groups.find((group) => group.id === "task-mode-actions").controls;
+assert.deepEqual(taskModeControls.map((control) => [control.id, control.halpin]), [
+ ["tbtn-estop", "gmoccapy.v-button.button-0"],
+ ["tbtn-on", "gmoccapy.v-button.button-1"],
+ ["rbt-manual", "gmoccapy.v-button.button-2"],
+ ["rbt-mdi", "gmoccapy.v-button.button-3"],
+ ["rbt-auto", "gmoccapy.v-button.button-4"],
+]);
assert.deepEqual(fiveAxisProfiles.map(({ id }) => id), ["xyzac-trt", "xyzbc-trt", "gmoccapy-xyzab"]);
diff --git a/web-rtcp-5axis-sim-plan/tests/node/verify_profile_boundary.mjs b/web-rtcp-5axis-sim-plan/tests/node/verify_profile_boundary.mjs
index dda20aa..5adf821 100644
--- a/web-rtcp-5axis-sim-plan/tests/node/verify_profile_boundary.mjs
+++ b/web-rtcp-5axis-sim-plan/tests/node/verify_profile_boundary.mjs
@@ -67,6 +67,8 @@ assert.equal(gmoccapyXyzabProfile.tcpCapable, false);
assert.equal(gmoccapyXyzabProfile.rtcpProof, false);
assert.equal(gmoccapyXyzabProfile.promotionAllowed, false);
assert.equal(gmoccapyXyzabProfile.hal.postguiRequiresHalcompReady, true);
+assert.equal(gmoccapyXyzabProfile.hal.halcmd.toolChange.strategy, "iocontrol-loopback");
+assert.equal(gmoccapyXyzabProfile.hal.halcmd.toolChange.manualGmoccapyPinsConnected, false);
const gmoccapySourceSummary = createProfileSourceReferenceSummary("gmoccapy-xyzab");
assert.equal(gmoccapySourceSummary.referenceCount >= 10, true);
assert.ok(gmoccapySourceSummary.references.some((reference) => reference.path.endsWith("gmoccapy_XYZAB.ini")));
diff --git a/work/working5/03-推进台账.md b/work/working5/03-推进台账.md
index 08f1578..78cf332 100644
--- a/work/working5/03-推进台账.md
+++ b/work/working5/03-推进台账.md
@@ -57,3 +57,151 @@
下一步是什么:
- 下一轮可继续按任务矩阵深入核对 Tool/M6/M61 手动换刀对话框、限位 override、optional stop/block delete HAL pin 交叉行为等更细粒度 gmoccapy 行为。
+
+## 2026-06-26 R3:细化 gmoccapy XYZAB 换刀 HAL 语义
+
+做了什么:
+
+- 对照 `gmoccapy_postgui.hal`,确认该 XYZAB 仿真配置在 GUI ready 后执行 `unlinkp iocontrol.0.tool-change`、`unlinkp iocontrol.0.tool-changed`、`unlinkp iocontrol.0.tool-prep-number`,并将 `gmoccapy.toolchange-*` 手动换刀 GUI nets 保持注释状态。
+- 在 Web profile 和 HAL 诊断模型中显式记录换刀策略为 `iocontrol-loopback`,手动 gmoccapy 换刀 GUI pins 未连接,活动回环为 `iocontrol.0.tool-change -> iocontrol.0.tool-changed`。
+- 在 gmoccapy UI 诊断页新增 `Tool change` 行,显示换刀策略、手动 GUI pin 连接状态、POSTGUI `unlinkp` 数量和注释掉的手动换刀 nets 数量。
+- 补充 Node 与浏览器断言,防止后续误把此配置表述为需要 gmoccapy 手动换刀确认对话框。
+
+改了哪些文件:
+
+- `/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app/src/profiles/gmoccapy-xyzab.js`
+- `/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app/src/runtime/gmoccapy-hal-model.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_hal_model.mjs`
+- `/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/tests/node/verify_gmoccapy_xyzab_profile.mjs`
+- `/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/tests/node/verify_profile_boundary.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_hal_model.mjs && node ../tests/node/verify_gmoccapy_xyzab_profile.mjs && node ../tests/node/verify_profile_boundary.mjs` 通过。
+- `npm run smoke:node` 通过。
+- `npm run build && npm run smoke` 通过。
+
+下一步是什么:
+
+- 可继续核对限位 override、optional stop/block delete、外部 HAL hard-button pin 与 UI action 的交叉行为。
+
+## 2026-06-26 R4:细化外部 HAL hard-button pin 与 Web UI action 映射
+
+做了什么:
+
+- 对照 `gmoccapy.py` 的 `_make_hal_pins()`、`_button_pin_changed()`、`_get_child_button()` 和 `gmoccapy.glade` 的 `vbtb_main` 顺序,确认外部硬按钮 pin 只响应 rising edge,并按当前可见且敏感的 GTK 按钮位置触发;目标隐藏或不敏感时忽略。
+- 修正 `gmoccapy-xyzab` panel schema 中右侧主按钮 HAL pin 顺序:`tbtn_estop`/`tbtn_on`/`rbt_manual`/`rbt_mdi`/`rbt_auto` 对应 `gmoccapy.v-button.button-0..4`,不再把 Estop/Power 误列为 `h-button`。
+- 在 HAL model 和 communication model 中增加 hard-button 结构化映射,记录 7 个右侧 `v-button` pin、底部主页面 `h-button` 引用、rising-edge 语义、insensitive ignored 语义和 Web selector/action 对应关系。
+- 在 store 中新增 `GMOCAPY_HARDWARE_BUTTON` action,用同一套 Web action/gate 模拟外部 HAL hard-button pin;falling edge、未映射 pin 和诊断-only pin 均给出明确 operator message。
+- 在 UI 按钮 DOM 上新增 `data-gmoccapy-hal-pin`、`data-gmoccapy-native-panel`、`data-gmoccapy-native-button-index`,并在 gmoccapy HAL 诊断中显示 hard-button 摘要。
+- 补充 Node 和浏览器断言,覆盖 hard-button 模型、通信路径、profile pin 顺序、store gate 行为和 DOM 映射。
+
+改了哪些文件:
+
+- `/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app/src/profiles/gmoccapy-xyzab.js`
+- `/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app/src/runtime/gmoccapy-communication-model.js`
+- `/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app/src/runtime/gmoccapy-hal-model.js`
+- `/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app/src/state/store.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_communication_model.mjs`
+- `/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/tests/node/verify_gmoccapy_hal_model.mjs`
+- `/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_gmoccapy_xyzab_profile.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_hal_model.mjs && node ../tests/node/verify_gmoccapy_communication_model.mjs && node ../tests/node/verify_gmoccapy_xyzab_profile.mjs && node ../tests/node/verify_gmoccapy_xyzab_gates.mjs` 通过。
+- `npm run smoke:node` 通过。
+- `npm run build && npm run smoke` 通过。
+
+下一步是什么:
+
+- 可继续核对 `gmoccapy.ignore-limits`、`gmoccapy.optional-stop`、`gmoccapy.blockdelete`、override analog/count/reset pins 与 Web 状态的交叉行为。
+
+## 2026-06-27 R5:细化 gmoccapy HAL 输入 pin 与倍率 pin 语义
+
+做了什么:
+
+- 对照 `gmoccapy.py` 的 `_ignore_limits()`、`_optional_blocks()`、`_blockdelete()`、`_on_counts_changed()`、`_on_analog_value_changed()`、`_reset_override()` 和 `gmoccapy_XYZAB.pref`,确认本轮应保留 `optional-stop`/`blockdelete` 的交叉语义,以及 counts/direct-value/reset 的 enable 和 rising-edge 规则。
+- 在 Web store 中新增 `GMOCAPY_HAL_PIN` action,统一模拟 `gmoccapy.ignore-limits`、`gmoccapy.optional-stop`、`gmoccapy.blockdelete`、feed/rapid/spindle/jog override counts、analog direct-value 和 reset pins。
+- 在 `gmoccapyGui` 状态中记录 ignore limits、block delete、optional stop、override enable/counts、jog velocity 和最后一次 HAL pin 效果。
+- 扩展 profile、HAL model、communication model 和 UI 诊断,明确 `optional-stop -> block delete`、`blockdelete -> optional stop`、counts 需要 count-enable、direct-value 需要 analog-enable、reset 只响应 rising edge。
+- 在 override 面板增加 ignore limits、block delete、optional stop 和 100% override reset 控件,并补充浏览器 DOM/状态断言。
+
+改了哪些文件:
+
+- `/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app/src/profiles/gmoccapy-xyzab.js`
+- `/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app/src/runtime/gmoccapy-communication-model.js`
+- `/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app/src/runtime/gmoccapy-hal-model.js`
+- `/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app/src/state/store.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/app/src/styles/gmoccapy.css`
+- `/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/tests/node/verify_gmoccapy_communication_model.mjs`
+- `/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/tests/node/verify_gmoccapy_hal_model.mjs`
+- `/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_gmoccapy_xyzab_profile.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_hal_model.mjs && node ../tests/node/verify_gmoccapy_communication_model.mjs && node ../tests/node/verify_gmoccapy_xyzab_profile.mjs && node ../tests/node/verify_gmoccapy_xyzab_gates.mjs && bash ../tests/browser/verify_gmoccapy_shell_browser.sh` 通过。
+- `npm run smoke:node` 通过。
+- `npm run build && npm run smoke` 通过。
+
+下一步是什么:
+
+- 可继续核对 `gmoccapy.jog.axis.*`、`gmoccapy.jog.jog-inc-*`、`gmoccapy.delete-message`、`gmoccapy.warning-confirm` 等剩余 HAL pin 与 Web 状态/消息系统的映射。
+
+## 2026-06-27 R6:细化 gmoccapy jog 与消息 HAL pin 语义
+
+做了什么:
+
+- 对照 `gmoccapy.py` 的 `_on_pin_jog_changed()`、`_on_pin_incr_changed()`、`_on_pin_turtle_jog()`、`_del_message_changed()` 和 `dialogs.warning_dialog()`,确认 jog axis pin 是按下/释放电平语义,jog increment pin 只响应 true/rising edge,`jog-inc-0` 为连续点动,`delete-message` 只在 true 时删除消息,`warning-confirm` 是活动 warning dialog 的轮询确认 pin。
+- 在 `gmoccapy-xyzab` profile 中补齐 XYZAB 的 10 个 axis jog pins、`jog-inc-0..5`、`jog.jog-increment`、`jog.turtle-jog`、`delete-message` 和 `warning-confirm`。
+- 扩展 HAL model 和 communication model,记录 jog axis、jog increment、turtle jog、message pins 的源码回调、summary 数量和 Web 映射。
+- 扩展 `GMOCAPY_HAL_PIN`:模拟 jog axis press/release、jog increment rising-edge 选择、turtle jog level、delete-message falling-edge 忽略和 warning-confirm level 状态。
+- 在 gmoccapy UI 诊断中新增 `HAL jog` 和 `HAL messages` 行,并补充 Node/浏览器断言。
+
+改了哪些文件:
+
+- `/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app/src/profiles/gmoccapy-xyzab.js`
+- `/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app/src/runtime/gmoccapy-communication-model.js`
+- `/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app/src/runtime/gmoccapy-hal-model.js`
+- `/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app/src/state/store.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_communication_model.mjs`
+- `/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/tests/node/verify_gmoccapy_hal_model.mjs`
+- `/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_gmoccapy_xyzab_profile.mjs`
+- `/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_hal_model.mjs && node ../tests/node/verify_gmoccapy_communication_model.mjs` 通过。
+- `node ../tests/node/verify_gmoccapy_xyzab_profile.mjs && node ../tests/node/verify_gmoccapy_xyzab_gates.mjs` 通过。
+- `bash ../tests/browser/verify_gmoccapy_shell_browser.sh` 通过。
+- `npm run smoke:node` 通过。
+- `npm run build && npm run smoke` 通过。
+
+下一步是什么:
+
+- 可继续核对 `gmoccapy.unlock-settings`、tool measurement/blockheight/probeheight、INI user messages 等更低频 HAL pins;本轮 W5-190 已完成。
diff --git a/work/working5/04-任务矩阵.md b/work/working5/04-任务矩阵.md
index dccc79a..efa4fdd 100644
--- a/work/working5/04-任务矩阵.md
+++ b/work/working5/04-任务矩阵.md
@@ -13,4 +13,8 @@
| 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-160 | 细化 M6/M61 换刀 HAL 语义 | 完成 | profile、HAL model 和 UI 诊断明确 `gmoccapy_postgui.hal` 断开手动换刀 GUI pins,并使用 `iocontrol.0.tool-change -> iocontrol.0.tool-changed` 回环;相关 Node、构建、浏览器 smoke 通过 |
+| W5-170 | 细化外部 HAL hard-button pin 与 UI action 映射 | 完成 | profile/panel schema、HAL model、communication model、store 和浏览器 DOM 均明确 `gmoccapy.v-button.button-0..6`、`gmoccapy.h-button.button-0` 等硬件按钮 pin 的 rising-edge、可见敏感按钮映射和 Web gate 行为;相关 Node、构建、浏览器 smoke 通过 |
+| W5-180 | 细化 ignore-limits、optional-stop、blockdelete 与 override HAL input pin 语义 | 完成 | profile、HAL model、communication model、store 和浏览器 UI 均明确 `gmoccapy.ignore-limits`、`gmoccapy.optional-stop`、`gmoccapy.blockdelete`、override counts/direct-value/reset pins 的 Web 映射;Node、构建、浏览器 smoke 通过 |
+| W5-190 | 细化 jog、jog-increment、delete-message、warning-confirm HAL input pin 语义 | 完成 | profile、HAL model、communication model、store 和浏览器 UI 均明确 `gmoccapy.jog.axis.*`、`gmoccapy.jog.jog-inc-*`、`gmoccapy.jog.turtle-jog`、`gmoccapy.delete-message`、`gmoccapy.warning-confirm` 的 Web 映射;Node、构建、浏览器 smoke 通过 |
| W5-900 | 更新验收证据和决策记录 | 完成 | 命令、结果、修改原因和后续项已记录 |
diff --git a/work/working5/05-验收证据.md b/work/working5/05-验收证据.md
index 3161b04..4067a67 100644
--- a/work/working5/05-验收证据.md
+++ b/work/working5/05-验收证据.md
@@ -9,6 +9,14 @@
| 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 |
+| EV-W5-160 | W5-120/W5-160 | 命令 | `node ../tests/node/verify_gmoccapy_hal_model.mjs && node ../tests/node/verify_gmoccapy_xyzab_profile.mjs && node ../tests/node/verify_profile_boundary.mjs` | 通过,验证 `gmoccapy_postgui.hal` 换刀策略为 `iocontrol-loopback`、手动 gmoccapy 换刀 GUI pins 未连接、3 个 POSTGUI `unlinkp` 被记录 |
+| EV-W5-161 | W5-020/W5-030/W5-040/W5-160 | 命令 | `npm run smoke:node`、`npm run build && npm run smoke` | 通过,完整 Node、静态构建、浏览器 gmoccapy shell/dist smoke 均覆盖新增换刀诊断 |
+| EV-W5-170 | W5-150/W5-170 | 命令 | `node ../tests/node/verify_gmoccapy_hal_model.mjs && node ../tests/node/verify_gmoccapy_communication_model.mjs && node ../tests/node/verify_gmoccapy_xyzab_profile.mjs && node ../tests/node/verify_gmoccapy_xyzab_gates.mjs` | 通过,验证 hard-button 模型、通信路径、profile pin 顺序和 `GMOCAPY_HARDWARE_BUTTON` gate 行为 |
+| EV-W5-171 | W5-020/W5-030/W5-040/W5-170 | 命令 | `npm run smoke:node`、`npm run build && npm run smoke` | 通过,完整 Node、静态构建、浏览器 gmoccapy shell/dist smoke 均覆盖 hard-button DOM 映射和诊断 |
+| EV-W5-180 | W5-150/W5-180 | 命令 | `node ../tests/node/verify_gmoccapy_hal_model.mjs && node ../tests/node/verify_gmoccapy_communication_model.mjs && node ../tests/node/verify_gmoccapy_xyzab_profile.mjs && node ../tests/node/verify_gmoccapy_xyzab_gates.mjs && bash ../tests/browser/verify_gmoccapy_shell_browser.sh` | 通过,验证 `GMOCAPY_HAL_PIN` 对 ignore-limits、optional-stop、blockdelete、override counts/direct-value/reset pins 的映射和 UI 诊断 |
+| EV-W5-181 | W5-020/W5-030/W5-040/W5-180 | 命令 | `npm run smoke:node`、`npm run build && npm run smoke` | 通过,完整 Node、静态构建、浏览器 gmoccapy shell/dist smoke 均覆盖新增 HAL input/override 诊断和 Web 控件 |
+| EV-W5-190 | W5-150/W5-190 | 命令 | `node ../tests/node/verify_gmoccapy_hal_model.mjs && node ../tests/node/verify_gmoccapy_communication_model.mjs`、`node ../tests/node/verify_gmoccapy_xyzab_profile.mjs && node ../tests/node/verify_gmoccapy_xyzab_gates.mjs`、`bash ../tests/browser/verify_gmoccapy_shell_browser.sh` | 通过,验证 `GMOCAPY_HAL_PIN` 对 jog axis press/release、jog increment rising-edge、turtle jog、delete-message、warning-confirm pins 的映射和 UI 诊断 |
+| EV-W5-191 | W5-020/W5-030/W5-040/W5-190 | 命令 | `npm run smoke:node`、`npm run build && npm run smoke` | 通过,完整 Node、静态构建、浏览器 gmoccapy shell/dist smoke 均覆盖新增 jog/message HAL pin 诊断和 Web 行为 |
## 命令记录
@@ -64,6 +72,231 @@ node ../tests/node/verify_gmoccapy_xyzab_gates.mjs && node ../tests/node/verify_
- `run_preconditions_machine_file_smoke=ok`
- `gmoccapy_shell_smoke=ok`
+### 2026-06-26 R3
+
+```bash
+cd /home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app
+node ../tests/node/verify_gmoccapy_hal_model.mjs && node ../tests/node/verify_gmoccapy_xyzab_profile.mjs && node ../tests/node/verify_profile_boundary.mjs
+```
+
+结果:通过,输出:
+
+- `gmoccapy_hal_model_smoke=ok`
+- `gmoccapy_xyzab_profile_smoke=ok`
+- `profile_boundary_smoke=ok`
+
+```bash
+cd /home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app
+npm run smoke:node
+```
+
+结果:通过。
+
+关键输出摘要:
+
+- `linuxcnc_task_hal_runtime_smoke=ok`
+- `profile_boundary_smoke=ok`
+- `gmoccapy_xyzab_profile_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 && npm run smoke
+```
+
+结果:通过,输出:
+
+- `gmoccapy_static_build=ok`
+- `gmoccapy_shell_smoke=ok`
+- `gmoccapy_dist_smoke=ok`
+
+本轮新增浏览器断言:
+
+- `data-gmoccapy-hal="tool-change"` 必须包含 `iocontrol-loopback`。
+- 同一诊断行必须包含 `manual GUI pins disconnected`。
+- 同一诊断行必须包含 `3 postgui unlinkp`。
+
+### 2026-06-26 R4
+
+```bash
+cd /home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app
+node ../tests/node/verify_gmoccapy_hal_model.mjs && node ../tests/node/verify_gmoccapy_communication_model.mjs && node ../tests/node/verify_gmoccapy_xyzab_profile.mjs && node ../tests/node/verify_gmoccapy_xyzab_gates.mjs
+```
+
+结果:通过,输出:
+
+- `gmoccapy_hal_model_smoke=ok`
+- `gmoccapy_communication_model_smoke=ok`
+- `gmoccapy_xyzab_profile_smoke=ok`
+- `gmoccapy_xyzab_gates_smoke=ok`
+
+```bash
+cd /home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app
+npm run smoke:node
+```
+
+结果:通过。
+
+关键输出摘要:
+
+- `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 && npm run smoke
+```
+
+结果:通过,输出:
+
+- `gmoccapy_static_build=ok`
+- `gmoccapy_shell_smoke=ok`
+- `gmoccapy_dist_smoke=ok`
+
+本轮新增 Node 断言:
+
+- `gmoccapy.v-button.button-0..6` 对应 `tbtn_estop`、`tbtn_on`、`rbt_manual`、`rbt_mdi`、`rbt_auto`、`tbtn_user_tabs`、`tbtn_setup`。
+- `gmoccapy.h-button.button-0` 对应底部主页面 `btn_homing`,Web dispatch 为 `HOME`。
+- `GMOCAPY_HARDWARE_BUTTON` 忽略 falling edge;rising edge 复用现有 Web gate;未映射或诊断-only pin 给出明确 operator message。
+
+本轮新增浏览器断言:
+
+- 侧边 `estop`、`power`、`mode-manual`、`mode-mdi`、`mode-auto`、`mode-jog` 按钮必须带有对应 `data-gmoccapy-hal-pin="gmoccapy.v-button.button-N"`。
+- 底部 `HOME` 按钮必须带有 `data-gmoccapy-hal-pin="gmoccapy.h-button.button-0"`。
+- `data-gmoccapy-hal="hard-buttons"` 必须包含 `7 right v-button pins`、`4 bottom h-button main refs`、`7 mapped Web buttons`、`rising-edge-only` 和 `insensitive ignored`。
+
+### 2026-06-27 R5
+
+```bash
+cd /home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app
+node ../tests/node/verify_gmoccapy_hal_model.mjs && node ../tests/node/verify_gmoccapy_communication_model.mjs && node ../tests/node/verify_gmoccapy_xyzab_profile.mjs && node ../tests/node/verify_gmoccapy_xyzab_gates.mjs && bash ../tests/browser/verify_gmoccapy_shell_browser.sh
+```
+
+结果:通过,输出:
+
+- `gmoccapy_hal_model_smoke=ok`
+- `gmoccapy_communication_model_smoke=ok`
+- `gmoccapy_xyzab_profile_smoke=ok`
+- `gmoccapy_xyzab_gates_smoke=ok`
+- `gmoccapy_shell_smoke=ok`
+
+```bash
+cd /home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app
+npm run smoke:node
+```
+
+结果:通过。
+
+关键输出摘要:
+
+- `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 && npm run smoke
+```
+
+结果:通过,输出:
+
+- `gmoccapy_static_build=ok`
+- `gmoccapy_shell_smoke=ok`
+- `gmoccapy_dist_smoke=ok`
+
+本轮新增 Node 断言:
+
+- `gmoccapy.ignore-limits` 设置 `gmoccapyGui.ignoreLimits`,active 时记录 `command.override_limits()` 请求语义。
+- `gmoccapy.optional-stop` 按源码 `_optional_blocks()` 交叉驱动 `optionalBlocks`/block delete;`gmoccapy.blockdelete` 按 `_blockdelete()` 交叉驱动 `optionalStop`。
+- override counts 在 count-enable 为 false 时只同步 counts;count-enable 为 true 时按 `gmoccapy_XYZAB.pref` 中 scale 调整 feed/rapid/spindle/jog velocity。
+- direct-value 在 analog-enable 为 false 时忽略;analog-enable 为 true 时按 0..1 比例映射滑块范围。
+- reset pins 忽略 falling edge,rising edge 将 feed/rapid/spindle override 复位到 100。
+
+本轮新增浏览器断言:
+
+- `data-gmoccapy-hal="operator-inputs"` 必须包含 `3 operator pins`、`optional-stop -> block delete`、`blockdelete -> optional stop`。
+- `data-gmoccapy-hal="override-inputs"` 必须包含 `4 override targets`、`counts require enable`、`direct-value requires analog-enable`、`reset rising-edge-only`。
+- Web override 面板的 feed/spindle 100% reset、ignore limits、block delete、optional stop 控件必须更新 store 状态和最后一次 HAL input DOM。
+- 通过 `window.webRtcp5AxisSimulation.dispatch({ type: "GMOCAPY_HAL_PIN", ... })` 验证浏览器内 optional/blockdelete 交叉 pin 行为。
+
+### 2026-06-27 R6
+
+```bash
+cd /home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app
+node ../tests/node/verify_gmoccapy_hal_model.mjs && node ../tests/node/verify_gmoccapy_communication_model.mjs
+```
+
+结果:通过,输出:
+
+- `gmoccapy_hal_model_smoke=ok`
+- `gmoccapy_communication_model_smoke=ok`
+
+```bash
+cd /home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app
+node ../tests/node/verify_gmoccapy_xyzab_profile.mjs && node ../tests/node/verify_gmoccapy_xyzab_gates.mjs
+```
+
+结果:通过,输出:
+
+- `gmoccapy_xyzab_profile_smoke=ok`
+- `gmoccapy_xyzab_gates_smoke=ok`
+
+```bash
+cd /home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app
+bash ../tests/browser/verify_gmoccapy_shell_browser.sh
+```
+
+结果:通过,输出:
+
+- `gmoccapy_shell_smoke=ok`
+
+```bash
+cd /home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app
+npm run smoke:node
+```
+
+结果:通过。
+
+关键输出摘要:
+
+- `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 && npm run smoke
+```
+
+结果:通过,输出:
+
+- `gmoccapy_static_build=ok`
+- `gmoccapy_shell_smoke=ok`
+- `gmoccapy_dist_smoke=ok`
+
+本轮新增 Node 断言:
+
+- `gmoccapy.jog.axis.jog-x/y/z/a/b-{plus,minus}` 共 10 个 axis jog pins 按 press/release 电平进入和退出 jog 状态。
+- `gmoccapy.jog.jog-inc-0..5` 只响应 true/rising edge,`jog-inc-0` 为连续点动,`jog-inc-5` 按 `1.2345 in` 映射为 31.3563 mm。
+- `gmoccapy.jog.turtle-jog` 为 level-driven 状态。
+- `gmoccapy.delete-message` falling edge 忽略,true 时删除消息并清除 alert/error 语义。
+- `gmoccapy.warning-confirm` 记录 active warning dialog 的 confirm pin level。
+
+本轮新增浏览器断言:
+
+- `data-gmoccapy-hal="jog-inputs"` 必须包含 `10 axis jog pins`、`6 jog-inc pins`、`press/release level`、`jog-inc rising-edge-only`、`jog-inc-0 continuous`。
+- `data-gmoccapy-hal="message-inputs"` 必须包含 `3 message pins`、`delete-message rising-edge-only`、`warning-confirm level-polled`。
+- 通过 `window.webRtcp5AxisSimulation.dispatch({ type: "GMOCAPY_HAL_PIN", ... })` 验证浏览器内 jog increment、axis jog press/release、turtle jog、delete-message 和 warning-confirm 状态。
+
## 页面/截图/report/job 记录
本轮未生成截图、PDF、job_id 或 report_id。浏览器验收由 headless Chromium smoke 脚本完成。
diff --git a/work/working5/06-决策记录.md b/work/working5/06-决策记录.md
index 92bd29d..a3b7186 100644
--- a/work/working5/06-决策记录.md
+++ b/work/working5/06-决策记录.md
@@ -40,3 +40,99 @@
- `app/src/state/linuxcnc-task-policy.js` 成为模式切换和执行动作的统一互锁入口。
- `app/src/ui/gmoccapy-shell.js` 侧边模式按钮使用同一 gate 输出 disabled、`data-command-ready` 和 `aria-disabled`。
- Node 和浏览器测试增加下电、未回零、已回零三个阶段的断言。
+
+## DR-W5-003:XYZAB 换刀按 POSTGUI iocontrol 回环建模,不实现手动换刀弹窗
+
+日期:2026-06-26
+
+决策:
+
+- `gmoccapy-xyzab` profile 和 `gmoccapyHalModel` 将 M6/M61 换刀执行路径标记为 `iocontrol-loopback`。
+- `manualGmoccapyPinsConnected = false`,用于说明 `gmoccapy.toolchange-change`、`gmoccapy.toolchange-changed`、`gmoccapy.toolchange-number` 在该配置中没有作为活动 HAL net 接入。
+- UI 只显示换刀 HAL 诊断,不弹出 gmoccapy 手动换刀确认流程。
+
+原因:
+
+- `gmoccapy_postgui.hal` 在 GUI HAL pins 创建后执行 `unlinkp iocontrol.0.tool-change`、`unlinkp iocontrol.0.tool-changed`、`unlinkp iocontrol.0.tool-prep-number`。
+- 同一文件里连接 `gmoccapy.toolchange-*` 的手动换刀 nets 保持注释状态。
+- 活动换刀完成路径是 `net tool-change-loop iocontrol.0.tool-change => iocontrol.0.tool-changed`,与 `core_sim_XYZAB.hal` 的仿真换刀回环语义一致。
+
+影响:
+
+- HAL/profile 诊断现在能区分“存在 gmoccapy toolchange pins”与“此配置没有把这些 pins 接入手动换刀流程”。
+- `verify_gmoccapy_hal_model.mjs`、`verify_gmoccapy_xyzab_profile.mjs`、`verify_profile_boundary.mjs` 和浏览器 smoke 均覆盖该换刀语义。
+
+## DR-W5-004:外部 HAL hard-button pin 使用显式映射,不依赖 Web DOM 顺序
+
+日期:2026-06-26
+
+决策:
+
+- Web 侧将 `gmoccapy.h-button.*`、`gmoccapy.v-button.*` 建模为显式 pin 到 Web action/selector 的映射。
+- `GMOCAPY_HARDWARE_BUTTON` action 只模拟 rising edge;falling edge 忽略;可执行 pin 复用原有 Web action 和 `linuxcnc-task-policy` gate。
+- Web DOM 使用 `data-gmoccapy-hal-pin`、`data-gmoccapy-native-panel`、`data-gmoccapy-native-button-index` 标记原生 hard-button 对应关系,而不是依赖当前页面按钮的 DOM 顺序。
+- 未实现的 native 页面按钮,如 `tbtn_setup`、`btn_touch`、`btn_tool`、`tbtn_switch_mode`,只保留诊断映射,不伪装成已实现控制。
+
+原因:
+
+- LinuxCNC `gmoccapy.py` 的 `_button_pin_changed()` 只响应 pin 为 true 的 rising edge。
+- `_get_child_button()` 按当前 notebook 页或 `vbtb_main` 的可见非 label 子控件位置查找按钮,并在目标不敏感时忽略。
+- `gmoccapy.glade` 中 `vbtb_main` 的原生顺序是 `tbtn_estop`、`tbtn_on`、`rbt_manual`、`rbt_mdi`、`rbt_auto`、`tbtn_user_tabs`、`tbtn_setup`,Web shell 右侧还包含 reset/TCP 等非原生布局控件,直接用 DOM 顺序会误表达 native 语义。
+
+影响:
+
+- `gmoccapy-xyzab` panel schema 修正 Estop/Power/Manual/MDI/Auto 的 `v-button` pin 顺序。
+- HAL model、communication model、store、UI DOM 和浏览器 smoke 都能追踪 hard-button pin 到 Web action/gate 的行为。
+- 未完成的 optional-stop、blockdelete、ignore-limits 等 pin 仍作为后续独立任务处理。
+
+## DR-W5-005:HAL input pin 按 gmoccapy 回调语义建模,不按名称直连
+
+日期:2026-06-27
+
+决策:
+
+- Web 侧新增 `GMOCAPY_HAL_PIN` action,用于模拟外部 HAL 输入 pin 对 gmoccapy GUI 状态和命令的影响。
+- `gmoccapy.ignore-limits` 设置 `chk_ignore_limits` 等效状态,active 时记录 `command.override_limits()` 请求语义。
+- `gmoccapy.optional-stop` 不直连 optional stop,而是按源码 `_optional_blocks()` 驱动 `tbtn_optional_blocks`,也就是 block delete。
+- `gmoccapy.blockdelete` 不直连 block delete,而是按源码 `_blockdelete()` 调用 `command.set_optional_stop()`。
+- override counts 只有对应 `*.count-enable` 为 true 时才调整滑块;否则只同步 counts 基线。
+- override direct-value 只有对应 `*.analog-enable` 为 true 时才接管滑块;reset pins 只响应 true/rising edge。
+
+原因:
+
+- `gmoccapy.py` 中 `optional-stop` pin 连接 `_optional_blocks()`,该回调设置 `tbtn_optional_blocks`,随后 `on_tbtn_optional_blocks_toggled()` 调用 `command.set_block_delete()`。
+- `gmoccapy.py` 中 `blockdelete` pin 连接 `_blockdelete()`,该回调调用 `command.set_optional_stop()`。
+- `_on_counts_changed()`、`_on_analog_value_changed()` 和 `_reset_override()` 明确要求 enable/rising-edge 条件;`gmoccapy_XYZAB.pref` 给出本配置默认 scale:feed/rapid/spindle 为 1,jog velocity 为 140.4。
+- 如果按 pin 名称直接连同名 Web 状态,会掩盖 gmoccapy 原生回调的交叉行为和 enable 条件。
+
+影响:
+
+- `app/src/state/store.js` 新增 `gmoccapyGui` 状态与 `GMOCAPY_HAL_PIN` 分发。
+- `gmoccapyHalModel` 和 `gmoccapyCommunicationModel` 现在显式记录 operator input pins、override pins、enable 规则和 reset 规则。
+- UI override 面板提供 ignore limits、block delete、optional stop 和 100% reset 控件,诊断区显示 operator input 与 override input 语义。
+- Node 和浏览器 smoke 均覆盖 HAL input/override 语义,防止后续把交叉 pin 改成同名直连。
+
+## DR-W5-006:jog 与消息 HAL pin 按 gmoccapy 回调边界建模
+
+日期:2026-06-27
+
+决策:
+
+- `gmoccapy.jog.axis.jog-*-plus/minus` 作为按下/释放电平输入处理:pin true 触发 jog pressed,pin false 触发 jog released。
+- `gmoccapy.jog.jog-inc-N` 只响应 true/rising edge;`jog-inc-0` 表示 continuous jog,`jog-inc-1..5` 映射 `gmoccapy_XYZAB.ini` 的 `[DISPLAY] INCREMENTS`。
+- `gmoccapy.jog.turtle-jog` 作为 level-driven 状态记录,不伪装成 Web 普通倍率按钮。
+- `gmoccapy.delete-message` 只在 true 时执行删除消息语义,falling edge 忽略。
+- `gmoccapy.warning-confirm` 作为 warning dialog 轮询确认 pin 记录 level,不强行制造不存在的 Web modal 流程。
+
+原因:
+
+- `gmoccapy.py` 中 `_on_pin_jog_changed()` 明确在 pin true 时调用 `_on_btn_jog_pressed()`,在 pin false 时调用 `_on_btn_jog_released()`;这与 hard-button 的 rising-edge-only 语义不同。
+- `_on_pin_incr_changed()` 对 `not pin.get()` 直接 return,因此 jog increment pin 只响应 true;`_make_jog_increments()` 会把 continuous 的 `rbt_0` 放在第一位,`get_increments()` 再把 INI 的 5 个增量值追加到后面。
+- `_del_message_changed()` 只在 pin true 时删除 alert 或最后一条 notification;`dialogs.warning_dialog()` 每 100ms 轮询 `warning-confirm` pin 来接受活动 warning dialog。
+- 如果把这些 pin 当作普通 Web click 或同名状态直连,会混淆 gmoccapy 源码中 “硬按钮”、“jog pin”、“message pin” 三类不同输入边界。
+
+影响:
+
+- `GMOCAPY_HAL_PIN` 现在覆盖 jog axis press/release、jog increment selection、turtle jog、delete-message 和 warning-confirm。
+- HAL model 和 communication model 现在能分别呈现 hard-button、operator/override input、jog input、message input 的不同语义。
+- UI 诊断增加 `HAL jog` 和 `HAL messages`,浏览器 smoke 通过 DOM 和公开 dispatch 验证这些语义。
diff --git a/备忘/备忘3.txt b/备忘/备忘3.txt
index d8b7eb7..1aad29f 100644
--- a/备忘/备忘3.txt
+++ b/备忘/备忘3.txt
@@ -1,8 +1,23 @@
-对标“/home/meswork/cnc_wams/work/working3/gmoccapy_XYZAB_execution_analysis.md“的五轴机床的执行流程,执行、测试和完善”/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan“这个从linuxcnc移植过来的数控系统仿真的功能。在数控系统仿真仿真功能方面,与linuxcnc的源程序实现的功能完全一直。不一致的地方,进行修改”/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan“这个项目。先在“/home/meswork/cnc_wams/work/working5”中创建如下文件,之后,按如下文件工作。
+对标“/home/meswork/cnc_wams/work/working3/gmoccapy_XYZAB_execution_analysis.md“的五轴机床的执行流程,
+执行、测试和完善”/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan“这个从linuxcnc移植过来的数控系统仿真的功能。
+在数控系统仿真仿真功能方面,与linuxcnc的源程序实现的功能完全一直。
+不一致的地方,进行修改”/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan“这个项目。
+先在“/home/meswork/cnc_wams/work/working5”中创建如下文件,之后,按如下文件工作。
+
01-项目功能内容
02-项目程序开发详细步骤
03-推进台账:每轮做了什么、改了哪些文件、验证了什么、下一步是什么。
04-任务矩阵:所有任务编号、状态、验收标准,防止重复做。
05-验收证据:命令、页面、job_id、report_id、PDF、截图等证据。
06-决策记录:为什么这么做,后续变更时不反复争论同一个问题
-另加 README 索引,便于后续按任务矩阵继续推进。
\ No newline at end of file
+另加 README 索引,便于后续按任务矩阵继续推进。
+
+## Logging Rule
+
+After every GPT/Codex execution completes, append the full execution process
+log to:
+
+`/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md`
+
+
+代码区也有一批未提交改动
\ No newline at end of file