diff --git a/src/printer/database.js b/src/printer/database.js index f7c4509..1d1d23a 100644 --- a/src/printer/database.js +++ b/src/printer/database.js @@ -1,3 +1,4 @@ +import { randomUUID } from "crypto"; import log4js from "log4js"; import NodeCache from "node-cache"; import { loadConfig } from "../config.js"; @@ -369,14 +370,20 @@ export class PrinterDatabase { async addAlert(alert) { try { - logger.debug(`Adding alert to printer ${this.id}:`, alert); + const alertWithId = { + ...alert, + _id: alert._id ?? randomUUID(), + }; + + logger.debug(`Adding alert to printer ${this.id}:`, alertWithId); const printer = await this.getPrinter(); - const updatedAlerts = [...printer.alerts, alert]; + const updatedAlerts = [...printer.alerts, alertWithId]; const updatedPrinter = await this.editPrinter({ alerts: updatedAlerts }); logger.info(`Added new alert to printer ${this.id}:`, { type: alert.type, + code: alert.code, priority: alert.priority, hasMessage: !!alert.message, }); @@ -388,22 +395,18 @@ export class PrinterDatabase { } } - async removeAlert(alertId) { + async removeAlertById(alertId) { try { - logger.debug(`Clearing alert ${alertId} for printer ${this.id}`); + logger.debug(`Removing alert by id ${alertId} for printer ${this.id}`); const printer = await this.getPrinter(); - let filteredAlerts = printer.alerts; - - if (alertId) { - filteredAlerts = filteredAlerts.filter( - (alert) => alert._id !== alertId, - ); - } + const filteredAlerts = printer.alerts.filter( + (alert) => alert._id !== alertId, + ); const updatedPrinter = await this.editPrinter({ alerts: filteredAlerts }); - logger.info(`Cleared alert ${alertId} for printer ${this.id}:`, { + logger.info(`Removed alert ${alertId} for printer ${this.id}:`, { alertId, alerts: updatedPrinter.alerts, }); @@ -411,7 +414,33 @@ export class PrinterDatabase { return updatedPrinter; } catch (error) { logger.error( - `Failed to clear alert ${alertId} for printer ${this.id}:`, + `Failed to remove alert ${alertId} for printer ${this.id}:`, + error, + ); + throw error; + } + } + + async removeAlertsByCode(code) { + try { + logger.debug(`Removing alerts by code ${code} for printer ${this.id}`); + + const printer = await this.getPrinter(); + const filteredAlerts = printer.alerts.filter( + (alert) => alert.code !== code, + ); + + const updatedPrinter = await this.editPrinter({ alerts: filteredAlerts }); + + logger.info(`Removed alerts with code ${code} for printer ${this.id}:`, { + code, + alerts: updatedPrinter.alerts, + }); + + return updatedPrinter; + } catch (error) { + logger.error( + `Failed to remove alerts with code ${code} for printer ${this.id}:`, error, ); throw error; @@ -431,7 +460,9 @@ export class PrinterDatabase { } // Sort alerts by priority - alerts.sort((a, b) => a.priority.localeCompare(b.priority)); + alerts.sort((a, b) => + String(a.priority ?? "").localeCompare(String(b.priority ?? "")), + ); logger.info(`Retrieved ${alerts.length} alerts for printer ${this.id}`); return alerts; diff --git a/src/printer/printerclient.js b/src/printer/printerclient.js index 6dec6ef..a5c7044 100644 --- a/src/printer/printerclient.js +++ b/src/printer/printerclient.js @@ -285,7 +285,7 @@ export class PrinterClient { logger.error( `Klippy error for ${this.printer.name}: ${klippyResult.state_message}`, this.database.addAlert({ - _id: "klippyError", + code: "klippyError", type: "error", message: klippyResult.state_message, actions: ["restart", "restartFirmware"], @@ -297,7 +297,7 @@ export class PrinterClient { logger.error( `Klippy error for ${this.printer.name}: ${klippyResult.state_message}`, this.database.addAlert({ - _id: "klippyError", + code: "klippyError", type: "error", message: klippyResult.state_message, actions: ["restart", "restartFirmware"], @@ -310,6 +310,7 @@ export class PrinterClient { `Klippy startup message for ${this.printer.name}: ${klippyResult.state_message}`, this.database.addAlert({ type: "info", + code: "klippyStartup", message: klippyResult.state_message, priority: 8, timestamp: new Date(), @@ -490,6 +491,64 @@ export class PrinterClient { } } + printerActionResult(success, error = null, code = null) { + if (success) { + return { success: true }; + } + + const result = { success: false, error }; + if (code !== undefined && code !== null) { + result.code = code; + } + return result; + } + + async addMotionErrorAlert(error) { + const code = error?.code; + if (code === undefined || code === null) { + return; + } + + try { + await this.database.addAlert({ + code: `motionError${code}`, + type: "error", + priority: "9", + message: error?.message || "Failed to execute printer movement command", + canDismiss: true, + }); + } catch (alertError) { + logger.error( + `Failed to add motion error alert for ${this.printer.name}:`, + alertError, + ); + } + } + + async runGcodeScript(script) { + if (!this.isOnline) { + return this.printerActionResult( + false, + "Printer is not connected to Moonraker", + ); + } + + try { + await this.jsonRpc.callMethodWithKwargs("printer.gcode.script", { + script, + }); + return this.printerActionResult(true); + } catch (error) { + logger.error(`G-code script failed for ${this.printer.name}:`, error); + await this.addMotionErrorAlert(error); + return this.printerActionResult( + false, + error?.message || "Failed to send command to printer", + error?.code, + ); + } + } + async handleStatusUpdate(status) { logger.trace("Status update:", status); status = status[0]; @@ -706,9 +765,9 @@ export class PrinterClient { console.log(this.currentFilamentStock); if (newFilamentDetected == false && this.currentFilamentStock == null) { - await this.database.removeAlert("noFilamentLoaded"); + await this.database.removeAlertsByCode("noFilamentLoaded"); await this.database.addAlert({ - _id: "noFilamentSelected", + code: "noFilamentSelected", type: "info", actions: ["loadFilamentStock"], canDismiss: false, @@ -721,9 +780,9 @@ export class PrinterClient { ) { await this.setCurrentFilamentStock(null); await this.database.setCurrentFilamentStock(null); - await this.database.removeAlert("noFilamentSelected"); + await this.database.removeAlertsByCode("noFilamentSelected"); await this.database.addAlert({ - _id: "noFilamentLoaded", + code: "noFilamentLoaded", type: "info", actions: ["loadFilamentStock"], canDismiss: false, @@ -734,15 +793,15 @@ export class PrinterClient { newFilamentDetected == true && this.currentFilamentStock != null ) { - await this.database.removeAlert("noFilamentSelected"); - await this.database.removeAlert("noFilamentLoaded"); + await this.database.removeAlertsByCode("noFilamentSelected"); + await this.database.removeAlertsByCode("noFilamentLoaded"); } else if ( newFilamentDetected == false && this.currentFilamentStock != null ) { - await this.database.removeAlert("noFilamentLoaded"); + await this.database.removeAlertsByCode("noFilamentLoaded"); await this.database.addAlert({ - _id: "noFilamentLoaded", + code: "noFilamentLoaded", type: "info", message: "No filament loaded. Please load filament to continue printing.", @@ -1330,8 +1389,103 @@ export class PrinterClient { } } - async homeAxis(axis) { - + async homeAxis({ axis }) { + logger.info(`Homing axis for ${this.printer.name}:`, axis); + + if (!this.isOnline) { + logger.error( + `Cannot home axis: Not connected to Moonraker (${this.printer.name})`, + ); + return this.printerActionResult( + false, + "Printer is not connected to Moonraker", + ); + } + + try { + if (!axis) { + logger.warn(`No axis provided for homing on ${this.printer.name}`); + return this.printerActionResult(false, "No axis provided for homing"); + } + + const gcodeCommand = axis === "ALL" ? "G28" : `G28 ${axis}`; + const result = await this.runGcodeScript(gcodeCommand); + + if (!result.success) { + logger.error( + `Failed to home axis with command: ${gcodeCommand}`, + result.error, + ); + return result; + } + + logger.info(`Successfully homed axis for ${this.printer.name}`); + return result; + } catch (error) { + logger.error(`Error homing axis for ${this.printer.name}:`, error); + await this.addMotionErrorAlert(error); + return this.printerActionResult( + false, + error?.message || "Failed to home axis", + error?.code, + ); + } + } + + async moveAxis({ axis, distance, rate }) { + logger.info(`Moving axis for ${this.printer.name}:`, { + axis, + distance, + rate, + }); + + if (!this.isOnline) { + logger.error( + `Cannot move axis: Not connected to Moonraker (${this.printer.name})`, + ); + return this.printerActionResult( + false, + "Printer is not connected to Moonraker", + ); + } + + try { + const validAxes = ["X", "Y", "Z", "E"]; + if (!axis || !validAxes.includes(axis)) { + logger.warn( + `Invalid axis provided for move on ${this.printer.name}: ${axis}`, + ); + return this.printerActionResult(false, `Invalid axis: ${axis}`); + } + + if (distance === undefined || distance === null) { + logger.warn(`No distance provided for move on ${this.printer.name}`); + return this.printerActionResult(false, "No distance provided for move"); + } + + const feedRate = rate ?? 1000; + const gcodeCommand = `G91\nG1 ${axis}${distance} F${feedRate}\nG90`; + const result = await this.runGcodeScript(gcodeCommand); + + if (!result.success) { + logger.error( + `Failed to move axis with command: ${gcodeCommand}`, + result.error, + ); + return result; + } + + logger.info(`Successfully moved axis for ${this.printer.name}`); + return result; + } catch (error) { + logger.error(`Error moving axis for ${this.printer.name}:`, error); + await this.addMotionErrorAlert(error); + return this.printerActionResult( + false, + error?.message || "Failed to move axis", + error?.code, + ); + } } async restartPrinterFirmware() { @@ -1525,8 +1679,8 @@ export class PrinterClient { await this.database.setCurrentFilamentStock(filamentStock); await this.setCurrentFilamentStock(filamentStock); await this.getCurrentFilament(); - await this.database.removeAlert("noFilamentSelected"); - await this.database.removeAlert("noFilamentLoaded"); + await this.database.removeAlertsByCode("noFilamentSelected"); + await this.database.removeAlertsByCode("noFilamentLoaded"); } async disconnect() { diff --git a/src/printer/printermanager.js b/src/printer/printermanager.js index 59849ab..0dcdfdc 100644 --- a/src/printer/printermanager.js +++ b/src/printer/printermanager.js @@ -79,6 +79,16 @@ export class PrinterManager { callback(setTempResult); return; + case "homeAxis": + const homeAxisResult = await printer.homeAxis(action.data); + callback(homeAxisResult); + return; + + case "moveAxis": + const moveAxisResult = await printer.moveAxis(action.data); + callback(moveAxisResult); + return; + case "restartPrinterFirmware": const restartPrinterFirmwareResult = await printer.restartPrinterFirmware();