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 logger.debug(f"Yielded new connection event in robot ping router: {str(connected)}") connectedJson = json.dumps(connected) yield (f"data: {connectedJson}\n\n") return StreamingResponse(event_stream(), media_type="text/event-stream")