Added CI Support and other things.

This commit is contained in:
Tom Butcher 2025-06-01 20:44:06 +01:00
parent 2ac65ec717
commit 90d031c5fe
12 changed files with 4115 additions and 286 deletions

45
Jenkinsfile vendored Normal file
View File

@ -0,0 +1,45 @@
pipeline {
agent {
docker {
image 'node:20-alpine'
args '-v /var/run/docker.sock:/var/run/docker.sock'
}
}
environment {
NODE_ENV = 'production'
}
stages {
stage('Install Dependencies') {
steps {
sh 'npm ci'
}
}
stage('Build') {
steps {
sh 'npm run build'
}
}
stage('Deploy') {
steps {
echo 'Deploying application...'
// Add your deployment steps here
}
}
}
post {
always {
cleanWs()
}
success {
echo 'Pipeline completed successfully!'
}
failure {
echo 'Pipeline failed!'
}
}
}

View File

@ -1,7 +1,7 @@
{ {
"server": { "server": {
"port": 8081, "port": 8081,
"logLevel": "debug" "logLevel": "info"
}, },
"auth": { "auth": {
"enabled": true, "enabled": true,

3543
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,6 @@
"main": "src/index.js", "main": "src/index.js",
"type": "module", "type": "module",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node src/index.js", "start": "node src/index.js",
"dev": "nodemon src/index.js" "dev": "nodemon src/index.js"
}, },
@ -22,6 +21,8 @@
"ws": "^8.18.1" "ws": "^8.18.1"
}, },
"devDependencies": { "devDependencies": {
"nodemon": "^3.1.9" "jest": "^29.7.0",
"nodemon": "^3.1.9",
"supertest": "^6.3.4"
} }
} }

View File

@ -13,13 +13,7 @@ const filamentStockSchema = new Schema(
currentGrossWeight: { type: Number, required: true }, currentGrossWeight: { type: Number, required: true },
currentNetWeight: { type: Number, required: true }, currentNetWeight: { type: Number, required: true },
filament: { type: mongoose.Schema.Types.ObjectId, ref: "Filament" }, filament: { type: mongoose.Schema.Types.ObjectId, ref: "Filament" },
stockEvents: [{ stockEvents: [{ type: mongoose.Schema.Types.ObjectId, ref: "StockEvent" }]
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 }, { timestamps: true },
); );

View File

@ -17,6 +17,7 @@ const alertSchema = new Schema(
{ {
priority: { type: String, required: true }, // order to show priority: { type: String, required: true }, // order to show
type: { type: String, required: true }, // selectFilament, error, info, message, type: { type: String, required: true }, // selectFilament, error, info, message,
message: { type: String, required: false }
}, },
{ timestamps: true, _id: false } { timestamps: true, _id: false }
); );

View File

