完善 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

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