Update dependencies, improve document printer management, and add workspace configuration
Some checks failed
farmcontrol/farmcontrol-server/pipeline/head There was a failure building this commit

This commit is contained in:
Tom Butcher 2026-06-15 00:53:28 +01:00
parent 3cc2cbe020
commit ffa6d84251
11 changed files with 1959 additions and 1417 deletions

View File

@ -5,7 +5,7 @@
"apiUrl": "https://dev.tombutcher.work/api",
"host": {
"id": "691a1db49ce913faf0e51284",
"authCode": "FvD3qnNh8FP_xJShlECfYshqQawfD5oPP4xlGOFV2vQIDPRxkAjH4rO6sIgpLucX"
"authCode": "9uu3DC0si__-F9FnGWTKudle5z6yZasFMlKohnShElPekRYteh-LlZaksHOXFfOO"
}
},
"production": {

View File

@ -20,45 +20,46 @@
"author": "Tom Butcher",
"license": "ISC",
"dependencies": {
"axios": "^1.13.2",
"canvas": "^3.2.0",
"axios": "^1.16.1",
"canvas": "^3.2.3",
"etcd3": "^1.1.2",
"express": "^5.1.0",
"express": "^5.2.1",
"form-data": "^4.0.5",
"ipp": "^2.0.1",
"jsonwebtoken": "^9.0.2",
"jsonwebtoken": "^9.0.3",
"keycloak-connect": "^26.1.1",
"lodash": "^4.17.21",
"lodash": "^4.18.1",
"log4js": "^6.9.1",
"mongoose": "^9.0.0",
"mongoose": "^9.6.2",
"node-cache": "^5.1.2",
"node-thermal-printer": "^4.5.0",
"pdf-to-img": "^5.0.0",
"node-thermal-printer": "^4.6.0",
"pdf-to-img": "^6.1.0",
"sharp": "^0.34.5",
"socket.io": "^4.8.1",
"socket.io-client": "^4.8.1",
"ws": "^8.18.3"
"socket.io": "^4.8.3",
"socket.io-client": "^4.8.3",
"ws": "^8.20.1"
},
"devDependencies": {
"@ant-design/icons": "^6.1.0",
"antd": "^5.28.0",
"@electron/rebuild": "^4.0.1",
"@vitejs/plugin-react": "^5.1.1",
"@ant-design/icons": "^6.2.3",
"@electron/rebuild": "^4.0.4",
"@vitejs/plugin-react": "^6.0.2",
"antd": "^5.29.2",
"concurrently": "^9.2.1",
"cross-env": "^10.1.0",
"electron": "^38.7.1",
"electron-builder": "^26.0.12",
"jest": "^30.2.0",
"nodemon": "^3.1.11",
"electron": "^42.1.0",
"electron-builder": "^26.8.1",
"jest": "^30.4.2",
"nodemon": "^3.1.14",
"pkg": "^5.8.1",
"prop-types": "^15.8.1",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"rimraf": "^6.1.2",
"shx": "^0.3.4",
"supertest": "^7.1.4",
"vite": "^7.2.4",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"rimraf": "^6.1.3",
"shx": "^0.4.0",
"supertest": "^7.2.2",
"vite": "^8.0.13",
"vite-plugin-svgo": "^2.0.0",
"vite-plugin-svgr": "^4.5.0"
"vite-plugin-svgr": "^5.2.0"
},
"pkg": {
"assets": [

2922
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

9
pnpm-workspace.yaml Normal file
View File

@ -0,0 +1,9 @@
allowBuilds:
canvas: true
chromedriver: true
electron-winstaller: true
electron: true
esbuild: true
protobufjs: true
sharp: true
unrs-resolver: true

View File

@ -15,7 +15,7 @@ export class DocumentPrinterClient {
documentPrinter = {
connection: { interface: "cups", host: "localhost", port: 9100 },
},
documentPrinterManager
documentPrinterManager,
) {
this.id = documentPrinter._id;
this.documentPrinter = documentPrinter;
@ -23,9 +23,10 @@ export class DocumentPrinterClient {
this.queue = [];
this.documentPrinterManager = documentPrinterManager;
this.currentJob = null;
this.active = documentPrinter.active == true;
this.socketClient = documentPrinterManager.socketClient;
this.interface = documentPrinter.connection.interface || "cups"; // cups, receipt, or os
this.state = { type: "offline" };
this.state = { type: this.active == true ? "offline" : "inactive" };
this.isOnline = documentPrinter.online || false;
this.shouldReconnect = true;
this.isProcessingQueue = false;
@ -38,7 +39,7 @@ export class DocumentPrinterClient {
initializeInterface() {
logger.info(
`Initializing ${this.interface} interface for document printer ${this.id}`
`Initializing ${this.interface} interface for document printer ${this.id}`,
);
switch (this.interface) {
case "cups":
@ -104,6 +105,16 @@ export class DocumentPrinterClient {
await this.reconnect();
}
}
this.documentPrinter = { ...this.documentPrinter, ...data };
if (Object.hasOwn(data || {}, "active") && data.active != this.active) {
if (data.active == true) {
await this.setActive();
} else {
await this.setInactive();
}
}
}
registerEventHandlers() {
@ -126,8 +137,18 @@ export class DocumentPrinterClient {
}
async connect() {
if (this.active == false) {
logger.info(
`Document printer ${this.id} is not active, skipping connection`,
);
this.shouldReconnect = false;
this.isOnline = false;
this.state = { type: "inactive" };
await this.updateDocumentPrinterState();
return false;
}
logger.info(
`Connecting to document printer ${this.id} (${this.interface})`
`Connecting to document printer ${this.id} (${this.interface})`,
);
clearTimeout(this.reconnectTimeout);
@ -140,7 +161,7 @@ export class DocumentPrinterClient {
if (!this.printerInterface) {
logger.error(
`Cannot connect: No interface initialized for ${this.interface}`
`Cannot connect: No interface initialized for ${this.interface}`,
);
return false;
}
@ -150,7 +171,7 @@ export class DocumentPrinterClient {
if (result.error) {
logger.error(
`Error connecting to document printer ${this.documentPrinter.name}:`,
result.error
result.error,
);
this.isOnline = false;
this.state = { type: "offline", message: result.error };
@ -158,27 +179,37 @@ export class DocumentPrinterClient {
return false;
}
logger.info(
`Connected to document printer ${this.documentPrinter.name} (${this.interface})`
`Connected to document printer ${this.documentPrinter.name} (${this.interface})`,
);
return true;
}
async reconnect() {
if (this.active == false) {
logger.info(
`Document printer ${this.documentPrinter.name} is inactive, skipping reconnect`,
);
this.shouldReconnect = false;
this.isOnline = false;
this.state = { type: "inactive" };
await this.updateDocumentPrinterState();
return false;
}
if (this.isOnline == true) {
logger.info(
`Disconnecting from document printer ${this.documentPrinter.name} before reconnecting...`
`Disconnecting from document printer ${this.documentPrinter.name} before reconnecting...`,
);
await this.disconnect();
}
logger.info(
`Reconnecting to document printer ${this.documentPrinter.name}`
`Reconnecting to document printer ${this.documentPrinter.name}`,
);
this.shouldReconnect = true;
const connectResult = await this.connect();
if (connectResult == false) {
logger.error(
`Error reconnecting to document printer ${this.documentPrinter.name}:`,
connectResult.error
connectResult.error,
);
if (this.shouldReconnect) {
// Attempt to reconnect after delay
@ -190,7 +221,7 @@ export class DocumentPrinterClient {
if (initializeResult == false) {
logger.error(
`Error initializing document printer ${this.documentPrinter.name}:`,
initializeResult.error
initializeResult.error,
);
if (this.shouldReconnect) {
// Attempt to reconnect after delay
@ -213,7 +244,7 @@ export class DocumentPrinterClient {
if (result.error) {
logger.error(
`Error initializing document printer ${this.documentPrinter.name}:`,
result.error
result.error,
);
this.state = { type: "offline", message: result.error };
await this.updateDocumentPrinterState();
@ -223,7 +254,7 @@ export class DocumentPrinterClient {
await this.updateDocumentPrinterState();
this.eventUpdateInterval = setInterval(
this.handleEventUpdate.bind(this),
3000
3000,
);
return true;
}
@ -236,7 +267,7 @@ export class DocumentPrinterClient {
} catch (error) {
logger.error(
`Error retrieving status for document printer ${this.documentPrinter.name}:`,
error
error,
);
}
}
@ -272,19 +303,19 @@ export class DocumentPrinterClient {
async deployDocumentJob(documentJob) {
logger.info(
`Deploying document job ${documentJob._id} to ${this.documentPrinter.name}`
`Deploying document job ${documentJob._id} to ${this.documentPrinter.name}`,
);
if (!this.isOnline) {
logger.error(
`Cannot deploy job: Document printer not connected (${this.documentPrinter.name})`
`Cannot deploy job: Document printer not connected (${this.documentPrinter.name})`,
);
return { error: "Document printer not connected" };
}
if (!this.printerInterface) {
logger.error(
`Cannot deploy job: No interface initialized (${this.documentPrinter.name})`
`Cannot deploy job: No interface initialized (${this.documentPrinter.name})`,
);
return { error: "No interface initialized" };
}
@ -310,7 +341,7 @@ export class DocumentPrinterClient {
});
if (!documentTemplate) {
logger.error(
`Document template not found for job ${documentJob._id}`
`Document template not found for job ${documentJob._id}`,
);
return { error: "Document template not found" };
}
@ -325,20 +356,20 @@ export class DocumentPrinterClient {
});
if (!pdfObj) {
logger.error(
`Failed to render document template for job ${documentJob._id}`
`Failed to render document template for job ${documentJob._id}`,
);
return { error: "Failed to render document template" };
}
const result = await this.printerInterface.deploy(
documentJob,
pdfObj.pdf
pdfObj.pdf,
);
await this.updateJobState(documentJob._id, {
type: "deploying",
progress: 1.0,
});
logger.info(
`Deployed document job ${documentJob._id} to ${this.documentPrinter.name}`
`Deployed document job ${documentJob._id} to ${this.documentPrinter.name}`,
);
await this.updateJobState(documentJob._id, {
type: "queued",
@ -349,21 +380,21 @@ export class DocumentPrinterClient {
this.queue.push(documentJob._id);
} else {
logger.warn(
`Job ${documentJob._id} is already in the queue for ${this.documentPrinter.name}`
`Job ${documentJob._id} is already in the queue for ${this.documentPrinter.name}`,
);
}
this.startQueue();
return result;
} else {
logger.error(
`Interface ${this.interface} does not support deployDocumentJob`
`Interface ${this.interface} does not support deployDocumentJob`,
);
return { error: "Interface does not support this operation" };
}
} catch (error) {
logger.error(
`Error deploying document job to ${this.documentPrinter.name}:`,
error
error,
);
await this.updateJobState(documentJob._id, {
type: "error",
@ -377,20 +408,20 @@ export class DocumentPrinterClient {
async startQueue() {
if (!this.isOnline) {
logger.error(
`Cannot start queue: Document printer not connected (${this.documentPrinter.name})`
`Cannot start queue: Document printer not connected (${this.documentPrinter.name})`,
);
return { error: "Document printer not connected" };
}
if (!this.printerInterface) {
logger.error(
`Cannot start queue: No interface initialized (${this.documentPrinter.name})`
`Cannot start queue: No interface initialized (${this.documentPrinter.name})`,
);
return { error: "No interface initialized" };
}
// Prevent concurrent queue processing
if (this.isProcessingQueue) {
logger.debug(
`Queue is already being processed for ${this.documentPrinter.name}`
`Queue is already being processed for ${this.documentPrinter.name}`,
);
return { info: "Queue is already being processed" };
}
@ -404,19 +435,19 @@ export class DocumentPrinterClient {
async runQueue() {
if (!this.isOnline) {
logger.error(
`Cannot print next job: Document printer not connected (${this.documentPrinter.name})`
`Cannot print next job: Document printer not connected (${this.documentPrinter.name})`,
);
return { error: "Document printer not connected" };
}
if (!this.printerInterface) {
logger.error(
`Cannot print next job: No interface initialized (${this.documentPrinter.name})`
`Cannot print next job: No interface initialized (${this.documentPrinter.name})`,
);
return { error: "No interface initialized" };
}
if (this.state.type != "standby") {
logger.error(
`Cannot print next job: Document printer not in standby mode (${this.documentPrinter.name})`
`Cannot print next job: Document printer not in standby mode (${this.documentPrinter.name})`,
);
return { error: "Document printer not in standby mode" };
}
@ -424,7 +455,7 @@ export class DocumentPrinterClient {
// Prevent concurrent queue processing
if (this.isProcessingQueue) {
logger.debug(
`Queue is already being processed for ${this.documentPrinter.name}`
`Queue is already being processed for ${this.documentPrinter.name}`,
);
return { info: "Queue is already being processed" };
}
@ -439,7 +470,7 @@ export class DocumentPrinterClient {
// Re-check connection status before each job
if (!this.isOnline) {
logger.error(
`Printer went offline while printing (${this.documentPrinter.name})`
`Printer went offline while printing (${this.documentPrinter.name})`,
);
this.state = {
type: "offline",
@ -450,7 +481,7 @@ export class DocumentPrinterClient {
}
if (!this.printerInterface) {
logger.error(
`Printer interface lost while printing (${this.documentPrinter.name})`
`Printer interface lost while printing (${this.documentPrinter.name})`,
);
this.state = {
type: "offline",
@ -472,7 +503,7 @@ export class DocumentPrinterClient {
await this.printerInterface.print(jobId);
logger.info(
`Successfully printed job ${jobId} for ${this.documentPrinter.name}`
`Successfully printed job ${jobId} for ${this.documentPrinter.name}`,
);
// Only remove job from queue after successful printing
this.queue.shift();
@ -480,7 +511,7 @@ export class DocumentPrinterClient {
} catch (error) {
logger.error(
`Error printing job ${jobId} for ${this.documentPrinter.name}:`,
error
error,
);
// Remove failed job from queue to prevent infinite retry loop
@ -492,7 +523,7 @@ export class DocumentPrinterClient {
}
logger.info(
`Finished printing all jobs for ${this.documentPrinter.name}`
`Finished printing all jobs for ${this.documentPrinter.name}`,
);
this.state = { type: "standby", message: null };
await this.updateDocumentPrinterState();
@ -513,7 +544,7 @@ export class DocumentPrinterClient {
await this.printerInterface.disconnect();
}
this.isOnline = false;
this.state = { type: "offline" };
this.state = { type: this.active == false ? "inactive" : "offline" };
this.isProcessingQueue = false;
this.queue = []; // Clear queue on disconnect
await this.updateDocumentPrinterState();
@ -521,4 +552,21 @@ export class DocumentPrinterClient {
logger.info(`Successfully disconnected from ${this.documentPrinter.name}`);
return true;
}
async setInactive() {
this.active = false;
this.documentPrinter.active = false;
this.isOnline = false;
await this.disconnect();
this.state = { type: "inactive" };
await this.updateDocumentPrinterState();
}
async setActive() {
this.active = true;
this.documentPrinter.active = true;
this.shouldReconnect = true;
this.isOnline = false;
await this.reconnect();
}
}

View File

@ -86,7 +86,7 @@ export class DocumentPrinterManager {
logger.debug("Handling document printer update for id:", id);
const documentPrinter = this.getDocumentPrinterClient(id);
if (documentPrinter) {
documentPrinter.updateDocumentPrinter(data.object);
await documentPrinter.updateDocumentPrinter(data.object);
}
}

View File

@ -64,8 +64,8 @@ const App = () => {
prev.map((printer) =>
printer._id === newPrinter._id
? _.merge(printer, newPrinter)
: printer
)
: printer,
),
);
});
@ -73,7 +73,7 @@ const App = () => {
"setDocumentPrinters",
(newDocumentPrinters) => {
setDocumentPrinters(newDocumentPrinters);
}
},
);
window.electronAPI.onIPCData("setDocumentPrinter", (newDocumentPrinter) => {
@ -81,8 +81,8 @@ const App = () => {
prev.map((documentPrinter) =>
documentPrinter._id === newDocumentPrinter._id
? _.merge(documentPrinter, newDocumentPrinter)
: documentPrinter
)
: documentPrinter,
),
);
});
@ -229,7 +229,7 @@ const App = () => {
className="ant-menu-horizontal ant-menu-light electron-drag-area"
style={{
lineHeight: "40px",
padding: "0 8px 0 75px",
padding: "0 8px 0 83px",
}}
justify="space-between"
>
@ -246,6 +246,7 @@ const App = () => {
flexWrap: "wrap",
border: 0,
lineHeight: "38px",
marginTop: "1px",
}}
overflowedIndicator={
<Button type="text" icon={<MenuOutlined />} />

View File

@ -60,6 +60,14 @@ const StateTag = ({ state, showBadge = true, style = {} }) => {
status = "success";
text = "Unconsumed";
break;
case "inactive":
status = "default";
text = "Inactive";
break;
case "connecting":
status = "warning";
text = "Connecting";
break;
case "error":
status = "error";
text = "Error";

View File

@ -23,7 +23,11 @@ export class PrinterClient {
this.socketClient = printerManager.socketClient;
this.database = new PrinterDatabase(this.socketClient, this.printer);
this.printerFileManager = new PrinterFileManager(this);
this.state = { type: "offline", message: "Moonraker disconnected." };
this.active = printer.active !== false;
this.state =
this.active == true
? { type: "offline", message: "Moonraker disconnected." }
: { type: "inactive" };
this.klippyState = { type: "offline", message: "Klippy disconnected." };
this.config = printer.moonraker;
this.version = printer.version;
@ -37,10 +41,11 @@ export class PrinterClient {
this.motionObject = {};
this.miscObject = {};
this.currentFilamentStock = printer.currentFilamentStock;
this.currentFilament = null;
this.currentFilamentSku = null;
this.currentFilamentUsed = 0;
this.registerEventHandlers();
this.subscribeToActions();
this.subscribeToObjectUpdates();
this.baseSubscription = {
print_stats: null,
display_status: null,
@ -74,32 +79,32 @@ export class PrinterClient {
registerEventHandlers() {
// Register event handlers for Moonraker notifications
this.jsonRpc.registerMethod(
"notify_gcode_response"
"notify_gcode_response",
//this.handleGcodeResponse.bind(this),
);
this.jsonRpc.registerMethod(
"notify_status_update",
this.handleStatusUpdate.bind(this)
this.handleStatusUpdate.bind(this),
);
this.jsonRpc.registerMethod(
"notify_klippy_disconnected",
this.handleKlippyDisconnected.bind(this)
this.handleKlippyDisconnected.bind(this),
);
this.jsonRpc.registerMethod(
"notify_klippy_ready",
this.handleKlippyReady.bind(this)
this.handleKlippyReady.bind(this),
);
this.jsonRpc.registerMethod(
"notify_filelist_changed",
this.handleFileListChanged.bind(this)
this.handleFileListChanged.bind(this),
);
this.jsonRpc.registerMethod(
"notify_metadata_update",
this.handleMetadataUpdate.bind(this)
this.handleMetadataUpdate.bind(this),
);
this.jsonRpc.registerMethod(
"notify_power_changed",
this.handlePowerChanged.bind(this)
this.handlePowerChanged.bind(this),
);
}
@ -110,6 +115,31 @@ export class PrinterClient {
});
}
subscribeToObjectUpdates() {
this.socketClient.subscribeToObjectUpdates({
objectType: "printer",
_id: this.id,
});
}
async updatePrinter(data) {
logger.debug(`Updating printer ${this.id} with data...`);
this.printer = { ...this.printer, ...data };
this.database.printer = this.printer;
if (data?.moonraker) {
this.config = data.moonraker;
}
if (Object.hasOwn(data || {}, "active") && data.active != this.active) {
if (data.active == true) {
await this.setActive();
} else {
await this.setInactive();
}
}
}
async _runQueueMutation(task) {
const executeTask = async () => task();
this.queueMutationChain = this.queueMutationChain
@ -119,6 +149,15 @@ export class PrinterClient {
}
async connect() {
if (this.active == false) {
logger.info(`Printer ${this.id} is not active, skipping connection`);
this.shouldReconnect = false;
this.isOnline = false;
this.state = { type: "inactive" };
await this.updatePrinterState();
return false;
}
const { protocol, host, port } = this.config;
const wsUrl = `${protocol}://${host}:${port}/websocket`;
@ -143,7 +182,10 @@ export class PrinterClient {
this.socket.addEventListener("close", () => {
logger.info(`Disconnected from Moonraker (${this.printer.name})`);
this.isOnline = false;
this.state = { type: "offline", message: "Moonraker disconnected." };
this.state =
this.active == true
? { type: "offline", message: "Moonraker disconnected." }
: { type: "inactive" };
this.connectedAt = null;
this.updatePrinterState();
this.connectionId = null;
@ -182,14 +224,14 @@ export class PrinterClient {
.then(async (result) => {
this.connectionId = result.connection_id;
logger.info(
`Connection identified with ID: ${this.connectionId} (${this.printer.name})`
`Connection identified with ID: ${this.connectionId} (${this.printer.name})`,
);
await this.initialize();
})
.catch((error) => {
logger.error(
`Error identifying connection (${this.printer.name}):`,
error
error,
);
});
}
@ -207,7 +249,7 @@ export class PrinterClient {
await this.syncSubJobs();
this.eventUpdateInterval = setInterval(
this.handleEventUpdate.bind(this),
500
500,
);
}
@ -221,20 +263,20 @@ export class PrinterClient {
logger.info(
"Server:",
`Moonraker ${serverResult.moonraker_version} (${this.printer.name})`,
`State: ${this.klippyState.type}`
`State: ${this.klippyState.type}`,
);
try {
const klippyResult = await this.jsonRpc.callMethod("printer.info");
logger.info(
`Klippy info for ${this.printer.name}: ${klippyResult.hostname}, ${klippyResult.software_version}`
`Klippy info for ${this.printer.name}: ${klippyResult.hostname}, ${klippyResult.software_version}`,
);
// Update firmware version in database
await this.database.updatePrinterFirmware(
klippyResult.software_version
klippyResult.software_version,
);
logger.info(
`Updated firmware version for ${this.printer.name} to ${klippyResult.software_version}`
`Updated firmware version for ${this.printer.name} to ${klippyResult.software_version}`,
);
if (klippyResult.state === "error" && klippyResult.state_message) {
@ -245,7 +287,7 @@ export class PrinterClient {
type: "error",
message: klippyResult.state_message,
actions: ["restartPrinter"],
})
}),
);
}
@ -257,7 +299,7 @@ export class PrinterClient {
type: "error",
message: klippyResult.state_message,
actions: ["restartPrinter"],
})
}),
);
}
@ -269,19 +311,19 @@ export class PrinterClient {
message: klippyResult.state_message,
priority: 8,
timestamp: new Date(),
})
}),
);
}
} catch (error) {
logger.error(
`Error getting Klippy info for ${this.printer.name}:`,
error
error,
);
}
} catch (error) {
logger.error(
`Error getting server info for ${this.printer.name}:`,
error
error,
);
}
}
@ -294,7 +336,7 @@ export class PrinterClient {
for (const file of result) {
if (file.path.startsWith("farmcontrol/")) {
this.printrFileIds.push(
file.path.replace("farmcontrol/", "").replace(".gcode", "")
file.path.replace("farmcontrol/", "").replace(".gcode", ""),
);
}
}
@ -305,17 +347,17 @@ export class PrinterClient {
logger.info(`Getting current filament for printer: ${this.printer.name}`);
if (!this.currentFilamentStock || !this.currentFilamentStock?._id) {
logger.warn(
`No current filament stock found for printer: ${this.printer.name}`
`No current filament stock found for printer: ${this.printer.name}`,
);
return null;
}
const result = await this.socketClient.getObject({
objectType: "filamentStock",
_id: this.currentFilamentStock._id,
populate: ["filament"],
populate: ["filamentSku"],
});
this.currentFilament = result.filament;
return this.currentFilament;
this.currentFilamentSku = result.filamentSku;
return this.currentFilamentSku;
}
async getQueuedJobIds() {
@ -332,7 +374,7 @@ export class PrinterClient {
logger.info(`Getting state of (${this.printer.name})`);
if (!this.isOnline) {
logger.error(
`Cannot send command: Not connected to Moonraker. (${this.printer.name})`
`Cannot send command: Not connected to Moonraker. (${this.printer.name})`,
);
return false;
}
@ -354,7 +396,7 @@ export class PrinterClient {
try {
const result = await this.jsonRpc.callMethodWithKwargs(
"printer.objects.query",
{ objects: this.baseSubscription }
{ objects: this.baseSubscription },
);
logger.debug(`Command sent to ${this.printer.name}`);
if (result.status != undefined) {
@ -380,12 +422,12 @@ export class PrinterClient {
logger.debug(
"Combined subscriptions:",
Object.keys(allSubscriptions).join(", ")
Object.keys(allSubscriptions).join(", "),
);
if (!this.isOnline) {
logger.error(
`Cannot send command: Not connected to Moonraker (${this.printer.name})`
`Cannot send command: Not connected to Moonraker (${this.printer.name})`,
);
return false;
}
@ -406,7 +448,7 @@ export class PrinterClient {
logger.info(`Sending ${command.method} command to (${this.printer.name})`);
if (!this.isOnline) {
logger.error(
`Cannot send command: Not connected to Moonraker (${this.printer.name})`
`Cannot send command: Not connected to Moonraker (${this.printer.name})`,
);
return false;
}
@ -414,7 +456,7 @@ export class PrinterClient {
try {
const result = await this.jsonRpc.callMethodWithKwargs(
command.method,
command.params
command.params,
);
logger.debug(`Command sent to ${this.printer.name}`);
if (result.status != undefined) {
@ -443,7 +485,7 @@ export class PrinterClient {
const oldState = this.state.type;
if (newState !== oldState) {
logger.info(
`Printer ${this.printer.name} state changed from ${this.state.type} to ${newState}`
`Printer ${this.printer.name} state changed from ${this.state.type} to ${newState}`,
);
if (oldState == "printing" && newState == "complete") {
printerFinished = true;
@ -476,7 +518,7 @@ export class PrinterClient {
const newProgress = status.display_status.progress;
if (newProgress !== this.state.progress) {
logger.info(
`Printer ${this.printer.name} progress changed from ${this.state.progress} to ${newProgress}`
`Printer ${this.printer.name} progress changed from ${this.state.progress} to ${newProgress}`,
);
this.state.progress = newProgress;
progressChanged = true;
@ -639,7 +681,7 @@ export class PrinterClient {
if (newFilamentDetected !== this.filamentDetected) {
console.log(status["filament_switch_sensor fsensor"]);
logger.info(
`Printer ${this.printer.name} filament detection changed from ${this.filamentDetected} to ${newFilamentDetected}.`
`Printer ${this.printer.name} filament detection changed from ${this.filamentDetected} to ${newFilamentDetected}.`,
);
this.filamentDetected = newFilamentDetected;
@ -701,11 +743,11 @@ export class PrinterClient {
if (stateChanged == true) {
if (printerFinished == true && isCurrentJobSubJob == true) {
logger.info(
`Subjob ${this.currentSubJob._id} completed, posting part stock items for printer ${this.id}`
`Subjob ${this.currentSubJob._id} completed, posting part stock items for printer ${this.id}`,
);
await this.database.setSubJobFinishedAt(
this.currentSubJob._id,
new Date()
new Date(),
);
await this.database.postSubJobPartStockItems(this.currentSubJob._id);
}
@ -720,7 +762,7 @@ export class PrinterClient {
this.currentSubJob.startedAt = new Date();
await this.database.setSubJobStartedAt(
this.currentSubJob._id,
new Date()
new Date(),
);
}
if (this.currentJob.startedAt == null) {
@ -738,7 +780,7 @@ export class PrinterClient {
};
await this.database.updateSubJobState(
this.currentSubJob._id,
subJobState
subJobState,
);
await this.database.updateJobState(this.currentJob._id);
}
@ -780,13 +822,13 @@ export class PrinterClient {
) {
logger.debug(
"Updating stock event value:",
this.currentFilamentUsed.toFixed(2)
this.currentFilamentUsed.toFixed(2),
);
this.database.updateFilamentStockWeight(
this.currentFilamentStock,
this.currentFilamentUsed,
this.currentSubJob,
this.currentJob
this.currentJob,
);
}
}
@ -794,11 +836,15 @@ export class PrinterClient {
async updatePrinterState() {
try {
const state =
this.klippyState.type !== "ready" ? this.klippyState : this.state;
this.active == false
? { type: "inactive" }
: this.klippyState.type !== "ready"
? this.klippyState
: this.state;
await this.database.updatePrinterState(
state,
this.isOnline,
this.connectedAt
this.connectedAt,
);
} catch (error) {
logger.error(`Failed to update printer state:`, error);
@ -822,11 +868,11 @@ export class PrinterClient {
const serverSubJobs = await this.database.getQueuedSubJobs();
const queuedSubJobs = serverSubJobs.filter((subJob) =>
this.queuedJobIds.includes(subJob.moonrakerJobId)
this.queuedJobIds.includes(subJob.moonrakerJobId),
);
const unqueuedSubJobs = serverSubJobs.filter(
(subJob) => !this.queuedJobIds.includes(subJob.moonrakerJobId)
(subJob) => !this.queuedJobIds.includes(subJob.moonrakerJobId),
);
this.queue = queuedSubJobs;
@ -850,7 +896,7 @@ export class PrinterClient {
printingTypes.includes(stateType)
) {
logger.info(
`No current subjob or job, setting first unqueued subjob as current...`
`No current subjob or job, setting first unqueued subjob as current...`,
);
const targetSubJob = unqueuedSubJobs[0];
const targetJob = await this.database.getJobById(targetSubJob.job);
@ -869,7 +915,7 @@ export class PrinterClient {
});
this.currentSubJob.state.type = stateType;
this.currentJob.state = await this.database.updateJobState(
this.currentJob._id
this.currentJob._id,
);
}
}
@ -941,7 +987,7 @@ export class PrinterClient {
handleMetadataUpdate(metadata) {
logger.info(
`Metadata updated for ${this.printer.name}:`,
metadata.filename
metadata.filename,
);
}
@ -953,7 +999,7 @@ export class PrinterClient {
logger.info(`Uploading G-code file ${fileName} to ${this.printer.name}`);
if (!this.isOnline) {
logger.error(
`Cannot upload file: Not connected to Moonraker (${this.printer.name})`
`Cannot upload file: Not connected to Moonraker (${this.printer.name})`,
);
return false;
}
@ -986,14 +1032,14 @@ export class PrinterClient {
headers,
onUploadProgress: (progressEvent) => {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
(progressEvent.loaded * 100) / progressEvent.total,
);
logger.debug(
`Uploading file to ${this.printer.name}: ` +
fileName +
" " +
percentCompleted +
"%"
"%",
);
this.socketManager.broadcast("notify_printer_update", {
printerId: this.id,
@ -1011,7 +1057,7 @@ export class PrinterClient {
}
logger.info(
`Successfully uploaded file ${fileName} to ${this.printer.name}`
`Successfully uploaded file ${fileName} to ${this.printer.name}`,
);
return true;
} catch (error) {
@ -1024,7 +1070,7 @@ export class PrinterClient {
// Add subJob to queue
this.deploySubJobQueue.push(subJob);
logger.debug(
`Queued sub job ${subJob._id} for deployment. Queue size: ${this.deploySubJobQueue.length}`
`Queued sub job ${subJob._id} for deployment. Queue size: ${this.deploySubJobQueue.length}`,
);
const now = Date.now();
@ -1065,7 +1111,7 @@ export class PrinterClient {
this.deploySubJobQueue = [];
logger.info(
`Processing ${subJobsToDeploy.length} queued sub job(s) for printer ${this.id}`
`Processing ${subJobsToDeploy.length} queued sub job(s) for printer ${this.id}`,
);
// Process sub jobs in parallel with a 250ms stagger between starts
@ -1082,10 +1128,10 @@ export class PrinterClient {
} catch (error) {
logger.error(
`Error deploying sub job ${subJob._id} to printer ${this.id}:`,
error
error,
);
}
})()
})(),
);
await Promise.all(deployPromises);
@ -1098,7 +1144,7 @@ export class PrinterClient {
"to printer:",
this.id,
"with gcode file:",
`${subJob.gcodeFile.name}`
`${subJob.gcodeFile.name}`,
);
const gcodeFile = await this.socketClient.getObject({
objectType: "gcodeFile",
@ -1117,7 +1163,7 @@ export class PrinterClient {
progress: (progress / 100 / 2).toFixed(2),
});
await this.database.updateJobState(subJob.job._id);
}
},
);
if (!file) {
throw new Error("Error getting file");
@ -1134,7 +1180,7 @@ export class PrinterClient {
progress: (progress / 100 / 2 + 0.5).toFixed(2),
});
await this.database.updateJobState(subJob.job._id);
}
},
);
this.printrFileIds.push(gcodeFile.file.toString());
if (!uploadResult) {
@ -1161,7 +1207,7 @@ export class PrinterClient {
const mostRecentQueuedJob = queuedJobs[queuedJobs.length - 1];
const updatedSubJob = await this.database.setSubJobMoonrakerJobId(
subJob._id,
mostRecentQueuedJob.job_id
mostRecentQueuedJob.job_id,
);
await this.database.updateSubJobState(subJob._id, {
type: "queued",
@ -1206,7 +1252,7 @@ export class PrinterClient {
if (!this.isOnline) {
logger.error(
`Cannot set temperature: Not connected to Moonraker (${this.printer.name})`
`Cannot set temperature: Not connected to Moonraker (${this.printer.name})`,
);
return false;
}
@ -1217,20 +1263,20 @@ export class PrinterClient {
// Handle extruder temperature
if (temperature.extruder?.target !== undefined) {
gcodeCommands.push(
`SET_HEATER_TEMPERATURE HEATER=extruder TARGET=${temperature.extruder.target}`
`SET_HEATER_TEMPERATURE HEATER=extruder TARGET=${temperature.extruder.target}`,
);
}
// Handle bed temperature
if (temperature.bed?.target !== undefined) {
gcodeCommands.push(
`SET_HEATER_TEMPERATURE HEATER=heater_bed TARGET=${temperature.bed.target}`
`SET_HEATER_TEMPERATURE HEATER=heater_bed TARGET=${temperature.bed.target}`,
);
}
if (gcodeCommands.length === 0) {
logger.warn(
`No valid temperature targets provided for ${this.printer.name}`
`No valid temperature targets provided for ${this.printer.name}`,
);
return false;
}
@ -1245,7 +1291,7 @@ export class PrinterClient {
if (!result) {
logger.error(
`Failed to set temperature with command: ${gcodeCommand}`
`Failed to set temperature with command: ${gcodeCommand}`,
);
return false;
}
@ -1258,7 +1304,7 @@ export class PrinterClient {
} catch (error) {
logger.error(
`Error setting temperature for ${this.printer.name}:`,
error
error,
);
return false;
}
@ -1268,7 +1314,7 @@ export class PrinterClient {
logger.info(`Restarting printer firmware for ${this.printer.name}`);
if (!this.isOnline) {
logger.error(
`Cannot restart printer firmware: Not connected to Moonraker (${this.printer.name})`
`Cannot restart printer firmware: Not connected to Moonraker (${this.printer.name})`,
);
return false;
}
@ -1281,13 +1327,13 @@ export class PrinterClient {
throw new Error("Failed to restart printer firmware");
}
logger.info(
`Successfully restarted printer firmware for ${this.printer.name}`
`Successfully restarted printer firmware for ${this.printer.name}`,
);
return true;
} catch (error) {
logger.error(
`Error restarting printer firmware for ${this.printer.name}:`,
error
error,
);
return false;
}
@ -1297,7 +1343,7 @@ export class PrinterClient {
logger.info(`Restarting printer for ${this.printer.name}`);
if (!this.isOnline) {
logger.error(
`Cannot restart printer: Not connected to Moonraker (${this.printer.name})`
`Cannot restart printer: Not connected to Moonraker (${this.printer.name})`,
);
return false;
}
@ -1320,7 +1366,7 @@ export class PrinterClient {
logger.info(`Restarting moonraker server for ${this.printer.name}`);
if (!this.isOnline) {
logger.error(
`Cannot restart moonraker server: Not connected to moonraker server (${this.printer.name})`
`Cannot restart moonraker server: Not connected to moonraker server (${this.printer.name})`,
);
return false;
}
@ -1333,13 +1379,13 @@ export class PrinterClient {
throw new Error("Failed to restart moonraker server");
}
logger.info(
`Successfully restarted moonraker server for ${this.printer.name}`
`Successfully restarted moonraker server for ${this.printer.name}`,
);
return true;
} catch (error) {
logger.error(
`Error restarting moonraker server for ${this.printer.name}:`,
error
error,
);
return false;
}
@ -1349,7 +1395,7 @@ export class PrinterClient {
logger.info(`Starting queue for ${this.printer.name}`);
if (!this.isOnline) {
logger.error(
`Cannot start queue: Not connected to Moonraker (${this.printer.name})`
`Cannot start queue: Not connected to Moonraker (${this.printer.name})`,
);
return false;
}
@ -1372,7 +1418,7 @@ export class PrinterClient {
logger.info(`Pausing job for ${this.printer.name}`);
if (!this.isOnline) {
logger.error(
`Cannot pause job: Not connected to Moonraker (${this.printer.name})`
`Cannot pause job: Not connected to Moonraker (${this.printer.name})`,
);
return false;
}
@ -1395,7 +1441,7 @@ export class PrinterClient {
logger.info(`Resuming job for ${this.printer.name}`);
if (!this.isOnline) {
logger.error(
`Cannot resume job: Not connected to Moonraker (${this.printer.name})`
`Cannot resume job: Not connected to Moonraker (${this.printer.name})`,
);
return false;
}
@ -1418,7 +1464,7 @@ export class PrinterClient {
logger.info(`Cancelling job for ${this.printer.name}`);
if (!this.isOnline) {
logger.error(
`Cannot cancel job: Not connected to Moonraker (${this.printer.name})`
`Cannot cancel job: Not connected to Moonraker (${this.printer.name})`,
);
return false;
}
@ -1477,4 +1523,26 @@ export class PrinterClient {
logger.info(`Successfully disconnected from ${this.printer.name}`);
return true;
}
async setInactive() {
this.active = false;
this.printer.active = false;
this.database.printer = this.printer;
this.isOnline = false;
await this.disconnect();
this.state = { type: "inactive" };
this.klippyState = { type: "inactive" };
this.connectedAt = null;
await this.updatePrinterState();
}
async setActive() {
this.active = true;
this.printer.active = true;
this.database.printer = this.printer;
this.klippyState = { type: "offline", message: "Klippy disconnected." };
this.shouldReconnect = true;
this.isOnline = false;
await this.connect();
}
}

View File

@ -119,7 +119,7 @@ export class PrinterManager {
return;
case "loadFilamentStock":
const loadFilamentStockResult = await printer.loadFilamentStock(
action.data.filamentStock
action.data.filamentStock,
);
callback(loadFilamentStockResult);
return;
@ -127,6 +127,14 @@ export class PrinterManager {
callback({ error: "Unknown command." });
}
async handlePrinterUpdate(id, data) {
logger.debug("Handling printer update for id:", id);
const printer = this.getPrinterClient(id);
if (printer) {
await printer.updatePrinter(data.object);
}
}
getPrinterClient(printerId) {
return this.printerClients.get(printerId);
}
@ -138,7 +146,7 @@ export class PrinterManager {
// Close all printer connections
async closeAllConnections() {
logger.info(
`Closing all printer connections... current count: ${this.printerClients.size}`
`Closing all printer connections... current count: ${this.printerClients.size}`,
);
// Take a snapshot so any mutations during disconnects don't affect iteration
@ -150,14 +158,14 @@ export class PrinterManager {
printerClient.shouldReconnect = false;
await printerClient.disconnect();
logger.info(
`Disconnected printer client ${printerClient?.id || "unknown"}`
`Disconnected printer client ${printerClient?.id || "unknown"}`,
);
} catch (error) {
logger.error(
`Failed to disconnect printer client ${
printerClient?.id || "unknown"
}:`,
error
error,
);
}
}
@ -169,7 +177,7 @@ export class PrinterManager {
this.printers = [];
logger.info(
`All printer connections closed. Remaining clients: ${this.printerClients.size}`
`All printer connections closed. Remaining clients: ${this.printerClients.size}`,
);
}
}

View File

@ -216,6 +216,9 @@ export class SocketClient {
callback
);
}
if (data.objectType == "printer") {
this.printerManager.handlePrinterUpdate(data._id, data, callback);
}
}
handleConnect() {