
    ީi]X                       d Z ddlmZ ddlZddlZddlZddlmZ ddlm	Z	m
Z
mZmZmZ ddlZddlmZ ddlmZmZmZmZmZmZmZmZmZmZmZmZmZmZm Z  e
ege	f   Z!e
ege	f   Z"e
ege	f   Z#e
ege	f   Z$e
ege	f   Z%e
ege	f   Z&e
ege	f   Z'e
ege	f   Z(e
e)ge	f   Z* G d	 d
e+      Z, G d de,      Z- G d de,      Z.e G d d             Z/ G d d      Z0y)a  Edge Proxy WebSocket client.

This module provides the EdgeProxyClient for bidirectional WebSocket communication
with the robot Edge Proxy, following the Edge Proxy Design spec.
See: /home/nelsen/Projects/HRI/docs/plans/2026-02-04-edge-proxy-design.md

Topology (current implementation):
- Edge Proxy runs as a WebSocket *server* on the robot (default :8080/edge).
- Orchestrator connects to it as a WebSocket *client*.
    )annotationsN)	dataclass)AnyCallableDictListOptional)WebSocketClientProtocol   )CancelNavigationCommandCaptureFrameCommandErrorMessageEventLogMessageFRDetectionsMessageFrameResponseMessageGetStateCommandMessageTypeNavigateCommandNavStatusMessagePongMessage
RobotStateWaypointWaypointListMessageparse_edge_messagec                      e Zd ZdZy)EdgeProxyClientErrorz,Base exception for Edge Proxy client errors.N__name__
__module____qualname____doc__     E/home/nelsen/Projects/kognitive/orchestrator/src/edge_proxy/client.pyr   r   6   s    6r#   r   c                      e Zd ZdZy)EdgeProxyConnectionErrorz+Raised when connection to Edge Proxy fails.Nr   r"   r#   r$   r&   r&   <   s    5r#   r&   c                      e Zd ZdZy)MessageErrorz0Raised when message parsing or validation fails.Nr   r"   r#   r$   r(   r(   B   s    :r#   r(   c                      e Zd ZU dZdZded<   dZded<   dZded	<   d
Zded<   dZ	ded<   dZ
ded<   dZded<   dZded<   y)ClientConfigzSConfiguration for EdgeProxyClient.

    Spec: Section 3.1 of Edge Proxy Design
    	localhoststrhosti  intportz/edgews_pathTbool	reconnectg      ?floatreconnect_delayg      >@max_reconnect_delayg      $@ping_intervalping_timeoutN)r   r   r    r!   r-   __annotations__r/   r0   r2   r4   r5   r6   r7   r"   r#   r$   r*   r*   H   sZ    
 D#D#GSIt OU !%%M5L%r#   r*   c                  h   e Zd ZdZ	 	 d	 	 	 	 	 d dZd!dZd"dZd#dZd$dZd%dZ	d&d	Z
d'd
Zd(dZd)dZed*d       Zd+dZd+dZ	 	 d,	 	 	 	 	 	 	 d-dZ	 	 	 d.	 	 	 	 	 	 	 	 	 	 	 d/dZ	 	 d0	 	 	 	 	 	 	 	 	 d1dZ	 	 d2	 	 	 	 	 d3dZd4d5dZd+dZd4d5dZd+dZd+dZd+dZd6dZd7dZd8dZd9dZd:dZ y);EdgeProxyClienta"  WebSocket client for Edge Proxy communication.

    This client connects to the robot's Edge Proxy server and handles
    bidirectional communication with automatic reconnection.

    Example:
        client = EdgeProxyClient(host="robot.local", port=8080)

        @client.on_nav_status
        def handle_nav_status(msg):
            print(f"Nav status: {msg.status}, progress: {msg.progress}")

        await client.connect()
        await client.send_navigate_waypoint("waypoint1", request_id="nav_001")
        await client.disconnect()
    Nc                   |xs
 t               | _        |xs t        j                  t              | _        d| _        d| _        d| _        g | _	        g | _
        g | _        g | _        g | _        g | _        g | _        g | _        g | _        d| _        d| _        d| _        d| _        t-               | _        g | _        d| _        i | _        y)zInitialize the Edge Proxy client.

        Args:
            config: Client configuration. Uses defaults if not provided.
            logger: Logger instance. Creates default logger if not provided.
        NFi  )r*   configlogging	getLoggerr   logger_ws
