Overhauled server code.
Some checks reported errors
farmcontrol/farmcontrol-server/pipeline/head Something is wrong with the build of this commit
Some checks reported errors
farmcontrol/farmcontrol-server/pipeline/head Something is wrong with the build of this commit
This commit is contained in:
parent
5db74f2c5c
commit
7dbe7da4ee
4
.gitignore
vendored
4
.gitignore
vendored
@ -135,3 +135,7 @@ build
|
|||||||
.pnp.*
|
.pnp.*
|
||||||
|
|
||||||
.nova
|
.nova
|
||||||
|
|
||||||
|
temp_files/*
|
||||||
|
|
||||||
|
dist/*
|
||||||
74
Jenkinsfile
vendored
74
Jenkinsfile
vendored
@ -1,55 +1,59 @@
|
|||||||
node {
|
def buildOnLabel(label, buildCommand) {
|
||||||
env.NODE_ENV = 'production'
|
return {
|
||||||
|
node(label) {
|
||||||
try {
|
stage("Checkout (${label})") {
|
||||||
stage('Checkout') {
|
|
||||||
checkout scm
|
checkout scm
|
||||||
}
|
}
|
||||||
|
|
||||||
stage('Setup Node.js') {
|
stage("Setup Node.js (${label})") {
|
||||||
nodejs(nodeJSInstallationName: 'Node23') {
|
nodejs(nodeJSInstallationName: 'Node23') {
|
||||||
|
if (isUnix()) {
|
||||||
sh 'node -v'
|
sh 'node -v'
|
||||||
sh 'npm -v'
|
sh 'yarn -v'
|
||||||
|
} else {
|
||||||
|
bat 'node -v'
|
||||||
|
bat 'yarn -v'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stage('Install Dependencies') {
|
stage("Install Dependencies (${label})") {
|
||||||
nodejs(nodeJSInstallationName: 'Node23') {
|
nodejs(nodeJSInstallationName: 'Node23') {
|
||||||
sh 'npm ci --include=dev'
|
if (isUnix()) {
|
||||||
|
sh 'yarn install --frozen-lockfile --production=false'
|
||||||
|
} else {
|
||||||
|
bat 'yarn install --frozen-lockfile --production=false'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stage('Build') {
|
stage("Build (${label})") {
|
||||||
nodejs(nodeJSInstallationName: 'Node23') {
|
nodejs(nodeJSInstallationName: 'Node23') {
|
||||||
sh 'npm run build'
|
if (isUnix()) {
|
||||||
sh 'ls -la build || echo "Build directory not found"'
|
sh "NODE_ENV=production ${buildCommand}"
|
||||||
|
} else {
|
||||||
|
bat "set NODE_ENV=production && ${buildCommand}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stage('Verify Build') {
|
stage("Archive Artifacts (${label})") {
|
||||||
sh 'test -d build || (echo "Build directory does not exist" && exit 1)'
|
archiveArtifacts artifacts: 'app_dist/**/*.dmg, app_dist/**/*.exe, app_dist/**/*.AppImage, app_dist/**/*.deb, app_dist/**/*.rpm', fingerprint: true
|
||||||
}
|
}
|
||||||
|
|
||||||
stage('Deploy to printer1') {
|
|
||||||
def remote = [:]
|
|
||||||
remote.name = 'farmcontrolserver'
|
|
||||||
remote.host = 'farmcontrol.tombutcher.local'
|
|
||||||
remote.user = 'ci'
|
|
||||||
remote.password = 'ci'
|
|
||||||
remote.allowAnyHosts = true
|
|
||||||
|
|
||||||
// Copy the build directory to the remote server
|
|
||||||
sshPut remote: remote, from: 'build/*', into: '/srv/farmcontrol-server/'
|
|
||||||
|
|
||||||
// Restart the service using sudo
|
|
||||||
sshCommand remote: remote, command: 'sudo /bin/systemctl restart farmcontrol-server.service'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
echo 'Pipeline completed successfully!'
|
|
||||||
} catch (Exception e) {
|
|
||||||
echo 'Pipeline failed!'
|
|
||||||
throw e
|
|
||||||
} finally {
|
|
||||||
cleanWs()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
parallel(
|
||||||
|
'Windows Build': buildOnLabel('windows', 'yarn build:electron'),
|
||||||
|
'MacOS Build': buildOnLabel('macos', 'yarn build:electron'),
|
||||||
|
'Linux Build': buildOnLabel('ubuntu', 'yarn build:electron')
|
||||||
|
)
|
||||||
|
|
||||||
|
echo 'All parallel stages completed successfully!'
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
echo "Pipeline failed: ${e.message}"
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
|||||||
BIN
assets/farmcontrolhosticon.png
Normal file
BIN
assets/farmcontrolhosticon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 504 KiB |
10
config.json
10
config.json
@ -1,15 +1,17 @@
|
|||||||
{
|
{
|
||||||
"development": {
|
"development": {
|
||||||
"logLevel": "debug",
|
"logLevel": "debug",
|
||||||
"url": "http://192.168.68.53:9090",
|
"url": "https://dev-wss.tombutcher.work",
|
||||||
|
"apiUrl": "https://dev.tombutcher.work/api",
|
||||||
"host": {
|
"host": {
|
||||||
"id": "68a0b5d7c873abe59a995431",
|
"id": "691a1db49ce913faf0e51284",
|
||||||
"authCode": "OHHRijUj-PJnsxx6qAb7hesAlB64SdFBpDrJszComy225KIQ3M3uvMMKhdVCeGfB"
|
"authCode": "FvD3qnNh8FP_xJShlECfYshqQawfD5oPP4xlGOFV2vQIDPRxkAjH4rO6sIgpLucX"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"production": {
|
"production": {
|
||||||
"logLevel": "info",
|
"logLevel": "info",
|
||||||
"url": "192.168.68.53:8001",
|
"url": "https://ws.farmcontrol.app",
|
||||||
|
"apiUrl": "https://api.farmcontrol.app",
|
||||||
"host": {
|
"host": {
|
||||||
"id": "",
|
"id": "",
|
||||||
"authCode": ""
|
"authCode": ""
|
||||||
|
|||||||
BIN
design_files/farmcontrolhosticon.af
Normal file
BIN
design_files/farmcontrolhosticon.af
Normal file
Binary file not shown.
10284
package-lock.json
generated
10284
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
94
package.json
94
package.json
@ -2,47 +2,101 @@
|
|||||||
"name": "farmcontrol-server",
|
"name": "farmcontrol-server",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Connects to moonraker and also manages the socket connection to the printer.",
|
"description": "Connects to moonraker and also manages the socket connection to the printer.",
|
||||||
"main": "src/index.js",
|
"main": "build/index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node build/index.js",
|
"start": "node build/index.js",
|
||||||
"dev": "nodemon src/index.js",
|
"dev": "cross-env NODE_ENV=development nodemon src/index.js",
|
||||||
"dev:electron": "concurrently \"NODE_ENV=development electron .\" \"vite src/electron --port 5173\"",
|
"dev:electron": "concurrently \"cross-env NODE_ENV=development vite src/electron --port 5287 --no-open\" \"cross-env NODE_ENV=development electron src/index.js\"",
|
||||||
"build": "rimraf build && mkdir build && cp -r src/* build/ && cp package.json config.json build/ && npm run build:electron && cp src/electron/preload.js build/electron/ && rm -rf build/electron/App.jsx build/electron/main.jsx build/electron/App.css build/electron/index.css build/electron/FarmControlLogo.jsx build/electron/vite.config.js build/electron/public build/electron/build",
|
"build": "yarn clean && mkdir build && cp -r src/* build/ && cp package.json config.json build/ && yarn build:electron-renderer && cp src/electron/preload.js build/electron/ && rm -rf build/electron/App.jsx build/electron/main.jsx build/electron/App.css build/electron/index.css build/electron/FarmControlLogo.jsx build/electron/vite.config.js build/electron/public build/electron/build",
|
||||||
"build:electron": "vite build src/electron --outDir build/electron",
|
"build:electron-renderer": "vite build src/electron --outDir build/electron",
|
||||||
|
"build:electron": "yarn build && electron-builder",
|
||||||
"clean": "rimraf build"
|
"clean": "rimraf build"
|
||||||
},
|
},
|
||||||
"author": "Tom Butcher",
|
"author": "Tom Butcher",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^6.0.0",
|
"@ant-design/icons": "^6.1.0",
|
||||||
"ant-design": "^1.0.0",
|
"ant-design": "^1.0.0",
|
||||||
"antd": "^5.27.0",
|
"antd": "^5.28.0",
|
||||||
"axios": "^1.8.4",
|
"axios": "^1.13.2",
|
||||||
|
"canvas": "^3.2.0",
|
||||||
"etcd3": "^1.1.2",
|
"etcd3": "^1.1.2",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
"ipp": "^2.0.1",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"keycloak-connect": "^26.1.1",
|
"keycloak-connect": "^26.1.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"log4js": "^6.9.1",
|
"log4js": "^6.9.1",
|
||||||
"mongoose": "^8.13.2",
|
"mongoose": "^9.0.0",
|
||||||
|
"node-cache": "^5.1.2",
|
||||||
|
"node-thermal-printer": "^4.5.0",
|
||||||
|
"pdf-to-img": "^5.0.0",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
|
"sharp": "^0.34.5",
|
||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
"socket.io-client": "^4.8.1",
|
"socket.io-client": "^4.8.1",
|
||||||
"ws": "^8.18.1"
|
"ws": "^8.18.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@electron/rebuild": "^4.0.1",
|
||||||
"concurrently": "^9.2.0",
|
"@vitejs/plugin-react": "^5.1.1",
|
||||||
"electron": "^37.2.6",
|
"concurrently": "^9.2.1",
|
||||||
"jest": "^29.7.0",
|
"cross-env": "^10.1.0",
|
||||||
"nodemon": "^3.1.9",
|
"electron": "^38.7.1",
|
||||||
"react": "^18.2.0",
|
"electron-builder": "^26.0.12",
|
||||||
"react-dom": "^18.2.0",
|
"jest": "^30.2.0",
|
||||||
"rimraf": "^5.0.5",
|
"nodemon": "^3.1.11",
|
||||||
"supertest": "^6.3.4",
|
"react": "^19.2.0",
|
||||||
"vite": "^5.0.12",
|
"react-dom": "^19.2.0",
|
||||||
|
"rimraf": "^6.1.2",
|
||||||
|
"supertest": "^7.1.4",
|
||||||
|
"vite": "^7.2.4",
|
||||||
"vite-plugin-svgo": "^2.0.0",
|
"vite-plugin-svgo": "^2.0.0",
|
||||||
"vite-plugin-svgr": "^4.5.0"
|
"vite-plugin-svgr": "^4.5.0"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"appId": "com.tombutcher.farmcontrolserver",
|
||||||
|
"productName": "Farm Control Server",
|
||||||
|
"executableName": "farmcontrol-server",
|
||||||
|
"icon": "assets/farmcontrolhosticon.png",
|
||||||
|
"directories": {
|
||||||
|
"output": "app_dist"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"build/**/*",
|
||||||
|
"node_modules/**/*"
|
||||||
|
],
|
||||||
|
"mac": {
|
||||||
|
"target": "dmg"
|
||||||
|
},
|
||||||
|
"win": {
|
||||||
|
"target": "nsis"
|
||||||
|
},
|
||||||
|
"linux": {
|
||||||
|
"target": [
|
||||||
|
"AppImage",
|
||||||
|
"deb",
|
||||||
|
"rpm"
|
||||||
|
],
|
||||||
|
"category": "Utility",
|
||||||
|
"maintainer": "Tom Butcher <tom@tombutcher.work>",
|
||||||
|
"executableName": "farmcontrol-server"
|
||||||
|
},
|
||||||
|
"deb": {
|
||||||
|
"priority": "optional",
|
||||||
|
"afterInstall": "packaging/linux/after-install.sh",
|
||||||
|
"afterRemove": "packaging/linux/after-remove.sh"
|
||||||
|
},
|
||||||
|
"rpm": {
|
||||||
|
"afterInstall": "packaging/linux/after-install.sh",
|
||||||
|
"afterRemove": "packaging/linux/after-remove.sh"
|
||||||
|
},
|
||||||
|
"extraResources": [
|
||||||
|
{
|
||||||
|
"from": "packaging/linux/farmcontrol-server.service",
|
||||||
|
"to": "farmcontrol-server.service"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
22
packaging/linux/after-install.sh
Executable file
22
packaging/linux/after-install.sh
Executable file
@ -0,0 +1,22 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Copy systemd service file
|
||||||
|
# The installation directory is usually /opt/Farm Control Server
|
||||||
|
# but we can try to find it if it's different.
|
||||||
|
APP_DIR="/opt/Farm Control Server"
|
||||||
|
if [ ! -d "$APP_DIR" ]; then
|
||||||
|
# Fallback to slugified name
|
||||||
|
APP_DIR="/opt/farmcontrol-server"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cp "$APP_DIR/resources/farmcontrol-server.service" /etc/systemd/system/farmcontrol-server.service
|
||||||
|
|
||||||
|
# Reload systemd to recognize the new service
|
||||||
|
systemctl daemon-reload
|
||||||
|
|
||||||
|
# Enable the service to start on boot
|
||||||
|
systemctl enable farmcontrol-server.service
|
||||||
|
|
||||||
|
# Start the service
|
||||||
|
systemctl start farmcontrol-server.service
|
||||||
|
|
||||||
14
packaging/linux/after-remove.sh
Executable file
14
packaging/linux/after-remove.sh
Executable file
@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Stop the service
|
||||||
|
systemctl stop farmcontrol-server.service
|
||||||
|
|
||||||
|
# Disable the service
|
||||||
|
systemctl disable farmcontrol-server.service
|
||||||
|
|
||||||
|
# Remove the systemd service file
|
||||||
|
rm -f /etc/systemd/system/farmcontrol-server.service
|
||||||
|
|
||||||
|
# Reload systemd
|
||||||
|
systemctl daemon-reload
|
||||||
|
|
||||||
16
packaging/linux/farmcontrol-server.service
Normal file
16
packaging/linux/farmcontrol-server.service
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Farm Control Server
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/bin/farmcontrol-server --headless
|
||||||
|
Restart=always
|
||||||
|
User=root
|
||||||
|
Group=root
|
||||||
|
Environment=NODE_ENV=production
|
||||||
|
WorkingDirectory=/opt/Farm Control Server
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
|
||||||
524
src/documentprinter/documentprinterclient.js
Normal file
524
src/documentprinter/documentprinterclient.js
Normal file
@ -0,0 +1,524 @@
|
|||||||
|
// documentprinterclient.js - Handles connection to a single document printer
|
||||||
|
import { loadConfig } from "../config.js";
|
||||||
|
import log4js from "log4js";
|
||||||
|
import CupsPrinterInterface from "./interfaces/cupsinterface.js";
|
||||||
|
import ReceiptInterface from "./interfaces/receiptinterface.js";
|
||||||
|
import { sendIPC } from "../electron/ipc.js";
|
||||||
|
// Load configuration
|
||||||
|
const config = loadConfig();
|
||||||
|
|
||||||
|
const logger = log4js.getLogger("Document Printer Client");
|
||||||
|
logger.level = config.logLevel;
|
||||||
|
|
||||||
|
export class DocumentPrinterClient {
|
||||||
|
constructor(
|
||||||
|
documentPrinter = {
|
||||||
|
connection: { interface: "cups", host: "localhost", port: 9100 },
|
||||||
|
},
|
||||||
|
documentPrinterManager
|
||||||
|
) {
|
||||||
|
this.id = documentPrinter._id;
|
||||||
|
this.documentPrinter = documentPrinter;
|
||||||
|
this.connection = documentPrinter.connection;
|
||||||
|
this.queue = [];
|
||||||
|
this.documentPrinterManager = documentPrinterManager;
|
||||||
|
this.currentJob = null;
|
||||||
|
this.socketClient = documentPrinterManager.socketClient;
|
||||||
|
this.interface = documentPrinter.connection.interface || "cups"; // cups, receipt, or os
|
||||||
|
this.state = { type: "offline" };
|
||||||
|
this.isOnline = documentPrinter.online || false;
|
||||||
|
this.shouldReconnect = true;
|
||||||
|
this.isProcessingQueue = false;
|
||||||
|
this.eventUpdateInterval = null;
|
||||||
|
this.initializeInterface();
|
||||||
|
this.registerEventHandlers();
|
||||||
|
this.subscribeToActions();
|
||||||
|
this.subscribeToObjectUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeInterface() {
|
||||||
|
logger.info(
|
||||||
|
`Initializing ${this.interface} interface for document printer ${this.id}`
|
||||||
|
);
|
||||||
|
switch (this.interface) {
|
||||||
|
case "cups":
|
||||||
|
this.printerInterface = new CupsPrinterInterface(this);
|
||||||
|
logger.debug("Cups printer interface initialized");
|
||||||
|
break;
|
||||||
|
case "epsonReceipt":
|
||||||
|
this.printerInterface = new ReceiptInterface(this);
|
||||||
|
logger.debug("Epson receipt printer interface initialized");
|
||||||
|
break;
|
||||||
|
case "starReceipt":
|
||||||
|
this.printerInterface = new ReceiptInterface(this);
|
||||||
|
logger.debug("Star receipt printer interface initialized");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
logger.error(`Unknown interface type: ${this.interface}`);
|
||||||
|
this.printerInterface = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateDocumentPrinter(data) {
|
||||||
|
logger.debug(`Updating document printer ${this.id} with data...`);
|
||||||
|
sendIPC("setDocumentPrinter", { _id: this.id, ...data });
|
||||||
|
|
||||||
|
// Check for connection changes before updating
|
||||||
|
|
||||||
|
if (data?.connection) {
|
||||||
|
const oldConnection = this.connection || {};
|
||||||
|
const newConnection = data?.connection || {};
|
||||||
|
|
||||||
|
const hostChanged =
|
||||||
|
newConnection.host != null && newConnection.host !== oldConnection.host;
|
||||||
|
const protocolChanged =
|
||||||
|
newConnection.protocol != null &&
|
||||||
|
newConnection.protocol !== oldConnection.protocol;
|
||||||
|
const interfaceChanged =
|
||||||
|
newConnection.interface != null &&
|
||||||
|
newConnection.interface !== oldConnection.interface;
|
||||||
|
const portChanged = newConnection?.port !== oldConnection?.port;
|
||||||
|
|
||||||
|
const connectionChanged =
|
||||||
|
hostChanged || interfaceChanged || portChanged || protocolChanged;
|
||||||
|
|
||||||
|
logger.debug(`Connection changed: ${connectionChanged}`);
|
||||||
|
logger.debug(`Host changed: ${hostChanged}`);
|
||||||
|
logger.debug(`Interface changed: ${interfaceChanged}`);
|
||||||
|
logger.debug(`Port changed: ${portChanged}`);
|
||||||
|
logger.debug(`Protocol changed: ${protocolChanged}`);
|
||||||
|
|
||||||
|
// Update document printer data
|
||||||
|
this.documentPrinter = { ...this.documentPrinter, ...data };
|
||||||
|
|
||||||
|
// Update interface if it changed
|
||||||
|
if (interfaceChanged) {
|
||||||
|
this.interface = newConnection.interface;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-initialize only if connection properties changed
|
||||||
|
if (connectionChanged) {
|
||||||
|
this.connection = newConnection;
|
||||||
|
await this.printerInterface.disconnect();
|
||||||
|
this.initializeInterface();
|
||||||
|
await this.reconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerEventHandlers() {
|
||||||
|
// Register event handlers for document printer notifications
|
||||||
|
// This can be extended as needed
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribeToActions() {
|
||||||
|
this.socketClient.subscribeToObjectActions({
|
||||||
|
objectType: "documentPrinter",
|
||||||
|
_id: this.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribeToObjectUpdates() {
|
||||||
|
this.socketClient.subscribeToObjectUpdates({
|
||||||
|
objectType: "documentPrinter",
|
||||||
|
_id: this.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async connect() {
|
||||||
|
logger.info(
|
||||||
|
`Connecting to document printer ${this.id} (${this.interface})`
|
||||||
|
);
|
||||||
|
|
||||||
|
clearTimeout(this.reconnectTimeout);
|
||||||
|
// Always stop the event update interval when connecting
|
||||||
|
clearInterval(this.eventUpdateInterval);
|
||||||
|
this.eventUpdateInterval = null;
|
||||||
|
this.state = { type: "connecting", message: null };
|
||||||
|
this.isOnline = false;
|
||||||
|
await this.updateDocumentPrinterState();
|
||||||
|
|
||||||
|
if (!this.printerInterface) {
|
||||||
|
logger.error(
|
||||||
|
`Cannot connect: No interface initialized for ${this.interface}`
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.printerInterface.connect();
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
logger.error(
|
||||||
|
`Error connecting to document printer ${this.documentPrinter.name}:`,
|
||||||
|
result.error
|
||||||
|
);
|
||||||
|
this.isOnline = false;
|
||||||
|
this.state = { type: "offline", message: result.error };
|
||||||
|
await this.updateDocumentPrinterState();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
logger.info(
|
||||||
|
`Connected to document printer ${this.documentPrinter.name} (${this.interface})`
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async reconnect() {
|
||||||
|
if (this.isOnline == true) {
|
||||||
|
logger.info(
|
||||||
|
`Disconnecting from document printer ${this.documentPrinter.name} before reconnecting...`
|
||||||
|
);
|
||||||
|
await this.disconnect();
|
||||||
|
}
|
||||||
|
logger.info(
|
||||||
|
`Reconnecting to document printer ${this.documentPrinter.name}`
|
||||||
|
);
|
||||||
|
this.shouldReconnect = true;
|
||||||
|
const connectResult = await this.connect();
|
||||||
|
if (connectResult == false) {
|
||||||
|
logger.error(
|
||||||
|
`Error reconnecting to document printer ${this.documentPrinter.name}:`,
|
||||||
|
connectResult.error
|
||||||
|
);
|
||||||
|
if (this.shouldReconnect) {
|
||||||
|
// Attempt to reconnect after delay
|
||||||
|
setTimeout(() => this.reconnect(), 30000);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const initializeResult = await this.initialize();
|
||||||
|
if (initializeResult == false) {
|
||||||
|
logger.error(
|
||||||
|
`Error initializing document printer ${this.documentPrinter.name}:`,
|
||||||
|
initializeResult.error
|
||||||
|
);
|
||||||
|
if (this.shouldReconnect) {
|
||||||
|
// Attempt to reconnect after delay
|
||||||
|
setTimeout(() => this.reconnect(), 30000);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async initialize() {
|
||||||
|
logger.info("Running document printer initialization...");
|
||||||
|
this.state = { type: "initializing", message: null };
|
||||||
|
this.isOnline = true;
|
||||||
|
this.connectedAt = new Date();
|
||||||
|
await this.updateDocumentPrinterState();
|
||||||
|
if (this.printerInterface && this.printerInterface.initialize) {
|
||||||
|
const result = await this.printerInterface.initialize();
|
||||||
|
if (result.error) {
|
||||||
|
logger.error(
|
||||||
|
`Error initializing document printer ${this.documentPrinter.name}:`,
|
||||||
|
result.error
|
||||||
|
);
|
||||||
|
this.state = { type: "offline", message: result.error };
|
||||||
|
await this.updateDocumentPrinterState();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.state = { type: "standby", message: null };
|
||||||
|
await this.updateDocumentPrinterState();
|
||||||
|
this.eventUpdateInterval = setInterval(
|
||||||
|
this.handleEventUpdate.bind(this),
|
||||||
|
3000
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleEventUpdate() {
|
||||||
|
if (this.printerInterface && this.printerInterface.retrieveStatus) {
|
||||||
|
try {
|
||||||
|
//await this.printerInterface.retrieveStatus();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`Error retrieving status for document printer ${this.documentPrinter.name}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateDocumentPrinterState() {
|
||||||
|
try {
|
||||||
|
// Update state in database or via socket client
|
||||||
|
// This can be implemented based on your database structure
|
||||||
|
this.socketClient.editObject({
|
||||||
|
_id: this.id,
|
||||||
|
objectType: "documentPrinter",
|
||||||
|
updateData: {
|
||||||
|
online: this.isOnline,
|
||||||
|
state: this.state,
|
||||||
|
connectedAt: this.connectedAt,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to update document printer state:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateJobState(jobId, state) {
|
||||||
|
logger.info(`Updating job state for ${jobId}`);
|
||||||
|
await this.socketClient.editObject({
|
||||||
|
_id: jobId,
|
||||||
|
objectType: "documentJob",
|
||||||
|
updateData: { state: state },
|
||||||
|
});
|
||||||
|
logger.info(`Updated job state for ${jobId}:`, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deployDocumentJob(documentJob) {
|
||||||
|
logger.info(
|
||||||
|
`Deploying document job ${documentJob._id} to ${this.documentPrinter.name}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!this.isOnline) {
|
||||||
|
logger.error(
|
||||||
|
`Cannot deploy job: Document printer not connected (${this.documentPrinter.name})`
|
||||||
|
);
|
||||||
|
return { error: "Document printer not connected" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.printerInterface) {
|
||||||
|
logger.error(
|
||||||
|
`Cannot deploy job: No interface initialized (${this.documentPrinter.name})`
|
||||||
|
);
|
||||||
|
return { error: "No interface initialized" };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (this.printerInterface.deploy) {
|
||||||
|
await this.updateJobState(documentJob._id, { type: "deploying" });
|
||||||
|
const documentTemplate = await this.socketClient.getObject({
|
||||||
|
objectType: "documentTemplate",
|
||||||
|
_id: documentJob.documentTemplate._id,
|
||||||
|
});
|
||||||
|
await this.updateJobState(documentJob._id, {
|
||||||
|
type: "deploying",
|
||||||
|
progress: 0.25,
|
||||||
|
});
|
||||||
|
const object = await this.socketClient.getObject({
|
||||||
|
objectType: documentJob.objectType,
|
||||||
|
_id: documentJob.object._id,
|
||||||
|
});
|
||||||
|
await this.updateJobState(documentJob._id, {
|
||||||
|
type: "deploying",
|
||||||
|
progress: 0.5,
|
||||||
|
});
|
||||||
|
if (!documentTemplate) {
|
||||||
|
logger.error(
|
||||||
|
`Document template not found for job ${documentJob._id}`
|
||||||
|
);
|
||||||
|
return { error: "Document template not found" };
|
||||||
|
}
|
||||||
|
const pdfObj = await this.socketClient.renderTemplatePDF({
|
||||||
|
_id: documentTemplate._id,
|
||||||
|
content: documentTemplate.content,
|
||||||
|
object: object,
|
||||||
|
});
|
||||||
|
await this.updateJobState(documentJob._id, {
|
||||||
|
type: "deploying",
|
||||||
|
progress: 0.75,
|
||||||
|
});
|
||||||
|
if (!pdfObj) {
|
||||||
|
logger.error(
|
||||||
|
`Failed to render document template for job ${documentJob._id}`
|
||||||
|
);
|
||||||
|
return { error: "Failed to render document template" };
|
||||||
|
}
|
||||||
|
const result = await this.printerInterface.deploy(
|
||||||
|
documentJob,
|
||||||
|
pdfObj.pdf
|
||||||
|
);
|
||||||
|
await this.updateJobState(documentJob._id, {
|
||||||
|
type: "deploying",
|
||||||
|
progress: 1.0,
|
||||||
|
});
|
||||||
|
logger.info(
|
||||||
|
`Deployed document job ${documentJob._id} to ${this.documentPrinter.name}`
|
||||||
|
);
|
||||||
|
await this.updateJobState(documentJob._id, {
|
||||||
|
type: "queued",
|
||||||
|
progress: null,
|
||||||
|
});
|
||||||
|
// Only add job to queue if it's not already there
|
||||||
|
if (!this.queue.includes(documentJob._id)) {
|
||||||
|
this.queue.push(documentJob._id);
|
||||||
|
} else {
|
||||||
|
logger.warn(
|
||||||
|
`Job ${documentJob._id} is already in the queue for ${this.documentPrinter.name}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.startQueue();
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
logger.error(
|
||||||
|
`Interface ${this.interface} does not support deployDocumentJob`
|
||||||
|
);
|
||||||
|
return { error: "Interface does not support this operation" };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`Error deploying document job to ${this.documentPrinter.name}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
await this.updateJobState(documentJob._id, {
|
||||||
|
type: "error",
|
||||||
|
progress: null,
|
||||||
|
message: error.message || "Failed to deploy document job",
|
||||||
|
});
|
||||||
|
return { error: error.message || "Failed to deploy document job" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async startQueue() {
|
||||||
|
if (!this.isOnline) {
|
||||||
|
logger.error(
|
||||||
|
`Cannot start queue: Document printer not connected (${this.documentPrinter.name})`
|
||||||
|
);
|
||||||
|
return { error: "Document printer not connected" };
|
||||||
|
}
|
||||||
|
if (!this.printerInterface) {
|
||||||
|
logger.error(
|
||||||
|
`Cannot start queue: No interface initialized (${this.documentPrinter.name})`
|
||||||
|
);
|
||||||
|
return { error: "No interface initialized" };
|
||||||
|
}
|
||||||
|
// Prevent concurrent queue processing
|
||||||
|
if (this.isProcessingQueue) {
|
||||||
|
logger.debug(
|
||||||
|
`Queue is already being processed for ${this.documentPrinter.name}`
|
||||||
|
);
|
||||||
|
return { info: "Queue is already being processed" };
|
||||||
|
}
|
||||||
|
if (this.state.type == "standby") {
|
||||||
|
logger.info(`Starting queue for ${this.documentPrinter.name}`);
|
||||||
|
|
||||||
|
await this.runQueue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async runQueue() {
|
||||||
|
if (!this.isOnline) {
|
||||||
|
logger.error(
|
||||||
|
`Cannot print next job: Document printer not connected (${this.documentPrinter.name})`
|
||||||
|
);
|
||||||
|
return { error: "Document printer not connected" };
|
||||||
|
}
|
||||||
|
if (!this.printerInterface) {
|
||||||
|
logger.error(
|
||||||
|
`Cannot print next job: No interface initialized (${this.documentPrinter.name})`
|
||||||
|
);
|
||||||
|
return { error: "No interface initialized" };
|
||||||
|
}
|
||||||
|
if (this.state.type != "standby") {
|
||||||
|
logger.error(
|
||||||
|
`Cannot print next job: Document printer not in standby mode (${this.documentPrinter.name})`
|
||||||
|
);
|
||||||
|
return { error: "Document printer not in standby mode" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent concurrent queue processing
|
||||||
|
if (this.isProcessingQueue) {
|
||||||
|
logger.debug(
|
||||||
|
`Queue is already being processed for ${this.documentPrinter.name}`
|
||||||
|
);
|
||||||
|
return { info: "Queue is already being processed" };
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isProcessingQueue = true;
|
||||||
|
logger.info(`Starting to print jobs for ${this.documentPrinter.name}`);
|
||||||
|
this.state = { type: "printing", message: null };
|
||||||
|
await this.updateDocumentPrinterState();
|
||||||
|
try {
|
||||||
|
// Process all jobs in the queue using a loop instead of recursion
|
||||||
|
while (this.queue.length > 0) {
|
||||||
|
// Re-check connection status before each job
|
||||||
|
if (!this.isOnline) {
|
||||||
|
logger.error(
|
||||||
|
`Printer went offline while printing (${this.documentPrinter.name})`
|
||||||
|
);
|
||||||
|
this.state = {
|
||||||
|
type: "offline",
|
||||||
|
message: "Connection lost during printing",
|
||||||
|
};
|
||||||
|
await this.updateDocumentPrinterState();
|
||||||
|
return { error: "Document printer not connected" };
|
||||||
|
}
|
||||||
|
if (!this.printerInterface) {
|
||||||
|
logger.error(
|
||||||
|
`Printer interface lost while printing (${this.documentPrinter.name})`
|
||||||
|
);
|
||||||
|
this.state = {
|
||||||
|
type: "offline",
|
||||||
|
message: "Interface lost during printing",
|
||||||
|
};
|
||||||
|
await this.updateDocumentPrinterState();
|
||||||
|
return { error: "No interface initialized" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the next job ID but don't remove it yet
|
||||||
|
const jobId = this.queue[0];
|
||||||
|
if (!jobId) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.info(`Printing job ${jobId} for ${this.documentPrinter.name}`);
|
||||||
|
await this.updateJobState(jobId, { type: "printing" });
|
||||||
|
|
||||||
|
await this.printerInterface.print(jobId);
|
||||||
|
logger.info(
|
||||||
|
`Successfully printed job ${jobId} for ${this.documentPrinter.name}`
|
||||||
|
);
|
||||||
|
// Only remove job from queue after successful printing
|
||||||
|
this.queue.shift();
|
||||||
|
await this.updateJobState(jobId, { type: "complete" });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`Error printing job ${jobId} for ${this.documentPrinter.name}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove failed job from queue to prevent infinite retry loop
|
||||||
|
// You may want to implement retry logic or error handling here
|
||||||
|
this.queue.shift();
|
||||||
|
await this.updateJobState(jobId, { type: "failed" });
|
||||||
|
// Continue with next job even if one fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`Finished printing all jobs for ${this.documentPrinter.name}`
|
||||||
|
);
|
||||||
|
this.state = { type: "standby", message: null };
|
||||||
|
await this.updateDocumentPrinterState();
|
||||||
|
return true;
|
||||||
|
} finally {
|
||||||
|
// Always reset the processing flag, even if there was an error
|
||||||
|
this.isProcessingQueue = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async disconnect() {
|
||||||
|
logger.info(`Disconnecting from ${this.documentPrinter.name}`);
|
||||||
|
this.shouldReconnect = false;
|
||||||
|
// Always stop the event update interval when disconnecting
|
||||||
|
clearInterval(this.eventUpdateInterval);
|
||||||
|
this.eventUpdateInterval = null;
|
||||||
|
if (this.printerInterface && this.printerInterface.disconnect) {
|
||||||
|
await this.printerInterface.disconnect();
|
||||||
|
}
|
||||||
|
this.isOnline = false;
|
||||||
|
this.state = { type: "offline" };
|
||||||
|
this.isProcessingQueue = false;
|
||||||
|
this.queue = []; // Clear queue on disconnect
|
||||||
|
await this.updateDocumentPrinterState();
|
||||||
|
clearTimeout(this.reconnectTimeout);
|
||||||
|
logger.info(`Successfully disconnected from ${this.documentPrinter.name}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,14 +1,109 @@
|
|||||||
import { loadConfig } from "../config.js";
|
import { loadConfig } from "../config.js";
|
||||||
import log4js from "log4js";
|
import log4js from "log4js";
|
||||||
|
import { sendIPC } from "../electron/ipc.js";
|
||||||
|
import { DocumentPrinterClient } from "./documentprinterclient.js";
|
||||||
|
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
|
|
||||||
const logger = log4js.getLogger("Document Printer Manager");
|
const logger = log4js.getLogger("Document Printer Manager");
|
||||||
logger.level = config.server.logLevel;
|
logger.level = config.logLevel;
|
||||||
|
|
||||||
export class DocumentPrinterManager {
|
export class DocumentPrinterManager {
|
||||||
constructor(socketClient) {
|
constructor(socketClient) {
|
||||||
this.socketClient = socketClient;
|
this.socketClient = socketClient;
|
||||||
this.documentPrinterClients = new Map();
|
this.documentPrinterClients = new Map();
|
||||||
|
this.documentPrinters = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async reloadDocumentPrinters() {
|
||||||
|
logger.info("Reloading document printers...");
|
||||||
|
try {
|
||||||
|
this.documentPrinters = await this.socketClient.listObjects({
|
||||||
|
objectType: "documentPrinter",
|
||||||
|
filter: { host: this.socketClient.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
sendIPC("setDocumentPrinters", this.documentPrinters);
|
||||||
|
|
||||||
|
// Remove printer clients that are no longer in the printers list
|
||||||
|
const documentPrinterIds = this.documentPrinters.map(
|
||||||
|
(documentPrinter) => documentPrinter._id
|
||||||
|
);
|
||||||
|
for (const [
|
||||||
|
documentPrinterId,
|
||||||
|
documentPrinterClient,
|
||||||
|
] of this.documentPrinterClients.entries()) {
|
||||||
|
if (!documentPrinterIds.includes(documentPrinterId)) {
|
||||||
|
// Close the connection before removing
|
||||||
|
documentPrinterClient.shouldReconnect = false;
|
||||||
|
documentPrinterClient.disconnect();
|
||||||
|
this.documentPrinterClients.delete(documentPrinterId);
|
||||||
|
logger.info(
|
||||||
|
`Removed document printer client for printer ID: ${documentPrinterId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new printer clients for printers not in the documentPrinterClients map
|
||||||
|
for (const documentPrinter of this.documentPrinters) {
|
||||||
|
const documentPrinterId = documentPrinter._id;
|
||||||
|
if (!this.documentPrinterClients.has(documentPrinterId)) {
|
||||||
|
const documentPrinterClient = new DocumentPrinterClient(
|
||||||
|
documentPrinter,
|
||||||
|
this
|
||||||
|
);
|
||||||
|
this.documentPrinterClients.set(
|
||||||
|
documentPrinterId,
|
||||||
|
documentPrinterClient
|
||||||
|
);
|
||||||
|
await documentPrinterClient.reconnect();
|
||||||
|
logger.info(
|
||||||
|
`Added document printer client for printer ID: ${documentPrinterId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Failed to update document printers:", error);
|
||||||
|
this.documentPrinters = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleDocumentPrinterAction(id, action, callback) {
|
||||||
|
logger.debug("Running document printer action...", action);
|
||||||
|
const documentPrinter = this.getDocumentPrinterClient(id);
|
||||||
|
switch (action.type) {
|
||||||
|
case "deploy":
|
||||||
|
const deployResult = await documentPrinter.deployDocumentJob(
|
||||||
|
action.data
|
||||||
|
);
|
||||||
|
callback(deployResult);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback({ error: "Unknown command." });
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleDocumentPrinterUpdate(id, data) {
|
||||||
|
logger.debug("Handling document printer update for id:", id);
|
||||||
|
const documentPrinter = this.getDocumentPrinterClient(id);
|
||||||
|
if (documentPrinter) {
|
||||||
|
documentPrinter.updateDocumentPrinter(data.object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getDocumentPrinterClient(documentPrinterId) {
|
||||||
|
return this.documentPrinterClients.get(documentPrinterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllDocumentPrinterClients() {
|
||||||
|
return this.documentPrinterClients.values();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close all printer connections
|
||||||
|
closeAllConnections() {
|
||||||
|
for (const documentPrinterClient of this.documentPrinterClients.values()) {
|
||||||
|
documentPrinterClient.shouldReconnect = false;
|
||||||
|
documentPrinterClient.disconnect();
|
||||||
|
}
|
||||||
|
this.documentPrinterClients.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
306
src/documentprinter/interfaces/cupsinterface.js
Normal file
306
src/documentprinter/interfaces/cupsinterface.js
Normal file
@ -0,0 +1,306 @@
|
|||||||
|
// cupsprinterinterface.js - CUPS printer interface implementation
|
||||||
|
import log4js from "log4js";
|
||||||
|
import { loadConfig } from "../../config.js";
|
||||||
|
import ipp from "ipp";
|
||||||
|
|
||||||
|
const config = loadConfig();
|
||||||
|
const logger = log4js.getLogger("CUPS Printer Interface");
|
||||||
|
logger.level = config.logLevel;
|
||||||
|
|
||||||
|
export default class CupsInterface {
|
||||||
|
constructor(documentPrinterClient) {
|
||||||
|
this.documentPrinterClient = documentPrinterClient;
|
||||||
|
this.host = documentPrinterClient.connection.host;
|
||||||
|
this.port = documentPrinterClient.connection.port || 631;
|
||||||
|
this.interface = documentPrinterClient.connection.interface || "cups";
|
||||||
|
this.protocol = documentPrinterClient.connection.protocol;
|
||||||
|
this.name = documentPrinterClient.documentPrinter.name;
|
||||||
|
this.isConnected = false;
|
||||||
|
this.cupsPrinter = null;
|
||||||
|
this.pdfs = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the IPP printer URL from connection settings
|
||||||
|
*/
|
||||||
|
buildPrinterUrl() {
|
||||||
|
var hostWithPort = `${this.host}:${this.port}`;
|
||||||
|
var path = "/";
|
||||||
|
if (this.host.includes("/")) {
|
||||||
|
const parts = this.host.split("/");
|
||||||
|
var hostName = parts[0];
|
||||||
|
if (hostName.includes(":")) {
|
||||||
|
const portHost = hostName.split(":");
|
||||||
|
hostName = portHost[0];
|
||||||
|
}
|
||||||
|
hostWithPort = `${hostName}:${this.port}`;
|
||||||
|
path += parts.slice(1).join("/");
|
||||||
|
}
|
||||||
|
switch (this.protocol) {
|
||||||
|
case "ipp":
|
||||||
|
return `ipp://${hostWithPort}${path}`;
|
||||||
|
case "http":
|
||||||
|
return `http://${hostWithPort}${path}`;
|
||||||
|
default:
|
||||||
|
logger.warn(`Unknown protocol ${this.protocol}, defaulting to ipp.`);
|
||||||
|
return `ipp://${hostWithPort}${path}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Promisify IPP execute method
|
||||||
|
*/
|
||||||
|
async executeIPP(operation, message) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.cupsPrinter.execute(operation, message, (err, res) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve(res);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async connect() {
|
||||||
|
logger.info(`Connecting to CUPS printer ${this.name}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.cupsPrinterUrl = this.buildPrinterUrl();
|
||||||
|
logger.debug(`Printer URL: ${this.cupsPrinterUrl}`);
|
||||||
|
|
||||||
|
// Create IPP printer instance
|
||||||
|
this.cupsPrinter = ipp.Printer(this.cupsPrinterUrl);
|
||||||
|
|
||||||
|
// Test connection by getting printer attributes
|
||||||
|
const getPrinterAttributes = {
|
||||||
|
"operation-attributes-tag": {
|
||||||
|
"requested-attributes": [
|
||||||
|
"printer-name",
|
||||||
|
"printer-state",
|
||||||
|
"printer-state-message",
|
||||||
|
"printer-uri-supported",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.executeIPP(
|
||||||
|
"Get-Printer-Attributes",
|
||||||
|
getPrinterAttributes
|
||||||
|
);
|
||||||
|
|
||||||
|
const printerState =
|
||||||
|
response["printer-attributes-tag"]?.["printer-state"];
|
||||||
|
logger.info(
|
||||||
|
`Successfully connected to CUPS printer ${this.name}. State: ${printerState}`
|
||||||
|
);
|
||||||
|
|
||||||
|
this.isConnected = true;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`Failed to get printer attributes for ${this.name}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
error: "Failed to get printer attributes. " + error.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to connect to CUPS printer:`, error);
|
||||||
|
this.isConnected = false;
|
||||||
|
this.cupsPrinter = null;
|
||||||
|
return {
|
||||||
|
error: "Failed to connect to CUPS printer. " + error.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async disconnect() {
|
||||||
|
logger.info(`Disconnecting from CUPS printer...`);
|
||||||
|
this.isConnected = false;
|
||||||
|
this.cupsPrinter = null;
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
async initialize() {
|
||||||
|
logger.info(`Initializing CUPS printer...`);
|
||||||
|
|
||||||
|
if (!this.isConnected || !this.cupsPrinter) {
|
||||||
|
return { error: "Printer is not connected" };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Verify printer is ready by checking attributes
|
||||||
|
const getPrinterAttributes = {
|
||||||
|
"operation-attributes-tag": {
|
||||||
|
"requested-attributes": [
|
||||||
|
"printer-state",
|
||||||
|
"printer-state-message",
|
||||||
|
"printer-state-reasons",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await this.executeIPP(
|
||||||
|
"Get-Printer-Attributes",
|
||||||
|
getPrinterAttributes
|
||||||
|
);
|
||||||
|
|
||||||
|
const printerState =
|
||||||
|
response["printer-attributes-tag"]?.["printer-state"];
|
||||||
|
const stateMessage =
|
||||||
|
response["printer-attributes-tag"]?.["printer-state-message"] || "";
|
||||||
|
|
||||||
|
// Printer states: 3 = idle, 4 = processing, 5 = stopped
|
||||||
|
if (printerState === 5) {
|
||||||
|
logger.warn(`Printer ${this.name} is stopped: ${stateMessage}`);
|
||||||
|
return { error: `Printer is stopped: ${stateMessage}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`CUPS printer ${this.name} initialized successfully. State: ${printerState}`
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to initialize CUPS printer ${this.name}:`, error);
|
||||||
|
return { error: error.message || "Failed to initialize CUPS printer" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get document data from documentJob
|
||||||
|
* Supports file references, buffers, and content
|
||||||
|
*/
|
||||||
|
async getDocumentData(documentJob) {
|
||||||
|
let data = null;
|
||||||
|
|
||||||
|
// If documentJob has a file reference, fetch it
|
||||||
|
if (documentJob.file || documentJob.fileId) {
|
||||||
|
const fileId =
|
||||||
|
documentJob.file?._id || documentJob.file || documentJob.fileId;
|
||||||
|
if (this.documentPrinterClient.socketClient?.fileManager) {
|
||||||
|
data =
|
||||||
|
await this.documentPrinterClient.socketClient.fileManager.getFile(
|
||||||
|
fileId
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new Error("File manager not available to fetch file");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If documentJob has a buffer directly
|
||||||
|
else if (documentJob.buffer || documentJob.data) {
|
||||||
|
data = documentJob.buffer || documentJob.data;
|
||||||
|
}
|
||||||
|
// If documentJob has rendered image
|
||||||
|
else if (documentJob.renderedImage) {
|
||||||
|
data = documentJob.renderedImage;
|
||||||
|
}
|
||||||
|
// If documentJob has content (text), convert to buffer
|
||||||
|
else if (documentJob.content) {
|
||||||
|
data = Buffer.from(documentJob.content, "utf-8");
|
||||||
|
} else {
|
||||||
|
throw new Error("Document job has no printable content");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure data is a Buffer
|
||||||
|
if (!Buffer.isBuffer(data)) {
|
||||||
|
if (data instanceof Uint8Array) {
|
||||||
|
data = Buffer.from(data);
|
||||||
|
} else if (data instanceof ArrayBuffer) {
|
||||||
|
data = Buffer.from(data);
|
||||||
|
} else if (typeof data === "string") {
|
||||||
|
data = Buffer.from(data, "utf-8");
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
"Document data must be a Buffer, Uint8Array, ArrayBuffer, or string"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deploy(documentJob, pdf) {
|
||||||
|
logger.info(
|
||||||
|
`Deploying job ${documentJob._id} to CUPS printer ${this.name}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Store the PDF buffer
|
||||||
|
this.pdfs.set(documentJob._id, pdf);
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
async print(jobId) {
|
||||||
|
logger.info(`Printing job ${jobId} to CUPS printer ${this.name}`);
|
||||||
|
|
||||||
|
if (!this.isConnected || !this.cupsPrinter) {
|
||||||
|
throw new Error("Printer is not connected");
|
||||||
|
}
|
||||||
|
|
||||||
|
const pdf = this.pdfs.get(jobId);
|
||||||
|
if (!pdf) {
|
||||||
|
throw new Error("PDF not found for job");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Ensure PDF is a Buffer
|
||||||
|
let documentData = pdf;
|
||||||
|
if (!Buffer.isBuffer(documentData)) {
|
||||||
|
if (documentData instanceof Uint8Array) {
|
||||||
|
documentData = Buffer.from(documentData);
|
||||||
|
} else if (documentData instanceof ArrayBuffer) {
|
||||||
|
documentData = Buffer.from(documentData);
|
||||||
|
} else if (typeof documentData === "string") {
|
||||||
|
documentData = Buffer.from(documentData, "utf-8");
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
"PDF data must be a Buffer, Uint8Array, ArrayBuffer, or string"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build IPP print job message
|
||||||
|
const jobName = jobId?.toString() || "Print Job";
|
||||||
|
const userName = process.env.USER || "system";
|
||||||
|
|
||||||
|
const printJobMessage = {
|
||||||
|
"operation-attributes-tag": {
|
||||||
|
"requesting-user-name": userName,
|
||||||
|
"job-name": jobName,
|
||||||
|
"document-format": "application/pdf",
|
||||||
|
},
|
||||||
|
data: documentData,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send print job
|
||||||
|
logger.debug(`Sending print job ${jobId} to ${this.cupsPrinterUrl}`);
|
||||||
|
const response = await this.executeIPP("Print-Job", printJobMessage);
|
||||||
|
|
||||||
|
// Extract job ID from response
|
||||||
|
const ippJobId = response["job-attributes-tag"]?.["job-id"];
|
||||||
|
const jobUri = response["job-attributes-tag"]?.["job-uri"];
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`Successfully printed job ${jobId} to CUPS printer ${this.name}. IPP Job ID: ${ippJobId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
jobId: ippJobId,
|
||||||
|
jobUri: jobUri,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`Failed to print job ${jobId} to CUPS printer ${this.name}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message || "Failed to print job",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
200
src/documentprinter/interfaces/receiptinterface.js
Normal file
200
src/documentprinter/interfaces/receiptinterface.js
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
// receiptinterface.js - Thermal receipt printer interface implementation
|
||||||
|
import log4js from "log4js";
|
||||||
|
import { loadConfig } from "../../config.js";
|
||||||
|
import { ThermalPrinter, PrinterTypes } from "node-thermal-printer";
|
||||||
|
import { convertPDFToImage } from "../../utils.js";
|
||||||
|
|
||||||
|
const config = loadConfig();
|
||||||
|
const logger = log4js.getLogger("Receipt Printer Interface");
|
||||||
|
logger.level = config.logLevel;
|
||||||
|
|
||||||
|
export default class ReceiptInterface {
|
||||||
|
constructor(documentPrinterClient) {
|
||||||
|
this.documentPrinterClient = documentPrinterClient;
|
||||||
|
this.host = documentPrinterClient.connection.host;
|
||||||
|
this.name = documentPrinterClient.documentPrinter.name;
|
||||||
|
this.port = documentPrinterClient.connection.port || 9100;
|
||||||
|
this.interface =
|
||||||
|
documentPrinterClient.connection.interface || "epsonReceipt";
|
||||||
|
this.protocol = documentPrinterClient.connection.protocol;
|
||||||
|
this.isConnected = false;
|
||||||
|
this.receiptPrinter = null;
|
||||||
|
this.retrieveStatusInterval = null;
|
||||||
|
this.images = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
buildPrinterUrl() {
|
||||||
|
switch (this.protocol) {
|
||||||
|
case "tcp":
|
||||||
|
return `tcp://${this.host}:${this.port}`;
|
||||||
|
case "system":
|
||||||
|
return `printer:${this.host}`;
|
||||||
|
case "serial":
|
||||||
|
return this.host;
|
||||||
|
default:
|
||||||
|
logger.warn(`Unknown protocol ${this.protocol}, defaulting to tcp.`);
|
||||||
|
return `tcp://${this.host}:${this.port}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async connect() {
|
||||||
|
try {
|
||||||
|
// Determine printer type enum
|
||||||
|
let type;
|
||||||
|
switch (this.interface) {
|
||||||
|
case "epsonReceipt":
|
||||||
|
type = PrinterTypes.EPSON;
|
||||||
|
break;
|
||||||
|
case "starReceipt":
|
||||||
|
type = PrinterTypes.STAR;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
type = PrinterTypes.EPSON;
|
||||||
|
logger.warn(
|
||||||
|
`Unknown interface ${this.interface}, defaulting to EPSON`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine interface based on connection type
|
||||||
|
const interfaceStr = this.buildPrinterUrl();
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`Connecting to receipt printer ${this.name} (${interfaceStr})`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initialize thermal printer
|
||||||
|
this.receiptPrinter = new ThermalPrinter({
|
||||||
|
type: type,
|
||||||
|
interface: interfaceStr,
|
||||||
|
options: {
|
||||||
|
timeout: 10000,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test connection
|
||||||
|
const isConnected = await this.receiptPrinter.isPrinterConnected();
|
||||||
|
if (!isConnected) {
|
||||||
|
logger.error("Printer is not connected or not reachable");
|
||||||
|
return { error: "Printer is not connected or not reachable." };
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isConnected = true;
|
||||||
|
logger.info(`Successfully connected to receipt printer ${this.name}`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to connect to receipt printer ${this.name}:`, error);
|
||||||
|
this.isConnected = false;
|
||||||
|
return {
|
||||||
|
error: "Failed to connect to receipt printer. " + error.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async disconnect() {
|
||||||
|
logger.info(`Disconnecting from receipt printer ${this.name}`);
|
||||||
|
this.isConnected = false;
|
||||||
|
this.receiptPrinter = null;
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
async initialize() {
|
||||||
|
logger.info(`Initializing receipt printer ${this.name}`);
|
||||||
|
// Thermal printers typically don't need special initialization
|
||||||
|
// but we can test the connection
|
||||||
|
if (this.receiptPrinter) {
|
||||||
|
try {
|
||||||
|
const isConnected = await this.receiptPrinter.isPrinterConnected();
|
||||||
|
if (!isConnected) {
|
||||||
|
logger.error(
|
||||||
|
`Printer not connected during initialization for receipt printer ${this.name}`
|
||||||
|
);
|
||||||
|
return { error: "Printer not connected during initialization" };
|
||||||
|
}
|
||||||
|
logger.info(`Receipt printer ${this.name} initialized successfully`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`Failed to initialize receipt printer ${this.name}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async print(jobId) {
|
||||||
|
logger.info(`Printing job ${jobId} to receipt printer ${this.name}`);
|
||||||
|
|
||||||
|
if (!this.isConnected || !this.receiptPrinter) {
|
||||||
|
throw new Error("Printer is not connected");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Clear the printer buffer to prevent previous print jobs from being included
|
||||||
|
this.receiptPrinter.clear();
|
||||||
|
|
||||||
|
const images = this.images.get(jobId);
|
||||||
|
if (!images || (Array.isArray(images) && images.length === 0)) {
|
||||||
|
throw new Error("Image not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertPDFToImage returns an array of buffers, so handle multiple pages
|
||||||
|
const imageArray = Array.isArray(images) ? images : [images];
|
||||||
|
|
||||||
|
for (const image of imageArray) {
|
||||||
|
// Ensure the image is a proper Buffer and create a fresh copy
|
||||||
|
// This prevents issues with pngjs reading from consumed streams
|
||||||
|
// pdf-to-img returns PNG buffers, but we create a copy to avoid any stream issues
|
||||||
|
const imageBuffer = Buffer.isBuffer(image)
|
||||||
|
? Buffer.from(image) // Create a fresh copy
|
||||||
|
: Buffer.from(image); // Convert if needed
|
||||||
|
this.receiptPrinter.printImageBuffer(imageBuffer);
|
||||||
|
this.receiptPrinter.cut();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the print job
|
||||||
|
await this.receiptPrinter.execute({ waitForResponse: true });
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`Successfully printed job ${jobId} to receipt printer ${this.name}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`Failed to print job ${jobId} to receipt printer ${this.name}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deploy(documentJob, pdf) {
|
||||||
|
logger.info(
|
||||||
|
`Deploying job ${documentJob._id} to receipt printer ${this.name}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const images = await convertPDFToImage(pdf, { width: 512 });
|
||||||
|
// Store the array of image buffers
|
||||||
|
this.images.set(documentJob._id, images);
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
async retrieveStatus() {
|
||||||
|
logger.debug(`Getting status of receipt printer ${this.name}`);
|
||||||
|
if (this.isOnline == false) {
|
||||||
|
logger.error("Printer is not connected or not reachable");
|
||||||
|
return { error: "Printer is not connected or not reachable." };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const status = await this.receiptPrinter.raw(
|
||||||
|
Buffer.from([0x10, 0x04, 0x04])
|
||||||
|
);
|
||||||
|
logger.info(`Printer status: ${status}`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to execute printer status:`, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`Receipt printer ${this.name} is connected.`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,32 +1,28 @@
|
|||||||
import "./App.css";
|
import "./App.css";
|
||||||
import {
|
import { Flex, Button, Tag, Menu, ConfigProvider, theme, Layout } from "antd";
|
||||||
Flex,
|
|
||||||
Button,
|
|
||||||
Typography,
|
|
||||||
Tag,
|
|
||||||
Menu,
|
|
||||||
ConfigProvider,
|
|
||||||
theme,
|
|
||||||
Layout,
|
|
||||||
Modal,
|
|
||||||
} from "antd";
|
|
||||||
import { MenuOutlined } from "@ant-design/icons";
|
import { MenuOutlined } from "@ant-design/icons";
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import merge from "lodash/merge";
|
import _ from "lodash";
|
||||||
import unionBy from "lodash/unionBy";
|
import unionBy from "lodash/unionBy";
|
||||||
import Overview from "./pages/Overview";
|
import Overview from "./pages/Overview";
|
||||||
import Printers from "./pages/Printers";
|
import Printers from "./pages/Printers";
|
||||||
|
import DocumentPrinters from "./pages/DocumentPrinters";
|
||||||
import Loading from "./pages/Loading";
|
import Loading from "./pages/Loading";
|
||||||
import OTPInput from "./pages/OTPInput";
|
import OTPInput from "./pages/OTPInput";
|
||||||
import CloudIcon from "./icons/CloudIcon";
|
import CloudIcon from "./icons/CloudIcon";
|
||||||
import LockIcon from "./icons/LockIcon";
|
import LockIcon from "./icons/LockIcon";
|
||||||
import SettingsIcon from "./icons/SettingsIcon";
|
import SettingsIcon from "./icons/SettingsIcon";
|
||||||
import Disconnected from "./pages/Disconnected";
|
import Disconnected from "./pages/Disconnected";
|
||||||
|
import Files from "./pages/Files";
|
||||||
|
import HostIcon from "./icons/HostIcon";
|
||||||
|
import PrinterIcon from "./icons/PrinterIcon";
|
||||||
|
import DocumentPrinterIcon from "./icons/DocumentPrinterIcon";
|
||||||
|
import FileIcon from "./icons/FileIcon";
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const [host, setHost] = useState({});
|
const [host, setHost] = useState({});
|
||||||
const [printers, setPrinters] = useState([]);
|
const [printers, setPrinters] = useState([]);
|
||||||
const [documentPrinters, setDocumentPrinters] = useState([]);
|
const [documentPrinters, setDocumentPrinters] = useState([]);
|
||||||
|
const [files, setFiles] = useState([]);
|
||||||
const [connected, setConnected] = useState(false);
|
const [connected, setConnected] = useState(false);
|
||||||
const [authenticated, setAuthenticated] = useState(false);
|
const [authenticated, setAuthenticated] = useState(false);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
@ -51,29 +47,45 @@ const App = () => {
|
|||||||
// Set up IPC listeners when component mounts
|
// Set up IPC listeners when component mounts
|
||||||
window.electronAPI.onIPCData("setHost", (newHost) => {
|
window.electronAPI.onIPCData("setHost", (newHost) => {
|
||||||
console.log("Host data received:", newHost);
|
console.log("Host data received:", newHost);
|
||||||
setHost((prev) => merge(prev, newHost));
|
setHost((prev) => _.merge(prev, newHost));
|
||||||
|
});
|
||||||
|
|
||||||
|
window.electronAPI.onIPCData("setFiles", (newFiles) => {
|
||||||
|
console.log("Files data received:", newFiles);
|
||||||
|
setFiles(newFiles);
|
||||||
});
|
});
|
||||||
|
|
||||||
window.electronAPI.onIPCData("setPrinters", (newPrinters) => {
|
window.electronAPI.onIPCData("setPrinters", (newPrinters) => {
|
||||||
console.log("Printers data:", newPrinters);
|
|
||||||
setPrinters(newPrinters);
|
setPrinters(newPrinters);
|
||||||
});
|
});
|
||||||
|
|
||||||
window.electronAPI.onIPCData("setPrinter", (newPrinter) => {
|
window.electronAPI.onIPCData("setPrinter", (newPrinter) => {
|
||||||
console.log("Printer data:", newPrinter);
|
setPrinters((prev) =>
|
||||||
setPrinters((prev) => unionBy(prev, [newPrinter], "_id"));
|
prev.map((printer) =>
|
||||||
|
printer._id === newPrinter._id
|
||||||
|
? _.merge(printer, newPrinter)
|
||||||
|
: printer
|
||||||
|
)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
window.electronAPI.onIPCData(
|
window.electronAPI.onIPCData(
|
||||||
"setDocumentPrinters",
|
"setDocumentPrinters",
|
||||||
(newDocumentPrinters) => {
|
(newDocumentPrinters) => {
|
||||||
console.log("Document printers data:", newDocumentPrinters);
|
setDocumentPrinters(newDocumentPrinters);
|
||||||
setDocumentPrinters((prev) =>
|
|
||||||
unionBy(prev, newDocumentPrinters, "_id")
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
window.electronAPI.onIPCData("setDocumentPrinter", (newDocumentPrinter) => {
|
||||||
|
setDocumentPrinters((prev) =>
|
||||||
|
prev.map((documentPrinter) =>
|
||||||
|
documentPrinter._id === newDocumentPrinter._id
|
||||||
|
? _.merge(documentPrinter, newDocumentPrinter)
|
||||||
|
: documentPrinter
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
window.electronAPI.onIPCData("setAuthenticated", (setAuthenticated) => {
|
window.electronAPI.onIPCData("setAuthenticated", (setAuthenticated) => {
|
||||||
console.log("Set authenticated:", setAuthenticated);
|
console.log("Set authenticated:", setAuthenticated);
|
||||||
setLoading(setAuthenticated);
|
setLoading(setAuthenticated);
|
||||||
@ -107,13 +119,24 @@ const App = () => {
|
|||||||
return () => {
|
return () => {
|
||||||
window.electronAPI.removeAllListeners("setHost");
|
window.electronAPI.removeAllListeners("setHost");
|
||||||
window.electronAPI.removeAllListeners("setPrinters");
|
window.electronAPI.removeAllListeners("setPrinters");
|
||||||
|
window.electronAPI.removeAllListeners("setPrinter");
|
||||||
window.electronAPI.removeAllListeners("setDocumentPrinters");
|
window.electronAPI.removeAllListeners("setDocumentPrinters");
|
||||||
|
window.electronAPI.removeAllListeners("setDocumentPrinter");
|
||||||
window.electronAPI.removeAllListeners("setAuthenticated");
|
window.electronAPI.removeAllListeners("setAuthenticated");
|
||||||
|
window.electronAPI.removeAllListeners("setFiles");
|
||||||
window.electronAPI.removeAllListeners("setConnected");
|
window.electronAPI.removeAllListeners("setConnected");
|
||||||
window.electronAPI.removeAllListeners("setLoading");
|
window.electronAPI.removeAllListeners("setLoading");
|
||||||
};
|
};
|
||||||
}, []); // Empty dependency array means this runs once on mount
|
}, []); // Empty dependency array means this runs once on mount
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("Document printers:", documentPrinters);
|
||||||
|
}, [documentPrinters]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("Printers:", printers);
|
||||||
|
}, [printers]);
|
||||||
|
|
||||||
// Function to render the appropriate page based on currentPageKey and auth status
|
// Function to render the appropriate page based on currentPageKey and auth status
|
||||||
const renderCurrentPage = () => {
|
const renderCurrentPage = () => {
|
||||||
// If loading, show loading
|
// If loading, show loading
|
||||||
@ -134,11 +157,20 @@ const App = () => {
|
|||||||
// If authenticated and connected, show the selected page
|
// If authenticated and connected, show the selected page
|
||||||
switch (currentPageKey) {
|
switch (currentPageKey) {
|
||||||
case "overview":
|
case "overview":
|
||||||
return <Overview printers={printers} host={host} loading={loading} />;
|
return (
|
||||||
|
<Overview
|
||||||
|
printers={printers}
|
||||||
|
host={host}
|
||||||
|
loading={loading}
|
||||||
|
documentPrinters={documentPrinters}
|
||||||
|
/>
|
||||||
|
);
|
||||||
case "printers":
|
case "printers":
|
||||||
return <Printers printers={printers} />;
|
return <Printers printers={printers} />;
|
||||||
|
case "files":
|
||||||
|
return <Files files={files} />;
|
||||||
case "documentPrinters":
|
case "documentPrinters":
|
||||||
return <div>Document Printers Page (to be implemented)</div>;
|
return <DocumentPrinters documentPrinters={documentPrinters} />;
|
||||||
default:
|
default:
|
||||||
return <Overview />;
|
return <Overview />;
|
||||||
}
|
}
|
||||||
@ -153,14 +185,22 @@ const App = () => {
|
|||||||
{
|
{
|
||||||
key: "overview",
|
key: "overview",
|
||||||
label: "Overview",
|
label: "Overview",
|
||||||
|
icon: <HostIcon />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "printers",
|
key: "printers",
|
||||||
label: "Printers",
|
label: "Printers",
|
||||||
|
icon: <PrinterIcon />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "documentPrinters",
|
key: "documentPrinters",
|
||||||
label: "Document Printers",
|
label: "Document Printers",
|
||||||
|
icon: <DocumentPrinterIcon />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "files",
|
||||||
|
label: "Files",
|
||||||
|
icon: <FileIcon />,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -186,10 +226,18 @@ const App = () => {
|
|||||||
<Layout>
|
<Layout>
|
||||||
<Flex style={{ width: "100vw", height: "100vh" }} vertical>
|
<Flex style={{ width: "100vw", height: "100vh" }} vertical>
|
||||||
<Flex
|
<Flex
|
||||||
className="ant-menu-horizontal ant-menu-light"
|
className="ant-menu-horizontal ant-menu-light electron-drag-area"
|
||||||
style={{ lineHeight: "40px", padding: "0 8px 0 75px" }}
|
style={{
|
||||||
|
lineHeight: "40px",
|
||||||
|
padding: "0 8px 0 75px",
|
||||||
|
}}
|
||||||
|
justify="space-between"
|
||||||
>
|
>
|
||||||
{loading == false && authenticated == true && connected == true ? (
|
{loading == false && authenticated == true && connected == true ? (
|
||||||
|
<div
|
||||||
|
className="electron-navigation"
|
||||||
|
style={{ width: "max-content" }}
|
||||||
|
>
|
||||||
<Menu
|
<Menu
|
||||||
mode="horizontal"
|
mode="horizontal"
|
||||||
items={mainMenuItems}
|
items={mainMenuItems}
|
||||||
@ -197,17 +245,16 @@ const App = () => {
|
|||||||
style={{
|
style={{
|
||||||
flexWrap: "wrap",
|
flexWrap: "wrap",
|
||||||
border: 0,
|
border: 0,
|
||||||
lineHeight: "40px",
|
lineHeight: "38px",
|
||||||
}}
|
}}
|
||||||
overflowedIndicator={
|
overflowedIndicator={
|
||||||
<Button type="text" icon={<MenuOutlined />} />
|
<Button type="text" icon={<MenuOutlined />} />
|
||||||
}
|
}
|
||||||
onClick={handleMenuClick}
|
onClick={handleMenuClick}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
<Flex align="center" gap={"small"} className="electron-navigation">
|
||||||
<div className="electron-navigation" style={{ flexGrow: 1 }}></div>
|
|
||||||
<Flex align="center" gap={"small"}>
|
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
icon={<SettingsIcon />}
|
icon={<SettingsIcon />}
|
||||||
|
|||||||
8
src/electron/assets/icons/fileicon.svg
Normal file
8
src/electron/assets/icons/fileicon.svg
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 12 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
|
<g transform="matrix(0.156042,0,0,0.156042,1.80453,0.721021)">
|
||||||
|
<rect x="0" y="0" width="53.774" height="67.661" style="fill-opacity:0;"/>
|
||||||
|
<path d="M10.149,67.641L43.438,67.641C50.137,67.641 53.586,64.135 53.586,57.41L53.586,29.058C53.586,24.717 53.028,22.725 50.308,19.954L33.894,3.284C31.282,0.616 29.111,0 25.212,0L10.149,0C3.481,0 0,3.532 0,10.257L0,57.41C0,64.161 3.455,67.641 10.149,67.641ZM10.637,61.519C7.621,61.519 6.122,59.941 6.122,57.029L6.122,10.637C6.122,7.752 7.621,6.122 10.663,6.122L23.984,6.122L23.984,23.261C23.984,27.733 26.167,29.895 30.619,29.895L47.464,29.895L47.464,57.029C47.464,59.941 45.965,61.519 42.929,61.519L10.637,61.519ZM31.198,24.496C29.903,24.496 29.384,23.945 29.384,22.656L29.384,6.973L46.613,24.496L31.198,24.496Z" style="fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
57
src/electron/components/FileList.jsx
Normal file
57
src/electron/components/FileList.jsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import React from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import { Typography, List, Button, Flex, Tag } from "antd";
|
||||||
|
import FileIcon from "../icons/FileIcon";
|
||||||
|
import InfoCircleIcon from "../icons/InfoCircleIcon";
|
||||||
|
import StateDisplay from "./StateDisplay";
|
||||||
|
import MissingPlaceholder from "./MissingPlaceholder";
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
const FileList = ({ files, type = "file" }) => {
|
||||||
|
if ((files?.length || 0) <= 0) {
|
||||||
|
return (
|
||||||
|
<MissingPlaceholder
|
||||||
|
message={`No ${type == "file" ? "files" : "document files"} added.`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<List
|
||||||
|
dataSource={files}
|
||||||
|
size="small"
|
||||||
|
bordered
|
||||||
|
renderItem={(file) => (
|
||||||
|
<List.Item actions={[]}>
|
||||||
|
<List.Item.Meta
|
||||||
|
description={
|
||||||
|
<Flex gap={"middle"} justify="space-between" align="center">
|
||||||
|
<Flex gap={"small"}>
|
||||||
|
<Text>
|
||||||
|
<FileIcon />
|
||||||
|
</Text>
|
||||||
|
<Text>{file.name || file._id}</Text>
|
||||||
|
<Tag>{file?.extension ? file.extension : "Unknown"}</Tag>
|
||||||
|
</Flex>
|
||||||
|
<Flex gap={"middle"} align="center">
|
||||||
|
<StateDisplay state={file.state} showProgress={true} />
|
||||||
|
<Button
|
||||||
|
key="info"
|
||||||
|
icon={<InfoCircleIcon />}
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</List.Item>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
FileList.propTypes = {
|
||||||
|
files: PropTypes.array.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FileList;
|
||||||
@ -1,36 +1,36 @@
|
|||||||
// PrinterSelect.js
|
// PrinterSelect.js
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from "prop-types";
|
||||||
import { Progress, Flex, Space } from 'antd'
|
import { Progress, Flex, Space } from "antd";
|
||||||
import StateTag from './StateTag'
|
import StateTag from "./StateTag";
|
||||||
|
|
||||||
const StateDisplay = ({ state, showProgress = true, showState = true }) => {
|
const StateDisplay = ({ state, showProgress = true, showState = true }) => {
|
||||||
const currentState = state || {
|
const currentState = state || {
|
||||||
type: 'unknown',
|
type: "unknown",
|
||||||
progress: 0
|
progress: 0,
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex gap='small' align={'center'}>
|
<Flex gap="small" align={"center"}>
|
||||||
{showState && (
|
{showState && (
|
||||||
<Space>
|
<Space>
|
||||||
<StateTag state={currentState.type} />
|
<StateTag state={currentState.type} />
|
||||||
</Space>
|
</Space>
|
||||||
)}
|
)}
|
||||||
{showProgress && currentState?.progress && currentState?.progress > 0 ? (
|
{showProgress && currentState?.percent > 0 ? (
|
||||||
<Progress
|
<Progress
|
||||||
percent={Math.round(currentState.progress * 100)}
|
percent={Math.round(currentState.percent)}
|
||||||
status='active'
|
status="active"
|
||||||
style={{ width: '150px', marginBottom: '2px' }}
|
style={{ width: "150px", marginBottom: "2px" }}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</Flex>
|
</Flex>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
StateDisplay.propTypes = {
|
StateDisplay.propTypes = {
|
||||||
state: PropTypes.object,
|
state: PropTypes.object,
|
||||||
showProgress: PropTypes.bool,
|
showProgress: PropTypes.bool,
|
||||||
showState: PropTypes.bool
|
showState: PropTypes.bool,
|
||||||
}
|
};
|
||||||
|
|
||||||
export default StateDisplay
|
export default StateDisplay;
|
||||||
|
|||||||
@ -1,92 +1,100 @@
|
|||||||
import PropTypes from 'prop-types'
|
import PropTypes from "prop-types";
|
||||||
import { Badge, Flex, Tag } from 'antd'
|
import { Badge, Flex, Tag } from "antd";
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from "react";
|
||||||
|
|
||||||
const StateTag = ({ state, showBadge = true, style = {} }) => {
|
const StateTag = ({ state, showBadge = true, style = {} }) => {
|
||||||
const { badgeStatus, badgeText } = useMemo(() => {
|
const { badgeStatus, badgeText } = useMemo(() => {
|
||||||
let status = 'default'
|
let status = "default";
|
||||||
let text = 'Unknown'
|
let text = "Unknown";
|
||||||
|
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case 'online':
|
case "online":
|
||||||
status = 'success'
|
status = "success";
|
||||||
text = 'Online'
|
text = "Online";
|
||||||
break
|
break;
|
||||||
case 'standby':
|
case "standby":
|
||||||
status = 'success'
|
status = "success";
|
||||||
text = 'Standby'
|
text = "Standby";
|
||||||
break
|
break;
|
||||||
case 'complete':
|
case "complete":
|
||||||
status = 'success'
|
status = "success";
|
||||||
text = 'Complete'
|
text = "Complete";
|
||||||
break
|
break;
|
||||||
case 'offline':
|
case "offline":
|
||||||
status = 'default'
|
status = "default";
|
||||||
text = 'Offline'
|
text = "Offline";
|
||||||
break
|
break;
|
||||||
case 'shutdown':
|
case "shutdown":
|
||||||
status = 'default'
|
status = "default";
|
||||||
text = 'Shutdown'
|
text = "Shutdown";
|
||||||
break
|
break;
|
||||||
case 'initializing':
|
case "initializing":
|
||||||
status = 'warning'
|
status = "warning";
|
||||||
text = 'Initializing'
|
text = "Initializing";
|
||||||
break
|
break;
|
||||||
case 'printing':
|
case "printing":
|
||||||
status = 'processing'
|
status = "processing";
|
||||||
text = 'Printing'
|
text = "Printing";
|
||||||
break
|
break;
|
||||||
case 'paused':
|
case "paused":
|
||||||
status = 'warning'
|
status = "warning";
|
||||||
text = 'Paused'
|
text = "Paused";
|
||||||
break
|
break;
|
||||||
case 'cancelled':
|
case "cancelled":
|
||||||
status = 'error'
|
status = "error";
|
||||||
text = 'Cancelled'
|
text = "Cancelled";
|
||||||
break
|
break;
|
||||||
case 'loading':
|
case "loading":
|
||||||
status = 'processing'
|
status = "processing";
|
||||||
text = 'Uploading'
|
text = "Uploading";
|
||||||
break
|
break;
|
||||||
case 'processing':
|
case "processing":
|
||||||
status = 'processing'
|
status = "processing";
|
||||||
text = 'Processing'
|
text = "Processing";
|
||||||
break
|
break;
|
||||||
case 'ready':
|
case "ready":
|
||||||
status = 'success'
|
status = "success";
|
||||||
text = 'Ready'
|
text = "Ready";
|
||||||
break
|
break;
|
||||||
case 'unconsumed':
|
case "unconsumed":
|
||||||
status = 'success'
|
status = "success";
|
||||||
text = 'Unconsumed'
|
text = "Unconsumed";
|
||||||
break
|
break;
|
||||||
case 'error':
|
case "error":
|
||||||
status = 'error'
|
status = "error";
|
||||||
text = 'Error'
|
text = "Error";
|
||||||
break
|
break;
|
||||||
case 'startup':
|
case "startup":
|
||||||
status = 'warning'
|
status = "warning";
|
||||||
text = 'Startup'
|
text = "Startup";
|
||||||
break
|
break;
|
||||||
case 'draft':
|
case "draft":
|
||||||
status = 'default'
|
status = "default";
|
||||||
text = 'Draft'
|
text = "Draft";
|
||||||
break
|
break;
|
||||||
case 'failed':
|
case "failed":
|
||||||
status = 'error'
|
status = "error";
|
||||||
text = 'Failed'
|
text = "Failed";
|
||||||
break
|
break;
|
||||||
case 'queued':
|
case "queued":
|
||||||
status = 'warning'
|
status = "warning";
|
||||||
text = 'Queued'
|
text = "Queued";
|
||||||
break
|
break;
|
||||||
|
case "downloading":
|
||||||
|
status = "processing";
|
||||||
|
text = "Downloading";
|
||||||
|
break;
|
||||||
|
case "downloaded":
|
||||||
|
status = "success";
|
||||||
|
text = "Downloaded";
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
status = 'default'
|
status = "default";
|
||||||
text = state || 'Unknown'
|
text = state || "Unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
return { badgeStatus: status, badgeText: text }
|
return { badgeStatus: status, badgeText: text };
|
||||||
}, [state])
|
}, [state]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tag color={badgeStatus} style={{ marginRight: 0, ...style }}>
|
<Tag color={badgeStatus} style={{ marginRight: 0, ...style }}>
|
||||||
@ -95,13 +103,13 @@ const StateTag = ({ state, showBadge = true, style = {} }) => {
|
|||||||
{badgeText}
|
{badgeText}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Tag>
|
</Tag>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
StateTag.propTypes = {
|
StateTag.propTypes = {
|
||||||
state: PropTypes.string,
|
state: PropTypes.string,
|
||||||
showBadge: PropTypes.bool,
|
showBadge: PropTypes.bool,
|
||||||
style: PropTypes.object
|
style: PropTypes.object,
|
||||||
}
|
};
|
||||||
|
|
||||||
export default StateTag
|
export default StateTag;
|
||||||
|
|||||||
6
src/electron/icons/FileIcon.jsx
Normal file
6
src/electron/icons/FileIcon.jsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import Icon from "@ant-design/icons";
|
||||||
|
import CustomIconSvg from "../assets/icons/fileicon.svg?react";
|
||||||
|
|
||||||
|
const FileIcon = (props) => <Icon component={CustomIconSvg} {...props} />;
|
||||||
|
|
||||||
|
export default FileIcon;
|
||||||
@ -21,8 +21,14 @@ button {
|
|||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure proper theme support */
|
.electron-navigation {
|
||||||
#root {
|
-webkit-app-region: no-drag;
|
||||||
width: 100%;
|
}
|
||||||
height: 100%;
|
|
||||||
|
.electron-drag-area {
|
||||||
|
-webkit-app-region: drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
.electron-drag-area:after {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,11 +6,6 @@
|
|||||||
<link rel="stylesheet" href="./fonts.css" />
|
<link rel="stylesheet" href="./fonts.css" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Farm Control Server</title>
|
<title>Farm Control Server</title>
|
||||||
<style>
|
|
||||||
.electron-navigation {
|
|
||||||
-webkit-app-region: drag;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@ -21,8 +21,7 @@ export async function setupIPC() {
|
|||||||
logger.warn("ipcMain not available, skipping IPC setup");
|
logger.warn("ipcMain not available, skipping IPC setup");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Generic IPC handler for custom messages
|
|
||||||
console.log("SETTING GET DATA HANDLER");
|
|
||||||
ipcMain.on("getData", (event) => {
|
ipcMain.on("getData", (event) => {
|
||||||
logger.info("Getting data...");
|
logger.info("Getting data...");
|
||||||
try {
|
try {
|
||||||
@ -35,6 +34,7 @@ export async function setupIPC() {
|
|||||||
sendIPC("setLoading", false);
|
sendIPC("setLoading", false);
|
||||||
sendIPC("setHost", {});
|
sendIPC("setHost", {});
|
||||||
sendIPC("setPrinters", []);
|
sendIPC("setPrinters", []);
|
||||||
|
sendIPC("setDocumentPrinters", []);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,6 +44,10 @@ export async function setupIPC() {
|
|||||||
sendIPC("setLoading", socketClient.loading);
|
sendIPC("setLoading", socketClient.loading);
|
||||||
sendIPC("setHost", socketClient.host || {});
|
sendIPC("setHost", socketClient.host || {});
|
||||||
sendIPC("setPrinters", socketClient.printerManager.printers || []);
|
sendIPC("setPrinters", socketClient.printerManager.printers || []);
|
||||||
|
sendIPC(
|
||||||
|
"setDocumentPrinters",
|
||||||
|
socketClient.documentPrinterManager.documentPrinters || []
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error getting printer data:", error);
|
logger.error("Error getting printer data:", error);
|
||||||
sendIPC("setAuthenticated", false);
|
sendIPC("setAuthenticated", false);
|
||||||
|
|||||||
34
src/electron/pages/DocumentPrinters.jsx
Normal file
34
src/electron/pages/DocumentPrinters.jsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Space, Card, Flex } from "antd";
|
||||||
|
import DocumentPrinterIcon from "../icons/DocumentPrinterIcon.jsx";
|
||||||
|
import PrinterList from "../components/PrinterList.jsx";
|
||||||
|
|
||||||
|
const DocumentPrinters = ({ documentPrinters }) => {
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
vertical
|
||||||
|
size="large"
|
||||||
|
style={{ width: "100%", height: "100%" }}
|
||||||
|
gap={"middle"}
|
||||||
|
>
|
||||||
|
<Flex gap={"middle"} wrap style={{ flexGrow: 1 }}>
|
||||||
|
<Card
|
||||||
|
size="small"
|
||||||
|
style={{ minWidth: "400px", flexGrow: 1 }}
|
||||||
|
title={
|
||||||
|
<Space>
|
||||||
|
<DocumentPrinterIcon />
|
||||||
|
Document Printers
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<PrinterList printers={documentPrinters} type="documentPrinter" />
|
||||||
|
</Card>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
DocumentPrinters.propTypes = {};
|
||||||
|
|
||||||
|
export default DocumentPrinters;
|
||||||
34
src/electron/pages/Files.jsx
Normal file
34
src/electron/pages/Files.jsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Card, Flex, Space } from "antd";
|
||||||
|
import FileList from "../components/FileList.jsx";
|
||||||
|
import FileIcon from "../icons/FileIcon";
|
||||||
|
|
||||||
|
const Files = ({ files }) => {
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
vertical
|
||||||
|
size="large"
|
||||||
|
style={{ width: "100%", height: "100%" }}
|
||||||
|
gap={"middle"}
|
||||||
|
>
|
||||||
|
<Flex gap={"middle"} wrap style={{ flexGrow: 1 }}>
|
||||||
|
<Card
|
||||||
|
size="small"
|
||||||
|
style={{ minWidth: "400px", flexGrow: 1 }}
|
||||||
|
title={
|
||||||
|
<Space>
|
||||||
|
<FileIcon />
|
||||||
|
Files
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FileList files={files} type="file" />
|
||||||
|
</Card>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Files.propTypes = {};
|
||||||
|
|
||||||
|
export default Files;
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Space, Card, Flex } from "antd";
|
import { Space, Card, Flex, Button } from "antd";
|
||||||
import PrinterIcon from "../icons/PrinterIcon.jsx";
|
import PrinterIcon from "../icons/PrinterIcon.jsx";
|
||||||
import PrinterList from "../components/PrinterList.jsx";
|
import PrinterList from "../components/PrinterList.jsx";
|
||||||
|
|
||||||
@ -12,7 +12,16 @@ const Printers = ({ printers }) => {
|
|||||||
gap={"middle"}
|
gap={"middle"}
|
||||||
>
|
>
|
||||||
<Flex gap={"middle"} wrap style={{ flexGrow: 1 }}>
|
<Flex gap={"middle"} wrap style={{ flexGrow: 1 }}>
|
||||||
<Card size="small" style={{ minWidth: "400px", flexGrow: 1 }}>
|
<Card
|
||||||
|
size="small"
|
||||||
|
style={{ minWidth: "400px", flexGrow: 1 }}
|
||||||
|
title={
|
||||||
|
<Space>
|
||||||
|
<PrinterIcon />
|
||||||
|
Printers
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
<PrinterList printers={printers} type="printer" />
|
<PrinterList printers={printers} type="printer" />
|
||||||
</Card>
|
</Card>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@ -17,6 +17,6 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 5173,
|
port: 5287,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -62,7 +62,7 @@ export async function createElectronWindow() {
|
|||||||
logger.info("Preload Script", path.join(__dirname, "preload.js"));
|
logger.info("Preload Script", path.join(__dirname, "preload.js"));
|
||||||
if (process.env.NODE_ENV === "development") {
|
if (process.env.NODE_ENV === "development") {
|
||||||
logger.info("Loading development url...");
|
logger.info("Loading development url...");
|
||||||
win.loadURL("http://localhost:5173"); // Vite dev server
|
win.loadURL("http://localhost:5287"); // Vite dev server
|
||||||
} else {
|
} else {
|
||||||
// In production, the built files will be in the build/electron directory
|
// In production, the built files will be in the build/electron directory
|
||||||
win.loadFile(
|
win.loadFile(
|
||||||
|
|||||||
264
src/files/filemanager.js
Normal file
264
src/files/filemanager.js
Normal file
@ -0,0 +1,264 @@
|
|||||||
|
// filemanager.js - Manages file downloads and caching
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import axios from "axios";
|
||||||
|
import log4js from "log4js";
|
||||||
|
import _ from "lodash";
|
||||||
|
import { loadConfig } from "../config.js";
|
||||||
|
import { sendIPC } from "../electron/ipc.js";
|
||||||
|
|
||||||
|
const config = loadConfig();
|
||||||
|
|
||||||
|
const logger = log4js.getLogger("File Manager");
|
||||||
|
logger.level = config.logLevel;
|
||||||
|
|
||||||
|
// Configure paths relative to this file
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
const TEMP_FILES_DIR = path.resolve(__dirname, "../../temp_files");
|
||||||
|
|
||||||
|
export class FileManager {
|
||||||
|
constructor(socketClient) {
|
||||||
|
this.socketClient = socketClient;
|
||||||
|
this.files = [];
|
||||||
|
this.downloadingFiles = new Map(); // Track ongoing downloads by fileId
|
||||||
|
this.ensureTempFilesDirectory();
|
||||||
|
this.progressCallbacks = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure the temp_files directory exists
|
||||||
|
*/
|
||||||
|
ensureTempFilesDirectory() {
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(TEMP_FILES_DIR)) {
|
||||||
|
fs.mkdirSync(TEMP_FILES_DIR, { recursive: true });
|
||||||
|
logger.info(`Created temp_files directory at ${TEMP_FILES_DIR}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to create temp_files directory: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the files list from the host's files property
|
||||||
|
*/
|
||||||
|
async updateFiles() {
|
||||||
|
const host = await this.socketClient.getObject({
|
||||||
|
objectType: "host",
|
||||||
|
_id: this.socketClient.host._id,
|
||||||
|
populate: ["files"],
|
||||||
|
});
|
||||||
|
this.files = (host.files || []).map((file) => {
|
||||||
|
return { ...file, state: { type: "downloaded" } };
|
||||||
|
});
|
||||||
|
sendIPC("setFiles", this.files);
|
||||||
|
logger.debug(`Updated files list: ${this.files.length} files`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the API URL for downloading files
|
||||||
|
* Converts the socket URL to HTTP/HTTPS URL
|
||||||
|
*/
|
||||||
|
getApiUrl() {
|
||||||
|
let apiUrl = config.apiUrl;
|
||||||
|
if (!apiUrl) {
|
||||||
|
throw new Error("API URL not configured");
|
||||||
|
}
|
||||||
|
return apiUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the file path in temp_files directory
|
||||||
|
*/
|
||||||
|
getFilePath(fileId) {
|
||||||
|
return path.join(TEMP_FILES_DIR, fileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if file exists in temp_files directory
|
||||||
|
*/
|
||||||
|
fileExistsInCache(fileId) {
|
||||||
|
const filePath = this.getFilePath(fileId);
|
||||||
|
return fs.existsSync(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get file from cache
|
||||||
|
*/
|
||||||
|
getFileFromCache(fileId) {
|
||||||
|
const filePath = this.getFilePath(fileId);
|
||||||
|
try {
|
||||||
|
const fileBuffer = fs.readFileSync(filePath);
|
||||||
|
logger.debug(`Retrieved file ${fileId} from cache`);
|
||||||
|
return fileBuffer;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to read file from cache: ${error.message}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save file to cache
|
||||||
|
*/
|
||||||
|
saveFileToCache(fileId, fileBuffer) {
|
||||||
|
const filePath = this.getFilePath(fileId);
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(filePath, fileBuffer);
|
||||||
|
logger.debug(`Saved file ${fileId} to cache`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to save file to cache: ${error.message}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download file from API
|
||||||
|
*/
|
||||||
|
async downloadFileFromApi(fileId) {
|
||||||
|
const apiUrl = this.getApiUrl();
|
||||||
|
const downloadUrl = `${apiUrl}/files/${fileId}/content/`;
|
||||||
|
|
||||||
|
const fileObject = await this.socketClient.getObject({
|
||||||
|
objectType: "file",
|
||||||
|
_id: fileId,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.files = [
|
||||||
|
...this.files,
|
||||||
|
{ ...fileObject, state: { type: "downloading", percent: 0 } },
|
||||||
|
];
|
||||||
|
sendIPC("setFiles", this.files);
|
||||||
|
logger.info(`Downloading file ${fileId} from ${downloadUrl}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(downloadUrl, {
|
||||||
|
responseType: "arraybuffer", // Get binary data
|
||||||
|
headers: {
|
||||||
|
"X-Auth-Code": config.host.authCode,
|
||||||
|
"X-Host-Id": config.host.id,
|
||||||
|
},
|
||||||
|
onDownloadProgress: (progressEvent) => {
|
||||||
|
console.log(progressEvent);
|
||||||
|
const percent = Math.round(
|
||||||
|
(progressEvent.loaded * 100) / progressEvent.total
|
||||||
|
);
|
||||||
|
logger.debug(`Downloading file ${fileId}: ${percent}%`);
|
||||||
|
this.files = _.unionBy(
|
||||||
|
[{ ...fileObject, state: { type: "downloading", percent } }],
|
||||||
|
this.files,
|
||||||
|
"_id"
|
||||||
|
);
|
||||||
|
sendIPC("setFiles", this.files);
|
||||||
|
if (this.progressCallbacks.has(fileId)) {
|
||||||
|
const progressCallbacks = this.progressCallbacks.get(fileId);
|
||||||
|
for (const callback of progressCallbacks) {
|
||||||
|
callback(percent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const fileBuffer = Buffer.from(response.data);
|
||||||
|
logger.info(`Successfully downloaded file ${fileId}`);
|
||||||
|
this.saveFileToCache(fileId, fileBuffer);
|
||||||
|
this.files = _.unionBy(
|
||||||
|
[{ ...fileObject, state: { type: "downloaded" } }],
|
||||||
|
this.files,
|
||||||
|
"_id"
|
||||||
|
);
|
||||||
|
if (this.progressCallbacks.has(fileId)) {
|
||||||
|
const progressCallbacks = this.progressCallbacks.get(fileId);
|
||||||
|
for (const callback of progressCallbacks) {
|
||||||
|
callback(100);
|
||||||
|
}
|
||||||
|
this.progressCallbacks.delete(fileId);
|
||||||
|
}
|
||||||
|
this.socketClient.editObject({
|
||||||
|
objectType: "host",
|
||||||
|
_id: this.socketClient.host._id,
|
||||||
|
updateData: {
|
||||||
|
files: this.files.map((file) => file._id),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
sendIPC("setFiles", this.files);
|
||||||
|
return fileBuffer;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to download file ${fileId}: ${error.message}`);
|
||||||
|
if (error.response) {
|
||||||
|
logger.error(`Response status: ${error.response.status}`);
|
||||||
|
logger.error(`Response data: ${error.response.data}`);
|
||||||
|
}
|
||||||
|
this.files = _.unionBy(
|
||||||
|
[{ ...fileObject, state: { type: "error" } }],
|
||||||
|
this.files,
|
||||||
|
"_id"
|
||||||
|
);
|
||||||
|
sendIPC("setFiles", this.files);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get file by ID
|
||||||
|
* Checks temp_files directory first, then downloads from API if not found
|
||||||
|
* If a download is already in progress for the same fileId, waits for that download
|
||||||
|
*/
|
||||||
|
async getFile(fileId, onProgress) {
|
||||||
|
if (!fileId) {
|
||||||
|
throw new Error("File ID is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update files list from host
|
||||||
|
this.updateFiles();
|
||||||
|
|
||||||
|
if (onProgress) {
|
||||||
|
const progressCallbacks = this.progressCallbacks.get(fileId) || [];
|
||||||
|
progressCallbacks.push(onProgress);
|
||||||
|
this.progressCallbacks.set(fileId, progressCallbacks);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if file exists in cache
|
||||||
|
if (this.fileExistsInCache(fileId)) {
|
||||||
|
logger.debug(`File ${fileId} found in cache`);
|
||||||
|
if (this.progressCallbacks.has(fileId)) {
|
||||||
|
const progressCallbacks = this.progressCallbacks.get(fileId);
|
||||||
|
for (const callback of progressCallbacks) {
|
||||||
|
callback(100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.getFileFromCache(fileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if download is already in progress for this file
|
||||||
|
if (this.downloadingFiles.has(fileId)) {
|
||||||
|
logger.debug(`File ${fileId} download already in progress, waiting...`);
|
||||||
|
try {
|
||||||
|
return await this.downloadingFiles.get(fileId);
|
||||||
|
} catch (error) {
|
||||||
|
// If the download failed, remove it from the map so it can be retried
|
||||||
|
this.downloadingFiles.delete(fileId);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// File not in cache and no download in progress, start new download
|
||||||
|
logger.debug(`File ${fileId} not in cache, downloading from API`);
|
||||||
|
const downloadPromise = this.downloadFileFromApi(fileId)
|
||||||
|
.then((fileBuffer) => {
|
||||||
|
// Remove from downloading map on success
|
||||||
|
this.downloadingFiles.delete(fileId);
|
||||||
|
return fileBuffer;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
// Remove from downloading map on error
|
||||||
|
this.downloadingFiles.delete(fileId);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store the promise in the map
|
||||||
|
this.downloadingFiles.set(fileId, downloadPromise);
|
||||||
|
|
||||||
|
return await downloadPromise;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,7 +10,10 @@ const config = loadConfig();
|
|||||||
const logger = log4js.getLogger("App");
|
const logger = log4js.getLogger("App");
|
||||||
logger.level = config.logLevel;
|
logger.level = config.logLevel;
|
||||||
|
|
||||||
|
const isHeadless = process.argv.includes("--headless");
|
||||||
|
|
||||||
export async function init() {
|
export async function init() {
|
||||||
|
if (!isHeadless) {
|
||||||
// Create Electron window first
|
// Create Electron window first
|
||||||
logger.info("Creating electron window...");
|
logger.info("Creating electron window...");
|
||||||
await createElectronWindow().catch((err) => {
|
await createElectronWindow().catch((err) => {
|
||||||
@ -21,6 +24,10 @@ export async function init() {
|
|||||||
setupIPC().catch((err) => {
|
setupIPC().catch((err) => {
|
||||||
logger.warn("Failed to setup IPC:", err);
|
logger.warn("Failed to setup IPC:", err);
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
logger.info("Running in headless mode. Skipping window creation.");
|
||||||
|
}
|
||||||
|
|
||||||
const socketClient = new SocketClient();
|
const socketClient = new SocketClient();
|
||||||
// Make socket client globally accessible for IPC handlers
|
// Make socket client globally accessible for IPC handlers
|
||||||
global.socketClient = socketClient;
|
global.socketClient = socketClient;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
155
src/printer/printerfilemanager.js
Normal file
155
src/printer/printerfilemanager.js
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
// printerfilemanager.js - Manages file uploads from FileManager to Moonraker
|
||||||
|
import axios from "axios";
|
||||||
|
import FormData from "form-data";
|
||||||
|
import log4js from "log4js";
|
||||||
|
import { loadConfig } from "../config.js";
|
||||||
|
|
||||||
|
const config = loadConfig();
|
||||||
|
|
||||||
|
const logger = log4js.getLogger("Printer File Manager");
|
||||||
|
logger.level = config.logLevel;
|
||||||
|
|
||||||
|
export class PrinterFileManager {
|
||||||
|
constructor(printerClient) {
|
||||||
|
this.printerClient = printerClient;
|
||||||
|
this.socketClient = printerClient.socketClient;
|
||||||
|
this.uploadingFiles = new Map(); // Track ongoing uploads by fileId
|
||||||
|
this.progressCallbacks = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload a file from FileManager to Moonraker
|
||||||
|
* @param {string} fileId - The ID of the file to upload (from FileManager)
|
||||||
|
* @param {string} [fileName] - Optional name to use for the file on Moonraker (will use file object name if not provided)
|
||||||
|
* @param {Object} moonrakerConfig - Moonraker configuration object with protocol, host, port, and optional apiKey
|
||||||
|
* @param {Function} [onProgress] - Optional progress callback (percent) => void
|
||||||
|
* @returns {Promise<boolean>} - True if upload succeeded, false otherwise
|
||||||
|
*/
|
||||||
|
async uploadFile(fileId, file, onProgress = null) {
|
||||||
|
if (!fileId) {
|
||||||
|
throw new Error("File ID is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onProgress) {
|
||||||
|
const progressCallbacks = this.progressCallbacks.get(fileId) || [];
|
||||||
|
progressCallbacks.push(onProgress);
|
||||||
|
this.progressCallbacks.set(fileId, progressCallbacks);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if upload is already in progress for this file
|
||||||
|
if (this.uploadingFiles.has(fileId)) {
|
||||||
|
logger.debug(`File ${fileId} upload already in progress, waiting...`);
|
||||||
|
try {
|
||||||
|
return await this.uploadingFiles.get(fileId);
|
||||||
|
} catch (error) {
|
||||||
|
// If the upload failed, remove it from the map so it can be retried
|
||||||
|
this.uploadingFiles.delete(fileId);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!this.printerClient.config ||
|
||||||
|
!this.printerClient.config.host ||
|
||||||
|
!this.printerClient.config.port
|
||||||
|
) {
|
||||||
|
throw new Error("Printer configuration is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create upload promise and store it
|
||||||
|
const uploadPromise = this._performUpload(fileId, file)
|
||||||
|
.then((result) => {
|
||||||
|
// Remove from uploading map on success
|
||||||
|
this.uploadingFiles.delete(fileId);
|
||||||
|
return result;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
// Remove from uploading map on error
|
||||||
|
this.uploadingFiles.delete(fileId);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store the promise in the map
|
||||||
|
this.uploadingFiles.set(fileId, uploadPromise);
|
||||||
|
|
||||||
|
return await uploadPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal method to perform the actual upload
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
async _performUpload(fileId, file, onProgress = null) {
|
||||||
|
try {
|
||||||
|
const uploadFileName = `${fileId}.gcode`;
|
||||||
|
|
||||||
|
const { protocol, host, port } = this.printerClient.config;
|
||||||
|
const httpUrl = `${
|
||||||
|
protocol === "ws" ? "http" : "https"
|
||||||
|
}://${host}:${port}/server/files/upload`;
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`Uploading file ${uploadFileName} to printer ${this.printerClient.id} at ${httpUrl}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create FormData with the file buffer
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", file, {
|
||||||
|
filename: "farmcontrol/" + uploadFileName,
|
||||||
|
contentType: "text/plain",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set up headers
|
||||||
|
const headers = {
|
||||||
|
...formData.getHeaders(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add API key if provided
|
||||||
|
if (this.printerClient.config.apiKey) {
|
||||||
|
headers["X-Api-Key"] = this.printerClient.config.apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload to Moonraker
|
||||||
|
const response = await axios.post(httpUrl, formData, {
|
||||||
|
headers,
|
||||||
|
onUploadProgress: (progressEvent) => {
|
||||||
|
if (progressEvent.total) {
|
||||||
|
const percentCompleted = Math.round(
|
||||||
|
(progressEvent.loaded * 100) / progressEvent.total
|
||||||
|
);
|
||||||
|
logger.debug(
|
||||||
|
`Uploading file to printer ${this.printerClient.id}: ${uploadFileName} ${percentCompleted}%`
|
||||||
|
);
|
||||||
|
if (this.progressCallbacks.has(fileId)) {
|
||||||
|
const progressCallbacks = this.progressCallbacks.get(fileId);
|
||||||
|
for (const callback of progressCallbacks) {
|
||||||
|
callback(percentCompleted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// Check response
|
||||||
|
if (response.data && response.data.action) {
|
||||||
|
logger.info(
|
||||||
|
`Successfully uploaded file ${uploadFileName} to printer ${this.printerClient.id}`
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
logger.error(
|
||||||
|
`Failed to upload file ${uploadFileName} to printer ${this.printerClient.id}: Invalid response`
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`Error uploading file ${fileId} to printer ${this.printerClient.id}: ${error.message}`
|
||||||
|
);
|
||||||
|
if (error.response) {
|
||||||
|
logger.error(`Response status: ${error.response.status}`);
|
||||||
|
logger.error(`Response data: ${JSON.stringify(error.response.data)}`);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -17,27 +17,39 @@ export class PrinterManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async reloadPrinters() {
|
async reloadPrinters() {
|
||||||
|
logger.info("Reloading printers...");
|
||||||
try {
|
try {
|
||||||
this.printers = await this.socketClient.listObjects({
|
this.printers = await this.socketClient.listObjects({
|
||||||
objectType: "printer",
|
objectType: "printer",
|
||||||
filter: { host: this.socketClient.id },
|
filter: { host: this.socketClient.id },
|
||||||
|
populate: [
|
||||||
|
"currentFilamentStock",
|
||||||
|
"currentJob",
|
||||||
|
"currentSubJob",
|
||||||
|
"queue",
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
sendIPC("setPrinters", this.printers);
|
sendIPC("setPrinters", this.printers);
|
||||||
|
|
||||||
|
var removedPrintersCount = 0;
|
||||||
|
|
||||||
// Remove printer clients that are no longer in the printers list
|
// Remove printer clients that are no longer in the printers list
|
||||||
const printerIds = this.printers.map((printer) => printer._id);
|
const printerIds = this.printers.map((printer) => printer._id);
|
||||||
for (const [printerId, printerClient] of this.printerClients.entries()) {
|
for (const [printerId, printerClient] of this.printerClients.entries()) {
|
||||||
if (!printerIds.includes(printerId)) {
|
if (!printerIds.includes(printerId)) {
|
||||||
// Close the connection before removing
|
// Close the connection before removing
|
||||||
if (printerClient.socket) {
|
if (printerClient.socket) {
|
||||||
|
printerClient.shouldReconnect = false;
|
||||||
printerClient.socket.close();
|
printerClient.socket.close();
|
||||||
}
|
}
|
||||||
this.printerClients.delete(printerId);
|
this.printerClients.delete(printerId);
|
||||||
logger.info(`Removed printer client for printer ID: ${printerId}`);
|
logger.info(`Removed printer client for printer ID: ${printerId}`);
|
||||||
|
removedPrintersCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var addedPrintersCount = 0;
|
||||||
// Add new printer clients for printers not in the printerClients map
|
// Add new printer clients for printers not in the printerClients map
|
||||||
for (const printer of this.printers) {
|
for (const printer of this.printers) {
|
||||||
const printerId = printer._id;
|
const printerId = printer._id;
|
||||||
@ -46,15 +58,74 @@ export class PrinterManager {
|
|||||||
await printerClient.connect();
|
await printerClient.connect();
|
||||||
this.printerClients.set(printerId, printerClient);
|
this.printerClients.set(printerId, printerClient);
|
||||||
logger.info(`Added printer client for printer ID: ${printerId}`);
|
logger.info(`Added printer client for printer ID: ${printerId}`);
|
||||||
|
addedPrintersCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.debug("Printers added:", addedPrintersCount);
|
||||||
|
logger.debug("Printers removed:", removedPrintersCount);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Failed to update printers:", error);
|
logger.error("Failed to update printers:", error);
|
||||||
this.printers = [];
|
this.printers = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async setupPrintersListener() {}
|
async handlePrinterAction(id, action, callback) {
|
||||||
|
logger.debug("Running printer action...", action);
|
||||||
|
const printer = this.getPrinterClient(id);
|
||||||
|
switch (action.type) {
|
||||||
|
case "setTemperature":
|
||||||
|
const setTempResult = await printer.setTemperature(action.data);
|
||||||
|
callback(setTempResult);
|
||||||
|
return;
|
||||||
|
|
||||||
|
case "restartPrinterFirmware":
|
||||||
|
const restartPrinterFirmwareResult =
|
||||||
|
await printer.restartPrinterFirmware();
|
||||||
|
callback(restartPrinterFirmwareResult);
|
||||||
|
return;
|
||||||
|
|
||||||
|
case "restartPrinter":
|
||||||
|
const restartPrinterResult = await printer.restartPrinter();
|
||||||
|
callback(restartPrinterResult);
|
||||||
|
return;
|
||||||
|
case "restartMoonraker":
|
||||||
|
const restartMoonrakerResult = await printer.restartMoonraker();
|
||||||
|
callback(restartMoonrakerResult);
|
||||||
|
return;
|
||||||
|
case "deploy":
|
||||||
|
const deployResult = await printer.deploySubJob(action.data);
|
||||||
|
callback(deployResult);
|
||||||
|
return;
|
||||||
|
case "startQueue":
|
||||||
|
const startQueueResult = await printer.startQueue();
|
||||||
|
callback(startQueueResult);
|
||||||
|
return;
|
||||||
|
case "pauseJob":
|
||||||
|
const pauseJobResult = await printer.pauseJob();
|
||||||
|
callback(pauseJobResult);
|
||||||
|
return;
|
||||||
|
case "resumeJob":
|
||||||
|
const resumeJobResult = await printer.resumeJob();
|
||||||
|
callback(resumeJobResult);
|
||||||
|
return;
|
||||||
|
case "cancelJob":
|
||||||
|
const cancelJobResult = await printer.cancelJob();
|
||||||
|
callback(cancelJobResult);
|
||||||
|
return;
|
||||||
|
case "unloadFilamentStock":
|
||||||
|
const unloadFilamentStockResult = await printer.unloadFilamentStock();
|
||||||
|
callback(unloadFilamentStockResult);
|
||||||
|
return;
|
||||||
|
case "loadFilamentStock":
|
||||||
|
const loadFilamentStockResult = await printer.loadFilamentStock(
|
||||||
|
action.data.filamentStock
|
||||||
|
);
|
||||||
|
callback(loadFilamentStockResult);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback({ error: "Unknown command." });
|
||||||
|
}
|
||||||
|
|
||||||
getPrinterClient(printerId) {
|
getPrinterClient(printerId) {
|
||||||
return this.printerClients.get(printerId);
|
return this.printerClients.get(printerId);
|
||||||
@ -64,112 +135,41 @@ export class PrinterManager {
|
|||||||
return this.printerClients.values();
|
return this.printerClients.values();
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateSubscription(printerId, socketId, mergedSubscription) {
|
|
||||||
const printerClient = this.printerClients.get(printerId);
|
|
||||||
if (!printerClient) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: `Printer with ID ${printerId} not found`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
printerClient.subscriptions.set(socketId, mergedSubscription);
|
|
||||||
return await printerClient.updateSubscriptions();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close all printer connections
|
// Close all printer connections
|
||||||
closeAllConnections() {
|
async closeAllConnections() {
|
||||||
for (const printerClient of this.printerClients.values()) {
|
logger.info(
|
||||||
if (printerClient.socket) {
|
`Closing all printer connections... current count: ${this.printerClients.size}`
|
||||||
printerClient.socket.close();
|
);
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async downloadGCODE(gcodeFileId) {
|
// Take a snapshot so any mutations during disconnects don't affect iteration
|
||||||
logger.info(`Downloading G-code file ${gcodeFileId}`);
|
const clients = Array.from(this.printerClients.values());
|
||||||
|
|
||||||
|
for (const printerClient of clients) {
|
||||||
try {
|
try {
|
||||||
// Download the G-code file with authentication
|
// Ensure we never auto-reconnect after a manual close-all
|
||||||
const url = `http://localhost:8080/gcodefiles/${gcodeFileId}/content/`;
|
printerClient.shouldReconnect = false;
|
||||||
const response = await fetch(url, {
|
await printerClient.disconnect();
|
||||||
headers: {
|
logger.info(
|
||||||
Authorization: `Bearer ${
|
`Disconnected printer client ${printerClient?.id || "unknown"}`
|
||||||
this.socketManager.socketClientConnections.values().next().value
|
);
|
||||||
.socket.handshake.auth.token
|
} catch (error) {
|
||||||
}`,
|
logger.error(
|
||||||
},
|
`Failed to disconnect printer client ${
|
||||||
});
|
printerClient?.id || "unknown"
|
||||||
if (!response.ok) {
|
}:`,
|
||||||
throw new Error(
|
error
|
||||||
`Failed to download G-code file: ${response.statusText}`
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
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) {
|
console.log("Printer clients:", this.printerClients);
|
||||||
logger.info(`Deploying print job ${printJobId}`);
|
|
||||||
const printJob = await jobModel
|
|
||||||
.findById(printJobId)
|
|
||||||
.populate("printers")
|
|
||||||
.populate("subJobs");
|
|
||||||
if (!printJob) {
|
|
||||||
throw new Error("Print job not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!printJob.gcodeFile) {
|
// Clear local references so no stale clients remain
|
||||||
throw new Error("No G-code file associated with this print job");
|
this.printerClients.clear();
|
||||||
}
|
this.printers = [];
|
||||||
|
|
||||||
const gcodeFileId = printJob.gcodeFile.toString();
|
logger.info(
|
||||||
const fileName = `${printJob.id}.gcode`;
|
`All printer connections closed. Remaining clients: ${this.printerClients.size}`
|
||||||
|
);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
printJob.state = { type: "queued" };
|
|
||||||
printJob.updatedAt = new Date();
|
|
||||||
await printJob.save();
|
|
||||||
|
|
||||||
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 subJobModel.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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,8 @@ import { askOtp, getDeviceInfo, notPrompting } from "../utils.js";
|
|||||||
import { sendIPC } from "../electron/ipc.js";
|
import { sendIPC } from "../electron/ipc.js";
|
||||||
import { PrinterManager } from "../printer/printermanager.js";
|
import { PrinterManager } from "../printer/printermanager.js";
|
||||||
import { HostManager } from "../host/hostmanager.js";
|
import { HostManager } from "../host/hostmanager.js";
|
||||||
|
import { FileManager } from "../files/filemanager.js";
|
||||||
|
import { DocumentPrinterManager } from "../documentprinter/documentprintermanager.js";
|
||||||
|
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
|
|
||||||
@ -22,9 +24,10 @@ export class SocketClient {
|
|||||||
this.host = null;
|
this.host = null;
|
||||||
this.id = null;
|
this.id = null;
|
||||||
this.reconnectTimeout = null;
|
this.reconnectTimeout = null;
|
||||||
this.reloadPrintersInterval = null;
|
|
||||||
this.hostManager = new HostManager(this);
|
this.hostManager = new HostManager(this);
|
||||||
|
this.fileManager = new FileManager(this);
|
||||||
this.printerManager = new PrinterManager(this);
|
this.printerManager = new PrinterManager(this);
|
||||||
|
this.documentPrinterManager = new DocumentPrinterManager(this);
|
||||||
this.scanner = new WebSocketScanner({ maxThreads: 50 });
|
this.scanner = new WebSocketScanner({ maxThreads: 50 });
|
||||||
this.readLine = null;
|
this.readLine = null;
|
||||||
sendIPC("setOnline", false);
|
sendIPC("setOnline", false);
|
||||||
@ -37,6 +40,7 @@ export class SocketClient {
|
|||||||
this.socket.on("connect_error", this.handleError.bind(this));
|
this.socket.on("connect_error", this.handleError.bind(this));
|
||||||
this.socket.on("objectUpdate", this.handleObjectUpdate.bind(this));
|
this.socket.on("objectUpdate", this.handleObjectUpdate.bind(this));
|
||||||
this.socket.on("objectAction", this.handleObjectAction.bind(this));
|
this.socket.on("objectAction", this.handleObjectAction.bind(this));
|
||||||
|
this.socket.on("objectEvent", this.handleObjectEvent.bind(this));
|
||||||
this.socket.on("disconnect", this.handleDisconnect.bind(this));
|
this.socket.on("disconnect", this.handleDisconnect.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,19 +58,40 @@ export class SocketClient {
|
|||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
scheduleReloadPrinters() {
|
subscribeToObjectEvents() {
|
||||||
// Clear any pending reconnect timeout
|
this.subscribeToObjectEvent({
|
||||||
if (this.reloadPrintersInterval) {
|
objectType: "host",
|
||||||
clearTimeout(this.reloadPrintersInterval);
|
_id: this.id,
|
||||||
this.reloadPrintersInterval = null;
|
eventType: "childUpdate",
|
||||||
|
});
|
||||||
|
this.subscribeToObjectEvent({
|
||||||
|
objectType: "host",
|
||||||
|
_id: this.id,
|
||||||
|
eventType: "childDelete",
|
||||||
|
});
|
||||||
|
this.subscribeToObjectEvent({
|
||||||
|
objectType: "host",
|
||||||
|
_id: this.id,
|
||||||
|
eventType: "childNew",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("Reloading printers...");
|
unsubscribeFromObjectEvents() {
|
||||||
this.printerManager.reloadPrinters();
|
this.unsubscribeFromObjectEvent({
|
||||||
this.reloadPrintersInterval = setInterval(() => {
|
objectType: "host",
|
||||||
logger.info("Reloading printers...");
|
_id: this.id,
|
||||||
this.printerManager.reloadPrinters();
|
eventType: "childUpdate",
|
||||||
}, 30000);
|
});
|
||||||
|
this.unsubscribeFromObjectEvent({
|
||||||
|
objectType: "host",
|
||||||
|
_id: this.id,
|
||||||
|
eventType: "childDelete",
|
||||||
|
});
|
||||||
|
this.unsubscribeFromObjectEvent({
|
||||||
|
objectType: "host",
|
||||||
|
_id: this.id,
|
||||||
|
eventType: "childNew",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
connect() {
|
connect() {
|
||||||
@ -74,6 +99,7 @@ export class SocketClient {
|
|||||||
logger.info(`Connecting to Socket.IO server: ${config.url}`);
|
logger.info(`Connecting to Socket.IO server: ${config.url}`);
|
||||||
this.socket = io(config.url, {
|
this.socket = io(config.url, {
|
||||||
auth: { type: "host" },
|
auth: { type: "host" },
|
||||||
|
reconnection: false,
|
||||||
timeout: 3000, // 3 second timeout
|
timeout: 3000, // 3 second timeout
|
||||||
});
|
});
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
@ -89,6 +115,7 @@ export class SocketClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
disconnect() {
|
disconnect() {
|
||||||
|
this.unsubscribeFromObjectEvents();
|
||||||
this.socket.disconnect();
|
this.socket.disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,7 +156,10 @@ export class SocketClient {
|
|||||||
config.host = { id: this.id, authCode: this.host.authCode };
|
config.host = { id: this.id, authCode: this.host.authCode };
|
||||||
saveConfig(config);
|
saveConfig(config);
|
||||||
this.sendDeviceInfo();
|
this.sendDeviceInfo();
|
||||||
this.scheduleReloadPrinters();
|
this.subscribeToObjectEvents();
|
||||||
|
this.documentPrinterManager.reloadDocumentPrinters();
|
||||||
|
this.printerManager.reloadPrinters();
|
||||||
|
await this.fileManager.updateFiles();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -146,18 +176,6 @@ export class SocketClient {
|
|||||||
callback({ success: true });
|
callback({ success: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
async handlePrinterAction(id, action, callback) {
|
|
||||||
console.log("RUNNING PRINTER ACTION");
|
|
||||||
const printer = this.printerManager.getPrinterClient(id);
|
|
||||||
switch (action.type) {
|
|
||||||
case "setTemperature":
|
|
||||||
const result = await printer.setTemperature(action.data);
|
|
||||||
callback(result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
callback({ success: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
handleObjectAction(data, callback) {
|
handleObjectAction(data, callback) {
|
||||||
logger.debug("Running object action...", data);
|
logger.debug("Running object action...", data);
|
||||||
const id = data._id;
|
const id = data._id;
|
||||||
@ -169,15 +187,35 @@ export class SocketClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (objectType == "printer") {
|
if (objectType == "printer") {
|
||||||
this.handlePrinterAction(id, action, callback);
|
this.printerManager.handlePrinterAction(id, action, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (objectType == "documentPrinter") {
|
||||||
|
this.documentPrinterManager.handleDocumentPrinterAction(
|
||||||
|
id,
|
||||||
|
action,
|
||||||
|
callback
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleObjectUpdate(data, callback) {
|
handleObjectUpdate(data, callback) {
|
||||||
logger.debug("Got object update", data);
|
logger.debug(
|
||||||
|
"Got object update for type:",
|
||||||
|
data.objectType,
|
||||||
|
" id:",
|
||||||
|
data._id
|
||||||
|
);
|
||||||
if (data.objectType == "host") {
|
if (data.objectType == "host") {
|
||||||
this.handleHostAction(action, callback);
|
this.handleHostAction(action, callback);
|
||||||
}
|
}
|
||||||
|
if (data.objectType == "documentPrinter") {
|
||||||
|
this.documentPrinterManager.handleDocumentPrinterUpdate(
|
||||||
|
data._id,
|
||||||
|
data,
|
||||||
|
callback
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleConnect() {
|
handleConnect() {
|
||||||
@ -237,7 +275,7 @@ export class SocketClient {
|
|||||||
project,
|
project,
|
||||||
cached,
|
cached,
|
||||||
}) {
|
}) {
|
||||||
logger.debug("Listing objects...", {
|
logger.trace("Listing objects...", {
|
||||||
objectType,
|
objectType,
|
||||||
populate,
|
populate,
|
||||||
filter,
|
filter,
|
||||||
@ -279,8 +317,35 @@ export class SocketClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async editObject({ objectType, _id, populate, updateData }) {
|
async newObject({ objectType, newData }) {
|
||||||
logger.debug("Editing object...", {
|
logger.debug("Creating object...", {
|
||||||
|
objectType,
|
||||||
|
newData,
|
||||||
|
});
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.socket.emit(
|
||||||
|
"newObject",
|
||||||
|
{
|
||||||
|
objectType,
|
||||||
|
newData,
|
||||||
|
},
|
||||||
|
(result) => {
|
||||||
|
if (result && result.error) {
|
||||||
|
reject(new Error(result.error));
|
||||||
|
} else {
|
||||||
|
logger.trace("Created object.", {
|
||||||
|
objectType,
|
||||||
|
newData,
|
||||||
|
});
|
||||||
|
resolve(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async editObject({ objectType, _id, populate, updateData, auditLog = true }) {
|
||||||
|
logger.trace("Editing object...", {
|
||||||
objectType,
|
objectType,
|
||||||
_id,
|
_id,
|
||||||
populate,
|
populate,
|
||||||
@ -293,6 +358,7 @@ export class SocketClient {
|
|||||||
_id,
|
_id,
|
||||||
populate,
|
populate,
|
||||||
updateData,
|
updateData,
|
||||||
|
auditLog,
|
||||||
},
|
},
|
||||||
(result) => {
|
(result) => {
|
||||||
if (result && result.error) {
|
if (result && result.error) {
|
||||||
@ -359,6 +425,21 @@ export class SocketClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async renderTemplatePDF(templateData) {
|
||||||
|
logger.debug("Rendering template PDF...", templateData);
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.socket.emit("renderTemplatePDF", templateData, (result) => {
|
||||||
|
if (result && result.error) {
|
||||||
|
logger.error("Failed to render template PDF:", result.error);
|
||||||
|
reject(new Error(result.error));
|
||||||
|
} else {
|
||||||
|
logger.trace("Template PDF rendered successfully.");
|
||||||
|
resolve(result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async subscribeToObjectActions({ objectType, _id }) {
|
async subscribeToObjectActions({ objectType, _id }) {
|
||||||
logger.debug("Suscribing to object actions...", {
|
logger.debug("Suscribing to object actions...", {
|
||||||
objectType,
|
objectType,
|
||||||
@ -369,6 +450,81 @@ export class SocketClient {
|
|||||||
_id,
|
_id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async subscribeToObjectUpdates({ objectType, _id }) {
|
||||||
|
logger.debug("Suscribing to object updates...", {
|
||||||
|
objectType,
|
||||||
|
_id,
|
||||||
|
});
|
||||||
|
this.socket.emit("subscribeToObjectUpdates", {
|
||||||
|
objectType,
|
||||||
|
_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async unsubscribeFromObjectUpdates({ objectType, _id }) {
|
||||||
|
logger.debug("Unsuscribing from object updates...", {
|
||||||
|
objectType,
|
||||||
|
_id,
|
||||||
|
});
|
||||||
|
this.socket.emit("unsubscribeFromObjectUpdates", {
|
||||||
|
objectType,
|
||||||
|
_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async subscribeToObjectEvent({ objectType, _id, eventType }) {
|
||||||
|
logger.debug("Suscribing to object event...", {
|
||||||
|
objectType,
|
||||||
|
_id,
|
||||||
|
eventType,
|
||||||
|
});
|
||||||
|
this.socket.emit("subscribeToObjectEvent", {
|
||||||
|
objectType,
|
||||||
|
_id,
|
||||||
|
eventType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async unsubscribeFromObjectEvent({ objectType, _id, eventType }) {
|
||||||
|
logger.debug("Unsuscribing from object event...", {
|
||||||
|
objectType,
|
||||||
|
_id,
|
||||||
|
eventType,
|
||||||
|
});
|
||||||
|
this.socket.emit("unsubscribeFromObjectEvent", {
|
||||||
|
objectType,
|
||||||
|
_id,
|
||||||
|
eventType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleHostChildUpdate(data) {
|
||||||
|
if (data.parentType == "printer") {
|
||||||
|
this.printerManager.reloadPrinters();
|
||||||
|
} else if (data.parentType == "documentPrinter") {
|
||||||
|
this.documentPrinterManager.reloadDocumentPrinters();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleObjectEvent({ objectType, _id, event }) {
|
||||||
|
logger.debug("Received object event...", {
|
||||||
|
objectType,
|
||||||
|
_id,
|
||||||
|
event,
|
||||||
|
});
|
||||||
|
const data = event.data;
|
||||||
|
if (
|
||||||
|
(event.type == "childUpdate" ||
|
||||||
|
event.type == "childDelete" ||
|
||||||
|
event.type == "childNew") &&
|
||||||
|
objectType == "host" &&
|
||||||
|
_id == this.id
|
||||||
|
) {
|
||||||
|
await this.handleHostChildUpdate(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//-------------------------------------- RE-WRITE ENDS HERE ---------------------------------------
|
//-------------------------------------- RE-WRITE ENDS HERE ---------------------------------------
|
||||||
|
|
||||||
async handleScanNetworkStart(data, callback) {
|
async handleScanNetworkStart(data, callback) {
|
||||||
@ -432,138 +588,6 @@ export class SocketClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async handlePrinterObjectsSubscribe(data, callback) {
|
|
||||||
logger.debug("Received printer.objects.subscribe event:", data);
|
|
||||||
try {
|
|
||||||
if (data && data.printerId) {
|
|
||||||
const printerId = data.printerId;
|
|
||||||
|
|
||||||
// Get existing subscription or create new one
|
|
||||||
const existingSubscription =
|
|
||||||
this.activeSubscriptions.get(printerId) || {};
|
|
||||||
|
|
||||||
// Merge the new subscription data with existing data
|
|
||||||
const mergedSubscription = {
|
|
||||||
...existingSubscription,
|
|
||||||
...data.objects,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.activeSubscriptions.set(printerId, mergedSubscription);
|
|
||||||
|
|
||||||
logger.trace("Merged subscription:", mergedSubscription);
|
|
||||||
const result = await this.printerManager.updateSubscription(
|
|
||||||
printerId,
|
|
||||||
this.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 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async handlePrinterObjectsUnsubscribe(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 };
|
|
||||||
if (data.objects) {
|
|
||||||
for (const key of Object.keys(data.objects)) {
|
|
||||||
delete remainingObjects[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("remainingObjects", remainingObjects);
|
|
||||||
console.log("existingSubscription", existingSubscription);
|
|
||||||
console.log("unsubscribe", data.objects);
|
|
||||||
|
|
||||||
// If there are no remaining objects, remove the entire subscription
|
|
||||||
if (Object.keys(remainingObjects).length === 0) {
|
|
||||||
this.activeSubscriptions.delete(printerId);
|
|
||||||
|
|
||||||
logger.warn("Removing entire subscription");
|
|
||||||
|
|
||||||
// Send subscribe command with updated subscription
|
|
||||||
const result = await this.printerManager.updateSubscription(
|
|
||||||
printerId,
|
|
||||||
this.socket.id,
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
if (callback) {
|
|
||||||
callback(result);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.activeSubscriptions.set(printerId, remainingObjects);
|
|
||||||
|
|
||||||
logger.warn(remainingObjects);
|
|
||||||
|
|
||||||
// Send subscribe command with updated subscription
|
|
||||||
const result = await this.printerManager.updateSubscription(
|
|
||||||
printerId,
|
|
||||||
this.socket.id,
|
|
||||||
remainingObjects
|
|
||||||
);
|
|
||||||
|
|
||||||
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 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async handleGcodeScript(data, callback) {
|
|
||||||
logger.debug("Received printer.gcode.script event:", data);
|
|
||||||
try {
|
|
||||||
const result = await this.printerManager.processPrinterCommand({
|
|
||||||
method: "printer.gcode.script",
|
|
||||||
params: data,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (callback) {
|
|
||||||
callback(result);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
logger.error("Error processing gcode script request:", e);
|
|
||||||
if (callback) {
|
|
||||||
callback({ error: e.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async handlePrinterObjectsQuery(data, callback) {
|
async handlePrinterObjectsQuery(data, callback) {
|
||||||
logger.debug("Received printer.objects.query event:", data);
|
logger.debug("Received printer.objects.query event:", data);
|
||||||
try {
|
try {
|
||||||
@ -602,197 +626,6 @@ export class SocketClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleFirmwareRestart(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 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async handlePrinterRestart(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 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async handleJobQueueStatus(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 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async handleJobQueueDeploy(data, callback) {
|
|
||||||
logger.debug("Received server.job_queue.deploy event:", data);
|
|
||||||
try {
|
|
||||||
if (!data || !data.jobId) {
|
|
||||||
throw new Error("Missing required job ID");
|
|
||||||
}
|
|
||||||
// Deploy the job to all printers
|
|
||||||
const result = await this.printerManager.deployJob(data.jobId);
|
|
||||||
|
|
||||||
if (callback) {
|
|
||||||
callback(result);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
logger.error("Error processing job queue deploy request:", e);
|
|
||||||
if (callback) {
|
|
||||||
callback({ error: e.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async handlePrintResume(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 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async handleJobQueueCancel(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 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async handlePrintCancel(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 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async handlePrintPause(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 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async handleJobQueuePause(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 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async handleJobQueueStart(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 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async handleFilamentStockLoad(data, callback) {
|
async handleFilamentStockLoad(data, callback) {
|
||||||
logger.debug("Received printer.filamentstock.load event:", data);
|
logger.debug("Received printer.filamentstock.load event:", data);
|
||||||
try {
|
try {
|
||||||
@ -827,17 +660,14 @@ export class SocketClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDisconnect() {
|
async handleDisconnect() {
|
||||||
logger.info("Disconnected from FarmControl Api.");
|
logger.info("Disconnected from FarmControl Api.");
|
||||||
this.printerManager.closeAllConnections();
|
await this.printerManager.closeAllConnections();
|
||||||
|
await this.documentPrinterManager.closeAllConnections();
|
||||||
this.connected = false;
|
this.connected = false;
|
||||||
sendIPC("setConnected", false);
|
sendIPC("setConnected", false);
|
||||||
this.authenticated = false;
|
this.authenticated = false;
|
||||||
sendIPC("setAuthenticated", false);
|
sendIPC("setAuthenticated", false);
|
||||||
if (this.reloadPrintersInterval) {
|
|
||||||
clearTimeout(this.reloadPrintersInterval);
|
|
||||||
this.reloadPrintersInterval = null;
|
|
||||||
}
|
|
||||||
notPrompting();
|
notPrompting();
|
||||||
this.scheduleReconnect();
|
this.scheduleReconnect();
|
||||||
}
|
}
|
||||||
|
|||||||
70
src/utils.js
70
src/utils.js
@ -1,5 +1,7 @@
|
|||||||
import readline from "node:readline";
|
import readline from "node:readline";
|
||||||
import os from "os";
|
import os from "os";
|
||||||
|
import { pdf } from "pdf-to-img";
|
||||||
|
import sharp from "sharp";
|
||||||
|
|
||||||
let isPrompting = false; // prevent multiple prompts at the same time
|
let isPrompting = false; // prevent multiple prompts at the same time
|
||||||
|
|
||||||
@ -54,3 +56,71 @@ export function getDeviceInfo() {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a PDF buffer or file path to an array of image buffers
|
||||||
|
* @param {Buffer|string} pdfInput - PDF buffer or file path
|
||||||
|
* @param {Object} options - Conversion options (optional)
|
||||||
|
* @param {number} options.scale - Scale factor for image resolution (default: 2)
|
||||||
|
* @param {number} options.width - Target width in pixels (will calculate scale if provided)
|
||||||
|
* @param {number} options.height - Target height in pixels (optional)
|
||||||
|
* @param {number[]} options.page_numbers - Array of page numbers to convert (optional, converts all pages by default)
|
||||||
|
* @returns {Promise<Buffer[]>} Array of image buffers, one for each page
|
||||||
|
*/
|
||||||
|
export async function convertPDFToImage(pdfInput, options = {}) {
|
||||||
|
try {
|
||||||
|
// pdf-to-img uses scale instead of width/height directly
|
||||||
|
// Default scale of 2 provides good quality for thermal printers
|
||||||
|
let scale = options.scale || 2;
|
||||||
|
|
||||||
|
const pdfOptions = {
|
||||||
|
scale,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
|
||||||
|
const document = await pdf(pdfInput, pdfOptions);
|
||||||
|
const outputImages = [];
|
||||||
|
|
||||||
|
// If specific page numbers are requested, use getPage() for each
|
||||||
|
if (
|
||||||
|
options.page_numbers &&
|
||||||
|
Array.isArray(options.page_numbers) &&
|
||||||
|
options.page_numbers.length > 0
|
||||||
|
) {
|
||||||
|
for (const pageNum of options.page_numbers) {
|
||||||
|
// page numbers are 1-indexed in the API
|
||||||
|
let image = await document.getPage(pageNum);
|
||||||
|
|
||||||
|
// Resize with Sharp if width or height are provided
|
||||||
|
if (options.width || options.height) {
|
||||||
|
const resizeOptions = {};
|
||||||
|
if (options.width) resizeOptions.width = options.width;
|
||||||
|
if (options.height) resizeOptions.height = options.height;
|
||||||
|
image = await sharp(image).resize(resizeOptions).toBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
outputImages.push(image);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Convert all pages: async iterable to array
|
||||||
|
for await (const image of document) {
|
||||||
|
let processedImage = image;
|
||||||
|
|
||||||
|
// Resize with Sharp if width or height are provided
|
||||||
|
if (options.width || options.height) {
|
||||||
|
const resizeOptions = {};
|
||||||
|
if (options.width) resizeOptions.width = options.width;
|
||||||
|
if (options.height) resizeOptions.height = options.height;
|
||||||
|
processedImage = await sharp(image).resize(resizeOptions).toBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
outputImages.push(processedImage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return outputImages; // Array of image buffers
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error converting PDF to image:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user