Files
Presa-Mano/web/index.html
T
Adriano Dal PastroandClaude Opus 4.7 ac573505b7 feat: initial commit — orchestratore presa-mano con AI assistant + dual-robot
Pipeline completa per calibrazione hand-eye e grasp planning su robot
Unitree G1/H1 con camera depth Nuwa-HP60C. Esposta come FastAPI + GUI vanilla
JS, con assistente AI (OpenRouter + tool calling curato) per pilotare il
sistema in linguaggio naturale.

Architettura:
- FastAPI orchestrator (porta da .env, default 8890) che coordina 4
  microservizi upstream: Nuwa-HP60C, marker-finder, YOLO, robot bridge.
- Dual-robot: services.g1 e services.h1 in config.yaml; switchabili a
  runtime via PUT /api/home/robot.
- Calibrazione hand-eye eye-to-hand split per-side (right/left).
- Marker pose: marker-finder riceve gli intrinseci RGB della Nuwa al boot,
  PnP server-side con quei dati. Nessun re-PnP locale richiesto.
- Depth-to-RGB: registrazione iterativa IR↔RGB per back-projection 3D
  corretta (Nuwa /api/measure usa coords IR; il nostro /api/perception/
  measure_rgb prende pixel RGB e ritorna 3D nel frame RGB OpenCV).
- Correzione affine sul depth bias (calibrabile via marker come
  ground-truth) — riduce errore Z da ~6% a ~1.5% nei test.
- AI assistant: 16 tool curati (move_wrist_*, hand_action, get_marker[_3d],
  detect_object, calibration_*, set_active_robot…), system prompt
  dinamico che riflette robot attivo + marker config + addendum
  customizzabile da GUI. Temperature di default 0.1 per evitare invenzioni.

GUI (7 tab):
- Home: stato comunicazione device + selettore robot attivo G1/H1
- Assistente AI: chat con OpenRouter, history persistita lato browser,
  task riutilizzabili (prompt salvati), trace tool calls
- Impostazioni AI: modello da combo curata, temperature, max iterations,
  prompt addendum, anteprima system prompt
- Calibrazione, Auto-Calibrazione, TrovaUtensile, Servo Visuale, Vista 3D,
  Movimento EE — invariate dalle versioni precedenti

Setup:
- Variabili in presa-mano/.env (template in .env.example)
- OPENROUTER_API_KEY richiesta solo per il tab Assistente AI
- PRESA_MANO_PORT configurabile

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 13:28:23 +02:00