_connected_should_stop_nav_status_handlers_robot_state_handlers_waypoint_list_handlers_error_handlers_pong_handlers_frame_response_handlers_fr_detections_handlers_event_log_handlers_connection_change_handlers_receiver_tasklast_robot_statelast_fr_detectionslast_event_logset_seen_event_ids_seen_event_ids_order_seen_event_ids_limit_pending_frames)selfr<   r?   s      r$   __init__zEdgeProxyClient.__init__l   s     .; 1 1( ; 7;! =?!>@"BD$3513DF%BD$:< JL( =A 7; BF9= *-02"%)" ;=r#   c                <    | j                   j                  |       |S )a  Register a navigation status event handler.

        Spec: Section 5.1 - Nav status messages

        Args:
            handler: Async or sync function that takes NavStatusMessage.

        Returns:
            The handler function for use as a decorator.
        )rC   appendrU   handlers     r$   on_nav_statuszEdgeProxyClient.on_nav_status   s     	!!((1r#   c                <    | j                   j                  |       |S )zRegister a robot state event handler.

        Spec: Section 5.2 - Robot state messages

        Args:
            handler: Async or sync function that takes RobotState.

        Returns:
            The handler function for use as a decorator.
        )rD   rX   rY   s     r$   on_robot_statezEdgeProxyClient.on_robot_state   s     	""))'2r#   c                <    | j                   j                  |       |S )a
  Register a waypoint list event handler.

        Spec: Section 5.3 - Waypoint list messages

        Args:
            handler: Async or sync function that takes WaypointListMessage.

        Returns:
            The handler function for use as a decorator.
        )rE   rX   rY   s     r$   on_waypoint_listz EdgeProxyClient.on_waypoint_list   s     	$$++G4r#   c                <    | j                   j                  |       |S )zRegister an error event handler.

        Spec: Section 5.4 - Error messages

        Args:
            handler: Async or sync function that takes ErrorMessage.

        Returns:
            The handler function for use as a decorator.
        )rF   rX   rY   s     r$   on_errorzEdgeProxyClient.on_error   s     	##G,r#   c                <    | j                   j                  |       |S )zRegister a pong event handler.

        Spec: Section 5.5 - Pong messages

        Args:
            handler: Async or sync function that takes PongMessage.

        Returns:
            The handler function for use as a decorator.
        )rG   rX   rY   s     r$   on_pongzEdgeProxyClient.on_pong   s     	""7+r#   c                <    | j                   j                  |       |S )a  Register a frame response event handler.

        Called when the Edge Proxy sends a ``frame_response`` message.
        Note: futures registered via ``send_capture_frame`` are resolved
        *before* these handlers are called, so handlers see all responses
        including those matched to pending futures.

        Args:
            handler: Async or sync function that takes FrameResponseMessage.

        Returns:
            The handler function for use as a decorator.
        )rH   rX   rY   s     r$   on_frame_responsez!EdgeProxyClient.on_frame_response   s     	%%,,W5r#   c                <    | j                   j                  |       |S )aH  Register a face recognition detections event handler.

        Called when the Edge Proxy broadcasts ``fr_detections`` messages
        from its FR loop.

        Args:
            handler: Async or sync function that takes FRDetectionsMessage.

        Returns:
            The handler function for use as a decorator.
        )rI   rX   rY   s     r$   on_fr_detectionsz EdgeProxyClient.on_fr_detections   s     	$$++G4r#   c                <    | j                   j                  |       |S )z#Register an edge event_log handler.)rJ   rX   rY   s     r$   on_event_logzEdgeProxyClient.on_event_log  s      ''0r#   c                <    | j                   j                  |       |S )zRegister a connection state change handler.

        Args:
            handler: Async or sync function that takes bool (connected).

        Returns:
            The handler function for use as a decorator.
        )rK   rX   rY   s     r$   on_connection_changez$EdgeProxyClient.on_connection_change  s     	((//8r#   c                l    | j                   xr' | j                  duxr | j                  j                   S )zvCheck if the client is currently connected.

        Returns:
            True if connected, False otherwise.
        N)rA   r@   closedrU   s    r$   is_connectedzEdgeProxyClient.is_connected  s,     O4884#7O<OOr#   c                N   K   d| _         | j                          d{    y7 w)a)  Connect to the Edge Proxy server.

        Starts the connection and message receiver task.
        If reconnect is enabled in config, will automatically reconnect on disconnect.

        Raises:
            EdgeProxyConnectionError: If initial connection fails and reconnect is disabled.
        FN)rB   _connect_with_retryrn   s    r$   connectzEdgeProxyClient.connect  s"      "&&(((s   %#%c                  K   d| _         | j                  r6| j                  j                          	 | j                   d{    d| _        | j
                  r8| j
                  j                  s"| j
                  j                          d{    | j                  }d| _        |r| j                  d       d{    | j                  j                  d       y7 # t        j                  $ r Y w xY w7 i7 =w)zuDisconnect from the Edge Proxy server.

        Stops the receiver task and closes the WebSocket connection.
        TNFzDisconnected from Edge Proxy)rB   rL   cancelasyncioCancelledErrorr@   rm   closerA   _notify_connection_changer?   info)rU   was_connecteds     r$   
disconnectzEdgeProxyClient.disconnect)  s     
 !&&()))) #'D88DHHOO((.."""0077778 *)) 
 # 8sY   .C9C  CC AC9C5-C99C7: C9C C2/C91C22C97C9c                   K   t        j                  |||      }| j                  |j                                d{    y7 w)aH  Send a waypoint navigation command.

        Spec: Section 4.1 - Navigate to Waypoint

        Args:
            name: Waypoint name.
            request_id: Optional correlation ID.
            speed: Navigation speed ("slow", "normal", "fast").

        Raises:
            EdgeProxyConnectionError: If not connected.
        N)r   to_waypoint_send_messageto_dict)rU   name
