124 lines
3.7 KiB
Python
124 lines
3.7 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import time
|
|
|
|
from django.views.decorators.csrf import csrf_exempt
|
|
from django.http import HttpResponse, StreamingHttpResponse
|
|
from rest_framework.decorators import api_view
|
|
from rest_framework.response import Response
|
|
|
|
from .services import camera_control_service, gps_service, network_service, video_service
|
|
|
|
|
|
@api_view(["GET"])
|
|
def dashboard_snapshot(request):
|
|
return Response(
|
|
{
|
|
"gps": gps_service.get_latest(),
|
|
"network": network_service.get_latest(),
|
|
"video": video_service.get_status(),
|
|
}
|
|
)
|
|
|
|
|
|
@api_view(["GET"])
|
|
def gps_latest(request):
|
|
return Response(gps_service.get_latest())
|
|
|
|
|
|
@api_view(["GET"])
|
|
def network_latest(request):
|
|
return Response(network_service.get_latest())
|
|
|
|
|
|
@api_view(["GET"])
|
|
def clock_calibration(request):
|
|
server_received_unix_ns = time.time_ns()
|
|
server_sent_unix_ns = time.time_ns()
|
|
response = Response(
|
|
{
|
|
"server_received_unix_ms": round(server_received_unix_ns / 1_000_000.0, 3),
|
|
"server_sent_unix_ms": round(server_sent_unix_ns / 1_000_000.0, 3),
|
|
}
|
|
)
|
|
response["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
|
|
return response
|
|
|
|
|
|
@api_view(["GET"])
|
|
def video_status(request):
|
|
return Response(video_service.get_status())
|
|
|
|
|
|
@csrf_exempt
|
|
@api_view(["GET", "POST"])
|
|
def video_camera(request):
|
|
if request.method == "GET":
|
|
return Response(camera_control_service.get_camera_status())
|
|
|
|
camera = str(request.data.get("camera") or "").strip().lower()
|
|
if camera not in {"head", "waist"}:
|
|
return Response({"detail": "camera must be 'head' or 'waist'"}, status=400)
|
|
|
|
try:
|
|
result = camera_control_service.select_camera(camera)
|
|
except (OSError, RuntimeError) as error:
|
|
return Response({"detail": str(error)}, status=503)
|
|
return Response(result, status=200 if result.get("confirmed") else 504)
|
|
|
|
|
|
def video_frame(request):
|
|
try:
|
|
frame, headers = video_service.get_next_frame_with_headers()
|
|
except (FileNotFoundError, RuntimeError) as error:
|
|
status = video_service.get_status()
|
|
return HttpResponse(
|
|
status.get("source_detail") or str(error),
|
|
status=503,
|
|
content_type="text/plain; charset=utf-8",
|
|
)
|
|
|
|
response = HttpResponse(frame, content_type="image/jpeg")
|
|
response["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
|
|
for key, value in headers.items():
|
|
response[key] = value
|
|
return response
|
|
|
|
|
|
def video_stream(request):
|
|
status = video_service.get_status()
|
|
if not status["available"]:
|
|
return HttpResponse(
|
|
status.get("source_detail") or f"JPEG frame directory not found: {status['frame_dir']}",
|
|
status=503,
|
|
content_type="text/plain; charset=utf-8",
|
|
)
|
|
|
|
try:
|
|
fps = float(request.GET.get("fps", status["fps"]))
|
|
except (TypeError, ValueError):
|
|
fps = float(status["fps"])
|
|
|
|
response = StreamingHttpResponse(
|
|
video_service.iter_mjpeg(fps=fps),
|
|
content_type="multipart/x-mixed-replace; boundary=frame",
|
|
)
|
|
response["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
|
|
return response
|
|
|
|
|
|
@csrf_exempt
|
|
@api_view(["POST"])
|
|
def video_display_probe(request):
|
|
try:
|
|
payload = json.loads(request.body.decode("utf-8"))
|
|
except (UnicodeDecodeError, json.JSONDecodeError):
|
|
return Response({"detail": "invalid json"}, status=400)
|
|
|
|
if not isinstance(payload, dict):
|
|
return Response({"detail": "expected json object"}, status=400)
|
|
|
|
video_service.record_display_probe(payload)
|
|
return Response({"ok": True})
|