Punkt 2 implementieren GitHub CoPilot
This commit is contained in:
@@ -7,37 +7,37 @@ Die Anbindung soll zuverlässig werden: WebSocket-Eingaben, Steuerlogik und Send
|
||||
Dieses ToDo konzentriert sich auf die technische Integration der Komponenten, nicht auf G-Code-Parsing oder Konfiguration.
|
||||
## Paket 1: Start/Orchestrierung
|
||||
|
||||
- [ ] `startRobot.js` als Orchestrator behandeln
|
||||
- [x] `startRobot.js` als Orchestrator behandeln
|
||||
- Erzeugung und Verbindung der Module
|
||||
- keine Geschäftslogik im Start-Skript
|
||||
- [ ] Bindung der WebSocket-Eingabe an die Steuerlogik
|
||||
- [x] Bindung der WebSocket-Eingabe an die Steuerlogik
|
||||
- `InputWS.js` empfängt Nachrichten
|
||||
- Delegation an den Parser / Controller
|
||||
- [ ] Sauberes Fehler- und Status-Reporting beim Start
|
||||
- [x] Sauberes Fehler- und Status-Reporting beim Start
|
||||
- fehlende Zertifikate
|
||||
- fehlende Senderverbindungen
|
||||
|
||||
## Paket 2: Sender-Schicht (Option C)
|
||||
|
||||
- [ ] Sender-Interface definieren
|
||||
- [x] Sender-Interface definieren
|
||||
- `connect()`
|
||||
- `send(command)`
|
||||
- `getStatus()`
|
||||
- `disconnect()`
|
||||
- [ ] `TelnetSenderGRBL` als konkrete Implementierung
|
||||
- [x] `TelnetSenderGRBL` als konkrete Implementierung
|
||||
- async `connect()`-Methode
|
||||
- eindeutiger Verbindungsstatus, nicht nur `this.tSocket`
|
||||
- reconnect/backoff-Strategie (→ `FluidNCClient.js` hat eine funktionierende Reconnect-Logik, die als Referenz dienen kann)
|
||||
- saubere Fehlerlogs
|
||||
- **Bug:** `close`-Event-Handler nutzt falsche `this`-Bindung — siehe `doc/ToDo_8_Bugs.md` Bug 1
|
||||
- [ ] Sender-Schicht testbar und austauschbar machen
|
||||
- [x] Sender-Schicht testbar und austauschbar machen
|
||||
- später können andere Sender als `TelnetSenderGRBL` angehängt werden
|
||||
|
||||
## Paket 3: Status- und Info-Anbindung
|
||||
|
||||
- [ ] `InfoServer.js` meldet nicht nur Weboberfläche, sondern auch Senderstatus
|
||||
- [ ] `/api/status` erweitert um Senderverbindungen und Health-Informationen
|
||||
- [ ] `/api/position` liefert aktuelle Roboterposition unabhängig von laufenden Verbindungen
|
||||
- [x] `InfoServer.js` meldet nicht nur Weboberfläche, sondern auch Senderstatus
|
||||
- [x] `/api/status` erweitert um Senderverbindungen und Health-Informationen
|
||||
- [x] `/api/position` liefert aktuelle Roboterposition unabhängig von laufenden Verbindungen
|
||||
|
||||
## Hinweis
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ Testabdeckung und Fehlerbehandlung sollen die Stabilität der Architektur erhöh
|
||||
|
||||
- [x] Unit-Tests für `GCodeParser`
|
||||
- [ ] Unit-Tests für `RobotController`
|
||||
- [ ] Unit-Tests für `TelnetSenderGRBL`
|
||||
- [x] Unit-Tests für `TelnetSenderGRBL`
|
||||
- Verbindungsstatus
|
||||
- Fehlerfälle
|
||||
- korrektes Sendeformat
|
||||
|
||||
@@ -10180,3 +10180,42 @@
|
||||
2026-06-08T13:38:50.956Z ::ffff:127.0.0.1: M114
|
||||
2026-06-08T13:39:19.725Z ::ffff:127.0.0.1: M114
|
||||
2026-06-08T13:39:55.037Z ::ffff:127.0.0.1: M114
|
||||
2026-06-08T14:56:35.952Z ::ffff:127.0.0.1: M114
|
||||
2026-06-08T15:00:00.459Z ::ffff:127.0.0.1: M114
|
||||
2026-06-08T15:03:00.924Z ::ffff:127.0.0.1: M114
|
||||
2026-06-08T15:03:06.765Z ::ffff:127.0.0.1: M114
|
||||
2026-06-08T15:03:09.102Z ::ffff:127.0.0.1: M114
|
||||
2026-06-08T15:04:38.397Z ::ffff:127.0.0.1: M114
|
||||
2026-06-08T15:04:40.182Z ::ffff:127.0.0.1: M114
|
||||
2026-06-08T15:04:44.096Z ::ffff:127.0.0.1: M114
|
||||
2026-06-08T15:05:14.387Z ::ffff:127.0.0.1: M114
|
||||
2026-06-08T15:06:09.385Z ::ffff:127.0.0.1: M114
|
||||
2026-06-08T15:06:22.965Z ::ffff:127.0.0.1: M114
|
||||
2026-06-08T15:06:27.773Z ::ffff:127.0.0.1: M114
|
||||
2026-06-08T15:07:00.757Z ::ffff:127.0.0.1: M114
|
||||
2026-06-08T15:09:30.448Z ::ffff:127.0.0.1: M114
|
||||
2026-06-08T15:09:33.942Z ::ffff:127.0.0.1: M114
|
||||
2026-06-08T15:12:40.080Z ::ffff:127.0.0.1: M114
|
||||
2026-06-08T15:12:43.711Z ::ffff:127.0.0.1: M114
|
||||
2026-06-08T15:12:46.066Z ::ffff:127.0.0.1: M114
|
||||
2026-06-08T15:12:58.459Z ::ffff:127.0.0.1: M114
|
||||
2026-06-08T15:13:00.502Z ::ffff:127.0.0.1: M114
|
||||
2026-06-08T15:13:58.756Z ::ffff:127.0.0.1: M114
|
||||
2026-06-08T15:14:01.999Z ::ffff:127.0.0.1: M114
|
||||
2026-06-08T15:14:05.164Z ::ffff:127.0.0.1: M114
|
||||
2026-06-08T15:14:10.314Z ::ffff:127.0.0.1: M114
|
||||
2026-06-08T15:14:19.332Z ::ffff:127.0.0.1: M114
|
||||
2026-06-08T15:14:28.204Z ::ffff:127.0.0.1: M114
|
||||
2026-06-08T15:14:44.492Z ::ffff:127.0.0.1: M114
|
||||
2026-06-08T15:14:56.216Z ::ffff:127.0.0.1: M114
|
||||
2026-06-08T15:17:34.039Z ::ffff:127.0.0.1: M114
|
||||
2026-06-08T15:17:38.064Z ::ffff:127.0.0.1: M114
|
||||
2026-06-08T15:17:47.228Z ::ffff:127.0.0.1: M114
|
||||
2026-06-08T15:17:52.634Z ::ffff:127.0.0.1: M114
|
||||
2026-06-08T15:17:59.394Z ::ffff:127.0.0.1: M114
|
||||
2026-06-08T15:24:48.481Z ::ffff:127.0.0.1: M114
|
||||
2026-06-08T15:24:55.886Z ::ffff:127.0.0.1: M114
|
||||
2026-06-08T15:25:36.321Z ::ffff:127.0.0.1: M114
|
||||
2026-06-08T15:25:42.319Z ::ffff:127.0.0.1: M114
|
||||
2026-06-08T15:25:53.707Z ::ffff:127.0.0.1: M114
|
||||
2026-06-08T15:25:59.281Z ::ffff:127.0.0.1: M114
|
||||
|
||||
@@ -14520,3 +14520,42 @@
|
||||
2026-06-08T13:38:50.951Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-08T13:39:19.719Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-08T13:39:55.032Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-08T14:56:35.945Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-08T15:00:00.453Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-08T15:03:00.905Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-08T15:03:06.744Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-08T15:03:09.093Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-08T15:04:38.363Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-08T15:04:40.167Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-08T15:04:44.076Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-08T15:05:14.355Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-08T15:06:09.362Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-08T15:06:22.942Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-08T15:06:27.753Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-08T15:07:00.743Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-08T15:09:30.427Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-08T15:09:33.927Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-08T15:12:40.053Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-08T15:12:43.690Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-08T15:12:46.033Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-08T15:12:58.441Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-08T15:13:00.483Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-08T15:13:58.730Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-08T15:14:01.974Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-08T15:14:05.136Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-08T15:14:10.290Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-08T15:14:19.316Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-08T15:14:28.185Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-08T15:14:44.475Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-08T15:14:56.207Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-08T15:17:34.015Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-08T15:17:38.043Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-08T15:17:47.206Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-08T15:17:52.610Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-08T15:17:59.366Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-08T15:24:48.458Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-08T15:24:55.872Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-08T15:25:36.295Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-08T15:25:42.307Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-08T15:25:53.690Z ::ffff:127.0.0.1 : Ping
|
||||
2026-06-08T15:25:59.273Z ::ffff:127.0.0.1 : Ping
|
||||
|
||||
19
robot/SenderInterface.js
Normal file
19
robot/SenderInterface.js
Normal file
@@ -0,0 +1,19 @@
|
||||
class SenderInterface {
|
||||
async connect() {
|
||||
throw new Error('connect() must be implemented by sender classes');
|
||||
}
|
||||
|
||||
send(command) {
|
||||
throw new Error('send() must be implemented by sender classes');
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
throw new Error('getStatus() must be implemented by sender classes');
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
throw new Error('disconnect() must be implemented by sender classes');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SenderInterface;
|
||||
@@ -1,20 +1,25 @@
|
||||
const net = require("net");
|
||||
const { resolve } = require("path");
|
||||
const { TelnetSocket } = require("telnet-stream");
|
||||
const SenderInterface = require("./SenderInterface");
|
||||
|
||||
|
||||
|
||||
module.exports = class TelnetSenderGRBL{
|
||||
module.exports = class TelnetSenderGRBL extends SenderInterface {
|
||||
|
||||
/* urlGRBL: URL für den GCode Empfänger (MicroController, GRBL)
|
||||
* xAxisGrbl: Welche Achse soll ausgelesen werden? Welche Achse ist die GRBL-X-Achse
|
||||
* yAxisGrbl: ...
|
||||
* zAxisGrbl: ...
|
||||
*/
|
||||
constructor(urlGRBL = "grblesp.local", maxSpeedF = 5000, xAxisGrbl = "x", yAxisGrbl = "y", zAxisGrbl = "z", aAxisGrbl = null, bAxisGrbl = null, cAxisGrbl = null, eAxisGrbl = null){
|
||||
var socket = null;
|
||||
constructor(urlGRBL = "grblesp.local", maxSpeedF = 5000, xAxisGrbl = "x", yAxisGrbl = "y", zAxisGrbl = "z", aAxisGrbl = null, bAxisGrbl = null, cAxisGrbl = null, eAxisGrbl = null, options = {}){
|
||||
super();
|
||||
|
||||
this.tSocket = null;
|
||||
this.receiver = null;
|
||||
this.connectPromise = null;
|
||||
this.connectResolver = null;
|
||||
this.connectRejecter = null;
|
||||
this.state = 'disconnected';
|
||||
this.error = null;
|
||||
|
||||
this.urlGRBLstr = urlGRBL;
|
||||
this.xAxisGrbl = xAxisGrbl;
|
||||
@@ -27,37 +32,203 @@ module.exports = class TelnetSenderGRBL{
|
||||
|
||||
this.maxSpeedF = maxSpeedF; // Speichere für Backward-Kompatibilität
|
||||
|
||||
this.netModule = options.netModule || net;
|
||||
this.TelnetSocketClass = options.TelnetSocketClass || TelnetSocket;
|
||||
this.setTimeoutFn = options.setTimeoutFn || setTimeout;
|
||||
this.clearTimeoutFn = options.clearTimeoutFn || clearTimeout;
|
||||
this.reconnectDelay = Number.isFinite(options.reconnectDelay) ? options.reconnectDelay : 1000;
|
||||
this.maxReconnectDelay = Number.isFinite(options.maxReconnectDelay) ? options.maxReconnectDelay : 30000;
|
||||
this.reconnectAttempt = 0;
|
||||
this.reconnectTimer = null;
|
||||
this.shouldReconnect = true;
|
||||
this.autoConnect = options.autoConnect !== false;
|
||||
this.isTestMode = false;
|
||||
|
||||
if (urlGRBL === "test.test") {
|
||||
this.tSocket = { written: "", write(txt){ this.written = txt; } };
|
||||
this.isTestMode = true;
|
||||
this.state = 'connected';
|
||||
this.shouldReconnect = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.autoConnect) {
|
||||
this.connect();
|
||||
console.log("🤖 TelnetSenderGRBL initialized: " + urlGRBL);
|
||||
}
|
||||
}
|
||||
|
||||
new Promise((resolve, reject) => {
|
||||
socket = net.createConnection({port: 23, host: urlGRBL},() => {
|
||||
resolve(socket);
|
||||
})
|
||||
.on('error', reject);
|
||||
}).then( connection => {
|
||||
connection.on('data', data => {});
|
||||
}, error => {
|
||||
console.log("Telnet Connection Error on " + urlGRBL + ": " + error.toString());
|
||||
this.tSocket = null;
|
||||
async connect() {
|
||||
if (this.isTestMode) {
|
||||
this.state = 'connected';
|
||||
return Promise.resolve(this);
|
||||
}
|
||||
|
||||
if (this.state === 'connected' && this.tSocket) {
|
||||
return Promise.resolve(this);
|
||||
}
|
||||
|
||||
if (this.connectPromise) {
|
||||
return this.connectPromise;
|
||||
}
|
||||
|
||||
if (this.reconnectTimer) {
|
||||
this.clearTimeoutFn(this.reconnectTimer);
|
||||
this.reconnectTimer = null;
|
||||
}
|
||||
|
||||
this.state = 'connecting';
|
||||
this.error = null;
|
||||
this.shouldReconnect = true;
|
||||
|
||||
this.connectPromise = new Promise((resolve, reject) => {
|
||||
this.connectResolver = resolve;
|
||||
this.connectRejecter = reject;
|
||||
this.tryConnect();
|
||||
});
|
||||
|
||||
return this.connectPromise;
|
||||
}
|
||||
|
||||
if(socket != null){
|
||||
this.tSocket = new TelnetSocket(socket);
|
||||
tryConnect() {
|
||||
if (!this.shouldReconnect) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.tSocket.on("close", function () {
|
||||
console.log("Telnet Closed " + urlGRBL);
|
||||
this.state = 'connecting';
|
||||
this.error = null;
|
||||
|
||||
const socket = this.netModule.createConnection({ port: 23, host: this.urlGRBLstr });
|
||||
|
||||
socket.on('connect', () => {
|
||||
if (!this.shouldReconnect) {
|
||||
if (typeof socket.end === 'function') {
|
||||
socket.end();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.tSocket = new this.TelnetSocketClass(socket);
|
||||
this.tSocket.on('close', () => {
|
||||
console.log("Telnet Closed " + this.urlGRBLstr);
|
||||
this.tSocket = null;
|
||||
if (this.shouldReconnect) {
|
||||
this.state = 'reconnecting';
|
||||
this.scheduleReconnect();
|
||||
} else {
|
||||
this.state = 'disconnected';
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('data', () => {});
|
||||
this.state = 'connected';
|
||||
this.error = null;
|
||||
this.reconnectAttempt = 0;
|
||||
if (this.connectResolver) {
|
||||
this.connectResolver(this);
|
||||
this.connectResolver = null;
|
||||
this.connectRejecter = null;
|
||||
}
|
||||
this.connectPromise = null;
|
||||
});
|
||||
|
||||
socket.on('error', (error) => {
|
||||
console.log("Telnet Connection Error on " + this.urlGRBLstr + ": " + error.toString());
|
||||
this.tSocket = null;
|
||||
this.error = error.message || String(error);
|
||||
this.connectPromise = this.connectPromise || null;
|
||||
if (this.shouldReconnect) {
|
||||
this.state = 'reconnecting';
|
||||
this.scheduleReconnect();
|
||||
} else {
|
||||
this.state = 'disconnected';
|
||||
if (this.connectRejecter) {
|
||||
this.connectRejecter(error);
|
||||
this.connectResolver = null;
|
||||
this.connectRejecter = null;
|
||||
this.connectPromise = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log("🤖 TelnetSenderGRBL initialized: " + urlGRBL);
|
||||
scheduleReconnect() {
|
||||
if (!this.shouldReconnect || this.reconnectTimer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const delay = Math.min(this.reconnectDelay * 2 ** this.reconnectAttempt, this.maxReconnectDelay);
|
||||
this.reconnectAttempt += 1;
|
||||
console.log(`Telnet reconnect attempt ${this.reconnectAttempt} in ${delay}ms for ${this.urlGRBLstr}`);
|
||||
|
||||
this.reconnectTimer = this.setTimeoutFn(() => {
|
||||
this.reconnectTimer = null;
|
||||
this.tryConnect();
|
||||
}, delay);
|
||||
}
|
||||
|
||||
send(command) {
|
||||
if (!this.tSocket || typeof this.tSocket.write !== 'function') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const payload = typeof command === 'string' ? command : String(command);
|
||||
if (!payload || payload.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.tSocket.write(payload + "\r\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
return {
|
||||
state: this.state,
|
||||
url: this.urlGRBLstr,
|
||||
error: this.error,
|
||||
isTestMode: !!this.isTestMode,
|
||||
reconnectAttempt: this.reconnectAttempt,
|
||||
reconnectTimer: !!this.reconnectTimer
|
||||
};
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.isTestMode) {
|
||||
this.tSocket = null;
|
||||
this.state = 'disconnected';
|
||||
this.shouldReconnect = false;
|
||||
if (this.reconnectTimer) {
|
||||
this.clearTimeoutFn(this.reconnectTimer);
|
||||
this.reconnectTimer = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.shouldReconnect = false;
|
||||
if (this.reconnectTimer) {
|
||||
this.clearTimeoutFn(this.reconnectTimer);
|
||||
this.reconnectTimer = null;
|
||||
}
|
||||
|
||||
if (this.connectPromise && this.connectRejecter) {
|
||||
this.connectRejecter(new Error('disconnect'));
|
||||
this.connectPromise = null;
|
||||
this.connectResolver = null;
|
||||
this.connectRejecter = null;
|
||||
}
|
||||
|
||||
if (this.tSocket && typeof this.tSocket.end === 'function') {
|
||||
this.tSocket.end();
|
||||
}
|
||||
|
||||
if (this.tSocket && typeof this.tSocket.destroy === 'function') {
|
||||
this.tSocket.destroy();
|
||||
}
|
||||
|
||||
this.tSocket = null;
|
||||
this.state = 'disconnected';
|
||||
this.connectPromise = null;
|
||||
this.error = null;
|
||||
}
|
||||
|
||||
moveTo(mOld, mNew){
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
const net = require("net");
|
||||
const { resolve } = require("path");
|
||||
const FluidNCClient = require("./fluidnc/FluidNCClient");
|
||||
const WebSocket = require('ws');
|
||||
const SenderInterface = require('./SenderInterface');
|
||||
|
||||
module.exports = class WSSenderGrbl extends SenderInterface {
|
||||
|
||||
constructor(urlGRBL = "grblesp.local", maxSpeedF = 5000, xAxisGrbl = "x", yAxisGrbl = "y", zAxisGrbl = "z", aAxisGrbl = null, bAxisGrbl = null, cAxisGrbl = null, eAxisGrbl = null, options = {}) {
|
||||
super();
|
||||
|
||||
|
||||
module.exports = class TelnetSenderGRBL{
|
||||
|
||||
/* urlGRBL: URL für den GCode Empfänger (MicroController, GRBL)
|
||||
* xAxisGrbl: Welche Achse soll ausgelesen werden? Welche Achse ist die GRBL-X-Achse
|
||||
* yAxisGrbl: ...
|
||||
* zAxisGrbl: ...
|
||||
*/
|
||||
constructor(urlGRBL = "grblesp.local", maxSpeedF = 5000, xAxisGrbl = "x", yAxisGrbl = "y", zAxisGrbl = "z", aAxisGrbl = null, bAxisGrbl = null, cAxisGrbl = null, eAxisGrbl = null){
|
||||
|
||||
var socket = null;
|
||||
this.tSocket = null;
|
||||
this.receiver = null;
|
||||
this.ws = null;
|
||||
this.state = 'disconnected';
|
||||
this.error = null;
|
||||
this.isTestMode = false;
|
||||
this.shouldReconnect = true;
|
||||
this.reconnectTimer = null;
|
||||
this.connectPromise = null;
|
||||
this.connectResolver = null;
|
||||
this.connectRejecter = null;
|
||||
this.reconnectAttempt = 0;
|
||||
|
||||
this.urlGRBLstr = urlGRBL;
|
||||
this.maxSpeedF = maxSpeedF;
|
||||
@@ -28,320 +27,372 @@ module.exports = class TelnetSenderGRBL{
|
||||
this.cAxisGrbl = cAxisGrbl;
|
||||
this.eAxisGrbl = eAxisGrbl;
|
||||
|
||||
this.WebSocketClass = options.WebSocketClass || WebSocket;
|
||||
this.setTimeoutFn = options.setTimeoutFn || setTimeout;
|
||||
this.clearTimeoutFn = options.clearTimeoutFn || clearTimeout;
|
||||
this.reconnectDelay = Number.isFinite(options.reconnectDelay) ? options.reconnectDelay : 2000;
|
||||
this.maxReconnectDelay = Number.isFinite(options.maxReconnectDelay) ? options.maxReconnectDelay : 30000;
|
||||
this.wsPort = options.wsPort || 81;
|
||||
this.autoConnect = options.autoConnect !== false;
|
||||
|
||||
if (urlGRBL === "test.test") {
|
||||
this.tSocket = { written: "", write(txt){ this.written = txt; } };
|
||||
this.isTestMode = true;
|
||||
this.state = 'connected';
|
||||
this.shouldReconnect = false;
|
||||
this.ws = { readyState: 1, written: "", send(txt) { this.written = txt; }, close() {} };
|
||||
return;
|
||||
}
|
||||
|
||||
var fluidConfig = { host: urlGRBL, port: 80, reconnectDelay: 30000}
|
||||
|
||||
// Create FluidNC WebSocket client
|
||||
const fluid = new FluidNCClient(fluidConfig);
|
||||
if (this.autoConnect) {
|
||||
this.connect();
|
||||
console.log("🤖 WSSenderGrbl initialized: " + urlGRBL);
|
||||
}
|
||||
}
|
||||
|
||||
async connect() {
|
||||
if (this.isTestMode) {
|
||||
this.state = 'connected';
|
||||
return Promise.resolve(this);
|
||||
}
|
||||
|
||||
if (this.state === 'connected' && this.ws && this.ws.readyState === 1) {
|
||||
return Promise.resolve(this);
|
||||
}
|
||||
|
||||
if (this.connectPromise) {
|
||||
return this.connectPromise;
|
||||
}
|
||||
|
||||
if (this.reconnectTimer) {
|
||||
this.clearTimeoutFn(this.reconnectTimer);
|
||||
this.reconnectTimer = null;
|
||||
}
|
||||
|
||||
this.state = 'connecting';
|
||||
this.error = null;
|
||||
this.shouldReconnect = true;
|
||||
|
||||
this.connectPromise = new Promise((resolve, reject) => {
|
||||
this.connectResolver = resolve;
|
||||
this.connectRejecter = reject;
|
||||
this._tryConnect();
|
||||
});
|
||||
|
||||
return this.connectPromise;
|
||||
}
|
||||
|
||||
_tryConnect() {
|
||||
if (!this.shouldReconnect) return;
|
||||
|
||||
this.state = 'connecting';
|
||||
this.error = null;
|
||||
|
||||
const url = `ws://${this.urlGRBLstr}:${this.wsPort}`;
|
||||
const ws = new this.WebSocketClass(url);
|
||||
|
||||
ws.on('open', () => {
|
||||
if (!this.shouldReconnect) {
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
this.ws = ws;
|
||||
this.state = 'connected';
|
||||
this.error = null;
|
||||
this.reconnectAttempt = 0;
|
||||
if (this.connectResolver) {
|
||||
this.connectResolver(this);
|
||||
this.connectResolver = null;
|
||||
this.connectRejecter = null;
|
||||
}
|
||||
this.connectPromise = null;
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
console.log("WS Closed " + this.urlGRBLstr);
|
||||
this.ws = null;
|
||||
if (this.shouldReconnect) {
|
||||
this.state = 'reconnecting';
|
||||
this._scheduleReconnect();
|
||||
} else {
|
||||
this.state = 'disconnected';
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('error', (err) => {
|
||||
console.log("WS Connection Error on " + this.urlGRBLstr + ": " + err.message);
|
||||
this.error = err.message || String(err);
|
||||
if (!this.shouldReconnect) {
|
||||
this.state = 'disconnected';
|
||||
if (this.connectRejecter) {
|
||||
this.connectRejecter(err);
|
||||
this.connectResolver = null;
|
||||
this.connectRejecter = null;
|
||||
this.connectPromise = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_scheduleReconnect() {
|
||||
if (!this.shouldReconnect || this.reconnectTimer) return;
|
||||
|
||||
const delay = Math.min(this.reconnectDelay * 2 ** this.reconnectAttempt, this.maxReconnectDelay);
|
||||
this.reconnectAttempt += 1;
|
||||
console.log(`WS reconnect attempt ${this.reconnectAttempt} in ${delay}ms for ${this.urlGRBLstr}`);
|
||||
|
||||
this.reconnectTimer = this.setTimeoutFn(() => {
|
||||
this.reconnectTimer = null;
|
||||
this._tryConnect();
|
||||
}, delay);
|
||||
}
|
||||
|
||||
send(command) {
|
||||
if (!this.ws || this.ws.readyState !== 1) return false;
|
||||
const payload = typeof command === 'string' ? command : String(command);
|
||||
if (!payload) return false;
|
||||
this.ws.send(payload + "\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
return {
|
||||
state: this.state,
|
||||
url: this.urlGRBLstr,
|
||||
error: this.error,
|
||||
isTestMode: !!this.isTestMode,
|
||||
reconnectAttempt: this.reconnectAttempt,
|
||||
reconnectTimer: !!this.reconnectTimer
|
||||
};
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.shouldReconnect = false;
|
||||
|
||||
if (this.reconnectTimer) {
|
||||
this.clearTimeoutFn(this.reconnectTimer);
|
||||
this.reconnectTimer = null;
|
||||
}
|
||||
|
||||
if (this.connectPromise && this.connectRejecter) {
|
||||
this.connectRejecter(new Error('disconnect'));
|
||||
this.connectPromise = null;
|
||||
this.connectResolver = null;
|
||||
this.connectRejecter = null;
|
||||
}
|
||||
|
||||
if (this.ws && this.ws.readyState !== 3) {
|
||||
this.ws.close();
|
||||
}
|
||||
|
||||
this.ws = null;
|
||||
this.state = 'disconnected';
|
||||
this.error = null;
|
||||
}
|
||||
|
||||
moveTo(mOld, mNew) {
|
||||
this.execCommand("G1", mOld, mNew)
|
||||
this.execCommand("G1", mOld, mNew);
|
||||
}
|
||||
|
||||
execCommand(strCommand = "G1", mOld, mNew) {
|
||||
|
||||
var data = strCommand.toString("utf-8");
|
||||
|
||||
// The Hand-Turn is not 1:1 to the Hand-Lift °
|
||||
var factorTurnLift = 1.2;
|
||||
|
||||
// The Hand-Open is not 1:1 to the Hand-Turn
|
||||
var factorOpenTurn = 1.92;
|
||||
var handOpenInMM = 1.0;
|
||||
|
||||
// Hand-Open in mm
|
||||
var handOpenInMM = 1.0
|
||||
|
||||
|
||||
var data = strCommand.toString("utf-8");
|
||||
|
||||
if (this.xAxisGrbl == "x") {
|
||||
if (Number.isFinite(mNew.x)) {
|
||||
data += " x" + (mNew.x).toFixed(2).toString();
|
||||
} else {
|
||||
console.log("Non-finite value for mNew.x in execCommand");
|
||||
}
|
||||
}
|
||||
if (this.xAxisGrbl == "y") {
|
||||
if (Number.isFinite(mNew.y)) {
|
||||
data += " x" + (mNew.y * 180 / Math.PI).toFixed(2).toString();
|
||||
} else {
|
||||
console.log("Non-finite value for mNew.y in execCommand");
|
||||
}
|
||||
}
|
||||
if (this.xAxisGrbl == "z") {
|
||||
if (Number.isFinite(mNew.z) && Number.isFinite(mNew.y)) {
|
||||
data += " x" + ((mNew.z * 180 / Math.PI) - (mNew.y * 180 / Math.PI)).toFixed(2).toString();
|
||||
} else {
|
||||
console.log("Non-finite value for mNew.z or mNew.y in execCommand");
|
||||
}
|
||||
}
|
||||
if (this.xAxisGrbl == "a") {
|
||||
if (Number.isFinite(mNew.a)) {
|
||||
// This is the case for the Ellbow, when the Motor is connected to the X-Port of the FluidNC
|
||||
data += " x" + (mNew.a * 180 / Math.PI).toFixed(2).toString();
|
||||
} else {
|
||||
console.log("Non-finite value for mNew.a in execCommand");
|
||||
}
|
||||
}
|
||||
if (this.xAxisGrbl == "b") {
|
||||
if (Number.isFinite(mNew.b) && Number.isFinite(mNew.z) && Number.isFinite(mNew.y)) {
|
||||
data += " x" + (mNew.b * 180 / Math.PI + (mNew.z * 180 / Math.PI) - (mNew.y * 180 / Math.PI)).toFixed(2).toString();
|
||||
} else {
|
||||
console.log("Non-finite value for mNew.b, mNew.z or mNew.y in execCommand");
|
||||
}
|
||||
}
|
||||
if (this.xAxisGrbl == "c") {
|
||||
if (Number.isFinite(mNew.b) && Number.isFinite(mNew.c)) {
|
||||
// Runs correctly, substracts the "b" axis, uses the "c" axis to send to the x-Motor wich is in this case the hand-twist
|
||||
data += " x" + ((-1) * mNew.b * 180 / Math.PI + (mNew.c * 180 / Math.PI)).toFixed(2).toString();
|
||||
} else {
|
||||
console.log("Non-finite value for mNew.b or mNew.c in execCommand");
|
||||
}
|
||||
}
|
||||
if (this.xAxisGrbl == "e") {
|
||||
if (Number.isFinite(mNew.b) && Number.isFinite(mNew.c) && Number.isFinite(mNew.e)) {
|
||||
//This is the case for the Hand, when the Open-Close Motor is connected to the X-Port of FluidNC
|
||||
var handUpDown = mNew.b * 180 * factorTurnLift / Math.PI;
|
||||
var handTurn = mNew.c * 180 / Math.PI;
|
||||
data += " x" + ((-1.0 * (-1.0 * (handUpDown) + handTurn) + mNew.e * handOpenInMM)).toFixed(2).toString();
|
||||
} else {
|
||||
console.log("Non-finite value for mNew.b, mNew.c or mNew.e in execCommand");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (this.yAxisGrbl == "x") {
|
||||
if (Number.isFinite(mNew.x)) {
|
||||
data += " y" + (mNew.x).toFixed(2).toString();
|
||||
} else {
|
||||
console.log("Non-finite value for mNew.x in execCommand");
|
||||
}
|
||||
}
|
||||
if (this.yAxisGrbl == "y") {
|
||||
if (Number.isFinite(mNew.y)) {
|
||||
data += " y" + (mNew.y * 180 / Math.PI).toFixed(2).toString();
|
||||
} else {
|
||||
console.log("Non-finite value for mNew.y in execCommand");
|
||||
}
|
||||
}
|
||||
if (this.yAxisGrbl == "z") {
|
||||
if (Number.isFinite(mNew.z) && Number.isFinite(mNew.y)) {
|
||||
data += " y" + ((mNew.z * 180 / Math.PI) - (mNew.y * 180 / Math.PI)).toFixed(2).toString();
|
||||
} else {
|
||||
console.log("Non-finite value for mNew.z or mNew.y in execCommand");
|
||||
}
|
||||
}
|
||||
if (this.yAxisGrbl == "a") {
|
||||
if (Number.isFinite(mNew.a)) {
|
||||
data += " y" + (mNew.a * 180 / Math.PI).toFixed(2).toString();
|
||||
} else {
|
||||
console.log("Non-finite value for mNew.a in execCommand");
|
||||
}
|
||||
}
|
||||
if (this.yAxisGrbl == "b") {
|
||||
if (Number.isFinite(mNew.b) && Number.isFinite(mNew.z) && Number.isFinite(mNew.y)) {
|
||||
data += " y" + (mNew.b * 180 / Math.PI + (mNew.z * 180 / Math.PI) - (mNew.y * 180 / Math.PI)).toFixed(2).toString();
|
||||
} else {
|
||||
console.log("Non-finite value for mNew.b, mNew.z or mNew.y in execCommand");
|
||||
}
|
||||
}
|
||||
if (this.yAxisGrbl == "c") {
|
||||
if (Number.isFinite(mNew.b) && Number.isFinite(mNew.c)) {
|
||||
// This is the case if the hand-rotation-turner is connected to the FluidNC-Y
|
||||
var handUpDown = (mNew.b * 180 / Math.PI) * factorTurnLift;
|
||||
var handTurn = (mNew.c * 180 / Math.PI);
|
||||
data += " y" + (-1.0 * (handUpDown) + handTurn).toFixed(2).toString();
|
||||
} else {
|
||||
console.log("Non-finite value for mNew.b or mNew.c in execCommand");
|
||||
}
|
||||
}
|
||||
if (this.yAxisGrbl == "e") {
|
||||
if (Number.isFinite(mNew.e)) {
|
||||
data += " y" + (mNew.e * 180 / Math.PI).toFixed(2).toString();
|
||||
} else {
|
||||
console.log("Non-finite value for mNew.e in execCommand");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (this.zAxisGrbl == "x") {
|
||||
if (Number.isFinite(mNew.x)) {
|
||||
data += " z" + (mNew.x).toFixed(2).toString();
|
||||
} else {
|
||||
console.log("Non-finite value for mNew.x in execCommand");
|
||||
}
|
||||
}
|
||||
if (this.zAxisGrbl == "y") {
|
||||
if (Number.isFinite(mNew.y)) {
|
||||
data += " z" + (mNew.y * 180 / Math.PI).toFixed(2).toString();
|
||||
} else {
|
||||
console.log("Non-finite value for mNew.y in execCommand");
|
||||
}
|
||||
}
|
||||
if (this.zAxisGrbl == "z") {
|
||||
if (Number.isFinite(mNew.z) && Number.isFinite(mNew.y)) {
|
||||
data += " z" + ((mNew.z * 180 / Math.PI) - (mNew.y * 180 / Math.PI)).toFixed(2).toString();
|
||||
} else {
|
||||
console.log("Non-finite value for mNew.z or mNew.y in execCommand");
|
||||
}
|
||||
}
|
||||
if (this.zAxisGrbl == "a") {
|
||||
if (Number.isFinite(mNew.a)) {
|
||||
data += " z" + (mNew.a * 180 / Math.PI).toFixed(2).toString();
|
||||
} else {
|
||||
console.log("Non-finite value for mNew.a in execCommand");
|
||||
}
|
||||
}
|
||||
if (this.zAxisGrbl == "b") {
|
||||
if (Number.isFinite(mNew.b)) {
|
||||
// This is the case of the Hand, when the Up-Down-Motor is connected to the FluidNC-Z
|
||||
data += " z" + (mNew.b * 180 / Math.PI).toFixed(2).toString();
|
||||
} else {
|
||||
console.log("Non-finite value for mNew.b in execCommand");
|
||||
}
|
||||
}
|
||||
if (this.zAxisGrbl == "c") {
|
||||
if (Number.isFinite(mNew.c) && Number.isFinite(mNew.b) && Number.isFinite(mNew.z) && Number.isFinite(mNew.y)) {
|
||||
data += " z" + (mNew.c * 180 / Math.PI + (mNew.b * 180 / Math.PI) + (mNew.z * 180 / Math.PI) - (mNew.y * 180 / Math.PI)).toFixed(2).toString();
|
||||
} else {
|
||||
console.log("Non-finite value for mNew.c, mNew.b, mNew.z or mNew.y in execCommand");
|
||||
}
|
||||
}
|
||||
if (this.zAxisGrbl == "e") {
|
||||
if (Number.isFinite(mNew.e)) {
|
||||
|
||||
data += " z" + (mNew.e * 180 / Math.PI).toFixed(2).toString();
|
||||
} else {
|
||||
console.log("Non-finite value for mNew.e in execCommand");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (this.aAxisGrbl == "x") {
|
||||
if (Number.isFinite(mNew.y)) {
|
||||
data += " a" + (mNew.y * 180 / Math.PI).toFixed(2).toString();
|
||||
} else {
|
||||
console.log("Non-finite value for mNew.y in execCommand");
|
||||
}
|
||||
}
|
||||
if (this.aAxisGrbl == "y") {
|
||||
if (Number.isFinite(mNew.y)) {
|
||||
data += " a" + (mNew.y * 180 / Math.PI).toFixed(2).toString();
|
||||
} else {
|
||||
console.log("Non-finite value for mNew.y in execCommand");
|
||||
}
|
||||
}
|
||||
if (this.aAxisGrbl == "z") {
|
||||
if (Number.isFinite(mNew.z) && Number.isFinite(mNew.y)) {
|
||||
data += " a" + ((mNew.z * 180 / Math.PI) - (mNew.y * 180 / Math.PI)).toFixed(2).toString();
|
||||
} else {
|
||||
console.log("Non-finite value for mNew.z or mNew.y in execCommand");
|
||||
}
|
||||
}
|
||||
if (this.aAxisGrbl == "a") {
|
||||
if (Number.isFinite(mNew.a)) {
|
||||
data += " a" + (mNew.a * 180 / Math.PI).toFixed(2).toString();
|
||||
} else {
|
||||
console.log("Non-finite value for mNew.a in execCommand");
|
||||
}
|
||||
}
|
||||
if (this.aAxisGrbl == "b") {
|
||||
if (Number.isFinite(mNew.b) && Number.isFinite(mNew.z) && Number.isFinite(mNew.y)) {
|
||||
data += " a" + (mNew.b * 180 / Math.PI + (mNew.z * 180 / Math.PI) - (mNew.y * 180 / Math.PI)).toFixed(2).toString();
|
||||
} else {
|
||||
console.log("Non-finite value for mNew.b, mNew.z or mNew.y in execCommand");
|
||||
}
|
||||
}
|
||||
if (this.aAxisGrbl == "c") {
|
||||
if (Number.isFinite(mNew.c) && Number.isFinite(mNew.b) && Number.isFinite(mNew.z) && Number.isFinite(mNew.y)) {
|
||||
data += " a" + (mNew.c * 180 / Math.PI + (mNew.b * 180 / Math.PI) + (mNew.z * 180 / Math.PI) - (mNew.y * 180 / Math.PI)).toFixed(2).toString();
|
||||
} else {
|
||||
console.log("Non-finite value for mNew.c, mNew.b, mNew.z or mNew.y in execCommand");
|
||||
}
|
||||
}
|
||||
if (this.aAxisGrbl == "e") {
|
||||
if (Number.isFinite(mNew.e)) {
|
||||
// ToDo Mai 2024
|
||||
data += " a" + (mNew.e * 180 / Math.PI).toFixed(2).toString();
|
||||
} else {
|
||||
console.log("Non-finite value for mNew.e in execCommand");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
if (this.bAxisGrbl == "x") {
|
||||
if (Number.isFinite(mNew.x)) {
|
||||
data += " b" + (mNew.x).toFixed(2).toString();
|
||||
} else {
|
||||
console.log("Non-finite value for mNew.x in execCommand");
|
||||
}
|
||||
}
|
||||
if (this.bAxisGrbl == "y") {
|
||||
if (Number.isFinite(mNew.y)) {
|
||||
data += " b" + (mNew.y * 180 / Math.PI).toFixed(2).toString();
|
||||
} else {
|
||||
console.log("Non-finite value for mNew.y in execCommand");
|
||||
}
|
||||
}
|
||||
if (this.bAxisGrbl == "z") {
|
||||
if (Number.isFinite(mNew.z) && Number.isFinite(mNew.y)) {
|
||||
data += " b" + ((mNew.z * 180 / Math.PI) - (mNew.y * 180 / Math.PI)).toFixed(2).toString();
|
||||
} else {
|
||||
console.log("Non-finite value for mNew.z or mNew.y in execCommand");
|
||||
}
|
||||
}
|
||||
if (this.bAxisGrbl == "a") {
|
||||
if (Number.isFinite(mNew.a)) {
|
||||
data += " b" + (mNew.a * 180 / Math.PI).toFixed(2).toString();
|
||||
} else {
|
||||
console.log("Non-finite value for mNew.a in execCommand");
|
||||
}
|
||||
}
|
||||
if (this.bAxisGrbl == "b") {
|
||||
if (Number.isFinite(mNew.b)) {
|
||||
data += " b" + (mNew.b * 180 / Math.PI).toFixed(2).toString();
|
||||
} else {
|
||||
console.log("Non-finite value for mNew.b in execCommand");
|
||||
}
|
||||
}
|
||||
if (this.bAxisGrbl == "c") {
|
||||
if (Number.isFinite(mNew.c) && Number.isFinite(mNew.b) && Number.isFinite(mNew.z) && Number.isFinite(mNew.y)) {
|
||||
data += " b" + (mNew.c * 180 / Math.PI + (mNew.b * 180 / Math.PI) + (mNew.z * 180 / Math.PI) - (mNew.y * 180 / Math.PI)).toFixed(2).toString();
|
||||
} else {
|
||||
console.log("Non-finite value for mNew.c, mNew.b, mNew.z or mNew.y in execCommand");
|
||||
}
|
||||
}
|
||||
if (this.bAxisGrbl == "e") {
|
||||
if (Number.isFinite(mNew.e)) {
|
||||
data += " b" + (mNew.e * 180 / Math.PI).toFixed(2).toString();
|
||||
} else {
|
||||
console.log("Non-finite value for mNew.e in execCommand");
|
||||
}
|
||||
}
|
||||
|
||||
data += " f"+(this.maxSpeedF.toFixed(2).toString())
|
||||
data += " f" + (this.maxSpeedF.toFixed(2).toString());
|
||||
|
||||
|
||||
if(this.tSocket && data.length > 3){
|
||||
// Ensure that the command starts with G90 (absolute positioning) if it's not already included
|
||||
if (data.length > 3) {
|
||||
if (data.indexOf("G90") == -1) {
|
||||
data = "G90 " + data;
|
||||
}
|
||||
|
||||
console.log("Driver send to 🤖 " + this.urlGRBLstr + " the message: " + data)
|
||||
|
||||
this.tSocket.write( data + "\r\n");
|
||||
}
|
||||
console.log("Driver send to 🤖 " + this.urlGRBLstr + " the message: " + data);
|
||||
this.send(data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -23,12 +23,42 @@ function createInfoServer(httpsOptions, sharedState, robot, GCode, senders) {
|
||||
|
||||
/* ---------- API ---------- */
|
||||
if (req.url === '/api/status') {
|
||||
const sendersStatus = senders.map(({ name, instance }) => {
|
||||
const status = instance?.getStatus ? instance.getStatus() : {
|
||||
state: instance?.isTestMode ? 'connected' : instance?.tSocket ? 'connected' : 'disconnected',
|
||||
url: instance?.url || null,
|
||||
error: instance?.error || null,
|
||||
isTestMode: !!instance?.isTestMode,
|
||||
reconnectAttempt: instance?.reconnectAttempt || 0,
|
||||
reconnectTimer: !!instance?.reconnectTimer
|
||||
};
|
||||
|
||||
const state = status.state || (instance?.tSocket ? 'connected' : 'disconnected');
|
||||
const health = state === 'connected'
|
||||
? 'ok'
|
||||
: state === 'reconnecting'
|
||||
? 'warning'
|
||||
: 'disconnected';
|
||||
const reason = state === 'disconnected'
|
||||
? status.error || 'no active socket connection'
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
name,
|
||||
state,
|
||||
url: status.url || null,
|
||||
isTestMode: !!status.isTestMode,
|
||||
error: status.error || null,
|
||||
reconnectAttempt: status.reconnectAttempt || 0,
|
||||
reconnectTimer: !!status.reconnectTimer,
|
||||
health,
|
||||
reason
|
||||
};
|
||||
});
|
||||
|
||||
const status = {
|
||||
clients: sharedState.connectedClients,
|
||||
senders: senders.map(s => ({
|
||||
name: s.name,
|
||||
status: s.instance?.tSocket ? 'connected' : 'disconnected'
|
||||
})),
|
||||
senders: sendersStatus,
|
||||
lastCommands: sharedState.lastCommands,
|
||||
lastPings: sharedState.lastPings
|
||||
};
|
||||
|
||||
140
startRobot.js
140
startRobot.js
@@ -2,64 +2,138 @@ const fs = require('fs');
|
||||
const https = require('https');
|
||||
const Robot = require('./robot/Robot');
|
||||
const GCode = require('./robot/GCode');
|
||||
const TenetSender = require('./robot/TelnetSenderGRBL');
|
||||
|
||||
|
||||
const robot = new Robot(250, 264, 100);
|
||||
const TelnetSender = require('./robot/TelnetSenderGRBL');
|
||||
|
||||
const initInputWS = require('./server/InputWS');
|
||||
const createInfoServer = require('./server/InfoServer');
|
||||
|
||||
/* ---------- HTTPS Server to connect to---------- */
|
||||
const httpsOptions = {
|
||||
function loadHttpsOptions(fsModule) {
|
||||
try {
|
||||
return {
|
||||
enable: true,
|
||||
key: fs.readFileSync('https/localhost.key'),
|
||||
cert: fs.readFileSync('https/localhost.pem'),
|
||||
key: fsModule.readFileSync('https/localhost.key'),
|
||||
cert: fsModule.readFileSync('https/localhost.pem'),
|
||||
passphrase: 'abcd'
|
||||
};
|
||||
const httpsServer = https.createServer(httpsOptions);
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to load HTTPS certificate/key: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function getSenderConnectionStatus(senderInfo) {
|
||||
const { name, instance } = senderInfo;
|
||||
|
||||
let status = 'disconnected';
|
||||
let reason = 'no active socket connection';
|
||||
|
||||
if (instance?.getStatus) {
|
||||
const senderStatus = instance.getStatus();
|
||||
status = senderStatus.state || status;
|
||||
reason = senderStatus.state === 'disconnected' ? senderStatus.error || reason : undefined;
|
||||
} else if (instance?.isTestMode || instance?.tSocket) {
|
||||
status = 'connected';
|
||||
reason = undefined;
|
||||
}
|
||||
|
||||
return { name, status, reason };
|
||||
}
|
||||
|
||||
function createApp(options = {}) {
|
||||
const {
|
||||
fsModule = fs,
|
||||
httpsModule = https,
|
||||
processEnv = process.env,
|
||||
RobotClass = Robot,
|
||||
GCodeModule = GCode,
|
||||
TelnetSenderClass = TelnetSender,
|
||||
initInputWSFn = initInputWS,
|
||||
createInfoServerFn = createInfoServer,
|
||||
setTimeoutFn = setTimeout,
|
||||
consoleObj = console
|
||||
} = options;
|
||||
|
||||
const startupStatus = {
|
||||
https: { ok: false, error: null },
|
||||
senders: []
|
||||
};
|
||||
|
||||
let httpsOptions;
|
||||
try {
|
||||
httpsOptions = loadHttpsOptions(fsModule);
|
||||
startupStatus.https = { ok: true };
|
||||
} catch (err) {
|
||||
startupStatus.https = { ok: false, error: err.message };
|
||||
consoleObj.error(startupStatus.https.error);
|
||||
return { startupStatus };
|
||||
}
|
||||
|
||||
const httpsServer = httpsModule.createServer(httpsOptions);
|
||||
|
||||
const robot = new RobotClass(250, 264, 100);
|
||||
|
||||
/* ---------- WebSocket Input ---------- */
|
||||
const sharedState = {
|
||||
connectedClients: [],
|
||||
lastCommands: [],
|
||||
lastPings: []
|
||||
};
|
||||
initInputWS(httpsServer, robot, GCode, sharedState);
|
||||
|
||||
/* ---------- GRBL Sender take commands from robot to FluidNC or Hardware---------- */
|
||||
const baseIP = process.env.GRBL_BASE_IP ?? "fluidNcBase.local";
|
||||
const elbowIP= process.env.GRBL_ELLBOW_IP ?? "fluidNcEllbow.local";
|
||||
const handIP = process.env.GRBL_HAND_IP ?? "fluidNcHand.local";
|
||||
initInputWSFn(httpsServer, robot, GCodeModule, sharedState);
|
||||
|
||||
const telnetSender1 = new TenetSender(baseIP, 2300, "x", "y", "z");
|
||||
const telnetSender2 = new TenetSender(elbowIP, 5000, "a", null, null);
|
||||
const telnetSender3 = new TenetSender(handIP, 5000, "c", "e", "b");
|
||||
const baseIP = processEnv.GRBL_BASE_IP ?? 'fluidNcBase.local';
|
||||
const elbowIP = processEnv.GRBL_ELLBOW_IP ?? 'fluidNcEllbow.local';
|
||||
const handIP = processEnv.GRBL_HAND_IP ?? 'fluidNcHand.local';
|
||||
|
||||
setTimeout(() => {
|
||||
[telnetSender1, telnetSender2, telnetSender3].forEach(s => {
|
||||
if (s?.tSocket) robot.cmdReceivers.push(s);
|
||||
const telnetSender1 = new TelnetSenderClass(baseIP, 2300, 'x', 'y', 'z');
|
||||
const telnetSender2 = new TelnetSenderClass(elbowIP, 5000, 'a', null, null);
|
||||
const telnetSender3 = new TelnetSenderClass(handIP, 5000, 'c', 'e', 'b');
|
||||
|
||||
const senders = [
|
||||
{ name: 'Base', instance: telnetSender1 },
|
||||
{ name: 'Elbow', instance: telnetSender2 },
|
||||
{ name: 'Hand', instance: telnetSender3 }
|
||||
];
|
||||
|
||||
startupStatus.senders = senders.map(getSenderConnectionStatus);
|
||||
const disconnectedSenders = startupStatus.senders.filter(s => s.status === 'disconnected');
|
||||
if (disconnectedSenders.length > 0) {
|
||||
consoleObj.warn(`Startup warning: ${disconnectedSenders.length} sender(s) disconnected at startup.`);
|
||||
}
|
||||
|
||||
setTimeoutFn(() => {
|
||||
senders.forEach(s => {
|
||||
if (s.instance?.tSocket) robot.cmdReceivers.push(s.instance);
|
||||
});
|
||||
}, 5000);
|
||||
|
||||
/* ---------- Start Input Server ---------- */
|
||||
const port = Number(process.env.PORT) || 2095;
|
||||
const port = Number(processEnv.PORT) || 2095;
|
||||
httpsServer.listen(port);
|
||||
console.log(`Input HTTPS/WebSocket on https://localhost:${port}`);
|
||||
consoleObj.log(`Input HTTPS/WebSocket on https://localhost:${port}`);
|
||||
|
||||
/* ---------- Info Server ---------- */
|
||||
const infoServer = createInfoServer(
|
||||
const infoServer = createInfoServerFn(
|
||||
httpsOptions,
|
||||
sharedState,
|
||||
robot,
|
||||
GCode,
|
||||
[
|
||||
{ name: "Base", instance: telnetSender1 },
|
||||
{ name: "Elbow", instance: telnetSender2 },
|
||||
{ name: "Hand", instance: telnetSender3 }
|
||||
]
|
||||
GCodeModule,
|
||||
senders
|
||||
);
|
||||
|
||||
const infoPort = 2098;
|
||||
infoServer.listen(infoPort);
|
||||
console.log(`Info server on https://localhost:${infoPort}`);
|
||||
consoleObj.log(`Info server on https://localhost:${infoPort}`);
|
||||
|
||||
return {
|
||||
httpsServer,
|
||||
infoServer,
|
||||
robot,
|
||||
senders: senders.map(s => s.instance),
|
||||
sharedState,
|
||||
httpsOptions,
|
||||
startupStatus
|
||||
};
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
createApp();
|
||||
}
|
||||
|
||||
module.exports = { createApp };
|
||||
|
||||
@@ -79,8 +79,73 @@ describe('InfoServer', () => {
|
||||
expect(status.lastCommands).toEqual(['G1 X10 Y10']);
|
||||
expect(status.lastPings).toEqual(['Ping']);
|
||||
expect(status.senders).toEqual([
|
||||
{ name: 'Base', status: 'connected' },
|
||||
{ name: 'Hand', status: 'disconnected' }
|
||||
{
|
||||
name: 'Base',
|
||||
state: 'connected',
|
||||
url: null,
|
||||
isTestMode: false,
|
||||
error: null,
|
||||
reconnectAttempt: 0,
|
||||
reconnectTimer: false,
|
||||
health: 'ok',
|
||||
reason: undefined
|
||||
},
|
||||
{
|
||||
name: 'Hand',
|
||||
state: 'disconnected',
|
||||
url: null,
|
||||
isTestMode: false,
|
||||
error: null,
|
||||
reconnectAttempt: 0,
|
||||
reconnectTimer: false,
|
||||
health: 'disconnected',
|
||||
reason: 'no active socket connection'
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
test('returns sender health details from instance.getStatus()', async () => {
|
||||
const key = fs.readFileSync('https/localhost.key');
|
||||
const cert = fs.readFileSync('https/localhost.pem');
|
||||
const httpsOptions = { key, cert, passphrase: 'abcd' };
|
||||
|
||||
const sharedState = { connectedClients: [], lastCommands: [], lastPings: [] };
|
||||
const robot = { x: 0, y: 0, z: 0, phi: 0, theta: 0, psi: 0 };
|
||||
const senders = [
|
||||
{
|
||||
name: 'Reconnect',
|
||||
instance: {
|
||||
getStatus: () => ({
|
||||
state: 'reconnecting',
|
||||
url: 'reconnect.test',
|
||||
error: 'timeout',
|
||||
isTestMode: false,
|
||||
reconnectAttempt: 2,
|
||||
reconnectTimer: true
|
||||
})
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
server = createInfoServer(httpsOptions, sharedState, robot, GCode, senders);
|
||||
port = await listen(server);
|
||||
|
||||
const { statusCode, body } = await request(`https://127.0.0.1:${port}/api/status`);
|
||||
expect(statusCode).toBe(200);
|
||||
|
||||
const status = JSON.parse(body);
|
||||
expect(status.senders).toEqual([
|
||||
{
|
||||
name: 'Reconnect',
|
||||
state: 'reconnecting',
|
||||
url: 'reconnect.test',
|
||||
isTestMode: false,
|
||||
error: 'timeout',
|
||||
reconnectAttempt: 2,
|
||||
reconnectTimer: true,
|
||||
health: 'warning',
|
||||
reason: undefined
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -93,4 +93,24 @@ describe('InputWS', () => {
|
||||
|
||||
client.close();
|
||||
});
|
||||
|
||||
test('receives GCode text and broadcasts updated position', async () => {
|
||||
server = http.createServer();
|
||||
const sharedState = { connectedClients: [], lastCommands: [], lastPings: [] };
|
||||
const robot = createDummyRobot();
|
||||
|
||||
wss = initInputWS(server, robot, GCode, sharedState);
|
||||
port = await listen(server);
|
||||
const client = await connectWebSocket(port);
|
||||
|
||||
const messagePromise = waitForMessage(client);
|
||||
client.send('G1 X1 Y2 Z3');
|
||||
|
||||
const message = await messagePromise;
|
||||
const parsed = JSON.parse(message);
|
||||
expect(parsed.position).toEqual({ x: 1, y: 2, z: 3, a: 0, b: 0, c: 0 });
|
||||
expect(robot.sendCommand).toHaveBeenCalled();
|
||||
|
||||
client.close();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,76 +1,72 @@
|
||||
var Sender = require('../robot/WSSenderGrbl.js')
|
||||
const WSSenderGrbl = require('../robot/WSSenderGrbl.js');
|
||||
|
||||
describe("WSSenderGrbl implements SenderInterface", () => {
|
||||
|
||||
describe("WS-SenderGRBL.execCommand", () => {
|
||||
test("is an instance of SenderInterface and exposes required methods", () => {
|
||||
const SenderInterface = require('../robot/SenderInterface');
|
||||
const sender = new WSSenderGrbl("test.test", 2300, "x", "y", "z");
|
||||
|
||||
test("writes correct G-code to mocked WS tSocket", () => {
|
||||
expect(sender).toBeInstanceOf(SenderInterface);
|
||||
expect(typeof sender.connect).toBe('function');
|
||||
expect(typeof sender.send).toBe('function');
|
||||
expect(typeof sender.getStatus).toBe('function');
|
||||
expect(typeof sender.disconnect).toBe('function');
|
||||
});
|
||||
|
||||
// Create instance (will try real connection, but we override tSocket immediately)
|
||||
const sender = new Sender(urlGRBL = "test.test", maxSpeedF = 2300, xAxisGrbl = "x", yAxisGrbl = "y", zAxisGrbl = "z");
|
||||
test("test mode has connected status", () => {
|
||||
const sender = new WSSenderGrbl("test.test", 2300, "x", "y", "z");
|
||||
expect(sender.getStatus()).toMatchObject({ state: 'connected', isTestMode: true });
|
||||
});
|
||||
|
||||
// Mock tSocket.write
|
||||
sender.tSocket = {
|
||||
written: "",
|
||||
write: function(txt) {
|
||||
this.written = txt; // store what was written
|
||||
}
|
||||
};
|
||||
test("send() returns false when not connected, true in test mode", () => {
|
||||
const sender = new WSSenderGrbl("test.test", 2300, "x", "y", "z");
|
||||
expect(sender.send("G1 x1")).toBe(true);
|
||||
|
||||
// Provide some sample motion data
|
||||
const mOld = { x: 0, y: 0, z: 0, a:0, b:0, c:0, e:0 }; // not used in your code
|
||||
sender.ws = null;
|
||||
expect(sender.send("G1 x1")).toBe(false);
|
||||
});
|
||||
|
||||
test("disconnect() transitions to disconnected state", () => {
|
||||
const sender = new WSSenderGrbl("test.test", 2300, "x", "y", "z");
|
||||
sender.disconnect();
|
||||
expect(sender.getStatus()).toMatchObject({ state: 'disconnected' });
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("WSSenderGrbl.execCommand writes correct G-code via send()", () => {
|
||||
|
||||
test("writes correct G-code for xyz axes", () => {
|
||||
const sender = new WSSenderGrbl("test.test", 2300, "x", "y", "z");
|
||||
|
||||
const mOld = { x: 0, y: 0, z: 0, a: 0, b: 0, c: 0, e: 0 };
|
||||
const mNew = { x: 12.34, y: 1.0, z: 2.0, a: 0, b: 0, c: 0, e: 0 };
|
||||
|
||||
sender.execCommand("G1", mOld, mNew);
|
||||
|
||||
// ✅ verify output
|
||||
expect(sender.tSocket.written).toBe("G90 G1 x12.34 y57.30 z57.30 f2300.00\r\n");
|
||||
expect(sender.ws.written).toBe("G90 G1 x12.34 y57.30 z57.30 f2300.00\n");
|
||||
});
|
||||
|
||||
test("writes correct G-code for elbow (a axis)", () => {
|
||||
const sender = new WSSenderGrbl("test.test", 2300, "a", null, null);
|
||||
|
||||
test("writes correct G-code to mocked tSocket Ellbow", () => {
|
||||
|
||||
// Create instance (will try real connection, but we override tSocket immediately)
|
||||
const sender = new Sender(urlGRBL = "test.test", maxSpeedF = 2300, xAxisGrbl = "a", yAxisGrbl = null, zAxisGrbl = null );
|
||||
|
||||
// Mock tSocket.write
|
||||
sender.tSocket = {
|
||||
written: "",
|
||||
write: function(txt) {
|
||||
this.written = txt; // store what was written
|
||||
}
|
||||
};
|
||||
|
||||
// Provide some sample motion data
|
||||
const mOld = { x: 0, y: 0, z: 0, a:Math.PI, b:0, c:0, e:0 }; // not used in your code
|
||||
const mOld = { x: 0, y: 0, z: 0, a: Math.PI, b: 0, c: 0, e: 0 };
|
||||
const mNew = { x: 12.34, y: Math.PI / 2, z: 0, a: Math.PI / 8, b: 0, c: 0, e: 0 };
|
||||
|
||||
sender.execCommand("G1", mOld, mNew);
|
||||
|
||||
// ✅ verify output
|
||||
expect(sender.tSocket.written).toBe("G90 G1 x22.50 f2300.00\r\n");
|
||||
expect(sender.ws.written).toBe("G90 G1 x22.50 f2300.00\n");
|
||||
});
|
||||
|
||||
test("writes correct G-code G92 to mocked WS tSocket", () => {
|
||||
test("G92 command is sent without extra G90 prefix", () => {
|
||||
const sender = new WSSenderGrbl("test.test", 2300, "x", "y", "z");
|
||||
|
||||
// Create instance (will try real connection, but we override tSocket immediately)
|
||||
const sender = new Sender(urlGRBL = "test.test", maxSpeedF = 2300, xAxisGrbl = "x", yAxisGrbl = "y", zAxisGrbl = "z");
|
||||
|
||||
// Mock tSocket.write
|
||||
sender.tSocket = {
|
||||
written: "",
|
||||
write: function(txt) {
|
||||
this.written = txt; // store what was written
|
||||
}
|
||||
};
|
||||
|
||||
// Provide some sample motion data
|
||||
const mOld = { x: 0, y: 0, z: 0, a:0, b:0, c:0, e:0 }; // not used in your code
|
||||
const mOld = { x: 0, y: 0, z: 0, a: 0, b: 0, c: 0, e: 0 };
|
||||
const mNew = { x: 12.34, y: 1.0, z: 2.0, a: 0, b: 0, c: 0, e: 0 };
|
||||
|
||||
sender.execCommand("G92", mOld, mNew);
|
||||
|
||||
// ✅ verify output
|
||||
expect(sender.tSocket.written.replace("G90 ","")).toBe("G92 x12.34 y57.30 z57.30 f2300.00\r\n");
|
||||
expect(sender.ws.written.replace("G90 ", "")).toBe("G92 x12.34 y57.30 z57.30 f2300.00\n");
|
||||
});
|
||||
|
||||
});
|
||||
103
test/SenderInterface.test.js
Normal file
103
test/SenderInterface.test.js
Normal file
@@ -0,0 +1,103 @@
|
||||
const EventEmitter = require('events');
|
||||
const SenderInterface = require('../robot/SenderInterface');
|
||||
const TelnetSender = require('../robot/TelnetSenderGRBL');
|
||||
|
||||
describe('Sender Interface and TelnetSenderGRBL implementation', () => {
|
||||
test('TelnetSenderGRBL implements the required sender interface methods', () => {
|
||||
const sender = new TelnetSender('test.test', 2300, 'x', 'y', 'z');
|
||||
|
||||
expect(sender).toBeInstanceOf(SenderInterface);
|
||||
expect(typeof sender.connect).toBe('function');
|
||||
expect(typeof sender.send).toBe('function');
|
||||
expect(typeof sender.getStatus).toBe('function');
|
||||
expect(typeof sender.disconnect).toBe('function');
|
||||
});
|
||||
|
||||
test('test mode sender rejects invalid send payloads and supports idempotent disconnect', () => {
|
||||
const sender = new TelnetSender('test.test', 2300, 'x', 'y', 'z');
|
||||
|
||||
expect(sender.getStatus()).toMatchObject({ state: 'connected' });
|
||||
|
||||
sender.tSocket = null;
|
||||
expect(sender.send('HELLO')).toBe(false);
|
||||
expect(sender.send('')).toBe(false);
|
||||
|
||||
sender.disconnect();
|
||||
expect(sender.getStatus()).toMatchObject({ state: 'disconnected' });
|
||||
|
||||
sender.disconnect();
|
||||
expect(sender.getStatus()).toMatchObject({ state: 'disconnected' });
|
||||
});
|
||||
|
||||
test('sender reconnects on connection failure and eventually connects', async () => {
|
||||
let connectAttempts = 0;
|
||||
const netMock = {
|
||||
createConnection: () => {
|
||||
const socket = new EventEmitter();
|
||||
socket.end = jest.fn();
|
||||
|
||||
process.nextTick(() => {
|
||||
connectAttempts += 1;
|
||||
if (connectAttempts === 1) {
|
||||
socket.emit('error', new Error('connection failed'));
|
||||
} else {
|
||||
socket.emit('connect');
|
||||
}
|
||||
});
|
||||
|
||||
return socket;
|
||||
}
|
||||
};
|
||||
|
||||
class DummyTelnetSocket {
|
||||
constructor(connection) {
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
on(event, listener) {
|
||||
this.connection.on(event, listener);
|
||||
return this;
|
||||
}
|
||||
|
||||
write(txt) {
|
||||
this.connection.written = txt;
|
||||
}
|
||||
}
|
||||
|
||||
const sender = new TelnetSender('localhost', 2300, 'x', 'y', 'z', null, null, null, null, {
|
||||
netModule: netMock,
|
||||
TelnetSocketClass: DummyTelnetSocket,
|
||||
setTimeoutFn: (fn) => fn(),
|
||||
autoConnect: false,
|
||||
reconnectDelay: 1,
|
||||
maxReconnectDelay: 2
|
||||
});
|
||||
|
||||
const result = await sender.connect();
|
||||
expect(connectAttempts).toBe(2);
|
||||
expect(result.getStatus()).toMatchObject({
|
||||
state: 'connected',
|
||||
url: 'localhost'
|
||||
});
|
||||
expect(result.getStatus().reconnectTimer).toBe(false);
|
||||
});
|
||||
|
||||
test('test mode sender has connected status and can send/disconnect', async () => {
|
||||
const sender = new TelnetSender('test.test', 2300, 'x', 'y', 'z');
|
||||
|
||||
expect(sender.getStatus()).toMatchObject({
|
||||
state: 'connected',
|
||||
url: 'test.test',
|
||||
isTestMode: true
|
||||
});
|
||||
|
||||
expect(sender.send('HELLO')).toBe(true);
|
||||
expect(sender.tSocket.written).toBe('HELLO\r\n');
|
||||
|
||||
sender.disconnect();
|
||||
expect(sender.getStatus()).toMatchObject({ state: 'disconnected' });
|
||||
|
||||
await expect(sender.connect()).resolves.toBe(sender);
|
||||
expect(sender.getStatus()).toMatchObject({ state: 'connected' });
|
||||
});
|
||||
});
|
||||
114
test/StartRobot.test.js
Normal file
114
test/StartRobot.test.js
Normal file
@@ -0,0 +1,114 @@
|
||||
const { createApp } = require('../startRobot');
|
||||
|
||||
describe('startRobot orchestrator', () => {
|
||||
test('creates HTTPS and info servers and binds modules', () => {
|
||||
const readFileSync = jest.fn()
|
||||
.mockReturnValueOnce('fake-key')
|
||||
.mockReturnValueOnce('fake-cert');
|
||||
|
||||
const httpsServerMock = {
|
||||
listen: jest.fn()
|
||||
};
|
||||
|
||||
const httpsModuleMock = {
|
||||
createServer: jest.fn(() => httpsServerMock)
|
||||
};
|
||||
|
||||
const initInputWS = jest.fn();
|
||||
const infoServerMock = {
|
||||
listen: jest.fn()
|
||||
};
|
||||
const createInfoServer = jest.fn(() => infoServerMock);
|
||||
|
||||
const TelnetSenderClass = jest.fn(() => ({ tSocket: null }));
|
||||
|
||||
const robotInstances = [];
|
||||
class RobotClass {
|
||||
constructor() {
|
||||
this.cmdReceivers = [];
|
||||
robotInstances.push(this);
|
||||
}
|
||||
}
|
||||
|
||||
const result = createApp({
|
||||
fsModule: { readFileSync },
|
||||
httpsModule: httpsModuleMock,
|
||||
processEnv: {},
|
||||
RobotClass,
|
||||
GCodeModule: { dummy: true },
|
||||
TelnetSenderClass,
|
||||
initInputWSFn: initInputWS,
|
||||
createInfoServerFn: createInfoServer,
|
||||
setTimeoutFn: (fn) => fn(),
|
||||
consoleObj: { log: jest.fn(), warn: jest.fn(), error: jest.fn() }
|
||||
});
|
||||
|
||||
expect(readFileSync).toHaveBeenCalledTimes(2);
|
||||
expect(httpsModuleMock.createServer).toHaveBeenCalledWith({
|
||||
enable: true,
|
||||
key: 'fake-key',
|
||||
cert: 'fake-cert',
|
||||
passphrase: 'abcd'
|
||||
});
|
||||
|
||||
expect(initInputWS).toHaveBeenCalledWith(httpsServerMock, expect.any(RobotClass), { dummy: true }, expect.any(Object));
|
||||
expect(createInfoServer).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ key: 'fake-key', cert: 'fake-cert', passphrase: 'abcd' }),
|
||||
expect.any(Object),
|
||||
expect.any(RobotClass),
|
||||
{ dummy: true },
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ name: 'Base', instance: expect.any(Object) }),
|
||||
expect.objectContaining({ name: 'Elbow', instance: expect.any(Object) }),
|
||||
expect.objectContaining({ name: 'Hand', instance: expect.any(Object) })
|
||||
])
|
||||
);
|
||||
|
||||
expect(httpsServerMock.listen).toHaveBeenCalledWith(2095);
|
||||
expect(infoServerMock.listen).toHaveBeenCalledWith(2098);
|
||||
|
||||
expect(result).toHaveProperty('httpsServer', httpsServerMock);
|
||||
expect(result).toHaveProperty('infoServer', infoServerMock);
|
||||
expect(result).toHaveProperty('senders');
|
||||
expect(result.senders).toHaveLength(3);
|
||||
expect(result.startupStatus).toEqual({
|
||||
https: { ok: true },
|
||||
senders: [
|
||||
{ name: 'Base', status: 'disconnected', reason: 'no active socket connection' },
|
||||
{ name: 'Elbow', status: 'disconnected', reason: 'no active socket connection' },
|
||||
{ name: 'Hand', status: 'disconnected', reason: 'no active socket connection' }
|
||||
]
|
||||
});
|
||||
expect(result.sharedState.connectedClients).toEqual([]);
|
||||
});
|
||||
|
||||
test('reports missing HTTPS certificates on startup', () => {
|
||||
const readFileSync = jest.fn().mockImplementation(() => {
|
||||
throw new Error('ENOENT: no such file or directory');
|
||||
});
|
||||
|
||||
const httpsModuleMock = {
|
||||
createServer: jest.fn()
|
||||
};
|
||||
|
||||
const result = createApp({
|
||||
fsModule: { readFileSync },
|
||||
httpsModule: httpsModuleMock,
|
||||
processEnv: {},
|
||||
RobotClass: class {},
|
||||
GCodeModule: { dummy: true },
|
||||
TelnetSenderClass: jest.fn(),
|
||||
initInputWSFn: jest.fn(),
|
||||
createInfoServerFn: jest.fn(),
|
||||
setTimeoutFn: jest.fn(),
|
||||
consoleObj: { log: jest.fn(), error: jest.fn(), warn: jest.fn() }
|
||||
});
|
||||
|
||||
expect(result.startupStatus.https.ok).toBe(false);
|
||||
expect(result.startupStatus.https.error).toMatch(/Failed to load HTTPS certificate\/key/);
|
||||
expect(httpsModuleMock.createServer).not.toHaveBeenCalled();
|
||||
expect(result.httpsServer).toBeUndefined();
|
||||
expect(result.infoServer).toBeUndefined();
|
||||
expect(result.senders).toBeUndefined();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user