request_idspeedcmds        r$   send_navigate_waypointz&EdgeProxyClient.send_navigate_waypointG  s6     $ ))$
EB  ///s   :AAAc                   K   t        j                  |||||      }| j                  |j                                d{    y7 w)a  Send a pose navigation command.

        Spec: Section 4.2 - Navigate to Pose

        Args:
            x: X coordinate in map frame (meters).
            y: Y coordinate in map frame (meters).
            theta: Orientation in radians.
            request_id: Optional correlation ID.
            speed: Navigation speed ("slow", "normal", "fast").

        Raises:
            EdgeProxyConnectionError: If not connected.
        N)r   to_poser~   r   )rU   xythetar   r   r   s          r$   send_navigate_posez"EdgeProxyClient.send_navigate_pose\  s:     , %%aE:uE  ///s   <AAAc                   K   t        j                  ||||      }| j                  |j                                d{    y7 w)a  Send a relative navigation command.

        Spec: Section 4.3 - Navigate Relative

        Args:
            direction: Direction ("forward", "backward", "left", "right").
            distance: Distance in meters.
            request_id: Optional correlation ID.
            speed: Navigation speed (default "slow" for relative).

        Raises:
            EdgeProxyConnectionError: If not connected.
        N)r   to_relativer~   r   )rU   	directiondistancer   r   r   s         r$   send_navigate_relativez&EdgeProxyClient.send_navigate_relativeu  s8     ( )))Xz5Q  ///s   ;AAAc                   K   t        t        j                  ||      }| j                  |j	                                d{    y7 w)a  Send a cancel navigation command.

        Spec: Section 4.4 - Cancel Navigation

        Args:
            request_id: Optional correlation ID.
            reason: Optional reason for cancellation.

        Raises:
            EdgeProxyConnectionError: If not connected.
        )typer   reasonN)r   r   CANCEL_NAVIGATIONr~   r   )rU   r   r   r   s       r$   send_cancel_navigationz&EdgeProxyClient.send_cancel_navigation  s=       &..!

   ///s   ?A	AA	c                   K   t        t        j                  |      }| j                  |j	                                d{    y7 w)a"  Request the current robot state.

        Spec: Section 4.5 - Request State

        The response will be delivered via on_robot_state handlers.

        Args:
            request_id: Optional correlation ID.

        Raises:
            EdgeProxyConnectionError: If not connected.
        )r   r   N)r   r   	GET_STATEr~   r   rU   r   r   s      r$   send_get_statezEdgeProxyClient.send_get_state  s4      ;#8#8ZP  ///s   >A AAc                x   K   ddl m}  |       }| j                  |j                                d{    y7 w)zSend a ping message for keepalive.

        Spec: Section 4.6 - Ping

        Raises:
            EdgeProxyConnectionError: If not connected.
        r   )PingMessageN)messagesr   r~   r   )rU   r   r   s      r$   	send_pingzEdgeProxyClient.send_ping  s,      	*m  ///s   0:8:c                v   K   t        |      }| j                  |j                                d{    y7 w)a  Send a capture_frame command to the Edge Proxy.

        The Edge Proxy will capture one JPEG frame from the camera and reply
        with a ``frame_response`` message.  To await the response, create an
        ``asyncio.Future`` and store it in ``self._pending_frames[request_id]``
        *before* calling this method, then ``await`` the future.

        In practice callers should use ``ActionExecutor._capture_frame()``
        which handles the Future lifecycle automatically.

        Args:
            request_id: Correlation ID used to match the response.

        Raises:
            EdgeProxyConnectionError: If not connected.
        )r   N)r   r~   r   r   s      r$   send_capture_framez"EdgeProxyClient.send_capture_frame  s,     " "Z8  ///s   /979c                "  K   | j                   j                  }| j                  s	 | j                          d{    yyy7 # t        t
        j                  j                  f$ r}| j                  j                  d|       | j                   j                  r| j                  rt        d|       |t        j                  t        || j                   j                               d{  7   |dz  }Y d}~nd}~ww xY w| j                  sЭw)zeConnect with exponential backoff retry if enabled.

        Spec: Section 7.3 - Reconnection
        NzConnection failed: %szFailed to connect:    )r<   r4   rB   _connect_onceOSError
