""" This program has been developed by students from the bachelor Computer Science at Utrecht University within the Software Project course. © Copyright Utrecht University (Department of Information and Computing Sciences) """ import asyncio import json import logging import zmq.asyncio from fastapi import APIRouter, Request from fastapi.responses import StreamingResponse from zmq.asyncio import Context, Socket from control_backend.core.config import settings from control_backend.schemas.ri_message import GestureCommand, SpeechCommand logger = logging.getLogger(__name__) router = APIRouter() @router.post("/command/speech", status_code=202) async def receive_command_speech(command: SpeechCommand, request: Request): """ Send a direct speech command to the robot. Publishes the command to the internal 'command' topic. The :class:`~control_backend.agents.actuation.robot_speech_agent.RobotSpeechAgent` will forward this to the robot. :param command: The speech command payload. :param request: The FastAPI request object. """ topic = b"command" pub_socket: Socket = request.app.state.endpoints_pub_socket await pub_socket.send_multipart([topic, command.model_dump_json().encode()]) return {"status": "Speech command received"} @router.post("/command/gesture", status_code=202) async def receive_command_gesture(command: GestureCommand, request: Request): """ Send a direct gesture command to the robot. Publishes the command to the internal 'command' topic. The :class:`~control_backend.agents.actuation.robot_speech_agent.RobotGestureAgent` will forward this to the robot. :param command: The speech command payload. :param request: The FastAPI request object. """ topic = b"command" pub_socket: Socket = request.app.state.endpoints_pub_socket await pub_socket.send_multipart([topic, command.model_dump_json().encode()]) return {"status": "Gesture command received"} @router.get("/ping_check") async def ping(request: Request): """ Simple HTTP ping endpoint to check if the backend is reachable. """ pass @router.get("/commands/gesture/tags") async def get_available_gesture_tags(request: Request, count=0): """ Endpoint to retrieve the available gesture tags for the robot. :param request: The FastAPI request object. :return: A list of available gesture tags. """ req_socket = Context.instance().socket(zmq.REQ) req_socket.connect(settings.zmq_settings.internal_gesture_rep_adress) # Check to see if we've got any count given in the query parameter amount = count or None timeout = 5 # seconds await req_socket.send(f"{amount}".encode() if amount else b"None") try: body = await asyncio.wait_for(req_socket.recv(), timeout=timeout) except TimeoutError: body = '{"tags": []}' logger.debug("Got timeout error fetching gestures.") # Handle empty response and JSON decode errors available_tags = [] if body: try: available_tags = json.loads(body).get("tags", []) except json.JSONDecodeError as e: logger.error(f"Failed to parse gesture tags JSON: {e}, body: {body}") # Return empty list on JSON error available_tags = [] return {"available_gesture_tags": available_tags} @router.get("/ping_stream") async def ping_stream(request: Request): """ SSE endpoint for monitoring the Robot Interface connection status. Subscribes to the internal 'ping' topic (published by the RI Communication Agent) and yields status updates to the client. :return: A StreamingResponse of connection status events. """ async def event_stream(): # Set up internal socket to receive ping updates sub_socket = Context.instance().socket(zmq.SUB) sub_socket.connect(settings.zmq_settings.internal_sub_address) sub_socket.setsockopt(zmq.SUBSCRIBE, b"ping") connected = False ping_frequency = 2 # Even though its most likely the updates should alternate # (So, True - False - True - False for connectivity), # let's still check. while True: try: topic, body = await asyncio.wait_for( sub_socket.recv_multipart(), timeout=ping_frequency ) connected = json.loads(body) except TimeoutError: logger.debug("got timeout error in ping loop in ping router") connected = False # Stop if client disconnected if await request.is_disconnected(): logger.info("Client disconnected from SSE") break connectedJson = json.dumps(connected) yield (f"data: {connectedJson}\n\n") return StreamingResponse(event_stream(), media_type="text/event-stream")