fix(right_arm): bbox dict, image_size da /api/status, tab non seleziona robot

Tre problemi della tab Braccio DX dopo il primo rodaggio:

1. /detect/front e /detect/wrist ritornavano bbox raw dal YOLO server,
   che lo manda come lista [x1,y1,x2,y2]; la GUI legge bbox.x1 e mostra
   NaN. Ora il router normalizza il payload via perception/detector
   ._normalize, allineandosi al resto del codice. Aggiunta regressione
   con payload reale del server.

2. La tab gateava su robot.active==g1 && wrist_side==right con banner
   "Attiva G1 right" e POST /activate. Confonde: la selezione del
   robot e' globale e non si flippa per-feature. Rimossi banner,
   bottone, endpoint /activate e _guard_g1_right(); gli endpoint
   operano sul polso destro del robot attivo qualunque esso sia.

3. image_size: [0,0] perche' Nuwa /api/intrinsics non include
   width/height; vivono in /api/status > data.resolution. Aggiunto
   NuwaClient.image_size() e usato in detect_front. Aggiornato
   docstring di intrinsics() per coerenza con il payload reale.

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-21 11:43:05 +02:00
1 parent 3284cd0424
commit 2f6444c0a5
5 files changed
+57 -93

No files matched your search

+37 -25
View File
@@ -49,19 +49,6 @@ def test_state_502_when_g1_unavailable():
assert r.status_code == 502
def test_activate_sets_robot_active_g1_and_side_right():
# /activate è idempotente: chiama PUT /api/config e ritorna /state
with patch("presa_mano.api.right_arm_routes._deps.g1") as g1_factory:
g1 = MagicMock()
g1.wrist.return_value = _fake_wrist_response()
g1_factory.return_value = g1
r = client.post("/api/right_arm/activate")
assert r.status_code == 200
body = r.json()
assert body["robot_active"] == "g1"
assert body["side"] == "right"
def test_snapshot_front_returns_jpeg():
fake_jpeg = b"\xff\xd8\xff" + b"\x00" * 100 # magic JPEG bytes
with patch("presa_mano.api.right_arm_routes._deps.front_camera") as factory:
@@ -112,7 +99,7 @@ def test_detect_front_filters_class_and_returns_top():
ying.return_value = yolo
cam = MagicMock()
cam.rgb_url.return_value = "http://192.168.123.99:8878/api/rgb"
cam.intrinsics.return_value = {"width": 640, "height": 480}
cam.image_size.return_value = (640, 480)
cam_f.return_value = cam
r = client.post("/api/right_arm/detect/front",
@@ -123,6 +110,40 @@ def test_detect_front_filters_class_and_returns_top():
assert body["image_size"] == [640, 480]
def test_detect_front_normalizes_real_yolo_envelope_with_list_bbox():
"""Regressione: il YOLO server reale ritorna bbox come lista [x1,y1,x2,y2]
dentro {success, data, error}. Il router deve normalizzare in {x1,y1,x2,y2}."""
real_payload = {
"success": True,
"data": {
"detections": [
{"bbox": [376.7, 17.1, 500.7, 193.7],
"confidence": 0.949, "class": "Pestello"},
{"bbox": [95.6, 134.8, 207.7, 231.3],
"confidence": 0.917, "class": "Mortaio"},
],
},
"error": None,
}
with patch("presa_mano.api.right_arm_routes._deps.yolo") as ying, \
patch("presa_mano.api.right_arm_routes._deps.front_camera") as cam_f:
yolo = MagicMock()
yolo.detect_url.return_value = real_payload
ying.return_value = yolo
cam = MagicMock()
cam.rgb_url.return_value = "http://192.168.123.99:8878/api/rgb"
cam.image_size.return_value = (640, 480)
cam_f.return_value = cam
r = client.post("/api/right_arm/detect/front",
json={"class_name": "Pestello"})
assert r.status_code == 200
body = r.json()
top = body["detections"][0]
assert top["class"] == "Pestello"
assert top["bbox"] == {"x1": 376.7, "y1": 17.1, "x2": 500.7, "y2": 193.7}
def test_detect_front_404_when_no_match():
with patch("presa_mano.api.right_arm_routes._deps.yolo") as ying, \
patch("presa_mano.api.right_arm_routes._deps.front_camera") as cam_f:
@@ -131,7 +152,7 @@ def test_detect_front_404_when_no_match():
ying.return_value = yolo
cam = MagicMock()
cam.rgb_url.return_value = "http://x/api/rgb"
cam.intrinsics.return_value = {"width": 640, "height": 480}
cam.image_size.return_value = (640, 480)
cam_f.return_value = cam
r = client.post("/api/right_arm/detect/front",
json={"class_name": "pestello"})
@@ -155,7 +176,7 @@ def test_detect_front_falls_back_to_proxy_url_on_yolo_failure():
ying.return_value = yolo
cam = MagicMock()
cam.rgb_url.return_value = "http://192.168.123.99:8878/api/rgb"
cam.intrinsics.return_value = {"width": 640, "height": 480}
cam.image_size.return_value = (640, 480)
cam_f.return_value = cam
r = client.post("/api/right_arm/detect/front",
@@ -319,15 +340,6 @@ def test_move_toward_422_oversize_delta_without_force():
assert "delta" in r.json()["detail"].lower()
def test_move_toward_412_when_not_g1_right():
"""Se config attiva != g1 o side != right, rifiuta con 412."""
with patch("presa_mano.api.right_arm_routes._deps.active_robot_key", return_value="h1"), \
patch("presa_mano.api.right_arm_routes._deps.default_side", return_value="left"):
r = client.post("/api/right_arm/move/toward",
json={"class_name": "pestello", "dry_run": True})
assert r.status_code == 412
def test_home_calls_wrist_absolute_with_home_pose():
calls = []
def _abs(**kw):