websockets
exceptionsWebSocketExceptionr?   warningr2   r&   ru   sleepminr5   )rU   delayexcs      r$   rq   z#EdgeProxyClient._connect_with_retry  s     
 ++##"((*** $*Z22EEF "##$;SA{{,,0A0A25H3NOUXX mmCt{{/N/N$OPPP	"	 ##sJ   #DA AA DA #C>&BC9)C,*
C94D9C>>Dc                V  K   d| j                   j                   d| j                   j                   | j                   j                   }| j                  j                  d|       t        j                  || j                   j                  | j                   j                         d{   | _
        | j                   }d| _        |r| j                  d       d{    | j                  j                  d       t        j                  | j                               | _        y7 |7 Jw)zPerform a single connection attempt.

        Spec: Section 3.1 - Connection
        Endpoint: ws://<robot-ip>:8080/edge (defaults; configurable)
        zws://:zConnecting to Edge Proxy at %s)r6   r7   NTzConnected to Edge Proxy)r<   r-   r/   r0   r?   ry   r   rr   r6   r7   r@   rA   rx   ru   create_task_receive_messagesrL   )rU   uriwas_not_connecteds      r$   r   zEdgeProxyClient._connect_once  s      dkk&&'q)9)9(:4;;;N;N:OP93?#++++3311
 
 !%/0066623 &11$2H2H2JK
 7s%   B&D)(D%)3D)D'A	D)'D)c                <  K   | j                   sy	 | j                   2 3 d{   }| j                  |       d{    #7 7 6 n# t        j                  j                  $ rG}| j
                  j                  d|       d| _        | j                  d       d{  7   Y d}~n5d}~wt        $ r&}| j
                  j                  d|       Y d}~nd}~ww xY wd| _        | j                  d       d{  7   | j                  j                  r'| j                  s| j                          d{  7   yyy# d| _        | j                  d       d{  7   | j                  j                  r'| j                  s| j                          d{  7   w w w xY ww)zfReceive and handle incoming messages.

        Runs in background until connection is closed.
        NzConnection closed: %sFzError receiving message: %s)r@   _handle_messager   r   ConnectionClosedr?   r   rA   rx   	Exceptionerrorr<   r2   rB   rq   )rU   messager   s      r$   r   z!EdgeProxyClient._receive_messages  sY    
 xx	1!% 4 4g**733343 "*$$55 	8KK 7=#DO00777 	BKK;SAA	B $DO00777 {{$$T->->..000 .?$	 $DO00777 {{$$T->->..000 .?$s   FA AA AA AA  AA A D9 C#7B%BB% D9 %C1CD9 CD9 F5C86:F0D31F9FE:FFFFc                :  K   t        |t        t        f      r	 |j                  dd      }t        |t              s&| j
                  j                  dt        |             y	 t        j                  |      }t        |t              s| j
                  j                  d       y	 t        |      }|,| j
                  j                  d