580 lines
28 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>presa-mano</title>
<link rel="stylesheet" href="/static/style.css" />
</head>
<body>
<header>
<h1>presa-mano</h1>
<nav class="tabs">
<button class="tab active" data-tab="home">Home</button>
<button class="tab" data-tab="agent">Assistente AI</button>
<button class="tab" data-tab="ai-settings">Impostazioni AI</button>
<button class="tab" data-tab="calibration">Calibrazione</button>
<button class="tab" data-tab="autocal">Auto-Calibrazione</button>
<button class="tab" data-tab="grasp">TrovaUtensile</button>
<button class="tab" data-tab="servo">Servo Visuale</button>
<button class="tab" data-tab="view3d">Vista 3D</button>
<button class="tab" data-tab="ee">Movimento EE</button>
</nav>
<div class="status-strip" id="statusStrip"></div>
<span class="active-robot-pill" id="activeRobotPill" title="Robot attualmente collegato">robot: —</span>
</header>
<main>
<!-- ============================== HOME ============================== -->
<section class="panel active" id="tab-home">
<div class="posture-toolbar">
<label class="inline">Robot attivo
<select id="homeRobotSelect">
<option value="g1">G1</option>
<option value="h1">H1</option>
</select>
</label>
<button id="homeApplyRobotBtn" class="primary">Applica</button>
<label class="inline"><input type="checkbox" id="homeAutoPoll" checked /> Auto-poll</label>
<label class="inline">ogni
<input type="number" id="homePollMs" value="2000" min="500" step="500" style="width:80px"> ms
</label>
<button id="homeRefreshBtn">Aggiorna ora</button>
</div>
<div class="info-box">
Stato di comunicazione con i microservizi upstream. Il selettore sopra cambia il robot attivo (G1 o H1) e aggiorna <code>config.yaml</code> a runtime; le chiamate <code>/api/wrist</code>, <code>/api/posture</code>, <code>/api/robot</code> useranno il robot selezionato.
</div>
<h2>Stato dispositivi</h2>
<table class="device-status-table" id="homeDeviceTable">
<thead>
<tr>
<th>Servizio</th>
<th>URL</th>
<th>Stato</th>
<th>Latenza</th>
<th>Dettagli</th>
</tr>
</thead>
<tbody>
<tr><td colspan="5" class="dim">caricamento…</td></tr>
</tbody>
</table>
<h3>Ultimo esito</h3>
<pre id="homeLog" class="log small">—</pre>
</section>
<!-- ============================== ASSISTENTE AI ============================== -->
<section class="panel" id="tab-agent">
<div class="posture-toolbar">
<span class="post-health" id="agentHealth">—</span>
<span class="active-robot-pill" id="agentActiveRobotPill" title="Robot attivo">robot: —</span>
<button id="agentClearBtn" title="Resetta la conversazione">Nuova conversazione</button>
</div>
<div class="grid-2 agent-layout">
<!-- LEFT: chat history + input -->
<div>
<h2>Chat</h2>
<div id="agentChat" class="agent-chat">
<div class="agent-msg system">Ciao. Dimmi cosa vuoi fare con il robot. Es: "muovi il braccio destro 200 mm in X+", "calibra il braccio destro", "mostra lo stato dei dispositivi".</div>
</div>
<div class="agent-input-row">
<textarea id="agentInput" rows="3" placeholder="Scrivi un comando o una richiesta in linguaggio naturale… (Ctrl+Invio per inviare)"></textarea>
<button id="agentSendBtn" class="primary">Invia ▸</button>
</div>
<div class="marker-info" id="agentStatus">—</div>
</div>
<!-- RIGHT: saved tasks + tool trace -->
<div>
<h2>Task salvati</h2>
<div class="info-box">
Salva un prompt usato spesso (es. "calibra braccio dx") per riutilizzarlo con un click. Il prompt corrente nella textarea viene salvato.
</div>
<div class="kv compact">
<label>Nome <input type="text" id="agentTaskName" placeholder="es. calibrazione_dx" maxlength="80"></label>
<label>Descrizione <input type="text" id="agentTaskDesc" placeholder="(opzionale)" maxlength="400"></label>
</div>
<div class="actions">
<button id="agentTaskSaveBtn" class="primary">💾 Salva prompt corrente</button>
<button id="agentTaskReloadBtn">⟳ Ricarica</button>
</div>
<ul id="agentTaskList" class="agent-task-list">
<li class="dim">caricamento…</li>
</ul>
<h2>Tool trace ultima risposta</h2>
<pre id="agentTrace" class="log small">(nessuna chiamata)</pre>
</div>
</div>
</section>
<!-- ============================== IMPOSTAZIONI AI ============================== -->
<section class="panel" id="tab-ai-settings">
<div class="info-box">
Parametri del client OpenRouter usato dall'Assistente AI. Le modifiche vengono persistite in <code>config.yaml</code> via <code>PUT /api/config</code> e applicate alla chat successiva (non c'è bisogno di restart). Il <strong>prompt aggiuntivo</strong> viene appeso al prompt di sistema dinamico — non sostituisce le regole base (frame, tool, sicurezza).
</div>
<div class="grid-2">
<div>
<h2>Modello e parametri</h2>
<div class="ctrl-block">
<div class="kv compact">
<label>Modello (combo)
<select id="aiModelSelect">
<option value="">caricamento…</option>
</select>
</label>
<label>Modello (custom — sovrascrive la combo)
<input type="text" id="aiModelCustom" placeholder="es. anthropic/claude-sonnet-4.5" />
</label>
<label title="0 = deterministico, 1 = creativo. Per orchestrazione robotica tieni basso (0.0–0.3).">
Temperature
<input type="number" id="aiTemperature" min="0" max="2" step="0.05" />
</label>
<label title="Massimo numero di iterazioni di tool calling per singolo messaggio">
Max tool iterations
<input type="number" id="aiMaxIterations" min="1" max="20" step="1" />
</label>
<label title="Timeout HTTP per la singola chiamata a OpenRouter (secondi)">
Timeout (s)
<input type="number" id="aiTimeout" min="5" max="300" step="5" />
</label>
</div>
<div class="actions">
<button id="aiSaveBtn" class="primary">💾 Salva impostazioni</button>
<button id="aiReloadBtn">⟳ Ricarica da config</button>
<button id="aiResetTempBtn" title="Riporta temperature a 0.1 (default safe)">Reset temp</button>
</div>
<div class="marker-info" id="aiSettingsStatus">—</div>
</div>
<h2>Prompt aggiuntivo</h2>
<div class="ctrl-block">
<div class="info-box">
Testo che viene appeso al system prompt dinamico (sezione "Istruzioni aggiuntive dell'operatore"). Usa per personalizzare lo stile, aggiungere contesto specifico al tuo setup, o regole custom. Lascia vuoto per non aggiungere nulla.
</div>
<textarea id="aiPromptAddendum" rows="10" style="width:100%;font-family:ui-monospace,monospace;font-size:12px;padding:8px;background:#0e1116;color:inherit;border:1px solid var(--border);border-radius:4px"></textarea>
</div>
</div>
<div>
<h2>Anteprima system prompt corrente</h2>
<div class="info-box">
Quello che il modello riceve davvero al prossimo messaggio (include lo stato corrente: robot attivo, marker, ecc., più l'eventuale addendum sopra dopo che lo salvi).
</div>
<div class="actions">
<button id="aiPromptPreviewBtn">⟳ Aggiorna anteprima</button>
<button id="aiPromptCopyBtn">📋 Copia</button>
</div>
<pre id="aiPromptPreview" class="log small" style="max-height:560px;overflow:auto">(premi 'Aggiorna anteprima')</pre>
</div>
</div>
</section>
<!-- ============================== CALIBRAZIONE ============================== -->
<section class="panel" id="tab-calibration">
<div class="grid-2">
<div>
<h2>Vista camera</h2>
<div class="canvas-wrap">
<canvas id="calibCanvas" width="640" height="480"></canvas>
</div>
<div class="kv">
<label>ArUco ID <input type="number" id="arucoId" value="10" min="0" /></label>
<label>Size (mm) <input type="number" id="arucoSize" value="40" step="1" /></label>
<label>Side
<select id="side"><option value="right" selected>right</option><option value="left">left</option></select>
</label>
<label>Metodo
<select id="method">
<option value="TSAI" selected>TSAI</option>
<option value="PARK">PARK</option>
<option value="HORAUD">HORAUD</option>
<option value="ANDREFF">ANDREFF</option>
<option value="DANIILIDIS">DANIILIDIS</option>
</select>
</label>
<label class="inline"><input type="checkbox" id="livePreview" checked /> Preview marker</label>
<button id="saveConfigBtn">Salva configurazione</button>
</div>
<div class="marker-info" id="markerInfo">marker: —</div>
</div>
<div>
<h2>Sample e matrice</h2>
<div class="actions">
<button id="sampleBtn" class="primary">Memorizza posizione (sample)</button>
<button id="solveBtn" class="primary">Calcola matrice</button>
<button id="clearSamplesBtn" class="danger">Rimuovi tutti i sample</button>
</div>
<div class="samples">
<div class="samples-head">
<strong>Sample acquisiti</strong>
<span id="sampleCount">0</span>
</div>
<ul id="sampleList"></ul>
</div>
<div class="result">
<strong>T<sub>base ← camera</sub></strong>
<pre id="calibMatrix">(non ancora calcolata)</pre>
<div id="calibMeta"></div>
</div>
</div>
</div>
<details class="auto-grid">
<summary>Auto-grid (future update) — 3×3 × 3 altezze</summary>
<div class="kv">
<label>Start X (m) <input type="number" id="gridX0" value="0.25" step="0.01" /></label>
<label>Start Y (m) <input type="number" id="gridY0" value="0.0" step="0.01" /></label>
<label>Start Z (m) <input type="number" id="gridZ0" value="0.15" step="0.01" /></label>
<label>Step X (m) <input type="number" id="gridDX" value="0.05" step="0.005" /></label>
<label>Step Y (m) <input type="number" id="gridDY" value="0.05" step="0.005" /></label>
<label>Step Z (m) <input type="number" id="gridDZ" value="0.05" step="0.005" /></label>
<label>min Z tavolo (m) <input type="number" id="gridMinZ" value="0.10" step="0.005" /></label>
<label>Settle (s) <input type="number" id="gridSettle" value="1.0" step="0.1" /></label>
<label>Quat xyzw <input type="text" id="gridQuat" value="0,0,0,1" /></label>
<label class="inline"><input type="checkbox" id="gridDryRun" checked /> Dry-run (consigliato finché G1 down)</label>
<button id="runGridBtn">Esegui auto-grid</button>
</div>
<pre id="gridLog" class="log"></pre>
</details>
</section>
<!-- ============================== AUTO-CALIBRAZIONE ============================== -->
<section class="panel" id="tab-autocal">
<div class="diag3d-toolbar">
<button id="acFitBtn">Auto-fit</button>
<button id="acResetViewBtn">Reset vista</button>
<label class="inline"><input type="checkbox" id="acShowTriads" checked /> Triad sui waypoint</label>
<span class="post-health" id="autocalStatus">—</span>
</div>
<div class="grid-2 diag3d-layout">
<div>
<div class="canvas-wrap">
<canvas id="autocalCanvas" width="720" height="560"></canvas>
</div>
<div class="marker-info">drag = ruota · wheel = zoom · shift+drag = pan</div>
<div class="diag3d-legend">
<div><span class="legend-sw" style="background:#aaaaaa;border-radius:50%"></span> raggiungibile</div>
<div><span class="legend-sw" style="background:var(--danger);border-radius:50%"></span> non raggiungibile (skip)</div>
<div><span class="legend-sw" style="background:var(--accent);border-radius:50%"></span> corrente</div>
<div><span class="legend-sw" style="background:var(--ok);border-radius:50%"></span> campione acquisito</div>
<div><span class="legend-sw" style="background:var(--warn);border-radius:50%"></span> fallito</div>
</div>
</div>
<div>
<h2>Parametri griglia</h2>
<div class="ctrl-block">
<div class="info-box">
Parti dalla posizione corrente del polso (letta dal G1). La griglia copre ±delta/2 su ciascun asse. Le divisioni sono i punti per asse: 3 significa [start-Δ/2, start, start+Δ/2].
</div>
<div class="kv compact">
<label>Δx (mm) <input type="number" id="acDx" value="100" step="10" min="0" max="500"></label>
<label>Δy (mm) <input type="number" id="acDy" value="100" step="10" min="0" max="500"></label>
<label>Δz (mm) <input type="number" id="acDz" value="80" step="10" min="0" max="500"></label>
<label title="Spostamento del centro griglia rispetto alla posa polso attuale">offset x (mm) <input type="number" id="acOx" value="0" step="10" min="-500" max="500"></label>
<label title="Spostamento del centro griglia rispetto alla posa polso attuale">offset y (mm) <input type="number" id="acOy" value="0" step="10" min="-500" max="500"></label>
<label title="Spostamento del centro griglia rispetto alla posa polso attuale">offset z (mm) <input type="number" id="acOz" value="0" step="10" min="-500" max="500"></label>
<label>n_x <input type="number" id="acNx" value="3" step="1" min="1" max="7"></label>
<label>n_y <input type="number" id="acNy" value="3" step="1" min="1" max="7"></label>
<label>n_z <input type="number" id="acNz" value="3" step="1" min="1" max="7"></label>
<label>rot polso (°) <input type="number" id="acRotDeg" value="10" step="1" min="0" max="45"></label>
<label>min z (m) <input type="number" id="acMinZ" value="0.05" step="0.01"></label>
<label>settle (s) <input type="number" id="acSettle" value="1.0" step="0.1" min="0.1" max="5"></label>
<label>side
<select id="acSide">
<option value="">(config)</option>
<option value="right">right</option>
<option value="left">left</option>
</select>
</label>
<label class="inline"><input type="checkbox" id="acAutoSolve" checked /> auto-solve finale</label>
<label class="inline"><input type="checkbox" id="acDryRun" /> dry-run (preview, nessun moto reale)</label>
</div>
<div class="actions">
<button id="acPreviewBtn" class="primary">Calcola griglia</button>
<button id="acVerifyBtn" title="muove a ogni punto senza acquisire — testa raggiungibilità reale">Verifica reale</button>
<button id="acStartBtn" disabled>▶ Start calibrazione</button>
<button id="acStopBtn" class="danger" disabled>■ Stop + torna a home</button>
<button id="acSolveBtn">Calcola matrice</button>
<button id="acClearBtn" class="danger">Rimuovi sample</button>
</div>
<div class="kv compact">
<label>Ordine di visita
<select id="acOrder">
<option value="serpentine" selected>serpentina (distanza min.)</option>
<option value="nested">nested (xyz semplice)</option>
</select>
</label>
</div>
</div>
<h2>Stato</h2>
<div id="autocalStats" class="ctrl-block">
<div class="dim">nessuna griglia calcolata</div>
</div>
<h2>Tentativi per punto</h2>
<canvas id="autocalChart" width="520" height="140" class="autocal-chart"></canvas>
<h2>Risultato calibrazione</h2>
<pre id="autocalResult" class="log small">—</pre>
</div>
</div>
</section>
<!-- ============================== TROVA UTENSILE ============================== -->
<section class="panel" id="tab-grasp">
<div class="grid-2">
<div>
<h2>RGB + detection — click per misurare</h2>
<div class="canvas-wrap">
<canvas id="graspCanvas" width="640" height="480"></canvas>
</div>
<div class="measure-strip" id="measureInfo">click sull'immagine per la distanza</div>
<div class="kv">
<label>Nuvola di <select id="graspObjectPick"><option value="">(top overall)</option></select></label>
<label>Confidenza <input type="number" id="graspConf" value="0.5" step="0.05" min="0" max="1" /></label>
<label class="inline"><input type="checkbox" id="graspDryRun" checked /> Dry-run</label>
</div>
<div id="graspLegend" class="grasp-legend"></div>
<div class="actions">
<button id="detectBtn">Rileva oggetto</button>
<button id="loopBtn">▶ Loop riconoscimento</button>
<label class="inline" style="margin-left:8px"><input type="number" id="loopRateMs" value="500" min="100" step="50" style="width:80px"> ms</label>
<button id="planBtn" class="primary">Pianifica grasp</button>
<button id="executeBtn" class="primary">Esegui presa</button>
</div>
</div>
<div>
<h2>Crop utensile</h2>
<div class="canvas-wrap small">
<canvas id="cropCanvas" width="320" height="240"></canvas>
</div>
<h2>Nuvola punti oggetto (base frame)</h2>
<div class="scatter-row">
<div>
<div class="scatter-title">Top-down (X-Y)</div>
<canvas id="scatterXY" width="260" height="260"></canvas>
</div>
<div>
<div class="scatter-title">Laterale (X-Z)</div>
<canvas id="scatterXZ" width="260" height="260"></canvas>
</div>
</div>
<h3>Precisione (ultime <span id="statsWindow">5</span> detection)</h3>
<table class="stats" id="statsTable">
<thead>
<tr><th>asse</th><th>media [m]</th><th>σ [mm]</th><th>range [mm]</th><th>min</th><th>max</th></tr>
</thead>
<tbody>
<tr><td>x</td><td>—</td><td>—</td><td>—</td><td>—</td><td>—</td></tr>
<tr><td>y</td><td>—</td><td>—</td><td>—</td><td>—</td><td>—</td></tr>
<tr><td>z</td><td>—</td><td>—</td><td>—</td><td>—</td><td>—</td></tr>
</tbody>
</table>
<div id="statsMeta" class="marker-info">loop: inattivo</div>
<h3>Dettagli</h3>
<pre id="graspOut" class="log">(nessuna detection)</pre>
</div>
</div>
</section>
<!-- ============================== SERVO VISUALE ============================== -->
<section class="panel" id="tab-servo">
<div class="diag3d-toolbar">
<span class="post-health" id="servoStatus">—</span>
</div>
<div class="grid-2 diag3d-layout">
<div>
<div id="servoViewport" style="position:relative;width:min(100%,720px);height:440px;overflow:hidden;background:#000;border:1px solid var(--border);border-radius:4px;cursor:grab;user-select:none">
<div id="servoPanZoom" style="position:absolute;left:0;top:0;transform-origin:0 0;will-change:transform">
<img id="servoImg" alt="Nuwa RGB" style="display:block;max-width:none;pointer-events:none" draggable="false" />
<canvas id="servoOverlay" style="position:absolute;left:0;top:0;pointer-events:none"></canvas>
</div>
</div>
<div class="marker-info">RGB live · ciano = marker · giallo = oggetto · wheel zoom · drag pan · doppio click reset</div>
<h3 style="margin-top:14px">Vista 3D (frame camera)</h3>
<div class="canvas-wrap" style="width:min(100%,720px)">
<canvas id="servo3dCanvas" width="720" height="360" style="display:block;background:#0a0d11;border:1px solid var(--border);border-radius:4px"></canvas>
</div>
<div class="marker-info">origine = camera · piano viola · marker ciano · oggetto giallo · proiezioni più chiare · drag ruota · wheel zoom</div>
<div class="diag3d-legend">
<div><span class="legend-sw" style="background:#4dd0e1;border-radius:50%"></span> marker polso</div>
<div><span class="legend-sw" style="background:#ffd54f;border-radius:50%"></span> oggetto (centro bbox)</div>
</div>
</div>
<div>
<h2>Parametri</h2>
<div class="ctrl-block">
<div class="info-box">
Avvicinamento marker-relativo senza hand-eye calibration: la camera vede marker e oggetto nello stesso frame, ogni step sposta il polso della frazione indicata della distanza rimanente. Si ferma a <code>min_distance_m</code>.
</div>
<div class="kv compact">
<label>Classe (filtro) <input type="text" id="svClass" placeholder="(tutte)" /></label>
<label>Confidenza <input type="number" id="svConf" value="0.5" step="0.05" min="0" max="1" /></label>
<label>min dist (mm) <input type="number" id="svMinDist" value="100" step="10" min="20" max="500" /></label>
<label>frazione step <input type="number" id="svStepFrac" value="0.5" step="0.05" min="0.05" max="1.0" /></label>
<label>timeout (s) <input type="number" id="svTimeout" value="5" step="0.5" min="1" max="30" /></label>
<label>side
<select id="svSide">
<option value="">(config)</option>
<option value="right">right</option>
<option value="left">left</option>
</select>
</label>
<label class="inline"><input type="checkbox" id="svDryRun" checked /> dry-run (nessun moto reale)</label>
</div>
<div class="info-box" style="margin-top:8px">
<strong>Orientazione marker rispetto al polso</strong> (rvec in gradi). Se non allineato al frame polso il target risulta spostato: tuna finché marker, oggetto e target sono coerenti.
</div>
<div class="kv compact">
<label>rx (°) <input type="number" id="svRx" value="0" step="5" min="-180" max="180" /></label>
<label>ry (°) <input type="number" id="svRy" value="0" step="5" min="-180" max="180" /></label>
<label>rz (°) <input type="number" id="svRz" value="0" step="5" min="-180" max="180" /></label>
<button id="svSaveRvecBtn" title="Salva l'rvec corrente in config.yaml">Salva rvec in config</button>
</div>
<div class="actions">
<button id="svPlanBtn" class="primary">Rileva / ricalcola</button>
<button id="svStepBtn">▶ Vai al prossimo</button>
<button id="svResetBtn">Reset</button>
</div>
<div class="info-box" style="margin-top:8px">
<strong>Piano di lavoro</strong>. Clicca "Seleziona area", poi traccia un rettangolo sull'immagine con shift+drag: il backend calcola il piano dalla depth in quell'area. Marker e oggetto vengono poi proiettati su questo piano.
</div>
<div class="actions">
<button id="svPlaneSelBtn">Seleziona area</button>
<button id="svPlaneFitBtn">Calcola piano</button>
<button id="svPlaneClearBtn" class="danger">Rimuovi piano</button>
</div>
<div id="svPlaneInfo" class="marker-info">piano: —</div>
</div>
<h2>Stato</h2>
<div id="servoStats" class="ctrl-block">
<div class="dim">nessun piano calcolato</div>
</div>
<h2>Dettagli piano</h2>
<pre id="servoResult" class="log small">—</pre>
</div>
</div>
</section>
<!-- ============================== VISTA 3D ============================== -->
<section class="panel" id="tab-view3d">
<div class="grid-2">
<div>
<h2>Scena 3D — frame base robot</h2>
<div class="canvas-wrap">
<canvas id="view3dCanvas" width="720" height="540"></canvas>
</div>
<div class="marker-info" id="view3dHint">drag = ruota · wheel = zoom · doppio click = reset vista · shift+drag = pan</div>
<div class="marker-info" id="view3dStatusLine">—</div>
</div>
<div>
<h2>Dati</h2>
<div class="kv">
<label>Classe (filtro) <input type="text" id="view3dClass" placeholder="(tutte)" /></label>
<label>Confidenza <input type="number" id="view3dConf" value="0.5" step="0.05" min="0" max="1" /></label>
<label class="inline"><input type="checkbox" id="view3dShowCloud" checked /> Punti oggetto</label>
<label class="inline"><input type="checkbox" id="view3dShowGrid" checked /> Griglia tavolo</label>
<label class="inline"><input type="checkbox" id="view3dShowPlan" checked /> Pianifica grasp</label>
<label class="inline"><input type="checkbox" id="view3dAutoFit" checked /> Auto-fit vista</label>
</div>
<div class="actions">
<button id="view3dRefreshBtn" class="primary">Aggiorna scena</button>
<button id="view3dLoopBtn">▶ Loop</button>
<label class="inline" style="margin-left:8px"><input type="number" id="view3dRateMs" value="800" min="200" step="50" style="width:80px"> ms</label>
<button id="view3dResetBtn">Reset vista</button>
</div>
<div class="legend">
<div class="legend-row"><span class="dot" style="background:#e74c3c"></span> X (rosso) — base</div>
<div class="legend-row"><span class="dot" style="background:#3ecf8e"></span> Y (verde) — base</div>
<div class="legend-row"><span class="dot" style="background:#4da3ff"></span> Z (blu) — base</div>
<div class="legend-row"><span class="dot" style="background:#f0ad4e"></span> Polso destro</div>
<div class="legend-row"><span class="dot" style="background:#9b59b6"></span> Polso sinistro</div>
<div class="legend-row"><span class="dot" style="background:#aaaaaa"></span> Punti oggetto</div>
<div class="legend-row"><span class="dot" style="background:#ffd54f"></span> Centroide oggetto</div>
<div class="legend-row"><span class="dot" style="background:#ff5e7e"></span> Punto di presa</div>
<div class="legend-row"><span class="dot" style="background:#ffffff;border:1px solid #888"></span> Pre-grasp + traiettoria</div>
</div>
<h3>Stato</h3>
<pre id="view3dOut" class="log">(nessun dato — premi "Aggiorna scena")</pre>
</div>
</div>
</section>
<!-- ============================== EE ============================== -->
<section class="panel" id="tab-ee">
<div class="grid-2">
<div>
<h2>Polso corrente</h2>
<pre id="wristCurrent" class="log">—</pre>
<button id="refreshWristBtn">Aggiorna pose</button>
<h3>Posa assoluta</h3>
<div class="kv">
<label>Side <select id="eeSide"><option value="right">right</option><option value="left">left</option></select></label>
<label>Px <input type="number" id="absPx" step="0.01" value="0.30"></label>
<label>Py <input type="number" id="absPy" step="0.01" value="0.10"></label>
<label>Pz <input type="number" id="absPz" step="0.01" value="0.20"></label>
<label>Qx <input type="number" id="absQx" step="0.01" value="0"></label>
<label>Qy <input type="number" id="absQy" step="0.01" value="0"></label>
<label>Qz <input type="number" id="absQz" step="0.01" value="0"></label>
<label>Qw <input type="number" id="absQw" step="0.01" value="1"></label>
<label class="inline"><input type="checkbox" id="eeDryRun" /> Dry-run</label>
</div>
<button id="absSendBtn" class="primary">Invia assoluta</button>
<h3>Delta relativo</h3>
<div class="kv">
<label>ΔX <input type="number" id="relDx" step="0.01" value="0"></label>
<label>ΔY <input type="number" id="relDy" step="0.01" value="0"></label>
<label>ΔZ <input type="number" id="relDz" step="0.01" value="0.05"></label>
</div>
<button id="relSendBtn" class="primary">Invia relativa</button>
</div>
<div>
<h2>Mano + sicurezza</h2>
<div class="actions">
<button id="handOpenBtn">RIGHT_OPEN</button>
<button id="handCloseBtn">RIGHT_CLOSE</button>
<button id="handOpenLBtn">LEFT_OPEN</button>
<button id="handCloseLBtn">LEFT_CLOSE</button>
</div>
<div class="actions">
<button id="estopBtn" class="danger">EMERGENCY STOP</button>
<button id="unlockBtn">Unlock</button>
</div>
<h3>Ultimo esito</h3>
<pre id="eeLog" class="log">—</pre>
</div>
</div>
</section>
</main>
<div id="toastHost" class="toast-host" aria-live="polite"></div>
<script type="importmap">
{
"imports": {
"three": "/static/vendor/three/build/three.module.js",
"three/addons/": "/static/vendor/three/addons/"
}
}
</script>
<script src="/static/app.js"></script>
<script type="module" src="/static/autocal3d.js"></script>
<script type="module" src="/static/servo.js"></script>
</body>
</html>