Initial Commit
This commit is contained in:
commit
92906e940d
134
.gitignore
vendored
Normal file
134
.gitignore
vendored
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
.DS_STORE
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# vuepress v2.x temp and cache directory
|
||||||
|
.temp
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# Docusaurus cache and generated files
|
||||||
|
.docusaurus
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
||||||
|
|
||||||
|
.nova
|
||||||
19
config.json
Normal file
19
config.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"server": {
|
||||||
|
"port": 8080,
|
||||||
|
"logLevel": "debug"
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"enabled": true,
|
||||||
|
"keycloak": {
|
||||||
|
"url": "https://auth.tombutcher.work",
|
||||||
|
"realm": "master",
|
||||||
|
"clientId": "farmcontrol-client",
|
||||||
|
"clientSecret": ""
|
||||||
|
},
|
||||||
|
"requiredRoles": []
|
||||||
|
},
|
||||||
|
"database": {
|
||||||
|
"url": "mongodb://192.168.68.53:27017/farmcontrol"
|
||||||
|
}
|
||||||
|
}
|
||||||
3
nodemon.json
Normal file
3
nodemon.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"ignore": ["node_modules/*", "*.log", "public/*", "config.json"]
|
||||||
|
}
|
||||||
2706
package-lock.json
generated
Normal file
2706
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
package.json
Normal file
27
package.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "farmcontrol-server",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Connects to moonraker and also manages the socket connection to the printer.",
|
||||||
|
"main": "src/index.js",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"start": "node src/index.js",
|
||||||
|
"dev": "nodemon src/index.js"
|
||||||
|
},
|
||||||
|
"author": "Tom Butcher",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.8.4",
|
||||||
|
"express": "^5.1.0",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"keycloak-connect": "^26.1.1",
|
||||||
|
"log4js": "^6.9.1",
|
||||||
|
"mongoose": "^8.13.2",
|
||||||
|
"socket.io": "^4.8.1",
|
||||||
|
"ws": "^8.18.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"nodemon": "^3.1.9"
|
||||||
|
}
|
||||||
|
}
|
||||||
83
readme.md
Normal file
83
readme.md
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
# Farm Control Server
|
||||||
|
|
||||||
|
A Node.js application that bridges communication between external websocket clients and a Moonraker-controlled 3D printer.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Connects to Moonraker API via websocket
|
||||||
|
- Provides a websocket server for external clients
|
||||||
|
- Relays JSON-RPC commands between clients and the printer
|
||||||
|
- Broadcasts printer status updates to all connected clients
|
||||||
|
- Handles reconnection if the Moonraker connection is lost
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Clone this repository
|
||||||
|
2. Install dependencies:
|
||||||
|
```
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
3. Edit the `config.json` file to match your Moonraker setup
|
||||||
|
4. Start the server:
|
||||||
|
```
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The `config.json` file contains the following options:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"moonraker": {
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 7125,
|
||||||
|
"protocol": "ws",
|
||||||
|
"apiKey": null,
|
||||||
|
"identity": {
|
||||||
|
"name": "printer-bridge",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "external"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"server": {
|
||||||
|
"port": 8080
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `moonraker.host`: The hostname or IP address of your Moonraker instance
|
||||||
|
- `moonraker.port`: The port number of your Moonraker instance
|
||||||
|
- `moonraker.protocol`: The protocol to use (`ws` or `wss`)
|
||||||
|
- `moonraker.apiKey`: Your Moonraker API key (if required)
|
||||||
|
- `server.port`: The port number for the websocket server
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Connect to the websocket server at `ws://[host]:[port]` and send JSON-RPC formatted messages to control the printer.
|
||||||
|
|
||||||
|
Example client-side code:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const ws = new WebSocket('ws://localhost:8080');
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
console.log('Connected to printer bridge');
|
||||||
|
|
||||||
|
// Send a command to get printer info
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
method: "printer.info",
|
||||||
|
id: 1
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
const message = JSON.parse(event.data);
|
||||||
|
console.log('Received:', message);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
150
src/auth/auth.js
Normal file
150
src/auth/auth.js
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
// auth.js - Keycloak authentication handler
|
||||||
|
import axios from "axios";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
import log4js from "log4js";
|
||||||
|
// Load configuration
|
||||||
|
import { loadConfig } from "../config.js";
|
||||||
|
|
||||||
|
const config = loadConfig();
|
||||||
|
|
||||||
|
const logger = log4js.getLogger("MongoDB");
|
||||||
|
logger.level = config.server.logLevel;
|
||||||
|
|
||||||
|
export class KeycloakAuth {
|
||||||
|
constructor(config) {
|
||||||
|
this.config = config.auth;
|
||||||
|
this.tokenCache = new Map(); // Cache for verified tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify a token with Keycloak server
|
||||||
|
async verifyToken(token) {
|
||||||
|
// Check cache first
|
||||||
|
if (this.tokenCache.has(token)) {
|
||||||
|
const cachedInfo = this.tokenCache.get(token);
|
||||||
|
if (cachedInfo.expiresAt > Date.now()) {
|
||||||
|
return { valid: true, user: cachedInfo.user };
|
||||||
|
} else {
|
||||||
|
// Token expired, remove from cache
|
||||||
|
this.tokenCache.delete(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Verify token with Keycloak introspection endpoint
|
||||||
|
const response = await axios.post(
|
||||||
|
`${this.config.keycloak.url}/realms/${this.config.keycloak.realm}/protocol/openid-connect/token/introspect`,
|
||||||
|
new URLSearchParams({
|
||||||
|
token: token,
|
||||||
|
client_id: this.config.keycloak.clientId,
|
||||||
|
client_secret: this.config.keycloak.clientSecret,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const introspection = response.data;
|
||||||
|
|
||||||
|
if (!introspection.active) {
|
||||||
|
logger.info("Token is not active");
|
||||||
|
return { valid: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify required roles if configured
|
||||||
|
if (
|
||||||
|
this.config.requiredRoles &&
|
||||||
|
this.config.requiredRoles.length > 0
|
||||||
|
) {
|
||||||
|
const hasRequiredRole = this.checkRoles(
|
||||||
|
introspection,
|
||||||
|
this.config.requiredRoles,
|
||||||
|
);
|
||||||
|
if (!hasRequiredRole) {
|
||||||
|
logger.info("User doesn't have required roles");
|
||||||
|
return { valid: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse token to extract user info
|
||||||
|
const decodedToken = jwt.decode(token);
|
||||||
|
const user = {
|
||||||
|
id: decodedToken.sub,
|
||||||
|
username: decodedToken.preferred_username,
|
||||||
|
email: decodedToken.email,
|
||||||
|
name: decodedToken.name,
|
||||||
|
roles: this.extractRoles(decodedToken),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cache the verified token
|
||||||
|
const expiresAt = introspection.exp * 1000; // Convert to milliseconds
|
||||||
|
this.tokenCache.set(token, { expiresAt, user });
|
||||||
|
|
||||||
|
return { valid: true, user };
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Token verification error:", error.message);
|
||||||
|
return { valid: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract roles from token
|
||||||
|
extractRoles(token) {
|
||||||
|
const roles = [];
|
||||||
|
|
||||||
|
// Extract realm roles
|
||||||
|
if (token.realm_access && token.realm_access.roles) {
|
||||||
|
roles.push(...token.realm_access.roles);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract client roles
|
||||||
|
if (token.resource_access) {
|
||||||
|
for (const client in token.resource_access) {
|
||||||
|
if (token.resource_access[client].roles) {
|
||||||
|
roles.push(
|
||||||
|
...token.resource_access[client].roles.map(
|
||||||
|
(role) => `${client}:${role}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return roles;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user has required roles
|
||||||
|
checkRoles(tokenInfo, requiredRoles) {
|
||||||
|
// Extract roles from token
|
||||||
|
const userRoles = this.extractRoles(tokenInfo);
|
||||||
|
|
||||||
|
// Check if user has any of the required roles
|
||||||
|
return requiredRoles.some((role) => userRoles.includes(role));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Socket.IO middleware for authentication
|
||||||
|
export function createAuthMiddleware(auth) {
|
||||||
|
return async (socket, next) => {
|
||||||
|
const token = socket.handshake.auth.token;
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return next(new Error("Authentication token is required"));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const authResult = await auth.verifyToken(token);
|
||||||
|
|
||||||
|
if (!authResult.valid) {
|
||||||
|
return next(new Error("Invalid authentication token"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach user information to socket
|
||||||
|
socket.user = authResult.user;
|
||||||
|
next();
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("Authentication error:", err);
|
||||||
|
next(new Error("Authentication failed"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
64
src/config.js
Normal file
64
src/config.js
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
// config.js - Configuration handling
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
|
// Configure paths relative to this file
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
const CONFIG_PATH = path.resolve(__dirname, "../config.json");
|
||||||
|
|
||||||
|
// Default configuration
|
||||||
|
const DEFAULT_CONFIG = {
|
||||||
|
server: {
|
||||||
|
port: 8080,
|
||||||
|
cors: {
|
||||||
|
origin: "*",
|
||||||
|
methods: ["GET", "POST"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
auth: {
|
||||||
|
enabled: true,
|
||||||
|
keycloak: {
|
||||||
|
url: "https://auth.tombutcher.work",
|
||||||
|
realm: "master",
|
||||||
|
clientId: "farmcontrol-client",
|
||||||
|
clientSecret: "MBtsENnnYRdJJrc1tLBJZrhnSQqNXqGk",
|
||||||
|
},
|
||||||
|
requiredRoles: ["printer-user"],
|
||||||
|
},
|
||||||
|
database: {
|
||||||
|
url: "mongodb://localhost:27017/farmcontrol",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load or create config file
|
||||||
|
export function loadConfig() {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(CONFIG_PATH)) {
|
||||||
|
const configData = fs.readFileSync(CONFIG_PATH, "utf8");
|
||||||
|
return JSON.parse(configData);
|
||||||
|
} else {
|
||||||
|
fs.writeFileSync(
|
||||||
|
CONFIG_PATH,
|
||||||
|
JSON.stringify(DEFAULT_CONFIG, null, 2),
|
||||||
|
);
|
||||||
|
console.log(`Created default configuration at ${CONFIG_PATH}`);
|
||||||
|
return DEFAULT_CONFIG;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error loading config:", err);
|
||||||
|
return DEFAULT_CONFIG;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save configuration
|
||||||
|
export function saveConfig(config) {
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error saving config:", err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/database/mongo.js
Normal file
15
src/database/mongo.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import mongoose from "mongoose";
|
||||||
|
import { loadConfig } from "../config.js";
|
||||||
|
import log4js from "log4js";
|
||||||
|
// Load configuration
|
||||||
|
const config = loadConfig();
|
||||||
|
|
||||||
|
const logger = log4js.getLogger("Mongo DB");
|
||||||
|
logger.level = config.server.logLevel;
|
||||||
|
|
||||||
|
function dbConnect() {
|
||||||
|
mongoose.connection.once("open", () => logger.info("Database connected."));
|
||||||
|
return mongoose.connect(config.database.url, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { dbConnect };
|
||||||
45
src/database/printer.schema.js
Normal file
45
src/database/printer.schema.js
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import mongoose from "mongoose";
|
||||||
|
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 },
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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 },
|
||||||
|
},
|
||||||
|
{ timestamps: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add virtual id getter
|
||||||
|
printerSchema.virtual("id").get(function () {
|
||||||
|
return this._id.toHexString();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configure JSON serialization to include virtuals
|
||||||
|
printerSchema.set("toJSON", { virtuals: true });
|
||||||
|
|
||||||
|
// Create and export the model
|
||||||
|
export const printerModel = mongoose.model("Printer", printerSchema);
|
||||||
29
src/index.js
Normal file
29
src/index.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
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 log4js from "log4js";
|
||||||
|
// Load configuration
|
||||||
|
const config = loadConfig();
|
||||||
|
|
||||||
|
const logger = log4js.getLogger("FarmControl Server");
|
||||||
|
logger.level = config.server.logLevel;
|
||||||
|
|
||||||
|
// Connect to database
|
||||||
|
dbConnect();
|
||||||
|
|
||||||
|
// Setup Keycloak Integration
|
||||||
|
const keycloakAuth = new KeycloakAuth(config);
|
||||||
|
|
||||||
|
// Create printer manager
|
||||||
|
const printerManager = new PrinterManager(config);
|
||||||
|
const socketManager = new SocketManager(config, printerManager);
|
||||||
|
|
||||||
|
process.on("SIGINT", () => {
|
||||||
|
logger.info("Shutting down...");
|
||||||
|
printerManager.closeAllConnections();
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
82
src/printer/jsonrpc.js
Normal file
82
src/printer/jsonrpc.js
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
// jsonrpc.js - Implementation of JSON-RPC 2.0 protocol for Moonraker communication
|
||||||
|
|
||||||
|
export class JsonRPC {
|
||||||
|
constructor() {
|
||||||
|
this.id_counter = 0;
|
||||||
|
this.methods = {};
|
||||||
|
this.pending_requests = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a unique ID for RPC requests
|
||||||
|
generate_id() {
|
||||||
|
return this.id_counter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register a method to handle incoming notifications/responses
|
||||||
|
register_method(method_name, callback) {
|
||||||
|
this.methods[method_name] = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process incoming messages
|
||||||
|
process_message(message) {
|
||||||
|
if (message.method && this.methods[message.method]) {
|
||||||
|
// Handle method call or notification
|
||||||
|
this.methods[message.method](message.params);
|
||||||
|
} else if (message.id !== undefined) {
|
||||||
|
// Handle response to a previous request
|
||||||
|
const rpc_promise = this.pending_requests[message.id];
|
||||||
|
if (rpc_promise) {
|
||||||
|
if (message.error) {
|
||||||
|
rpc_promise.reject(message.error);
|
||||||
|
} else {
|
||||||
|
rpc_promise.resolve(message.result);
|
||||||
|
}
|
||||||
|
delete this.pending_requests[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, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call a method with parameters
|
||||||
|
call_method_with_kwargs(method, params) {
|
||||||
|
const id = this.generate_id();
|
||||||
|
const request = {
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
method: method,
|
||||||
|
params: params,
|
||||||
|
id: id,
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.pending_requests[id] = { resolve, reject };
|
||||||
|
|
||||||
|
console.log(`Sending JSON-RPC request: ${JSON.stringify(request)}`);
|
||||||
|
// The actual sending of the message is done by the WebSocket connection
|
||||||
|
// This just prepares the message and returns a promise
|
||||||
|
if (this.socket) {
|
||||||
|
this.socket.send(JSON.stringify(request));
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// For external socket handling
|
||||||
|
get_last_request() {
|
||||||
|
const req = this.last_request;
|
||||||
|
this.last_request = null;
|
||||||
|
return req;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Associate a WebSocket with this RPC instance for direct communication
|
||||||
|
set_socket(socket) {
|
||||||
|
this.socket = socket;
|
||||||
|
}
|
||||||
|
}
|
||||||
355
src/printer/printerclient.js
Normal file
355
src/printer/printerclient.js
Normal file
@ -0,0 +1,355 @@
|
|||||||
|
// moonraker-connection.js - Handles connection to a single Moonraker instance
|
||||||
|
|
||||||
|
export class PrinterClient {
|
||||||
|
constructor(printerConfig) {
|
||||||
|
this.id = printerConfig.id;
|
||||||
|
this.printerName = printerConfig.printerName;
|
||||||
|
this.state = printerConfig.state;
|
||||||
|
this.online = printerConfig.online;
|
||||||
|
this.config = printerConfig.moonraker;
|
||||||
|
this.jsonRpc = new JsonRPC();
|
||||||
|
this.socket = null;
|
||||||
|
this.connectionId = null;
|
||||||
|
this.clients = new Set();
|
||||||
|
this.registerEventHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
registerEventHandlers() {
|
||||||
|
// Register event handlers for Moonraker notifications
|
||||||
|
this.jsonRpc.register_method(
|
||||||
|
"notify_gcode_response",
|
||||||
|
this.handleGcodeResponse.bind(this),
|
||||||
|
);
|
||||||
|
this.jsonRpc.register_method(
|
||||||
|
"notify_status_update",
|
||||||
|
this.handleStatusUpdate.bind(this),
|
||||||
|
);
|
||||||
|
this.jsonRpc.register_method(
|
||||||
|
"notify_klippy_disconnected",
|
||||||
|
this.handleKlippyDisconnected.bind(this),
|
||||||
|
);
|
||||||
|
this.jsonRpc.register_method(
|
||||||
|
"notify_klippy_ready",
|
||||||
|
this.handleKlippyReady.bind(this),
|
||||||
|
);
|
||||||
|
this.jsonRpc.register_method(
|
||||||
|
"notify_filelist_changed",
|
||||||
|
this.handleFileListChanged.bind(this),
|
||||||
|
);
|
||||||
|
this.jsonRpc.register_method(
|
||||||
|
"notify_metadata_update",
|
||||||
|
this.handleMetadataUpdate.bind(this),
|
||||||
|
);
|
||||||
|
this.jsonRpc.register_method(
|
||||||
|
"notify_power_changed",
|
||||||
|
this.handlePowerChanged.bind(this),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
console.log(this.config);
|
||||||
|
const { protocol, host, port } = this.config;
|
||||||
|
const wsUrl = `${protocol}://${host}:${port}/websocket`;
|
||||||
|
|
||||||
|
logger.info(`Connecting to Moonraker at ${wsUrl} (${this.id})`);
|
||||||
|
|
||||||
|
this.socket = new WebSocket(wsUrl);
|
||||||
|
|
||||||
|
this.jsonRpc.set_socket(this.socket);
|
||||||
|
|
||||||
|
this.socket.on("open", () => {
|
||||||
|
logger.info(`Connected to Moonraker (${this.printerName})`);
|
||||||
|
this.online = true;
|
||||||
|
this.identifyConnection();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.on("message", (data) => {
|
||||||
|
const message = data.toString();
|
||||||
|
logger.trace(
|
||||||
|
`Received message from Moonraker (${this.printerName}): ${message}`,
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(message);
|
||||||
|
this.jsonRpc.process_message(parsed);
|
||||||
|
|
||||||
|
if (parsed.method != undefined) {
|
||||||
|
//console.log(parsed.params);
|
||||||
|
this.broadcastToClients(message);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(
|
||||||
|
`Error processing message for ${this.printerName}:`,
|
||||||
|
e,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.on("close", () => {
|
||||||
|
logger.info(`Disconnected from Moonraker (${this.printerName})`);
|
||||||
|
this.online = false;
|
||||||
|
this.state = { type: "offline" };
|
||||||
|
this.connectionId = null;
|
||||||
|
// Attempt to reconnect after delay
|
||||||
|
setTimeout(() => this.connect(), 5000);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.on("error", (error) => {
|
||||||
|
logger.error(
|
||||||
|
`Moonraker connection error (${this.printerName}):`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
identifyConnection() {
|
||||||
|
const args = {
|
||||||
|
client_name: "farmcontrol-server",
|
||||||
|
version: "0.1.0",
|
||||||
|
type: "web",
|
||||||
|
url: "https://github.com/printer-bridge",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.config.apiKey) {
|
||||||
|
args.api_key = this.config.apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.jsonRpc
|
||||||
|
.call_method_with_kwargs("server.connection.identify", args)
|
||||||
|
.then((result) => {
|
||||||
|
this.connectionId = result.connection_id;
|
||||||
|
logger.info(
|
||||||
|
`Connection identified with ID: ${this.connectionId} (${this.printerName})`,
|
||||||
|
);
|
||||||
|
this.getServerInfo();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
logger.error(
|
||||||
|
`Error identifying connection (${this.printerName}):`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getServerInfo() {
|
||||||
|
logger.trace("Getting server info");
|
||||||
|
this.jsonRpc
|
||||||
|
.call_method("server.info")
|
||||||
|
.then((result) => {
|
||||||
|
this.online = true;
|
||||||
|
this.state = { type: result.klippy_state };
|
||||||
|
logger.info(
|
||||||
|
"Server:",
|
||||||
|
`Moonraker ${result.moonraker_version} (${this.printerName})`,
|
||||||
|
);
|
||||||
|
if (result.klippy_state === "ready") {
|
||||||
|
this.getKlippyInfo();
|
||||||
|
this.subscribeToStateUpdates();
|
||||||
|
} else {
|
||||||
|
logger.info(
|
||||||
|
`Waiting for Klippy to be ready. Current state: ${result.klippy_state} (${this.printerName})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
logger.error(
|
||||||
|
`Error getting server info (${this.printerName}):`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getKlippyInfo() {
|
||||||
|
this.jsonRpc
|
||||||
|
.call_method("printer.info")
|
||||||
|
.then((result) => {
|
||||||
|
logger.info(
|
||||||
|
`Klippy info for ${this.printerName}: ${result.hostname}, ${result.software_version}`,
|
||||||
|
);
|
||||||
|
if (result.state === "error") {
|
||||||
|
logger.error(
|
||||||
|
`Klippy error for ${this.printerName}: ${result.state_message}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
logger.error(
|
||||||
|
`Error getting Klippy info (${this.printerName}):`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribeToAllUpdates() {
|
||||||
|
const subscriptions = {
|
||||||
|
objects: {
|
||||||
|
gcode_move: [
|
||||||
|
"gcode_position",
|
||||||
|
"speed",
|
||||||
|
"speed_factor",
|
||||||
|
"extrude_factor",
|
||||||
|
],
|
||||||
|
toolhead: ["position", "status"],
|
||||||
|
virtual_sdcard: null,
|
||||||
|
heater_bed: null,
|
||||||
|
extruder: null,
|
||||||
|
fan: null,
|
||||||
|
print_stats: null,
|
||||||
|
motion_report: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
this.jsonRpc
|
||||||
|
.call_method_with_kwargs("printer.objects.subscribe", subscriptions)
|
||||||
|
.then((result) => {
|
||||||
|
logger.info(
|
||||||
|
`Subscribed to all printer updates (${this.printerName})`,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
logger.error(
|
||||||
|
`Error subscribing to all printer updates (${this.printerName}):`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribeToStateUpdates() {
|
||||||
|
const subscriptions = {
|
||||||
|
objects: {
|
||||||
|
toolhead: ["status"],
|
||||||
|
print_stats: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
this.jsonRpc
|
||||||
|
.call_method_with_kwargs("printer.objects.subscribe", subscriptions)
|
||||||
|
.then((result) => {
|
||||||
|
logger.info(
|
||||||
|
`Subscribed to printer state updates (${this.printerName})`,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
logger.error(
|
||||||
|
`Error subscribing to printer state updates (${this.printerName}):`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMessage(message) {
|
||||||
|
if (!this.online) {
|
||||||
|
logger.error(
|
||||||
|
`Cannot send message: Not connected to Moonraker (${this.printerName})`,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.jsonRpc
|
||||||
|
.call_method_with_kwargs(message.method, message.params)
|
||||||
|
.then((result) => {
|
||||||
|
logger.info(`Message sent to (${this.printerName})`);
|
||||||
|
if (result.status != undefined) {
|
||||||
|
const resultStatusMessage = {
|
||||||
|
method: "notify_status_update",
|
||||||
|
params: [result.status],
|
||||||
|
};
|
||||||
|
this.broadcastToClients(
|
||||||
|
JSON.stringify(resultStatusMessage),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
logger.error(
|
||||||
|
`Error sending message to (${this.printerName}):`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
addClient(client) {
|
||||||
|
this.clients.add(client);
|
||||||
|
logger.info(
|
||||||
|
`Client subscribed to ${this.printerName}. Total subscribers: ${this.clients.size}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeClient(client) {
|
||||||
|
this.clients.delete(client);
|
||||||
|
logger.info(
|
||||||
|
`Client unsubscribed from ${this.printerName}. Total subscribers: ${this.clients.size}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcastToClients(message) {
|
||||||
|
for (const client of this.clients) {
|
||||||
|
if (client.conn._readyState === "open") {
|
||||||
|
var jsonMessage = JSON.parse(message);
|
||||||
|
jsonMessage.params = {
|
||||||
|
printerId: this.id,
|
||||||
|
...jsonMessage.params,
|
||||||
|
};
|
||||||
|
client.emit(jsonMessage.method, jsonMessage.params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getStatus() {
|
||||||
|
return {
|
||||||
|
id: this.printerId,
|
||||||
|
name: this.printerName,
|
||||||
|
connected: this.online,
|
||||||
|
connectionId: this.connectionId,
|
||||||
|
host: this.config.host,
|
||||||
|
port: this.config.port,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event handlers
|
||||||
|
handleGcodeResponse(response) {
|
||||||
|
logger.info(`GCode response (${this.printerName}): ${response}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleStatusUpdate(status) {
|
||||||
|
// Process printer status updates
|
||||||
|
if (status.print_stats && status.print_stats.state) {
|
||||||
|
logger.info(
|
||||||
|
`Print state for ${this.printerName}: ${status.print_stats.state}`,
|
||||||
|
);
|
||||||
|
this.state.type = status.print_stats.state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKlippyDisconnected() {
|
||||||
|
this.online = false;
|
||||||
|
logger.info(`Klippy disconnected (${this.printerName})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKlippyReady() {
|
||||||
|
logger.info(`Klippy ready (${this.printerName})`);
|
||||||
|
this.getKlippyInfo();
|
||||||
|
this.subscribeToStateUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFileListChanged(fileInfo) {
|
||||||
|
logger.info(
|
||||||
|
`File list changed for ${this.printerName}:`,
|
||||||
|
fileInfo.action,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMetadataUpdate(metadata) {
|
||||||
|
logger.info(
|
||||||
|
`Metadata updated for ${this.printerName}:`,
|
||||||
|
metadata.filename,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePowerChanged(powerStatus) {
|
||||||
|
logger.info(
|
||||||
|
`Power status changed for ${this.printerName}:`,
|
||||||
|
powerStatus,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
90
src/printer/printermanager.js
Normal file
90
src/printer/printermanager.js
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
// 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 log4js from "log4js";
|
||||||
|
|
||||||
|
// Load configuration
|
||||||
|
const config = loadConfig();
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set up periodic status checking
|
||||||
|
this.startStatusChecking();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`Error initializing printer connections: ${error.message}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async connectToPrinter(printer) {
|
||||||
|
// Create and store the connection
|
||||||
|
const printerClientConnection = new PrinterClient(printer);
|
||||||
|
this.moonrakerConnections.set(printer.id, printerClientConnection);
|
||||||
|
|
||||||
|
// Connect to the printer
|
||||||
|
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
|
||||||
|
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",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close all printer connections
|
||||||
|
closeAllConnections() {
|
||||||
|
for (const printerClientConnection of this.printerClientConnections.values()) {
|
||||||
|
if (printerClientConnection.socket) {
|
||||||
|
printerClientConnection.socket.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
63
src/socket/socketclient.js
Normal file
63
src/socket/socketclient.js
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { WebSocket } from "ws";
|
||||||
|
import { JsonRPC } from "./jsonrpc.js";
|
||||||
|
import log4js from "log4js";
|
||||||
|
// Load configuration
|
||||||
|
import { loadConfig } from "./config.js";
|
||||||
|
|
||||||
|
const config = loadConfig();
|
||||||
|
|
||||||
|
const logger = log4js.getLogger("Moonraker");
|
||||||
|
logger.level = config.server.logLevel;
|
||||||
|
|
||||||
|
export class SocketClient {
|
||||||
|
constructor(socket, printerManager) {
|
||||||
|
this.socket = socket;
|
||||||
|
this.user = socket?.user;
|
||||||
|
this.printerManager = printerManager;
|
||||||
|
|
||||||
|
this.socket.on("bridge.list_printers", (data) => {
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.on("bridge.add_printer", (data, callback) => {
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.on("bridge.remove_printer", (data, callback) => {
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.on("bridge.update_printer", (data, callback) => {
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
const result = printerManager.processCommand(
|
||||||
|
printerId,
|
||||||
|
cleanCommand,
|
||||||
|
);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
logger.error("Missing Printer ID");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.error("Error processing client command:", e);
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.on("disconnect", () => {
|
||||||
|
// Unsubscribe from all printers when client disconnects
|
||||||
|
printerManager.unsubscribeClientFromAll(socket);
|
||||||
|
logger.info("External client disconnected:", socket.user?.username);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
207
src/socket/socketmanager.js
Normal file
207
src/socket/socketmanager.js
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
// server.js - HTTP and Socket.IO server setup
|
||||||
|
import { Server } from "socket.io";
|
||||||
|
import http from "http";
|
||||||
|
import { createAuthMiddleware } from "./auth.js";
|
||||||
|
import log4js from "log4js";
|
||||||
|
// Load configuration
|
||||||
|
import { loadConfig } from "./config.js";
|
||||||
|
|
||||||
|
const config = loadConfig();
|
||||||
|
|
||||||
|
const logger = log4js.getLogger("Server");
|
||||||
|
logger.level = config.server.logLevel;
|
||||||
|
|
||||||
|
export class SocketServer {
|
||||||
|
constructor(config, printerManager, auth) {
|
||||||
|
this.socketClientConnections = new Map();
|
||||||
|
// Create HTTP server
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
res.writeHead(200, { "Content-Type": "text/plain" });
|
||||||
|
res.end("Multi-Printer Bridge Server");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create Socket.IO server
|
||||||
|
const io = new Server(server, {
|
||||||
|
cors: {
|
||||||
|
origin: config.server.corsOrigins || "*",
|
||||||
|
methods: ["GET", "POST"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Apply authentication middleware
|
||||||
|
io.use(createAuthMiddleware(auth));
|
||||||
|
|
||||||
|
// Handle client connections
|
||||||
|
io.on("connection", (socket) => {
|
||||||
|
logger.info("External client connected:", socket.user?.username);
|
||||||
|
this.socketClientConnections.set(socket.id, socket);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start the server
|
||||||
|
server.listen(config.server.port, () => {
|
||||||
|
logger.info(
|
||||||
|
`Multi-Printer Bridge server listening on port ${config.server.port}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { server, io };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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}`);
|
||||||
|
if (callback) {
|
||||||
|
callback({
|
||||||
|
success: true,
|
||||||
|
printer_id: printerId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.warn(`Client failed to subscribe to printer ${printerId}`);
|
||||||
|
if (callback) {
|
||||||
|
callback({
|
||||||
|
success: false,
|
||||||
|
error: {
|
||||||
|
code: -32001,
|
||||||
|
message: `Printer ${printerId} not found`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user