完善 working5 gmoccapy HAL 输入对标

This commit is contained in:
2026-06-26 22:07:41 -04:00
parent 0d79b3545b
commit 917cec15ad
20 changed files with 2853 additions and 27 deletions

View File

@@ -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,

View File

@@ -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,
};

View File

@@ -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;
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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) })}
<button type="button" class="sidebar-button" data-action="kins-identity" data-active="${state.kinsType === "identity"}">IDENTITY</button>
<button type="button" class="sidebar-button" data-action="kins-tcp" data-active="${state.kinsType.startsWith("tcp-")}"${tcpDisabled}>TCP</button>
<time>13:30:31<br />20.06.2026</time>
@@ -449,8 +449,14 @@ function renderInfoTabs(element, state) {
<dt>HAL model:</dt><dd data-gmoccapy-hal="boundary">${gmoccapyHalModel.semanticBoundary}</dd>
<dt>HAL files:</dt><dd data-gmoccapy-hal="files">${halSummary.halFileCount} files / POSTGUI ${halSummary.postguiRequiresHalcompReady ? "after halcomp.ready" : "pending"}</dd>
<dt>HAL pins:</dt><dd data-gmoccapy-hal="pins">${halSummary.nativePinGroupCount} groups / ${halSummary.nativePinCount} pins</dd>
<dt>Hard buttons:</dt><dd data-gmoccapy-hal="hard-buttons">${formatGmoccapyHardButtons(halSummary)}</dd>
<dt>HAL jog:</dt><dd data-gmoccapy-hal="jog-inputs">${formatGmoccapyJogInputs(halSummary, state)}</dd>
<dt>HAL inputs:</dt><dd data-gmoccapy-hal="operator-inputs">${formatGmoccapyHalInputs(halSummary, state)}</dd>
<dt>HAL overrides:</dt><dd data-gmoccapy-hal="override-inputs">${formatGmoccapyOverrideInputs(halSummary, state)}</dd>
<dt>HAL messages:</dt><dd data-gmoccapy-hal="message-inputs">${formatGmoccapyMessageInputs(halSummary, state)}</dd>
<dt>postgui nets:</dt><dd data-gmoccapy-hal="postgui">${gmoccapyHalModel.postguiNets.map((net) => net.signal).join(" / ")}</dd>
<dt>Tool loop:</dt><dd data-gmoccapy-hal="tool-loop">${halSummary.hasToolChangeSimulationLoop ? "simulated tool-change loop" : "pending"}</dd>
<dt>Tool change:</dt><dd data-gmoccapy-hal="tool-change">${formatGmoccapyToolChange(halSummary)}</dd>
<dt>Operator:</dt><dd data-operator-message>${escapeHtml(state.operatorMessage)}</dd>
</dl>
`;
@@ -597,6 +603,15 @@ function renderOverride(element, state, dispatch) {
</section>
${overrideControl("Rapid Override", "rapid", state.feed.rapidOverride)}
${overrideControl("Feed Rate", "feed", state.feed.feedOverride, `F ${state.feed.feedRate}`)}
<section class="override-control" data-control="gmoccapy-hal-inputs">
<h2>HAL Inputs</h2>
<div class="toggle-row">
<button type="button" data-action="ignore-limits" data-active="${state.gmoccapyGui.ignoreLimits}">Limits</button>
<button type="button" data-action="block-delete" data-active="${state.gmoccapyGui.optionalBlocks}">/ Block</button>
<button type="button" data-action="optional-stop" data-active="${state.gmoccapyGui.optionalStop}">M1</button>
</div>
<div class="status-chip" data-value="gmoccapy-hal-last">${escapeHtml(formatGmoccapyHalLastInput(state))}</div>
</section>
`;
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 = "") {
<button type="button" data-action="${target}-override-down">-</button>
<div data-value="${target}-override">${value} %</div>
<button type="button" data-action="${target}-override-up">+</button>
<button type="button" data-action="${target}-override-reset">100</button>
</div>
</section>
`;
@@ -647,6 +678,7 @@ function renderSpindleCoolant(element, state, dispatch) {
<button type="button" data-action="spindle-override-down">-</button>
<div data-value="spindle-override">${state.spindle.override} %</div>
<button type="button" data-action="spindle-override-up">+</button>
<button type="button" data-action="spindle-override-reset">100</button>
</div>
<div class="spindle-range"><i style="width: ${(state.spindle.rpm / 6000) * 100}%"></i></div>
</section>
@@ -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);
}