From 4d01a6a04fb8f2a2baea2d4c29f8b32d057b786b Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Sun, 31 Aug 2025 21:29:00 +0100 Subject: [PATCH] Overhaul printer control 1. --- .../Production/Printers/ControlPrinter.jsx | 1286 +++++------------ .../Dashboard/common/DashboardBreadcrumb.jsx | 3 +- .../Dashboard/common/ObjectInfo.jsx | 37 +- .../common/PrinterTemperaturePanel.jsx | 231 ++- .../Dashboard/context/ApiServerContext.jsx | 172 ++- src/database/models/Printer.js | 15 + 6 files changed, 665 insertions(+), 1079 deletions(-) diff --git a/src/components/Dashboard/Production/Printers/ControlPrinter.jsx b/src/components/Dashboard/Production/Printers/ControlPrinter.jsx index 6b3dab8..76ad5ec 100644 --- a/src/components/Dashboard/Production/Printers/ControlPrinter.jsx +++ b/src/components/Dashboard/Production/Printers/ControlPrinter.jsx @@ -1,947 +1,395 @@ -import { useState, useContext, useCallback, useEffect } from 'react' -import axios from 'axios' +import { useState, useRef, useEffect } from 'react' import { useLocation } from 'react-router-dom' +import { Space, Flex, Card, Splitter, Divider } from 'antd' +import loglevel from 'loglevel' +import config from '../../../../config.js' +import useCollapseState from '../../hooks/useCollapseState.js' +import NotesPanel from '../../common/NotesPanel.jsx' +import InfoCollapse from '../../common/InfoCollapse.jsx' +import ViewButton from '../../common/ViewButton.jsx' +import NoteIcon from '../../../Icons/NoteIcon.jsx' +import ObjectForm from '../../common/ObjectForm.jsx' +import EditButtons from '../../common/EditButtons.jsx' +import LockIndicator from '../../common/LockIndicator.jsx' +import ActionHandler from '../../common/ActionHandler.jsx' +import ObjectActions from '../../common/ObjectActions.jsx' + +import ObjectInfo from '../../common/ObjectInfo.jsx' +import PrinterIcon from '../../../Icons/PrinterIcon.jsx' + +import PrinterTemperaturePanel from '../../common/PrinterTemperaturePanel.jsx' +import PrinterPositionPanel from '../../common/PrinterPositionPanel.jsx' +import PrinterMovementPanel from '../../common/PrinterMovementPanel.jsx' + +import JobIcon from '../../../Icons/JobIcon.jsx' +import SubJobIcon from '../../../Icons/SubJobIcon.jsx' +import FilamentStockIcon from '../../../Icons/FilamentStockIcon.jsx' +import MissingPlaceholder from '../../common/MissingPlaceholder.jsx' import { useMediaQuery } from 'react-responsive' -import { - Button, - message, - Spin, - Flex, - Card, - Dropdown, - Space, - Descriptions, - Progress, - Modal, - Typography, - Badge, - Alert, - Popover, - Checkbox, - Collapse -} from 'antd' -import { LoadingOutlined, CaretLeftOutlined } from '@ant-design/icons' -import { PrintServerContext } from '../../context/PrintServerContext' - -import PrinterTemperaturePanel from '../../common/PrinterTemperaturePanel' -import PrinterPositionPanel from '../../common/PrinterPositionPanel' -import PrinterMovementPanel from '../../common/PrinterMovementPanel' -import PrinterMiscPanel from '../../common/PrinterMiscPanel' -import PrinterState from '../../common/StateDisplay' -import { AuthContext } from '../../context/AuthContext' -import PrinterSubJobsTree from '../../common/PrinterJobsTree' -import IdDisplay from '../../common/IdDisplay' - -import FilamentIcon from '../../../Icons/FilamentIcon' -import FilamentStockIcon from '../../../Icons/FilamentStockIcon' -import ReloadIcon from '../../../Icons/ReloadIcon' - -import GCodeFileIcon from '../../../Icons/GCodeFileIcon' - -import LoadFilamentStock from '../../Inventory/FilamentStocks/LoadFilamentStock' -import UnloadFilamentStock from '../../Inventory/FilamentStocks/UnloadFilamentStock' -import FilamentStockState from '../../common/FilamentStockState' -import useCollapseState from '../../hooks/useCollapseState' - -import config from '../../../../config' -import PlayCircleIcon from '../../../Icons/PlayCircleIcon' -import XMarkCircleIcon from '../../../Icons/XMarkCircleIcon' -import PauseCircleIcon from '../../../Icons/PauseCircleIcon' -import ExclamationOctagonIcon from '../../../Icons/ExclamationOctagonIcon' -import TimeDisplay from '../../common/TimeDisplay' - -const { Text, Title } = Typography - -// Helper function to parse query parameters -const useQuery = () => { - return new URLSearchParams(useLocation().search) -} +const log = loglevel.getLogger('ControlPrinter') +log.setLevel(config.logLevel) const ControlPrinter = () => { - const [messageApi] = message.useMessage() - const query = useQuery() - const printerId = query.get('printerId') + const location = useLocation() + const objectFormRef = useRef(null) + const actionHandlerRef = useRef(null) const isMobile = useMediaQuery({ maxWidth: 768 }) - - const [printerData, setPrinterData] = useState(null) - const [fetchLoading, setFetchLoading] = useState(true) - const [initialized, setInitialized] = useState(false) - const [loadFilamentStockModalOpen, setLoadFilamentStockModalOpen] = - useState(false) - const [unloadFilamentStockModalOpen, setUnloadFilamentStockModalOpen] = - useState(false) - const [klippyErrorModalOpen, setKlippyErrorModalOpen] = useState(false) - const [klippyErrorMessage, setKlippyErrorMessage] = useState('') - const [klippyStartupMessage, setKlippyStartupMessage] = useState('') - + const printerId = new URLSearchParams(location.search).get('printerId') const [collapseState, updateCollapseState] = useCollapseState( 'ControlPrinter', { + printer: true, job: true, - filament: true, - gcodefile: true, - jobs: true + subjob: true, + filamentStock: true, + temperature: true, + position: true, + movement: true } ) - // Load visibility preferences from sessionStorage on component mount - const [componentVisibility, setComponentVisibility] = useState(() => { - const savedVisibility = sessionStorage.getItem('printerControlVisibility') - if (savedVisibility) { - return JSON.parse(savedVisibility) - } - return { - temperature: true, - movement: true, - subjobs: true - } + const [sideBarVisible, setSideBarVisible] = useState( + collapseState.temperature || + collapseState.position || + collapseState.movement + ) + + useEffect(() => { + setSideBarVisible( + collapseState.temperature || + collapseState.position || + collapseState.movement + ) + }, [collapseState]) + + const [objectFormState, setEditFormState] = useState({ + isEditing: false, + editLoading: false, + formValid: false, + locked: false, + loading: false }) - // Save visibility preferences to sessionStorage whenever they change - useEffect(() => { - sessionStorage.setItem( - 'printerControlVisibility', - JSON.stringify(componentVisibility) - ) - }, [componentVisibility]) - - const { printServer } = useContext(PrintServerContext) - const { authenticated } = useContext(AuthContext) - - // Fetch printer details when the component mounts - const fetchPrinterDetails = useCallback(async () => { - if (printerId) { - setFetchLoading(true) - try { - const response = await axios.get( - `${config.backendUrl}/printers/${printerId}`, - { - headers: { - Accept: 'application/json' - }, - withCredentials: true // Important for including cookies - } - ) - setFetchLoading(false) - setPrinterData(response.data) - } catch (error) { - setFetchLoading(false) - if (error.response) { - messageApi.error( - 'Error fetching printer data:', - error.response.status - ) - } else { - messageApi.error( - 'An unexpected error occurred. Please try again later.' - ) - } - } - } - }, [printerId, messageApi]) - - // Add WebSocket event listener for real-time updates - useEffect(() => { - if (printServer && !initialized && printerId) { - setInitialized(true) - printServer.on('notify_printer_update', (statusUpdate) => { - setPrinterData((prevData) => { - if (statusUpdate?._id === printerId) { - return { - ...prevData, - ...statusUpdate - } - } - return prevData - }) - }) - - // Add WebSocket event listener for filament stock updates - printServer.on('notify_filamentstock_update', (filamentStockUpdate) => { - setPrinterData((prevData) => { - if (prevData?.currentFilamentStock) { - if ( - prevData?.currentFilamentStock?._id === filamentStockUpdate?._id - ) { - return { - ...prevData, - currentFilamentStock: { - ...prevData.currentFilamentStock, - ...filamentStockUpdate - } - } - } - } - return prevData - }) - }) - } - return () => { - if (printServer && initialized) { - printServer.off('notify_printer_update') - printServer.off('notify_filamentstock_update') - } - } - }, [printServer, initialized, printerId]) - - function handleEmergencyStop() { - printServer.emit('printer.emergency_stop', { printerId }) - } - - useEffect(() => { - if (authenticated) { - fetchPrinterDetails() - } - }, [authenticated, fetchPrinterDetails]) - - useEffect(() => { - const loadFilamentStock = printerData?.alerts?.find( - (alert) => alert.type === 'loadFilamentStock' - ) - const klippyError = printerData?.alerts?.find( - (alert) => alert.type === 'klippyError' - ) - const klippyStartup = printerData?.alerts?.find( - (alert) => alert.type === 'klippyStartup' - ) - - if (loadFilamentStock) { - setLoadFilamentStockModalOpen(true) - } else { - setLoadFilamentStockModalOpen(false) - } - - if (klippyError) { - setKlippyErrorModalOpen(true) - setKlippyErrorMessage(klippyError.message) - } else { - setKlippyErrorModalOpen(false) - setKlippyErrorMessage('') - } - - if (klippyStartup) { - setKlippyStartupMessage(klippyStartup.message) - } else { - setKlippyStartupMessage('') - } - }, [printerData?.alerts]) - - const actionItems = { - items: [ - { - label: 'Resume Print', - key: 'resumePrint', - icon: , - disabled: printerData?.state?.type !== 'paused' - }, - { - label: 'Pause Print', - key: 'pausePrint', - icon: , - disabled: printerData?.state?.type !== 'printing' - }, - { - label: 'Cancel Print', - key: 'cancelPrint', - icon: , - disabled: !( - printerData?.state?.type === 'printing' || - printerData?.state?.type === 'paused' - ) - }, - { - type: 'divider' - }, - { - label: 'Queue', - key: 'queue', - children: [ - { - label: 'Start Queue', - key: 'startQueue', - disabled: - printerData?.state?.type === 'printing' || - printerData?.state?.type === 'deploying' || - printerData?.state?.type === 'paused' || - printerData?.state?.type === 'error', - - icon: - }, - { - label: 'Pause Queue', - key: 'pauseQueue', - icon: - } - ] - }, - { - label: 'Filament', - key: 'filament', - children: [ - { - label: 'Load Filament Stock', - key: 'loadFilamentStock', - icon: , - disabled: - printerData?.state?.type === 'printing' || - printerData?.state?.type === 'error' || - printerData?.state?.type === 'offline' || - printerData?.currentFilamentStock !== null - }, - { - label: 'Unload Filament Stock', - key: 'unloadFilamentStock', - icon: , - disabled: - printerData?.state?.type === 'printing' || - printerData?.state?.type === 'error' || - printerData?.state?.type === 'offline' || - printerData?.currentFilamentStock === null - }, - { - type: 'divider' - }, - { - label: 'Filament Info', - key: 'filamentInfo', - icon: - } - ] - }, - - { - type: 'divider' - }, - { - label: 'Restart Host', - key: 'restartHost', - icon: - }, - { - label: 'Restart Firmware', - key: 'restartFirmware', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'restartHost') { - printServer.emit('printer.restart', { printerId }) - } else if (key === 'restartFirmware') { - printServer.emit('printer.firmware_restart', { printerId }) - } else if (key === 'resumePrint') { - printServer.emit('printer.print.resume', { printerId }) - } else if (key === 'pausePrint') { - printServer.emit('printer.print.pause', { printerId }) - } else if (key === 'cancelPrint') { - printServer.emit('printer.print.cancel', { printerId }) - } else if (key === 'startQueue') { - printServer.emit('server.job_queue.start', { printerId }) - } else if (key === 'pauseQueue') { - printServer.emit('server.job_queue.pause', { printerId }) - } else if (key === 'loadFilamentStock') { - setLoadFilamentStockModalOpen(true) - } else if (key === 'unloadFilamentStock') { - setUnloadFilamentStockModalOpen(true) - } + const actions = { + reload: () => { + objectFormRef?.current.handleFetchObject() + return true + }, + edit: () => { + objectFormRef?.current.startEditing() + return false + }, + cancelEdit: () => { + objectFormRef?.current.cancelEditing() + return true + }, + finishEdit: () => { + objectFormRef?.current.handleUpdate() + return true } } - const getViewDropdownItems = () => { - return ( - - - { - setComponentVisibility((prev) => ({ - ...prev, - temperature: e.target.checked - })) - }} - > - Temperature Panel - - { - setComponentVisibility((prev) => ({ - ...prev, - position: e.target.checked - })) - }} - > - Position Panel - - { - setComponentVisibility((prev) => ({ - ...prev, - movement: e.target.checked - })) - }} - > - Movement Panel - - { - setComponentVisibility((prev) => ({ - ...prev, - subjobs: e.target.checked - })) - }} - > - Sub Jobs - - { - setComponentVisibility((prev) => ({ - ...prev, - misc: e.target.checked - })) - }} - > - Misc Panel - - - - ) - } + const sideBarItems = ( + + {collapseState.temperature && ( + + + + )} + {collapseState.position && ( + + + + )} + {collapseState.movement && ( + + + + )} + + ) return ( - <> - - - - - - - - - - - - {printerData ? ( - - ) : ( - } size='small' /> - )} - + + + - - - + + - -
- - - - {printerData?.alerts?.some( - (alert) => alert.type === 'klippyError' - ) && } - {printerData?.alerts?.some( - (alert) => alert.type === 'klippyStartup' - ) && } - - updateCollapseState('job', keys.length > 0)} - expandIcon={({ isActive }) => ( - - )} - style={{ padding: 0 }} - className='no-h-padding-collapse no-t-padding-collapse' - > - - - Current Job - - - } - key='1' - > - } - spinning={fetchLoading} - > - - - {printerData?.name ? ( - {printerData.name} - ) : ( - n/a - )} - - - - {printerData?._id ? ( - - ) : ( - n/a - )} - - - - {printerData?.currentJob?.gcodeFile?.name ? ( - - - - {printerData.currentJob?.gcodeFile?.name} - - - ) : ( - n/a - )} - - - {printerData?.currentJob?.gcodeFile ? ( - - ) : ( - n/a - )} - - - - {printerData?.currentJob?.id ? ( - - ) : ( - n/a - )} - - - - {printerData?.currentSubJob?.id ? ( - - ) : ( - n/a - )} - - - {printerData?.state?.type === 'printing' && ( - <> - - - - - {printerData?.currentSubJob?.startedAt ? ( - - ) : ( - n/a - )} - - - )} - - - {printerData?.currentJob?.gcodeFile?.gcodeFileInfo - ?.printSettingsId ? ( - - {printerData.currentJob.gcodeFile.gcodeFileInfo.printSettingsId.replaceAll( - '"', - '' - )} - - ) : ( - n/a - )} - - - - {printerData?.currentJob?.gcodeFile?.gcodeFileInfo - ?.estimatedPrintingTimeNormalMode ? ( - - { - printerData.currentJob.gcodeFile.gcodeFileInfo - .estimatedPrintingTimeNormalMode - } - - ) : ( - n/a - )} - - - - - - - updateCollapseState('filament', keys.length > 0) - } - expandIcon={({ isActive }) => ( - - )} - style={{ padding: 0 }} - className='no-h-padding-collapse' - > - - - Loaded Filament Stock - - - } - key='1' - > - } - spinning={fetchLoading} - > - - - {printerData?.currentFilamentStock ? ( - - ) : ( - n/a - )} - - - {printerData?.currentFilamentStock?._id ? ( - - ) : ( - n/a - )} - - - {printerData?.currentFilamentStock?.filament?.name ? ( - - - - - ) : ( - n/a - )} - - - {printerData?.currentFilamentStock?.filament ? ( - - ) : ( - n/a - )} - - - {printerData?.currentFilamentStock?.currentNetWeight ? ( -
- - - {printerData.currentFilamentStock.currentNetWeight.toFixed( - 2 - ) + 'g'} - - - {printerData.currentFilamentStock.currentGrossWeight.toFixed( - 2 - ) + 'g'} - - -
- ) : ( - n/a - )} -
-
-
- - - - updateCollapseState('jobs', keys.length > 0) - } - expandIcon={({ isActive }) => ( - - )} - style={{ padding: 0 }} - className='no-h-padding-collapse' - > - - - Printer Jobs - - - } - key='1' - > - - - - - - {componentVisibility.temperature && ( - } - spinning={fetchLoading} - > - - - - - )} - - {componentVisibility.position && ( - } - spinning={fetchLoading} - > - - - - - )} - - {componentVisibility.movement && ( - } - spinning={fetchLoading} - > - - - - - )} - - {componentVisibility.misc && ( - } - spinning={fetchLoading} - > - - - - - )} - - -
+ + + + { + actionHandlerRef.current.callAction('finishEdit') + }} + cancelEditing={() => { + actionHandlerRef.current.callAction('cancelEdit') + }} + startEditing={() => { + actionHandlerRef.current.callAction('edit') + }} + editLoading={objectFormState.editLoading} + formValid={objectFormState.formValid} + disabled={objectFormState.lock?.locked || objectFormState.loading} + loading={objectFormState.editLoading} + /> +
- { - setLoadFilamentStockModalOpen(false) - }} - > - { - setLoadFilamentStockModalOpen(false) - messageApi.success('New print job created successfully.') - }} - isFilamentLoaded={false} - printer={printerData} - reset={loadFilamentStockModalOpen} - /> - - { - setUnloadFilamentStockModalOpen(false) - }} - > - { - setUnloadFilamentStockModalOpen(false) - messageApi.success('Filament unloaded successfully.') - }} - printer={printerData} - reset={unloadFilamentStockModalOpen} - /> - - - - Klipper Error - - } - onCancel={() => setKlippyErrorModalOpen(false)} - footer={[ - , - - ]} - > - {klippyErrorMessage} - - + + + + +
+ +
) } diff --git a/src/components/Dashboard/common/DashboardBreadcrumb.jsx b/src/components/Dashboard/common/DashboardBreadcrumb.jsx index 5494278..776a472 100644 --- a/src/components/Dashboard/common/DashboardBreadcrumb.jsx +++ b/src/components/Dashboard/common/DashboardBreadcrumb.jsx @@ -12,7 +12,8 @@ const breadcrumbNameMap = { developer: 'Developer', overview: 'Overview', info: 'Info', - design: 'Design' + design: 'Design', + control: 'Control' } const mainSections = ['production', 'inventory', 'management', 'developer'] diff --git a/src/components/Dashboard/common/ObjectInfo.jsx b/src/components/Dashboard/common/ObjectInfo.jsx index 97e66d5..f6a3299 100644 --- a/src/components/Dashboard/common/ObjectInfo.jsx +++ b/src/components/Dashboard/common/ObjectInfo.jsx @@ -8,11 +8,21 @@ const ObjectInfo = ({ loading = false, isEditing = false, type = 'unknown', + showHyperlink, + showLabels = true, objectData = null, properties = [], required = undefined, visibleProperties = {}, objectPropertyProps = {}, + column = { + xs: 1, + sm: 1, + md: 1, + lg: 2, + xl: 2, + xxl: 2 + }, ...rest }) => { const allItems = getModelProperties(type) @@ -47,33 +57,29 @@ const ObjectInfo = ({ const key = item.name || item.label || idx return { key, - label: ( - - {item.label} - - ), + label: + showLabels == true ? ( + + {item.label} + + ) : null, children: ( - ) + ), + span: item?.span || undefined } }) return ( }> { const [temperatureData, setTemperatureData] = useState({ - hotEnd: {}, - heatedBed: {} + extruder: { + current: 0, + target: 0, + power: 0 + }, + bed: { + current: 0, + target: 0, + power: 0 + }, + pinda: 0, + ambiant: 0 }) - const [hotEndTemperature, setHotEndTemperature] = useState( - temperatureData?.hotEnd?.target || 0 - ) - const [heatedBedTemperature, setHeatedBedTemperature] = useState( - temperatureData?.heatedBed?.target || 0 - ) - const { printServer } = useContext(PrintServerContext) + const { subscribeToObjectEvent, connected } = useContext(ApiServerContext) - const notifyTemperatureStatusUpdate = useCallback((statusUpdate) => { - setTemperatureData((prev) => { - const temperatureObject = { - ...prev - } - if (statusUpdate?.extruder?.temperature !== undefined) { - temperatureObject.hotEnd.current = statusUpdate?.extruder?.temperature - } - - if (statusUpdate?.heater_bed?.temperature !== undefined) { - temperatureObject.heatedBed.current = - statusUpdate?.heater_bed?.temperature - } - - if (statusUpdate?.extruder?.target !== undefined) { - temperatureObject.hotEnd.target = statusUpdate?.extruder?.target - setHotEndTemperature(statusUpdate?.extruder?.target) - } - - if (statusUpdate?.heater_bed?.target !== undefined) { - temperatureObject.heatedBed.target = statusUpdate?.heater_bed?.target - setHeatedBedTemperature(statusUpdate?.heater_bed?.target) - } - - if (statusUpdate?.extruder?.power !== undefined) { - temperatureObject.hotEnd.power = statusUpdate?.extruder?.power - } - - if (statusUpdate?.heater_bed?.power !== undefined) { - temperatureObject.heatedBed.power = statusUpdate?.heater_bed?.power - } - - return temperatureObject - }) - }, []) + // Sync input values with actual temperature targets + useEffect(() => { + if (temperatureData.extruder?.target !== undefined) { + setExtruderTarget(temperatureData.extruder.target) + } + }, [temperatureData.extruder?.target]) useEffect(() => { - const params = { - printerId, - objects: { - extruder: null, - heater_bed: null // eslint-disable-line - } + if (temperatureData.bed?.target !== undefined) { + setBedTarget(temperatureData.bed.target) } - if (printServer?.connected == true) { - printServer.emit('printer.objects.subscribe', params) - printServer.emit('printer.objects.query', params) - printServer.on('notify_status_update', notifyTemperatureStatusUpdate) - } - return () => { - if (printServer && shouldUnsubscribe == true) { - printServer.off('notify_status_update', notifyTemperatureStatusUpdate) - printServer.emit('printer.objects.unsubscribe', params) - } - } - }, [printServer, printerId, notifyTemperatureStatusUpdate, shouldUnsubscribe]) + }, [temperatureData.bed?.target]) - const handleSetTemperatureClick = (target, value) => { - if (printServer) { - printServer.emit('printer.gcode.script', { - printerId, - script: `SET_HEATER_TEMPERATURE HEATER=${target} TARGET=${value}` - }) + useEffect(() => { + if (id && connected) { + const temperatureEventUnsubscribe = subscribeToObjectEvent( + id, + 'printer', + 'temperature', + (event) => { + setTemperatureData((prev) => { + const merged = merge({}, prev, event.data) + return merged + }) + } + ) + return () => { + if (temperatureEventUnsubscribe) temperatureEventUnsubscribe() + } + } + }, [id, connected]) + + const [extruderTarget, setExtruderTarget] = useState(0) + const [bedTarget, setBedTarget] = useState(0) + + const handleSetTemperature = (data) => { + if (id && connected == true) { + console.log(data) + //sendObjectAction(id, 'printer', { type: 'setTemperature', data }) } } @@ -126,33 +106,32 @@ const PrinterTemperaturePanel = ({ Hot End Power:{' '} - {Math.round((temperatureData.hotEnd.power || 0) * 100)}% + {Math.round((temperatureData.extruder.power || 0) * 100)}% - Bed Power:{' '} - {Math.round((temperatureData.heatedBed.power || 0) * 100)}% + Bed Power: {Math.round((temperatureData.bed.power || 0) * 100)}% - {typeof temperatureData.pindaTemp !== 'undefined' && ( + {typeof temperatureData.pinda !== 'undefined' && ( Pinda Temp: {temperatureData.pindaTemp}°C )} - {typeof temperatureData.ambiantActual !== 'undefined' && ( + {typeof temperatureData.ambiant !== 'undefined' && ( - Ambient Actual: {temperatureData.ambiantActual}°C + Ambient Actual: {temperatureData.ambiant}°C )} @@ -165,41 +144,41 @@ const PrinterTemperaturePanel = ({
{temperatureData ? ( - {temperatureData.hotEnd && showHotEnd && ( + {temperatureData.extruder && showExtruder && ( - Hot End: {temperatureData.hotEnd.current}°C /{' '} - {temperatureData.hotEnd.target}°C + Hot End: {temperatureData.extruder.current}°C /{' '} + {temperatureData.extruder.target}°C - {showHotEndControls && ( + {showExtruderControls && ( setHotEndTemperature(value)} - onPressEnter={() => - handleSetTemperatureClick('extruder', hotEndTemperature) - } + onChange={(value) => setExtruderTarget(value || 0)} + onPressEnter={handleSetTemperature({ + extruder: { target: extruderTarget } + })} /> @@ -208,7 +187,9 @@ const PrinterTemperaturePanel = ({ type='default' size='small' style={{ width: 40 }} - onClick={() => handleSetTemperatureClick('extruder', 0)} + onClick={() => + handleSetTemperature({ extruder: { target: 0 } }) + } > Off @@ -217,50 +198,44 @@ const PrinterTemperaturePanel = ({ )} - {temperatureData.heatedBed && showHeatedBed && ( + {temperatureData.bed && showBed && ( - Heated Bed: {temperatureData.heatedBed.current}°C /{' '} - {temperatureData.heatedBed.target}°C + Heated Bed: {temperatureData.bed.current}°C /{' '} + {temperatureData.bed.target}°C - {showHeatedBedControls && ( + {showBedControls && ( setHeatedBedTemperature(value)} - onPressEnter={() => - handleSetTemperatureClick( - 'heater_bed', - heatedBedTemperature - ) - } + onChange={(value) => setBedTarget(value || 0)} + onPressEnter={handleSetTemperature({ + bed: { target: bedTarget } + })} /> @@ -269,7 +244,11 @@ const PrinterTemperaturePanel = ({ type='default' size='small' style={{ width: 40 }} - onClick={() => handleSetTemperatureClick('heater_bed', 0)} + onClick={() => + handleSetTemperature({ + bed: { target: 0 } + }) + } > Off @@ -298,11 +277,11 @@ const PrinterTemperaturePanel = ({ } PrinterTemperaturePanel.propTypes = { - printerId: PropTypes.string.isRequired, - showHotEndControls: PropTypes.bool, - showHeatedBedControls: PropTypes.bool, - showHotEnd: PropTypes.bool, - showHeatedBed: PropTypes.bool, + id: PropTypes.string.isRequired, + showExtruderControls: PropTypes.bool, + showBedControls: PropTypes.bool, + showExtruder: PropTypes.bool, + showBed: PropTypes.bool, showMoreInfo: PropTypes.bool, shouldUnsubscribe: PropTypes.bool } diff --git a/src/components/Dashboard/context/ApiServerContext.jsx b/src/components/Dashboard/context/ApiServerContext.jsx index 004bb43..faec43e 100644 --- a/src/components/Dashboard/context/ApiServerContext.jsx +++ b/src/components/Dashboard/context/ApiServerContext.jsx @@ -93,7 +93,9 @@ const ApiServerProvider = ({ children }) => { }) newSocket.on('objectUpdate', handleObjectUpdate) + newSocket.on('objectEvent', handleObjectEvent) newSocket.on('objectNew', handleObjectNew) + newSocket.on('objectDelete', handleObjectDelete) newSocket.on('lockUpdate', handleLockUpdate) newSocket.on('disconnect', () => { @@ -216,6 +218,37 @@ const ApiServerProvider = ({ children }) => { } } + const handleObjectEvent = async (data) => { + const id = data._id + const objectType = data.objectType + + const callbacksRefKey = `${objectType}:${id}:events:${data.event.type}` + logger.debug('Notifying object event:', data) + if ( + id && + objectType && + subscribedCallbacksRef.current.has(callbacksRefKey) + ) { + const callbacks = subscribedCallbacksRef.current.get(callbacksRefKey) + logger.debug( + `Calling ${callbacks.length} callbacks for object:`, + callbacksRefKey + ) + callbacks.forEach((callback) => { + try { + callback(data.event) + } catch (error) { + logger.error('Error in object event callback:', error) + } + }) + } else { + logger.debug( + `No callbacks found for object: ${callbacksRefKey}, subscribed callbacks:`, + Array.from(subscribedCallbacksRef.current.keys()) + ) + } + } + const handleObjectNew = async (data) => { logger.debug('Notifying object new:', data) const objectType = data.objectType || 'unknown' @@ -241,6 +274,31 @@ const ApiServerProvider = ({ children }) => { } } + const handleObjectDelete = async (data) => { + logger.debug('Notifying object delete:', data) + const objectType = data.objectType || 'unknown' + + if (objectType && subscribedCallbacksRef.current.has(objectType)) { + const callbacks = subscribedCallbacksRef.current.get(objectType) + logger.debug( + `Calling ${callbacks.length} callbacks for type:`, + objectType + ) + callbacks.forEach((callback) => { + try { + callback(data.object) + } catch (error) { + logger.error('Error in object new callback:', error) + } + }) + } else { + logger.debug( + `No callbacks found for object: ${objectType}, subscribed callbacks:`, + Array.from(subscribedCallbacksRef.current.keys()) + ) + } + } + const offObjectUpdatesEvent = useCallback((id, objectType, callback) => { if (socketRef.current && socketRef.current.connected == true) { const callbacksRefKey = `${objectType}:${id}` @@ -251,7 +309,10 @@ const ApiServerProvider = ({ children }) => { .filter((cb) => cb !== callback) if (callbacks.length === 0) { subscribedCallbacksRef.current.delete(callbacksRefKey) - socketRef.current.emit('unsubscribe', { id: id, type: objectType }) + socketRef.current.emit('unsubscribeObjectUpdate', { + id: id, + objectType: objectType + }) } else { subscribedCallbacksRef.current.set(callbacksRefKey, callbacks) } @@ -262,14 +323,22 @@ const ApiServerProvider = ({ children }) => { const offObjectTypeUpdatesEvent = useCallback((objectType, callback) => { if (socketRef.current && socketRef.current.connected == true) { // Remove callback from the subscribed callbacks map - console.log('Unsubscribing from type') + console.log( + 'Unsubscribing from type', + objectType, + subscribedCallbacksRef.current.has(objectType) + ) if (subscribedCallbacksRef.current.has(objectType)) { const callbacks = subscribedCallbacksRef.current .get(objectType) .filter((cb) => cb !== callback) + console.log('API: CALLBACKS', callbacks) + if (callbacks.length === 0) { subscribedCallbacksRef.current.delete(objectType) - socketRef.current.emit('unsubscribe', { objectType: objectType }) + socketRef.current.emit('unsubscribeObjectTypeUpdate', { + objectType: objectType + }) } else { subscribedCallbacksRef.current.set(objectType, callbacks) } @@ -342,7 +411,7 @@ const ApiServerProvider = ({ children }) => { } } ) - logger.debug('Registered update event listener for object:', objectType) + logger.debug('Registered type event listener for object:', objectType) // Return cleanup function return () => offObjectTypeUpdatesEvent(objectType, callback) @@ -369,6 +438,84 @@ const ApiServerProvider = ({ children }) => { } }, []) + const offObjectEventEvent = useCallback( + (id, objectType, eventType, callback) => { + if (socketRef.current && socketRef.current.connected == true) { + const callbacksRefKey = `${objectType}:${id}:events:${eventType}` + // Remove callback from the subscribed callbacks map + if (subscribedCallbacksRef.current.has(callbacksRefKey)) { + const callbacks = subscribedCallbacksRef.current + .get(callbacksRefKey) + .filter((cb) => cb !== callback) + if (callbacks.length === 0) { + subscribedCallbacksRef.current.delete(callbacksRefKey) + socketRef.current.emit('unsubscribeObjectEvent', { + _id: id, + objectType, + eventType + }) + } else { + subscribedCallbacksRef.current.set(callbacksRefKey, callbacks) + } + } + } + }, + [] + ) + + const subscribeToObjectEvent = useCallback( + (id, objectType, eventType, callback) => { + if (socketRef.current && socketRef.current.connected == true) { + const callbacksRefKey = `${objectType}:${id}:events:${eventType}` + // Add callback to the subscribed callbacks map immediately + if (!subscribedCallbacksRef.current.has(callbacksRefKey)) { + subscribedCallbacksRef.current.set(callbacksRefKey, []) + } + + const callbacksLength = + subscribedCallbacksRef.current.get(callbacksRefKey).length + + if (callbacksLength <= 0) { + socketRef.current.emit( + 'subscribeToObjectEvent', + { + _id: id, + objectType: objectType, + eventType: eventType + }, + (result) => { + if (result.success) { + logger.info( + 'Subscribed to event id:', + id, + 'objectType:', + objectType, + 'eventType:', + eventType + ) + } + } + ) + } + logger.info( + 'Adding event callback id:', + id, + 'objectType:', + objectType, + 'eventType:', + eventType, + 'callbacks length:', + callbacksLength + 1 + ) + subscribedCallbacksRef.current.get(callbacksRefKey).push(callback) + + // Return cleanup function + return () => offObjectEventEvent(id, objectType, eventType, callback) + } + }, + [offObjectUpdatesEvent] + ) + const subscribeToObjectLock = useCallback( (id, type, callback) => { logger.debug('Subscribing to lock for object:', id, 'type:', type) @@ -548,13 +695,6 @@ const ApiServerProvider = ({ children }) => { } }) logger.debug('Object updated successfully') - if (socketRef.current && socketRef.current.connected == true) { - await socketRef.current.emit('update', { - _id: id, - type: type, - updatedAt: response.data.updatedAt - }) - } return response.data } catch (err) { console.error(err) @@ -577,13 +717,6 @@ const ApiServerProvider = ({ children }) => { } }) logger.debug('Object deleted successfully') - if (socketRef.current && socketRef.current.connected == true) { - await socketRef.current.emit('update', { - _id: id, - type: type, - updatedAt: response.data.updatedAt - }) - } return response.data } catch (err) { console.error(err) @@ -649,7 +782,7 @@ const ApiServerProvider = ({ children }) => { try { const response = await axios.get(`${config.backendUrl}/notes`, { params: { - parent: parentId, + 'parent._id': parentId, sort: 'createdAt', order: 'ascend' }, @@ -755,6 +888,7 @@ const ApiServerProvider = ({ children }) => { createObject, deleteObject, subscribeToObjectUpdates, + subscribeToObjectEvent, subscribeToObjectTypeUpdates, subscribeToObjectLock, fetchObject, diff --git a/src/database/models/Printer.js b/src/database/models/Printer.js index 5aed554..df3f019 100644 --- a/src/database/models/Printer.js +++ b/src/database/models/Printer.js @@ -143,6 +143,21 @@ export const Printer = { type: 'text', required: false, readOnly: true + }, + { + name: 'currentFilamentStock', + label: 'Filament Stock', + type: 'object', + objectType: 'filamentStock', + required: true + }, + { + name: 'currentFilamentStock._id', + label: 'Filament Stock ID', + type: 'id', + objectType: 'filamentStock', + showHyperlink: true, + readOnly: true } ] }