Updated to update the DB and handle print commands.
This commit is contained in:
parent
92906e940d
commit
81196897fa
@ -1,6 +1,6 @@
|
||||
{
|
||||
"server": {
|
||||
"port": 8080,
|
||||
"port": 8081,
|
||||
"logLevel": "debug"
|
||||
},
|
||||
"auth": {
|
||||
@ -9,7 +9,7 @@
|
||||
"url": "https://auth.tombutcher.work",
|
||||
"realm": "master",
|
||||
"clientId": "farmcontrol-client",
|
||||
"clientSecret": ""
|
||||
"clientSecret": "GPyh59xctRX83yfKWb83ShK6VEwHIvLF"
|
||||
},
|
||||
"requiredRoles": []
|
||||
},
|
||||
|
||||
@ -7,7 +7,7 @@ import { loadConfig } from "../config.js";
|
||||
|
||||
const config = loadConfig();
|
||||
|
||||
const logger = log4js.getLogger("MongoDB");
|
||||
const logger = log4js.getLogger("Auth");
|
||||
logger.level = config.server.logLevel;
|
||||
|
||||
export class KeycloakAuth {
|
||||
|
||||
@ -3,39 +3,43 @@ const { Schema } = mongoose;
|
||||
|
||||
// Define the moonraker connection schema
|
||||
const moonrakerSchema = new Schema(
|
||||
{
|
||||
host: { type: String, required: true },
|
||||
port: { type: Number, required: true },
|
||||
protocol: { type: String, required: true },
|
||||
apiKey: { type: String, default: null },
|
||||
},
|
||||
{ _id: false },
|
||||
{
|
||||
host: { type: String, required: true },
|
||||
port: { type: Number, required: true },
|
||||
protocol: { type: String, required: true },
|
||||
apiKey: { type: String, default: null, required: false },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
// Define the main printer schema
|
||||
const printerSchema = new Schema(
|
||||
{
|
||||
printerId: { type: String, required: true, unique: true },
|
||||
printerName: { type: String, required: true },
|
||||
online: { type: Boolean, required: true, default: false },
|
||||
state: {
|
||||
type: { type: String, required: true, default: "Offline" },
|
||||
percent: { type: Number, required: false },
|
||||
},
|
||||
connectedAt: { type: Date, default: null },
|
||||
loadedFillament: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Fillament",
|
||||
default: null,
|
||||
},
|
||||
moonraker: { type: moonrakerSchema, required: true },
|
||||
{
|
||||
printerName: { type: String, required: true },
|
||||
online: { type: Boolean, required: true, default: false },
|
||||
state: {
|
||||
type: { type: String, required: true, default: "Offline" },
|
||||
progress: { required: false, type: Number, default: 0 },
|
||||
},
|
||||
{ timestamps: true },
|
||||
connectedAt: { type: Date, default: null },
|
||||
loadedFilament: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Filament",
|
||||
default: null,
|
||||
},
|
||||
moonraker: { type: moonrakerSchema, required: true },
|
||||
tags: [{ type: String }],
|
||||
firmware: { type: String },
|
||||
currentJob: { type: Schema.Types.ObjectId, ref: "PrintJob" },
|
||||
currentSubJob: { type: Schema.Types.ObjectId, ref: "PrintSubJob" },
|
||||
subJobs: [{ type: Schema.Types.ObjectId, ref: "PrintSubJob" }],
|
||||
},
|
||||
{ timestamps: true },
|
||||
);
|
||||
|
||||
// Add virtual id getter
|
||||
printerSchema.virtual("id").get(function () {
|
||||
return this._id.toHexString();
|
||||
return this._id.toHexString();
|
||||
});
|
||||
|
||||
// Configure JSON serialization to include virtuals
|
||||
|
||||
35
src/database/printjob.schema.js
Normal file
35
src/database/printjob.schema.js
Normal file
@ -0,0 +1,35 @@
|
||||
import mongoose from "mongoose";
|
||||
const { Schema } = mongoose;
|
||||
|
||||
const printJobSchema = new mongoose.Schema({
|
||||
state: {
|
||||
type: { required: true, type: String },
|
||||
progress: { required: false, type: Number, default: 0 },
|
||||
},
|
||||
printers: [{ type: Schema.Types.ObjectId, ref: "Printer", required: false }],
|
||||
createdAt: { required: true, type: Date },
|
||||
updatedAt: { required: true, type: Date },
|
||||
startedAt: { required: true, type: Date },
|
||||
gcodeFile: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "GCodeFile",
|
||||
required: false,
|
||||
},
|
||||
quantity: {
|
||||
type: Number,
|
||||
required: true,
|
||||
default: 1,
|
||||
min: 1,
|
||||
},
|
||||
subJobs: [
|
||||
{type: Schema.Types.ObjectId, ref: "PrintSubJob", required: false}
|
||||
],
|
||||
});
|
||||
|
||||
printJobSchema.virtual("id").get(function () {
|
||||
return this._id.toHexString();
|
||||
});
|
||||
|
||||
printJobSchema.set("toJSON", { virtuals: true });
|
||||
|
||||
export const printJobModel = mongoose.model("PrintJob", printJobSchema);
|
||||
48
src/database/printsubjob.schema.js
Normal file
48
src/database/printsubjob.schema.js
Normal file
@ -0,0 +1,48 @@
|
||||
import mongoose from "mongoose";
|
||||
const { Schema } = mongoose;
|
||||
|
||||
const printSubJobSchema = new mongoose.Schema({
|
||||
printer: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Printer",
|
||||
required: true
|
||||
},
|
||||
printJob: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "PrintJob",
|
||||
required: true
|
||||
},
|
||||
subJobId: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
gcodeFile: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "GCodeFile",
|
||||
required: true,
|
||||
},
|
||||
state: {
|
||||
type: { required: true, type: String },
|
||||
progress: { required: false, type: Number, default: 0 },
|
||||
},
|
||||
number: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
createdAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
},
|
||||
updatedAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
});
|
||||
|
||||
printSubJobSchema.virtual("id").get(function () {
|
||||
return this._id.toHexString();
|
||||
});
|
||||
|
||||
printSubJobSchema.set("toJSON", { virtuals: true });
|
||||
|
||||
export const printSubJobModel = mongoose.model("PrintSubJob", printSubJobSchema);
|
||||
15
src/index.js
15
src/index.js
@ -2,16 +2,19 @@ import { loadConfig } from "./config.js";
|
||||
import { dbConnect } from "./database/mongo.js";
|
||||
import { PrinterManager } from "./printer/printermanager.js";
|
||||
import { SocketManager } from "./socket/socketmanager.js";
|
||||
|
||||
import { KeycloakAuth } from "./auth/auth.js";
|
||||
|
||||
import express from "express";
|
||||
import log4js from "log4js";
|
||||
|
||||
// Load configuration
|
||||
const config = loadConfig();
|
||||
|
||||
const logger = log4js.getLogger("FarmControl Server");
|
||||
logger.level = config.server.logLevel;
|
||||
|
||||
// Create Express app
|
||||
const app = express();
|
||||
|
||||
// Connect to database
|
||||
dbConnect();
|
||||
|
||||
@ -20,7 +23,13 @@ const keycloakAuth = new KeycloakAuth(config);
|
||||
|
||||
// Create printer manager
|
||||
const printerManager = new PrinterManager(config);
|
||||
const socketManager = new SocketManager(config, printerManager);
|
||||
const socketManager = new SocketManager(config, printerManager, keycloakAuth);
|
||||
printerManager.setSocketManager(socketManager);
|
||||
|
||||
// Start Express server
|
||||
app.listen(config.server.port, () => {
|
||||
logger.info(`Server listening on port ${config.server.port}`);
|
||||
});
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
logger.info("Shutting down...");
|
||||
|
||||
185
src/network/websocketScanner.js
Normal file
185
src/network/websocketScanner.js
Normal file
@ -0,0 +1,185 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import os from 'os';
|
||||
import { Worker } from 'worker_threads';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
export class WebSocketScanner extends EventEmitter {
|
||||
constructor(options = {}) {
|
||||
super();
|
||||
this.scanning = false;
|
||||
this.workers = [];
|
||||
// Default to number of CPU cores, but allow override
|
||||
this.maxThreads = options.maxThreads || os.cpus().length;
|
||||
this.totalIPs = 0;
|
||||
this.scannedIPs = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans the local network for websocket services on a specified port
|
||||
* @param {number} port - Port number to scan
|
||||
* @returns {Promise<Array>} Array of IP addresses where websocket service was found
|
||||
*/
|
||||
async scanNetwork(port, protocol) {
|
||||
// Clean up any existing workers before starting a new scan
|
||||
this.cleanupWorkers();
|
||||
console.log("Cleaned up workers");
|
||||
|
||||
if (this.scanning) {
|
||||
throw new Error('Scan already in progress');
|
||||
}
|
||||
|
||||
this.scanning = true;
|
||||
this.scannedIPs = 0;
|
||||
const foundServices = [];
|
||||
|
||||
// Get local network range
|
||||
const { startIP, endIP } = this.getLocalNetworkRange();
|
||||
const start = this.ipToNumber(startIP);
|
||||
const end = this.ipToNumber(endIP);
|
||||
|
||||
// Calculate IP ranges for each worker
|
||||
this.totalIPs = end - start + 1;
|
||||
const ipsPerWorker = Math.ceil(this.totalIPs / this.maxThreads);
|
||||
const workerPromises = [];
|
||||
|
||||
for (let i = 0; i < this.maxThreads; i++) {
|
||||
const workerStart = start + (i * ipsPerWorker);
|
||||
const workerEnd = Math.min(workerStart + ipsPerWorker - 1, end);
|
||||
|
||||
if (workerStart > end) break;
|
||||
|
||||
const workerStartIP = this.numberToIP(workerStart);
|
||||
const workerEndIP = this.numberToIP(workerEnd);
|
||||
|
||||
const worker = new Worker(path.join(__dirname, 'websocketScannerWorker.js'));
|
||||
this.workers.push(worker);
|
||||
console.log("Created worker", i);
|
||||
|
||||
const workerPromise = new Promise((resolve) => {
|
||||
worker.on('message', (message) => {
|
||||
switch (message.type) {
|
||||
case 'serviceFound':
|
||||
foundServices.push({ ip: message.ip, hostname: message.hostname });
|
||||
this.emit('serviceFound', { ip: message.ip, hostname: message.hostname });
|
||||
break;
|
||||
case 'scanProgress':
|
||||
this.scannedIPs += message.increment;
|
||||
const totalProgress = (this.scannedIPs / this.totalIPs) * 100;
|
||||
this.emit('scanProgress', {
|
||||
currentIP: message.currentIP,
|
||||
progress: totalProgress
|
||||
});
|
||||
break;
|
||||
case 'scanComplete':
|
||||
resolve(message.results);
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
worker.postMessage({
|
||||
type: 'scan',
|
||||
startIP: workerStartIP,
|
||||
endIP: workerEndIP,
|
||||
port,
|
||||
protocol
|
||||
});
|
||||
|
||||
workerPromises.push(workerPromise);
|
||||
}
|
||||
|
||||
await Promise.all(workerPromises);
|
||||
this.cleanupWorkers();
|
||||
this.scanning = false;
|
||||
return foundServices;
|
||||
}
|
||||
|
||||
cleanupWorkers() {
|
||||
this.workers.forEach(worker => worker.terminate());
|
||||
this.workers = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the local network IP range
|
||||
* @returns {Object} Object containing startIP and endIP
|
||||
*/
|
||||
getLocalNetworkRange() {
|
||||
const interfaces = os.networkInterfaces();
|
||||
let localIP = null;
|
||||
let subnetMask = null;
|
||||
|
||||
// Find the first non-internal IPv4 address
|
||||
for (const name of Object.keys(interfaces)) {
|
||||
for (const iface of interfaces[name]) {
|
||||
if (iface.family === 'IPv4' && !iface.internal) {
|
||||
localIP = iface.address;
|
||||
subnetMask = iface.netmask;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (localIP) break;
|
||||
}
|
||||
|
||||
if (!localIP) {
|
||||
throw new Error('Could not determine local network IP address');
|
||||
}
|
||||
|
||||
// Convert IP and subnet mask to numbers
|
||||
const ipNum = this.ipToNumber(localIP);
|
||||
const maskNum = this.ipToNumber(subnetMask);
|
||||
|
||||
// Calculate network address
|
||||
const networkNum = ipNum & maskNum;
|
||||
|
||||
// Calculate broadcast address
|
||||
const broadcastNum = networkNum | (~maskNum >>> 0);
|
||||
|
||||
// Start IP is network address + 1
|
||||
const startIP = this.numberToIP(networkNum + 1);
|
||||
// End IP is broadcast address - 1
|
||||
const endIP = this.numberToIP(broadcastNum - 1);
|
||||
|
||||
return { startIP, endIP };
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an IP address to a number
|
||||
* @param {string} ip - IP address to convert
|
||||
* @returns {number} Numeric representation of IP
|
||||
*/
|
||||
ipToNumber(ip) {
|
||||
return ip.split('.')
|
||||
.reduce((acc, octet) => (acc << 8) + parseInt(octet), 0) >>> 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a number to an IP address
|
||||
* @param {number} num - Number to convert
|
||||
* @returns {string} IP address
|
||||
*/
|
||||
numberToIP(num) {
|
||||
return [
|
||||
(num >>> 24) & 255,
|
||||
(num >>> 16) & 255,
|
||||
(num >>> 8) & 255,
|
||||
num & 255
|
||||
].join('.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the current scan
|
||||
*/
|
||||
stopScan() {
|
||||
this.scanning = false;
|
||||
this.cleanupWorkers();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// To stop scanning at any time:
|
||||
// scanner.stopScan();
|
||||
110
src/network/websocketScannerWorker.js
Normal file
110
src/network/websocketScannerWorker.js
Normal file
@ -0,0 +1,110 @@
|
||||
import { parentPort, workerData } from 'worker_threads';
|
||||
import WebSocket from 'ws';
|
||||
import { loadConfig } from "../config.js";
|
||||
import log4js from "log4js";
|
||||
import dns from 'dns';
|
||||
|
||||
// Load configuration
|
||||
const config = loadConfig();
|
||||
|
||||
class WebSocketScannerWorker {
|
||||
constructor() {
|
||||
this.randomId = Math.floor(Math.random() * 1000).toString().padStart(3, '0');
|
||||
this.logger = log4js.getLogger(`WS Scanner #${this.randomId}`);
|
||||
this.logger.level = config.server.logLevel;
|
||||
this.timeout = 2000; // 2 second timeout for each connection attempt
|
||||
}
|
||||
|
||||
async scanRange(startIP, endIP, port, protocol) {
|
||||
const start = this.ipToNumber(startIP);
|
||||
const end = this.ipToNumber(endIP);
|
||||
const foundServices = [];
|
||||
|
||||
this.logger.info(`Scanning ${startIP} - ${endIP} on port: ${port} using: ${protocol}`);
|
||||
|
||||
for (let ip = start; ip <= end; ip++) {
|
||||
const currentIP = this.numberToIP(ip);
|
||||
const url = `${protocol}://${currentIP}:${port}/websocket`;
|
||||
|
||||
try {
|
||||
this.logger.debug(`Checking ${currentIP} for websocket service on port ${port}`);
|
||||
const isOpen = await this.checkWebSocket(url);
|
||||
if (isOpen) {
|
||||
const hostname = await this.resolveHostname(currentIP);
|
||||
foundServices.push({ ip: currentIP, hostname });
|
||||
this.logger.info(`WebSocket connection successful for ${currentIP}${hostname ? ` (${hostname})` : ''}`);
|
||||
parentPort.postMessage({ type: 'serviceFound', ip: currentIP, hostname });
|
||||
} else {
|
||||
this.logger.debug(`WebSocket connection failed for ${currentIP}`);
|
||||
}
|
||||
} catch (error) {
|
||||
// Connection failed, continue scanning
|
||||
}
|
||||
|
||||
parentPort.postMessage({
|
||||
type: 'scanProgress',
|
||||
currentIP,
|
||||
increment: 1
|
||||
});
|
||||
}
|
||||
|
||||
return foundServices;
|
||||
}
|
||||
|
||||
async resolveHostname(ip) {
|
||||
try {
|
||||
const hostnames = await dns.promises.reverse(ip);
|
||||
return hostnames[0] || null;
|
||||
} catch (error) {
|
||||
// Only log errors that aren't ENOTFOUND (which is expected for many IPs)
|
||||
if (error.code !== 'ENOTFOUND') {
|
||||
this.logger.warn(`Unexpected error resolving hostname for ${ip}: ${error.message}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
checkWebSocket(url) {
|
||||
return new Promise((resolve) => {
|
||||
const ws = new WebSocket(url);
|
||||
let timeout = setTimeout(() => {
|
||||
ws.terminate();
|
||||
resolve(false);
|
||||
}, this.timeout);
|
||||
|
||||
ws.on('open', () => {
|
||||
clearTimeout(timeout);
|
||||
ws.close();
|
||||
resolve(true);
|
||||
});
|
||||
|
||||
ws.on('error', () => {
|
||||
clearTimeout(timeout);
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ipToNumber(ip) {
|
||||
return ip.split('.')
|
||||
.reduce((acc, octet) => (acc << 8) + parseInt(octet), 0) >>> 0;
|
||||
}
|
||||
|
||||
numberToIP(num) {
|
||||
return [
|
||||
(num >>> 24) & 255,
|
||||
(num >>> 16) & 255,
|
||||
(num >>> 8) & 255,
|
||||
num & 255
|
||||
].join('.');
|
||||
}
|
||||
}
|
||||
|
||||
// Handle messages from the main thread
|
||||
parentPort.on('message', async (data) => {
|
||||
if (data.type === 'scan') {
|
||||
const scanner = new WebSocketScannerWorker();
|
||||
const results = await scanner.scanRange(data.startIP, data.endIP, data.port, data.protocol);
|
||||
parentPort.postMessage({ type: 'scanComplete', results });
|
||||
}
|
||||
});
|
||||
@ -1,50 +1,66 @@
|
||||
// jsonrpc.js - Implementation of JSON-RPC 2.0 protocol for Moonraker communication
|
||||
import { loadConfig } from "../config.js";
|
||||
import log4js from "log4js";
|
||||
|
||||
// Load configuration
|
||||
const config = loadConfig();
|
||||
|
||||
const logger = log4js.getLogger("JSON RPC");
|
||||
logger.level = config.server.logLevel;
|
||||
|
||||
export class JsonRPC {
|
||||
constructor() {
|
||||
this.id_counter = 0;
|
||||
this.idCounter = 0;
|
||||
this.methods = {};
|
||||
this.pending_requests = {};
|
||||
this.pendingRequests = {};
|
||||
}
|
||||
|
||||
// Generate a unique ID for RPC requests
|
||||
generate_id() {
|
||||
return this.id_counter++;
|
||||
generateId() {
|
||||
return this.idCounter++;
|
||||
}
|
||||
|
||||
// Register a method to handle incoming notifications/responses
|
||||
register_method(method_name, callback) {
|
||||
this.methods[method_name] = callback;
|
||||
registerMethod(methodName, callback) {
|
||||
this.methods[methodName] = callback;
|
||||
}
|
||||
|
||||
// Process incoming messages
|
||||
process_message(message) {
|
||||
processMessage(message) {
|
||||
|
||||
if (message.method && this.methods[message.method]) {
|
||||
// Handle method call or notification
|
||||
this.methods[message.method](message.params);
|
||||
logger.trace(`JSON-RPC notification: ${message.method}`);
|
||||
} else if (message.id !== undefined) {
|
||||
// Handle response to a previous request
|
||||
const rpc_promise = this.pending_requests[message.id];
|
||||
if (rpc_promise) {
|
||||
const rpcPromise = this.pendingRequests[message.id];
|
||||
if (rpcPromise) {
|
||||
if (message.error) {
|
||||
rpc_promise.reject(message.error);
|
||||
logger.error(`Error in JSON-RPC response: ${message.error}`);
|
||||
rpcPromise.reject(message.error);
|
||||
} else {
|
||||
rpc_promise.resolve(message.result);
|
||||
logger.debug(`JSON-RPC response: OK`);
|
||||
logger.trace("Result:", message.result);
|
||||
|
||||
rpcPromise.resolve(message.result);
|
||||
}
|
||||
delete this.pending_requests[message.id];
|
||||
delete this.pendingRequests[message.id];
|
||||
}
|
||||
}
|
||||
// If it's a notification without a registered method, ignore it
|
||||
}
|
||||
|
||||
// Call a method without parameters
|
||||
call_method(method) {
|
||||
return this.call_method_with_kwargs(method, {});
|
||||
callMethod(method) {
|
||||
return this.callMethodWithKwargs(method, {});
|
||||
}
|
||||
|
||||
// Call a method with parameters
|
||||
call_method_with_kwargs(method, params) {
|
||||
const id = this.generate_id();
|
||||
callMethodWithKwargs(method, params) {
|
||||
logger.debug(`Calling method: ${method}`);
|
||||
logger.trace("Params:", params);
|
||||
const id = this.generateId();
|
||||
const request = {
|
||||
jsonrpc: "2.0",
|
||||
method: method,
|
||||
@ -53,9 +69,7 @@ export class JsonRPC {
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.pending_requests[id] = { resolve, reject };
|
||||
|
||||
console.log(`Sending JSON-RPC request: ${JSON.stringify(request)}`);
|
||||
this.pendingRequests[id] = { resolve, reject };
|
||||
// The actual sending of the message is done by the WebSocket connection
|
||||
// This just prepares the message and returns a promise
|
||||
if (this.socket) {
|
||||
@ -63,20 +77,20 @@ export class JsonRPC {
|
||||
} else {
|
||||
// If socket is not directly attached to this instance, the caller
|
||||
// is responsible for sending the serialized request
|
||||
this.last_request = JSON.stringify(request);
|
||||
this.lastRequest = JSON.stringify(request);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// For external socket handling
|
||||
get_last_request() {
|
||||
const req = this.last_request;
|
||||
this.last_request = null;
|
||||
getLastRequest() {
|
||||
const req = this.lastRequest;
|
||||
this.lastRequest = null;
|
||||
return req;
|
||||
}
|
||||
|
||||
// Associate a WebSocket with this RPC instance for direct communication
|
||||
set_socket(socket) {
|
||||
setSocket(socket) {
|
||||
this.socket = socket;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,9 +1,10 @@
|
||||
// printer-manager.js - Manages multiple printer connections through MongoDB
|
||||
import { PrinterClient } from "./printerclient.js";
|
||||
import { saveConfig, loadConfig } from "../config.js";
|
||||
import { printerModel } from "../database/printer.schema.js"; // Import your printer model
|
||||
import { printSubJobModel } from "../database/printsubjob.schema.js"; // Import your subjob model
|
||||
import { loadConfig } from "../config.js";
|
||||
import log4js from "log4js";
|
||||
|
||||
import { printJobModel } from "../database/printjob.schema.js";
|
||||
// Load configuration
|
||||
const config = loadConfig();
|
||||
|
||||
@ -11,80 +12,175 @@ const logger = log4js.getLogger("Printer Manager");
|
||||
logger.level = config.server.logLevel;
|
||||
|
||||
export class PrinterManager {
|
||||
constructor(config) {
|
||||
this.config = config;
|
||||
this.printerClientConnections = new Map();
|
||||
this.statusCheckInterval = null;
|
||||
this.initializePrinterConnections();
|
||||
constructor(config) {
|
||||
this.config = config;
|
||||
this.printerClientConnections = new Map();
|
||||
this.statusCheckInterval = null;
|
||||
this.initializePrinterConnections();
|
||||
}
|
||||
|
||||
async initializePrinterConnections() {
|
||||
try {
|
||||
// Get all printers from the database
|
||||
const printers = await printerModel.find({});
|
||||
|
||||
for (const printer of printers) {
|
||||
await this.connectToPrinter(printer);
|
||||
}
|
||||
|
||||
logger.info(`Initialized connections to ${printers.length} printers`);
|
||||
} catch (error) {
|
||||
logger.error(`Error initializing printer connections: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async connectToPrinter(printer) {
|
||||
// Create and store the connection
|
||||
const printerClientConnection = new PrinterClient(
|
||||
printer,
|
||||
this,
|
||||
this.socketManager,
|
||||
);
|
||||
this.printerClientConnections.set(printer.id, printerClientConnection);
|
||||
|
||||
// Connect to the printer
|
||||
await printerClientConnection.connect();
|
||||
|
||||
logger.info(`Connected to printer: ${printer.printerName} (${printer.id})`);
|
||||
return true;
|
||||
}
|
||||
|
||||
getPrinterClient(printerId) {
|
||||
return this.printerClientConnections.get(printerId);
|
||||
}
|
||||
|
||||
getAllPrinterClients() {
|
||||
return this.printerClientConnections.values();
|
||||
}
|
||||
|
||||
// Process command for a specific printer
|
||||
async processPrinterCommand(command) {
|
||||
const printerId = command.params.printerId;
|
||||
const printerClientConnection =
|
||||
this.printerClientConnections.get(printerId);
|
||||
if (!printerClientConnection) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Printer with ID ${printerId} not found`,
|
||||
};
|
||||
}
|
||||
|
||||
async initializePrinterConnections() {
|
||||
try {
|
||||
// Get all printers from the database
|
||||
const printers = await printerModel.find({});
|
||||
return await printerClientConnection.sendPrinterCommand(command);
|
||||
}
|
||||
|
||||
for (const printer of printers) {
|
||||
await this.connectToPrinter(printer);
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`Initialized connections to ${printers.length} printers`,
|
||||
);
|
||||
|
||||
// Set up periodic status checking
|
||||
this.startStatusChecking();
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Error initializing printer connections: ${error.message}`,
|
||||
);
|
||||
}
|
||||
async updateSubscription(printerId, socketId, mergedSubscription) {
|
||||
const printerClientConnection =
|
||||
this.printerClientConnections.get(printerId);
|
||||
if (!printerClientConnection) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Printer with ID ${printerId} not found`,
|
||||
};
|
||||
}
|
||||
printerClientConnection.subscriptions.set(socketId, mergedSubscription);
|
||||
return await printerClientConnection.updateSubscriptions();
|
||||
}
|
||||
|
||||
async connectToPrinter(printer) {
|
||||
// Create and store the connection
|
||||
const printerClientConnection = new PrinterClient(printer);
|
||||
this.moonrakerConnections.set(printer.id, printerClientConnection);
|
||||
// Close all printer connections
|
||||
closeAllConnections() {
|
||||
for (const printerClientConnection of this.printerClientConnections.values()) {
|
||||
if (printerClientConnection.socket) {
|
||||
printerClientConnection.socket.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Connect to the printer
|
||||
printerClientConnection.connect();
|
||||
setSocketManager(socketManager) {
|
||||
this.socketManager = socketManager;
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`Connected to printer: ${printer.printerName} (${printer.id})`,
|
||||
async downloadGCODE(gcodeFileId) {
|
||||
logger.info(`Downloading G-code file ${gcodeFileId}`);
|
||||
try {
|
||||
// Download the G-code file with authentication
|
||||
const url = `http://localhost:8080/gcodefiles/${gcodeFileId}/content/`;
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.socketManager.socketClientConnections.values().next().value.socket.handshake.auth.token}`,
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to download G-code file: ${response.statusText}`,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
const gcodeContent = await response.blob();
|
||||
|
||||
logger.info(`G-code file ${gcodeFileId} downloaded!`);
|
||||
|
||||
return gcodeContent;
|
||||
} catch (error) {
|
||||
logger.error("Error in deployGcodeToAllPrinters:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async deployPrintJob(printJobId) {
|
||||
logger.info(`Deploying print job ${printJobId}`);
|
||||
const printJob = await printJobModel
|
||||
.findById(printJobId)
|
||||
.populate("printers")
|
||||
.populate("subJobs");
|
||||
if (!printJob) {
|
||||
throw new Error("Print job not found");
|
||||
}
|
||||
|
||||
getPrinterClient(printerId) {
|
||||
return this.printerClientConnections.get(printerId);
|
||||
if (!printJob.gcodeFile) {
|
||||
throw new Error("No G-code file associated with this print job");
|
||||
}
|
||||
|
||||
getAllPrinterClients() {
|
||||
return this.printerClientConnections.values();
|
||||
const gcodeFileId = printJob.gcodeFile.toString();
|
||||
const fileName = `${printJob.id}.gcode`;
|
||||
|
||||
const gcodeFile = await this.downloadGCODE(gcodeFileId);
|
||||
|
||||
for (const printer of printJob.printers) {
|
||||
const printerClient = this.getPrinterClient(printer.id);
|
||||
if (!printerClient) {
|
||||
throw new Error(`Printer with ID ${printer.id} not found`);
|
||||
return false;
|
||||
}
|
||||
await printerClient.uploadGcodeFile(gcodeFile, fileName);
|
||||
await printerClient.deploySubJobs(printJob.id);
|
||||
}
|
||||
|
||||
// Process command for a specific printer
|
||||
processCommand(printerId, command) {
|
||||
const printerClientConnection =
|
||||
this.printerClientConnections.get(printerId);
|
||||
if (!printerClientConnection) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Printer with ID ${printerId} not found`,
|
||||
};
|
||||
}
|
||||
const success = connection.sendCommand(message);
|
||||
return {
|
||||
success,
|
||||
error: success ? null : "Printer not connected",
|
||||
};
|
||||
}
|
||||
printJob.state = { type: "queued" };
|
||||
printJob.updatedAt = new Date();
|
||||
await printJob.save();
|
||||
|
||||
// Close all printer connections
|
||||
closeAllConnections() {
|
||||
for (const printerClientConnection of this.printerClientConnections.values()) {
|
||||
if (printerClientConnection.socket) {
|
||||
printerClientConnection.socket.close();
|
||||
}
|
||||
}
|
||||
this.socketManager.broadcast("notify_job_update", {
|
||||
id: printJob.id,
|
||||
state: { type: "queued" },
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async cancelSubJob(subJobId) {
|
||||
logger.info(`Canceling sub job ${subJobId}`);
|
||||
const subJob = await printSubJobModel.findById(subJobId);
|
||||
if (!subJob) {
|
||||
throw new Error("Sub job not found");
|
||||
}
|
||||
const printerClient = this.getPrinterClient(subJob.printer.toString());
|
||||
if (!printerClient) {
|
||||
throw new Error(`Printer with ID ${printer.id} not found`);
|
||||
return false;
|
||||
}
|
||||
await printerClient.cancelSubJob(subJob.subJobId);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,63 +1,460 @@
|
||||
import { WebSocket } from "ws";
|
||||
import { JsonRPC } from "./jsonrpc.js";
|
||||
import log4js from "log4js";
|
||||
import { printJobModel } from "../database/printjob.schema.js";
|
||||
import { printSubJobModel } from "../database/printsubjob.schema.js";
|
||||
import { WebSocketScanner } from "../network/websocketScanner.js";
|
||||
// Load configuration
|
||||
import { loadConfig } from "./config.js";
|
||||
import { loadConfig } from "../config.js";
|
||||
import { printerModel } from "../database/printer.schema.js";
|
||||
|
||||
const config = loadConfig();
|
||||
|
||||
const logger = log4js.getLogger("Moonraker");
|
||||
const logger = log4js.getLogger("Socket Client");
|
||||
logger.level = config.server.logLevel;
|
||||
|
||||
export class SocketClient {
|
||||
constructor(socket, printerManager) {
|
||||
this.socket = socket;
|
||||
this.user = socket?.user;
|
||||
this.printerManager = printerManager;
|
||||
constructor(socket, socketManager, printerManager) {
|
||||
this.socket = socket;
|
||||
this.user = socket?.user;
|
||||
this.socketManager = socketManager;
|
||||
this.printerManager = printerManager;
|
||||
this.activeSubscriptions = new Map();
|
||||
this.scanner = new WebSocketScanner({ maxThreads: 50 });
|
||||
|
||||
this.socket.on("bridge.list_printers", (data) => {
|
||||
this.socket.on("bridge.list_printers", (data) => {});
|
||||
|
||||
});
|
||||
this.socket.on("bridge.add_printer", (data, callback) => {});
|
||||
|
||||
this.socket.on("bridge.add_printer", (data, callback) => {
|
||||
this.socket.on("bridge.remove_printer", (data, callback) => {});
|
||||
|
||||
});
|
||||
this.socket.on("bridge.update_printer", (data, callback) => {});
|
||||
|
||||
this.socket.on("bridge.remove_printer", (data, callback) => {
|
||||
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);
|
||||
});
|
||||
|
||||
});
|
||||
// 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,
|
||||
});
|
||||
});
|
||||
|
||||
this.socket.on("bridge.update_printer", (data, callback) => {
|
||||
// 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("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 commands
|
||||
this.socket.on("printer_command", (data, callback) => {
|
||||
try {
|
||||
if (data && data.params.printerId) {
|
||||
const printerId = data.params.printerId;
|
||||
// Remove the printer_id before forwarding
|
||||
const cleanCommand = { ...data };
|
||||
delete cleanCommand.params.printerId;
|
||||
// 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;
|
||||
|
||||
const result = printerManager.processCommand(
|
||||
printerId,
|
||||
cleanCommand,
|
||||
);
|
||||
// Get existing subscription or create new one
|
||||
const existingSubscription =
|
||||
this.activeSubscriptions.get(printerId) || {};
|
||||
|
||||
} else {
|
||||
logger.error("Missing Printer ID");
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error("Error processing client command:", e);
|
||||
// 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,
|
||||
);
|
||||
|
||||
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.on("disconnect", () => {
|
||||
// Unsubscribe from all printers when client disconnects
|
||||
printerManager.unsubscribeClientFromAll(socket);
|
||||
logger.info("External client disconnected:", socket.user?.username);
|
||||
if (callback) {
|
||||
callback(result);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error("Error processing gcode script request:", e);
|
||||
if (callback) {
|
||||
callback({ error: e.message });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
callback(result);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error("Error processing printer objects query request:", e);
|
||||
if (callback) {
|
||||
callback({ error: e.message });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
if (callback) {
|
||||
callback(result);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error("Error processing gcode script request:", e);
|
||||
if (callback) {
|
||||
callback({ error: e.message });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
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,
|
||||
);
|
||||
|
||||
if (callback) {
|
||||
callback(result);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error("Error processing job queue deploy 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,
|
||||
});
|
||||
|
||||
if (callback) {
|
||||
callback(result);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error("Error processing print resume request:", e);
|
||||
if (callback) {
|
||||
callback({ error: e.message });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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 });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
if (callback) {
|
||||
callback(result);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error("Error processing print cancel request:", e);
|
||||
if (callback) {
|
||||
callback({ error: e.message });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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 });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
if (callback) {
|
||||
callback(result);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error("Error processing job queue pause request:", e);
|
||||
if (callback) {
|
||||
callback({ error: e.message });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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 });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.socket.on("disconnect", () => {
|
||||
logger.info("External client disconnected:", socket.user?.username);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,19 +1,22 @@
|
||||
// server.js - HTTP and Socket.IO server setup
|
||||
import { Server } from "socket.io";
|
||||
import http from "http";
|
||||
import { createAuthMiddleware } from "./auth.js";
|
||||
import { createAuthMiddleware } from "../auth/auth.js";
|
||||
import log4js from "log4js";
|
||||
// Load configuration
|
||||
import { loadConfig } from "./config.js";
|
||||
import { loadConfig } from "../config.js";
|
||||
import { SocketClient } from "./socketclient.js";
|
||||
|
||||
const config = loadConfig();
|
||||
|
||||
const logger = log4js.getLogger("Server");
|
||||
const logger = log4js.getLogger("Socket Manager");
|
||||
logger.level = config.server.logLevel;
|
||||
|
||||
export class SocketServer {
|
||||
export class SocketManager {
|
||||
constructor(config, printerManager, auth) {
|
||||
this.socketClientConnections = new Map();
|
||||
this.printerManager = printerManager;
|
||||
|
||||
// Create HTTP server
|
||||
const server = http.createServer((req, res) => {
|
||||
res.writeHead(200, { "Content-Type": "text/plain" });
|
||||
@ -34,7 +37,7 @@ export class SocketServer {
|
||||
// Handle client connections
|
||||
io.on("connection", (socket) => {
|
||||
logger.info("External client connected:", socket.user?.username);
|
||||
this.socketClientConnections.set(socket.id, socket);
|
||||
this.addClient(socket);
|
||||
});
|
||||
|
||||
// Start the server
|
||||
@ -44,164 +47,224 @@ export class SocketServer {
|
||||
);
|
||||
});
|
||||
|
||||
return { server, io };
|
||||
this.io = io;
|
||||
this.server = server;
|
||||
}
|
||||
}
|
||||
export function setupServer(config, printerManager, auth) {}
|
||||
|
||||
// Command handlers
|
||||
function handleSubscribe(
|
||||
socket,
|
||||
data,
|
||||
clientSubscriptions,
|
||||
printerManager,
|
||||
callback,
|
||||
) {
|
||||
logger.info("Handling subscribe command...");
|
||||
const printerId = data.printerId;
|
||||
if (printerManager.subscribeClient(printerId, socket)) {
|
||||
clientSubscriptions.add(printerId);
|
||||
logger.info(`Client subscribed to printer ${printerId}`);
|
||||
addClient(socket) {
|
||||
const client = new SocketClient(socket, this, this.printerManager);
|
||||
this.socketClientConnections.set(socket.id, client);
|
||||
logger.info("External client connected:", socket.user?.username);
|
||||
// Handle disconnection
|
||||
socket.on("disconnect", () => {
|
||||
logger.info("External client disconnected:", socket.user?.username);
|
||||
this.removeClient(socket.id);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
removeClient(socketClientId) {
|
||||
const socketClient = this.socketClientConnections.get(socketClientId);
|
||||
if (socketClient) {
|
||||
this.socketClientConnections.delete(socketClientId);
|
||||
logger.info("External client disconnected:", socketClient.socket.user?.username);
|
||||
}
|
||||
}
|
||||
|
||||
getSocketClient(clientId) {
|
||||
return this.socketClientConnections.get(clientId);
|
||||
}
|
||||
|
||||
getAllSocketClients() {
|
||||
return Array.from(this.socketClientConnections.values());
|
||||
}
|
||||
|
||||
broadcast(event, data, excludeClientId = null) {
|
||||
for (const [clientId, socketClient] of this.socketClientConnections) {
|
||||
if (excludeClientId !== clientId) {
|
||||
socketClient.socket.emit(event, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
broadcastToSubscribers(printerId, command) {
|
||||
const paramsObject = command.params;
|
||||
for (const [clientId, socketClient] of this.socketClientConnections) {
|
||||
|
||||
// Check if this client has subscribed to the printer
|
||||
if (socketClient.activeSubscriptions.has(printerId)) {
|
||||
|
||||
const subscription = socketClient.activeSubscriptions.get(printerId);
|
||||
|
||||
// Filter the message based on the subscription filters
|
||||
const filteredMessage = this.filterMessage(paramsObject, subscription);
|
||||
if (filteredMessage) {
|
||||
logger.trace(`Broadcasting message to client ${clientId}: ${JSON.stringify(filteredMessage)}`);
|
||||
socketClient.socket.emit(command.method, {
|
||||
printerId,
|
||||
...filteredMessage
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
filterMessage(message, subscription) {
|
||||
if (!message || !subscription) return null;
|
||||
|
||||
const filtered = {};
|
||||
// Handle both subscription formats
|
||||
const subscriptionObjects = subscription.objects || subscription;
|
||||
|
||||
for (const [objectName, fields] of Object.entries(subscriptionObjects)) {
|
||||
if (message[objectName]) {
|
||||
if (fields === null) {
|
||||
// If fields is null, include all fields
|
||||
filtered[objectName] = message[objectName];
|
||||
} else if (Array.isArray(fields)) {
|
||||
// If fields is an array, only include specified fields
|
||||
filtered[objectName] = {};
|
||||
for (const field of fields) {
|
||||
if (message[objectName][field] !== undefined) {
|
||||
filtered[objectName][field] = message[objectName][field];
|
||||
}
|
||||
}
|
||||
// Only include the object if it has any filtered fields
|
||||
if (Object.keys(filtered[objectName]).length === 0) {
|
||||
delete filtered[objectName];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Object.keys(filtered).length > 0 ? filtered : null;
|
||||
}
|
||||
|
||||
handleListPrinters(socket, data, callback) {
|
||||
logger.info("handleListPrinters called with data:", data);
|
||||
if (callback) {
|
||||
callback({
|
||||
success: true,
|
||||
printer_id: printerId,
|
||||
printers: this.printerManager.getAllPrinters(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
logger.warn(`Client failed to subscribe to printer ${printerId}`);
|
||||
}
|
||||
|
||||
handleListPrintersSubscribe(socket, data, callback) {
|
||||
logger.info("handleListPrintersSubscribe called with data:", data);
|
||||
if (callback) {
|
||||
callback({
|
||||
success: false,
|
||||
error: {
|
||||
code: -32001,
|
||||
message: `Printer ${printerId} not found`,
|
||||
},
|
||||
printers: this.printerManager.getAllPrinters(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleUnsubscribe(
|
||||
socket,
|
||||
data,
|
||||
clientSubscriptions,
|
||||
printerManager,
|
||||
callback,
|
||||
) {
|
||||
const printerId = data.printer_id;
|
||||
if (printerManager.unsubscribeClient(printerId, socket)) {
|
||||
clientSubscriptions.delete(printerId);
|
||||
if (callback)
|
||||
callback({
|
||||
success: true,
|
||||
printer_id: printerId,
|
||||
});
|
||||
} else {
|
||||
if (callback)
|
||||
callback({
|
||||
success: false,
|
||||
error: {
|
||||
code: -32001,
|
||||
message: `Printer ${printerId} not found`,
|
||||
},
|
||||
});
|
||||
handleAddPrinter(socket, data, callback) {
|
||||
logger.info("handleAddPrinter called with data:", data);
|
||||
if (this.printerManager.addPrinter(data.printer_config)) {
|
||||
if (callback) {
|
||||
callback({
|
||||
success: true,
|
||||
printer: this.printerManager
|
||||
.getPrinter(data.printer_config.id)
|
||||
.getStatus(),
|
||||
});
|
||||
}
|
||||
this.broadcastPrinterList(socket);
|
||||
} else {
|
||||
if (callback) {
|
||||
callback({
|
||||
success: false,
|
||||
error: {
|
||||
code: -32003,
|
||||
message: `Failed to add printer with ID ${data.printer_config.id}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleListPrinters(socket, data, printerManager, callback) {
|
||||
if (callback)
|
||||
callback({
|
||||
printers: printerManager.getAllPrinters(),
|
||||
});
|
||||
}
|
||||
|
||||
function handleListPrintersSubscribe(socket, data, printerManager, callback) {
|
||||
if (callback)
|
||||
callback({
|
||||
printers: printerManager.getAllPrinters(),
|
||||
});
|
||||
}
|
||||
|
||||
function handleAddPrinter(socket, data, printerManager, callback) {
|
||||
if (printerManager.addPrinter(data.printer_config)) {
|
||||
if (callback)
|
||||
callback({
|
||||
success: true,
|
||||
printer: printerManager
|
||||
.getPrinter(data.printer_config.id)
|
||||
.getStatus(),
|
||||
});
|
||||
|
||||
// Notify all clients about the new printer
|
||||
broadcastPrinterList(printerManager, socket);
|
||||
} else {
|
||||
if (callback)
|
||||
callback({
|
||||
success: false,
|
||||
error: {
|
||||
code: -32003,
|
||||
message: `Failed to add printer with ID ${data.printer_config.id}`,
|
||||
},
|
||||
});
|
||||
handleRemovePrinter(socket, data, callback) {
|
||||
logger.info("handleRemovePrinter called with data:", data);
|
||||
if (this.printerManager.removePrinter(data.printer_id)) {
|
||||
if (callback) {
|
||||
callback({
|
||||
success: true,
|
||||
printer_id: data.printer_id,
|
||||
});
|
||||
}
|
||||
this.broadcastPrinterList(socket);
|
||||
} else {
|
||||
if (callback) {
|
||||
callback({
|
||||
success: false,
|
||||
error: {
|
||||
code: -32001,
|
||||
message: `Printer ${data.printer_id} not found`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleRemovePrinter(socket, data, printerManager, callback) {
|
||||
if (printerManager.removePrinter(data.printer_id)) {
|
||||
if (callback)
|
||||
callback({
|
||||
success: true,
|
||||
printer_id: data.printer_id,
|
||||
});
|
||||
|
||||
// Notify all clients about the updated printer list
|
||||
broadcastPrinterList(printerManager, socket);
|
||||
} else {
|
||||
if (callback)
|
||||
callback({
|
||||
success: false,
|
||||
error: {
|
||||
code: -32001,
|
||||
message: `Printer ${data.printer_id} not found`,
|
||||
},
|
||||
});
|
||||
handleUpdatePrinter(socket, data, callback) {
|
||||
logger.info("handleUpdatePrinter called with data:", data);
|
||||
if (this.printerManager.updatePrinter(data.printer_config)) {
|
||||
if (callback) {
|
||||
callback({
|
||||
success: true,
|
||||
printer: this.printerManager
|
||||
.getPrinter(data.printer_config.id)
|
||||
.getStatus(),
|
||||
});
|
||||
}
|
||||
this.broadcastPrinterList(socket);
|
||||
} else {
|
||||
if (callback) {
|
||||
callback({
|
||||
success: false,
|
||||
error: {
|
||||
code: -32001,
|
||||
message: `Failed to update printer ${data.printer_config.id}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleUpdatePrinter(socket, data, printerManager, callback) {
|
||||
if (printerManager.updatePrinter(data.printer_config)) {
|
||||
if (callback)
|
||||
callback({
|
||||
success: true,
|
||||
printer: printerManager
|
||||
.getPrinter(data.printer_config.id)
|
||||
.getStatus(),
|
||||
|
||||
resubscribeAllClients(printerId) {
|
||||
logger.info(`Resubscribing to all active subscriptions for printer ${printerId}`);
|
||||
|
||||
// Create a combined subscription object for the specified printer
|
||||
const combinedSubscription = {
|
||||
printerId: printerId,
|
||||
objects: {}
|
||||
};
|
||||
|
||||
// Combine all subscription objects for the specified printer
|
||||
for (const [clientId, socketClient] of this.socketClientConnections) {
|
||||
if (socketClient.activeSubscriptions.has(printerId)) {
|
||||
const subscription = socketClient.activeSubscriptions.get(printerId);
|
||||
|
||||
// Merge the objects from this subscription into the combined subscription
|
||||
Object.assign(combinedSubscription.objects, subscription.objects);
|
||||
logger.debug(`Adding subscription objects from client ${clientId} for printer ${printerId}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Only send the subscription command if there are any objects to subscribe to
|
||||
if (Object.keys(combinedSubscription.objects).length > 0) {
|
||||
logger.debug(`Sending combined subscription and query for printer ${printerId}:`, combinedSubscription);
|
||||
this.printerManager.processPrinterCommand({
|
||||
method: "printer.objects.subscribe",
|
||||
params: combinedSubscription
|
||||
});
|
||||
|
||||
// Notify all clients about the updated printer
|
||||
broadcastPrinterList(printerManager, socket);
|
||||
} else {
|
||||
if (callback)
|
||||
callback({
|
||||
success: false,
|
||||
error: {
|
||||
code: -32001,
|
||||
message: `Failed to update printer ${data.printer_config.id}`,
|
||||
},
|
||||
this.printerManager.processPrinterCommand({
|
||||
method: "printer.objects.query",
|
||||
params: combinedSubscription
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function broadcastPrinterList(printerManager, excludeSocket = null) {
|
||||
const printerList = printerManager.getAllPrinters();
|
||||
const message = {
|
||||
printers: printerList,
|
||||
};
|
||||
|
||||
for (const client of printerManager.getAllClients()) {
|
||||
if (client !== excludeSocket && client.connected) {
|
||||
client.emit("notify_printers_updated", message);
|
||||
} else {
|
||||
logger.debug(`No active subscriptions found for printer ${printerId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user