robosim.network.connection_manager
connection_manager.py - Handles the connection protocol and handshakes of all clients talking to the simulator
1"""connection_manager.py - Handles the connection protocol and handshakes of all clients talking to the simulator""" 2from typing import Callable 3from queue import Empty 4import atexit 5import traceback 6import threading 7import socket 8 9from robosim.utils import logger 10from robosim.constants import SOCKET_TIMEOUT_S 11from .msgpack_utils import send_packet, recv_packet 12from .channel import ObjToSimChannel 13 14NetworkHandlerType = Callable[[dict, ObjToSimChannel, ObjToSimChannel | None], dict | None] 15 16class ConnectionManager: 17 """connection class for managing connections and callbacks to the app. App defines the rest of the protocol""" 18 def __init__(self, host: str, port: int, n_channels: int, connect_command: str): 19 self.host = host 20 self.port = port 21 self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 22 self.connect_command = connect_command 23 24 self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 25 self.server.bind((host, port)) 26 self.server.listen(1) 27 self.server.settimeout(SOCKET_TIMEOUT_S) 28 atexit.register(lambda: self.server.close()) 29 30 self.channels = [ObjToSimChannel(index=i) for i in range (n_channels)] # used to communicate with robots. 31 self.global_channel = ObjToSimChannel(index=-1) # used to communicate with the simulator, not with a robot. 32 self.tcp_thread: threading.Thread | None = None # created in start() 33 34 def start(self, network_handler: NetworkHandlerType): 35 """Starts the main thread that listens to new connections""" 36 args = (self.server, self.channels, network_handler, self.global_channel) 37 self.tcp_thread = threading.Thread(target=self._tcp_listener, args=args, daemon=True) 38 self.tcp_thread.start() 39 logger.info(f"Started TCP server on port {self.port}") 40 41 def _tcp_listener(self, server: socket.socket, channels: list[ObjToSimChannel], network_handler: NetworkHandlerType, 42 global_channel: ObjToSimChannel): 43 """the thread that listens to the standard input for commands""" 44 while True: # server runs forever waiting for new connection 45 try: 46 client, addr = server.accept() 47 logger.info(f"Connected: '{addr[0]}:{addr[1]}'") 48 args = (client, addr, channels, network_handler, global_channel) 49 threading.Thread(target=self._handle_client, args=args, daemon=True).start() 50 except socket.timeout: 51 pass 52 except Exception as e: 53 logger.error(f"Error: {e}\n{''.join(traceback.format_exception(e))}") 54 55 def _get_first_free_channel(self, client: socket.socket, channels: list[ObjToSimChannel]) -> int: 56 # find first free channel to assign this client the robot 57 for i, channel in enumerate(channels): 58 with channel.lock: 59 if channel.client is None: 60 channel.client = client 61 return i 62 return -1 63 64 def _handle_stateless_client(self, client: socket.socket, addr: tuple[str, int], 65 network_handler: NetworkHandlerType, 66 global_channel: ObjToSimChannel) -> bool: 67 """We keep the TCP connection on, so the same socket can be re-used, but they are not supposed to hold any app 68 level state. Returns true if the client needs to be closed, false if we continue (connected)""" 69 try: 70 while True: 71 msg = recv_packet(client) 72 if "cmd" not in msg: 73 send_packet(client, {"error": "'cmd' not in message"}, close_connection=True) 74 return True 75 76 if msg["cmd"] == self.connect_command: 77 # Note: this message will be replied to in _handle_client 78 return False 79 80 res = network_handler(msg, global_channel, connected_channel=None) 81 # network_handler returned none -> message was not handled (needed connection) -> close 82 if res is None: 83 return True 84 else: 85 send_packet(client, res) 86 except ConnectionError: 87 logger.info(f"Disconnected: '{addr[0]}:{addr[1]}'") 88 except Exception as e: 89 logger.error(f"Error: {e}\n{''.join(traceback.format_exception(e))}") 90 return True 91 92 def _handle_client(self, client: socket.socket, addr: tuple, channels: list[ObjToSimChannel], 93 network_handler: NetworkHandlerType, global_channel: ObjToSimChannel): 94 # A client is handled in two steps: either it's a stateles connection or a robot connection. In the second case, 95 # the client asks for robot control ('connect') in which we first check if the robot is already controlled 96 # or keep it long term. Further communication with the client follows further allows stateless messages too. 97 should_close = self._handle_stateless_client(client, addr, network_handler, global_channel) 98 if should_close: 99 send_packet(client, {"error": "Must connect first"}, close_connection=True) 100 return 101 102 if (channel_ix := self._get_first_free_channel(client, channels)) == -1: 103 send_packet(client, {"error": f"All {len(channels)} robots are already controlled"}, 104 close_connection=True) 105 return 106 107 # if we reach here, the user is connected and we keep this thread alive 108 send_packet(client, {"status": "connected", "id": channel_ix}) 109 channel = channels[channel_ix] 110 111 try: 112 while True: 113 msg = recv_packet(client) 114 logger.log_every_s(f"Received: {msg}", "DEBUG", log_to_next_level=True) 115 # two-tier message handling: fast handler -> simulator queue via the channel (slowest) 116 # we past the message to the network handler which will route it properly (fast, global or connected) 117 if "cmd" not in msg: 118 res = {"error": "'cmd' not in message"} 119 elif msg["cmd"] == self.connect_command: 120 res = {"error": "robot already controlled"} 121 else: 122 res = network_handler(msg, global_channel, connected_channel=channel) 123 send_packet(client, res) 124 except Empty as e: 125 logger.error(f"Timeout waiting for simulator response: {e}\n{''.join(traceback.format_exception(e))}") 126 raise e 127 except ConnectionError: 128 pass 129 except Exception as e: 130 logger.error(f"Error: {e}\n{''.join(traceback.format_exception(e))}") 131 if "ndarray" in str(e): 132 logger.error(f"input: {res}") 133 finally: 134 logger.info(f"Disconnected: '{addr[0]}:{addr[1]}'") 135 with channel.lock: 136 channel.client = None 137 channel.clear() 138 139 def __repr__(self): 140 return f"[Connection Manager]\n- Host: {self.host}:{self.port}\n- Channels: {len(self. channels)}"
NetworkHandlerType =
typing.Callable[[dict, robosim.network.ObjToSimChannel, robosim.network.ObjToSimChannel | None], dict | None]
class
ConnectionManager:
17class ConnectionManager: 18 """connection class for managing connections and callbacks to the app. App defines the rest of the protocol""" 19 def __init__(self, host: str, port: int, n_channels: int, connect_command: str): 20 self.host = host 21 self.port = port 22 self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 23 self.connect_command = connect_command 24 25 self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 26 self.server.bind((host, port)) 27 self.server.listen(1) 28 self.server.settimeout(SOCKET_TIMEOUT_S) 29 atexit.register(lambda: self.server.close()) 30 31 self.channels = [ObjToSimChannel(index=i) for i in range (n_channels)] # used to communicate with robots. 32 self.global_channel = ObjToSimChannel(index=-1) # used to communicate with the simulator, not with a robot. 33 self.tcp_thread: threading.Thread | None = None # created in start() 34 35 def start(self, network_handler: NetworkHandlerType): 36 """Starts the main thread that listens to new connections""" 37 args = (self.server, self.channels, network_handler, self.global_channel) 38 self.tcp_thread = threading.Thread(target=self._tcp_listener, args=args, daemon=True) 39 self.tcp_thread.start() 40 logger.info(f"Started TCP server on port {self.port}") 41 42 def _tcp_listener(self, server: socket.socket, channels: list[ObjToSimChannel], network_handler: NetworkHandlerType, 43 global_channel: ObjToSimChannel): 44 """the thread that listens to the standard input for commands""" 45 while True: # server runs forever waiting for new connection 46 try: 47 client, addr = server.accept() 48 logger.info(f"Connected: '{addr[0]}:{addr[1]}'") 49 args = (client, addr, channels, network_handler, global_channel) 50 threading.Thread(target=self._handle_client, args=args, daemon=True).start() 51 except socket.timeout: 52 pass 53 except Exception as e: 54 logger.error(f"Error: {e}\n{''.join(traceback.format_exception(e))}") 55 56 def _get_first_free_channel(self, client: socket.socket, channels: list[ObjToSimChannel]) -> int: 57 # find first free channel to assign this client the robot 58 for i, channel in enumerate(channels): 59 with channel.lock: 60 if channel.client is None: 61 channel.client = client 62 return i 63 return -1 64 65 def _handle_stateless_client(self, client: socket.socket, addr: tuple[str, int], 66 network_handler: NetworkHandlerType, 67 global_channel: ObjToSimChannel) -> bool: 68 """We keep the TCP connection on, so the same socket can be re-used, but they are not supposed to hold any app 69 level state. Returns true if the client needs to be closed, false if we continue (connected)""" 70 try: 71 while True: 72 msg = recv_packet(client) 73 if "cmd" not in msg: 74 send_packet(client, {"error": "'cmd' not in message"}, close_connection=True) 75 return True 76 77 if msg["cmd"] == self.connect_command: 78 # Note: this message will be replied to in _handle_client 79 return False 80 81 res = network_handler(msg, global_channel, connected_channel=None) 82 # network_handler returned none -> message was not handled (needed connection) -> close 83 if res is None: 84 return True 85 else: 86 send_packet(client, res) 87 except ConnectionError: 88 logger.info(f"Disconnected: '{addr[0]}:{addr[1]}'") 89 except Exception as e: 90 logger.error(f"Error: {e}\n{''.join(traceback.format_exception(e))}") 91 return True 92 93 def _handle_client(self, client: socket.socket, addr: tuple, channels: list[ObjToSimChannel], 94 network_handler: NetworkHandlerType, global_channel: ObjToSimChannel): 95 # A client is handled in two steps: either it's a stateles connection or a robot connection. In the second case, 96 # the client asks for robot control ('connect') in which we first check if the robot is already controlled 97 # or keep it long term. Further communication with the client follows further allows stateless messages too. 98 should_close = self._handle_stateless_client(client, addr, network_handler, global_channel) 99 if should_close: 100 send_packet(client, {"error": "Must connect first"}, close_connection=True) 101 return 102 103 if (channel_ix := self._get_first_free_channel(client, channels)) == -1: 104 send_packet(client, {"error": f"All {len(channels)} robots are already controlled"}, 105 close_connection=True) 106 return 107 108 # if we reach here, the user is connected and we keep this thread alive 109 send_packet(client, {"status": "connected", "id": channel_ix}) 110 channel = channels[channel_ix] 111 112 try: 113 while True: 114 msg = recv_packet(client) 115 logger.log_every_s(f"Received: {msg}", "DEBUG", log_to_next_level=True) 116 # two-tier message handling: fast handler -> simulator queue via the channel (slowest) 117 # we past the message to the network handler which will route it properly (fast, global or connected) 118 if "cmd" not in msg: 119 res = {"error": "'cmd' not in message"} 120 elif msg["cmd"] == self.connect_command: 121 res = {"error": "robot already controlled"} 122 else: 123 res = network_handler(msg, global_channel, connected_channel=channel) 124 send_packet(client, res) 125 except Empty as e: 126 logger.error(f"Timeout waiting for simulator response: {e}\n{''.join(traceback.format_exception(e))}") 127 raise e 128 except ConnectionError: 129 pass 130 except Exception as e: 131 logger.error(f"Error: {e}\n{''.join(traceback.format_exception(e))}") 132 if "ndarray" in str(e): 133 logger.error(f"input: {res}") 134 finally: 135 logger.info(f"Disconnected: '{addr[0]}:{addr[1]}'") 136 with channel.lock: 137 channel.client = None 138 channel.clear() 139 140 def __repr__(self): 141 return f"[Connection Manager]\n- Host: {self.host}:{self.port}\n- Channels: {len(self. channels)}"
connection class for managing connections and callbacks to the app. App defines the rest of the protocol
ConnectionManager(host: str, port: int, n_channels: int, connect_command: str)
19 def __init__(self, host: str, port: int, n_channels: int, connect_command: str): 20 self.host = host 21 self.port = port 22 self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 23 self.connect_command = connect_command 24 25 self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 26 self.server.bind((host, port)) 27 self.server.listen(1) 28 self.server.settimeout(SOCKET_TIMEOUT_S) 29 atexit.register(lambda: self.server.close()) 30 31 self.channels = [ObjToSimChannel(index=i) for i in range (n_channels)] # used to communicate with robots. 32 self.global_channel = ObjToSimChannel(index=-1) # used to communicate with the simulator, not with a robot. 33 self.tcp_thread: threading.Thread | None = None # created in start()
def
start( self, network_handler: Callable[[dict, robosim.network.ObjToSimChannel, robosim.network.ObjToSimChannel | None], dict | None]):
35 def start(self, network_handler: NetworkHandlerType): 36 """Starts the main thread that listens to new connections""" 37 args = (self.server, self.channels, network_handler, self.global_channel) 38 self.tcp_thread = threading.Thread(target=self._tcp_listener, args=args, daemon=True) 39 self.tcp_thread.start() 40 logger.info(f"Started TCP server on port {self.port}")
Starts the main thread that listens to new connections