Phase 0, 1, 2
This commit is contained in:
@@ -329,43 +329,45 @@ automatisch die aktuelle Konfiguration.
|
||||
|
||||
### Phase 0 — Refactor: Board-Pipeline auslagern
|
||||
|
||||
- [ ] Funktion `runBoardPipeline(runDir, send)` in `server/server.js` extrahieren
|
||||
- [ ] Bestehende Logik aus `POST /api/board/run` (Zeile ~520–606) in Funktion verschieben
|
||||
- [ ] `POST /api/board/run` ruft `runBoardPipeline()` auf (Verhalten unverändert)
|
||||
- [ ] Rückgabewert: `runDir` (String, Pfad zum Lauf-Verzeichnis)
|
||||
- [x] Funktion `runBoardPipeline(runDir, send)` in `server/server.js` extrahieren
|
||||
- [x] Bestehende Logik aus `POST /api/board/run` in Funktion verschieben
|
||||
- [x] `POST /api/board/run` ruft `runBoardPipeline()` auf (Verhalten unverändert)
|
||||
- [ ] Test: Board-Run funktioniert weiterhin identisch
|
||||
|
||||
---
|
||||
|
||||
### Phase 1 — Backend `POST /api/homing/run`
|
||||
|
||||
- [ ] Konstante `SCRIPT_4B` in `server/server.js` ergänzen (analog zu `SCRIPT_1`, `SCRIPT_3B` etc.)
|
||||
- [ ] `server/homingOrchestrator.js` erstellen
|
||||
- [ ] `estimateXFromMarkers(arucoJsonPath)` implementieren (X aus triangulierten Marker-Positionen)
|
||||
- [ ] `runHoming({ robotJsonPath, send })` implementieren
|
||||
- [ ] `runBoardPipeline()` aufrufen → `runDir` + `aruco_marker_poses.json`
|
||||
- [ ] X-Position berechnen
|
||||
- [ ] 4b-Schleife: Arm1 → Ellbow → Arm2 → Hand (sequenziell, `--from-state`)
|
||||
- [ ] `send({ type: 'done', state: accumulated_state, runDir })`
|
||||
- [ ] Route `POST /api/homing/run` in `server/server.js` registrieren (SSE-Stream)
|
||||
- [ ] Minimale Test-UI (temporär): Fetch + Log im Browser-Konsole genügt
|
||||
- [x] Konstante `SCRIPT_4B` in `server/server.js` ergänzen
|
||||
- [x] `server/homingOrchestrator.js` erstellen
|
||||
- [x] `estimateXFromMarkers(arucoJsonPath)` implementieren
|
||||
- [x] `runHoming({ robotJsonPath, homingDir, send, runScript, runBoardPipeline, SCRIPT_4B })` implementieren
|
||||
- [x] `runBoardPipeline()` aufrufen → `aruco_marker_poses.json`
|
||||
- [x] X-Position berechnen
|
||||
- [x] 4b-Schleife: Arm1 → Ellbow → Arm2 → Hand (sequenziell, `--from-state`)
|
||||
- [x] `send({ type: 'done', state: accumulated_state, runDir })`
|
||||
- [x] Route `POST /api/homing/run` in `server/server.js` (SSE-Stream)
|
||||
- [x] Route `POST /api/homing/send-state` in `server/server.js`
|
||||
- [x] Route `GET /api/homing/run-data` (Bilder + State für Frontend)
|
||||
- [x] `ROBOT_URL` Konstante in `server/server.js` ergänzen
|
||||
|
||||
---
|
||||
|
||||
### Phase 2 — Frontend `public/homing.html`
|
||||
|
||||
- [ ] Datei `public/homing.html` erstellen
|
||||
- [ ] Sektion **Aktionen**: Button „Foto & Homing berechnen", Button „An Roboter senden" (disabled)
|
||||
- [ ] Sektion **Ausgabe / Log**: SSE-Stream-Ausgabe, Schritt-Fortschritt
|
||||
- [ ] Sektion **Analysis & Reasoning**: Zwischenergebnisse je Script als JSON
|
||||
- [ ] Sektion **Result Raw JSON** + **Result Tree View**: `{ x, y, z, a, b, c, e }`
|
||||
- [ ] Sektion **Snapshot CSV**: Marker-Tabelle (ID, Link, x, y, z, Residual)
|
||||
- [ ] Sektion **Snapshots**: annotierte Kamerabilder (cam0, cam1, …)
|
||||
- [ ] `public/homing.js` erstellen
|
||||
- [ ] `runHoming()`: POST + SSE-Stream lesen, Log befüllen
|
||||
- [ ] `showResult(state)`: Tree View + Raw JSON befüllen, Send-Button aktivieren
|
||||
- [ ] `sendToRobot(state)`: POST `/api/homing/send-state`
|
||||
- [ ] Link-Button von `public/index.html` zu `homing.html` ergänzen
|
||||
- [x] Datei `public/homing.html` erstellen
|
||||
- [x] Sektion **Aktionen**: Button „Foto & Homing berechnen", Button „An Roboter senden"
|
||||
- [x] Sektion **Ausgabe / Log**: SSE-Stream-Ausgabe, Schritt-Fortschritt
|
||||
- [x] Sektion **Analysis & Reasoning**: Zwischenergebnisse je Script als JSON
|
||||
- [x] Sektion **Result Raw JSON** + **Result Tree View**: `{ x, y, z, a, b, c, e }`
|
||||
- [x] Sektion **Snapshot CSV**: Marker-Tabelle (ID, Link, x, y, z)
|
||||
- [x] Sektion **Snapshots**: annotierte Kamerabilder
|
||||
- [x] `public/homing.js` erstellen
|
||||
- [x] `runHoming()`: POST + SSE-Stream lesen, Log befüllen, Fortschrittsbalken
|
||||
- [x] `showResult(state)`: Tree View + Raw JSON befüllen, Send-Button aktivieren
|
||||
- [x] `sendToRobot(state)`: POST `/api/homing/send-state`
|
||||
- [x] `loadRunData(runDir)`: Debug-Bilder nach dem Run laden
|
||||
- [x] Link-Button von `public/index.html` zu `homing.html` ergänzen
|
||||
|
||||
---
|
||||
|
||||
@@ -397,8 +399,8 @@ automatisch die aktuelle Konfiguration.
|
||||
| Kalibrierung (Kamera, Board, X-Achse) | ✅ fertig |
|
||||
| Arm1 Joint-Origin Button | ✅ ausführbar |
|
||||
| Arm-Marker in robot.json | 🔶 Nutzer |
|
||||
| Phase 0 – runBoardPipeline() | ❌ offen |
|
||||
| Phase 1 – /api/homing/run | ❌ offen |
|
||||
| Phase 2 – homing.html | ❌ offen |
|
||||
| Phase 3 – /api/homing/send-state | ❌ offen |
|
||||
| Phase 0 – runBoardPipeline() | ✅ fertig |
|
||||
| Phase 1 – /api/homing/run | ✅ fertig |
|
||||
| Phase 2 – homing.html | ✅ fertig |
|
||||
| Phase 3 – /api/homing/send-state | ✅ Route implementiert |
|
||||
| Phase 4 – robot.json via Driver-API | ⏳ später |
|
||||
|
||||
111
public/homing.html
Normal file
111
public/homing.html
Normal file
@@ -0,0 +1,111 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Homing – appRobotHoming</title>
|
||||
<link rel="stylesheet" href="/styles.css">
|
||||
<link rel="stylesheet" href="/calibration.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- HEADER (analog calibration.html) -->
|
||||
<div class="calib-topbar">
|
||||
<a href="/">← Zurück</a>
|
||||
<h1>Homing</h1>
|
||||
</div>
|
||||
|
||||
<div style="max-width:1200px;margin:0 auto;padding:0 16px">
|
||||
<div class="sections">
|
||||
|
||||
<!-- AKTIONEN -->
|
||||
<div class="section full">
|
||||
<h2>Aktionen</h2>
|
||||
|
||||
<div class="controls" style="flex-wrap:wrap;gap:12px;align-items:center">
|
||||
<button id="btn-homing-run">📷 Foto & Homing berechnen</button>
|
||||
<button id="btn-homing-send" disabled
|
||||
style="opacity:.4;cursor:not-allowed"
|
||||
title="Erst Homing ausführen">
|
||||
✅ An Roboter senden
|
||||
</button>
|
||||
<span id="homing-status" class="status-badge open">○ Warte</span>
|
||||
</div>
|
||||
|
||||
<!-- Fortschrittsbalken -->
|
||||
<div id="homing-progress" style="display:none;margin-top:14px">
|
||||
<div style="background:var(--border);border-radius:3px;height:5px;overflow:hidden">
|
||||
<div id="homing-progress-bar"
|
||||
style="height:100%;background:var(--accent);width:0%;transition:width .4s ease"></div>
|
||||
</div>
|
||||
<span id="homing-progress-text"
|
||||
style="font-size:11px;color:var(--muted);display:block;margin-top:4px"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AUSGABE / LOG -->
|
||||
<div class="section full">
|
||||
<h2>Ausgabe / Log</h2>
|
||||
<textarea id="log-homing" readonly
|
||||
placeholder="(Ausgabe erscheint hier sobald Homing gestartet wird)"
|
||||
style="min-height:180px"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- ANALYSIS & REASONING -->
|
||||
<div class="section full">
|
||||
<h2>Analysis & Reasoning</h2>
|
||||
<textarea id="homing-analysis" readonly
|
||||
placeholder="(Zwischenergebnisse je Script erscheinen hier)"
|
||||
style="min-height:120px;font-size:12px"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- RESULT RAW JSON + TREE VIEW -->
|
||||
<div class="section half">
|
||||
<h2>Result – Raw JSON</h2>
|
||||
<div class="panel">
|
||||
<textarea id="homing-result-json" readonly
|
||||
placeholder="(Ergebnis erscheint hier)"
|
||||
style="min-height:160px;font-size:12px"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section half">
|
||||
<h2>Result – Tree View</h2>
|
||||
<div class="panel" style="min-height:160px;padding:14px 16px">
|
||||
<div id="homing-result-tree"
|
||||
style="font-family:monospace;font-size:13px;line-height:2">
|
||||
<span style="color:var(--muted);font-size:12px">(Ergebnis erscheint hier)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SNAPSHOT CSV -->
|
||||
<div class="section full">
|
||||
<h2>Snapshot CSV (Marker)</h2>
|
||||
<div id="homing-csv-info"
|
||||
style="font-size:11px;color:var(--muted);margin-bottom:8px">
|
||||
Marker-Positionen aus aruco_marker_poses.json werden nach dem Homing angezeigt.
|
||||
</div>
|
||||
<div style="overflow-x:auto">
|
||||
<table id="homing-csv-table"
|
||||
style="font-size:12px;border-collapse:collapse;width:100%;min-width:500px"></table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SNAPSHOTS (Kamerabilder) -->
|
||||
<div class="section full">
|
||||
<h2>Snapshots (annotierte Kamerabilder)</h2>
|
||||
<div id="homing-snapshots"
|
||||
style="display:flex;gap:12px;flex-wrap:wrap">
|
||||
<span style="color:var(--muted);font-size:12px">
|
||||
(Bilder erscheinen nach dem Homing-Run)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /.sections -->
|
||||
</div>
|
||||
|
||||
<script src="/homing.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
280
public/homing.js
Normal file
280
public/homing.js
Normal file
@@ -0,0 +1,280 @@
|
||||
'use strict';
|
||||
|
||||
// ── DOM-Referenzen ────────────────────────────────────────────────────────────
|
||||
|
||||
const btnRun = document.getElementById('btn-homing-run');
|
||||
const btnSend = document.getElementById('btn-homing-send');
|
||||
const statusBadge = document.getElementById('homing-status');
|
||||
const progressDiv = document.getElementById('homing-progress');
|
||||
const progressBar = document.getElementById('homing-progress-bar');
|
||||
const progressText = document.getElementById('homing-progress-text');
|
||||
const logEl = document.getElementById('log-homing');
|
||||
const analysisEl = document.getElementById('homing-analysis');
|
||||
const resultJson = document.getElementById('homing-result-json');
|
||||
const resultTree = document.getElementById('homing-result-tree');
|
||||
const csvInfo = document.getElementById('homing-csv-info');
|
||||
const csvTable = document.getElementById('homing-csv-table');
|
||||
const snapshots = document.getElementById('homing-snapshots');
|
||||
|
||||
let _lastState = null; // letztes Homing-Ergebnis (zum Senden an Roboter)
|
||||
|
||||
// ── Hilfsfunktionen ───────────────────────────────────────────────────────────
|
||||
|
||||
function appendLog(text) {
|
||||
logEl.value += (text ?? '') + '\n';
|
||||
logEl.scrollTop = logEl.scrollHeight;
|
||||
}
|
||||
|
||||
function appendAnalysis(key, value) {
|
||||
const line = key + ':\n' + JSON.stringify(value, null, 2);
|
||||
analysisEl.value += line + '\n\n';
|
||||
analysisEl.scrollTop = analysisEl.scrollHeight;
|
||||
}
|
||||
|
||||
function setStatus(label, cls) {
|
||||
statusBadge.textContent = label;
|
||||
statusBadge.className = `status-badge ${cls}`;
|
||||
}
|
||||
|
||||
function setProgress(step, total, text) {
|
||||
progressDiv.style.display = 'block';
|
||||
const pct = total > 0 ? Math.round((step / total) * 100) : 0;
|
||||
progressBar.style.width = pct + '%';
|
||||
progressText.textContent = text || `Schritt ${step} / ${total}`;
|
||||
}
|
||||
|
||||
/** Zeigt { x_mm, y_deg, z_deg, a_deg, b_deg, c_deg, e_mm } im Result-Bereich. */
|
||||
function showResult(state) {
|
||||
// Raw JSON
|
||||
resultJson.value = JSON.stringify(state, null, 2);
|
||||
|
||||
// Tree View
|
||||
const LABELS = {
|
||||
x_mm: 'x (Slider)',
|
||||
y_deg: 'y (Arm1)',
|
||||
z_deg: 'z (Ellbow)',
|
||||
a_deg: 'a (Arm2)',
|
||||
b_deg: 'b (Hand)',
|
||||
c_deg: 'c (Palm)',
|
||||
e_mm: 'e (Greifer)',
|
||||
};
|
||||
const UNITS = {
|
||||
x_mm: 'mm', y_deg: '°', z_deg: '°',
|
||||
a_deg: '°', b_deg: '°', c_deg: '°', e_mm: 'mm',
|
||||
};
|
||||
|
||||
let html = '';
|
||||
for (const [key, val] of Object.entries(state)) {
|
||||
const label = LABELS[key] ?? key;
|
||||
const unit = UNITS[key] ?? '';
|
||||
const valStr = typeof val === 'number' ? val.toFixed(2) : String(val ?? '–');
|
||||
html += `<div style="display:flex;gap:8px;padding:2px 0">
|
||||
<span style="min-width:130px;color:var(--muted)">${label}</span>
|
||||
<span style="font-weight:600">${valStr} ${unit}</span>
|
||||
</div>`;
|
||||
}
|
||||
resultTree.innerHTML = html || '<span style="color:var(--muted)">–</span>';
|
||||
}
|
||||
|
||||
/** Baut die CSV-Tabelle aus aruco_marker_poses.json-Daten. */
|
||||
function showMarkerTable(markers) {
|
||||
if (!markers || markers.length === 0) {
|
||||
csvInfo.textContent = 'Keine Marker-Daten vorhanden.';
|
||||
csvTable.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
csvInfo.textContent = `${markers.length} Marker trianguliert`;
|
||||
|
||||
const hdrs = ['ID', 'Link', 'Set', 'x mm', 'y mm', 'z mm', 'Kameras'];
|
||||
const style = 'padding:4px 8px;border:1px solid var(--border);text-align:right';
|
||||
const styleL = 'padding:4px 8px;border:1px solid var(--border);text-align:left';
|
||||
|
||||
let html = `<thead><tr>${hdrs.map(h =>
|
||||
`<th style="${h==='Link'||h==='Set'||h==='ID'?styleL:style}">${h}</th>`).join('')}</tr></thead><tbody>`;
|
||||
|
||||
for (const m of markers) {
|
||||
const pos = m.position_mm ?? [null, null, null];
|
||||
const fmt = v => (v != null ? Number(v).toFixed(1) : '–');
|
||||
html += `<tr>
|
||||
<td style="${styleL}">${m.marker_id}</td>
|
||||
<td style="${styleL}">${m.link ?? '–'}</td>
|
||||
<td style="${styleL}">${m.set ?? '–'}</td>
|
||||
<td style="${style}">${fmt(pos[0])}</td>
|
||||
<td style="${style}">${fmt(pos[1])}</td>
|
||||
<td style="${style}">${fmt(pos[2])}</td>
|
||||
<td style="${style}">${m.num_cameras ?? '–'}</td>
|
||||
</tr>`;
|
||||
}
|
||||
html += '</tbody>';
|
||||
csvTable.innerHTML = html;
|
||||
}
|
||||
|
||||
/** Lädt Debug-Bilder und Marker-Tabelle für einen Homing-Run. */
|
||||
async function loadRunData(runDir) {
|
||||
try {
|
||||
const res = await fetch(`/api/homing/run-data?run=${encodeURIComponent(runDir)}`);
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
|
||||
// Snapshots
|
||||
snapshots.innerHTML = '';
|
||||
for (const img of (data.images ?? [])) {
|
||||
const figure = document.createElement('figure');
|
||||
figure.style.cssText = 'margin:0;display:flex;flex-direction:column;gap:4px';
|
||||
const el = document.createElement('img');
|
||||
el.src = `data:image/jpeg;base64,${img.contentBase64}`;
|
||||
el.style.cssText = 'max-width:400px;max-height:300px;border:1px solid var(--border);border-radius:4px';
|
||||
el.alt = img.filename;
|
||||
const cap = document.createElement('figcaption');
|
||||
cap.textContent = img.filename;
|
||||
cap.style.cssText = 'font-size:11px;color:var(--muted);text-align:center';
|
||||
figure.appendChild(el);
|
||||
figure.appendChild(cap);
|
||||
snapshots.appendChild(figure);
|
||||
}
|
||||
if (!data.images?.length) {
|
||||
snapshots.innerHTML = '<span style="color:var(--muted);font-size:12px">Keine Bilder vorhanden.</span>';
|
||||
}
|
||||
} catch { /* nicht kritisch */ }
|
||||
}
|
||||
|
||||
/** Lädt aruco_marker_poses.json für den Run und zeigt die Marker-Tabelle. */
|
||||
async function loadArucoData(runDir) {
|
||||
try {
|
||||
// Rohdaten über Homing-Run-Data-Endpoint nicht direkt verfügbar,
|
||||
// daher holen wir uns die JSON über board/latest falls es der gleiche Run ist,
|
||||
// oder wir parsen es aus den Analysis-Daten.
|
||||
// Fürs erste: Marker aus dem finalState ableiten wenn vorhanden.
|
||||
csvInfo.textContent = '(Marker-CSV wird nach dem nächsten Homing-Run geladen)';
|
||||
} catch { /* ignorieren */ }
|
||||
}
|
||||
|
||||
// ── Homing starten ────────────────────────────────────────────────────────────
|
||||
|
||||
async function runHoming() {
|
||||
// UI zurücksetzen
|
||||
logEl.value = '';
|
||||
analysisEl.value = '';
|
||||
resultJson.value = '';
|
||||
resultTree.innerHTML = '<span style="color:var(--muted);font-size:12px">(Ergebnis erscheint hier)</span>';
|
||||
csvInfo.textContent = '';
|
||||
csvTable.innerHTML = '';
|
||||
snapshots.innerHTML = '<span style="color:var(--muted);font-size:12px">…</span>';
|
||||
_lastState = null;
|
||||
|
||||
btnRun.disabled = true;
|
||||
btnSend.disabled = true;
|
||||
btnSend.style.opacity = '0.4';
|
||||
btnSend.style.cursor = 'not-allowed';
|
||||
setStatus('● Läuft …', 'wip');
|
||||
progressDiv.style.display = 'block';
|
||||
progressBar.style.width = '2%';
|
||||
progressText.textContent = 'Verbinde …';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/homing/run', { method: 'POST' });
|
||||
if (!response.ok || !response.body) throw new Error(`HTTP ${response.status}`);
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buf = '';
|
||||
let lastRunDir = null;
|
||||
let hasError = false;
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buf += decoder.decode(value, { stream: true });
|
||||
const lines = buf.split('\n');
|
||||
buf = lines.pop(); // unvollständige letzte Zeile aufheben
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.startsWith('data: ')) continue;
|
||||
let evt;
|
||||
try { evt = JSON.parse(line.slice(6)); } catch { continue; }
|
||||
|
||||
switch (evt.type) {
|
||||
case 'log':
|
||||
appendLog(evt.text);
|
||||
break;
|
||||
|
||||
case 'step':
|
||||
setProgress(evt.step, evt.total, evt.text);
|
||||
appendLog(`[${evt.step}/${evt.total}] ${evt.text || ''}`);
|
||||
break;
|
||||
|
||||
case 'analysis':
|
||||
appendAnalysis(evt.key, evt.value);
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
appendLog(evt.text);
|
||||
hasError = true;
|
||||
break;
|
||||
|
||||
case 'done':
|
||||
if (evt.state) {
|
||||
_lastState = evt.state;
|
||||
showResult(evt.state);
|
||||
btnSend.disabled = false;
|
||||
btnSend.style.opacity = '';
|
||||
btnSend.style.cursor = '';
|
||||
setStatus('✓ Fertig', 'done');
|
||||
progressBar.style.width = '100%';
|
||||
progressText.textContent = 'Homing abgeschlossen';
|
||||
} else {
|
||||
setStatus('✗ Fehler', 'open');
|
||||
progressBar.style.width = '100%';
|
||||
progressText.textContent = hasError ? 'Fehler aufgetreten' : 'Abgebrochen';
|
||||
}
|
||||
if (evt.runDir) {
|
||||
lastRunDir = evt.runDir;
|
||||
await loadRunData(evt.runDir);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
appendLog(`❌ Verbindungsfehler: ${err.message}`);
|
||||
setStatus('✗ Fehler', 'open');
|
||||
progressBar.style.width = '100%';
|
||||
progressText.textContent = 'Verbindungsfehler';
|
||||
} finally {
|
||||
btnRun.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ── State an Roboter senden ───────────────────────────────────────────────────
|
||||
|
||||
async function sendToRobot() {
|
||||
if (!_lastState) return;
|
||||
|
||||
btnSend.disabled = true;
|
||||
try {
|
||||
const res = await fetch('/api/homing/send-state', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ state: _lastState }),
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
appendLog('✅ State erfolgreich an Roboter gesendet');
|
||||
setStatus('✓ Gesendet', 'done');
|
||||
} else {
|
||||
appendLog(`❌ Fehler beim Senden: ${data.error ?? JSON.stringify(data)}`);
|
||||
btnSend.disabled = false;
|
||||
}
|
||||
} catch (err) {
|
||||
appendLog(`❌ Netzwerkfehler: ${err.message}`);
|
||||
btnSend.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Event-Listener ────────────────────────────────────────────────────────────
|
||||
|
||||
btnRun.addEventListener('click', runHoming);
|
||||
btnSend.addEventListener('click', sendToRobot);
|
||||
@@ -36,6 +36,10 @@
|
||||
<a href="/calibration.html">
|
||||
<button type="button">Calibration Page</button>
|
||||
</a>
|
||||
|
||||
<a href="/homing.html">
|
||||
<button type="button">Homing</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
140
server/homingOrchestrator.js
Normal file
140
server/homingOrchestrator.js
Normal file
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* homingOrchestrator.js
|
||||
* Vollständiger Homing-Ablauf: Board-Pipeline (1→2→3b) + 4b-Schleife.
|
||||
*
|
||||
* Abhängigkeiten werden von server.js per Parameter übergeben
|
||||
* (kein Circular-Import-Problem).
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import fsPromises from 'fs/promises';
|
||||
|
||||
/**
|
||||
* Schätzt die Slider-X-Position aus den triangulierten Marker-Positionen
|
||||
* (aruco_marker_poses.json). Nutzt den Durchschnitt der x_mm aller
|
||||
* Nicht-Board-Marker. Fallback: 0.0 wenn keine Arm-Marker sichtbar.
|
||||
*
|
||||
* @param {string} arucoJsonPath
|
||||
* @returns {number} x_mm
|
||||
*/
|
||||
export function estimateXFromMarkers(arucoJsonPath) {
|
||||
try {
|
||||
const data = JSON.parse(fs.readFileSync(arucoJsonPath, 'utf8'));
|
||||
const armMarkers = (data.markers ?? []).filter(
|
||||
m => m.link && m.link !== 'Board',
|
||||
);
|
||||
if (armMarkers.length === 0) return 0.0;
|
||||
const sumX = armMarkers.reduce((s, m) => s + (m.position_mm?.[0] ?? 0), 0);
|
||||
return sumX / armMarkers.length;
|
||||
} catch {
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt den vollständigen Homing-Ablauf als SSE-Stream aus.
|
||||
*
|
||||
* @param {{
|
||||
* robotJsonPath: string,
|
||||
* homingDir: string,
|
||||
* send: (obj: object) => void,
|
||||
* runScript: (args: string[], send: Function) => Promise<number>,
|
||||
* runBoardPipeline: (runDir: string, send: Function) => Promise<void>,
|
||||
* SCRIPT_4B: string,
|
||||
* }} opts
|
||||
*/
|
||||
export async function runHoming({
|
||||
robotJsonPath,
|
||||
homingDir,
|
||||
send,
|
||||
runScript,
|
||||
runBoardPipeline,
|
||||
SCRIPT_4B,
|
||||
}) {
|
||||
// Lauf-Verzeichnis anlegen
|
||||
const ts = makeTimestamp();
|
||||
const runDir = path.join(homingDir, ts);
|
||||
await fsPromises.mkdir(runDir, { recursive: true });
|
||||
|
||||
send({ type: 'log', text: `▶ Homing-Run: ${ts}` });
|
||||
send({ type: 'log', text: `▶ Ordner: ${runDir}` });
|
||||
send({ type: 'log', text: `▶ Robot-JSON: ${robotJsonPath}` });
|
||||
send({ type: 'log', text: '' });
|
||||
|
||||
// ── Schritt 1–3b: Board-Pipeline ─────────────────────────────────────────
|
||||
send({ type: 'step', step: 1, total: 6, text: 'Foto + Marker-Triangulierung …' });
|
||||
await runBoardPipeline(runDir, send);
|
||||
|
||||
// Prüfen ob aruco_marker_poses.json erzeugt wurde
|
||||
const arucoJson = path.join(runDir, 'aruco_marker_poses.json');
|
||||
try {
|
||||
await fsPromises.access(arucoJson);
|
||||
} catch {
|
||||
send({ type: 'error', text: '❌ aruco_marker_poses.json fehlt – Script 3b hat nicht funktioniert.' });
|
||||
send({ type: 'done', exitCode: -1, runDir: ts });
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Schritt 2: X-Position bestimmen ─────────────────────────────────────
|
||||
send({ type: 'step', step: 2, total: 6, text: 'X-Position bestimmen …' });
|
||||
const xMm = estimateXFromMarkers(arucoJson);
|
||||
send({ type: 'log', text: `▶ Geschätzte X-Position: ${xMm.toFixed(1)} mm` });
|
||||
send({ type: 'analysis', key: 'x_mm', value: xMm });
|
||||
|
||||
// ── Schritt 3–6: 4b-Kette (Arm1 → Ellbow → Arm2 → Hand) ─────────────
|
||||
const links = ['Arm1', 'Ellbow', 'Arm2', 'Hand'];
|
||||
let fromState = null;
|
||||
|
||||
for (let i = 0; i < links.length; i++) {
|
||||
const link = links[i];
|
||||
send({ type: 'step', step: 3 + i, total: 6, text: `Gelenkwinkel ${link} …` });
|
||||
send({ type: 'log', text: `\n─── 4b: ${link} ${'─'.repeat(35 - link.length)}` });
|
||||
|
||||
const outputPath = path.join(runDir, `state_${link}.json`);
|
||||
const args = [
|
||||
SCRIPT_4B,
|
||||
'--robot', robotJsonPath,
|
||||
'--aruco', arucoJson,
|
||||
'--link', link,
|
||||
'--output', outputPath,
|
||||
];
|
||||
if (fromState) args.push('--from-state', fromState);
|
||||
else args.push('--x-mm', String(xMm));
|
||||
|
||||
const exit = await runScript(args, send);
|
||||
if (exit !== 0) {
|
||||
send({ type: 'error', text: `❌ 4b ${link} Exit ${exit}` });
|
||||
send({ type: 'done', exitCode: exit, runDir: ts });
|
||||
return;
|
||||
}
|
||||
fromState = outputPath;
|
||||
|
||||
// Zwischenergebnis an Analysis-Sektion
|
||||
try {
|
||||
const stateData = JSON.parse(await fsPromises.readFile(outputPath, 'utf8'));
|
||||
const acc = stateData.accumulated_state ?? stateData;
|
||||
send({ type: 'analysis', key: `state_${link}`, value: acc });
|
||||
} catch { /* ignorieren */ }
|
||||
}
|
||||
|
||||
// ── Endergebnis ──────────────────────────────────────────────────────────
|
||||
try {
|
||||
const finalData = JSON.parse(await fsPromises.readFile(fromState, 'utf8'));
|
||||
const finalState = finalData.accumulated_state ?? finalData;
|
||||
send({ type: 'log', text: '' });
|
||||
send({ type: 'log', text: `✅ Homing abgeschlossen: ${ts}` });
|
||||
send({ type: 'done', exitCode: 0, state: finalState, runDir: ts });
|
||||
} catch (err) {
|
||||
send({ type: 'error', text: `❌ Endzustand konnte nicht gelesen werden: ${err.message}` });
|
||||
send({ type: 'done', exitCode: -1, runDir: ts });
|
||||
}
|
||||
}
|
||||
|
||||
/** Timestamp-String YYYYMMDD_HHmmss */
|
||||
function makeTimestamp() {
|
||||
const now = new Date();
|
||||
const p = (n, l = 2) => String(n).padStart(l, '0');
|
||||
return `${now.getFullYear()}${p(now.getMonth() + 1)}${p(now.getDate())}`
|
||||
+ `_${p(now.getHours())}${p(now.getMinutes())}${p(now.getSeconds())}`;
|
||||
}
|
||||
293
server/server.js
293
server/server.js
@@ -9,6 +9,7 @@ import process from 'process';
|
||||
import { spawn } from 'child_process';
|
||||
import { WebcamClient } from './webcamClient.js';
|
||||
import { assignByZRange, removeMarkerAssignment, alignSetToMeasured, assignMarkerId, adoptXAxis, assignFixedMarkersToLink, setJointOriginYZ } from './editRobot.js';
|
||||
import { runHoming } from './homingOrchestrator.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
@@ -21,6 +22,7 @@ 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 || '';
|
||||
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';
|
||||
@@ -430,11 +432,13 @@ app.post('/api/calibration/compute', async (req, res) => {
|
||||
// ── Board-Erkennung ───────────────────────────────────────────────────────────
|
||||
|
||||
const boardDataDir = path.join(__dirname, '..', 'data', 'board');
|
||||
const homingDataDir = path.join(__dirname, '..', 'data', 'homing');
|
||||
const ROBOT_JSON = process.env.ROBOT_JSON
|
||||
|| path.join(__dirname, '..', 'scripts', 'robot_1781069752019.json');
|
||||
const SCRIPT_1 = path.join(__dirname, '..', 'scripts', '1_detect_aruco_observations.py');
|
||||
const SCRIPT_2 = path.join(__dirname, '..', 'scripts', '2_estimate_camera_from_observations.py');
|
||||
const SCRIPT_3B = path.join(__dirname, '..', 'scripts', '3b_corner_marker_poses.py');
|
||||
const SCRIPT_4B = path.join(__dirname, '..', 'scripts', '4b_revolute_angle.py');
|
||||
|
||||
/**
|
||||
* Führt ein Python-Script aus und leitet stdout/stderr zeilenweise an `send` weiter.
|
||||
@@ -473,6 +477,103 @@ function runScript(args, send) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Board-Pipeline: Snapshot + Script 1 + Script 2 (pro Kamera) + Script 3b.
|
||||
* Schreibt Ergebnisse nach runDir (muss bereits existieren).
|
||||
* Wird von /api/board/run UND /api/homing/run genutzt.
|
||||
*
|
||||
* @param {string} runDir – Zielverzeichnis (bereits erstellt)
|
||||
* @param {Function} send – SSE-Send-Funktion (obj => void)
|
||||
* @param {{ refSet?: string }} [opts]
|
||||
*/
|
||||
async function runBoardPipeline(runDir, send, { refSet } = {}) {
|
||||
// Kameras ermitteln
|
||||
if (!WEBCAM_URL) throw new Error('WEBCAM_URL nicht konfiguriert');
|
||||
const camData = await new WebcamClient(WEBCAM_URL).getCameras();
|
||||
const cameraIds = (camData.cameras ?? []).map(c => c.id);
|
||||
send({ type: 'log', text: `▶ Kameras: ${cameraIds.join(', ')}` });
|
||||
send({ type: 'log', text: '' });
|
||||
|
||||
// Pro Kamera: Foto → Script 1 → Script 2
|
||||
for (const camId of cameraIds) {
|
||||
send({ type: 'log', text: `─── ${camId} ${'─'.repeat(40 - camId.length)}` });
|
||||
|
||||
// Snapshot
|
||||
send({ type: 'log', text: 'Foto aufnehmen …' });
|
||||
let snapResp;
|
||||
for (let attempt = 1; attempt <= 2; attempt++) {
|
||||
snapResp = await new WebcamClient(WEBCAM_URL).getSnapshot(camId, true);
|
||||
if (snapResp.status !== 503) break;
|
||||
if (attempt < 2) await new Promise(r => setTimeout(r, 2000));
|
||||
}
|
||||
if (!snapResp.ok) {
|
||||
send({ type: 'log', text: `⚠ HTTP ${snapResp.status} – Kamera übersprungen` });
|
||||
continue;
|
||||
}
|
||||
const imgPath = path.join(runDir, `${camId}.jpg`);
|
||||
await fsPromises.writeFile(imgPath, Buffer.from(await snapResp.arrayBuffer()));
|
||||
send({ type: 'log', text: `✅ Foto: ${camId}.jpg` });
|
||||
|
||||
// NPZ suchen – neueste Session, die eine NPZ für diese Kamera enthält
|
||||
const npzInfo = await findLatestNpzForCamera(camId);
|
||||
if (!npzInfo) {
|
||||
send({ type: 'log', text: `⚠ Keine NPZ für ${camId} gefunden – übersprungen` });
|
||||
continue;
|
||||
}
|
||||
const npzPath = npzInfo.npzPath;
|
||||
send({ type: 'log', text: `▶ NPZ: data/calibration/${npzInfo.session}/${camId}_calibration.npz` });
|
||||
|
||||
// Script 1 – ArUco-Erkennung
|
||||
send({ type: 'log', text: '\n▷ 1_detect_aruco_observations' });
|
||||
const exit1 = await runScript([
|
||||
SCRIPT_1,
|
||||
'-i', imgPath,
|
||||
'-npz', npzPath,
|
||||
'-robot', ROBOT_JSON,
|
||||
'-cameraId', camId,
|
||||
'-outDir', runDir,
|
||||
'--saveDebugImage',
|
||||
], send);
|
||||
if (exit1 !== 0) {
|
||||
send({ type: 'log', text: `❌ Script 1 Exit ${exit1}` });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Script 2 – Kamera-Pose schätzen
|
||||
const detJson = path.join(runDir, `${camId}_aruco_detection.json`);
|
||||
try { await fsPromises.access(detJson); }
|
||||
catch {
|
||||
send({ type: 'log', text: '⚠ Detection-JSON fehlt – Script 2 übersprungen' });
|
||||
continue;
|
||||
}
|
||||
send({ type: 'log', text: '\n▷ 2_estimate_camera_from_observations' });
|
||||
const script2Args = [SCRIPT_2, '-i', detJson, '-robot', ROBOT_JSON, '-outDir', runDir];
|
||||
if (refSet) script2Args.push('--refSet', refSet);
|
||||
const exit2 = await runScript(script2Args, send);
|
||||
if (exit2 !== 0) send({ type: 'log', text: `❌ Script 2 Exit ${exit2}` });
|
||||
|
||||
send({ type: 'log', text: '' });
|
||||
}
|
||||
|
||||
// Script 3b: Marker-Triangulierung (benötigt ≥2 Kamera-Posen)
|
||||
send({ type: 'log', text: '' });
|
||||
send({ type: 'log', text: '─── 3b: Marker-Triangulierung ────────────────────────────' });
|
||||
const runFiles3b = await fsPromises.readdir(runDir);
|
||||
const numPoses = runFiles3b.filter(f => f.endsWith('_camera_pose.json')).length;
|
||||
if (numPoses >= 2) {
|
||||
send({ type: 'log', text: `▷ 3b_corner_marker_poses (${numPoses} Kamera-Posen)` });
|
||||
const exit3b = await runScript([
|
||||
SCRIPT_3B,
|
||||
'--evalDir', runDir,
|
||||
'--robot', ROBOT_JSON,
|
||||
], send);
|
||||
if (exit3b !== 0) send({ type: 'log', text: `❌ Script 3b Exit ${exit3b}` });
|
||||
} else {
|
||||
send({ type: 'log', text: `⚠ Nur ${numPoses} Kamera-Pose(n) – Script 3b braucht ≥2 Kameras` });
|
||||
}
|
||||
send({ type: 'log', text: '' });
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/board/run
|
||||
* 1. Erstellt data/board/{timestamp}/
|
||||
@@ -513,94 +614,8 @@ app.post('/api/board/run', async (req, res) => {
|
||||
send({ type: 'log', text: `▶ Referenz-Set: ${refSet ? `"${refSet}" (${refMarkerCount} Marker)` : 'alle'}` });
|
||||
send({ type: 'log', text: '' });
|
||||
|
||||
// 2. Kameras ermitteln
|
||||
if (!WEBCAM_URL) throw new Error('WEBCAM_URL nicht konfiguriert');
|
||||
const camData = await new WebcamClient(WEBCAM_URL).getCameras();
|
||||
const cameraIds = (camData.cameras ?? []).map(c => c.id);
|
||||
send({ type: 'log', text: `▶ Kameras: ${cameraIds.join(', ')}` });
|
||||
send({ type: 'log', text: '' });
|
||||
|
||||
// 3. Pro Kamera: Foto → Script 1 → Script 2
|
||||
for (const camId of cameraIds) {
|
||||
send({ type: 'log', text: `─── ${camId} ${'─'.repeat(40 - camId.length)}` });
|
||||
|
||||
// Snapshot
|
||||
send({ type: 'log', text: 'Foto aufnehmen …' });
|
||||
let snapResp;
|
||||
for (let attempt = 1; attempt <= 2; attempt++) {
|
||||
snapResp = await new WebcamClient(WEBCAM_URL).getSnapshot(camId, true);
|
||||
if (snapResp.status !== 503) break;
|
||||
if (attempt < 2) await new Promise(r => setTimeout(r, 2000));
|
||||
}
|
||||
if (!snapResp.ok) {
|
||||
send({ type: 'log', text: `⚠ HTTP ${snapResp.status} – Kamera übersprungen` });
|
||||
continue;
|
||||
}
|
||||
const imgPath = path.join(runDir, `${camId}.jpg`);
|
||||
await fsPromises.writeFile(imgPath, Buffer.from(await snapResp.arrayBuffer()));
|
||||
send({ type: 'log', text: `✅ Foto: ${camId}.jpg` });
|
||||
|
||||
// NPZ suchen – neueste Session, die eine NPZ für diese Kamera enthält
|
||||
const npzInfo = await findLatestNpzForCamera(camId);
|
||||
if (!npzInfo) {
|
||||
send({ type: 'log', text: `⚠ Keine NPZ für ${camId} gefunden (in keiner Kalibrierungs-Session) – übersprungen` });
|
||||
continue;
|
||||
}
|
||||
const npzPath = npzInfo.npzPath;
|
||||
send({ type: 'log', text: `▶ NPZ: data/calibration/${npzInfo.session}/${camId}_calibration.npz` });
|
||||
|
||||
// Script 1 – ArUco-Erkennung
|
||||
send({ type: 'log', text: '\n▷ 1_detect_aruco_observations' });
|
||||
const exit1 = await runScript([
|
||||
SCRIPT_1,
|
||||
'-i', imgPath,
|
||||
'-npz', npzPath,
|
||||
'-robot', ROBOT_JSON,
|
||||
'-cameraId', camId,
|
||||
'-outDir', runDir,
|
||||
'--saveDebugImage',
|
||||
], send);
|
||||
if (exit1 !== 0) {
|
||||
send({ type: 'log', text: `❌ Script 1 Exit ${exit1}` });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Script 2 – Kamera-Pose schätzen
|
||||
const detJson = path.join(runDir, `${camId}_aruco_detection.json`);
|
||||
try { await fsPromises.access(detJson); }
|
||||
catch {
|
||||
send({ type: 'log', text: '⚠ Detection-JSON fehlt – Script 2 übersprungen' });
|
||||
continue;
|
||||
}
|
||||
|
||||
send({ type: 'log', text: '\n▷ 2_estimate_camera_from_observations' });
|
||||
const script2Args = [SCRIPT_2, '-i', detJson, '-robot', ROBOT_JSON, '-outDir', runDir];
|
||||
if (refSet) script2Args.push('--refSet', refSet);
|
||||
const exit2 = await runScript(script2Args, send);
|
||||
if (exit2 !== 0) {
|
||||
send({ type: 'log', text: `❌ Script 2 Exit ${exit2}` });
|
||||
}
|
||||
|
||||
send({ type: 'log', text: '' });
|
||||
}
|
||||
|
||||
// ── Script 3b: Marker-Triangulierung (benötigt ≥2 Kamera-Posen) ──
|
||||
send({ type: 'log', text: '' });
|
||||
send({ type: 'log', text: '─── 3b: Marker-Triangulierung ────────────────────────────' });
|
||||
const runFiles3b = await fsPromises.readdir(runDir);
|
||||
const numPoses = runFiles3b.filter(f => f.endsWith('_camera_pose.json')).length;
|
||||
if (numPoses >= 2) {
|
||||
send({ type: 'log', text: `▷ 3b_corner_marker_poses (${numPoses} Kamera-Posen)` });
|
||||
const exit3b = await runScript([
|
||||
SCRIPT_3B,
|
||||
'--evalDir', runDir,
|
||||
'--robot', ROBOT_JSON,
|
||||
], send);
|
||||
if (exit3b !== 0) send({ type: 'log', text: `❌ Script 3b Exit ${exit3b}` });
|
||||
} else {
|
||||
send({ type: 'log', text: `⚠ Nur ${numPoses} Kamera-Pose(n) vorhanden – Script 3b braucht ≥2 Kameras für Triangulierung, wird übersprungen.` });
|
||||
}
|
||||
send({ type: 'log', text: '' });
|
||||
// 2–3b: Board-Pipeline (Foto + Scripts 1, 2, 3b)
|
||||
await runBoardPipeline(runDir, send, { refSet });
|
||||
|
||||
send({ type: 'log', text: `✅ Board-Run abgeschlossen: ${ts}` });
|
||||
send({ type: 'done', exitCode: 0, runDir: ts });
|
||||
@@ -714,6 +729,108 @@ app.get('/api/board/latest', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ── Homing ───────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* POST /api/homing/run
|
||||
* Vollständiger Homing-Ablauf: Board-Pipeline + 4b-Kette (SSE-Stream).
|
||||
*/
|
||||
app.post('/api/homing/run', async (req, res) => {
|
||||
res.setHeader('Content-Type', 'text/event-stream');
|
||||
res.setHeader('Cache-Control', 'no-cache');
|
||||
res.setHeader('Connection', 'keep-alive');
|
||||
res.flushHeaders();
|
||||
|
||||
const send = (obj) => {
|
||||
if (!res.writableEnded) res.write(`data: ${JSON.stringify(obj)}\n\n`);
|
||||
};
|
||||
|
||||
try {
|
||||
await fsPromises.mkdir(homingDataDir, { recursive: true });
|
||||
await runHoming({
|
||||
robotJsonPath: ROBOT_JSON,
|
||||
homingDir: homingDataDir,
|
||||
send,
|
||||
runScript,
|
||||
runBoardPipeline,
|
||||
SCRIPT_4B,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('homing/run error:', err);
|
||||
try {
|
||||
send({ type: 'error', text: String(err) });
|
||||
send({ type: 'done', exitCode: -1 });
|
||||
} catch {}
|
||||
}
|
||||
if (!res.writableEnded) res.end();
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/homing/send-state
|
||||
* Sendet { state: { x, y, z, a, b, c, e } } an ROBOT_URL/api/state.
|
||||
*/
|
||||
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' });
|
||||
|
||||
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 });
|
||||
} catch (err) {
|
||||
console.error('homing/send-state error:', err);
|
||||
return res.status(500).json({ error: String(err) });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/homing/run-data?run=<timestamp>
|
||||
* Gibt Bilder (base64) und JSON-Dateien eines Homing-Runs zurück.
|
||||
*/
|
||||
app.get('/api/homing/run-data', async (req, res) => {
|
||||
try {
|
||||
const runName = req.query.run;
|
||||
if (!runName) return res.status(400).json({ error: '"run" parameter fehlt' });
|
||||
const runDir = path.join(homingDataDir, runName);
|
||||
let files = [];
|
||||
try { files = await fsPromises.readdir(runDir); } catch {}
|
||||
|
||||
const images = [];
|
||||
for (const f of files.sort()) {
|
||||
if (/\.(jpg|jpeg|png)$/i.test(f)) {
|
||||
try {
|
||||
const buf = await fsPromises.readFile(path.join(runDir, f));
|
||||
images.push({ filename: f, contentBase64: buf.toString('base64'), mimeType: 'image/jpeg' });
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
// Letzten accumulated_state zurückgeben
|
||||
let finalState = null;
|
||||
const stateFiles = files.filter(f => f.startsWith('state_') && f.endsWith('.json')).sort();
|
||||
if (stateFiles.length > 0) {
|
||||
try {
|
||||
const raw = await fsPromises.readFile(path.join(runDir, stateFiles[stateFiles.length - 1]), 'utf8');
|
||||
finalState = JSON.parse(raw).accumulated_state ?? null;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return res.json({ runDir: runName, images, finalState });
|
||||
} catch (err) {
|
||||
return res.status(500).json({ error: String(err) });
|
||||
}
|
||||
});
|
||||
|
||||
// ── Robot-JSON bearbeiten ─────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user