78 lines
2.9 KiB
JavaScript
78 lines
2.9 KiB
JavaScript
/**
|
||
* driverClient.js – WebSocket-Transport zum appRobotDriver
|
||
*
|
||
* Der Driver nimmt Steuerbefehle als Plain-Text-G-Code über einen WebSocket
|
||
* entgegen (wss://…:2096, self-signed), NICHT über HTTP — siehe
|
||
* appRobotDriver/doc/API.md. Ein früher angenommenes `POST /api/state` existiert
|
||
* dort nicht (war Platzhalter, vgl. doc/accessRobotAPI.md). G92 setzt am Driver
|
||
* die Motorposition ohne Bewegung (intern M92) = exakt die Homing-Semantik.
|
||
*
|
||
* DRIVER_WS_URL nicht gesetzt → kein Kontakt, klarer 501-Fehler (analog zum
|
||
* früheren ROBOT_URL-Verhalten).
|
||
*/
|
||
import { WebSocket } from 'ws';
|
||
|
||
const DRIVER_WS_URL = process.env.DRIVER_WS_URL || '';
|
||
|
||
/** true, wenn ein Driver-WebSocket konfiguriert ist. */
|
||
export function isDriverConfigured() {
|
||
return Boolean(DRIVER_WS_URL);
|
||
}
|
||
|
||
/**
|
||
* Öffnet eine kurzlebige WS-Verbindung zum Driver, sendet eine G-Code-Zeile und
|
||
* wartet auf die erste Antwort (Positions-JSON bzw. Fehler-Envelope). Der Driver
|
||
* broadcastet nach jedem G-Code das aktuelle Positions-JSON an alle Clients —
|
||
* der Sender ist selbst Client und bekommt es zurück.
|
||
*
|
||
* @param {string} line z.B. "G92 X1 Y2 …"
|
||
* @param {{timeoutMs?: number}} [opts]
|
||
* @returns {Promise<{ok:boolean, sent:string, response?:any, error?:string, note?:string}>}
|
||
*/
|
||
export function sendGcode(line, { timeoutMs = 4000 } = {}) {
|
||
const text = String(line ?? '').trim();
|
||
if (!text) {
|
||
return Promise.reject(Object.assign(new Error('Leere G-Code-Zeile'), { statusCode: 400 }));
|
||
}
|
||
if (!DRIVER_WS_URL) {
|
||
return Promise.reject(Object.assign(
|
||
new Error('DRIVER_WS_URL ist nicht konfiguriert'), { statusCode: 501 }));
|
||
}
|
||
|
||
return new Promise((resolve, reject) => {
|
||
// Self-signed Cert am Driver → Zertifikatsprüfung deaktivieren (interner Hop).
|
||
const ws = new WebSocket(DRIVER_WS_URL, { rejectUnauthorized: false });
|
||
let settled = false;
|
||
|
||
const finish = (fn, arg) => {
|
||
if (settled) return;
|
||
settled = true;
|
||
clearTimeout(timer);
|
||
try { ws.close(); } catch { /* egal */ }
|
||
fn(arg);
|
||
};
|
||
|
||
// Gesendet, aber keine Antwort rechtzeitig: kein harter Fehler — der Befehl
|
||
// ist raus, der Driver antwortet nur evtl. nicht broadcastfähig.
|
||
const timer = setTimeout(() => {
|
||
finish(resolve, { ok: true, sent: text, response: null, note: 'keine Antwort (Timeout)' });
|
||
}, timeoutMs);
|
||
|
||
ws.on('open', () => ws.send(text));
|
||
|
||
ws.on('message', (data) => {
|
||
const raw = data.toString();
|
||
let parsed;
|
||
try { parsed = JSON.parse(raw); } catch { parsed = raw; }
|
||
if (parsed && typeof parsed === 'object' && parsed.type === 'error') {
|
||
finish(resolve, { ok: false, sent: text, error: parsed.message || raw, response: parsed });
|
||
} else {
|
||
finish(resolve, { ok: true, sent: text, response: parsed });
|
||
}
|
||
});
|
||
|
||
ws.on('error', (err) => finish(reject, Object.assign(
|
||
new Error(`Driver-WS-Fehler: ${err.message}`), { statusCode: 502 })));
|
||
});
|
||
}
|