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
+5
-4
@@ -20,8 +20,8 @@ services:
|
|||||||
aruco:
|
aruco:
|
||||||
dictionary: DICT_4X4_50
|
dictionary: DICT_4X4_50
|
||||||
wrist_marker:
|
wrist_marker:
|
||||||
id: 20
|
id: 10
|
||||||
size_mm: 103.0
|
size_mm: 40.0
|
||||||
robot:
|
robot:
|
||||||
wrist_side: right
|
wrist_side: right
|
||||||
active: h1
|
active: h1
|
||||||
@@ -53,5 +53,6 @@ chat:
|
|||||||
api_key_env: OPENROUTER_API_KEY
|
api_key_env: OPENROUTER_API_KEY
|
||||||
referer: http://localhost:8890
|
referer: http://localhost:8890
|
||||||
title: presa-mano
|
title: presa-mano
|
||||||
user_prompt_addendum: L'operatore preferisce risposte molto telegrafiche, max
|
user_prompt_addendum: Misure sempre in mm interi. Sii conciso ma riporta i risultati
|
||||||
3 righe se possibile. Misure sempre in mm interi.
|
di OGNI tool che hai chiamato (anche solo in una riga compatta), non saltare
|
||||||
|
informazioni rilevanti.
|
||||||
@@ -52,11 +52,25 @@ un robot umanoide (Unitree G1 o H1) con una camera depth Nuwa-HP60C fissa.
|
|||||||
- Movimenti **relativi**: `move_wrist_relative(side, delta_position)` —
|
- Movimenti **relativi**: `move_wrist_relative(side, delta_position)` —
|
||||||
delta in metri, frame base. Se l'utente dice "muovi di 200 mm in X+", passa
|
delta in metri, frame base. Se l'utente dice "muovi di 200 mm in X+", passa
|
||||||
delta=[0.2, 0, 0]. Conferma sempre il risultato leggendo la nuova posa.
|
delta=[0.2, 0, 0]. Conferma sempre il risultato leggendo la nuova posa.
|
||||||
|
- **Modalità precise**: per default passa SEMPRE `precise=true` su
|
||||||
|
`move_wrist_absolute` e `move_wrist_relative`. Imposta `precise=false`
|
||||||
|
solo se l'utente lo chiede esplicitamente (es. "muovi veloce",
|
||||||
|
"avvicinati grossomodo"). Lo `precise=true` garantisce che il controller
|
||||||
|
raggiunga la posa target con accuratezza fine — necessario per
|
||||||
|
acquisizione sample di calibrazione, avvicinamento al grasp, e in generale
|
||||||
|
ogni movimento dove la posa finale conta.
|
||||||
- Default `dry_run=true` su tutte le operazioni di motion: NON fai mai partire
|
- Default `dry_run=true` su tutte le operazioni di motion: NON fai mai partire
|
||||||
un movimento reale senza che l'utente l'abbia chiesto esplicitamente.
|
un movimento reale senza che l'utente l'abbia chiesto esplicitamente.
|
||||||
- Le mani: `hand_action(side, action)` con `action ∈
|
- Le mani: `hand_action(side, action)` con `action ∈
|
||||||
{{LEFT_OPEN, LEFT_CLOSE, RIGHT_OPEN, RIGHT_CLOSE}}`.
|
{{LEFT_OPEN, LEFT_CLOSE, RIGHT_OPEN, RIGHT_CLOSE}}`.
|
||||||
|
|
||||||
|
## Posizione di riposo (home) delle braccia
|
||||||
|
- Quando l'operatore dice **"torna a casa"**, **"home"**, **"posizione di
|
||||||
|
riposo"** o simili, chiama `arms_go_neutral()`. Agisce su entrambe le
|
||||||
|
braccia simultaneamente (è un comando del bridge, niente side da scegliere).
|
||||||
|
- Default `dry_run=true` come per gli altri movimenti — chiedi conferma
|
||||||
|
prima di mandarlo dal vivo.
|
||||||
|
|
||||||
## Calibrazione hand-eye (eye-to-hand)
|
## Calibrazione hand-eye (eye-to-hand)
|
||||||
La calibrazione lega `T_base_camera`. È **specifica per braccio**: dx e sx
|
La calibrazione lega `T_base_camera`. È **specifica per braccio**: dx e sx
|
||||||
hanno file separati. Procedura completa quando l'utente dice "calibra":
|
hanno file separati. Procedura completa quando l'utente dice "calibra":
|
||||||
|
|||||||
@@ -89,7 +89,10 @@ def _h_set_active_robot(args: dict[str, Any]) -> dict[str, Any]:
|
|||||||
def _h_device_status(_: dict[str, Any]) -> dict[str, Any]:
|
def _h_device_status(_: dict[str, Any]) -> dict[str, Any]:
|
||||||
"""Reuses the home_routes implementation to avoid duplicating the ping logic."""
|
"""Reuses the home_routes implementation to avoid duplicating the ping logic."""
|
||||||
from ..api.home_routes import status as _status
|
from ..api.home_routes import status as _status
|
||||||
return _status()
|
try:
|
||||||
|
return _ok(**_status())
|
||||||
|
except Exception as e:
|
||||||
|
return _err(str(e))
|
||||||
|
|
||||||
|
|
||||||
# ----- tool: wrist read/move ------------------------------------------------
|
# ----- tool: wrist read/move ------------------------------------------------
|
||||||
@@ -118,11 +121,12 @@ def _h_move_wrist_absolute(args: dict[str, Any]) -> dict[str, Any]:
|
|||||||
return _err("position deve avere 3 float (m), quaternion 4 float (xyzw)")
|
return _err("position deve avere 3 float (m), quaternion 4 float (xyzw)")
|
||||||
dry_run = bool(args.get("dry_run", True))
|
dry_run = bool(args.get("dry_run", True))
|
||||||
timeout_s = float(args.get("timeout_s", 5.0))
|
timeout_s = float(args.get("timeout_s", 5.0))
|
||||||
|
precise = bool(args.get("precise", False))
|
||||||
result = _deps.g1(dry_run=dry_run).wrist_absolute(
|
result = _deps.g1(dry_run=dry_run).wrist_absolute(
|
||||||
side=side, position=position, quaternion=quaternion,
|
side=side, position=position, quaternion=quaternion,
|
||||||
wait=True, timeout_s=timeout_s,
|
wait=True, timeout_s=timeout_s, precise=precise,
|
||||||
)
|
)
|
||||||
return _ok(dry_run=dry_run, side=side, response=result)
|
return _ok(dry_run=dry_run, side=side, precise=precise, response=result)
|
||||||
except (G1Unavailable, G1StatusError) as e:
|
except (G1Unavailable, G1StatusError) as e:
|
||||||
return _err(f"motion failed: {e}")
|
return _err(f"motion failed: {e}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -140,19 +144,34 @@ def _h_move_wrist_relative(args: dict[str, Any]) -> dict[str, Any]:
|
|||||||
return _err("delta_quaternion deve avere 4 float (xyzw) o essere null")
|
return _err("delta_quaternion deve avere 4 float (xyzw) o essere null")
|
||||||
dry_run = bool(args.get("dry_run", True))
|
dry_run = bool(args.get("dry_run", True))
|
||||||
timeout_s = float(args.get("timeout_s", 5.0))
|
timeout_s = float(args.get("timeout_s", 5.0))
|
||||||
|
precise = bool(args.get("precise", False))
|
||||||
result = _deps.g1(dry_run=dry_run).wrist_relative(
|
result = _deps.g1(dry_run=dry_run).wrist_relative(
|
||||||
side=side,
|
side=side,
|
||||||
delta_position=list(dp) if dp is not None else None,
|
delta_position=list(dp) if dp is not None else None,
|
||||||
delta_quaternion=list(dq) if dq is not None else None,
|
delta_quaternion=list(dq) if dq is not None else None,
|
||||||
wait=True, timeout_s=timeout_s,
|
wait=True, timeout_s=timeout_s, precise=precise,
|
||||||
)
|
)
|
||||||
return _ok(dry_run=dry_run, side=side, response=result)
|
return _ok(dry_run=dry_run, side=side, precise=precise, response=result)
|
||||||
except (G1Unavailable, G1StatusError) as e:
|
except (G1Unavailable, G1StatusError) as e:
|
||||||
return _err(f"motion failed: {e}")
|
return _err(f"motion failed: {e}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return _err(str(e))
|
return _err(str(e))
|
||||||
|
|
||||||
|
|
||||||
|
def _h_arms_go_neutral(args: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Riporta entrambe le braccia alla posa di home / riposo built-in del
|
||||||
|
bridge (POST /arms/go_neutral). È l'unico modo standard di "tornare a
|
||||||
|
casa" — non serve registrare pose custom."""
|
||||||
|
try:
|
||||||
|
dry_run = bool(args.get("dry_run", True))
|
||||||
|
result = _deps.g1(dry_run=dry_run).arms_go_neutral()
|
||||||
|
return _ok(dry_run=dry_run, response=result)
|
||||||
|
except (G1Unavailable, G1StatusError) as e:
|
||||||
|
return _err(f"go_neutral failed: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
return _err(str(e))
|
||||||
|
|
||||||
|
|
||||||
def _h_hand_action(args: dict[str, Any]) -> dict[str, Any]:
|
def _h_hand_action(args: dict[str, Any]) -> dict[str, Any]:
|
||||||
try:
|
try:
|
||||||
action = str(args.get("action", "")).upper()
|
action = str(args.get("action", "")).upper()
|
||||||
@@ -422,6 +441,7 @@ _HANDLERS: dict[str, Callable[[dict[str, Any]], dict[str, Any]]] = {
|
|||||||
"move_wrist_absolute": _h_move_wrist_absolute,
|
"move_wrist_absolute": _h_move_wrist_absolute,
|
||||||
"move_wrist_relative": _h_move_wrist_relative,
|
"move_wrist_relative": _h_move_wrist_relative,
|
||||||
"hand_action": _h_hand_action,
|
"hand_action": _h_hand_action,
|
||||||
|
"arms_go_neutral": _h_arms_go_neutral,
|
||||||
"get_marker": _h_get_marker,
|
"get_marker": _h_get_marker,
|
||||||
"get_marker_3d": _h_get_marker_3d,
|
"get_marker_3d": _h_get_marker_3d,
|
||||||
"detect_object": _h_detect_object,
|
"detect_object": _h_detect_object,
|
||||||
@@ -479,7 +499,13 @@ TOOL_DEFINITIONS: list[dict[str, Any]] = [
|
|||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
"name": "move_wrist_absolute",
|
"name": "move_wrist_absolute",
|
||||||
"description": "Sposta il polso a una posa assoluta nel frame base. Default dry_run=true: il robot NON si muove davvero finché dry_run=false.",
|
"description": (
|
||||||
|
"Sposta il polso a una posa assoluta nel frame base. Default dry_run=true: "
|
||||||
|
"il robot NON si muove davvero finché dry_run=false. Usa precise=true per "
|
||||||
|
"movimenti che richiedono accuratezza fine (es. acquisizione sample di "
|
||||||
|
"calibrazione, avvicinamento finale al grasp); precise=false è il default, "
|
||||||
|
"più veloce ma con tolleranza maggiore."
|
||||||
|
),
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["side", "position", "quaternion"],
|
"required": ["side", "position", "quaternion"],
|
||||||
@@ -490,6 +516,8 @@ TOOL_DEFINITIONS: list[dict[str, Any]] = [
|
|||||||
"quaternion": {"type": "array", "items": {"type": "number"}, "minItems": 4, "maxItems": 4,
|
"quaternion": {"type": "array", "items": {"type": "number"}, "minItems": 4, "maxItems": 4,
|
||||||
"description": "x,y,z,w"},
|
"description": "x,y,z,w"},
|
||||||
"dry_run": {"type": "boolean", "default": True},
|
"dry_run": {"type": "boolean", "default": True},
|
||||||
|
"precise": {"type": "boolean", "default": False,
|
||||||
|
"description": "true = controllo accurato (più lento)"},
|
||||||
"timeout_s": {"type": "number", "default": 5.0},
|
"timeout_s": {"type": "number", "default": 5.0},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -499,7 +527,11 @@ TOOL_DEFINITIONS: list[dict[str, Any]] = [
|
|||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
"name": "move_wrist_relative",
|
"name": "move_wrist_relative",
|
||||||
"description": "Sposta il polso di un delta rispetto alla posa attuale. delta_position in metri (frame base), delta_quaternion opzionale. Default dry_run=true.",
|
"description": (
|
||||||
|
"Sposta il polso di un delta rispetto alla posa attuale. delta_position "
|
||||||
|
"in metri (frame base), delta_quaternion opzionale. Default dry_run=true. "
|
||||||
|
"Usa precise=true se serve accuratezza fine."
|
||||||
|
),
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["side"],
|
"required": ["side"],
|
||||||
@@ -508,11 +540,27 @@ TOOL_DEFINITIONS: list[dict[str, Any]] = [
|
|||||||
"delta_position": {"type": "array", "items": {"type": "number"}, "minItems": 3, "maxItems": 3},
|
"delta_position": {"type": "array", "items": {"type": "number"}, "minItems": 3, "maxItems": 3},
|
||||||
"delta_quaternion": {"type": "array", "items": {"type": "number"}, "minItems": 4, "maxItems": 4},
|
"delta_quaternion": {"type": "array", "items": {"type": "number"}, "minItems": 4, "maxItems": 4},
|
||||||
"dry_run": {"type": "boolean", "default": True},
|
"dry_run": {"type": "boolean", "default": True},
|
||||||
|
"precise": {"type": "boolean", "default": False},
|
||||||
"timeout_s": {"type": "number", "default": 5.0},
|
"timeout_s": {"type": "number", "default": 5.0},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "arms_go_neutral",
|
||||||
|
"description": (
|
||||||
|
"Riporta ENTRAMBE le braccia alla posa di home/riposo del robot "
|
||||||
|
"(POST /arms/go_neutral sul bridge). Usa quando l'utente dice "
|
||||||
|
"'torna a casa', 'home', 'posizione di riposo'. Default dry_run=true."
|
||||||
|
),
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"dry_run": {"type": "boolean", "default": True}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
|
|||||||
@@ -70,8 +70,11 @@ class G1Client:
|
|||||||
|
|
||||||
def wrist_relative(self, side: str, delta_position: list[float] | None = None,
|
def wrist_relative(self, side: str, delta_position: list[float] | None = None,
|
||||||
delta_quaternion: list[float] | None = None, wait: bool = True,
|
delta_quaternion: list[float] | None = None, wait: bool = True,
|
||||||
timeout_s: float = 5.0) -> dict[str, Any]:
|
timeout_s: float = 5.0, precise: bool = False) -> dict[str, Any]:
|
||||||
body: dict[str, Any] = {"side": side, "wait": wait, "timeout_s": timeout_s}
|
# `precise` is forwarded but the bridge silently ignores it on
|
||||||
|
# /wrist/relative if not supported — safer to always send it.
|
||||||
|
body: dict[str, Any] = {"side": side, "wait": wait, "timeout_s": timeout_s,
|
||||||
|
"precise": precise}
|
||||||
if delta_position is not None:
|
if delta_position is not None:
|
||||||
body["delta_position"] = delta_position
|
body["delta_position"] = delta_position
|
||||||
if delta_quaternion is not None:
|
if delta_quaternion is not None:
|
||||||
|
|||||||
+1
-260
@@ -109,259 +109,6 @@ async function api(method, path, body = null) {
|
|||||||
return data;
|
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 ====================
|
// ==================== TROVA UTENSILE ====================
|
||||||
const graspCanvas = $('graspCanvas');
|
const graspCanvas = $('graspCanvas');
|
||||||
const graspCtx = graspCanvas.getContext('2d');
|
const graspCtx = graspCanvas.getContext('2d');
|
||||||
@@ -1374,15 +1121,9 @@ startHomePoll();
|
|||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const cfg = await api('GET', '/api/config');
|
const cfg = await api('GET', '/api/config');
|
||||||
$('arucoId').value = cfg.aruco.wrist_marker.id;
|
if ($('eeSide')) $('eeSide').value = cfg.robot.wrist_side;
|
||||||
$('arucoSize').value = cfg.aruco.wrist_marker.size_mm;
|
|
||||||
$('side').value = cfg.robot.wrist_side;
|
|
||||||
$('eeSide').value = cfg.robot.wrist_side;
|
|
||||||
} catch {}
|
} catch {}
|
||||||
await loadSamples();
|
|
||||||
await loadCalibResult();
|
|
||||||
refreshWrist();
|
refreshWrist();
|
||||||
refreshRgb();
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// ==================== ASSISTENTE AI ====================
|
// ==================== ASSISTENTE AI ====================
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
<button class="tab active" data-tab="home">Home</button>
|
<button class="tab active" data-tab="home">Home</button>
|
||||||
<button class="tab" data-tab="agent">Assistente AI</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="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="autocal">Auto-Calibrazione</button>
|
||||||
<button class="tab" data-tab="grasp">TrovaUtensile</button>
|
<button class="tab" data-tab="grasp">TrovaUtensile</button>
|
||||||
<button class="tab" data-tab="servo">Servo Visuale</button>
|
<button class="tab" data-tab="servo">Servo Visuale</button>
|
||||||
@@ -176,76 +175,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</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">
|
<section class="panel" id="tab-autocal">
|
||||||
<div class="diag3d-toolbar">
|
<div class="diag3d-toolbar">
|
||||||
<button id="acFitBtn">Auto-fit</button>
|
<button id="acFitBtn">Auto-fit</button>
|
||||||
|
|||||||
Reference in new issue
Block a user