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:
1 parent
ac573505b7
commit
f6197488f9
6 files changed
+80
-344
No files matched your search
+1
-260
@@ -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 ====================
|
||||
|
||||
Reference in new issue
Block a user