robolib.netcode
netcode.py - Defines the recv/encode/decode functions to interact with the msgpack packets in robosim
Import level: 2
1""" 2netcode.py - Defines the recv/encode/decode functions to interact with the msgpack packets in robosim 3 4Import level: 2 5""" 6from typing import Any 7from dataclasses import dataclass 8from enum import StrEnum 9import socket 10 11from overrides import overrides 12from micronetcode import Codec, Client, Message, SourceType 13from microecs import EntityId 14import msgpack 15 16from robolib.utils import logger 17 18_SERIALIZABLE_TYPES = (str, int, float, bool, bytes, type(None)) 19PACKET_MAXLEN = 100_000 20 21class RobosimClientState(StrEnum): 22 """The states in which a client can be""" 23 STAGED = "staged" 24 CONNECTED = "connected" 25 26@dataclass 27class RobosimClient(Client): 28 """The robosim client: a regular micronetcode client + robosim client state""" 29 state: RobosimClientState = "staged" 30 robot_eid: EntityId | None = None 31 32class MsgpackCodec(Codec): 33 """Msgpack implementation for recv/encode and decode.""" 34 def __init__(self, packet_maxlen: int | None = None): 35 self.packet_maxlen = packet_maxlen 36 37 @overrides 38 def recv(self, sock: socket.socket) -> bytes | None: 39 # First, read the packet length based on the first 4 bytes. Then conver this to an integer and read n bytes. 40 packet_len_in_bytes = sock.recv(4) 41 if len(packet_len_in_bytes) == 0: 42 return None 43 if len(packet_len_in_bytes) < 4: 44 logger.error(f"The unexpected happened: {sock}. Closing connection. TO FIX") 45 return None 46 47 try: 48 packet_len = int.from_bytes(packet_len_in_bytes, "big") 49 except Exception as e: 50 logger.error(e) 51 return None 52 53 if packet_len > PACKET_MAXLEN: 54 logger.debug(f"Client: {sock} sent a packet of {packet_len} > {PACKET_MAXLEN}. Closing connection") 55 return None 56 57 buf = bytearray(packet_len) 58 view = memoryview(buf) 59 received = 0 60 while received < packet_len: 61 chunk = sock.recv(packet_len - received) 62 if len(chunk) == 0: 63 return None 64 view[received: received + len(chunk)] = chunk 65 received += len(chunk) 66 return bytes(buf) 67 68 @overrides 69 def decode(self, data: bytes) -> list[Message]: 70 packet: dict = msgpack.unpackb(data, raw=False) 71 if not isinstance(packet, dict): 72 raise TypeError(f"packet is not dictionary: {type(packet)}") 73 return [Message(source_type=SourceType.SOCKET, data=packet)] 74 75 def encode(self, resp: Any) -> bytes: 76 if not isinstance(resp, dict): 77 raise TypeError(f"resp is not dictionary: {type(resp)}") 78 79 try: 80 packed_data = msgpack.packb(resp) 81 except TypeError as e: 82 _find_bad(resp) 83 raise e 84 85 # <SIZE-4b><packed_data>. We should look into avoiding this concatenation at each encode(). 86 return len(packed_data).to_bytes(4, "big") + packed_data 87 88def _find_bad(d: Any, path: str="root"): 89 """ran in case of a TypeError in send_packet, so we know which part of the dict is not serializable""" 90 if isinstance(d, dict): 91 for k, v in d.items(): 92 _find_bad(v, f"{path}[{k!r}]") # !r calls repr() 93 elif isinstance(d, (list, tuple)): 94 for i, v in enumerate(d): 95 _find_bad(v, f"{path}[{i}]") 96 elif not isinstance(d, _SERIALIZABLE_TYPES): 97 logger.error(f"Unserializable at {path}: {type(d).__name__}") 98 # the 'else' is implied because only 'good' types should be left here
PACKET_MAXLEN =
100000
class
RobosimClientState(enum.StrEnum):
22class RobosimClientState(StrEnum): 23 """The states in which a client can be""" 24 STAGED = "staged" 25 CONNECTED = "connected"
The states in which a client can be
STAGED =
<RobosimClientState.STAGED: 'staged'>
CONNECTED =
<RobosimClientState.CONNECTED: 'connected'>
@dataclass
class
RobosimClient27@dataclass 28class RobosimClient(Client): 29 """The robosim client: a regular micronetcode client + robosim client state""" 30 state: RobosimClientState = "staged" 31 robot_eid: EntityId | None = None
The robosim client: a regular micronetcode client + robosim client state
RobosimClient( socket: socket.socket, channel: micronetcode.channel.Channel, state: RobosimClientState = 'staged', robot_eid: int | None = None)
class
MsgpackCodec(micronetcode.codec.Codec):
33class MsgpackCodec(Codec): 34 """Msgpack implementation for recv/encode and decode.""" 35 def __init__(self, packet_maxlen: int | None = None): 36 self.packet_maxlen = packet_maxlen 37 38 @overrides 39 def recv(self, sock: socket.socket) -> bytes | None: 40 # First, read the packet length based on the first 4 bytes. Then conver this to an integer and read n bytes. 41 packet_len_in_bytes = sock.recv(4) 42 if len(packet_len_in_bytes) == 0: 43 return None 44 if len(packet_len_in_bytes) < 4: 45 logger.error(f"The unexpected happened: {sock}. Closing connection. TO FIX") 46 return None 47 48 try: 49 packet_len = int.from_bytes(packet_len_in_bytes, "big") 50 except Exception as e: 51 logger.error(e) 52 return None 53 54 if packet_len > PACKET_MAXLEN: 55 logger.debug(f"Client: {sock} sent a packet of {packet_len} > {PACKET_MAXLEN}. Closing connection") 56 return None 57 58 buf = bytearray(packet_len) 59 view = memoryview(buf) 60 received = 0 61 while received < packet_len: 62 chunk = sock.recv(packet_len - received) 63 if len(chunk) == 0: 64 return None 65 view[received: received + len(chunk)] = chunk 66 received += len(chunk) 67 return bytes(buf) 68 69 @overrides 70 def decode(self, data: bytes) -> list[Message]: 71 packet: dict = msgpack.unpackb(data, raw=False) 72 if not isinstance(packet, dict): 73 raise TypeError(f"packet is not dictionary: {type(packet)}") 74 return [Message(source_type=SourceType.SOCKET, data=packet)] 75 76 def encode(self, resp: Any) -> bytes: 77 if not isinstance(resp, dict): 78 raise TypeError(f"resp is not dictionary: {type(resp)}") 79 80 try: 81 packed_data = msgpack.packb(resp) 82 except TypeError as e: 83 _find_bad(resp) 84 raise e 85 86 # <SIZE-4b><packed_data>. We should look into avoiding this concatenation at each encode(). 87 return len(packed_data).to_bytes(4, "big") + packed_data
Msgpack implementation for recv/encode and decode.
@overrides
def
recv(self, sock: socket.socket) -> bytes | None:
38 @overrides 39 def recv(self, sock: socket.socket) -> bytes | None: 40 # First, read the packet length based on the first 4 bytes. Then conver this to an integer and read n bytes. 41 packet_len_in_bytes = sock.recv(4) 42 if len(packet_len_in_bytes) == 0: 43 return None 44 if len(packet_len_in_bytes) < 4: 45 logger.error(f"The unexpected happened: {sock}. Closing connection. TO FIX") 46 return None 47 48 try: 49 packet_len = int.from_bytes(packet_len_in_bytes, "big") 50 except Exception as e: 51 logger.error(e) 52 return None 53 54 if packet_len > PACKET_MAXLEN: 55 logger.debug(f"Client: {sock} sent a packet of {packet_len} > {PACKET_MAXLEN}. Closing connection") 56 return None 57 58 buf = bytearray(packet_len) 59 view = memoryview(buf) 60 received = 0 61 while received < packet_len: 62 chunk = sock.recv(packet_len - received) 63 if len(chunk) == 0: 64 return None 65 view[received: received + len(chunk)] = chunk 66 received += len(chunk) 67 return bytes(buf)
Receive raw bytes from the wire (socket). Different protocols may have different ways to recv (len+data). Returns None if the socket closed the connection. This is used to re-use the client thread for new connections.
@overrides
def
decode(self, data: bytes) -> list[micronetcode.message.Message]:
69 @overrides 70 def decode(self, data: bytes) -> list[Message]: 71 packet: dict = msgpack.unpackb(data, raw=False) 72 if not isinstance(packet, dict): 73 raise TypeError(f"packet is not dictionary: {type(packet)}") 74 return [Message(source_type=SourceType.SOCKET, data=packet)]
Converts raw bytes data fro recv(sock) into a list of messages for the main app
def
encode(self, resp: Any) -> bytes:
76 def encode(self, resp: Any) -> bytes: 77 if not isinstance(resp, dict): 78 raise TypeError(f"resp is not dictionary: {type(resp)}") 79 80 try: 81 packed_data = msgpack.packb(resp) 82 except TypeError as e: 83 _find_bad(resp) 84 raise e 85 86 # <SIZE-4b><packed_data>. We should look into avoiding this concatenation at each encode(). 87 return len(packed_data).to_bytes(4, "big") + packed_data
Encodes the response of the main app back into bytes to be sent via the socket used at recv()