diff --git a/src/database/filament.schema.js b/src/database/filament.schema.js new file mode 100644 index 0000000..f1b9f68 --- /dev/null +++ b/src/database/filament.schema.js @@ -0,0 +1,26 @@ +import mongoose from "mongoose"; +const { Schema } = mongoose; + +const filamentSchema = new mongoose.Schema({ + name: { required: true, type: String }, + barcode: { required: false, type: String }, + url: { required: false, type: String }, + image: { required: false, type: Buffer }, + color: { required: true, type: String }, + vendor: { type: Schema.Types.ObjectId, ref: "Vendor", required: true }, + type: { required: true, type: String }, + cost: { required: true, type: Number }, + diameter: { required: true, type: Number }, + density: { required: true, type: Number }, + createdAt: { required: true, type: Date }, + updatedAt: { required: true, type: Date }, + emptySpoolWeight: { required: true, type: Number }, +}); + +filamentSchema.virtual("id").get(function () { + return this._id.toHexString(); +}); + +filamentSchema.set("toJSON", { virtuals: true }); + +export const filamentModel = mongoose.model("Filament", filamentSchema); diff --git a/src/database/filamentstock.schema.js b/src/database/filamentstock.schema.js new file mode 100644 index 0000000..6ad2242 --- /dev/null +++ b/src/database/filamentstock.schema.js @@ -0,0 +1,39 @@ +import mongoose from "mongoose"; +const { Schema } = mongoose; + +// Define the main filamentStock schema +const filamentStockSchema = new Schema( + { + state: { + type: { type: String, required: true }, + percent: { type: String, required: true }, + }, + startingGrossWeight: { type: Number, required: true }, + startingNetWeight: { type: Number, required: true }, + currentGrossWeight: { type: Number, required: true }, + currentNetWeight: { type: Number, required: true }, + filament: { type: mongoose.Schema.Types.ObjectId, ref: "Filament" }, + stockEvents: [{ + type: { type: String, required: true }, + value: { type: Number, required: true }, + subJob: { type: mongoose.Schema.Types.ObjectId, ref: "PrintSubJob", required: false }, + job: { type: mongoose.Schema.Types.ObjectId, ref: "PrintJob", required: false }, + timestamp: { type: Date, default: Date.now } + }] + }, + { timestamps: true }, +); + +// Add virtual id getter +filamentStockSchema.virtual("id").get(function () { + return this._id.toHexString(); +}); + +// Configure JSON serialization to include virtuals +filamentStockSchema.set("toJSON", { virtuals: true }); + +// Create and export the model +export const filamentStockModel = mongoose.model( + "FilamentStock", + filamentStockSchema, +); diff --git a/src/database/gcodefile.schema.js b/src/database/gcodefile.schema.js new file mode 100644 index 0000000..8a96891 --- /dev/null +++ b/src/database/gcodefile.schema.js @@ -0,0 +1,24 @@ +import mongoose from "mongoose"; +const { Schema } = mongoose; + +const gcodeFileSchema = new mongoose.Schema({ + name: { required: true, type: String }, + gcodeFileName: { required: false, type: String }, + gcodeFileInfo: { required: true, type: Object }, + size: { type: Number, required: false }, + filament: { type: Schema.Types.ObjectId, ref: "Filament", required: true }, + parts: [{ type: Schema.Types.ObjectId, ref: "Part", required: true }], + cost: { type: Number, required: false }, + createdAt: { type: Date }, + updatedAt: { type: Date }, +}); + +gcodeFileSchema.index({ name: "text", brand: "text" }); + +gcodeFileSchema.virtual("id").get(function () { + return this._id.toHexString(); +}); + +gcodeFileSchema.set("toJSON", { virtuals: true }); + +export const gcodeFileModel = mongoose.model("GCodeFile", gcodeFileSchema); diff --git a/src/database/printer.schema.js b/src/database/printer.schema.js index 5667689..fcf7905 100644 --- a/src/database/printer.schema.js +++ b/src/database/printer.schema.js @@ -12,10 +12,19 @@ const moonrakerSchema = new Schema( { _id: false }, ); +// Define the alert schema +const alertSchema = new Schema( + { + priority: { type: String, required: true }, // order to show + type: { type: String, required: true }, // selectFilament, error, info, message, + }, + { timestamps: true, _id: false } +); + // Define the main printer schema const printerSchema = new Schema( { - printerName: { type: String, required: true }, + name: { type: String, required: true }, online: { type: Boolean, required: true, default: false }, state: { type: { type: String, required: true, default: "Offline" }, @@ -32,7 +41,9 @@ const printerSchema = new Schema( firmware: { type: String }, currentJob: { type: Schema.Types.ObjectId, ref: "PrintJob" }, currentSubJob: { type: Schema.Types.ObjectId, ref: "PrintSubJob" }, + currentFilamentStock: { type: Schema.Types.ObjectId, ref: "FilamentStock" }, subJobs: [{ type: Schema.Types.ObjectId, ref: "PrintSubJob" }], + alerts: [alertSchema], }, { timestamps: true }, ); diff --git a/src/database/printjob.schema.js b/src/database/printjob.schema.js index ac74d56..5c49cd9 100644 --- a/src/database/printjob.schema.js +++ b/src/database/printjob.schema.js @@ -6,10 +6,13 @@ const printJobSchema = new mongoose.Schema({ type: { required: true, type: String }, progress: { required: false, type: Number, default: 0 }, }, + subJobStats : { + required: false, type: Object + }, printers: [{ type: Schema.Types.ObjectId, ref: "Printer", required: false }], createdAt: { required: true, type: Date }, updatedAt: { required: true, type: Date }, - startedAt: { required: true, type: Date }, + startedAt: { required: false, type: Date }, gcodeFile: { type: Schema.Types.ObjectId, ref: "GCodeFile", diff --git a/src/printer/database.js b/src/printer/database.js new file mode 100644 index 0000000..2df0448 --- /dev/null +++ b/src/printer/database.js @@ -0,0 +1,680 @@ +import { printerModel } from "../database/printer.schema.js"; +import { printJobModel } from "../database/printjob.schema.js"; +import { printSubJobModel } from "../database/printsubjob.schema.js"; +import { gcodeFileModel } from "../database/gcodefile.schema.js" +import { filamentStockModel } from "../database/filamentstock.schema.js"; +import { filamentModel } from "../database/filament.schema.js"; +import log4js from "log4js"; +import { loadConfig } from "../config.js"; + +const config = loadConfig(); +const logger = log4js.getLogger("Printer Database"); +logger.level = config.server.logLevel; + +export class PrinterDatabase { + constructor(socketManager) { + this.socketManager = socketManager; + logger.info("Initialized PrinterDatabase with socket manager"); + } + + async getPrinterConfig(printerId) { + try { + logger.debug(`Getting printer config for ${printerId}`); + + const printer = await printerModel.findById(printerId); + if (!printer) { + logger.error(`Printer with ID ${printerId} not found when getting config`); + return null; + } + + logger.debug(`Retrieved printer config for ${printerId}:`, printer.moonraker); + return printer.moonraker; + } catch (error) { + logger.error(`Failed to get printer config for ${printerId}:`, error); + throw error; + } + } + + async updatePrinterState(printerId, state, online) { + try { + logger.debug(`Updating printer state for ${printerId}:`, { state, online }); + + if (state.type === "printing" && state.progress === undefined) { + logger.debug(`Setting default progress for printing state on printer ${printerId}`); + state.progress = 0; + } + + const updatedPrinter = await printerModel.findByIdAndUpdate( + printerId, + { state, online }, + { new: true } + ); + + if (!updatedPrinter) { + logger.error(`Printer with ID ${printerId} not found when updating status`); + return; + } + + logger.info(`Updated printer ${printerId} state:`, { + type: state.type, + progress: state.progress, + online, + previousState: updatedPrinter.state + }); + + this.socketManager.broadcast("notify_printer_update", { + id: printerId, + state, + }); + + return updatedPrinter; + } catch (error) { + logger.error(`Failed to update printer state for ${printerId}:`, error); + throw error; + } + } + + async clearCurrentJob(printerId) { + try { + logger.warn(`Clearing current job for printer ${printerId}`); + + const updatedPrinter = await printerModel.findByIdAndUpdate( + printerId, + { + currentSubJob: null, + currentJob: null, + }, + { new: true } + ); + + if (!updatedPrinter) { + logger.error(`Printer with ID ${printerId} not found when clearing current job`); + return null; + } + + // Broadcast the update through websocket + this.socketManager.broadcast("notify_printer_update", { + id: printerId, + currentSubJob: null, + currentJob: null + }); + + return { + currentSubJob: null, + currentJob: null + }; + } catch (error) { + logger.error(`Failed to clear current job for printer ${printerId}:`, error); + throw error; + } + } + + async setCurrentJobForPrinting(printerId, queuedJobIds) { + try { + logger.error(`Setting current job for printer ${printerId} as printing starts`); + + const printer = await printerModel.findById(printerId).populate('subJobs'); + if (!printer) { + logger.error(`Printer with ID ${printerId} not found`); + return null; + } + + + const subJobs = printer.subJobs + for (const subJob of subJobs) { + if (!queuedJobIds.includes(subJob.subJobId) && ['printing', 'queued', 'paused'].includes(subJob.state.type)) { + logger.info(`Setting current job and subjob for printer ${printerId} as printing starts`, { + subJobId: subJob.id, + jobId: subJob.printJob + }); + + const now = new Date(); + + // Update printer with current job and startedAt + const updatedPrinter = await printerModel.findByIdAndUpdate( + printerId, + { + currentSubJob: subJob.id, + currentJob: subJob.printJob, + startedAt: now + }, + { new: true } + ); + + if (!updatedPrinter) { + logger.error(`Printer with ID ${printerId} not found when setting job and subjob`); + return null; + } + + // Update subjob with startedAt + await printSubJobModel.findByIdAndUpdate( + subJob.id, + { startedAt: now } + ); + + // Get the full job object and update its startedAt if null + const job = await printJobModel.findById(subJob.printJob).populate('gcodeFile'); + if (!job) { + logger.error(`Job with ID ${subJob.printJob} not found`); + return null; + } + + if (!job.startedAt) { + await printJobModel.findByIdAndUpdate( + subJob.printJob, + { startedAt: now } + ); + job.startedAt = now; + } + + // Broadcast the update through websocket + this.socketManager.broadcast("notify_printer_update", { + id: printerId, + currentSubJob: subJob, + currentJob: job + }); + + return { + currentSubJob: subJob, + currentJob: job + }; + } + } + return null; + } catch (error) { + logger.error(`Failed to set current job for printer ${printerId}:`, error); + throw error; + } + } + + async updateSubJobState(subJobId, state) { + try { + + if (state.type == "standby" || state.type == "error" || state.type == "offline") { + state.type = 'failed' +logger.warn(`Updating subjob state for ${subJobId}:`, state); + } else { + logger.debug(`Updating subjob state for ${subJobId}:`, state); + } + + + const updatedSubJob = await printSubJobModel.findByIdAndUpdate( + subJobId, + { state }, + { new: true } + ); + + if (!updatedSubJob) { + logger.error(`Sub job with ID ${subJobId} not found`); + return; + } + + logger.info(`Updated subjob ${subJobId} state:`, { + type: state.type, + progress: state.progress, + previousState: updatedSubJob.state, + printJob: updatedSubJob.printJob + }); + + // Update parent job state + await this.updateJobState(updatedSubJob.printJob); + + this.socketManager.broadcast("notify_subjob_update", { + id: subJobId, + state, + }); + + return updatedSubJob; + } catch (error) { + logger.error(`Failed to update sub job state for ${subJobId}:`, error); + throw error; + } + } + + async updateJobState(jobId) { + try { + logger.debug(`Updating job state for ${jobId}`); + + const job = await printJobModel.findById(jobId).populate("subJobs"); + if (!job) { + logger.error(`Job with ID ${jobId} not found`); + return; + } + + const subJobStates = job.subJobs.map(subJob => subJob.state); + const stateCounts = { + printing: 0, + paused: 0, + complete: 0, + failed: 0, + queued: 0 + }; + + let progress = 0; + subJobStates.forEach(state => { + stateCounts[state.type]++; + progress += state.progress || 0; + }); + + logger.debug(`Job ${jobId} state counts:`, stateCounts); + + const jobState = { + type: this.determineJobState(stateCounts), + progress: progress / job.subJobs.length + }; + + logger.debug(`Calculated job state for ${jobId}:`, { + type: jobState.type, + progress: jobState.progress, + subJobCount: job.subJobs.length + }); + + job.state = jobState; + job.subJobStats = stateCounts; + + await job.save(); + + logger.info(`Updated job ${jobId} state:`, { + id: jobId, + state: jobState, + subJobStats: stateCounts + }); + + this.socketManager.broadcast("notify_job_update", { + id: jobId, + state: jobState, + subJobStats: stateCounts + }); + + return job; + } catch (error) { + logger.error(`Failed to update job state for ${jobId}:`, error); + throw error; + } + } + + determineJobState(stateCounts) { + logger.debug("Determining job state from counts:", stateCounts); + + // If any subjob is printing, the overall state should be printing + if (stateCounts.printing > 0) { + logger.debug("Job state determined as 'printing' due to active printing subjobs"); + return "printing"; + } + + if (stateCounts.failed > 0 || stateCounts.cancelled > 0) { + logger.debug("Job state determined as 'failed' due to failed or cancelled subjobs"); + return "failed"; + } + if (stateCounts.paused > 0) { + logger.debug("Job state determined as 'paused'"); + return "paused"; + } + if (stateCounts.complete === Object.values(stateCounts).reduce((a, b) => a + b, 0)) { + logger.debug("Job state determined as 'complete'"); + return "complete"; + } + logger.debug("Job state determined as 'queued'"); + return "queued"; + } + + async addSubJobToPrinter(printerId, subJobId) { + try { + logger.debug(`Adding subjob ${subJobId} to printer ${printerId}`); + + const printer = await printerModel.findById(printerId); + if (!printer) { + logger.error(`Printer with ID ${printerId} not found`); + return null; + } + + printer.subJobs.push(subJobId); + await printer.save(); + + logger.info(`Added subjob ${subJobId} to printer ${printerId}`, { + currentSubJobs: printer.subJobs.length, + printerState: printer.state + }); + + return printer; + } catch (error) { + logger.error(`Failed to add subjob ${subJobId} to printer ${printerId}:`, error); + throw error; + } + } + + async removePrinterSubJob(printerId, subJobId) { + try { + logger.debug(`Removing subjob ${subJobId} from printer ${printerId}`); + + const updatedPrinter = await printerModel.findByIdAndUpdate( + printerId, + { $pull: { subJobs: subJobId } }, + { new: true } + ); + + if (!updatedPrinter) { + logger.error(`Printer with ID ${printerId} not found`); + return; + } + + logger.info(`Removed subjob ${subJobId} from printer ${printerId}`, { + remainingSubJobs: updatedPrinter.subJobs.length, + printerState: updatedPrinter.state + }); + + return updatedPrinter; + } catch (error) { + logger.error(`Failed to remove subjob ${subJobId} from printer ${printerId}:`, error); + throw error; + } + } + + async updateDisplayStatus(printerId, message) { + try { + logger.debug(`Updating display status for printer ${printerId}:`, { message }); + + this.socketManager.broadcast("notify_display_status", { + printerId, + message + }); + + logger.info(`Broadcast display status for printer ${printerId}:`, { message }); + } catch (error) { + logger.error(`Failed to broadcast display status for printer ${printerId}:`, error); + throw error; + } + } + + async updatePrinterFirmware(printerId, firmwareVersion) { + try { + logger.debug(`Updating firmware version for printer ${printerId}:`, { firmwareVersion }); + + const updatedPrinter = await printerModel.findByIdAndUpdate( + printerId, + { version: firmwareVersion }, + { new: true } + ); + + if (!updatedPrinter) { + logger.error(`Printer with ID ${printerId} not found when updating firmware version`); + return; + } + + logger.info(`Updated firmware version for printer ${printerId}:`, { + newVersion: firmwareVersion, + previousVersion: updatedPrinter.version + }); + + return updatedPrinter; + } catch (error) { + logger.error(`Failed to update firmware version for printer ${printerId}:`, error); + throw error; + } + } + + async addAlert(printerId, alert) { + try { + logger.debug(`Adding alert to printer ${printerId}:`, alert); + + const updatedPrinter = await printerModel.findByIdAndUpdate( + printerId, + { $push: { alerts: alert } }, + { new: true } + ); + + if (!updatedPrinter) { + logger.error(`Printer with ID ${printerId} not found when adding alert`); + return null; + } + + logger.info(`Added alert to printer ${printerId}:`, { + type: alert.type, + priority: alert.priority + }); + + // Broadcast the alert through websocket + this.socketManager.broadcast("notify_printer_update", { + id: printerId, + alerts: updatedPrinter.alerts + }); + + return updatedPrinter; + } catch (error) { + logger.error(`Failed to add alert to printer ${printerId}:`, error); + throw error; + } + } + + async removeAlerts(printerId, options = {}) { + try { + logger.debug(`Clearing alerts for printer ${printerId}:`, options); + + let query = {}; + if (options.type) { + query.type = options.type; + } + if (options.priority) { + query.priority = options.priority; + } + + const updatedPrinter = await printerModel.findByIdAndUpdate( + printerId, + { $pull: { alerts: query } }, + { new: true } + ); + + if (!updatedPrinter) { + logger.error(`Printer with ID ${printerId} not found when clearing alerts`); + return null; + } + + logger.info(`Cleared alerts for printer ${printerId}:`, { + options, + remainingAlerts: updatedPrinter.alerts.length + }); + + // Broadcast the alert clear through websocket + this.socketManager.broadcast("notify_printer_update", { + id: printerId, + alerts: updatedPrinter.alerts + }); + + return updatedPrinter; + } catch (error) { + logger.error(`Failed to clear alerts for printer ${printerId}:`, error); + throw error; + } + } + + async getAlerts(printerId, options = {}) { + try { + logger.debug(`Getting alerts for printer ${printerId}:`, options); + + const printer = await printerModel.findById(printerId); + if (!printer) { + logger.error(`Printer with ID ${printerId} not found when getting alerts`); + return null; + } + + let alerts = printer.alerts; + + // Filter alerts based on options + if (options.type) { + alerts = alerts.filter(alert => alert.type === options.type); + } + if (options.priority) { + alerts = alerts.filter(alert => alert.priority === options.priority); + } + + // Sort alerts by priority + alerts.sort((a, b) => a.priority.localeCompare(b.priority)); + + logger.info(`Retrieved ${alerts.length} alerts for printer ${printerId}`); + return alerts; + } catch (error) { + logger.error(`Failed to get alerts for printer ${printerId}:`, error); + throw error; + } + } + + async setCurrentFilamentStock(printerId, filamentStockId) { + try { + logger.debug(`Setting current filament stock for printer ${printerId}:`, { filamentStockId }); + + const updatedPrinter = await printerModel.findByIdAndUpdate( + printerId, + { currentFilamentStock: filamentStockId }, + { new: true } + ).populate({ path: "currentFilamentStock", + populate: { + path: "filament", + }}); + + if (!updatedPrinter) { + logger.error(`Printer with ID ${printerId} not found when setting current filament stock`); + return null; + } + + logger.info(`Updated current filament stock for printer ${printerId}:`, { + filamentStock: updatedPrinter.currentFilamentStock, + }); + + // Broadcast the update through websocket + this.socketManager.broadcast("notify_printer_update", { + id: printerId, + currentFilamentStock: updatedPrinter.currentFilamentStock + }); + + return updatedPrinter.currentFilamentStock; + } catch (error) { + logger.error(`Failed to set current filament stock for printer ${printerId}:`, error); + throw error; + } + } + + async updateFilamentStockWeight(filamentStockId, weight, subJobId = null, jobId = null) { + try { + logger.debug(`Updating filament stock weight for ${filamentStockId}:`, { weight, subJobId, jobId }); + + const filamentStock = await filamentStockModel.findById(filamentStockId); + if (!filamentStock) { + logger.error(`Filament stock with ID ${filamentStockId} not found`); + return null; + } + + // Check if a stock event already exists for this subJobId and jobId + const existingEventIndex = filamentStock.stockEvents.findIndex( + event => event.subJob?.toString() === subJobId.toString() && + event.job?.toString() === jobId.toString() + ); + + let updatedFilamentStock; + if (existingEventIndex !== -1) { + // Update existing event + logger.debug(`Updating existing stock event for subJobId ${subJobId} and jobId ${jobId}`); + updatedFilamentStock = await filamentStockModel.findOneAndUpdate( + { + _id: filamentStockId, + 'stockEvents.subJob': subJobId, + 'stockEvents.job': jobId + }, + { + $set: { + 'stockEvents.$.value': weight, + 'stockEvents.$.timestamp': new Date() + } + }, + { new: true } + ); + } else { + // Create new stock event + logger.debug(`Creating new stock event for subJobId ${subJobId} and jobId ${jobId}`); + const stockEvent = { + type: 'subJob', + value: weight, + subJob: subJobId, + job: jobId, + timestamp: new Date() + }; + + updatedFilamentStock = await filamentStockModel.findByIdAndUpdate( + filamentStockId, + { + $push: { stockEvents: stockEvent } + }, + { new: true } + ); + } + + if (!updatedFilamentStock) { + logger.error(`Failed to update filament stock ${filamentStockId}`); + return null; + } + + // Calculate new net weight based on all stock events + const totalEventWeight = updatedFilamentStock.stockEvents.reduce((sum, event) => sum + event.value, 0); + const newNetWeight = updatedFilamentStock.startingNetWeight + totalEventWeight; + const newGrossWeight = updatedFilamentStock.startingGrossWeight + totalEventWeight; + + + // Update the net weight + const finalFilamentStock = await filamentStockModel.findByIdAndUpdate( + filamentStockId, + { currentNetWeight: newNetWeight, currentGrossWeight: newGrossWeight }, + { + new: true, + populate: { + path: 'stockEvents', + populate: [ + { + path: 'subJob', + select: 'number' + }, + { + path: 'job', + select: 'startedAt' + } + ] + } + } + ); + + // Calculate remaining percentage and update state + const remainingPercent = newNetWeight / finalFilamentStock.startingNetWeight; + const state = { + type: 'partiallyconsumed', + percent: (1 - remainingPercent).toFixed(2) + }; + + const filamentStockWithState = await filamentStockModel.findByIdAndUpdate( + filamentStockId, + { state }, + { new: true } + ); + + logger.info(`Updated filament stock ${filamentStockId}:`, { + newGrossWeight: newGrossWeight, + newNetWeight: newNetWeight, + eventCount: finalFilamentStock.stockEvents.length, + updatedExistingEvent: existingEventIndex !== -1, + remainingPercent: remainingPercent.toFixed(2), + consumedPercent: state.percent, + }); + + // Broadcast the update through websocket + this.socketManager.broadcast("notify_filamentstock_update", { + id: filamentStockId, + currentNetWeight: newNetWeight, + currentGrossWeight: newGrossWeight, + stockEvents: finalFilamentStock.stockEvents, + state + }); + + return filamentStockWithState; + } catch (error) { + logger.error(`Failed to update filament stock weight for ${filamentStockId}:`, error); + throw error; + } + } +} \ No newline at end of file diff --git a/src/printer/printerclient.js b/src/printer/printerclient.js index dae5a3a..47c4ebe 100644 --- a/src/printer/printerclient.js +++ b/src/printer/printerclient.js @@ -2,9 +2,9 @@ import { JsonRPC } from "./jsonrpc.js"; import { WebSocket } from "ws"; import { loadConfig } from "../config.js"; -import { printerModel } from "../database/printer.schema.js"; // Import your printer model -import { printJobModel } from "../database/printjob.schema.js"; +import { printerModel } from "../database/printer.schema.js"; import { printSubJobModel } from "../database/printsubjob.schema.js"; +import { PrinterDatabase } from "./database.js"; import log4js from "log4js"; import axios from "axios"; import FormData from "form-data"; @@ -18,30 +18,36 @@ logger.level = config.server.logLevel; export class PrinterClient { constructor(printer, printerManager, socketManager) { this.id = printer.id; - this.name = printer.printerName; + this.name = printer.name; this.printerManager = printerManager; this.socketManager = socketManager; - this.state = printer.state; - this.klippyState = null; + this.state = { type: 'offline '}; + this.klippyState = { type: 'offline '}; this.config = printer.moonraker; this.version = printer.version; this.jsonRpc = new JsonRPC(); this.socket = null; this.connectionId = null; - this.currentJobId = printer.currentJob; + this.currentJobId = null; this.currentJobState = { type: "unknown", progress: 0 }; - this.currentSubJobId = printer.currentSubJob; - this.currentSubJobState = { type: "unknown", progress: 0 }; + this.currentSubJobId = null; + this.currentSubJobState = null; + this.currentFilamentStockId = printer.currentFilamentStock?._id.toString() || null; + this.currentFilamentStockDensity = printer.currentFilamentStock?.filament?.density || null this.registerEventHandlers(); this.baseSubscription = { print_stats: null, display_status: null, + 'filament_switch_sensor fsensor': null, + output_pin: null }; this.subscriptions = new Map(); this.queuedJobIds = []; this.isOnline = printer.online; this.subJobIsCancelling = false; this.subJobCancelId = null; + this.database = new PrinterDatabase(socketManager); + this.filamentDetected = false; } registerEventHandlers() { @@ -78,10 +84,12 @@ export class PrinterClient { async getPrinterConnectionConfig() { try { - const printer = await printerModel.findOne({ _id: this.id }); - this.config = printer.moonraker; - logger.info(`Reloaded connection config! (${this.name})`); - logger.debug(this.config); + const config = await this.database.getPrinterConfig(this.id); + if (config) { + this.config = config; + logger.info(`Reloaded connection config! (${this.name})`); + logger.debug(this.config); + } } catch (error) { logger.error( `Failed to get printer connection config! (${this.name}):`, @@ -181,21 +189,10 @@ export class PrinterClient { `Klippy info for ${this.name}: ${klippyResult.hostname}, ${klippyResult.software_version}`, ); // Update firmware version in database - try { - await printerModel.findByIdAndUpdate( - this.id, - { firmware: klippyResult.software_version }, - { new: true }, - ); - logger.info( - `Updated firmware version for ${this.name} to ${klippyResult.software_version}`, - ); - } catch (error) { - logger.error( - `Failed to update firmware version in database (${this.name}):`, - error, - ); - } + await this.database.updatePrinterFirmware(this.id, klippyResult.software_version); + logger.info( + `Updated firmware version for ${this.name} to ${klippyResult.software_version}`, + ); if (klippyResult.state === "error") { logger.error( @@ -321,287 +318,220 @@ export class PrinterClient { async handleStatusUpdate(status) { logger.trace("Status update:", status); status = status[0]; - // Process printer status updates - if (this.state.type != "deploying") { - if (status.print_stats && status.print_stats.state) { - logger.info( - `Print state for ${this.name}: ${status.print_stats.state}`, - ); + + if (this.state.type === "deploying") { + return; + } - const stateTypeChanged = status.print_stats.state != this.state.type; + let stateChanged = false; + let progressChanged = false; - // When status changes, update states - if (stateTypeChanged) { - this.state.type = status.print_stats.state; - await this.updatePrinterState(); + if (status.print_stats?.state) { + const newState = status.print_stats.state; + if (newState !== this.state.type) { + logger.info(`printer ${this.name} state changed from ${this.state.type} to ${newState}`); + this.state.type = newState; + stateChanged = true; + } + } + + if (status.print_stats?.filament_used) { + // Convert mm to cm and calculate volume + const filamentLengthCm = status.print_stats.filament_used / 10; + const filamentDiameterCm = 0.175; // 1.75mm in cm + const filamentRadiusCm = filamentDiameterCm / 2; + const filamentVolumeCm3 = Math.PI * Math.pow(filamentRadiusCm, 2) * filamentLengthCm; + + // Calculate weight in grams + const filamentWeightG = filamentVolumeCm3 * this.currentFilamentStockDensity; + + console.log('Filament used:', { + length: status.print_stats.filament_used, + lengthCm: filamentLengthCm, + volumeCm3: filamentVolumeCm3, + density: this.currentFilamentStockDensity, + weightG: filamentWeightG + }); + + if (this.currentSubJobId != null && this.currentJobId != null && this.currentFilamentStockId != null) { +this.database.updateFilamentStockWeight(this.currentFilamentStockId, (-1 * filamentWeightG), this.currentSubJobId, this.currentJobId); + } + + + } + + if (status.display_status?.progress !== undefined) { + const newProgress = status.display_status.progress; + if (newProgress !== this.state.progress) { + logger.info(`Printer ${this.name} progress changed from ${this.state.progress} to ${newProgress}`); + this.state.progress = newProgress; + progressChanged = true; + } + + if (status.display_status.message) { + await this.database.updateDisplayStatus(this.id, status.display_status.message); + } + } + + // Handle filament switch sensor + if (status['filament_switch_sensor fsensor']?.filament_detected !== undefined) { + const newFilamentDetected = status['filament_switch_sensor fsensor'].filament_detected; + if (newFilamentDetected !== this.filamentDetected) { + logger.info(`Printer ${this.name} filament detection changed from ${this.filamentDetected} to ${newFilamentDetected} with no currentFilamentId`); + this.filamentDetected = newFilamentDetected; + + if (newFilamentDetected == true && this.currentFilamentStockId == null) { + await this.database.addAlert(this.id, { + type: "loadFilamentStock", + priority: 1, + timestamp: new Date() + }); + } else if (newFilamentDetected == false && this.currentFilamentStockId != null) { + + this.currentFilamentStockId = null + await this.database.setCurrentFilamentStock(this.id, null) + // Remove filament select alert if it exists + await this.database.removeAlerts(this.id, {type:"loadFilamentStock"}); + } + } + } + + if (stateChanged || progressChanged) { + // Update printer state first + await this.database.updatePrinterState(this.id, this.state, this.online); + + logger.warn('State changed!') + // Set current job to null when not printing or paused + if (!["printing", "paused"].includes(this.state.type)) { + this.currentJobId = null; + this.currentSubJobId = null; + await this.database.clearCurrentJob(this.id, `Printer is in ${this.state.type} state`); + await this.getQueuedJobsInfo(); + } else { + // If we have a current subjob, update its state + if (this.currentSubJobId) { + logger.debug(`Updating current subjob ${this.currentSubJobId} state:`, this.state); + await this.database.updateSubJobState(this.currentSubJobId, this.state); + } else { + // If no current subjob but we have queued jobs, check if we need to update printer subjobs await this.getQueuedJobsInfo(); } } - - if (status.display_status) { - console.log("Display status:", status.display_status); - this.state.progress = status.display_status.progress; - await this.updatePrinterState(); - await this.updateCurrentJobAndSubJobState(); - } - - this.socketManager.broadcastToSubscribers(this.id, { - method: "notify_status_update", - params: status, - }); } + + + this.socketManager.broadcastToSubscribers(this.id, { + method: "notify_status_update", + params: status, + }); } async updatePrinterState() { try { - if (this.state.type == "printing" && this.state.progress == undefined) { - this.state.progress = 0; - } - // Update the printer's state - const updatedPrinter = await printerModel.findByIdAndUpdate( - this.id, - { state: this.state, online: this.online }, - { new: true }, - ); - - if (!updatedPrinter) { - logger.error( - `Printer with ID ${this.id} not found when updating status`, - ); - return; - } - - // Notify clients of the update - this.socketManager.broadcast("notify_printer_update", { - id: this.id, - state: this.state, - }); - - logger.info( - `Updated printer state to ${this.state.type} and progress to ${this.state?.progress} for ${this.id}`, - ); + await this.database.updatePrinterState(this.id, this.state, this.online); } catch (error) { - logger.error( - `Failed to update job status in database (${this.name}):`, - error, - ); + logger.error(`Failed to update printer state:`, error); } } async removePrinterSubJob(subJobId) { try { - const updatedPrinter = await printerModel.findByIdAndUpdate( - this.id, - { - $pull: { subJobs: subJobId }, - }, - { new: true }, - ); - - if (!updatedPrinter) { - logger.error( - `Printer with ID ${this.id} not found when removing failed subjob`, - ); - return; - } - - logger.info(`Removed subjob ${subJobId} from printer ${this.id}`); + await this.database.removePrinterSubJob(this.id, subJobId); } catch (error) { - logger.error( - `Failed to remove subjob ${subJobId} from printer ${this.id}:`, - error, - ); + logger.error(`Failed to remove subjob:`, error); } } async updatePrinterSubJobs() { try { - logger.debug("Updating Printer Subjobs..."); - // Get all subjobs for this printer - const subJobs = await printSubJobModel.find({ printer: this.id }); + logger.debug(`Updating printer subjobs for ${this.id}`, { + queuedJobIds: this.queuedJobIds, + currentSubJobId: this.currentSubJobId, + currentJobId: this.currentJobId, + printerState: this.state.type + }); - for (const subJob of subJobs) { - if ( - !this.queuedJobIds.includes(subJob.subJobId) && - !( - subJob.state.type == "failed" || - subJob.state.type == "complete" || - subJob.state.type == "draft" || - subJob.state.type == "cancelled" - ) - ) { - if (subJob.subJobId == this.subJobCancelId) { - const updatedSubJob = await printSubJobModel.findByIdAndUpdate( - subJob.id, - { state: { type: "cancelled" } }, - { new: true }, - ); + const printer = await printerModel.findById(this.id).populate('subJobs'); - await this.removePrinterSubJob(subJob.id); - // Notify clients of the update - this.socketManager.broadcast("notify_subjob_update", { - id: subJob.id, - state: { type: "cancelled" }, - }); - if (updatedSubJob) { - logger.debug(`Cancelled subjob ${updatedSubJob.id}`); - } - return; - } else { - logger.debug( - `Subjob ${subJob.id} is not in the queued job list. It must be printing or paused. Setting job and subjob for ${this.id}`, - ); - this.currentSubJobId = subJob.id; - this.currentJobId = subJob.printJob; - await this.updateCurrentJobAndSubJobState(); - const updatedPrinter = await printerModel.findByIdAndUpdate( - this.id, - { - currentSubJob: this.currentSubJobId, - currentJob: this.currentJobId, - }, - { new: true }, - ); + const subJobs = printer.subJobs + + // If printer is not printing or paused, clear current job/subjob + if (!["printing", "paused"].includes(this.state.type)) { + if (this.currentSubJobId || this.currentJobId) { + logger.info(`Clearing current job/subjob for printer ${this.name} as state is ${this.state.type}`); + await this.database.clearCurrentJob(this.id); + this.currentSubJobId = null; + this.currentJobId = null; + } + } else { + // Printer is printing or paused - find the current job + // Sort subjobs by number property + const sortedSubJobs = subJobs.sort((a, b) => a.number - b.number); + + // Find subjobs that are in queued state + const queuedSubJobs = sortedSubJobs.filter(subJob => + subJob.state.type === "queued" && + this.queuedJobIds.includes(subJob.subJobId) + ); - if (!updatedPrinter) { - logger.error( - `Printer with ID ${this.id} not found when setting job and subjob`, - ); - return; - } + // Find subjobs that are not in queued state but should be + const missingQueuedSubJobs = sortedSubJobs.filter(subJob => + subJob.state.type === "queued" && + !this.queuedJobIds.includes(subJob.subJobId) + ); - logger.info(`Set job and subjob for ${this.id}`); - // If the status is failed or completed, remove the subjob from the printer's subJobs array + // If we have missing queued jobs and printer is in standby, mark them as failed + if (missingQueuedSubJobs.length > 0 && this.state.type === "standby") { + logger.warn(`Found ${missingQueuedSubJobs.length} missing queued jobs for printer ${this.name} in standby state`); + for (const subJob of missingQueuedSubJobs) { + logger.info(`Marking missing queued subjob ${subJob.id} as failed`); + await this.database.updateSubJobState(subJob.id, { type: "failed" }); } } - if ( - subJob.state.type === "failed" || - subJob.state.type === "complete" - ) { - await this.removePrinterSubJob(subJob.id); + + // If we have a current subjob, verify it's still valid + if (this.currentSubJobId) { + const currentSubJob = sortedSubJobs.find(sj => sj.id === this.currentSubJobId); + if (!currentSubJob || !this.queuedJobIds.includes(currentSubJob.subJobId)) { + logger.info(`Current subjob ${this.currentSubJobId} is no longer valid, clearing it`); + await this.database.clearCurrentJob(this.id); + this.currentSubJobId = null; + this.currentJobId = null; + } + } + + // If we don't have a current subjob but have queued jobs, find the first one + if (!this.currentSubJobId) { + const result = await this.database.setCurrentJobForPrinting(this.id, this.queuedJobIds); + if (result) { + logger.info(`Setting first queued subjob as current for printer ${this.name}`, result); + this.currentSubJobId = result.currentSubJob._id; + this.currentJobId = result.currentJob._id; + await this.database.updateSubJobState(this.currentSubJobId, this.state); + } + } + } + + // Update states for all subjobs + for (const subJob of subJobs) { + if (!this.queuedJobIds.includes(subJob.subJobId)) { + if (subJob.subJobId === this.subJobCancelId) { + logger.info(`Cancelling subjob ${subJob.id}`); + await this.database.updateSubJobState(subJob.id, { type: "cancelled" }); + await this.database.removePrinterSubJob(this.id, subJob.id); + } else if (!["failed", "complete", "draft", "cancelled"].includes(subJob.state.type)) { + // Update the subjob state to match printer state + await this.database.updateSubJobState(subJob.id, this.state); + } + } + + if (["failed", "complete", "cancelled"].includes(subJob.state.type)) { + logger.info(`Removing completed/failed/cancelled subjob ${subJob.id} from printer ${this.name}`); + await this.database.removePrinterSubJob(this.id, subJob.id); } } } catch (error) { - logger.error( - `Failed to update job status in database (${this.name}):`, - error, - ); - } - } - - async updateCurrentJobAndSubJobState() { - try { - if (!this.currentJobId || !this.currentSubJobId) { - return; - } - - this.currentJobState = { type: "unknown", progress: 0 }; - this.currentSubJobState = { type: "unknown", progress: 0 }; - - // Get the current job - const currentJob = await printJobModel - .findById(this.currentJobId) - .populate("subJobs"); - - const jobLength = currentJob.subJobs.length; - - let externalProgressSum = 0; - - var printing = 0; - var paused = 0; - var complete = 0; - var failed = 0; - - for (const subJob of currentJob.subJobs.filter( - (subJob) => subJob.id != this.currentSubJobId, - )) { - if (subJob.state.type === "printing") { - externalProgressSum = externalProgressSum + subJob.state.progress; - printing = printing + 1; - } - if (subJob.state.type === "paused") { - paused = paused + 1; - } - if (subJob.state.type === "complete") { - complete = complete + 1; - } - if (subJob.state.type === "failed") { - failed = failed + 1; - } - } - - if (this.state.type === "printing") { - this.currentSubJobState.type = "printing"; - this.currentSubJobState.progress = this.state.progress; - printing = printing + 1; - } else if (this.state.type === "paused") { - this.currentSubJobState.type = "paused"; - paused = paused + 1; - } else if (this.state.type === "complete") { - this.currentSubJobState.type = "complete"; - complete = complete + 1; - } else { - this.currentSubJobState.type = "failed"; - failed = failed + 1; - } - - if (paused > 0) { - this.currentJobState.type = "paused"; - } else if (printing > 0) { - this.currentJobState.type = "printing"; - } else if (failed > 0) { - this.currentJobState.type = "failed"; - } else if (complete == jobLength) { - this.currentJobState.type = "complete"; - } else { - this.currentJobState.type = "queued"; - } - - if (this.state.type === "printing") { - this.currentJobState.progress = - (externalProgressSum + complete + (this.state.progress || 0)) / - jobLength; - } else { - this.currentJobState.progress = externalProgressSum / jobLength; - } - - currentJob.state = this.currentJobState; - - await currentJob.save(); - - this.socketManager.broadcast("notify_job_update", { - id: this.currentJobId, - state: this.currentJobState, - }); - - logger.info( - `Updated job status to ${this.currentJobState.type} (Progress: ${this.currentJobState.progress}) for ${this.currentSubJobId}`, - ); - - const updatedSubJob = await printSubJobModel.findByIdAndUpdate( - this.currentSubJobId, - { state: this.currentSubJobState }, - { new: true }, - ); - - if (!updatedSubJob) { - logger.error( - `Sub job with ID ${this.currentSubJobId} not found when updating status`, - ); - return; - } - - // Notify clients of the update - this.socketManager.broadcast("notify_subjob_update", { - id: this.currentSubJobId, - state: this.currentSubJobState, - }); - - logger.info( - `Updated sub job status to ${this.currentSubJobState.type} (Progress: ${this.currentSubJobState.progress}) for ${this.currentSubJobId}`, - ); - } catch (error) { - logger.error(`Error updating current job state:`, error); + logger.error(`Failed to update printer subjobs for ${this.id}:`, error); } } @@ -737,7 +667,7 @@ export class PrinterClient { state: { type: "queued" }, updatedAt: new Date(), }, - { new: true }, + { new: true } ); if (!updatedSubJob) { @@ -745,17 +675,8 @@ export class PrinterClient { } // Update the printer's subJobs array - const printer = await printerModel.findById(this.id); - if (printer) { - printer.subJobs.push(updatedSubJob._id); - await printer.save(); - } - - this.socketManager.broadcast("notify_subjob_update", { - id: subJob.id, - subJobId: result.queued_jobs[result.queued_jobs.length - 1].job_id, - state: { type: "queued" }, - }); + await this.database.addSubJobToPrinter(this.id, updatedSubJob._id); + await this.database.updateSubJobState(subJob.id, { type: "queued" }); logger.info("Sub job deployed to printer:", this.id); } @@ -786,4 +707,11 @@ export class PrinterClient { logger.error(`Error canceling sub job ${subJobId}:`, error); } } + + async loadFilamentStock(filamentStockId) { + this.currentFilamentStockId = filamentStockId; + const result = await this.database.setCurrentFilamentStock(this.id, this.currentFilamentStockId); + this.currentFilamentStockDensity = result.filament.density + await this.database.removeAlerts(this.id, {type: 'loadFilamentStock'}) + } } diff --git a/src/printer/printermanager.js b/src/printer/printermanager.js index 3d36cdf..7521c0c 100644 --- a/src/printer/printermanager.js +++ b/src/printer/printermanager.js @@ -22,7 +22,10 @@ export class PrinterManager { async initializePrinterConnections() { try { // Get all printers from the database - const printers = await printerModel.find({}); + const printers = await printerModel.find({}).populate({ path: "currentFilamentStock", + populate: { + path: "filament", + },}); for (const printer of printers) { await this.connectToPrinter(printer); @@ -46,7 +49,7 @@ export class PrinterManager { // Connect to the printer await printerClientConnection.connect(); - logger.info(`Connected to printer: ${printer.printerName} (${printer.id})`); + logger.info(`Connected to printer: ${printer.name} (${printer.id})`); return true; } diff --git a/src/socket/socketclient.js b/src/socket/socketclient.js index 2486b59..387a2e4 100644 --- a/src/socket/socketclient.js +++ b/src/socket/socketclient.js @@ -20,441 +20,510 @@ export class SocketClient { this.activeSubscriptions = new Map(); this.scanner = new WebSocketScanner({ maxThreads: 50 }); - this.socket.on("bridge.list_printers", (data) => {}); + this.setupSocketEventHandlers(); + } - this.socket.on("bridge.add_printer", (data, callback) => {}); + setupSocketEventHandlers() { + this.socket.on("bridge.list_printers", this.handleListPrinters.bind(this)); + this.socket.on("bridge.add_printer", this.handleAddPrinter.bind(this)); + this.socket.on("bridge.remove_printer", this.handleRemovePrinter.bind(this)); + this.socket.on("bridge.update_printer", this.handleUpdatePrinter.bind(this)); + this.socket.on("bridge.scan_network.start", this.handleScanNetworkStart.bind(this)); + this.socket.on("bridge.scan_network.stop", this.handleScanNetworkStop.bind(this)); + this.socket.on("printer.objects.subscribe", this.handlePrinterObjectsSubscribe.bind(this)); + this.socket.on("printer.objects.unsubscribe", this.handlePrinterObjectsUnsubscribe.bind(this)); + this.socket.on("printer.gcode.script", this.handleGcodeScript.bind(this)); + this.socket.on("printer.objects.query", this.handlePrinterObjectsQuery.bind(this)); + this.socket.on("printer.emergency_stop", this.handleEmergencyStop.bind(this)); + this.socket.on("printer.firmware_restart", this.handleFirmwareRestart.bind(this)); + this.socket.on("printer.restart", this.handlePrinterRestart.bind(this)); + this.socket.on("server.job_queue.status", this.handleJobQueueStatus.bind(this)); + this.socket.on("server.job_queue.deploy", this.handleJobQueueDeploy.bind(this)); + this.socket.on("printer.print.resume", this.handlePrintResume.bind(this)); + this.socket.on("server.job_queue.cancel", this.handleJobQueueCancel.bind(this)); + this.socket.on("printer.print.cancel", this.handlePrintCancel.bind(this)); + this.socket.on("printer.print.pause", this.handlePrintPause.bind(this)); + this.socket.on("server.job_queue.pause", this.handleJobQueuePause.bind(this)); + this.socket.on("server.job_queue.start", this.handleJobQueueStart.bind(this)); + this.socket.on("printer.filamentstock.load", this.handleFilamentStockLoad.bind(this)); + this.socket.on("disconnect", this.handleDisconnect.bind(this)); + } - this.socket.on("bridge.remove_printer", (data, callback) => {}); + handleListPrinters(data) { + // Implementation for bridge.list_printers + } - this.socket.on("bridge.update_printer", (data, callback) => {}); + handleAddPrinter(data, callback) { + // Implementation for bridge.add_printer + } - this.socket.on("bridge.scan_network.start", (data, callback) => { - if (this.scanner.scanning == false) { - try { - this.scanner = new WebSocketScanner({ maxThreads: 50 }); - // Listen for found services - this.scanner.on("serviceFound", (data) => { - logger.info( - `Found websocket service at ${data.hostname} (${data.ip})`, - ); - this.socket.emit("notify_scan_network_found", data); - }); + handleRemovePrinter(data, callback) { + // Implementation for bridge.remove_printer + } - // Listen for scan progress - this.scanner.on("scanProgress", ({ currentIP, progress }) => { - logger.info( - `Scanning ${currentIP} (${progress.toFixed(2)}% complete)`, - ); - this.socket.emit("notify_scan_network_progress", { - currentIP: currentIP, - progress: progress, - }); - }); + handleUpdatePrinter(data, callback) { + // Implementation for bridge.update_printer + } - // Start scanning on port + async handleScanNetworkStart(data, callback) { + if (this.scanner.scanning == false) { + try { + this.scanner = new WebSocketScanner({ maxThreads: 50 }); + // Listen for found services + this.scanner.on("serviceFound", (data) => { logger.info( - "Scanning network for websocket services on port:", - data?.port || 7125, - "using protocol:", - data?.protocol || "ws", + `Found websocket service at ${data.hostname} (${data.ip})`, ); - this.scanner - .scanNetwork(data?.port || 7125, data?.protocol || "ws") - .then((foundServices) => { - logger.info("Scan complete. Found services:", foundServices); - this.socket.emit("notify_scan_network_complete", foundServices); - }) - .catch((error) => { - logger.error("Scan error:", error); - this.socket.emit("notify_scan_network_complete", false); - }); - } catch (error) { - logger.error("Scan error:", error); - this.socket.emit("notify_scan_network_complete", false); - } - } - }); + this.socket.emit("notify_scan_network_found", data); + }); - this.socket.on("bridge.scan_network.stop", (callback) => { - if (this.scanner.scanning == true) { - logger.info("Stopping network scan"); - this.scanner.removeAllListeners("serviceFound"); - this.scanner.removeAllListeners("scanProgress"); - this.scanner.removeAllListeners("scanComplete"); - this.scanner.stopScan(); - callback(true); - } else { - logger.info("Scan not in progress"); - callback(false); - } - }); - - // Handle printer object subscriptions - this.socket.on("printer.objects.subscribe", async (data, callback) => { - logger.debug("Received printer.objects.subscribe event:", data); - try { - if (data && data.printerId) { - const printerId = data.printerId; - - // Get existing subscription or create new one - const existingSubscription = - this.activeSubscriptions.get(printerId) || {}; - - // Merge the new subscription data with existing data - const mergedSubscription = { - ...existingSubscription.objects, - ...data.objects, - }; - - this.activeSubscriptions.set(printerId, mergedSubscription); - - logger.trace("Merged subscription:", mergedSubscription); - const result = await this.printerManager.updateSubscription( - printerId, - socket.id, - mergedSubscription, + // Listen for scan progress + this.scanner.on("scanProgress", ({ currentIP, progress }) => { + logger.info( + `Scanning ${currentIP} (${progress.toFixed(2)}% complete)`, ); - - if (callback) { - callback(result); - } - } else { - logger.error("Missing Printer ID in subscription request"); - if (callback) { - callback({ error: "Missing Printer ID" }); - } - } - } catch (e) { - logger.error("Error processing subscription request:", e); - if (callback) { - callback({ error: e.message }); - } - } - }); - - this.socket.on("printer.objects.unsubscribe", async (data, callback) => { - logger.debug("Received printer.objects.unsubscribe event:", data); - try { - if (data && data.printerId) { - const printerId = data.printerId; - const existingSubscription = this.activeSubscriptions.get(printerId); - - if (existingSubscription) { - // Create a new objects object without the unsubscribed objects - const remainingObjects = { ...existingSubscription.objects }; - if (data.objects) { - for (const key of Object.keys(data.objects)) { - delete remainingObjects[key]; - } - } - - // If there are no remaining objects, remove the entire subscription - if (Object.keys(remainingObjects).length === 0) { - this.activeSubscriptions.delete(printerId); - - // Send subscribe command with updated subscription - const result = await this.printerManager.updateSubscription( - printerId, - socket.id, - {}, - ); - if (callback) { - callback(result); - } - } else { - // Update the subscription with remaining objects - const updatedSubscription = { - printerId: printerId, - objects: remainingObjects, - }; - this.activeSubscriptions.set(printerId, updatedSubscription); - - // Send subscribe command with updated subscription - const result = await this.printerManager.updateSubscription( - printerId, - socket.id, - updatedSubscription, - ); - - if (callback) { - callback(result); - } - } - } else { - logger.warn( - "No existing subscription found for printer:", - printerId, - ); - if (callback) { - callback({ success: true, message: "No subscription found" }); - } - } - } else { - logger.error("Missing Printer ID in unsubscribe request"); - if (callback) { - callback({ error: "Missing Printer ID" }); - } - } - } catch (e) { - logger.error("Error processing unsubscribe request:", e); - if (callback) { - callback({ error: e.message }); - } - } - }); - - this.socket.on("printer.gcode.script", async (data, callback) => { - logger.debug("Received printer.gcode.script event:", data); - try { - const result = await this.printerManager.processPrinterCommand({ - method: "printer.gcode.script", - params: data, + this.socket.emit("notify_scan_network_progress", { + currentIP: currentIP, + progress: progress, + }); }); - if (callback) { - callback(result); - } - } catch (e) { - logger.error("Error processing gcode script request:", e); - if (callback) { - callback({ error: e.message }); - } + // Start scanning on port + logger.info( + "Scanning network for websocket services on port:", + data?.port || 7125, + "using protocol:", + data?.protocol || "ws", + ); + this.scanner + .scanNetwork(data?.port || 7125, data?.protocol || "ws") + .then((foundServices) => { + logger.info("Scan complete. Found services:", foundServices); + this.socket.emit("notify_scan_network_complete", foundServices); + }) + .catch((error) => { + logger.error("Scan error:", error); + this.socket.emit("notify_scan_network_complete", false); + }); + } catch (error) { + logger.error("Scan error:", error); + this.socket.emit("notify_scan_network_complete", false); } - }); + } + } - this.socket.on("printer.objects.query", async (data, callback) => { - logger.debug("Received printer.objects.query event:", data); - try { - const result = await this.printerManager.processPrinterCommand({ - method: "printer.objects.query", - params: data, - }); + handleScanNetworkStop(callback) { + if (this.scanner.scanning == true) { + logger.info("Stopping network scan"); + this.scanner.removeAllListeners("serviceFound"); + this.scanner.removeAllListeners("scanProgress"); + this.scanner.removeAllListeners("scanComplete"); + this.scanner.stopScan(); + callback(true); + } else { + logger.info("Scan not in progress"); + callback(false); + } + } - if (callback) { - callback(result); - } - } catch (e) { - logger.error("Error processing printer objects query request:", e); - if (callback) { - callback({ error: e.message }); - } - } - }); + async handlePrinterObjectsSubscribe(data, callback) { + logger.debug("Received printer.objects.subscribe event:", data); + try { + if (data && data.printerId) { + const printerId = data.printerId; - this.socket.on("printer.emergency_stop", async (data, callback) => { - logger.debug("Received printer.gcode.script event:", data); - try { - const result = await this.printerManager.processPrinterCommand({ - method: "printer.emergency_stop", - params: data, - }); + // Get existing subscription or create new one + const existingSubscription = + this.activeSubscriptions.get(printerId) || {}; - if (callback) { - callback(result); - } - } catch (e) { - logger.error("Error processing gcode script request:", e); - if (callback) { - callback({ error: e.message }); - } - } - }); + // Merge the new subscription data with existing data + const mergedSubscription = { + ...existingSubscription.objects, + ...data.objects, + }; - this.socket.on("printer.firmware_restart", async (data, callback) => { - logger.debug("Received printer.firmware_restart event:", data); - try { - const result = await this.printerManager.processPrinterCommand({ - method: "printer.firmware_restart", - params: data, - }); + this.activeSubscriptions.set(printerId, mergedSubscription); - if (callback) { - callback(result); - } - } catch (e) { - logger.error("Error processing firmware restart request:", e); - if (callback) { - callback({ error: e.message }); - } - } - }); - - this.socket.on("printer.restart", async (data, callback) => { - logger.debug("Received printer.restart event:", data); - try { - const result = await this.printerManager.processPrinterCommand({ - method: "printer.restart", - params: data, - }); - - if (callback) { - callback(result); - } - } catch (e) { - logger.error("Error processing printer restart request:", e); - if (callback) { - callback({ error: e.message }); - } - } - }); - - this.socket.on("server.job_queue.status", async (data, callback) => { - logger.debug("Received server.job_queue.status event:", data); - try { - const result = await this.printerManager.processPrinterCommand({ - method: "server.job_queue.status", - params: data, - }); - - if (callback) { - callback(result); - } - } catch (e) { - logger.error("Error processing job queue status request:", e); - if (callback) { - callback({ error: e.message }); - } - } - }); - - this.socket.on("server.job_queue.deploy", async (data, callback) => { - logger.debug("Received server.job_queue.deploy event:", data); - try { - if (!data || !data.printJobId) { - throw new Error("Missing required print job ID"); - } - // Deploy the print job to all printers - const result = await this.printerManager.deployPrintJob( - data.printJobId, + logger.trace("Merged subscription:", mergedSubscription); + const result = await this.printerManager.updateSubscription( + printerId, + this.socket.id, + mergedSubscription, ); if (callback) { callback(result); } - } catch (e) { - logger.error("Error processing job queue deploy request:", e); + } else { + logger.error("Missing Printer ID in subscription request"); if (callback) { - callback({ error: e.message }); + callback({ error: "Missing Printer ID" }); } } - }); + } catch (e) { + logger.error("Error processing subscription request:", e); + if (callback) { + callback({ error: e.message }); + } + } + } - this.socket.on("printer.print.resume", async (data, callback) => { - logger.debug("Received printer.print.resume event:", data); - try { - const result = await this.printerManager.processPrinterCommand({ - method: "printer.print.resume", - params: data, - }); + async handlePrinterObjectsUnsubscribe(data, callback) { + logger.debug("Received printer.objects.unsubscribe event:", data); + try { + if (data && data.printerId) { + const printerId = data.printerId; + const existingSubscription = this.activeSubscriptions.get(printerId); - if (callback) { - callback(result); + if (existingSubscription) { + // Create a new objects object without the unsubscribed objects + const remainingObjects = { ...existingSubscription }; + if (data.objects) { + for (const key of Object.keys(data.objects)) { + delete remainingObjects[key]; + } + } + + console.log("remainingObjects", remainingObjects) + console.log("existingSubscription", existingSubscription) + console.log("unsubscribe", data.objects) + + // If there are no remaining objects, remove the entire subscription + if (Object.keys(remainingObjects).length === 0) { + this.activeSubscriptions.delete(printerId); + + logger.warn("Removing entire subscription") + + // Send subscribe command with updated subscription + const result = await this.printerManager.updateSubscription( + printerId, + this.socket.id, + {}, + ); + if (callback) { + callback(result); + } + } else { + + this.activeSubscriptions.set(printerId, remainingObjects); + + logger.warn(remainingObjects) + + // Send subscribe command with updated subscription + const result = await this.printerManager.updateSubscription( + printerId, + this.socket.id, + remainingObjects, + ); + + if (callback) { + callback(result); + } + } + } else { + logger.warn( + "No existing subscription found for printer:", + printerId, + ); + if (callback) { + callback({ success: true, message: "No subscription found" }); + } } - } catch (e) { - logger.error("Error processing print resume request:", e); + } else { + logger.error("Missing Printer ID in unsubscribe request"); if (callback) { - callback({ error: e.message }); + callback({ error: "Missing Printer ID" }); } } - }); - - this.socket.on("server.job_queue.cancel", async (data, callback) => { - logger.debug("Received server.job_queue.cancel event:", data); - try { - if (!data || !data.subJobId) { - throw new Error("Missing required sub job ID"); - } - const result = await this.printerManager.cancelSubJob(data.subJobId); - - if (callback) { - callback(result); - } - } catch (e) { - logger.error("Error processing job queue delete job request:", e); - if (callback) { - callback({ error: e.message }); - } + } catch (e) { + logger.error("Error processing unsubscribe request:", e); + if (callback) { + callback({ error: e.message }); } - }); + } + } - this.socket.on("printer.print.cancel", async (data, callback) => { - logger.debug("Received printer.print.cancel event:", data); - try { - const result = await this.printerManager.processPrinterCommand({ - method: "printer.print.cancel", - params: data, - }); + async handleGcodeScript(data, callback) { + logger.debug("Received printer.gcode.script event:", data); + try { + const result = await this.printerManager.processPrinterCommand({ + method: "printer.gcode.script", + params: data, + }); - if (callback) { - callback(result); - } - } catch (e) { - logger.error("Error processing print cancel request:", e); - if (callback) { - callback({ error: e.message }); - } + if (callback) { + callback(result); } - }); - - this.socket.on("printer.print.pause", async (data, callback) => { - logger.debug("Received printer.print.pause event:", data); - try { - const result = await this.printerManager.processPrinterCommand({ - method: "printer.print.pause", - params: data, - }); - - if (callback) { - callback(result); - } - } catch (e) { - logger.error("Error processing print pause request:", e); - if (callback) { - callback({ error: e.message }); - } + } catch (e) { + logger.error("Error processing gcode script request:", e); + if (callback) { + callback({ error: e.message }); } - }); + } + } - this.socket.on("server.job_queue.pause", async (data, callback) => { - logger.debug("Received server.job_queue.pause event:", data); - try { - const result = await this.printerManager.processPrinterCommand({ - method: "server.job_queue.pause", - params: data, - }); + async handlePrinterObjectsQuery(data, callback) { + logger.debug("Received printer.objects.query event:", data); + try { + const result = await this.printerManager.processPrinterCommand({ + method: "printer.objects.query", + params: data, + }); - if (callback) { - callback(result); - } - } catch (e) { - logger.error("Error processing job queue pause request:", e); - if (callback) { - callback({ error: e.message }); - } + if (callback) { + callback(result); } - }); - - this.socket.on("server.job_queue.start", async (data, callback) => { - logger.debug("Received server.job_queue.start event:", data); - try { - const result = await this.printerManager.processPrinterCommand({ - method: "server.job_queue.start", - params: data, - }); - - if (callback) { - callback(result); - } - } catch (e) { - logger.error("Error processing job queue start request:", e); - if (callback) { - callback({ error: e.message }); - } + } catch (e) { + logger.error("Error processing printer objects query request:", e); + if (callback) { + callback({ error: e.message }); } - }); + } + } - this.socket.on("disconnect", () => { - logger.info("External client disconnected:", socket.user?.username); - }); + async handleEmergencyStop(data, callback) { + logger.debug("Received printer.gcode.script event:", data); + try { + const result = await this.printerManager.processPrinterCommand({ + method: "printer.emergency_stop", + params: data, + }); + + if (callback) { + callback(result); + } + } catch (e) { + logger.error("Error processing gcode script request:", e); + if (callback) { + callback({ error: e.message }); + } + } + } + + async handleFirmwareRestart(data, callback) { + logger.debug("Received printer.firmware_restart event:", data); + try { + const result = await this.printerManager.processPrinterCommand({ + method: "printer.firmware_restart", + params: data, + }); + + if (callback) { + callback(result); + } + } catch (e) { + logger.error("Error processing firmware restart request:", e); + if (callback) { + callback({ error: e.message }); + } + } + } + + async handlePrinterRestart(data, callback) { + logger.debug("Received printer.restart event:", data); + try { + const result = await this.printerManager.processPrinterCommand({ + method: "printer.restart", + params: data, + }); + + if (callback) { + callback(result); + } + } catch (e) { + logger.error("Error processing printer restart request:", e); + if (callback) { + callback({ error: e.message }); + } + } + } + + async handleJobQueueStatus(data, callback) { + logger.debug("Received server.job_queue.status event:", data); + try { + const result = await this.printerManager.processPrinterCommand({ + method: "server.job_queue.status", + params: data, + }); + + if (callback) { + callback(result); + } + } catch (e) { + logger.error("Error processing job queue status request:", e); + if (callback) { + callback({ error: e.message }); + } + } + } + + async handleJobQueueDeploy(data, callback) { + logger.debug("Received server.job_queue.deploy event:", data); + try { + if (!data || !data.printJobId) { + throw new Error("Missing required print job ID"); + } + // Deploy the print job to all printers + const result = await this.printerManager.deployPrintJob( + data.printJobId, + ); + + if (callback) { + callback(result); + } + } catch (e) { + logger.error("Error processing job queue deploy request:", e); + if (callback) { + callback({ error: e.message }); + } + } + } + + async handlePrintResume(data, callback) { + logger.debug("Received printer.print.resume event:", data); + try { + const result = await this.printerManager.processPrinterCommand({ + method: "printer.print.resume", + params: data, + }); + + if (callback) { + callback(result); + } + } catch (e) { + logger.error("Error processing print resume request:", e); + if (callback) { + callback({ error: e.message }); + } + } + } + + async handleJobQueueCancel(data, callback) { + logger.debug("Received server.job_queue.cancel event:", data); + try { + if (!data || !data.subJobId) { + throw new Error("Missing required sub job ID"); + } + const result = await this.printerManager.cancelSubJob(data.subJobId); + + if (callback) { + callback(result); + } + } catch (e) { + logger.error("Error processing job queue delete job request:", e); + if (callback) { + callback({ error: e.message }); + } + } + } + + async handlePrintCancel(data, callback) { + logger.debug("Received printer.print.cancel event:", data); + try { + const result = await this.printerManager.processPrinterCommand({ + method: "printer.print.cancel", + params: data, + }); + + if (callback) { + callback(result); + } + } catch (e) { + logger.error("Error processing print cancel request:", e); + if (callback) { + callback({ error: e.message }); + } + } + } + + async handlePrintPause(data, callback) { + logger.debug("Received printer.print.pause event:", data); + try { + const result = await this.printerManager.processPrinterCommand({ + method: "printer.print.pause", + params: data, + }); + + if (callback) { + callback(result); + } + } catch (e) { + logger.error("Error processing print pause request:", e); + if (callback) { + callback({ error: e.message }); + } + } + } + + async handleJobQueuePause(data, callback) { + logger.debug("Received server.job_queue.pause event:", data); + try { + const result = await this.printerManager.processPrinterCommand({ + method: "server.job_queue.pause", + params: data, + }); + + if (callback) { + callback(result); + } + } catch (e) { + logger.error("Error processing job queue pause request:", e); + if (callback) { + callback({ error: e.message }); + } + } + } + + async handleJobQueueStart(data, callback) { + logger.debug("Received server.job_queue.start event:", data); + try { + const result = await this.printerManager.processPrinterCommand({ + method: "server.job_queue.start", + params: data, + }); + + if (callback) { + callback(result); + } + } catch (e) { + logger.error("Error processing job queue start request:", e); + if (callback) { + callback({ error: e.message }); + } + } + } + + async handleFilamentStockLoad(data, callback) { + logger.debug("Received printer.filamentstock.load event:", data); + try { + if (!data || !data.printerId) { + throw new Error("Missing required printer ID"); + } + if (!data || !data.filamentStockId) { + throw new Error("Missing required filament stock ID"); + } + + // Get the printer client + const printerClient = this.printerManager.getPrinterClient(data.printerId); + if (!printerClient) { + throw new Error(`Printer with ID ${data.printerId} not found`); + } + + // Load the filament stock + const result = await printerClient.loadFilamentStock(data.filamentStockId); + + if (callback) { + callback(result); + } + } catch (e) { + logger.error("Error processing filament load request:", e); + if (callback) { + callback({ error: e.message }); + } + } + } + + handleDisconnect() { + logger.info("External client disconnected:", this.socket.user?.username); } }