G92 senden
This commit is contained in:
42
server/buildG92.cjs
Normal file
42
server/buildG92.cjs
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* buildG92.cjs
|
||||
* Baut aus einem Homing-State {x,y,z,a,b,c,e} einen G92-G-Code-String.
|
||||
*
|
||||
* G92 setzt am appRobotDriver die Motorposition OHNE Bewegung (intern als M92
|
||||
* verarbeitet, siehe appRobotDriver/doc/API.md + robot/RobotController.js) —
|
||||
* exakt die Homing-Semantik. Die Achsbuchstaben bilden 1:1 auf die Motorachsen
|
||||
* ab: X→xMotor, Y→alpha, Z→beta, A→a, B→b, C→c, E→e.
|
||||
*
|
||||
* Die Homing-Kette (4b: Arm1→y, Ellbow→z, Arm2→a, Hand→b) bestimmt c (Palm) und
|
||||
* e (Greifer) nicht. Entscheidung: fehlende Achsen als 0 mitsenden
|
||||
* (`fillMissingWithZero`), damit G92 alle 7 Achsen trägt.
|
||||
*
|
||||
* CommonJS, damit Jest (CJS) und der ESM-Server dieselbe Funktion nutzen
|
||||
* (gleiches Muster wie spinNormalize.cjs / homingXEstimate.cjs).
|
||||
*/
|
||||
|
||||
// Reihenfolge + Achsbuchstaben wie vom Driver erwartet.
|
||||
const AXES = [
|
||||
['x', 'X'], ['y', 'Y'], ['z', 'Z'],
|
||||
['a', 'A'], ['b', 'B'], ['c', 'C'], ['e', 'E'],
|
||||
];
|
||||
|
||||
/**
|
||||
* @param {Record<string, number|null>} state flacher Joint-State (accumulated_state)
|
||||
* @param {{decimals?: number, fillMissingWithZero?: boolean}} [opts]
|
||||
* @returns {string} z.B. "G92 X192.73 Y35.99 Z-30.88 A-1.70 B12.34 C0.00 E0.00"
|
||||
*/
|
||||
function buildG92(state = {}, { decimals = 2, fillMissingWithZero = true } = {}) {
|
||||
const parts = [];
|
||||
for (const [key, axis] of AXES) {
|
||||
const num = Number(state?.[key]);
|
||||
if (state?.[key] != null && Number.isFinite(num)) {
|
||||
parts.push(`${axis}${num.toFixed(decimals)}`);
|
||||
} else if (fillMissingWithZero) {
|
||||
parts.push(`${axis}${(0).toFixed(decimals)}`);
|
||||
}
|
||||
}
|
||||
return `G92 ${parts.join(' ')}`;
|
||||
}
|
||||
|
||||
module.exports = { buildG92, AXES };
|
||||
77
server/driverClient.js
Normal file
77
server/driverClient.js
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* 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 })));
|
||||
});
|
||||
}
|
||||
@@ -12,6 +12,8 @@ import { assignByZRange, removeMarkerAssignment, alignSetToMeasured, assignMarke
|
||||
import multer from 'multer';
|
||||
import { runHoming, runHomingOffline } from './homingOrchestrator.js';
|
||||
import { fetchRobot, robotCachePath } from './robotConfig.js';
|
||||
import { sendGcode, isDriverConfigured } from './driverClient.js';
|
||||
import { buildG92 } from './buildG92.cjs';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
@@ -24,7 +26,8 @@ const publicDir = path.join(__dirname, '..', 'public');
|
||||
const snapshotsDir = path.join(publicDir, 'snapshots');
|
||||
const WEBCAM_URL = process.env.WEBCAM_URL || '';
|
||||
const BODYTRACKER_URL = process.env.BODYTRACKER_URL || '';
|
||||
const ROBOT_URL = process.env.ROBOT_URL || '';
|
||||
// Roboter-Transport läuft über den Driver-WebSocket (DRIVER_WS_URL,
|
||||
// server/driverClient.js), nicht mehr über HTTP ROBOT_URL.
|
||||
const HTTPS_KEY_PATH = process.env.HTTPS_KEY_PATH || path.join(__dirname, '..', 'https', 'localhost.key');
|
||||
const HTTPS_CERT_PATH = process.env.HTTPS_CERT_PATH || path.join(__dirname, '..', 'https', 'localhost.pem');
|
||||
const HTTPS_PASSPHRASE = process.env.HTTPS_PASSPHRASE || 'abcd';
|
||||
@@ -912,29 +915,50 @@ app.post('/api/homing/run', async (req, res) => {
|
||||
|
||||
/**
|
||||
* POST /api/homing/send-state
|
||||
* Sendet { state: { x, y, z, a, b, c, e } } an ROBOT_URL/api/state.
|
||||
* Baut aus { state: { x, y, z, a, b[, c, e] } } ein G92 und sendet es als
|
||||
* Plain-Text-G-Code über den Driver-WebSocket (DRIVER_WS_URL). G92 setzt am
|
||||
* Driver die Motorposition ohne Bewegung (intern M92) = Homing.
|
||||
* Fehlende Achsen (c/Palm, e/Greifer werden vom Homing nicht bestimmt) werden
|
||||
* als 0 mitgesendet (siehe server/buildG92.cjs).
|
||||
*/
|
||||
app.post('/api/homing/send-state', async (req, res) => {
|
||||
try {
|
||||
const { state } = req.body ?? {};
|
||||
if (!state) return res.status(400).json({ error: '"state" fehlt' });
|
||||
if (!ROBOT_URL) return res.status(501).json({ error: 'ROBOT_URL ist nicht konfiguriert' });
|
||||
if (!isDriverConfigured())
|
||||
return res.status(501).json({ error: 'DRIVER_WS_URL ist nicht konfiguriert' });
|
||||
|
||||
const url = new URL('/api/state', ROBOT_URL).toString();
|
||||
const upstream = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(state),
|
||||
});
|
||||
if (!upstream.ok) {
|
||||
const text = await upstream.text();
|
||||
return res.status(upstream.status).json({ error: `Robot-Fehler: ${text}` });
|
||||
}
|
||||
const result = await upstream.json().catch(() => ({}));
|
||||
return res.json({ ok: true, result });
|
||||
const gcode = buildG92(state);
|
||||
const result = await sendGcode(gcode);
|
||||
if (!result.ok)
|
||||
return res.status(502).json({ error: `Robot-Fehler: ${result.error}`, gcode });
|
||||
return res.json({ ok: true, gcode, result: result.response, note: result.note });
|
||||
} catch (err) {
|
||||
console.error('homing/send-state error:', err);
|
||||
return res.status(500).json({ error: String(err) });
|
||||
return res.status(err.statusCode || 500).json({ error: String(err.message || err) });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/robot/gcode { line: "G92 X… Y…" }
|
||||
* Sendet eine beliebige G-Code-Zeile über den Driver-WebSocket. Transport für
|
||||
* die G-Code-/Befehl-Buttons im Frontend (window.sendCommand) — ersetzt den
|
||||
* toten WSS-Altpfad.
|
||||
*/
|
||||
app.post('/api/robot/gcode', async (req, res) => {
|
||||
try {
|
||||
const line = (req.body?.line ?? '').toString().trim();
|
||||
if (!line) return res.status(400).json({ error: '"line" fehlt' });
|
||||
if (!isDriverConfigured())
|
||||
return res.status(501).json({ error: 'DRIVER_WS_URL ist nicht konfiguriert' });
|
||||
|
||||
const result = await sendGcode(line);
|
||||
if (!result.ok)
|
||||
return res.status(502).json({ error: `Robot-Fehler: ${result.error}`, line });
|
||||
return res.json({ ok: true, line, result: result.response, note: result.note });
|
||||
} catch (err) {
|
||||
console.error('robot/gcode error:', err);
|
||||
return res.status(err.statusCode || 500).json({ error: String(err.message || err) });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user