fix: make Pause button pause running program

This commit is contained in:
wangdequan
2026-07-05 21:55:17 -04:00
parent 220c15295c
commit 58854f9ecf
12 changed files with 1774 additions and 126 deletions

View File

@@ -163,10 +163,28 @@ export function gateLinuxCncTaskAction(state, action) {
return allow(status);
case "PAUSE":
if (status.taskState !== "on") return block(status, "pause blocked: machine must be on");
if (status.taskMode !== "auto" && status.taskMode !== "mdi") {
return block(status, "pause blocked: task mode must be auto or MDI");
{
const taskIsRunning = status.runState === "running" || status.runState === "stepping";
const interpIsRunning = status.interpState === "reading" || status.interpState === "waiting";
const interpCanPauseResume = status.interpState !== "idle" || taskIsRunning;
const pauseReady = action.source === "pauseresume" ? interpCanPauseResume : (interpIsRunning || taskIsRunning);
const requiredModeMessage = action.source === "pauseresume"
? "pause blocked: task mode must be auto or MDI"
: "pause blocked: task mode must be auto";
const notRunningMessage = action.source === "pauseresume"
? "pause ignored: interpreter is idle"
: "pause blocked: interpreter is not running";
if (action.source === "pauseresume") {
if (status.taskMode !== "auto" && status.taskMode !== "mdi") {
return block(status, requiredModeMessage);
}
} else if (status.taskMode !== "auto") {
return block(status, requiredModeMessage);
}
if (!pauseReady) return block(status, notRunningMessage);
return allow(status);
}
return allow(status);
case "RESUME":
if (status.taskState !== "on") return block(status, "resume blocked: machine must be on");
if (status.taskMode !== "auto" && status.taskMode !== "mdi") {

View File

@@ -282,6 +282,13 @@ const initialState = {
override: 100,
enabled: false,
direction: "stop",
halPins: {
on: 0,
forward: 0,
reverse: 0,
speedOut: 0,
atSpeed: 0,
},
},
coolant: {
flood: false,
@@ -1123,6 +1130,7 @@ export function createSimulationStore(seed = {}) {
taskHalStatusLoop: {
...state.taskHalStatusLoop,
active: false,
sequence: Number(state.taskHalStatusLoop?.sequence || 0) + 1,
stopReason: action.reason || "stopped",
lastError: action.error || null,
},
@@ -1279,7 +1287,7 @@ export function createSimulationStore(seed = {}) {
rtcpState: turningOff ? "off" : state.rtcpState,
feed: turningOff ? { ...state.feed, currentVelocity: 0 } : state.feed,
coolant: turningOff ? { ...state.coolant, flood: false, mist: false } : state.coolant,
spindle: turningOff ? { ...state.spindle, enabled: false, direction: "stop" } : state.spindle,
spindle: turningOff ? stoppedSpindleState(state.spindle) : state.spindle,
operatorMessage: turningOff ? "task/HAL machine power off" : "task/HAL machine power on",
});
runTaskHalCommandSequence([
@@ -1303,7 +1311,7 @@ export function createSimulationStore(seed = {}) {
rtcpState: turningOff ? "off" : state.rtcpState,
feed: turningOff ? { ...state.feed, currentVelocity: 0 } : state.feed,
coolant: turningOff ? { ...state.coolant, flood: false, mist: false } : state.coolant,
spindle: turningOff ? { ...state.spindle, enabled: false, direction: "stop" } : state.spindle,
spindle: turningOff ? stoppedSpindleState(state.spindle) : state.spindle,
operatorMessage: turningOff ? "machine power off" : "machine power on",
});
}
@@ -1333,9 +1341,7 @@ export function createSimulationStore(seed = {}) {
mist: false,
},
spindle: {
...state.spindle,
enabled: false,
direction: "stop",
...stoppedSpindleState(state.spindle),
},
operatorMessage: "emergency stop active",
});
@@ -1362,9 +1368,7 @@ export function createSimulationStore(seed = {}) {
mist: false,
},
spindle: {
...state.spindle,
enabled: false,
direction: "stop",
...stoppedSpindleState(state.spindle),
},
operatorMessage: "estop reset; machine off",
});
@@ -1619,6 +1623,18 @@ export function createSimulationStore(seed = {}) {
setState({ operatorMessage: preconditions.operatorMessage });
break;
}
setState({
machine: {
...state.machine,
mode: "auto",
manualPanel: null,
interpState: "reading",
interpResumeState: "reading",
taskPaused: false,
},
runState: "running",
operatorMessage: "task/HAL program run requested",
});
runValidatedTaskHalProgramRun().catch(() => {});
break;
}
@@ -1718,11 +1734,22 @@ export function createSimulationStore(seed = {}) {
break;
}
const direction = normalizeSpindleDirection(action.direction);
const spindleEnabled = direction !== "stop";
const spindleActualRpm = spindleEnabled
? Number(state.spindle.rpm || 0) * (Number(state.spindle.override || 100) / 100)
: 0;
setState({
spindle: {
...state.spindle,
enabled: direction !== "stop",
enabled: spindleEnabled,
direction,
halPins: {
on: spindleEnabled ? 1 : 0,
forward: direction === "forward" ? 1 : 0,
reverse: direction === "reverse" ? 1 : 0,
speedOut: spindleActualRpm,
atSpeed: spindleEnabled ? 1 : 0,
},
},
operatorMessage: direction === "stop" ? "spindle stopped" : `spindle ${direction}`,
});
@@ -1737,9 +1764,29 @@ export function createSimulationStore(seed = {}) {
}
if (state.taskHalRuntime?.loaded) {
stopTaskHalStatusLoop("paused", { operatorMessage: "task/HAL pause requested" });
const pausedMachine = {
...state.machine,
interpResumeState: state.machine.interpState === "paused"
? state.machine.interpResumeState
: state.machine.interpState || "reading",
interpState: "paused",
taskPaused: true,
};
setState({
machine: pausedMachine,
runState: "paused",
feed: {
...state.feed,
currentVelocity: 0,
},
operatorMessage: "task/HAL pause requested",
});
runTaskHalCommandSequence([
{ type: "EMC_TASK_PLAN_PAUSE" },
], { operatorMessage: "task/HAL program paused" }).catch(() => {});
], {
operatorMessage: "task/HAL program paused",
preserveMachine: pausedMachine,
}).catch(() => {});
break;
}
setState({
@@ -1756,6 +1803,26 @@ export function createSimulationStore(seed = {}) {
});
}
break;
case "PAUSE_RESUME":
{
const interpState = state.machine?.interpState || state.linuxCncTaskPolicy?.interpState || "idle";
const taskMode = state.machine?.mode || state.linuxCncTaskPolicy?.taskMode || "manual";
const taskIsRunning = state.runState === "running" || state.runState === "stepping";
if (state.machine?.taskPaused === true || interpState === "paused" || state.runState === "paused") {
if (taskMode === "auto" || taskMode === "mdi") {
dispatch({ type: "RESUME" });
break;
}
setState({ operatorMessage: "resume blocked: task mode must be auto or MDI" });
break;
}
if ((taskMode === "auto" || taskMode === "mdi") && (interpState !== "idle" || taskIsRunning)) {
dispatch({ type: "PAUSE", source: "pauseresume" });
break;
}
setState({ operatorMessage: "pause ignored: interpreter is idle" });
}
break;
case "RESUME":
{
const gate = gateLinuxCncTaskAction(state, action);
@@ -1764,10 +1831,25 @@ export function createSimulationStore(seed = {}) {
break;
}
if (state.taskHalRuntime?.loaded) {
const resumeState = state.machine.interpResumeState === "idle" || state.machine.interpResumeState === "paused"
? "reading"
: state.machine.interpResumeState || "reading";
const resumedMachine = {
...state.machine,
interpState: resumeState,
interpResumeState: resumeState,
taskPaused: false,
};
setState({
machine: resumedMachine,
runState: resumeState === "reading" ? "running" : "idle",
operatorMessage: "task/HAL resume requested",
});
runTaskHalCommandSequence([
{ type: "EMC_TASK_PLAN_RESUME" },
], {
operatorMessage: "task/HAL program resumed",
preserveMachine: resumedMachine,
}).then((status) => {
if (shouldContinueTaskHalStatusLoop(state, status)) {
startTaskHalStatusLoop({
@@ -1801,11 +1883,44 @@ export function createSimulationStore(seed = {}) {
}
if (state.taskHalRuntime?.loaded) {
stopTaskHalStatusLoop("step", { operatorMessage: "task/HAL step requested" });
const playback = nextProgramRuntimeSamplePlayback(state, 1);
const playbackPatch = applyProgramPlaybackUiPatch(state, {
activeLine: playback.activeLine,
axisPose: playback.axisPose,
kinsType: playback.kinsType,
rtcpState: playback.rtcpState,
motionIndex: playback.motionIndex,
sampleIndex: playback.sampleIndex,
runtimeFeedback: playback.runtimeFeedback,
});
const steppedMachine = {
...state.machine,
mode: "auto",
manualPanel: null,
interpResumeState: state.machine.interpState === "paused"
? state.machine.interpResumeState || "reading"
: state.machine.interpState || "reading",
interpState: "paused",
taskPaused: true,
};
setState({
machine: steppedMachine,
runState: "stepping",
...playbackPatch,
programElapsedSeconds: playback.timing.elapsedSeconds,
programRemainingSeconds: playback.timing.remainingSeconds,
feed: {
...state.feed,
currentVelocity: playback.timing.currentVelocity,
},
operatorMessage: `task/HAL step requested line ${playback.activeLine}`,
});
runTaskHalCommandSequence([
{ type: "EMC_TASK_PLAN_STEP" },
], {
taskCycles: 1,
operatorMessage: "task/HAL stepped one cycle",
preserveMachine: steppedMachine,
}).catch(() => {});
break;
}
@@ -2047,6 +2162,10 @@ export function createSimulationStore(seed = {}) {
spindle: {
...state.spindle,
override: clampPercent(state.spindle.override + action.delta, 0, 150),
halPins: spindleHalPinsForState({
...state.spindle,
override: clampPercent(state.spindle.override + action.delta, 0, 150),
}),
},
gmoccapyGui: {
...state.gmoccapyGui,
@@ -2399,10 +2518,13 @@ export function createSimulationStore(seed = {}) {
}
dispatch({ type: "TASK_HAL_SESSION_READY", session });
const status = await state.taskHalRuntime.readStatus();
const statusPreserveMachine = isTaskHalRunPausedByOperator(state)
? { ...state.machine }
: preserveMachine;
dispatch({
type: "TASK_HAL_STATUS_APPLIED",
status,
preserveMachine,
preserveMachine: statusPreserveMachine,
operatorMessage: `LinuxCNC task/HAL session ready ${session.programPath || "-"}`,
});
return session;
@@ -2418,8 +2540,14 @@ export function createSimulationStore(seed = {}) {
const expectedProgramPath = expectedTaskHalProgramPathForState(state);
if (!state.taskHalSession || (expectedProgramPath && state.taskHalSession.programPath !== expectedProgramPath)) {
await initializeTaskHalSession({ openProgram: true });
if (isTaskHalRunPausedByOperator(state)) {
return null;
}
}
const loadedMotionPlan = await loadTaskHalMotionPlanForSession(state.taskHalSession);
if (isTaskHalRunPausedByOperator(state)) {
return null;
}
if (!loadedMotionPlan) {
setState({ operatorMessage: "run blocked: task/HAL feed motion plan not loaded" });
return null;
@@ -2430,10 +2558,16 @@ export function createSimulationStore(seed = {}) {
setState({ operatorMessage: ready.operatorMessage });
return null;
}
if (isTaskHalRunPausedByOperator(state)) {
return null;
}
stopTaskHalStatusLoop("restarted", {
operatorMessage: "task/HAL status loop restarting",
});
if (isTaskHalRunPausedByOperator(state)) {
return null;
}
const status = await runTaskHalCommandSequence([
{ type: "EMC_TASK_SET_STATE", state: "ON" },
@@ -2444,6 +2578,9 @@ export function createSimulationStore(seed = {}) {
operatorMessage: `task/HAL program run ${ready.profileId} ${ready.kinematicsModuleId}`,
allowFixtureSession: false,
});
if (isTaskHalRunPausedByOperator(state)) {
return status;
}
if (shouldContinueTaskHalStatusLoop(state, status)) {
startTaskHalStatusLoop({
profileId: ready.profileId,
@@ -2548,20 +2685,26 @@ export function createSimulationStore(seed = {}) {
mode: "auto",
manualPanel: null,
allHomed: true,
interpState: "idle",
interpResumeState: "idle",
interpState: "reading",
interpResumeState: "reading",
taskPaused: false,
},
runState: "idle",
runState: "running",
operatorMessage: "RUN ready: motion plan source ready; preparing task/HAL session",
});
const expectedProgramPath = expectedTaskHalProgramPathForState(state);
if (!state.taskHalSession || (expectedProgramPath && state.taskHalSession.programPath !== expectedProgramPath)) {
setState({ operatorMessage: "RUN preparing: initializing task/HAL session" });
await initializeTaskHalSession({ openProgram: true });
if (isTaskHalRunPausedByOperator(state)) {
return state;
}
}
setState({ operatorMessage: "RUN preparing: loading task/HAL motion plan" });
const loadedMotionPlan = await loadTaskHalMotionPlanWithSessionRetry();
if (isTaskHalRunPausedByOperator(state)) {
return state;
}
if (!loadedMotionPlan) {
setState({ operatorMessage: "run blocked: task/HAL feed motion plan not loaded" });
return state;
@@ -2571,9 +2714,15 @@ export function createSimulationStore(seed = {}) {
setState({ operatorMessage: ready.operatorMessage });
return state;
}
if (isTaskHalRunPausedByOperator(state)) {
return state;
}
stopTaskHalStatusLoop("restarted", {
operatorMessage: "task/HAL status loop restarting",
});
if (isTaskHalRunPausedByOperator(state)) {
return state;
}
setState({
activeLine: state.programStartLine || 1,
programExecutionMotionIndex: 0,
@@ -2592,6 +2741,9 @@ export function createSimulationStore(seed = {}) {
operatorMessage: `task/HAL program run ${ready.profileId} ${ready.kinematicsModuleId}`,
allowFixtureSession: false,
});
if (isTaskHalRunPausedByOperator(state)) {
return state;
}
if (shouldContinueTaskHalStatusLoop(state, status)) {
startTaskHalStatusLoop({
profileId: ready.profileId,
@@ -2703,7 +2855,7 @@ export function createSimulationStore(seed = {}) {
taskCycles: loop.batchSize,
});
const status = await state.taskHalRuntime.readStatus();
if (state.taskHalStatusLoop?.sequence !== sequence) {
if (state.taskHalStatusLoop?.sequence !== sequence || state.taskHalStatusLoop?.active !== true) {
return status;
}
dispatch({
@@ -3187,6 +3339,7 @@ function createLinuxCncProcessMonitor(state = {}) {
const spindleActualRpm = state.spindle?.enabled
? spindleCommandRpm * (Number(state.spindle?.override || 100) / 100)
: 0;
const spindleHalPins = state.spindle?.halPins || {};
const toolChange = {
toolInSpindle: Number(toolRuntime.toolInSpindle || toolDb.toolInSpindle || 0),
toolFromPocket: Number(toolRuntime.toolFromPocket || toolDb.toolFromPocket || 0),
@@ -3280,10 +3433,11 @@ function createLinuxCncProcessMonitor(state = {}) {
actualRpm: spindleActualRpm,
overridePercent: Number(state.spindle?.override || 0),
halPins: {
on: Number(halPins["spindle.0.on"]?.value ?? (state.spindle?.enabled ? 1 : 0)),
forward: Number(halPins["spindle.0.forward"]?.value ?? (state.spindle?.direction === "forward" ? 1 : 0)),
reverse: Number(halPins["spindle.0.reverse"]?.value ?? (state.spindle?.direction === "reverse" ? 1 : 0)),
speedOut: Number(halPins["spindle.0.speed-out"]?.value ?? spindleActualRpm),
on: Number(spindleHalPins.on ?? halPins["spindle.0.on"]?.value ?? (state.spindle?.enabled ? 1 : 0)),
forward: Number(spindleHalPins.forward ?? halPins["spindle.0.forward"]?.value ?? (state.spindle?.direction === "forward" ? 1 : 0)),
reverse: Number(spindleHalPins.reverse ?? halPins["spindle.0.reverse"]?.value ?? (state.spindle?.direction === "reverse" ? 1 : 0)),
speedOut: Number(spindleHalPins.speedOut ?? halPins["spindle.0.speed-out"]?.value ?? spindleActualRpm),
atSpeed: Number(spindleHalPins.atSpeed ?? halPins["spindle.0.at-speed"]?.value ?? (state.spindle?.enabled ? 1 : 0)),
},
},
feed: {
@@ -3779,6 +3933,12 @@ function shouldContinueTaskHalStatusLoop(state = {}, status = {}) {
return !aborted && !paused && !complete && (interpState === "reading" || taskMode === "mdi");
}
function isTaskHalRunPausedByOperator(state = {}) {
return state.runState === "paused"
|| state.machine?.interpState === "paused"
|| state.machine?.taskPaused === true;
}
function createStoppedProgramStatePatch(state, {
reason = "stopped",
operatorMessage = "program stopped",
@@ -4404,9 +4564,9 @@ function homeAxisPoseForState(state = {}) {
if (state.profile?.id === "xyzac-trt" || state.profile?.id === "xyzbc-trt") {
return clampAxisPoseToProfile({
...initialAxisPose,
x: 43,
y: -32.15,
z: -11.306,
x: 0,
y: 0,
z: 10,
}, state.profile);
}
const pose = { ...initialAxisPose };
@@ -4489,6 +4649,12 @@ function executeMdiCommand(state, rawCommand) {
rpm: parsed.spindleRpm ?? state.spindle.rpm,
enabled: parsed.spindleEnabled ?? state.spindle.enabled,
direction: parsed.spindleDirection ?? state.spindle.direction,
halPins: spindleHalPinsForState({
...state.spindle,
rpm: parsed.spindleRpm ?? state.spindle.rpm,
enabled: parsed.spindleEnabled ?? state.spindle.enabled,
direction: parsed.spindleDirection ?? state.spindle.direction,
}),
}
: state.spindle,
coolant: parsed.coolantPatch
@@ -4592,6 +4758,33 @@ function applyMdiMCode(parsed, value) {
}
}
function spindleHalPinsForState(spindle = {}) {
const enabled = Boolean(spindle.enabled) && spindle.direction !== "stop";
const speedOut = enabled
? Number(spindle.rpm || 0) * (Number(spindle.override || 100) / 100)
: 0;
return {
on: enabled ? 1 : 0,
forward: spindle.direction === "forward" ? 1 : 0,
reverse: spindle.direction === "reverse" ? 1 : 0,
speedOut,
atSpeed: enabled ? 1 : 0,
};
}
function stoppedSpindleState(spindle = {}) {
return {
...spindle,
enabled: false,
direction: "stop",
halPins: spindleHalPinsForState({
...spindle,
enabled: false,
direction: "stop",
}),
};
}
function normalizeSpindleDirection(direction) {
const value = String(direction || "stop").toLowerCase();
return value === "forward" || value === "reverse" ? value : "stop";

View File

@@ -162,6 +162,12 @@ body {
object-fit: contain;
}
.axis-tool-button[data-paused="true"],
.axis-tool-button[data-active="true"] {
background: #d8e9f7;
box-shadow: inset 1px 1px #7d7d7d, inset -1px -1px #ffffff;
}
.axis-text-tool {
font: 700 18px/1 "Times New Roman", serif;
text-decoration: underline;

View File

@@ -28,7 +28,7 @@ export const AXIS_BUTTON_PARITY = [
{ id: "toolbar-load", action: "open", sourceSymbol: "commands.open_file", sourceLines: "axis.py:2243-2262", expected: "open local G-code" },
{ id: "toolbar-reload", action: "reload", sourceSymbol: "commands.reload_file", sourceLines: "axis.py:2296-2297", expected: "reload active program" },
{ id: "toolbar-run", action: "run", sourceSymbol: "commands.task_run", sourceLines: "axis.py:2308-2320", expected: "start AUTO run" },
{ id: "toolbar-pause-resume", action: "pause", sourceSymbol: "commands.task_pauseresume", sourceLines: "axis.py:2353-2363", expected: "pause or resume based on paused state" },
{ id: "toolbar-pause-resume", action: "pause-resume", sourceSymbol: "commands.task_pauseresume", sourceLines: "axis.py:2433-2443 / axis.tcl:543-549", expected: "AUTO_PAUSE when interpreter is not idle; AUTO_RESUME when paused" },
{ id: "toolbar-step", action: "step", sourceSymbol: "commands.task_step", sourceLines: "axis.py:2321-2327", expected: "single step" },
{ id: "toolbar-stop", action: "stop", sourceSymbol: "commands.task_stop", sourceLines: "axis.py:2365-2372", expected: "abort current task" },
{ id: "toolbar-view-z", action: "view-z", sourceSymbol: "commands.set_view_z", sourceLines: "axis.py:2208-2215", expected: "Z view" },
@@ -58,6 +58,7 @@ export const AXIS_BUTTON_PARITY = [
{ id: "manual-optional-stop", action: "optional-stop", sourceSymbol: "commands.toggle_optional_stop", sourceLines: "axis.py:2047-2049", expected: "optional stop state" },
{ id: "manual-flood", action: "toggle-flood", sourceSymbol: "commands.flood_toggle", sourceLines: "axis.py:3176", expected: "flood coolant toggle" },
{ id: "manual-mist", action: "toggle-mist", sourceSymbol: "commands.mist_toggle", sourceLines: "axis.py:3175", expected: "mist coolant toggle" },
{ id: "mdi-input", action: "mdi-input", sourceSymbol: "commands.mdi_command.entry", sourceLines: "axis.py:2413-2417,2499-2516", expected: "edit pending MDI command text before submit/history execution" },
{ id: "mdi-submit", action: "mdi-form", sourceSymbol: "commands.send_mdi", sourceLines: "axis.py:2413-2417", expected: "submit MDI command" },
{ id: "mdi-history", action: "mdi-history", sourceSymbol: "commands.mdi_history_butt_1", sourceLines: "axis.py:2499-2516", expected: "run/restore MDI history command" },
{ id: "pyvcp-identity", action: "kins-identity", sourceSymbol: "pyvcp.type0-button -> halui.mdi-command-00", sourceLines: "switchkins_postgui.hal", expected: "M429 sets identity kins" },
@@ -144,7 +145,8 @@ function renderMenu(element, state, dispatch) {
["home-all", "Home All"],
["run-ready", "Run Ready"],
["run", "Run"],
[state.runState === "paused" ? "resume" : "pause", state.runState === "paused" ? "Resume" : "Pause"],
["pause", "Pause"],
["resume", "Resume"],
["step", "Step"],
["stop", "Stop"],
])}
@@ -166,36 +168,50 @@ function renderMenu(element, state, dispatch) {
}
function renderToolbar(element, state, dispatch) {
element.innerHTML = `
<input type="file" class="axis-file-input" data-action="OPEN_FILE" accept=".ngc,.nc,.tap,.gcode,.txt" />
${toolButton("tbtn_estop", "estop", "Emergency stop", state.machine.estopActive)}
${toolButton("tbtn_on", "power", "Machine power", state.machine.powerOn)}
${modeToggleTool(state)}
${toolButton("btn_load", "open", "Open program")}
${toolButton("btn_reload", "reload", "Reload program")}
${toolButton("btn_run", "run", "Run program")}
${toolButton("tbtn_pause", state.runState === "paused" ? "resume" : "pause", state.runState === "paused" ? "Resume" : "Pause", state.runState === "paused")}
${toolButton("btn_step", "step", "Step")}
${toolButton("btn_stop", "stop", "Stop")}
<span class="axis-toolbar-separator"></span>
${smallTextTool("Z", "view-z", "View Z")}
${smallTextTool("Y", "view-y", "View Y")}
${smallTextTool("X", "view-x", "View X")}
${smallTextTool("P", "view-p", "Fit perspective")}
${toolButton("tbtn_view_tool_path", "clear-preview", "Clear plot")}
`;
element.__axisLatestState = state;
if (element.dataset.mounted !== "true") {
element.innerHTML = `
<input type="file" class="axis-file-input" data-action="OPEN_FILE" accept=".ngc,.nc,.tap,.gcode,.txt" />
${toolButton("tbtn_estop", "estop", "Emergency stop", state.machine.estopActive)}
${toolButton("tbtn_on", "power", "Machine power", state.machine.powerOn)}
${modeToggleTool(state)}
${toolButton("btn_load", "open", "Open program")}
${toolButton("btn_reload", "reload", "Reload program")}
${toolButton("btn_run", "run", "Run program")}
${toolButton("tbtn_pause", "pause-resume", pauseResumeToolbarTitle(state), isProgramPaused(state))}
${toolButton("btn_step", "step", "Step")}
${toolButton("btn_stop", "stop", "Stop")}
<span class="axis-toolbar-separator"></span>
${smallTextTool("Z", "view-z", "View Z")}
${smallTextTool("Y", "view-y", "View Y")}
${smallTextTool("X", "view-x", "View X")}
${smallTextTool("P", "view-p", "Fit perspective")}
${toolButton("tbtn_view_tool_path", "clear-preview", "Clear plot")}
`;
element.dataset.mounted = "true";
element.addEventListener("click", (event) => {
const button = event.target.closest?.(".axis-tool-button[data-action]");
if (!button || !element.contains(button)) return;
const action = button.dataset.action;
if (action === "open") {
element.querySelector('[data-action="OPEN_FILE"]').click();
return;
}
runAxisCommand(action, element.__axisLatestState || state, dispatch, element);
});
element.querySelector('[data-action="OPEN_FILE"]').addEventListener("change", async (event) => {
const [file] = event.target.files || [];
if (!file) return;
dispatch({ type: "LOAD_PROGRAM", filename: file.name, content: await file.text() });
event.target.value = "";
});
}
updateToolbarButton(element, "tbtn_estop", "estop", "Emergency stop", state.machine.estopActive);
updateToolbarButton(element, "tbtn_on", "power", "Machine power", state.machine.powerOn);
updateToolbarModeButton(element, state);
updateToolbarButton(element, "tbtn_pause", "pause-resume", pauseResumeToolbarTitle(state), isProgramPaused(state));
element.querySelectorAll(".axis-tool-button[data-action]").forEach((button) => {
tagAxisControl(button, button.dataset.action);
if (button.dataset.action === "open") return;
button.addEventListener("click", () => runAxisCommand(button.dataset.action, state, dispatch, element));
});
element.querySelector('[data-action="open"]').addEventListener("click", () => element.querySelector('[data-action="OPEN_FILE"]').click());
element.querySelector('[data-action="OPEN_FILE"]').addEventListener("change", async (event) => {
const [file] = event.target.files || [];
if (!file) return;
dispatch({ type: "LOAD_PROGRAM", filename: file.name, content: await file.text() });
event.target.value = "";
});
}
@@ -270,7 +286,7 @@ function renderManual(element, state, dispatch) {
<button type="button" data-action="toggle-mist">Mist</button>
</div>
<div class="active-gcodes-label">Active G-Codes:</div>
<div class="active-gcodes">G80 G17 G40 G21 G90 G94 G54 G49 G99 G64<br />G97 G91.1 G8 M5 M9 M48 M53 F0 S0</div>
${activeGcodesMarkup(state)}
</section>
`;
@@ -550,12 +566,13 @@ function monitorAxisRows(axisPose) {
`).join("");
}
function toolButton(buttonId, action, title, active = false) {
const icon = getGmoccapyIcon(buttonId, active ? "active" : "inactive");
function toolButton(buttonId, action, title, active = false, iconId = buttonId) {
const icon = getGmoccapyIcon(iconId, active ? "active" : "inactive");
const iconMarkup = icon.path
? `<img src="${escapeHtml(icon.path)}" alt="" />`
: `<span>${escapeHtml(title.slice(0, 1))}</span>`;
return `<button type="button" class="axis-tool-button" data-action="${action}" title="${escapeHtml(title)}">${iconMarkup}</button>`;
const pausedAttr = buttonId === "tbtn_pause" ? ` data-paused="${active ? "true" : "false"}"` : "";
return `<button type="button" class="axis-tool-button" data-tool-id="${escapeHtml(buttonId)}" data-action="${action}" data-active="${active ? "true" : "false"}"${pausedAttr} aria-label="${escapeHtml(title)}" aria-pressed="${active ? "true" : "false"}" title="${escapeHtml(title)}">${iconMarkup}</button>`;
}
function smallTextTool(label, action, title) {
@@ -570,6 +587,7 @@ function modeToggleTool(state) {
<button
type="button"
class="axis-tool-button axis-mode-toggle"
data-tool-id="toolbar-auto-manual"
data-action="toggle-auto-manual"
data-mode="${autoMode ? "auto" : "manual"}"
title="${escapeHtml(title)}"
@@ -577,6 +595,46 @@ function modeToggleTool(state) {
`;
}
function updateToolbarButton(element, buttonId, action, title, active = false, iconId = buttonId) {
const button = element.querySelector(`[data-tool-id="${buttonId}"]`);
if (!button) return;
const icon = getGmoccapyIcon(iconId, active ? "active" : "inactive");
const iconMarkup = icon.path
? `<img src="${escapeHtml(icon.path)}" alt="" />`
: `<span>${escapeHtml(title.slice(0, 1))}</span>`;
button.dataset.action = action;
button.title = title;
button.setAttribute("aria-label", title);
button.setAttribute("aria-pressed", active ? "true" : "false");
button.dataset.active = active ? "true" : "false";
if (buttonId === "tbtn_pause") {
button.dataset.paused = active ? "true" : "false";
}
if (button.innerHTML !== iconMarkup) button.innerHTML = iconMarkup;
}
function isProgramPaused(state) {
return state.runState === "paused" ||
state.machine?.interpState === "paused" ||
state.machine?.taskPaused === true;
}
function pauseResumeToolbarTitle(state) {
return isProgramPaused(state) ? "Resume program" : "Pause program";
}
function updateToolbarModeButton(element, state) {
const button = element.querySelector('[data-tool-id="toolbar-auto-manual"]');
if (!button) return;
const autoMode = state.machine?.mode === "auto";
const label = autoMode ? "MAN" : "AUTO";
const title = autoMode ? "Switch to Manual mode" : "Switch to Auto mode";
button.dataset.action = "toggle-auto-manual";
button.dataset.mode = autoMode ? "auto" : "manual";
button.title = title;
if (button.textContent !== label) button.textContent = label;
}
function menuButton(label, entries) {
return `
<div class="axis-menu">
@@ -653,6 +711,28 @@ function mdiQuickCommands(state) {
].filter((command, index, commands) => command && commands.indexOf(command) === index).slice(0, 8);
}
function activeGcodesMarkup(state) {
const spindleCode = state.spindle?.direction === "forward"
? "M3"
: state.spindle?.direction === "reverse"
? "M4"
: "M5";
const coolantCode = state.coolant?.flood
? "M8"
: state.coolant?.mist
? "M7"
: "M9";
const feedRate = formatNumber(state.feed?.feedRate || 0, 0);
const spindleRpm = state.spindle?.enabled ? formatNumber(state.spindle?.rpm || 0, 0) : "0";
const modalLine = "G80 G17 G40 G21 G90 G94 G54 G49 G99 G64";
const machineLine = `G97 G91.1 G8 ${spindleCode} ${coolantCode} M48 M53 F${feedRate} S${spindleRpm}`;
return `
<div class="active-gcodes" data-active-gcodes="${escapeHtml(`${modalLine} ${machineLine}`)}">
${escapeHtml(modalLine)}<br />${escapeHtml(machineLine)}
</div>
`;
}
function runAxisCommand(command, state, dispatch, root) {
const selectedJoint = Number(state.machine?.selectedJoint ?? 0);
const selectedAxis = jointAxis(selectedJoint);
@@ -662,6 +742,7 @@ function runAxisCommand(command, state, dispatch, root) {
"toggle-auto-manual": () => dispatch({ type: "SET_MODE", mode: state.machine?.mode === "auto" ? "manual" : "auto" }),
reload: () => dispatch({ type: "RELOAD_PROGRAM" }),
run: () => dispatch({ type: "RUN_FROM_OPERATOR" }),
"pause-resume": () => dispatch({ type: "PAUSE_RESUME" }),
pause: () => dispatch({ type: "PAUSE" }),
resume: () => dispatch({ type: "RESUME" }),
step: () => dispatch({ type: "STEP" }),

View File

@@ -163,10 +163,28 @@ export function gateLinuxCncTaskAction(state, action) {
return allow(status);
case "PAUSE":
if (status.taskState !== "on") return block(status, "pause blocked: machine must be on");
if (status.taskMode !== "auto" && status.taskMode !== "mdi") {
return block(status, "pause blocked: task mode must be auto or MDI");
{
const taskIsRunning = status.runState === "running" || status.runState === "stepping";
const interpIsRunning = status.interpState === "reading" || status.interpState === "waiting";
const interpCanPauseResume = status.interpState !== "idle" || taskIsRunning;
const pauseReady = action.source === "pauseresume" ? interpCanPauseResume : (interpIsRunning || taskIsRunning);
const requiredModeMessage = action.source === "pauseresume"
? "pause blocked: task mode must be auto or MDI"
: "pause blocked: task mode must be auto";
const notRunningMessage = action.source === "pauseresume"
? "pause ignored: interpreter is idle"
: "pause blocked: interpreter is not running";
if (action.source === "pauseresume") {
if (status.taskMode !== "auto" && status.taskMode !== "mdi") {
return block(status, requiredModeMessage);
}
} else if (status.taskMode !== "auto") {
return block(status, requiredModeMessage);
}
if (!pauseReady) return block(status, notRunningMessage);
return allow(status);
}
return allow(status);
case "RESUME":
if (status.taskState !== "on") return block(status, "resume blocked: machine must be on");
if (status.taskMode !== "auto" && status.taskMode !== "mdi") {

View File

@@ -282,6 +282,13 @@ const initialState = {
override: 100,
enabled: false,
direction: "stop",
halPins: {
on: 0,
forward: 0,
reverse: 0,
speedOut: 0,
atSpeed: 0,
},
},
coolant: {
flood: false,
@@ -1123,6 +1130,7 @@ export function createSimulationStore(seed = {}) {
taskHalStatusLoop: {
...state.taskHalStatusLoop,
active: false,
sequence: Number(state.taskHalStatusLoop?.sequence || 0) + 1,
stopReason: action.reason || "stopped",
lastError: action.error || null,
},
@@ -1279,7 +1287,7 @@ export function createSimulationStore(seed = {}) {
rtcpState: turningOff ? "off" : state.rtcpState,
feed: turningOff ? { ...state.feed, currentVelocity: 0 } : state.feed,
coolant: turningOff ? { ...state.coolant, flood: false, mist: false } : state.coolant,
spindle: turningOff ? { ...state.spindle, enabled: false, direction: "stop" } : state.spindle,
spindle: turningOff ? stoppedSpindleState(state.spindle) : state.spindle,
operatorMessage: turningOff ? "task/HAL machine power off" : "task/HAL machine power on",
});
runTaskHalCommandSequence([
@@ -1303,7 +1311,7 @@ export function createSimulationStore(seed = {}) {
rtcpState: turningOff ? "off" : state.rtcpState,
feed: turningOff ? { ...state.feed, currentVelocity: 0 } : state.feed,
coolant: turningOff ? { ...state.coolant, flood: false, mist: false } : state.coolant,
spindle: turningOff ? { ...state.spindle, enabled: false, direction: "stop" } : state.spindle,
spindle: turningOff ? stoppedSpindleState(state.spindle) : state.spindle,
operatorMessage: turningOff ? "machine power off" : "machine power on",
});
}
@@ -1333,9 +1341,7 @@ export function createSimulationStore(seed = {}) {
mist: false,
},
spindle: {
...state.spindle,
enabled: false,
direction: "stop",
...stoppedSpindleState(state.spindle),
},
operatorMessage: "emergency stop active",
});
@@ -1362,9 +1368,7 @@ export function createSimulationStore(seed = {}) {
mist: false,
},
spindle: {
...state.spindle,
enabled: false,
direction: "stop",
...stoppedSpindleState(state.spindle),
},
operatorMessage: "estop reset; machine off",
});
@@ -1619,6 +1623,18 @@ export function createSimulationStore(seed = {}) {
setState({ operatorMessage: preconditions.operatorMessage });
break;
}
setState({
machine: {
...state.machine,
mode: "auto",
manualPanel: null,
interpState: "reading",
interpResumeState: "reading",
taskPaused: false,
},
runState: "running",
operatorMessage: "task/HAL program run requested",
});
runValidatedTaskHalProgramRun().catch(() => {});
break;
}
@@ -1718,11 +1734,22 @@ export function createSimulationStore(seed = {}) {
break;
}
const direction = normalizeSpindleDirection(action.direction);
const spindleEnabled = direction !== "stop";
const spindleActualRpm = spindleEnabled
? Number(state.spindle.rpm || 0) * (Number(state.spindle.override || 100) / 100)
: 0;
setState({
spindle: {
...state.spindle,
enabled: direction !== "stop",
enabled: spindleEnabled,
direction,
halPins: {
on: spindleEnabled ? 1 : 0,
forward: direction === "forward" ? 1 : 0,
reverse: direction === "reverse" ? 1 : 0,
speedOut: spindleActualRpm,
atSpeed: spindleEnabled ? 1 : 0,
},
},
operatorMessage: direction === "stop" ? "spindle stopped" : `spindle ${direction}`,
});
@@ -1737,9 +1764,29 @@ export function createSimulationStore(seed = {}) {
}
if (state.taskHalRuntime?.loaded) {
stopTaskHalStatusLoop("paused", { operatorMessage: "task/HAL pause requested" });
const pausedMachine = {
...state.machine,
interpResumeState: state.machine.interpState === "paused"
? state.machine.interpResumeState
: state.machine.interpState || "reading",
interpState: "paused",
taskPaused: true,
};
setState({
machine: pausedMachine,
runState: "paused",
feed: {
...state.feed,
currentVelocity: 0,
},
operatorMessage: "task/HAL pause requested",
});
runTaskHalCommandSequence([
{ type: "EMC_TASK_PLAN_PAUSE" },
], { operatorMessage: "task/HAL program paused" }).catch(() => {});
], {
operatorMessage: "task/HAL program paused",
preserveMachine: pausedMachine,
}).catch(() => {});
break;
}
setState({
@@ -1756,6 +1803,26 @@ export function createSimulationStore(seed = {}) {
});
}
break;
case "PAUSE_RESUME":
{
const interpState = state.machine?.interpState || state.linuxCncTaskPolicy?.interpState || "idle";
const taskMode = state.machine?.mode || state.linuxCncTaskPolicy?.taskMode || "manual";
const taskIsRunning = state.runState === "running" || state.runState === "stepping";
if (state.machine?.taskPaused === true || interpState === "paused" || state.runState === "paused") {
if (taskMode === "auto" || taskMode === "mdi") {
dispatch({ type: "RESUME" });
break;
}
setState({ operatorMessage: "resume blocked: task mode must be auto or MDI" });
break;
}
if ((taskMode === "auto" || taskMode === "mdi") && (interpState !== "idle" || taskIsRunning)) {
dispatch({ type: "PAUSE", source: "pauseresume" });
break;
}
setState({ operatorMessage: "pause ignored: interpreter is idle" });
}
break;
case "RESUME":
{
const gate = gateLinuxCncTaskAction(state, action);
@@ -1764,10 +1831,25 @@ export function createSimulationStore(seed = {}) {
break;
}
if (state.taskHalRuntime?.loaded) {
const resumeState = state.machine.interpResumeState === "idle" || state.machine.interpResumeState === "paused"
? "reading"
: state.machine.interpResumeState || "reading";
const resumedMachine = {
...state.machine,
interpState: resumeState,
interpResumeState: resumeState,
taskPaused: false,
};
setState({
machine: resumedMachine,
runState: resumeState === "reading" ? "running" : "idle",
operatorMessage: "task/HAL resume requested",
});
runTaskHalCommandSequence([
{ type: "EMC_TASK_PLAN_RESUME" },
], {
operatorMessage: "task/HAL program resumed",
preserveMachine: resumedMachine,
}).then((status) => {
if (shouldContinueTaskHalStatusLoop(state, status)) {
startTaskHalStatusLoop({
@@ -1801,11 +1883,44 @@ export function createSimulationStore(seed = {}) {
}
if (state.taskHalRuntime?.loaded) {
stopTaskHalStatusLoop("step", { operatorMessage: "task/HAL step requested" });
const playback = nextProgramRuntimeSamplePlayback(state, 1);
const playbackPatch = applyProgramPlaybackUiPatch(state, {
activeLine: playback.activeLine,
axisPose: playback.axisPose,
kinsType: playback.kinsType,
rtcpState: playback.rtcpState,
motionIndex: playback.motionIndex,
sampleIndex: playback.sampleIndex,
runtimeFeedback: playback.runtimeFeedback,
});
const steppedMachine = {
...state.machine,
mode: "auto",
manualPanel: null,
interpResumeState: state.machine.interpState === "paused"
? state.machine.interpResumeState || "reading"
: state.machine.interpState || "reading",
interpState: "paused",
taskPaused: true,
};
setState({
machine: steppedMachine,
runState: "stepping",
...playbackPatch,
programElapsedSeconds: playback.timing.elapsedSeconds,
programRemainingSeconds: playback.timing.remainingSeconds,
feed: {
...state.feed,
currentVelocity: playback.timing.currentVelocity,
},
operatorMessage: `task/HAL step requested line ${playback.activeLine}`,
});
runTaskHalCommandSequence([
{ type: "EMC_TASK_PLAN_STEP" },
], {
taskCycles: 1,
operatorMessage: "task/HAL stepped one cycle",
preserveMachine: steppedMachine,
}).catch(() => {});
break;
}
@@ -2047,6 +2162,10 @@ export function createSimulationStore(seed = {}) {
spindle: {
...state.spindle,
override: clampPercent(state.spindle.override + action.delta, 0, 150),
halPins: spindleHalPinsForState({
...state.spindle,
override: clampPercent(state.spindle.override + action.delta, 0, 150),
}),
},
gmoccapyGui: {
...state.gmoccapyGui,
@@ -2399,10 +2518,13 @@ export function createSimulationStore(seed = {}) {
}
dispatch({ type: "TASK_HAL_SESSION_READY", session });
const status = await state.taskHalRuntime.readStatus();
const statusPreserveMachine = isTaskHalRunPausedByOperator(state)
? { ...state.machine }
: preserveMachine;
dispatch({
type: "TASK_HAL_STATUS_APPLIED",
status,
preserveMachine,
preserveMachine: statusPreserveMachine,
operatorMessage: `LinuxCNC task/HAL session ready ${session.programPath || "-"}`,
});
return session;
@@ -2418,8 +2540,14 @@ export function createSimulationStore(seed = {}) {
const expectedProgramPath = expectedTaskHalProgramPathForState(state);
if (!state.taskHalSession || (expectedProgramPath && state.taskHalSession.programPath !== expectedProgramPath)) {
await initializeTaskHalSession({ openProgram: true });
if (isTaskHalRunPausedByOperator(state)) {
return null;
}
}
const loadedMotionPlan = await loadTaskHalMotionPlanForSession(state.taskHalSession);
if (isTaskHalRunPausedByOperator(state)) {
return null;
}
if (!loadedMotionPlan) {
setState({ operatorMessage: "run blocked: task/HAL feed motion plan not loaded" });
return null;
@@ -2430,10 +2558,16 @@ export function createSimulationStore(seed = {}) {
setState({ operatorMessage: ready.operatorMessage });
return null;
}
if (isTaskHalRunPausedByOperator(state)) {
return null;
}
stopTaskHalStatusLoop("restarted", {
operatorMessage: "task/HAL status loop restarting",
});
if (isTaskHalRunPausedByOperator(state)) {
return null;
}
const status = await runTaskHalCommandSequence([
{ type: "EMC_TASK_SET_STATE", state: "ON" },
@@ -2444,6 +2578,9 @@ export function createSimulationStore(seed = {}) {
operatorMessage: `task/HAL program run ${ready.profileId} ${ready.kinematicsModuleId}`,
allowFixtureSession: false,
});
if (isTaskHalRunPausedByOperator(state)) {
return status;
}
if (shouldContinueTaskHalStatusLoop(state, status)) {
startTaskHalStatusLoop({
profileId: ready.profileId,
@@ -2548,20 +2685,26 @@ export function createSimulationStore(seed = {}) {
mode: "auto",
manualPanel: null,
allHomed: true,
interpState: "idle",
interpResumeState: "idle",
interpState: "reading",
interpResumeState: "reading",
taskPaused: false,
},
runState: "idle",
runState: "running",
operatorMessage: "RUN ready: motion plan source ready; preparing task/HAL session",
});
const expectedProgramPath = expectedTaskHalProgramPathForState(state);
if (!state.taskHalSession || (expectedProgramPath && state.taskHalSession.programPath !== expectedProgramPath)) {
setState({ operatorMessage: "RUN preparing: initializing task/HAL session" });
await initializeTaskHalSession({ openProgram: true });
if (isTaskHalRunPausedByOperator(state)) {
return state;
}
}
setState({ operatorMessage: "RUN preparing: loading task/HAL motion plan" });
const loadedMotionPlan = await loadTaskHalMotionPlanWithSessionRetry();
if (isTaskHalRunPausedByOperator(state)) {
return state;
}
if (!loadedMotionPlan) {
setState({ operatorMessage: "run blocked: task/HAL feed motion plan not loaded" });
return state;
@@ -2571,9 +2714,15 @@ export function createSimulationStore(seed = {}) {
setState({ operatorMessage: ready.operatorMessage });
return state;
}
if (isTaskHalRunPausedByOperator(state)) {
return state;
}
stopTaskHalStatusLoop("restarted", {
operatorMessage: "task/HAL status loop restarting",
});
if (isTaskHalRunPausedByOperator(state)) {
return state;
}
setState({
activeLine: state.programStartLine || 1,
programExecutionMotionIndex: 0,
@@ -2592,6 +2741,9 @@ export function createSimulationStore(seed = {}) {
operatorMessage: `task/HAL program run ${ready.profileId} ${ready.kinematicsModuleId}`,
allowFixtureSession: false,
});
if (isTaskHalRunPausedByOperator(state)) {
return state;
}
if (shouldContinueTaskHalStatusLoop(state, status)) {
startTaskHalStatusLoop({
profileId: ready.profileId,
@@ -2703,7 +2855,7 @@ export function createSimulationStore(seed = {}) {
taskCycles: loop.batchSize,
});
const status = await state.taskHalRuntime.readStatus();
if (state.taskHalStatusLoop?.sequence !== sequence) {
if (state.taskHalStatusLoop?.sequence !== sequence || state.taskHalStatusLoop?.active !== true) {
return status;
}
dispatch({
@@ -3187,6 +3339,7 @@ function createLinuxCncProcessMonitor(state = {}) {
const spindleActualRpm = state.spindle?.enabled
? spindleCommandRpm * (Number(state.spindle?.override || 100) / 100)
: 0;
const spindleHalPins = state.spindle?.halPins || {};
const toolChange = {
toolInSpindle: Number(toolRuntime.toolInSpindle || toolDb.toolInSpindle || 0),
toolFromPocket: Number(toolRuntime.toolFromPocket || toolDb.toolFromPocket || 0),
@@ -3280,10 +3433,11 @@ function createLinuxCncProcessMonitor(state = {}) {
actualRpm: spindleActualRpm,
overridePercent: Number(state.spindle?.override || 0),
halPins: {
on: Number(halPins["spindle.0.on"]?.value ?? (state.spindle?.enabled ? 1 : 0)),
forward: Number(halPins["spindle.0.forward"]?.value ?? (state.spindle?.direction === "forward" ? 1 : 0)),
reverse: Number(halPins["spindle.0.reverse"]?.value ?? (state.spindle?.direction === "reverse" ? 1 : 0)),
speedOut: Number(halPins["spindle.0.speed-out"]?.value ?? spindleActualRpm),
on: Number(spindleHalPins.on ?? halPins["spindle.0.on"]?.value ?? (state.spindle?.enabled ? 1 : 0)),
forward: Number(spindleHalPins.forward ?? halPins["spindle.0.forward"]?.value ?? (state.spindle?.direction === "forward" ? 1 : 0)),
reverse: Number(spindleHalPins.reverse ?? halPins["spindle.0.reverse"]?.value ?? (state.spindle?.direction === "reverse" ? 1 : 0)),
speedOut: Number(spindleHalPins.speedOut ?? halPins["spindle.0.speed-out"]?.value ?? spindleActualRpm),
atSpeed: Number(spindleHalPins.atSpeed ?? halPins["spindle.0.at-speed"]?.value ?? (state.spindle?.enabled ? 1 : 0)),
},
},
feed: {
@@ -3779,6 +3933,12 @@ function shouldContinueTaskHalStatusLoop(state = {}, status = {}) {
return !aborted && !paused && !complete && (interpState === "reading" || taskMode === "mdi");
}
function isTaskHalRunPausedByOperator(state = {}) {
return state.runState === "paused"
|| state.machine?.interpState === "paused"
|| state.machine?.taskPaused === true;
}
function createStoppedProgramStatePatch(state, {
reason = "stopped",
operatorMessage = "program stopped",
@@ -4404,9 +4564,9 @@ function homeAxisPoseForState(state = {}) {
if (state.profile?.id === "xyzac-trt" || state.profile?.id === "xyzbc-trt") {
return clampAxisPoseToProfile({
...initialAxisPose,
x: 43,
y: -32.15,
z: -11.306,
x: 0,
y: 0,
z: 10,
}, state.profile);
}
const pose = { ...initialAxisPose };
@@ -4489,6 +4649,12 @@ function executeMdiCommand(state, rawCommand) {
rpm: parsed.spindleRpm ?? state.spindle.rpm,
enabled: parsed.spindleEnabled ?? state.spindle.enabled,
direction: parsed.spindleDirection ?? state.spindle.direction,
halPins: spindleHalPinsForState({
...state.spindle,
rpm: parsed.spindleRpm ?? state.spindle.rpm,
enabled: parsed.spindleEnabled ?? state.spindle.enabled,
direction: parsed.spindleDirection ?? state.spindle.direction,
}),
}
: state.spindle,
coolant: parsed.coolantPatch
@@ -4592,6 +4758,33 @@ function applyMdiMCode(parsed, value) {
}
}
function spindleHalPinsForState(spindle = {}) {
const enabled = Boolean(spindle.enabled) && spindle.direction !== "stop";
const speedOut = enabled
? Number(spindle.rpm || 0) * (Number(spindle.override || 100) / 100)
: 0;
return {
on: enabled ? 1 : 0,
forward: spindle.direction === "forward" ? 1 : 0,
reverse: spindle.direction === "reverse" ? 1 : 0,
speedOut,
atSpeed: enabled ? 1 : 0,
};
}
function stoppedSpindleState(spindle = {}) {
return {
...spindle,
enabled: false,
direction: "stop",
halPins: spindleHalPinsForState({
...spindle,
enabled: false,
direction: "stop",
}),
};
}
function normalizeSpindleDirection(direction) {
const value = String(direction || "stop").toLowerCase();
return value === "forward" || value === "reverse" ? value : "stop";

View File

@@ -162,6 +162,12 @@ body {
object-fit: contain;
}
.axis-tool-button[data-paused="true"],
.axis-tool-button[data-active="true"] {
background: #d8e9f7;
box-shadow: inset 1px 1px #7d7d7d, inset -1px -1px #ffffff;
}
.axis-text-tool {
font: 700 18px/1 "Times New Roman", serif;
text-decoration: underline;

View File

@@ -28,7 +28,7 @@ export const AXIS_BUTTON_PARITY = [
{ id: "toolbar-load", action: "open", sourceSymbol: "commands.open_file", sourceLines: "axis.py:2243-2262", expected: "open local G-code" },
{ id: "toolbar-reload", action: "reload", sourceSymbol: "commands.reload_file", sourceLines: "axis.py:2296-2297", expected: "reload active program" },
{ id: "toolbar-run", action: "run", sourceSymbol: "commands.task_run", sourceLines: "axis.py:2308-2320", expected: "start AUTO run" },
{ id: "toolbar-pause-resume", action: "pause", sourceSymbol: "commands.task_pauseresume", sourceLines: "axis.py:2353-2363", expected: "pause or resume based on paused state" },
{ id: "toolbar-pause-resume", action: "pause-resume", sourceSymbol: "commands.task_pauseresume", sourceLines: "axis.py:2433-2443 / axis.tcl:543-549", expected: "AUTO_PAUSE when interpreter is not idle; AUTO_RESUME when paused" },
{ id: "toolbar-step", action: "step", sourceSymbol: "commands.task_step", sourceLines: "axis.py:2321-2327", expected: "single step" },
{ id: "toolbar-stop", action: "stop", sourceSymbol: "commands.task_stop", sourceLines: "axis.py:2365-2372", expected: "abort current task" },
{ id: "toolbar-view-z", action: "view-z", sourceSymbol: "commands.set_view_z", sourceLines: "axis.py:2208-2215", expected: "Z view" },
@@ -58,6 +58,7 @@ export const AXIS_BUTTON_PARITY = [
{ id: "manual-optional-stop", action: "optional-stop", sourceSymbol: "commands.toggle_optional_stop", sourceLines: "axis.py:2047-2049", expected: "optional stop state" },
{ id: "manual-flood", action: "toggle-flood", sourceSymbol: "commands.flood_toggle", sourceLines: "axis.py:3176", expected: "flood coolant toggle" },
{ id: "manual-mist", action: "toggle-mist", sourceSymbol: "commands.mist_toggle", sourceLines: "axis.py:3175", expected: "mist coolant toggle" },
{ id: "mdi-input", action: "mdi-input", sourceSymbol: "commands.mdi_command.entry", sourceLines: "axis.py:2413-2417,2499-2516", expected: "edit pending MDI command text before submit/history execution" },
{ id: "mdi-submit", action: "mdi-form", sourceSymbol: "commands.send_mdi", sourceLines: "axis.py:2413-2417", expected: "submit MDI command" },
{ id: "mdi-history", action: "mdi-history", sourceSymbol: "commands.mdi_history_butt_1", sourceLines: "axis.py:2499-2516", expected: "run/restore MDI history command" },
{ id: "pyvcp-identity", action: "kins-identity", sourceSymbol: "pyvcp.type0-button -> halui.mdi-command-00", sourceLines: "switchkins_postgui.hal", expected: "M429 sets identity kins" },
@@ -144,7 +145,8 @@ function renderMenu(element, state, dispatch) {
["home-all", "Home All"],
["run-ready", "Run Ready"],
["run", "Run"],
[state.runState === "paused" ? "resume" : "pause", state.runState === "paused" ? "Resume" : "Pause"],
["pause", "Pause"],
["resume", "Resume"],
["step", "Step"],
["stop", "Stop"],
])}
@@ -166,36 +168,50 @@ function renderMenu(element, state, dispatch) {
}
function renderToolbar(element, state, dispatch) {
element.innerHTML = `
<input type="file" class="axis-file-input" data-action="OPEN_FILE" accept=".ngc,.nc,.tap,.gcode,.txt" />
${toolButton("tbtn_estop", "estop", "Emergency stop", state.machine.estopActive)}
${toolButton("tbtn_on", "power", "Machine power", state.machine.powerOn)}
${modeToggleTool(state)}
${toolButton("btn_load", "open", "Open program")}
${toolButton("btn_reload", "reload", "Reload program")}
${toolButton("btn_run", "run", "Run program")}
${toolButton("tbtn_pause", state.runState === "paused" ? "resume" : "pause", state.runState === "paused" ? "Resume" : "Pause", state.runState === "paused")}
${toolButton("btn_step", "step", "Step")}
${toolButton("btn_stop", "stop", "Stop")}
<span class="axis-toolbar-separator"></span>
${smallTextTool("Z", "view-z", "View Z")}
${smallTextTool("Y", "view-y", "View Y")}
${smallTextTool("X", "view-x", "View X")}
${smallTextTool("P", "view-p", "Fit perspective")}
${toolButton("tbtn_view_tool_path", "clear-preview", "Clear plot")}
`;
element.__axisLatestState = state;
if (element.dataset.mounted !== "true") {
element.innerHTML = `
<input type="file" class="axis-file-input" data-action="OPEN_FILE" accept=".ngc,.nc,.tap,.gcode,.txt" />
${toolButton("tbtn_estop", "estop", "Emergency stop", state.machine.estopActive)}
${toolButton("tbtn_on", "power", "Machine power", state.machine.powerOn)}
${modeToggleTool(state)}
${toolButton("btn_load", "open", "Open program")}
${toolButton("btn_reload", "reload", "Reload program")}
${toolButton("btn_run", "run", "Run program")}
${toolButton("tbtn_pause", "pause-resume", pauseResumeToolbarTitle(state), isProgramPaused(state))}
${toolButton("btn_step", "step", "Step")}
${toolButton("btn_stop", "stop", "Stop")}
<span class="axis-toolbar-separator"></span>
${smallTextTool("Z", "view-z", "View Z")}
${smallTextTool("Y", "view-y", "View Y")}
${smallTextTool("X", "view-x", "View X")}
${smallTextTool("P", "view-p", "Fit perspective")}
${toolButton("tbtn_view_tool_path", "clear-preview", "Clear plot")}
`;
element.dataset.mounted = "true";
element.addEventListener("click", (event) => {
const button = event.target.closest?.(".axis-tool-button[data-action]");
if (!button || !element.contains(button)) return;
const action = button.dataset.action;
if (action === "open") {
element.querySelector('[data-action="OPEN_FILE"]').click();
return;
}
runAxisCommand(action, element.__axisLatestState || state, dispatch, element);
});
element.querySelector('[data-action="OPEN_FILE"]').addEventListener("change", async (event) => {
const [file] = event.target.files || [];
if (!file) return;
dispatch({ type: "LOAD_PROGRAM", filename: file.name, content: await file.text() });
event.target.value = "";
});
}
updateToolbarButton(element, "tbtn_estop", "estop", "Emergency stop", state.machine.estopActive);
updateToolbarButton(element, "tbtn_on", "power", "Machine power", state.machine.powerOn);
updateToolbarModeButton(element, state);
updateToolbarButton(element, "tbtn_pause", "pause-resume", pauseResumeToolbarTitle(state), isProgramPaused(state));
element.querySelectorAll(".axis-tool-button[data-action]").forEach((button) => {
tagAxisControl(button, button.dataset.action);
if (button.dataset.action === "open") return;
button.addEventListener("click", () => runAxisCommand(button.dataset.action, state, dispatch, element));
});
element.querySelector('[data-action="open"]').addEventListener("click", () => element.querySelector('[data-action="OPEN_FILE"]').click());
element.querySelector('[data-action="OPEN_FILE"]').addEventListener("change", async (event) => {
const [file] = event.target.files || [];
if (!file) return;
dispatch({ type: "LOAD_PROGRAM", filename: file.name, content: await file.text() });
event.target.value = "";
});
}
@@ -270,7 +286,7 @@ function renderManual(element, state, dispatch) {
<button type="button" data-action="toggle-mist">Mist</button>
</div>
<div class="active-gcodes-label">Active G-Codes:</div>
<div class="active-gcodes">G80 G17 G40 G21 G90 G94 G54 G49 G99 G64<br />G97 G91.1 G8 M5 M9 M48 M53 F0 S0</div>
${activeGcodesMarkup(state)}
</section>
`;
@@ -550,12 +566,13 @@ function monitorAxisRows(axisPose) {
`).join("");
}
function toolButton(buttonId, action, title, active = false) {
const icon = getGmoccapyIcon(buttonId, active ? "active" : "inactive");
function toolButton(buttonId, action, title, active = false, iconId = buttonId) {
const icon = getGmoccapyIcon(iconId, active ? "active" : "inactive");
const iconMarkup = icon.path
? `<img src="${escapeHtml(icon.path)}" alt="" />`
: `<span>${escapeHtml(title.slice(0, 1))}</span>`;
return `<button type="button" class="axis-tool-button" data-action="${action}" title="${escapeHtml(title)}">${iconMarkup}</button>`;
const pausedAttr = buttonId === "tbtn_pause" ? ` data-paused="${active ? "true" : "false"}"` : "";
return `<button type="button" class="axis-tool-button" data-tool-id="${escapeHtml(buttonId)}" data-action="${action}" data-active="${active ? "true" : "false"}"${pausedAttr} aria-label="${escapeHtml(title)}" aria-pressed="${active ? "true" : "false"}" title="${escapeHtml(title)}">${iconMarkup}</button>`;
}
function smallTextTool(label, action, title) {
@@ -570,6 +587,7 @@ function modeToggleTool(state) {
<button
type="button"
class="axis-tool-button axis-mode-toggle"
data-tool-id="toolbar-auto-manual"
data-action="toggle-auto-manual"
data-mode="${autoMode ? "auto" : "manual"}"
title="${escapeHtml(title)}"
@@ -577,6 +595,46 @@ function modeToggleTool(state) {
`;
}
function updateToolbarButton(element, buttonId, action, title, active = false, iconId = buttonId) {
const button = element.querySelector(`[data-tool-id="${buttonId}"]`);
if (!button) return;
const icon = getGmoccapyIcon(iconId, active ? "active" : "inactive");
const iconMarkup = icon.path
? `<img src="${escapeHtml(icon.path)}" alt="" />`
: `<span>${escapeHtml(title.slice(0, 1))}</span>`;
button.dataset.action = action;
button.title = title;
button.setAttribute("aria-label", title);
button.setAttribute("aria-pressed", active ? "true" : "false");
button.dataset.active = active ? "true" : "false";
if (buttonId === "tbtn_pause") {
button.dataset.paused = active ? "true" : "false";
}
if (button.innerHTML !== iconMarkup) button.innerHTML = iconMarkup;
}
function isProgramPaused(state) {
return state.runState === "paused" ||
state.machine?.interpState === "paused" ||
state.machine?.taskPaused === true;
}
function pauseResumeToolbarTitle(state) {
return isProgramPaused(state) ? "Resume program" : "Pause program";
}
function updateToolbarModeButton(element, state) {
const button = element.querySelector('[data-tool-id="toolbar-auto-manual"]');
if (!button) return;
const autoMode = state.machine?.mode === "auto";
const label = autoMode ? "MAN" : "AUTO";
const title = autoMode ? "Switch to Manual mode" : "Switch to Auto mode";
button.dataset.action = "toggle-auto-manual";
button.dataset.mode = autoMode ? "auto" : "manual";
button.title = title;
if (button.textContent !== label) button.textContent = label;
}
function menuButton(label, entries) {
return `
<div class="axis-menu">
@@ -653,6 +711,28 @@ function mdiQuickCommands(state) {
].filter((command, index, commands) => command && commands.indexOf(command) === index).slice(0, 8);
}
function activeGcodesMarkup(state) {
const spindleCode = state.spindle?.direction === "forward"
? "M3"
: state.spindle?.direction === "reverse"
? "M4"
: "M5";
const coolantCode = state.coolant?.flood
? "M8"
: state.coolant?.mist
? "M7"
: "M9";
const feedRate = formatNumber(state.feed?.feedRate || 0, 0);
const spindleRpm = state.spindle?.enabled ? formatNumber(state.spindle?.rpm || 0, 0) : "0";
const modalLine = "G80 G17 G40 G21 G90 G94 G54 G49 G99 G64";
const machineLine = `G97 G91.1 G8 ${spindleCode} ${coolantCode} M48 M53 F${feedRate} S${spindleRpm}`;
return `
<div class="active-gcodes" data-active-gcodes="${escapeHtml(`${modalLine} ${machineLine}`)}">
${escapeHtml(modalLine)}<br />${escapeHtml(machineLine)}
</div>
`;
}
function runAxisCommand(command, state, dispatch, root) {
const selectedJoint = Number(state.machine?.selectedJoint ?? 0);
const selectedAxis = jointAxis(selectedJoint);
@@ -662,6 +742,7 @@ function runAxisCommand(command, state, dispatch, root) {
"toggle-auto-manual": () => dispatch({ type: "SET_MODE", mode: state.machine?.mode === "auto" ? "manual" : "auto" }),
reload: () => dispatch({ type: "RELOAD_PROGRAM" }),
run: () => dispatch({ type: "RUN_FROM_OPERATOR" }),
"pause-resume": () => dispatch({ type: "PAUSE_RESUME" }),
pause: () => dispatch({ type: "PAUSE" }),
resume: () => dispatch({ type: "RESUME" }),
step: () => dispatch({ type: "STEP" }),