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 RobosimClient(micronetcode.client.Client):
27@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)
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.

MsgpackCodec(packet_maxlen: int | None = None)
35    def __init__(self, packet_maxlen: int | None = None):
36        self.packet_maxlen = packet_maxlen
packet_maxlen
@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()