|j!                  d             y| j#                  |       d{    y# t        $ r&}| j
                  j                  d|       Y d}~yd}~ww xY w# t        j                  $ r&}| j
                  j                  d|       Y d}~yd}~ww xY w# t        $ r&}| j
                  j                  d	|       Y d}~yd}~ww xY w7 w)zdHandle an incoming message.

        Args:
            message: Raw message (bytes or str).
        zutf-8replace)errorszFailed to decode message: %sNzUnsupported message type: %szInvalid JSON message: %szMessage payload is not a dictzFailed to parse message: %szUnknown message type: %sr   )
isinstancebytes	bytearraydecoder   r?   r   r,   r   jsonloadsJSONDecodeErrordictr   
ValueErrordebugget_dispatch_message)rU   r   r   datamsgs        r$   r   zEdgeProxyClient._handle_message  s]     gy12!...C
 '3'KK<d7mL	::g&D
 $%KK=>	$T*C
 ;KK8$((6:JK$$S)))=  !!"@#F ## 	KK8#>	  	KK;SA	 	*s   FC9 6F$D+ 9,F&E' 1AF3F4F9	D(D#F#D((F+E$>EFE$$F'	F0FFFFc                  K   t        |t              r%| j                  | j                  |       d{    yt        |t              r,|| _        | j                  | j                  |       d{    yt        |t              r%| j                  | j                  |       d{    yt        |t              r%| j                  | j                  |       d{    yt        |t              r%| j                  | j                  |       d{    yt        |t              rn| j                  j                  |j                   d      }|!|j#                         s|j%                  |       | j                  | j&                  |       d{    yt        |t(              r,|| _        | j                  | j,                  |       d{    yt        |t.              r|j0                  r|j0                  j3                         nd}|r|| j4                  v ry| j4                  j7                  |       | j8                  j;                  |       t=        | j8                        | j>                  kD  r6| j8                  j                  d      }| j4                  jA                  |       || _!        | j                  | jD                  |       d{    yy7 7 f7 47 7 7 U7 7 w)zhDispatch message to registered handlers.

        Args:
            msg: Parsed message object.
        N r   )#r   r   _call_handlersrC   r   rM   rD   r   rE   r   rF   r   rG   r   rT   popr   done
