Refactored and fixed printer stated system. Added filament loading and stock.

This commit is contained in:
Tom Butcher 2025-05-19 00:28:42 +01:00
parent 81196897fa
commit 2ac65ec717
9 changed files with 1469 additions and 686 deletions

View File

@ -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);

View File

@ -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,
);

View File

@ -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);

View File

@ -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 },
);

View File

@ -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",

680
src/printer/database.js Normal file
View File

@ -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;
}
}
}

View File

@ -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}`,
);
const stateTypeChanged = status.print_stats.state != this.state.type;
if (this.state.type === "deploying") {
return;
}
// When status changes, update states
if (stateTypeChanged) {
this.state.type = status.print_stats.state;
await this.updatePrinterState();
let stateChanged = false;
let progressChanged = false;
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 (!updatedPrinter) {
logger.error(
`Printer with ID ${this.id} not found when setting job and subjob`,
);
return;
}
// 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);
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
// Find subjobs that are in queued state
const queuedSubJobs = sortedSubJobs.filter(subJob =>
subJob.state.type === "queued" &&
this.queuedJobIds.includes(subJob.subJobId)
);
// 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)
);
// 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'})
}
}

View File

@ -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;
}

View File

@ -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);
}
}