Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,8 @@ class MachineStatusManager extends events.EventEmitter {
mpos: state.status?.mpos || null,
wpos: state.status?.wpos || null,
pinState: state.status?.pinState || null, // Grbl v1.1: input pin state ('XYZPDHRS' indicates triggered pins)
accessoryState: state.status?.accessoryState || null // Grbl v1.1: accessory state ('SCFM' indicates spindle/coolant state)
accessoryState: state.status?.accessoryState || null, // Grbl v1.1: accessory state ('SCFM' indicates spindle/coolant state)
ov: state.status?.ov || null // Grbl v1.1: override values [feed%, rapid%, spindle%]
},
// Include full parserstate (spindle, tool, feedrate, modal groups, WCS, etc.)
parserstate: state.parserstate || null,
Expand Down
12 changes: 8 additions & 4 deletions apps/web/src/routes/Monitor/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useState, useMemo, useEffect, useCallback, useRef } from 'react'
import { useNavigate } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import { Camera, Terminal, Maximize2, Clock, FileText, Gauge, Columns3, PictureInPicture, ArrowLeftRight, RotateCcw, RotateCw, Square, ChevronDown, GripVertical, BarChart3, Wrench, ActivitySquare, ClipboardList, Move } from 'lucide-react'
import { Camera, Terminal, Maximize2, Clock, FileText, Gauge, Columns3, PictureInPicture, ArrowLeftRight, RotateCcw, RotateCw, Square, ChevronDown, GripVertical, BarChart3, Wrench, ActivitySquare, ClipboardList, Move, SlidersHorizontal } from 'lucide-react'
import Hls from 'hls.js'
import { Button } from '@/components/ui/button'
import { Progress } from '@/components/ui/progress'
Expand Down Expand Up @@ -64,6 +64,7 @@ import {
import { CSS } from '@dnd-kit/utilities'
import { CurrentStatsPanel } from './panels/CurrentStatsPanel'
import { ToolsUsedPanel } from './panels/ToolsUsedPanel'
import { OverridePanel } from './panels/OverridePanel'
import { JogPanel } from '../Setup/panels/JogPanel'
import { formatTime } from '@/utils/formatTime'

Expand Down Expand Up @@ -873,6 +874,7 @@ type PanelConfigRecord = Record<string, {

function createPanelConfig(t: (key: string) => string): PanelConfigRecord {
return {
overrides: { title: t('Overrides'), icon: SlidersHorizontal, component: OverridePanel },
currentStats: { title: t('Current Stats'), icon: BarChart3, component: CurrentStatsPanel },
toolsUsed: { title: t('Tools Used'), icon: Wrench, component: ToolsUsedPanel },
jog: { title: t('Jog Control'), icon: Move, component: JogPanel },
Expand Down Expand Up @@ -1010,15 +1012,17 @@ export default function Monitor() {
if (stored) {
try {
const parsed = JSON.parse(stored)
const validPanels = ['currentStats', 'toolsUsed', 'jog']
const validPanels = ['overrides', 'currentStats', 'toolsUsed', 'jog']
if (Array.isArray(parsed) && parsed.every(id => validPanels.includes(id))) {
return parsed
// Add any new panels that aren't in the stored order
const missingPanels = validPanels.filter(id => !parsed.includes(id))
return [...parsed, ...missingPanels]
}
} catch {
// Invalid JSON, use default
}
}
return ['currentStats', 'toolsUsed', 'jog']
return ['overrides', 'currentStats', 'toolsUsed', 'jog']
})

// Track which panels are collapsed
Expand Down
123 changes: 123 additions & 0 deletions apps/web/src/routes/Monitor/panels/OverridePanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
import { useConnectedPort, useOverrideValues } from '@/store/hooks'
import { useGcodeCommand } from '@/hooks'
import type { PanelProps } from '../../Setup/types'

function OverrideSlider({
label,
value,
onReset,
onCoarseUp,
onCoarseDown,
onFineUp,
onFineDown
}: {
label: string
value: number
onReset: () => void
onCoarseUp: () => void
onCoarseDown: () => void
onFineUp: () => void
onFineDown: () => void
}) {
return (
<div className="space-y-1.5">
<div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground">{label}</span>
<span className={`text-sm font-mono font-semibold ${value !== 100 ? 'text-primary' : ''}`}>
{value}%
</span>
</div>
{/* Progress bar */}
<div className="relative h-2 bg-muted rounded-full overflow-hidden">
<div
className={`absolute inset-y-0 left-0 rounded-full transition-all ${
value === 100 ? 'bg-muted-foreground/40' : value > 100 ? 'bg-primary' : 'bg-orange-500'
}`}
style={{ width: `${Math.min(value, 200) / 2}%` }}
/>
</div>
{/* Controls */}
<div className="flex items-center gap-1">
<Button variant="outline" size="sm" className="h-7 px-1.5 text-xs flex-1" onClick={onCoarseDown}>
-10
</Button>
<Button variant="outline" size="sm" className="h-7 px-1.5 text-xs flex-1" onClick={onFineDown}>
-1
</Button>
<Button variant="outline" size="sm" className="h-7 px-2 text-xs flex-1" onClick={onReset}>
100%
</Button>
<Button variant="outline" size="sm" className="h-7 px-1.5 text-xs flex-1" onClick={onFineUp}>
+1
</Button>
<Button variant="outline" size="sm" className="h-7 px-1.5 text-xs flex-1" onClick={onCoarseUp}>
+10
</Button>
</div>
</div>
)
}

export function OverridePanel(_props: PanelProps) {
const { t } = useTranslation()
const connectedPort = useConnectedPort()
const { sendCommand } = useGcodeCommand(connectedPort)
const overrides = useOverrideValues()

return (
<div className="p-4 space-y-4">
{/* Feed Rate Override */}
<OverrideSlider
label={t('Feed Rate')}
value={overrides.feed}
onReset={() => sendCommand('feedOverride', 0)}
onCoarseUp={() => sendCommand('feedOverride', 10)}
onCoarseDown={() => sendCommand('feedOverride', -10)}
onFineUp={() => sendCommand('feedOverride', 1)}
onFineDown={() => sendCommand('feedOverride', -1)}
/>

{/* Spindle Speed Override */}
<OverrideSlider
label={t('Spindle Speed')}
value={overrides.spindle}
onReset={() => sendCommand('spindleOverride', 0)}
onCoarseUp={() => sendCommand('spindleOverride', 10)}
onCoarseDown={() => sendCommand('spindleOverride', -10)}
onFineUp={() => sendCommand('spindleOverride', 1)}
onFineDown={() => sendCommand('spindleOverride', -1)}
/>

{/* Rapid Override */}
<div className="space-y-1.5">
<div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground">{t('Rapid')}</span>
<span className={`text-sm font-mono font-semibold ${overrides.rapid !== 100 ? 'text-primary' : ''}`}>
{overrides.rapid}%
</span>
</div>
<div className="relative h-2 bg-muted rounded-full overflow-hidden">
<div
className={`absolute inset-y-0 left-0 rounded-full transition-all ${
overrides.rapid === 100 ? 'bg-muted-foreground/40' : 'bg-orange-500'
}`}
style={{ width: `${overrides.rapid}%` }}
/>
</div>
<div className="flex items-center gap-1">
<Button variant="outline" size="sm" className="h-7 px-2 text-xs flex-1" onClick={() => sendCommand('rapidOverride', 25)}>
25%
</Button>
<Button variant="outline" size="sm" className="h-7 px-2 text-xs flex-1" onClick={() => sendCommand('rapidOverride', 50)}>
50%
</Button>
<Button variant="outline" size="sm" className="h-7 px-2 text-xs flex-1" onClick={() => sendCommand('rapidOverride', 100)}>
100%
</Button>
</div>
</div>
</div>
)
}
1 change: 1 addition & 0 deletions apps/web/src/services/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export interface MachineStatus {
wpos: { x: string; y: string; z: string } | null
pinState?: string | null // Grbl v1.1: input pin state ('XYZPDHRS' indicates triggered pins)
accessoryState?: string | null // Grbl v1.1: accessory state ('SCFM' indicates spindle/coolant state)
ov?: number[] | null // Grbl v1.1: override values [feed%, rapid%, spindle%]
} | null
parserstate?: {
modal?: {
Expand Down
17 changes: 16 additions & 1 deletion apps/web/src/store/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,20 @@ export const selectFeedrate = createSelector(
}
)

// Computed override values (feed%, rapid%, spindle%)
export const selectOverrideValues = createSelector(
[selectBackendStatus],
(backendStatus) => {
const ov = backendStatus?.controllerState?.ov
if (!ov || ov.length < 3) return { feed: 100, rapid: 100, spindle: 100 }
return {
feed: ov[0],
rapid: ov[1],
spindle: ov[2],
}
}
)

// Computed buffer state
export const selectRxBufferSize = createSelector(
[selectBackendStatus],
Expand Down Expand Up @@ -146,4 +160,5 @@ export const usePlannerQueue = () => useAppSelector(selectPlannerQueue)
export const useWorkflowState = () => useAppSelector(selectWorkflowState)
export const useIsJobRunning = () => useAppSelector(selectIsJobRunning)
export const useIsHomed = () => useAppSelector(selectIsHomed)
export const useCurrentWCS = () => useAppSelector(selectCurrentWCS)
export const useCurrentWCS = () => useAppSelector(selectCurrentWCS)
export const useOverrideValues = () => useAppSelector(selectOverrideValues)
Loading