refactor: cleanup GUI + AI movement controls + depth-correction polish

GUI:
- Rimossa tab "Calibrazione" (sostituita dalla calibrazione via AI con saved
  task `calibrazione_dx`/`calibrazione_sx`, oppure dalla tab "Auto-
  Calibrazione" per il flusso a griglia automatica). Backend
  calibration_routes invariato — tool AI continuano a usarlo.

AI tools:
- Esposto `precise` su `move_wrist_absolute` e `move_wrist_relative`
  (forwarded al campo omonimo del bridge robot, controlla la modalità di
  controllo: false=veloce/tolleranza ampia, true=lento/posa accurata).
- Aggiunto `arms_go_neutral` come tool dedicato (POST /arms/go_neutral del
  bridge): usato dall'AI per "torna a casa / posizione di riposo". Niente
  side, agisce su entrambe le braccia.
- Fix `device_status`: ora wrappa la risposta con `_ok(...)` così il
  dispatcher la classifica correttamente.

Prompt:
- System prompt aggiornato: l'AI ora passa SEMPRE `precise=true` di default
  sui movimenti, escluso quando l'utente chiede esplicitamente velocità.
  Sezione dedicata su come tornare a riposo (arms_go_neutral).
- Documentato che `measured_side_mm` (ritornato da get_marker_3d) viene
  distorto dalla correzione affine sul depth — non usarlo per validare la
  size_mm dichiarata.

Saved tasks:
- `calibrazione_dx` e `calibrazione_sx` ora partono e finiscono con
  `arms_go_neutral`, cancellano i sample preesistenti per quel lato, e
  acquisiscono ≥10 sample con `precise=true` su una griglia ±80mm + ±15° di
  rotazione attorno alla posa home.

Config:
- Marker corrente: id=10, size=40mm (cambiato il tag fisico sul polso).
- Addendum prompt allentato: niente più "max 3 righe", ora chiede di
  riportare il risultato di OGNI tool chiamato.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal PastroandClaude Opus 4.7 committed 2026-05-06 17:05:52 +02:00
1 parent ac573505b7
commit f6197488f9
6 files changed
+80 -344

No files matched your search

+1 -260
View File
@@ -109,259 +109,6 @@ async function api(method, path, body = null) {
return data;
}
// ==================== CALIBRAZIONE ====================
const calibCanvas = $('calibCanvas');
const calibCtx = calibCanvas.getContext('2d');
let displayedRgb = null; // last successfully loaded Image (stays on screen)
let rgbInFlight = false;
let previewInFlight = false;
let lastMarker = null;
let lastMarkerTs = 0;
const MARKER_STALE_MS = 1500; // keep overlay this long across hiccups
function drawCalibFrame() {
if (displayedRgb) {
if (calibCanvas.width !== displayedRgb.naturalWidth) {
calibCanvas.width = displayedRgb.naturalWidth;
calibCanvas.height = displayedRgb.naturalHeight;
}
calibCtx.drawImage(displayedRgb, 0, 0);
} else {
calibCtx.fillStyle = '#000';
calibCtx.fillRect(0, 0, calibCanvas.width, calibCanvas.height);
calibCtx.fillStyle = '#666';
calibCtx.font = '14px sans-serif';
calibCtx.fillText('no RGB', 20, 30);
}
const fresh = lastMarker && lastMarker.found && (performance.now() - lastMarkerTs < MARKER_STALE_MS);
if (fresh) {
const c = lastMarker.corners;
calibCtx.strokeStyle = '#3ecf8e';
calibCtx.lineWidth = 3;
calibCtx.beginPath();
calibCtx.moveTo(c[0][0], c[0][1]);
for (let i = 1; i < c.length; i++) calibCtx.lineTo(c[i][0], c[i][1]);
calibCtx.closePath();
calibCtx.stroke();
calibCtx.fillStyle = '#3ecf8e';
c.forEach(p => { calibCtx.beginPath(); calibCtx.arc(p[0], p[1], 4, 0, 2 * Math.PI); calibCtx.fill(); });
const cx = lastMarker.center_px[0], cy = lastMarker.center_px[1];
calibCtx.font = 'bold 14px sans-serif';
const d = lastMarker.pose_camera && lastMarker.pose_camera.distance_m;
calibCtx.fillText(`ID ${lastMarker.marker_id}` + (d ? ` · ${(d*100).toFixed(1)} cm` : ''), cx + 10, cy - 10);
}
}
function refreshRgb() {
if (rgbInFlight) return;
rgbInFlight = true;
const img = new Image();
img.onload = () => {
displayedRgb = img;
rgbInFlight = false;
drawCalibFrame();
};
img.onerror = () => { rgbInFlight = false; /* keep previous frame */ };
img.src = `/api/calibration/rgb.jpg?t=${Date.now()}`;
}
async function previewMarker() {
if (previewInFlight) return;
if (!$('livePreview').checked) {
if (lastMarker) { lastMarker = null; drawCalibFrame(); }
return;
}
previewInFlight = true;
try {
const body = { marker_id: +$('arucoId').value, size_mm: +$('arucoSize').value };
const data = await api('POST', '/api/calibration/preview', body);
lastMarker = data;
lastMarkerTs = performance.now();
const info = $('markerInfo');
const t = fmtTimings(data.timings_ms);
if (data.found) {
const d = data.pose_camera && data.pose_camera.distance_m;
info.textContent = `marker ${data.marker_id} @ ${(d*100).toFixed(1)} cm · ${t}`;
info.style.color = 'var(--ok)';
} else {
info.textContent = `marker ${data.marker_id} non trovato · ${t}`;
info.style.color = 'var(--warn)';
}
} catch (e) {
// transient failure — don't wipe the overlay
$('markerInfo').textContent = `preview: ${e.message}`;
$('markerInfo').style.color = 'var(--danger)';
} finally {
previewInFlight = false;
drawCalibFrame();
}
}
// Two independent loops so a slow preview doesn't starve the RGB redraw,
// and a slow RGB doesn't block the marker polling.
setInterval(() => {
if ($('tab-calibration').classList.contains('active')) refreshRgb();
}, 150);
setInterval(() => {
if ($('tab-calibration').classList.contains('active')) previewMarker();
}, 500);
$('sampleBtn').addEventListener('click', async () => {
try {
setStatus('Acquisisco sample…');
const body = {
marker_id: +$('arucoId').value,
size_mm: +$('arucoSize').value,
side: $('side').value,
};
const r = await api('POST', '/api/calibration/sample', body);
setStatus(`Sample #${r.total} acquisito · ${fmtTimings(r.timings_ms)}`, 'ok');
await loadSamples();
} catch (e) { setStatus(e.message, 'err'); }
});
$('solveBtn').addEventListener('click', async () => {
try {
setStatus('Calcolo matrice…');
const side = $('side').value;
const r = await api('POST', '/api/calibration/solve', { method: $('method').value, side });
renderCalibResult(r);
setStatus(`Matrice calcolata (${side}) · ${fmtTimings(r.timings_ms)}`, 'ok');
} catch (e) { setStatus(e.message, 'err'); }
});
$('clearSamplesBtn').addEventListener('click', async () => {
const side = $('side').value;
if (!confirm(`Rimuovere tutti i sample del braccio ${side}?`)) return;
await api('DELETE', `/api/calibration/samples?side=${encodeURIComponent(side)}`);
await loadSamples();
});
$('saveConfigBtn').addEventListener('click', async () => {
const patch = {
aruco: { wrist_marker: { id: +$('arucoId').value, size_mm: +$('arucoSize').value } },
robot: { wrist_side: $('side').value },
calibration: { method: $('method').value },
};
try {
await api('PUT', '/api/config', patch);
setStatus('Configurazione salvata', 'ok');
} catch (e) { setStatus(e.message, 'err'); }
});
// When the user changes the side dropdown, reload per-side samples/result.
$('side').addEventListener('change', async () => {
await loadSamples().catch(() => {});
await loadCalibResult().catch(() => {});
});
// index -> { reason, detail } for samples the last solve dropped
const lastDrops = new Map();
async function loadSamples() {
const side = $('side') ? $('side').value : 'right';
const r = await api('GET', `/api/calibration/samples?side=${encodeURIComponent(side)}`);
$('sampleCount').textContent = r.total;
const ul = $('sampleList');
ul.innerHTML = '';
// Display newest first while keeping the server-side index (used in drop reports).
for (let i = r.samples.length - 1; i >= 0; i--) {
const s = r.samples[i];
const t = s.T_base_wrist;
const p = [t[0][3], t[1][3], t[2][3]].map(v => v.toFixed(3)).join(', ');
const li = document.createElement('li');
const drop = lastDrops.get(i);
let line = `#${i+1} side=${s.side} pos=[${p}] ${s.note || ''}`;
if (drop) {
line += ` · scartato (${drop.reason}: ${drop.detail})`;
li.classList.add('sample-dropped');
}
li.textContent = line;
ul.appendChild(li);
}
}
function renderCalibResult(r) {
const m = r.T_base_camera;
const fmt = (v) => (v >= 0 ? ' ' : '') + v.toFixed(4);
const rows = m.map(row => row.map(fmt).join(' '));
$('calibMatrix').textContent = rows.join('\n');
const meta = [
`method=${r.method}`,
`n=${r.n_samples}`,
`residual: ${r.residual_translation_mm.toFixed(2)} mm / ${r.residual_rotation_deg.toFixed(2)}°`,
];
const sc = r.sanity_check;
lastDrops.clear();
if (sc && sc.applied) {
const drops = [];
const pa = sc.dropped_by_pair_angle || [];
const wm = sc.dropped_by_wm_cluster || [];
const pq = sc.dropped_by_pose_quality || [];
for (const d of pq) {
const key = d.reprojection_px != null ? 'reproj' : 'ambiguity';
const val = d.reprojection_px != null ? `${d.reprojection_px.toFixed(2)} px` : `amb ${(d.ambiguity_ratio*100).toFixed(0)}%`;
lastDrops.set(d.sample_index, { reason: key, detail: val });
}
for (const d of pa) {
lastDrops.set(d.sample_index,
{ reason: 'pair-angle', detail: `${d.pair_angle_err_deg.toFixed(1)}°` });
}
for (const d of wm) {
// A sample can fail both checks; prefer the wm message (happens after solve).
lastDrops.set(d.sample_index,
{ reason: 'T_wrist_marker', detail: `${d.wm_translation_dev_mm.toFixed(1)} mm` });
}
if (pq.length) drops.push(`pose-quality: ${pq.map(d => `#${d.sample_index+1}(${d.reprojection_px != null ? d.reprojection_px.toFixed(2)+'px' : (d.ambiguity_ratio*100).toFixed(0)+'%'})`).join(', ')}`);
if (pa.length) drops.push(`pair-angle: ${pa.map(d => `#${d.sample_index+1}(${d.pair_angle_err_deg.toFixed(1)}°)`).join(', ')}`);
if (wm.length) drops.push(`T_wrist_marker: ${wm.map(d => `#${d.sample_index+1}(${d.wm_translation_dev_mm.toFixed(1)}mm)`).join(', ')}`);
const totalDropped = (sc.dropped_by_pose_quality || []).length + pa.length + wm.length;
if (drops.length) {
meta.push(`\n⚠ scartati ${totalDropped}/${sc.n_input}: ${drops.join(' · ')}`);
} else {
meta.push(`\n✓ nessun outlier (${sc.n_input} sample usati)`);
}
}
$('calibMeta').textContent = meta.join(' ');
$('calibMeta').style.whiteSpace = 'pre-wrap';
// Refresh the list so dropped rows turn red (if already visible).
loadSamples().catch(() => {});
}
async function loadCalibResult() {
const side = $('side') ? $('side').value : 'right';
try { renderCalibResult(await api('GET', `/api/calibration/result?side=${encodeURIComponent(side)}`)); } catch {}
}
// auto-grid
$('runGridBtn').addEventListener('click', async () => {
const quat = $('gridQuat').value.split(',').map(Number);
if (quat.length !== 4) { setStatus('quat deve avere 4 valori', 'err'); return; }
const body = {
start_position: [+$('gridX0').value, +$('gridY0').value, +$('gridZ0').value],
orientation_quaternion_xyzw: quat,
step_x_m: +$('gridDX').value,
step_y_m: +$('gridDY').value,
step_z_m: +$('gridDZ').value,
n_x: 3, n_y: 3, n_z: 3,
min_z_m: +$('gridMinZ').value,
settle_s: +$('gridSettle').value,
dry_run: $('gridDryRun').checked,
auto_solve: true,
};
$('gridLog').textContent = 'run in progress…';
try {
const r = await api('POST', '/api/calibration/auto_grid', body);
$('gridLog').textContent = JSON.stringify(r, null, 2);
if (r.calibration) renderCalibResult(r.calibration);
await loadSamples();
setStatus(body.dry_run ? 'auto-grid dry-run completato' : 'auto-grid completato', 'ok');
} catch (e) {
$('gridLog').textContent = e.message;
setStatus(e.message, 'err');
}
});
// ==================== TROVA UTENSILE ====================
const graspCanvas = $('graspCanvas');
const graspCtx = graspCanvas.getContext('2d');
@@ -1374,15 +1121,9 @@ startHomePoll();
(async () => {
try {
const cfg = await api('GET', '/api/config');
$('arucoId').value = cfg.aruco.wrist_marker.id;
$('arucoSize').value = cfg.aruco.wrist_marker.size_mm;
$('side').value = cfg.robot.wrist_side;
$('eeSide').value = cfg.robot.wrist_side;
if ($('eeSide')) $('eeSide').value = cfg.robot.wrist_side;
} catch {}
await loadSamples();
await loadCalibResult();
refreshWrist();
refreshRgb();
})();
// ==================== ASSISTENTE AI ====================