984 lines
27 KiB
JavaScript

import log4js from "log4js";
import { loadConfig } from "../config.js";
import { sendIPC } from "../electron/ipc.js";
const config = loadConfig();
const logger = log4js.getLogger("Printer Database");
logger.level = config.logLevel;
export class PrinterDatabase {
constructor(socketClient, printer) {
this.socketClient = socketClient;
this.printer = printer;
this.id = this.printer._id;
this.filamentStock = null; // Store current filament stock
this.existingEvent = null; // Store existing stock event
logger.info("Initialized PrinterDatabase with socket manager");
}
async getPrinter() {
const object = await this.socketClient.getObject({
_id: this.printer._id,
objectType: "printer",
populate: ["subJobs"],
});
return object;
}
async updatePrinter() {
sendIPC("setPrinter", this.printer);
const object = await this.socketClient.editObject({
_id: this.printer._id,
objectType: "printer",
updateData: this.printer,
});
return object;
}
async getPrinterConfig() {
try {
logger.debug(`Getting printer config for ${this.id}`);
const printers = await this.socketClient.listObjects({
objectType: "printer",
filter: { _id: this.id },
populate: ["moonraker"],
});
if (!printers || printers.length === 0) {
logger.error(
`Printer with ID ${this.id} not found when getting config`
);
return null;
}
const printer = printers[0];
logger.debug(
`Retrieved printer config for ${this.id}:`,
printer.moonraker
);
return printer.moonraker;
} catch (error) {
logger.error(`Failed to get printer config for ${this.id}:`, error);
throw error;
}
}
async updatePrinterState(state, online) {
try {
logger.debug(`Updating printer state for ${this.printer.name}:`, {
state,
online,
});
if (state.type === "printing" && state.progress === undefined) {
logger.debug(
`Setting default progress for printing state on printer ${this.printer.name}`
);
state.progress = 0;
}
this.printer.state = state;
this.printer.online = online;
const updatedPrinter = await this.updatePrinter();
logger.info(`Updated printer ${this.printer.name} state:`, {
type: state.type,
progress: state.progress,
online,
previousState: updatedPrinter.state,
});
return updatedPrinter;
} catch (error) {
logger.error(
`Failed to update printer state for ${this.printer.name}:`,
error
);
throw error;
}
}
async clearCurrentJob() {
try {
const updatedPrinter = await this.socketClient.editObject({
_id: this.id,
objectType: "printer",
updateData: {
currentSubJob: null,
currentJob: null,
},
});
if (!updatedPrinter) {
logger.error(
`Printer with ID ${this.id} not found when clearing current job`
);
return null;
}
logger.info(`Cleared current job for printer ${this.id}`);
return {
currentSubJob: null,
currentJob: null,
};
} catch (error) {
logger.error(
`Failed to clear current job for printer ${this.id}:`,
error
);
throw error;
}
}
async setCurrentJobForPrinting(queuedJobIds) {
try {
const printers = await this.socketClient.listObjects({
objectType: "printer",
filter: { _id: this.id },
populate: ["subJobs"],
});
if (!printers || printers.length === 0) {
logger.error(`Printer with ID ${this.id} not found`);
return null;
}
const printer = printers[0];
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 ${this.id} as printing starts`,
{
subJobId: subJob.id,
jobId: subJob.job,
}
);
const now = new Date();
// Update printer with current job and startedAt
const oldCurrentJob = printer.currentJob;
const oldCurrentSubJob = printer.currentSubJob;
const updatedPrinter = await this.socketClient.editObject({
_id: this.id,
objectType: "printer",
updateData: {
currentSubJob: subJob.id,
currentJob: subJob.job,
startedAt: now,
},
});
if (!updatedPrinter) {
logger.error(
`Printer with ID ${this.id} not found when setting job and subjob`
);
return null;
}
// Update subjob with startedAt
const oldStartedAt = subJob.startedAt;
await this.socketClient.editObject({
_id: subJob.id,
objectType: "subJob",
updateData: { startedAt: now },
});
// Get the full job object and update its startedAt if null
const jobs = await this.socketClient.listObjects({
objectType: "job",
filter: { _id: subJob.job },
populate: ["gcodeFile"],
});
if (!jobs || jobs.length === 0) {
logger.error(`Job with ID ${subJob.job} not found`);
return null;
}
const job = jobs[0];
const oldJobStartedAt = job.startedAt;
if (!job.startedAt) {
await this.socketClient.editObject({
_id: subJob.job,
objectType: "job",
updateData: { startedAt: now },
});
job.startedAt = now;
}
logger.info(`Set current job for printer ${this.id}:`, {
subJobId: subJob.id,
jobId: subJob.job,
});
return {
currentSubJob: subJob,
currentJob: job,
};
}
}
return null;
} catch (error) {
logger.error(`Failed to set current job for printer ${this.id}:`, 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);
}
// Set startedAt if the subjob is starting to print
const updateData = { state };
if (state.type === "printing") {
const subJobs = await this.socketClient.listObjects({
objectType: "subJob",
filter: { _id: subJobId },
});
if (subJobs && subJobs.length > 0) {
const subJob = subJobs[0];
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 subJobs = await this.socketClient.listObjects({
objectType: "subJob",
filter: { _id: subJobId },
});
if (subJobs && subJobs.length > 0) {
const subJob = subJobs[0];
if (!subJob.finishedAt) {
updateData.finishedAt = new Date();
logger.info(
`Setting finishedAt for subjob ${subJobId} as state is ${state.type}`
);
}
}
}
const subJobs = await this.socketClient.listObjects({
objectType: "subJob",
filter: { _id: subJobId },
});
if (!subJobs || subJobs.length === 0) {
logger.error(`Sub job with ID ${subJobId} not found`);
return;
}
const subJob = subJobs[0];
const oldState = subJob.state;
const updatedSubJob = await this.socketClient.editObject({
_id: subJobId,
objectType: "subJob",
updateData,
});
logger.info(`Updated subjob ${subJobId} state:`, {
type: state.type,
progress: state.progress,
previousState: updatedSubJob.state,
job: updatedSubJob.job,
startedAt: updatedSubJob.startedAt,
finishedAt: updatedSubJob.finishedAt,
});
// Update parent job state
await this.updateJobState(updatedSubJob.job, subJob.printer);
return updatedSubJob;
} catch (error) {
logger.error(`Failed to update sub job state for ${subJobId}:`, error);
throw error;
}
}
async updateJobState(jobId, printerId) {
try {
logger.debug(`Updating job state for ${jobId}`);
const jobs = await this.socketClient.listObjects({
objectType: "job",
filter: { _id: jobId },
populate: ["subJobs"],
});
if (!jobs || jobs.length === 0) {
logger.error(`Job with ID ${jobId} not found`);
return;
}
const job = jobs[0];
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,
});
var oldAuditLogValues = {};
var newAuditLogValues = {};
// Update finishedAt if all subjobs are complete and none are queued
const oldFinishedAt = job.finishedAt;
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();
oldAuditLogValues = {
...oldAuditLogValues,
finishedAt: oldFinishedAt,
};
newAuditLogValues = {
...newAuditLogValues,
finishedAt: job.finishedAt,
};
}
const oldState = job.state;
if (oldState.type !== jobState.type) {
oldAuditLogValues = { ...oldAuditLogValues, state: oldState };
newAuditLogValues = { ...newAuditLogValues, state: jobState };
}
const updateData = {
state: jobState,
subJobStats: stateCounts,
};
if (job.finishedAt) {
updateData.finishedAt = job.finishedAt;
}
const updatedJob = await this.socketClient.editObject({
_id: jobId,
objectType: "job",
updateData,
});
logger.info(`Updated job ${jobId} state:`, {
_id: jobId,
state: jobState,
subJobStats: stateCounts,
finishedAt: updatedJob.finishedAt,
});
return updatedJob;
} 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(subJobId) {
try {
logger.debug(`Adding subjob ${subJobId} to printer ${this.id}`);
const printers = await this.socketClient.listObjects({
objectType: "printer",
filter: { _id: this.id },
});
if (!printers || printers.length === 0) {
logger.error(`Printer with ID ${this.id} not found`);
return null;
}
const printer = printers[0];
const updatedSubJobs = [...printer.subJobs, subJobId];
const updatedPrinter = await this.socketClient.editObject({
_id: this.id,
objectType: "printer",
updateData: { subJobs: updatedSubJobs },
});
logger.info(`Added subjob ${subJobId} to printer ${this.id}`, {
currentSubJobs: updatedPrinter.subJobs.length,
printerState: updatedPrinter.state,
});
return updatedPrinter;
} catch (error) {
logger.error(
`Failed to add subjob ${subJobId} to printer ${this.id}:`,
error
);
throw error;
}
}
async removePrinterSubJob(subJobId) {
try {
logger.debug(`Removing subjob ${subJobId} from printer ${this.id}`);
const printers = await this.socketClient.listObjects({
objectType: "printer",
filter: { _id: this.id },
});
if (!printers || printers.length === 0) {
logger.error(`Printer with ID ${this.id} not found`);
return;
}
const printer = printers[0];
const updatedSubJobs = printer.subJobs.filter(
(id) => id.toString() !== subJobId.toString()
);
const updatedPrinter = await this.socketClient.editObject({
_id: this.id,
objectType: "printer",
updateData: { subJobs: updatedSubJobs },
});
logger.info(`Removed subjob ${subJobId} from printer ${this.id}`, {
remainingSubJobs: updatedPrinter.subJobs.length,
printerState: updatedPrinter.state,
});
return updatedPrinter;
} catch (error) {
logger.error(
`Failed to remove subjob ${subJobId} from printer ${this.id}:`,
error
);
throw error;
}
}
async updateDisplayStatus(message) {
try {
logger.debug(`Updating display status for printer ${this.id}:`, {
message,
});
logger.info(`Updated display status for printer ${this.id}:`, {
message,
});
} catch (error) {
logger.error(
`Failed to update display status for printer ${this.id}:`,
error
);
throw error;
}
}
async updatePrinterFirmware(firmwareVersion) {
try {
logger.debug(
`Updating firmware version for printer ${this.id}:`,
firmwareVersion
);
this.printer.firmware = firmwareVersion;
await this.updatePrinter();
logger.info(
`Updated firmware version for printer ${this.id}:`,
firmwareVersion
);
} catch (error) {
logger.error(
`Failed to update firmware version for printer ${this.id}:`,
error
);
throw error;
}
}
async addAlert(alert) {
try {
logger.debug(`Adding alert to printer ${this.id}:`, alert);
const printers = await this.socketClient.listObjects({
objectType: "printer",
filter: { _id: this.id },
});
if (!printers || printers.length === 0) {
logger.error(`Printer with ID ${this.id} not found`);
return null;
}
const printer = printers[0];
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 ${this.id}`
);
const updatedAlerts = [...printer.alerts];
updatedAlerts[existingAlertIndex].message = alert.message;
const updatedPrinter = await this.socketClient.editObject({
_id: this.id,
objectType: "printer",
updateData: { alerts: updatedAlerts },
});
logger.info(
`Updated message for existing alert on printer ${this.id}:`,
{
type: alert.type,
message: alert.message,
}
);
return updatedPrinter;
}
logger.debug(
`Alert of type ${alert.type} already exists for printer ${this.id}, skipping`
);
return printer;
}
// No existing alert found, create a new one
logger.debug(`Creating new alert for printer ${this.id}:`, {
type: alert.type,
priority: alert.priority,
hasMessage: !!alert.message,
});
const updatedAlerts = [...printer.alerts, alert];
const updatedPrinter = await this.socketClient.editObject({
_id: this.id,
objectType: "printer",
updateData: { alerts: updatedAlerts },
});
logger.info(`Added new alert to printer ${this.id}:`, {
type: alert.type,
priority: alert.priority,
hasMessage: !!alert.message,
});
return updatedPrinter;
} catch (error) {
logger.error(`Failed to add alert to printer ${this.id}:`, error);
throw error;
}
}
async removeAlerts(options = {}) {
try {
logger.debug(`Clearing alerts for printer ${this.id}:`, options);
const printers = await this.socketClient.listObjects({
objectType: "printer",
filter: { _id: this.id },
});
if (!printers || printers.length === 0) {
logger.error(
`Printer with ID ${this.id} not found when clearing alerts`
);
return null;
}
const printer = printers[0];
let filteredAlerts = printer.alerts;
if (options.type) {
filteredAlerts = filteredAlerts.filter(
(alert) => alert.type !== options.type
);
}
if (options.priority) {
filteredAlerts = filteredAlerts.filter(
(alert) => alert.priority !== options.priority
);
}
const updatedPrinter = await this.socketClient.editObject({
_id: this.id,
objectType: "printer",
updateData: { alerts: filteredAlerts },
});
logger.info(`Cleared alerts for printer ${this.id}:`, {
options,
remainingAlerts: updatedPrinter.alerts.length,
});
return updatedPrinter;
} catch (error) {
logger.error(`Failed to clear alerts for printer ${this.id}:`, error);
throw error;
}
}
async getAlerts(options = {}) {
try {
logger.debug(`Getting alerts for printer ${this.id}:`, options);
const printers = await this.socketClient.listObjects({
objectType: "printer",
filter: { _id: this.id },
});
if (!printers || printers.length === 0) {
logger.error(
`Printer with ID ${this.id} not found when getting alerts`
);
return null;
}
const printer = printers[0];
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 ${this.id}`);
return alerts;
} catch (error) {
logger.error(`Failed to get alerts for printer ${this.id}:`, error);
throw error;
}
}
async clearAlerts() {
try {
logger.debug(`Clearing all alerts for printer ${this.id}`);
const updatedPrinter = await this.socketClient.editObject({
_id: this.id,
objectType: "printer",
updateData: { alerts: [] },
});
if (!updatedPrinter) {
logger.error(
`Printer with ID ${this.id} not found when clearing alerts`
);
return null;
}
logger.info(`Cleared all alerts for printer ${this.id}`);
return updatedPrinter;
} catch (error) {
logger.error(`Failed to clear alerts for printer ${this.id}:`, error);
throw error;
}
}
async setCurrentFilamentStock(filamentStockId) {
try {
logger.debug(`Setting current filament stock for printer ${this.id}:`, {
filamentStockId,
});
const printers = await this.socketClient.listObjects({
objectType: "printer",
filter: { _id: this.id },
populate: [
{
path: "currentFilamentStock",
populate: {
path: "filament",
},
},
],
});
if (!printers || printers.length === 0) {
logger.error(
`Printer with ID ${this.id} not found when setting current filament stock`
);
return null;
}
const printer = printers[0];
const oldFilamentStock = printer.currentFilamentStock;
const updatedPrinter = await this.socketClient.editObject({
_id: this.id,
objectType: "printer",
updateData: { currentFilamentStock: filamentStockId },
populate: [
{
path: "currentFilamentStock",
populate: {
path: "filament",
},
},
],
});
if (!updatedPrinter) {
logger.error(
`Printer with ID ${this.id} not found when setting current filament stock`
);
return null;
}
logger.info(`Updated current filament stock for printer ${this.id}:`, {
filamentStock: updatedPrinter.currentFilamentStock,
});
return updatedPrinter.currentFilamentStock;
} catch (error) {
logger.error(
`Failed to set current filament stock for printer ${this.id}:`,
error
);
throw error;
}
}
async updateFilamentStockWeight(
filamentStockId,
weight,
subJobId = null,
jobId = null
) {
try {
// Get or fetch filament stock
if (
!this.filamentStock ||
this.filamentStock._id.toString() !== filamentStockId
) {
const filamentStocks = await this.socketClient.listObjects({
objectType: "filamentStock",
filter: { _id: filamentStockId },
populate: ["stockEvents"],
});
if (!filamentStocks || filamentStocks.length === 0) {
logger.error(`Filament stock with ID ${filamentStockId} not found`);
return null;
}
this.filamentStock = filamentStocks[0];
}
// 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: newNetWeight <= 0 ? "fullyconsumed" : "partiallyconsumed",
percent: (1 - remainingPercent).toFixed(2),
};
// Check if a stock event already exists for this subJobId and jobId
const stockEvents = await this.socketClient.listObjects({
objectType: "stockEvent",
filter: {
filamentStock: filamentStockId,
subJob: subJobId,
job: jobId,
},
});
let stockEvent;
if (stockEvents && stockEvents.length > 0) {
// Update existing event
this.existingEvent = stockEvents[0];
logger.debug(
`Updating existing stock event for subJobId ${subJobId} and jobId ${jobId}`
);
stockEvent = await this.socketClient.editObject({
_id: this.existingEvent._id,
objectType: "stockEvent",
updateData: {
value: weight,
updatedAt: new Date(),
},
});
} else {
// Create new stock event
logger.debug(
`Creating new stock event for subJobId ${subJobId} and jobId ${jobId}`
);
stockEvent = await this.socketClient.editObject({
_id: null, // This will create a new object
objectType: "stockEvent",
updateData: {
type: "subJob",
value: weight,
subJob: subJobId,
job: jobId,
filamentStock: filamentStockId,
unit: "g",
updatedAt: new Date(),
createdAt: new Date(),
},
});
// Add the new stock event to the filament stock
const updatedStockEvents = [
...this.filamentStock.stockEvents,
stockEvent._id,
];
await this.socketClient.editObject({
_id: filamentStockId,
objectType: "filamentStock",
updateData: { stockEvents: updatedStockEvents },
});
}
const oldState = this.filamentStock.state;
const updatedFilamentStock = await this.socketClient.editObject({
_id: filamentStockId,
objectType: "filamentStock",
updateData: {
currentNetWeight: newNetWeight,
currentGrossWeight: newGrossWeight,
state,
},
populate: [
{
path: "stockEvents",
populate: [
{
path: "subJob",
select: "number",
},
{
path: "job",
select: "startedAt",
},
],
},
],
});
// Update the cached filament stock
this.filamentStock = updatedFilamentStock;
logger.info(`Updated filament stock ${filamentStockId}:`, {
newGrossWeight: newGrossWeight,
newNetWeight: newNetWeight,
eventCount: updatedFilamentStock.stockEvents.length,
updatedExistingEvent: !!this.existingEvent,
remainingPercent: remainingPercent.toFixed(2),
consumedPercent: state.percent,
});
return updatedFilamentStock;
} catch (error) {
logger.error(
`Failed to update filament stock weight for ${filamentStockId}:`,
error
);
throw error;
}
}
}