828 lines
22 KiB
JavaScript

import { randomUUID } from "crypto";
import log4js from "log4js";
import NodeCache from "node-cache";
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;
// Initialize cache with 30 second TTL
this.printerCache = new NodeCache({ stdTTL: 30 });
logger.info("Initialized PrinterDatabase with socket manager");
}
async getPrinter() {
const populate = [
"currentFilamentStock",
"currentJob",
"currentSubJob",
"queue",
];
const populateKey = populate.sort().join(",");
const cacheKey = `printer_${this.printer._id}_${populateKey}`;
// Check if printer is in cache
const cachedPrinter = this.printerCache.get(cacheKey);
if (cachedPrinter) {
logger.debug(`Returning cached printer for ${this.id}`);
this.printer = cachedPrinter;
return cachedPrinter;
}
// Fetch from socketClient if not in cache
const object = await this.socketClient.getObject({
_id: this.printer._id,
objectType: "printer",
populate: populate,
});
this.printer = object;
// Store in cache
this.printerCache.set(cacheKey, object);
logger.debug(`Cached printer for ${this.id}`);
return object;
}
async editPrinter(updateData) {
sendIPC("setPrinter", this.printer);
const populate = [
"currentFilamentStock",
"currentJob",
"currentSubJob",
"queue",
];
const object = await this.socketClient.editObject({
_id: this.printer._id,
objectType: "printer",
updateData: updateData,
populate: populate,
});
// Update cache with the edited printer (overwrite existing cache)
const populateKey = populate.sort().join(",");
const cacheKey = `printer_${this.printer._id}_${populateKey}`;
this.printerCache.del(cacheKey); // Delete existing cache entry
this.printerCache.set(cacheKey, { ...object, ...updateData }); // Set new cache entry
this.printer = { ...object, ...updateData };
logger.debug(`Updated printer cache for ${this.id}`);
return object;
}
async setPrinterConnectedAt() {
const updatedPrinter = await this.editPrinter({
connectedAt: new Date(),
});
this.printer = updatedPrinter;
return updatedPrinter;
}
async removePrinterConnectedAt() {
const updatedPrinter = await this.editPrinter({
connectedAt: null,
});
this.printer = updatedPrinter;
return updatedPrinter;
}
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, connectedAt) {
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;
this.printer.connectedAt = connectedAt;
const updatedPrinter = await this.editPrinter({
state,
online,
connectedAt,
});
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 updateSubJobState(subJobId, state) {
try {
const updatedSubJob = await this.socketClient.editObject({
_id: subJobId,
objectType: "subJob",
updateData: { state },
});
logger.info(`Updated subjob ${subJobId} state:`, {
type: state.type,
progress: state.progress,
});
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 subJobs = await this.socketClient.listObjects({
objectType: "subJob",
filter: { job: { _id: jobId } },
cached: false,
});
const subJobStates = subJobs.map((subJob) => subJob.state);
const stateCounts = {
printing: 0,
paused: 0,
complete: 0,
failed: 0,
queued: 0,
cancelled: 0,
deploying: 0,
};
let jobProgress = 0;
subJobStates.forEach((state) => {
stateCounts[state.type]++;
var subJobProgress = state.progress || 0;
if (state.type === "complete") {
subJobProgress = 1;
}
jobProgress += subJobProgress;
});
logger.debug(`Job ${jobId} state counts:`, stateCounts);
const jobState = {
type: this.determineJobState(stateCounts),
progress: jobProgress / subJobs.length,
};
logger.debug(`Calculated job state for ${jobId}:`, {
type: jobState.type,
progress: jobState.progress,
subJobCount: subJobs.length,
});
if (jobState.type == "complete") {
await this.setJobFinishedAt(jobId, new Date());
}
await this.socketClient.editObject({
_id: jobId,
objectType: "job",
updateData: { state: jobState },
});
logger.info(`Updated job ${jobId} state:`, {
_id: jobId,
state: jobState,
subJobStats: stateCounts,
});
return jobState;
} catch (error) {
logger.error(`Failed to update job state for ${jobId}:`, error);
throw error;
}
}
async setSubJobMoonrakerJobId(subJobId, moonrakerJobId) {
try {
logger.debug(`Setting subjob ${subJobId} moonraker job id:`, {
moonrakerJobId,
});
const updatedSubJob = await this.socketClient.editObject({
_id: subJobId,
objectType: "subJob",
updateData: { moonrakerJobId: moonrakerJobId },
});
logger.info(`Set subjob ${subJobId} moonraker job id:`, {
moonrakerJobId,
});
return updatedSubJob;
} catch (error) {
logger.error(`Failed to set subjob ${subJobId} moonraker job id:`, 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.deploying > 0) {
logger.debug(
"Job state determined as 'deploying' due to active deploying subjobs",
);
return "deploying";
}
// 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 setCurrentJobAndSubJob(subJob, job) {
try {
logger.debug(`Setting current job and subjob for printer ${this.id}:`, {
subJobId: subJob?._id || null,
jobId: job?._id || null,
});
const updatedPrinter = await this.editPrinter({
currentSubJob: subJob,
currentJob: job,
});
return updatedPrinter;
} catch (error) {
logger.error(
`Failed to set current job and subjob for 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.editPrinter({ firmware: firmwareVersion });
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 {
const alertWithId = {
...alert,
_id: alert._id ?? randomUUID(),
};
logger.debug(`Adding alert to printer ${this.id}:`, alertWithId);
const printer = await this.getPrinter();
const updatedAlerts = [...printer.alerts, alertWithId];
const updatedPrinter = await this.editPrinter({ alerts: updatedAlerts });
logger.info(`Added new alert to printer ${this.id}:`, {
type: alert.type,
code: alert.code,
priority: alert.priority,
hasMessage: !!alert.message,
});
return updatedPrinter;
} catch (error) {
logger.error(`Failed to add alert to printer ${this.id}:`, error);
throw error;
}
}
async removeAlertById(alertId) {
try {
logger.debug(`Removing alert by id ${alertId} for printer ${this.id}`);
const printer = await this.getPrinter();
const filteredAlerts = printer.alerts.filter(
(alert) => alert._id !== alertId,
);
const updatedPrinter = await this.editPrinter({ alerts: filteredAlerts });
logger.info(`Removed alert ${alertId} for printer ${this.id}:`, {
alertId,
alerts: updatedPrinter.alerts,
});
return updatedPrinter;
} catch (error) {
logger.error(
`Failed to remove alert ${alertId} for printer ${this.id}:`,
error,
);
throw error;
}
}
async removeAlertsByCode(code) {
try {
logger.debug(`Removing alerts by code ${code} for printer ${this.id}`);
const printer = await this.getPrinter();
const filteredAlerts = printer.alerts.filter(
(alert) => alert.code !== code,
);
const updatedPrinter = await this.editPrinter({ alerts: filteredAlerts });
logger.info(`Removed alerts with code ${code} for printer ${this.id}:`, {
code,
alerts: updatedPrinter.alerts,
});
return updatedPrinter;
} catch (error) {
logger.error(
`Failed to remove alerts with code ${code} for printer ${this.id}:`,
error,
);
throw error;
}
}
async getAlerts(options = {}) {
try {
logger.debug(`Getting alerts for printer ${this.id}:`, options);
const printer = await this.getPrinter();
let alerts = printer.alerts;
// Filter alerts based on options
if (options.action) {
alerts = alerts.filter((alert) => alert.action === options.action);
}
// Sort alerts by priority
alerts.sort((a, b) =>
String(a.priority ?? "").localeCompare(String(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.editPrinter({ 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(filamentStock) {
try {
logger.debug(`Setting current filament stock for printer ${this.id}:`, {
filamentStock,
});
const updatedPrinter = await this.editPrinter({
currentFilamentStock: filamentStock,
});
logger.info(
`Updated current filament stock for printer ${this.id}:`,
filamentStock,
);
return updatedPrinter.currentFilamentStock;
} catch (error) {
logger.error(
`Failed to set current filament stock for printer ${this.id}:`,
error,
);
throw error;
}
}
async updateFilamentStockWeight(
filamentStock,
weight,
subJob = null,
job = null,
) {
const subJobId = subJob._id || subJob.id;
const jobId = job._id || job.id;
try {
const filamentStockStockEvents = await this.socketClient.listObjects({
objectType: "stockEvent",
filter: {
"parent._id": filamentStock._id,
parentType: "filamentStock",
},
populate: ["owner"],
});
const stockEvents = filamentStockStockEvents.filter(
(event) =>
event.owner._id.toString() === subJobId.toString() &&
event.ownerType === "subJob",
);
let stockEvent;
if (stockEvents && stockEvents.length > 0) {
const existingEvent = stockEvents[0];
logger.trace(
`Updating existing stock event for subJobId ${subJobId} and jobId ${jobId}`,
);
stockEvent = await this.socketClient.editObject({
_id: existingEvent._id,
objectType: "stockEvent",
updateData: {
value: weight,
updatedAt: new Date(),
},
auditLog: false,
});
} else {
logger.debug(
`Creating new stock event for subJobId ${subJobId} and jobId ${jobId}`,
);
stockEvent = await this.socketClient.newObject({
objectType: "stockEvent",
newData: {
filamentStock: filamentStock._id,
value: weight,
unit: "g",
parentType: "filamentStock",
parent: { _id: filamentStock._id },
owner: this.printer.currentSubJob,
ownerType: "subJob",
},
});
}
logger.trace(
`Updated stock event for filament stock ${filamentStock._id}:`,
{
value: weight,
updatedExistingEvent: stockEvents.length > 0,
},
);
return stockEvent;
} catch (error) {
logger.error(
`Failed to update stock event for filament stock ${filamentStock._id}:`,
error,
);
throw error;
}
}
async queueSubJob(subJob, moonrakerJobId) {
try {
logger.info(
`Queueing subjob ${subJob._id} for printer ${this.id} with Moonraker ID ${moonrakerJobId}`,
);
// 1. Update SubJob with Moonraker ID and State
await this.socketClient.editObject({
_id: subJob._id,
objectType: "subJob",
updateData: {
moonrakerJobId: moonrakerJobId,
state: { type: "queued" },
},
});
// 2. Add to Printer's Queue
const newQueue = [...this.printer.queue, subJob];
const updatedPrinter = await this.editPrinter({
queue: newQueue,
});
this.printer.queue = newQueue;
return updatedPrinter;
} catch (error) {
logger.error(`Failed to queue subjob for printer ${this.id}:`, error);
throw error;
}
}
async setQueuedSubJobs(subJobs) {
try {
logger.debug(
`Setting queued subjobs for printer ${this.id}:`,
subJobs.map((subJob) => subJob._id).join(", "),
);
const updatedPrinter = await this.editPrinter({
queue: subJobs,
});
return updatedPrinter;
} catch (error) {
logger.error(
`Failed to set queued subjobs for printer ${this.id}:`,
error,
);
throw error;
}
}
async getQueuedSubJobs() {
try {
logger.debug(`Getting queued subjobs for printer ${this.id}`);
const printer = await this.getPrinter();
return printer.queue;
} catch (error) {
logger.error(
`Failed to get queued subjobs for printer ${this.id}:`,
error,
);
throw error;
}
}
async getJobById(jobId) {
try {
logger.debug(`Getting job by ID ${jobId} for printer ${this.id}`);
const job = await this.socketClient.getObject({
objectType: "job",
_id: jobId,
});
return job;
} catch (error) {
logger.error(
`Failed to get job by ID ${jobId} for printer ${this.id}:`,
error,
);
throw error;
}
}
async postSubJobPartStockItems(subJobId) {
try {
logger.info(
`Posting subjob part stock items for subjob ${subJobId} for printer ${this.id}`,
);
logger.debug(`Getting subjob ${subJobId} for printer ${this.id}`);
const subJob = await this.socketClient.getObject({
objectType: "subJob",
_id: subJobId,
});
logger.debug(`Subjob ${subJobId} for printer ${this.id}:`, subJob);
logger.debug(
`Getting gcode file for subjob ${subJobId} for printer ${this.id}`,
);
const gcodeFile = await this.socketClient.getObject({
objectType: "gcodeFile",
_id: subJob.gcodeFile._id,
populate: ["parts.part", "parts.partSku"],
});
const partItems = gcodeFile.parts;
for (const partItem of partItems) {
const partSkuId = partItem.partSku?._id || partItem.partSku;
logger.info(
`Posting part stock item for part SKU ${
partItem.partSku?.name || partSkuId
} for subjob ${subJobId} for printer ${this.id}`,
);
const quantity = partItem.quantity;
const partStock = await this.socketClient.newObject({
objectType: "partStock",
newData: {
sourceType: "subJob",
source: subJobId,
partSku: partSkuId,
currentQuantity: quantity,
state: {
type: "new",
progress: 1,
},
},
});
logger.info(
`Creating stock event for part stock ${partStock._id} with quantity ${quantity} for subjob ${subJobId} for printer ${this.id}`,
);
await this.socketClient.newObject({
objectType: "stockEvent",
newData: {
value: quantity,
unit: "",
parentType: "partStock",
parent: partStock._id,
owner: subJobId,
ownerType: "subJob",
},
});
}
} catch (error) {
logger.error(
`Failed to post subjob part stock items for subjob ${subJobId} for printer ${this.id}:`,
error,
);
throw error;
}
}
async setSubJobStartedAt(subJobId, date) {
try {
logger.debug(
`Setting started at for subjob ${subJobId} for printer ${this.id}`,
);
const updatedSubJob = await this.socketClient.editObject({
_id: subJobId,
objectType: "subJob",
updateData: { startedAt: date },
});
return updatedSubJob;
} catch (error) {
logger.error(
`Failed to set started at for subjob ${subJobId} for printer ${this.id}:`,
error,
);
throw error;
}
}
async setSubJobFinishedAt(subJobId, date) {
try {
logger.debug(
`Setting finished at for subjob ${subJobId} for printer ${this.id}`,
);
const updatedSubJob = await this.socketClient.editObject({
_id: subJobId,
objectType: "subJob",
updateData: { finishedAt: date },
});
return updatedSubJob;
} catch (error) {
logger.error(
`Failed to set finished at for subjob ${subJobId} for printer ${this.id}:`,
error,
);
throw error;
}
}
async setJobStartedAt(jobId, date) {
try {
logger.debug(
`Setting started at for job ${jobId} for printer ${this.id}`,
);
const updatedJob = await this.socketClient.editObject({
_id: jobId,
objectType: "job",
updateData: { startedAt: date },
});
return updatedJob;
} catch (error) {
logger.error(
`Failed to set started at for job ${jobId} for printer ${this.id}:`,
error,
);
throw error;
}
}
async setJobFinishedAt(jobId, date) {
try {
logger.debug(
`Setting finished at for job ${jobId} for printer ${this.id}`,
);
const updatedJob = await this.socketClient.editObject({
_id: jobId,
objectType: "job",
updateData: { finishedAt: date },
});
return updatedJob;
} catch (error) {
logger.error(
`Failed to set finished at for job ${jobId} for printer ${this.id}:`,
error,
);
throw error;
}
}
}