set_resultrH   r   rN   rI   r   event_idstriprQ   addrR   rX   lenrS   discardrO   rJ   )rU   r   futurer   oldests        r$   r   z!EdgeProxyClient._dispatch_messageI  sa     c+,%%d&?&?EEEZ($'D!%%d&@&@#FFF01%%d&B&BCHHH\*%%d&:&:C@@@[)%%d&9&93???12))--cnndCF!&++-!!#&%%d&C&CSIII01&)D#%%d&B&BCHHH_-/2||s||))+Ht333$$((2**11(;t112T5O5OO!77;;A>F((008"%D%%d&>&>DDD .5 F G I A @ J I Es   0K(K;K(.K/4K(#K$4K(K4K(KA=K(K ;K(K#DK(
K&K(K(K(K(K( K(#K(&K(c                   K   |D ]*  }	  ||      }t        j                  |      r
| d{    , y7 # t        $ r1}| j                  j	                  d|j
                  |       Y d}~ed}~ww xY ww)zCall all handlers for a message type.

        Args:
            handlers: List of handler callables.
            msg: Message to pass to handlers.
        NzHandler %s failed: %s)ru   iscoroutiner   r?   r   r   )rU   handlersr   rZ   resultr   s         r$   r   zEdgeProxyClient._call_handlersw  ss        	G &&v. LL		 ! !!+W-=-=s s6   A2"535A25	A/'A*%A2*A//A2c                @  K   | j                   r| j                  st        d      	 t        j                  |      }| j                  j                  |       d{    y7 # t        $ r0}| j                  j                  d|       t        d|       |d}~ww xY ww)zSend a message to the Edge Proxy.

        Args:
            data: Message dictionary to send as JSON.

        Raises:
            EdgeProxyConnectionError: If not connected.
        zNot connected to Edge ProxyNzFailed to send message: %szFailed to send message: )	ro   r@   r&   r   dumpssendr   r?   r   )rU   r   r   r   s       r$   r~   zEdgeProxyClient._send_message  s        *+HII	Vjj&G((--((( 	VKK:C@*-EcU+KLRUU	Vs:   $B3A" A A" B A" "	B++BBBc                   K   | j                   D ]*  }	  ||      }t        j                  |      r
| d{    , y7 # t        $ r&}| j                  j                  d|       Y d}~Zd}~ww xY ww)zkNotify all connection change handlers.

        Args:
            connected: New connection state.
        Nz$Connection change handler failed: %s)rK   ru   r   r   r?   r   )rU   	connectedrZ   r   r   s        r$   rx   z)EdgeProxyClient._notify_connection_change  sr      77 	G +&&v. LL		 ! !!:C s7   A1"?=?A1?	A.A)$A1)A..A1)NN)r<   zOptional[ClientConfig]r?   zOptional[logging.Logger]returnNone)rZ   NavStatusHandlerr   r   )rZ   RobotStateHandlerr   r   )rZ   WaypointListHandlerr   r   )rZ   ErrorHandlerr   r   )rZ   PongHandlerr   r   )rZ   FrameResponseHandlerr   r   )rZ   FRDetectionsHandlerr   r   )rZ   EventLogHandlerr   r   )rZ   ConnectionChangeHandlerr   r   )r   r1   )r   r   )r   normal)r   r,   r   r,   r   r,   r   r   )g        r   r   )r   r3   r   r3   r   r3   r   r,   r   r,   r   r   )r   slow)
r   r,   r   r3   r   r,   r   r,   r   r   )r   r   )r   r,   r   r,   r   r   )r   )r   r,   r   r   )r   r   r   r   )r   r   r   r   )r   z	List[Any]r   r   r   r   )r   zDict[str, Any]r   r   )r   r1   r   r   )!r   r   r    r!   rV   r[   r]   r_   ra   rc   re   rg   ri   rk   propertyro   rr   r{   r   r   r   r   r   r   r   rq   r   r   r   r   r   r~   rx   r"   r#   r$   r:   r:   Z   s   & *.+//=&/= )/= 
	/=b"

 P P
)9B 	00 0 	0
 
02 00 0 	0
 0 0 
0: 00 0 	0
 0 
02 00 0 
	0.0 
000"*L412(*T,E\"V&r#   r:   )1r!   
__future__r   ru   r   r=   dataclassesr   typingr   r   r   r   r	   r   websockets.clientr
   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r1   r   r   r   r&   r(   r*   r:   r"   r#   r$   <module>r      s4  	 #    ! 6 6  5    ( -.34 j\3./  34c9: +,}c)*!5 6 ;<  34c9: O,c12"D63;/ 	9 		3 		' 	   "O	 O	r#   