@ -13,6 +13,7 @@ const printJobSchema = new mongoose.Schema({
createdAt: { required: true, type: Date }, createdAt: { required: true, type: Date },
updatedAt: { required: true, type: Date }, updatedAt: { required: true, type: Date },
startedAt: { required: false, type: Date }, startedAt: { required: false, type: Date },
finishedAt: { required: false, type: Date },
gcodeFile: { gcodeFile: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: "GCodeFile", ref: "GCodeFile",

View File

@ -36,7 +36,9 @@ const printSubJobSchema = new mongoose.Schema({
updatedAt: { updatedAt: {
type: Date, type: Date,
default: Date.now default: Date.now
} },
startedAt: { required: false, type: Date },
finishedAt: { required: false, type: Date },
}); });
printSubJobSchema.virtual("id").get(function () { printSubJobSchema.virtual("id").get(function () {

View File

@ -0,0 +1,25 @@
import mongoose from "mongoose";
const { Schema } = mongoose;
const stockEventSchema = new Schema(
{
type: { type: String, required: true },
value: { type: Number, required: true },
subJob: { type: Schema.Types.ObjectId, ref: "PrintSubJob", required: false },
job: { type: Schema.Types.ObjectId, ref: "PrintJob", required: false },
filamentStock: { type: Schema.Types.ObjectId, ref: "FilamentStock", required: true },
timestamp: { type: Date, default: Date.now }
},
{ timestamps: true }
);
// Add virtual id getter
stockEventSchema.virtual("id").get(function () {
return this._id.toHexString();
});
// Configure JSON serialization to include virtuals
stockEventSchema.set("toJSON", { virtuals: true });
// Create and export the model
export const stockEventModel = mongoose.model("StockEvent", stockEventSchema);

View File

@ -4,6 +4,7 @@ import { printSubJobModel } from "../database/printsubjob.schema.js";
import { gcodeFileModel } from "../database/gcodefile.schema.js" import { gcodeFileModel } from "../database/gcodefile.schema.js"
import { filamentStockModel } from "../database/filamentstock.schema.js"; import { filamentStockModel } from "../database/filamentstock.schema.js";
import { filamentModel } from "../database/filament.schema.js"; import { filamentModel } from "../database/filament.schema.js";
import { stockEventModel } from "../database/stockevent.schema.js";
import log4js from "log4js"; import log4js from "log4js";
import { loadConfig } from "../config.js"; import { loadConfig } from "../config.js";
@ -11,9 +12,52 @@ const config = loadConfig();
const logger = log4js.getLogger("Printer Database"); const logger = log4js.getLogger("Printer Database");
logger.level = config.server.logLevel; logger.level = config.server.logLevel;
// Debounce utility for rate limiting database updates
class Debouncer {
constructor(delay) {
this.delay = delay;
this.timeouts = new Map();
this.pendingUpdates = new Map();
}
debounce(key, fn) {
// If this is the first call for this key, execute immediately
if (!this.timeouts.has(key)) {
this.timeouts.set(key, true);
return fn().finally(() => {
// After the first call completes, set up the delayed call
setTimeout(() => {
this.timeouts.delete(key);
// If there's a pending update, execute it
if (this.pendingUpdates.has(key)) {
const pendingFn = this.pendingUpdates.get(key);
this.pendingUpdates.delete(key);
pendingFn();
}
}, this.delay);
});
} else {
// If there's already a call in progress, store this one as pending
return new Promise((resolve) => {
this.pendingUpdates.set(key, async () => {
try {
const result = await fn();
resolve(result);
} catch (error) {
resolve(error);
}
});
});
}
}
}
export class PrinterDatabase { export class PrinterDatabase {
constructor(socketManager) { constructor(socketManager) {
this.socketManager = socketManager; this.socketManager = socketManager;
this.debouncer = new Debouncer(2000); // 2 second delay
this.filamentStock = null; // Store current filament stock
this.existingEvent = null; // Store existing stock event
logger.info("Initialized PrinterDatabase with socket manager"); logger.info("Initialized PrinterDatabase with socket manager");
} }
@ -36,6 +80,14 @@ export class PrinterDatabase {
} }
async updatePrinterState(printerId, state, online) { async updatePrinterState(printerId, state, online) {
// Broadcast immediately
logger.debug(`Broadcasting printer state update for ${printerId}:`, { state, online });
this.socketManager.broadcast("notify_printer_update", {
_id: printerId,
state,
});
return this.debouncer.debounce(`printer_${printerId}`, async () => {
try { try {
logger.debug(`Updating printer state for ${printerId}:`, { state, online }); logger.debug(`Updating printer state for ${printerId}:`, { state, online });
@ -62,22 +114,16 @@ export class PrinterDatabase {
previousState: updatedPrinter.state previousState: updatedPrinter.state
}); });
this.socketManager.broadcast("notify_printer_update", {
id: printerId,
state,
});
return updatedPrinter; return updatedPrinter;
} catch (error) { } catch (error) {
logger.error(`Failed to update printer state for ${printerId}:`, error); logger.error(`Failed to update printer state for ${printerId}:`, error);
throw error; throw error;
} }
});
} }
async clearCurrentJob(printerId) { async clearCurrentJob(printerId) {
try { try {
logger.warn(`Clearing current job for printer ${printerId}`);
const updatedPrinter = await printerModel.findByIdAndUpdate( const updatedPrinter = await printerModel.findByIdAndUpdate(
printerId, printerId,
{ {
@ -93,8 +139,9 @@ export class PrinterDatabase {
} }
// Broadcast the update through websocket // Broadcast the update through websocket
logger.debug(`Broadcasting current job clear for printer ${printerId}`);
this.socketManager.broadcast("notify_printer_update", { this.socketManager.broadcast("notify_printer_update", {
id: printerId, _id: printerId,
currentSubJob: null, currentSubJob: null,
currentJob: null currentJob: null
}); });
@ -111,7 +158,6 @@ export class PrinterDatabase {
async setCurrentJobForPrinting(printerId, queuedJobIds) { async setCurrentJobForPrinting(printerId, queuedJobIds) {
try { try {
logger.error(`Setting current job for printer ${printerId} as printing starts`);
const printer = await printerModel.findById(printerId).populate('subJobs'); const printer = await printerModel.findById(printerId).populate('subJobs');
if (!printer) { if (!printer) {
@ -168,8 +214,12 @@ export class PrinterDatabase {
} }
// Broadcast the update through websocket // Broadcast the update through websocket
logger.debug(`Broadcasting current job update for printer ${printerId}:`, {
subJobId: subJob.id,
jobId: subJob.printJob
});
this.socketManager.broadcast("notify_printer_update", { this.socketManager.broadcast("notify_printer_update", {
id: printerId, _id: printerId,
currentSubJob: subJob, currentSubJob: subJob,
currentJob: job currentJob: job
}); });
@ -188,8 +238,15 @@ export class PrinterDatabase {
} }
async updateSubJobState(subJobId, state) { async updateSubJobState(subJobId, state) {
try { // Broadcast immediately
logger.debug(`Broadcasting subjob state update for ${subJobId}:`, { state });
this.socketManager.broadcast("notify_subjob_update", {
_id: subJobId,
state,
});
return this.debouncer.debounce(`subjob_${subJobId}`, async () => {
try {
if (state.type == "standby" || state.type == "error" || state.type == "offline") { if (state.type == "standby" || state.type == "error" || state.type == "offline") {
state.type = 'failed' state.type = 'failed'
logger.warn(`Updating subjob state for ${subJobId}:`, state); logger.warn(`Updating subjob state for ${subJobId}:`, state);
@ -197,10 +254,28 @@ logger.warn(`Updating subjob state for ${subJobId}:`, state);
logger.debug(`Updating subjob state for ${subJobId}:`, state); logger.debug(`Updating subjob state for ${subJobId}:`, state);
} }
// Set startedAt if the subjob is starting to print
const updateData = { state };
if (state.type === 'printing') {
const subJob = await printSubJobModel.findById(subJobId);
if (!subJob.startedAt) {
updateData.startedAt = new Date();
logger.info(`Setting startedAt for subjob ${subJobId} as printing begins`);
}
}
// Set finishedAt if the subjob is complete, failed, or cancelled
if (['complete', 'failed', 'cancelled'].includes(state.type)) {
const subJob = await printSubJobModel.findById(subJobId);
if (!subJob.finishedAt) {
updateData.finishedAt = new Date();
logger.info(`Setting finishedAt for subjob ${subJobId} as state is ${state.type}`);
}
}
const updatedSubJob = await printSubJobModel.findByIdAndUpdate( const updatedSubJob = await printSubJobModel.findByIdAndUpdate(
subJobId, subJobId,
{ state }, updateData,
{ new: true } { new: true }
); );
@ -213,25 +288,24 @@ logger.warn(`Updating subjob state for ${subJobId}:`, state);
type: state.type, type: state.type,
progress: state.progress, progress: state.progress,
previousState: updatedSubJob.state, previousState: updatedSubJob.state,
printJob: updatedSubJob.printJob printJob: updatedSubJob.printJob,
startedAt: updatedSubJob.startedAt,
finishedAt: updatedSubJob.finishedAt
}); });
// Update parent job state // Update parent job state
await this.updateJobState(updatedSubJob.printJob); await this.updateJobState(updatedSubJob.printJob);
this.socketManager.broadcast("notify_subjob_update", {
id: subJobId,
state,
});
return updatedSubJob; return updatedSubJob;
} catch (error) { } catch (error) {
logger.error(`Failed to update sub job state for ${subJobId}:`, error); logger.error(`Failed to update sub job state for ${subJobId}:`, error);
throw error; throw error;
} }
});
} }
async updateJobState(jobId) { async updateJobState(jobId) {
return this.debouncer.debounce(`job_${jobId}`, async () => {
try { try {
logger.debug(`Updating job state for ${jobId}`); logger.debug(`Updating job state for ${jobId}`);
@ -269,21 +343,35 @@ logger.warn(`Updating subjob state for ${subJobId}:`, state);
subJobCount: job.subJobs.length subJobCount: job.subJobs.length
}); });
// Update finishedAt if all subjobs are complete and none are queued
if ((stateCounts.complete + stateCounts.failed + stateCounts.cancelled) === job.subJobs.length && stateCounts.queued === 0 && !job.finishedAt) {
logger.info(`Setting finishedAt for job ${jobId} as all subjobs are complete`);
job.finishedAt = new Date();
}
job.state = jobState; job.state = jobState;
job.subJobStats = stateCounts; job.subJobStats = stateCounts;
await job.save(); await job.save();
logger.info(`Updated job ${jobId} state:`, { logger.info(`Updated job ${jobId} state:`, {
id: jobId, _id: jobId,
state: jobState, state: jobState,
subJobStats: stateCounts subJobStats: stateCounts,
finishedAt: job.finishedAt
}); });
this.socketManager.broadcast("notify_job_update", { // Broadcast immediately after calculating new state
id: jobId, logger.debug(`Broadcasting job state update for ${jobId}:`, {
state: jobState, state: jobState,
subJobStats: stateCounts subJobStats: stateCounts,
finishedAt: job.finishedAt
});
this.socketManager.broadcast("notify_job_update", {
_id: jobId,
state: jobState,
subJobStats: stateCounts,
finishedAt: job.finishedAt
}); });
return job; return job;
@ -291,6 +379,7 @@ logger.warn(`Updating subjob state for ${subJobId}:`, state);
logger.error(`Failed to update job state for ${jobId}:`, error); logger.error(`Failed to update job state for ${jobId}:`, error);
throw error; throw error;
} }
});
} }
determineJobState(stateCounts) { determineJobState(stateCounts) {
@ -374,6 +463,7 @@ logger.warn(`Updating subjob state for ${subJobId}:`, state);
try { try {
logger.debug(`Updating display status for printer ${printerId}:`, { message }); logger.debug(`Updating display status for printer ${printerId}:`, { message });
logger.debug(`Broadcasting display status for printer ${printerId}:`, { message });
this.socketManager.broadcast("notify_display_status", { this.socketManager.broadcast("notify_display_status", {
printerId, printerId,
message message
@ -417,25 +507,67 @@ logger.warn(`Updating subjob state for ${subJobId}:`, state);
try { try {
logger.debug(`Adding alert to printer ${printerId}:`, alert); logger.debug(`Adding alert to printer ${printerId}:`, alert);
// First check if an alert of this type already exists
const printer = await printerModel.findById(printerId);
if (!printer) {
logger.error(`Printer with ID ${printerId} not found when adding alert`);
return null;
}
const existingAlertIndex = printer.alerts.findIndex(a => a.type === alert.type);
if (existingAlertIndex !== -1) {
// If we have a message to update, update the existing alert
if (alert.message) {
logger.debug(`Updating message for existing alert of type ${alert.type} on printer ${printerId}`);
printer.alerts[existingAlertIndex].message = alert.message;
const updatedPrinter = await printer.save();
// Broadcast the update
logger.debug(`Broadcasting alert update for printer ${printerId}:`, {
alerts: updatedPrinter.alerts
});
this.socketManager.broadcast("notify_printer_update", {
_id: printerId,
alerts: updatedPrinter.alerts
});
logger.info(`Updated message for existing alert on printer ${printerId}:`, {
type: alert.type,
message: alert.message
});
return updatedPrinter;
}
logger.debug(`Alert of type ${alert.type} already exists for printer ${printerId}, skipping`);
return printer;
}
// No existing alert found, create a new one
logger.debug(`Creating new alert for printer ${printerId}:`, {
type: alert.type,
priority: alert.priority,
hasMessage: !!alert.message
});
const updatedPrinter = await printerModel.findByIdAndUpdate( const updatedPrinter = await printerModel.findByIdAndUpdate(
printerId, printerId,
{ $push: { alerts: alert } }, { $push: { alerts: alert } },
{ new: true } { new: true }
); );
if (!updatedPrinter) { logger.info(`Added new alert to printer ${printerId}:`, {
logger.error(`Printer with ID ${printerId} not found when adding alert`);
return null;
}
logger.info(`Added alert to printer ${printerId}:`, {
type: alert.type, type: alert.type,
priority: alert.priority priority: alert.priority,
hasMessage: !!alert.message
}); });
// Broadcast the alert through websocket // Broadcast the alert through websocket
logger.debug(`Broadcasting new alert for printer ${printerId}:`, {
alerts: updatedPrinter.alerts
});
this.socketManager.broadcast("notify_printer_update", { this.socketManager.broadcast("notify_printer_update", {
id: printerId, _id: printerId,
alerts: updatedPrinter.alerts alerts: updatedPrinter.alerts
}); });
@ -475,8 +607,12 @@ logger.warn(`Updating subjob state for ${subJobId}:`, state);
}); });
// Broadcast the alert clear through websocket // Broadcast the alert clear through websocket
logger.debug(`Broadcasting alert removal for printer ${printerId}:`, {
options,
remainingAlerts: updatedPrinter.alerts.length
});
this.socketManager.broadcast("notify_printer_update", { this.socketManager.broadcast("notify_printer_update", {
id: printerId, _id: printerId,
alerts: updatedPrinter.alerts alerts: updatedPrinter.alerts
}); });
@ -518,6 +654,37 @@ logger.warn(`Updating subjob state for ${subJobId}:`, state);
} }
} }
async clearAlerts(printerId) {
try {
logger.debug(`Clearing all alerts for printer ${printerId}`);
const updatedPrinter = await printerModel.findByIdAndUpdate(
printerId,
{ $set: { alerts: [] } },
{ new: true }
);
if (!updatedPrinter) {
logger.error(`Printer with ID ${printerId} not found when clearing alerts`);
return null;
}
logger.info(`Cleared all alerts for printer ${printerId}`);
// Broadcast the alert clear through websocket
logger.debug(`Broadcasting all alerts clear for printer ${printerId}`);
this.socketManager.broadcast("notify_printer_update", {
_id: printerId,
alerts: []
});
return updatedPrinter;
} catch (error) {
logger.error(`Failed to clear alerts for printer ${printerId}:`, error);
throw error;
}
}
async setCurrentFilamentStock(printerId, filamentStockId) { async setCurrentFilamentStock(printerId, filamentStockId) {
try { try {
logger.debug(`Setting current filament stock for printer ${printerId}:`, { filamentStockId }); logger.debug(`Setting current filament stock for printer ${printerId}:`, { filamentStockId });
@ -541,8 +708,11 @@ logger.warn(`Updating subjob state for ${subJobId}:`, state);
}); });
// Broadcast the update through websocket // Broadcast the update through websocket
logger.debug(`Broadcasting filament stock update for printer ${printerId}:`, {
filamentStock: updatedPrinter.currentFilamentStock
});
this.socketManager.broadcast("notify_printer_update", { this.socketManager.broadcast("notify_printer_update", {
id: printerId, _id: printerId,
currentFilamentStock: updatedPrinter.currentFilamentStock currentFilamentStock: updatedPrinter.currentFilamentStock
}); });
@ -555,73 +725,112 @@ logger.warn(`Updating subjob state for ${subJobId}:`, state);
async updateFilamentStockWeight(filamentStockId, weight, subJobId = null, jobId = null) { async updateFilamentStockWeight(filamentStockId, weight, subJobId = null, jobId = null) {
try { try {
logger.debug(`Updating filament stock weight for ${filamentStockId}:`, { weight, subJobId, jobId }); // Get or fetch filament stock
if (!this.filamentStock || this.filamentStock._id.toString() !== filamentStockId) {
const filamentStock = await filamentStockModel.findById(filamentStockId); this.filamentStock = await filamentStockModel.findById(filamentStockId).populate('stockEvents');
if (!filamentStock) { if (!this.filamentStock) {
logger.error(`Filament stock with ID ${filamentStockId} not found`); logger.error(`Filament stock with ID ${filamentStockId} not found`);
return null; return null;
} }
}
// Calculate new weights immediately
const totalEventWeight = this.filamentStock.stockEvents.reduce((sum, event) => {
// Skip the existing event if it exists
if (this.existingEvent && event._id.toString() === this.existingEvent._id.toString()) {
return sum;
}
return sum + event.value;
}, 0) + weight;
const newNetWeight = totalEventWeight;
const newGrossWeight = totalEventWeight + (this.filamentStock.startingGrossWeight - this.filamentStock.startingNetWeight);
const remainingPercent = newNetWeight / this.filamentStock.startingNetWeight;
const state = {
type: 'partiallyconsumed',
percent: (1 - remainingPercent).toFixed(2)
};
// Broadcast both updates with calculated values
logger.debug(`Broadcasting filament stock weight update for ${filamentStockId}:`, {
newNetWeight,
newGrossWeight,
state
});
this.socketManager.broadcast("notify_filamentstock_update", {
_id: filamentStockId,
currentNetWeight: newNetWeight,
currentGrossWeight: newGrossWeight,
updatedAt: new Date(),
state
});
if (this.existingEvent) {
logger.debug(`Broadcasting stock event update for ${filamentStockId}:`, {
_id: this.existingEvent._id,
value: weight,
subJob: subJobId,
job: jobId
})
this.socketManager.broadcast("notify_stockevent_update", {
_id: this.existingEvent._id,
value: weight,
updatedAt: new Date()
});
}
return this.debouncer.debounce('filament_stock', async () => {
try {
// Check if a stock event already exists for this subJobId and jobId // Check if a stock event already exists for this subJobId and jobId
const existingEventIndex = filamentStock.stockEvents.findIndex( this.existingEvent = await stockEventModel.findOne({
event => event.subJob?.toString() === subJobId.toString() && filamentStock: filamentStockId,
event.job?.toString() === jobId.toString() subJob: subJobId,
); job: jobId
});
let updatedFilamentStock; let stockEvent;
if (existingEventIndex !== -1) { if (this.existingEvent) {
// Update existing event // Update existing event
logger.debug(`Updating existing stock event for subJobId ${subJobId} and jobId ${jobId}`); logger.debug(`Updating existing stock event for subJobId ${subJobId} and jobId ${jobId}`);
updatedFilamentStock = await filamentStockModel.findOneAndUpdate( stockEvent = await stockEventModel.findByIdAndUpdate(
this.existingEvent._id,
{ {
_id: filamentStockId, value: weight,
'stockEvents.subJob': subJobId, updatedAt: new Date()
'stockEvents.job': jobId
},
{
$set: {
'stockEvents.$.value': weight,
'stockEvents.$.timestamp': new Date()
}
}, },
{ new: true } { new: true }
); );
} else { } else {
// Create new stock event // Create new stock event
logger.debug(`Creating new stock event for subJobId ${subJobId} and jobId ${jobId}`); logger.debug(`Creating new stock event for subJobId ${subJobId} and jobId ${jobId}`);
const stockEvent = { stockEvent = await stockEventModel.create({
type: 'subJob', type: 'subJob',
value: weight, value: weight,
subJob: subJobId, subJob: subJobId,
job: jobId, job: jobId,
timestamp: new Date() filamentStock: filamentStockId,
}; updatedAt: new Date(),
createdAt: new Date()
});
updatedFilamentStock = await filamentStockModel.findByIdAndUpdate( // Add the new stock event to the filament stock
await filamentStockModel.findByIdAndUpdate(
filamentStockId, filamentStockId,
{ { $push: { stockEvents: stockEvent._id } }
$push: { stockEvents: stockEvent }
},
{ new: true }
); );
} }
if (!updatedFilamentStock) { const updatedFilamentStock = await filamentStockModel.findByIdAndUpdate(
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, filamentStockId,
{ currentNetWeight: newNetWeight, currentGrossWeight: newGrossWeight }, {
currentNetWeight: newNetWeight,
currentGrossWeight: newGrossWeight,
state
},
{ {
new: true, new: true,
populate: { populate: {
@ -640,41 +849,41 @@ logger.warn(`Updating subjob state for ${subJobId}:`, state);
} }
); );
// Calculate remaining percentage and update state // Update the cached filament stock
const remainingPercent = newNetWeight / finalFilamentStock.startingNetWeight; this.filamentStock = updatedFilamentStock;
const state = {
type: 'partiallyconsumed',
percent: (1 - remainingPercent).toFixed(2)
};
const filamentStockWithState = await filamentStockModel.findByIdAndUpdate(
filamentStockId,
{ state },
{ new: true }
);
logger.info(`Updated filament stock ${filamentStockId}:`, { logger.info(`Updated filament stock ${filamentStockId}:`, {
newGrossWeight: newGrossWeight, newGrossWeight: newGrossWeight,
newNetWeight: newNetWeight, newNetWeight: newNetWeight,
eventCount: finalFilamentStock.stockEvents.length, eventCount: updatedFilamentStock.stockEvents.length,
updatedExistingEvent: existingEventIndex !== -1, updatedExistingEvent: !!this.existingEvent,
remainingPercent: remainingPercent.toFixed(2), remainingPercent: remainingPercent.toFixed(2),
consumedPercent: state.percent, consumedPercent: state.percent,
}); });
// Broadcast the update through websocket // Broadcast the final state after database update
logger.debug(`Broadcasting final filament stock update for ${filamentStockId}:`, {
newNetWeight,
newGrossWeight,
eventCount: updatedFilamentStock.stockEvents.length,
state
});
this.socketManager.broadcast("notify_filamentstock_update", { this.socketManager.broadcast("notify_filamentstock_update", {
id: filamentStockId, _id: filamentStockId,
currentNetWeight: newNetWeight, currentNetWeight: newNetWeight,
currentGrossWeight: newGrossWeight, currentGrossWeight: newGrossWeight,
stockEvents: finalFilamentStock.stockEvents,
state state
}); });
return filamentStockWithState; return updatedFilamentStock;
} catch (error) { } catch (error) {
logger.error(`Failed to update filament stock weight for ${filamentStockId}:`, error); logger.error(`Failed to update filament stock weight for ${filamentStockId}:`, error);
throw error; throw error;
} }
});
} catch (error) {
logger.error(`Failed to initialize update for filament stock ${filamentStockId}:`, error);
throw error;
}
} }
} }

View File

@ -111,7 +111,7 @@ export class PrinterClient {
this.socket.on("open", () => { this.socket.on("open", () => {
logger.info(`Connected to Moonraker (${this.name})`); logger.info(`Connected to Moonraker (${this.name})`);
this.online = true; this.isOnline = true;
this.identifyConnection(); this.identifyConnection();
}); });
@ -121,7 +121,7 @@ export class PrinterClient {
this.socket.on("close", () => { this.socket.on("close", () => {
logger.info(`Disconnected from Moonraker (${this.name})`); logger.info(`Disconnected from Moonraker (${this.name})`);
this.online = false; this.isOnline = false;
this.state = { type: "offline" }; this.state = { type: "offline" };
this.updatePrinterState(); this.updatePrinterState();
this.connectionId = null; this.connectionId = null;
@ -175,7 +175,7 @@ export class PrinterClient {
try { try {
// Get server info // Get server info
const serverResult = await this.jsonRpc.callMethod("server.info"); const serverResult = await this.jsonRpc.callMethod("server.info");
this.online = true; this.isOnline = true;
this.klippyState = { type: serverResult.klippy_state }; this.klippyState = { type: serverResult.klippy_state };
logger.info( logger.info(
"Server:", "Server:",
@ -194,9 +194,27 @@ export class PrinterClient {
`Updated firmware version for ${this.name} to ${klippyResult.software_version}`, `Updated firmware version for ${this.name} to ${klippyResult.software_version}`,
); );
if (klippyResult.state === "error") { if (klippyResult.state === "error" && klippyResult.state_message) {
logger.error( logger.error(
`Klippy error for ${this.name}: ${klippyResult.state_message}`, `Klippy error for ${this.name}: ${klippyResult.state_message}`,
this.database.addAlert(this.id, {
type: "klippyError",
message: klippyResult.state_message,
priority: 9,
timestamp: new Date()
})
);
}
if (klippyResult.state === "startup" && klippyResult.state_message) {
logger.warn(
`Klippy startup message for ${this.name}: ${klippyResult.state_message}`,
this.database.addAlert(this.id, {
type: "klippyStartup",
message: klippyResult.state_message,
priority: 8,
timestamp: new Date()
})
); );
} }
} catch (error) { } catch (error) {
@ -219,7 +237,7 @@ export class PrinterClient {
async getPrinterState() { async getPrinterState() {
logger.info(`Getting state of (${this.name})`); logger.info(`Getting state of (${this.name})`);
if (!this.online) { if (!this.isOnline) {
logger.error( logger.error(
`Cannot send command: Not connected to Moonraker (${this.name})`, `Cannot send command: Not connected to Moonraker (${this.name})`,
); );
@ -267,9 +285,9 @@ export class PrinterClient {
Object.assign(allSubscriptions, value); Object.assign(allSubscriptions, value);
} }
logger.debug("Combined subscriptions:", allSubscriptions); logger.debug("Combined subscriptions:", Object.keys(allSubscriptions).join(", "));
if (!this.online) { if (!this.isOnline) {
logger.error( logger.error(
`Cannot send command: Not connected to Moonraker (${this.name})`, `Cannot send command: Not connected to Moonraker (${this.name})`,
); );
@ -281,6 +299,9 @@ export class PrinterClient {
objects: allSubscriptions, objects: allSubscriptions,
}); });
logger.debug(`Command sent to (${this.name})`); logger.debug(`Command sent to (${this.name})`);
logger.debug({
objects: allSubscriptions,
})
return true; return true;
} catch (error) { } catch (error) {
logger.error(`Error sending command to (${this.name}):`, error); logger.error(`Error sending command to (${this.name}):`, error);
@ -290,7 +311,7 @@ export class PrinterClient {
async sendPrinterCommand(command) { async sendPrinterCommand(command) {
logger.info(`Sending ${command.method} command to (${this.name})`); logger.info(`Sending ${command.method} command to (${this.name})`);
if (!this.online) { if (!this.isOnline) {
logger.error( logger.error(
`Cannot send command: Not connected to Moonraker (${this.name})`, `Cannot send command: Not connected to Moonraker (${this.name})`,
); );
@ -329,7 +350,7 @@ export class PrinterClient {
if (status.print_stats?.state) { if (status.print_stats?.state) {
const newState = status.print_stats.state; const newState = status.print_stats.state;
if (newState !== this.state.type) { if (newState !== this.state.type) {
logger.info(`printer ${this.name} state changed from ${this.state.type} to ${newState}`); logger.info(`Printer ${this.name} state changed from ${this.state.type} to ${newState}`);
this.state.type = newState; this.state.type = newState;
stateChanged = true; stateChanged = true;
} }
@ -345,14 +366,6 @@ export class PrinterClient {
// Calculate weight in grams // Calculate weight in grams
const filamentWeightG = filamentVolumeCm3 * this.currentFilamentStockDensity; 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) { if (this.currentSubJobId != null && this.currentJobId != null && this.currentFilamentStockId != null) {
this.database.updateFilamentStockWeight(this.currentFilamentStockId, (-1 * filamentWeightG), this.currentSubJobId, this.currentJobId); this.database.updateFilamentStockWeight(this.currentFilamentStockId, (-1 * filamentWeightG), this.currentSubJobId, this.currentJobId);
} }
@ -398,9 +411,7 @@ this.database.updateFilamentStockWeight(this.currentFilamentStockId, (-1 * filam
if (stateChanged || progressChanged) { if (stateChanged || progressChanged) {
// Update printer state first // Update printer state first
await this.database.updatePrinterState(this.id, this.state, this.online); await this.updatePrinterState()
logger.warn('State changed!')
// Set current job to null when not printing or paused // Set current job to null when not printing or paused
if (!["printing", "paused"].includes(this.state.type)) { if (!["printing", "paused"].includes(this.state.type)) {
this.currentJobId = null; this.currentJobId = null;
@ -428,7 +439,8 @@ this.database.updateFilamentStockWeight(this.currentFilamentStockId, (-1 * filam
async updatePrinterState() { async updatePrinterState() {
try { try {
await this.database.updatePrinterState(this.id, this.state, this.online); const state = this.klippyState.type !== 'ready' ? this.klippyState : this.state;
await this.database.updatePrinterState(this.id, state, this.isOnline);
} catch (error) { } catch (error) {
logger.error(`Failed to update printer state:`, error); logger.error(`Failed to update printer state:`, error);
} }
@ -504,7 +516,7 @@ this.database.updateFilamentStockWeight(this.currentFilamentStockId, (-1 * filam
if (!this.currentSubJobId) { if (!this.currentSubJobId) {
const result = await this.database.setCurrentJobForPrinting(this.id, this.queuedJobIds); const result = await this.database.setCurrentJobForPrinting(this.id, this.queuedJobIds);
if (result) { if (result) {
logger.info(`Setting first queued subjob as current for printer ${this.name}`, result); logger.info(`Setting first queued subjob as current for printer ${this.name}: `, result.currentSubJob._id);
this.currentSubJobId = result.currentSubJob._id; this.currentSubJobId = result.currentSubJob._id;
this.currentJobId = result.currentJob._id; this.currentJobId = result.currentJob._id;
await this.database.updateSubJobState(this.currentSubJobId, this.state); await this.database.updateSubJobState(this.currentSubJobId, this.state);
@ -538,10 +550,12 @@ this.database.updateFilamentStockWeight(this.currentFilamentStockId, (-1 * filam
async handleKlippyDisconnected() { async handleKlippyDisconnected() {
logger.info(`Klippy disconnected (${this.name})`); logger.info(`Klippy disconnected (${this.name})`);
this.state = { type: "offline" }; this.state = { type: "offline" };
this.klippyState = { type: 'offline'};
this.isOnline = false; this.isOnline = false;
this.isPrinting = false; this.isPrinting = false;
this.isError = false; this.isError = false;
this.isReady = false; this.isReady = false;
await this.database.clearAlerts(this.id);
await this.updatePrinterState(); await this.updatePrinterState();
await this.updatePrinterSubJobs(); await this.updatePrinterSubJobs();
} }
@ -565,7 +579,7 @@ this.database.updateFilamentStockWeight(this.currentFilamentStockId, (-1 * filam
async uploadGcodeFile(fileBlob, fileName) { async uploadGcodeFile(fileBlob, fileName) {
logger.info(`Uploading G-code file ${fileName} to ${this.name}`); logger.info(`Uploading G-code file ${fileName} to ${this.name}`);
if (!this.online) { if (!this.isOnline) {
logger.error( logger.error(
`Cannot upload file: Not connected to Moonraker (${this.name})`, `Cannot upload file: Not connected to Moonraker (${this.name})`,
); );

View File

@ -138,7 +138,7 @@ export class SocketClient {
// Merge the new subscription data with existing data // Merge the new subscription data with existing data
const mergedSubscription = { const mergedSubscription = {
...existingSubscription.objects, ...existingSubscription,
...data.objects, ...data.objects,
}; };