14 KiB
Commands from GCode File — Konzept & Implementierungsplan
Ausgangslage
Der Browser-Datei-Browser (appRobotFileservice:2100) kann Zeilen im aktiven
Programm navigieren. Der Driver (appRobotDriver) führt G-Code aus und hat
den Roboter angeschlossen.
Aktuell sind die zwei Pfade strikt getrennt:
| Pfad | Wer | Effekt |
|---|---|---|
Browser → POST /api/active/next (Fileservice) |
nur Browser | Cursor bewegt sich, Datei gespeichert — kein Roboter |
| WS-Client → Driver → FCode → Fileservice | externer Controller | Cursor bewegt sich und Zeile wird ausgeführt |
Das Ziel: der Browser bekommt einen Modus-Schalter. Im Modus Senden wandert der Cursor nicht nur, sondern der Roboter fährt auch dorthin.
Sicherheit und Netzwerk-Topologie
Internet / User
│
▼
appRobotFileservice (Port 2100, erreichbar von aussen)
│ Browser spricht nur hier hin (HTTP/REST)
│
│ lokales Netz (server-seitig)
▼
appRobotDriver (Port 2095, nur im LAN erreichbar)
│
▼
GRBL/FluidNC-Controller → Roboter-Hardware
Der Browser verbindet sich niemals direkt mit dem Driver. Der Fileservice ist die einzige Aussenschnittstelle. Die Verbindung zum Driver läuft server-seitig im lokalen Netz.
UI-Konzept
Drei Button-Gruppen in einer Zeile (Links · Mitte · Rechts)
[ |◀ First ◀ Prev Next ▶ Last ▶| ] [ ✕ Clear ⬇ .gcode ] [ ↕ Navigieren | ▶ Senden ]
Gruppe Links Gruppe Mitte Gruppe Rechts
CSS-Ansatz: .controls-row { display: flex; justify-content: space-between; },
jede Gruppe ist ein <div class="ctrl-group">.
Schalter Navigieren / Senden
Zwei Buttons als Radiogruppe (einer immer aktiv):
<div class="toggle-group">
<button id="btn-mode-nav" class="toggle active">↕ Navigieren</button>
<button id="btn-mode-send" class="toggle" >▶ Senden</button>
</div>
CSS-State .toggle.active hebt den aktiven Modus hervor (z. B. Hintergrund
var(--active), Rand var(--accent)). Der Senden-Button bleibt ausgegraut,
solange keine DRIVER_WS_URL konfiguriert ist.
Architektur im Modus "Senden"
Warum kein FCode an den Driver?
Wenn der Fileservice dem Driver ein FCode schickte (z. B. FPlus), würde
der Driver seinerseits über FCodeClient wieder POST /api/active/next auf
den Fileservice aufrufen — der Cursor bewegt sich zweimal.
Richtiger Weg: G-Code-Zeile direkt an den Driver-WS
Der Driver-WebSocket (InputWS.js) akzeptiert rohe G-Code-Befehle
(G90 G1 X… Y… A… F…) direkt, ohne Fileservice-Rückruf. Die Fileservice-Logik:
- Cursor-Schritt wie bisher (
_gotoIndex()), liefert die saubere G-Code-Zeile - Diese Zeile wird über eine server-seitige WS-Verbindung an den Driver gesendet
- Driver führt Inverse Kinematik aus, schickt an GRBL-Controller, broadcast M114
Ablauf im Modus "Senden"
Browser klickt Next ▶
│
├─[Navigieren]─► POST /api/active/next (Fileservice)
│ Cursor +1, Datei schreibt ;!, Browser rendert Cursor-Position
│
└─[Senden]──────► POST /api/active/next?execute=true (Fileservice)
Fileservice: Cursor +1, holt line = 'G90 G1 X250 Y0 A45 F1000'
Fileservice: sendet line über lokale WS-Verbindung an Driver
Driver: Inverse Kinematik → GRBL-Controller → Roboter fährt
Driver: broadcast M114 (Position) an alle Driver-WS-Clients
Fileservice antwortet mit { cursor, line } — Browser rendert Cursor
Nötige Code-Änderungen
1. public/index.html — JS
Neuer Zustand (kein WS im Browser):
let sendMode = false; // false = Navigieren, true = Senden
step()-Funktion — Buttons sperren während Ausführung, Fehler anzeigen:
const STEP_BUTTONS = ['btn-first', 'btn-prev', 'btn-next', 'btn-last'];
function setStepButtonsEnabled(enabled) {
STEP_BUTTONS.forEach(id => {
document.getElementById(id).disabled = !enabled;
});
}
async function step(endpoint) {
setStatus(elActStatus, sendMode ? '⏳ Sende an Roboter…' : '…');
if (sendMode) setStepButtonsEnabled(false); // während Roboterfahrt sperren
try {
const url = sendMode
? `/api/active/${endpoint}?execute=true`
: `/api/active/${endpoint}`;
const r = await apiFetch('POST', url);
setStatus(elActStatus, sendMode
? `✓ Ausgeführt → Cursor ${r.cursor}`
: `Cursor → ${r.cursor}`);
await refresh();
} catch (err) {
// err.message enthält den Driver-Fehlertext (z. B. 'inverse kinematics failed')
setStatus(elActStatus, `Fehler: ${err.message}`, true);
} finally {
if (sendMode) setStepButtonsEnabled(true); // immer wieder freigeben
}
}
Toggle-Buttons:
document.getElementById('btn-mode-nav').addEventListener('click', () => {
sendMode = false;
document.getElementById('btn-mode-nav').classList.add('active');
document.getElementById('btn-mode-send').classList.remove('active');
});
document.getElementById('btn-mode-send').addEventListener('click', () => {
sendMode = true;
document.getElementById('btn-mode-send').classList.add('active');
document.getElementById('btn-mode-nav').classList.remove('active');
});
Senden-Button beim Start deaktivieren, wenn kein Driver konfiguriert:
// Beim Start prüfen ob Driver-URL gesetzt ist
const cfg = await apiFetch('GET', '/api/config');
if (!cfg.driverWsUrl) {
document.getElementById('btn-mode-send').disabled = true;
document.getElementById('btn-mode-send').title = 'Driver WS nicht konfiguriert';
}
Layout — controls aufteilen:
<div class="controls-row">
<div class="ctrl-group">
<button id="btn-first" disabled>|◀ First</button>
<button id="btn-prev" disabled>◀ Prev</button>
<button id="btn-next" disabled>Next ▶</button>
<button id="btn-last" disabled>Last ▶|</button>
</div>
<div class="ctrl-group">
<button id="btn-clear" disabled>✕ Clear</button>
<a id="btn-download" class="dl-link" style="display:none" download>⬇ .gcode</a>
</div>
<div class="ctrl-group">
<div class="toggle-group">
<button id="btn-mode-nav" class="toggle active">↕ Navigieren</button>
<button id="btn-mode-send" class="toggle">▶ Senden</button>
</div>
</div>
</div>
2. public/index.css
/* Drei-Gruppen-Zeile */
.controls-row {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
margin-bottom: 12px;
flex-wrap: wrap;
}
.ctrl-group {
display: flex;
gap: 6px;
align-items: center;
}
/* Toggle-Schalter */
.toggle-group {
display: flex;
border: 1px solid #334155;
border-radius: 6px;
overflow: hidden;
}
.toggle-group .toggle {
border: none;
border-radius: 0;
border-right: 1px solid #334155;
padding: 6px 12px;
}
.toggle-group .toggle:last-child { border-right: none; }
.toggle-group .toggle.active {
background: var(--active);
color: var(--accent);
border-color: var(--accent);
}
3. Driver-Protokoll — Bestätigung und Fehler
InputWS.js sendet nach G-Code-Verarbeitung:
| Ergebnis | Empfänger | Format |
|---|---|---|
| Erfolg | Broadcast an alle WS-Clients | {"position":{…}, "motorCounts":{…}} (M114) |
| Fehler | Nur an den anfragenden Client | { "type": "error", "code": "GCODE_ERROR", "message": "…", "input": "…" } |
Da driverClient.js der anfragende Client ist, empfängt er beides — Erfolg
(M114-Broadcast) und Fehler (targeted). Damit kann der Fileservice-Endpoint auf
die Driver-Antwort warten bevor er dem Browser antwortet.
Folge für das UI: Der Browser muss keine separate Bestätigungslogik implementieren. Die Pfeil-Buttons sperren einfach für die Dauer des HTTP-POST — Entsperrung und Fehleranzeige kommen mit der HTTP-Antwort.
Browser POST /api/active/next?execute=true
│
│ Fileservice: Cursor +1, holt line, sendet an Driver WS
│ ┌─ wartet auf nächste WS-Nachricht (max. DRIVER_TIMEOUT_MS) ─┐
│ │ M114 empfangen → HTTP 200 { cursor, line, driverPos } │
│ │ error empfangen → HTTP 502 { error.code, error.message } │
│ │ Timeout → HTTP 504 'Driver timeout' │
│ └──────────────────────────────────────────────────────────────┘
│
Browser: buttons gesperrt während fetch() läuft
buttons aktiv nach Antwort
bei 5xx: Fehlermeldung anzeigen
4. src/driverClient.js — neues Modul im Fileservice
Hält eine persistente WS-Verbindung zum Driver. send() gibt ein Promise zurück,
das mit der nächsten Driver-Antwort (M114 oder Fehler) resolved/rejected wird:
// src/driverClient.js — liest URL + Timeout aus cfg (src/config.js)
const WebSocket = require('ws');
const log = require('./log');
const cfg = require('./config');
let ws = null;
function getOrCreateWs() {
if (ws && ws.readyState !== WebSocket.CLOSED) return ws;
ws = new WebSocket(cfg.driverWsUrl, { rejectUnauthorized: false });
ws.on('error', (e) => log.warn('driverClient WS Fehler:', e.message));
ws.on('close', () => log.info('driverClient WS geschlossen'));
ws.on('open', () => log.info('driverClient WS verbunden mit ' + cfg.driverWsUrl));
return ws;
}
function send(line) {
if (!cfg.driverWsUrl) {
return Promise.reject(Object.assign(
new Error('DRIVER_WS_URL nicht konfiguriert'), { driverCode: 'NOT_CONFIGURED' }
));
}
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(Object.assign(
new Error(`Keine Antwort vom Driver innerhalb von ${cfg.driverTimeoutMs} ms`),
{ driverCode: 'TIMEOUT' }
));
}, cfg.driverTimeoutMs);
const conn = getOrCreateWs();
const handler = (data) => {
clearTimeout(timer);
const text = data.toString();
try {
const parsed = JSON.parse(text);
if (parsed.type === 'error') {
reject(Object.assign(new Error(parsed.message), { driverCode: parsed.code || 'DRIVER_ERROR' }));
} else {
resolve(parsed); // M114: { position: {…}, motorCounts: {…} }
}
} catch {
resolve({ raw: text }); // unbekanntes Format → als Erfolg werten
}
};
if (conn.readyState === WebSocket.OPEN) {
conn.once('message', handler);
conn.send(line);
} else {
conn.once('open', () => { conn.once('message', handler); conn.send(line); });
}
});
}
module.exports = { send, configured: () => !!cfg.driverWsUrl };
5. src/active/activeState.js — execute-Parameter
Ein interner _step()-Helper enthält die gemeinsame Logik inkl. _ensureActive().
Die öffentlichen Methoden delegieren nur noch:
const driverClient = require('../driverClient');
async _step(index, execute) {
await this._ensureActive();
const result = await this._gotoIndex(index);
if (execute) {
const driverPos = await driverClient.send(result.line); // wartet auf ACK
return { ...result, driverPos };
}
return result;
}
async next(execute = false) { return this._step(this.cursor + 1, execute); }
async prev(execute = false) { return this._step(this.cursor - 1, execute); }
async first(execute = false) { return this._step(0, execute); }
async last(execute = false) { return this._step(this.lines.length - 1, execute); }
async goto(index, execute = false) { await this._ensureActive(); return this._step(Number(index), execute); }
6. src/routes/active.js — execute-Flag + Fehlerweiterleitung
Alle Step-Routen sind auth-geschützt (requireAuth). Driver-Fehler (err.driverCode) → HTTP 502:
router.post('/next', requireAuth, asyncH(async (req, res) => {
const execute = req.query.execute === 'true';
try { res.json(await active.next(execute)); }
catch (err) { if (err.driverCode) return res.status(502).json({ error: err.driverCode, message: err.message }); throw err; }
}));
// analog für /prev, /first, /last, /goto
// analog für /prev, /first, /last
### 6. `src/config.js` — neue Option
```js
driverWsUrl: process.env.DRIVER_WS_URL || '',
Beim Start in server.js:
const driverClient = require('./driverClient');
if (cfg.driverWsUrl) driverClient.configure(cfg.driverWsUrl);
7. GET /api/config — Browser-Endpoint
app.get('/api/config', (req, res) => res.json({
driverWsUrl: cfg.driverWsUrl ? 'configured' : '', // URL nicht exponieren
}));
Die echte WS-URL wird dem Browser nicht mitgeteilt — nur ob ein Driver konfiguriert ist (boolean-ähnlich). Das verhindert, dass Clients die Driver- Adresse kennen.
8. Env-Variable DRIVER_WS_URL im Fileservice
DRIVER_WS_URL=wss://appRobotDriver:2095
Im Docker-Stack entspricht appRobotDriver dem Dienstnamen im Portainer-Stack
(analog zu FILESERVICE_URL=http://appRobot_Fileservice:2100 im Driver).
Keine Änderungen am Driver nötig
InputWS.js verarbeitet rohe G-Code-Befehle (G90 G1 X… A… F…) bereits
korrekt — der neue Sende-Modus nutzt exakt diesen bestehenden Pfad.
Offene Fragen / Risiken
| Thema | Hinweis |
|---|---|
| WSS / Zertifikat | Driver läuft über HTTPS/WSS mit selbstsigniertem Zertifikat — ws-Client braucht rejectUnauthorized: false (nur im LAN akzeptabel) |
| Timeout | DRIVER_TIMEOUT_MS (Default 10 s) muss länger sein als die längste Roboterfahrt; langsame Moves oder blockierte Controller könnten Timeouts auslösen |
| WS-Gleichzeitigkeit | driverClient.js verwendet ws.once('message', …) — funktioniert nur wenn jeweils ein Step auf einmal läuft. Buttons werden während des laufenden Steps gesperrt, daher sicher |
| Reconnect | driverClient.js öffnet WS neu bei CLOSED; kurze Unterbrechungen im LAN werden so automatisch überbrückt |
| Fehlermeldung im Browser | Driver-Fehlertext (err.message) wird 1:1 angezeigt — kann englisch oder intern sein; ggf. spätere Übersetzungstabelle ergänzen |
| FPlay / FStop | Analog zu Step könnten auch Playback-Befehle über diesen Kanal laufen — ist aber ein separates Feature |