Camera di stazione: acquisizione dal frontend e campi admin (#72)
Route POST /measure/api/vision/grab (proxy verso il servizio camera), get_station_by_code, pulsante «Acquisisci dalla camera», grab() in vision_execute.js, campi camera_url e camera_source nell'admin stazioni, traduzioni it/en, extra_hosts host.docker.internal nel compose di sviluppo. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -345,6 +345,7 @@ def task_execute(task_id: int):
|
||||
all_task_ids=all_task_ids,
|
||||
measurement_interval_minutes=measurement_interval_minutes,
|
||||
allow_manual_input=allow_manual_input,
|
||||
camera_enabled=_station_camera(_current_station()[0]) is not None,
|
||||
)
|
||||
|
||||
|
||||
@@ -1000,6 +1001,64 @@ def api_vision_execute():
|
||||
return jsonify(resp), 200
|
||||
|
||||
|
||||
def _station_camera(station_code: str | None) -> tuple[str, str] | None:
|
||||
"""(camera_url, camera_source) of the station, None when it has no camera.
|
||||
|
||||
A station that cannot be read counts as "no camera": the grab button simply
|
||||
does not show, the file input keeps working.
|
||||
"""
|
||||
if not station_code:
|
||||
return None
|
||||
station = api_client.get_station_by_code(station_code)
|
||||
if not isinstance(station, dict) or station.get("error"):
|
||||
return None
|
||||
url = station.get("camera_url")
|
||||
source = station.get("camera_source")
|
||||
if not url or not source:
|
||||
return None
|
||||
return str(url), str(source)
|
||||
|
||||
|
||||
# #72: the browser asks the station's camera service (vs-gui) for a frame; the
|
||||
# service address is a server-side matter, so Flask relays and the PNG goes back as is.
|
||||
@measure_bp.route("/api/vision/grab", methods=["POST"])
|
||||
@login_required
|
||||
@role_required("MeasurementTec")
|
||||
def api_vision_grab():
|
||||
"""Proxy: grab one frame from the current station's camera, answer a PNG."""
|
||||
camera = _station_camera(_current_station()[0])
|
||||
if camera is None:
|
||||
return jsonify({
|
||||
"error": True,
|
||||
"detail": _("Camera non configurata per questa stazione"),
|
||||
}), 409
|
||||
camera_url, camera_source = camera
|
||||
try:
|
||||
resp = http_requests.post(
|
||||
f"{camera_url.rstrip('/')}/camera/grab",
|
||||
json={"source": camera_source, "trigger": "continuo", "timeout_ms": 5000},
|
||||
timeout=15,
|
||||
)
|
||||
except http_requests.RequestException as exc:
|
||||
return jsonify({
|
||||
"error": True,
|
||||
"detail": _("Servizio camera non raggiungibile: %(detail)s", detail=str(exc)),
|
||||
}), 502
|
||||
if resp.status_code != 200:
|
||||
detail = ""
|
||||
try:
|
||||
body = resp.json()
|
||||
detail = body.get("detail", "") if isinstance(body, dict) else ""
|
||||
except ValueError:
|
||||
detail = (resp.text or "")[:200]
|
||||
return jsonify({
|
||||
"error": True,
|
||||
"detail": _("Acquisizione non riuscita (%(status)s): %(detail)s",
|
||||
status=resp.status_code, detail=detail),
|
||||
}), 502
|
||||
return Response(resp.content, content_type="image/png")
|
||||
|
||||
|
||||
@measure_bp.route("/api/vision/results/<int:result_id>/image", methods=["GET"])
|
||||
@login_required
|
||||
@role_required("MeasurementTec")
|
||||
|
||||
Reference in New Issue
Block a user