cubesat-space-protocol-py 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
csp_py/__init__.py ADDED
@@ -0,0 +1,18 @@
1
+ from .packet import CspId, CspPacket, CspPacketPriority, CspPacketFlags
2
+ from .router import CspRouter
3
+ from .node import CspNode
4
+ from .packet_handler import IPacketHandler
5
+
6
+ from .services.ping_client import ping
7
+
8
+
9
+ __all__ = [
10
+ 'CspId',
11
+ 'CspPacket',
12
+ 'CspPacketPriority',
13
+ 'CspPacketFlags',
14
+ 'CspRouter',
15
+ 'CspNode',
16
+ 'IPacketHandler',
17
+ 'ping',
18
+ ]
csp_py/crc32.py ADDED
@@ -0,0 +1,78 @@
1
+ import struct
2
+ from csp_py.packet import CspPacket
3
+ from .router import CspRouter
4
+
5
+
6
+ CRC_TABLE = [
7
+ 0x00000000, 0xF26B8303, 0xE13B70F7, 0x1350F3F4, 0xC79A971F, 0x35F1141C, 0x26A1E7E8, 0xD4CA64EB,
8
+ 0x8AD958CF, 0x78B2DBCC, 0x6BE22838, 0x9989AB3B, 0x4D43CFD0, 0xBF284CD3, 0xAC78BF27, 0x5E133C24,
9
+ 0x105EC76F, 0xE235446C, 0xF165B798, 0x030E349B, 0xD7C45070, 0x25AFD373, 0x36FF2087, 0xC494A384,
10
+ 0x9A879FA0, 0x68EC1CA3, 0x7BBCEF57, 0x89D76C54, 0x5D1D08BF, 0xAF768BBC, 0xBC267848, 0x4E4DFB4B,
11
+ 0x20BD8EDE, 0xD2D60DDD, 0xC186FE29, 0x33ED7D2A, 0xE72719C1, 0x154C9AC2, 0x061C6936, 0xF477EA35,
12
+ 0xAA64D611, 0x580F5512, 0x4B5FA6E6, 0xB93425E5, 0x6DFE410E, 0x9F95C20D, 0x8CC531F9, 0x7EAEB2FA,
13
+ 0x30E349B1, 0xC288CAB2, 0xD1D83946, 0x23B3BA45, 0xF779DEAE, 0x05125DAD, 0x1642AE59, 0xE4292D5A,
14
+ 0xBA3A117E, 0x4851927D, 0x5B016189, 0xA96AE28A, 0x7DA08661, 0x8FCB0562, 0x9C9BF696, 0x6EF07595,
15
+ 0x417B1DBC, 0xB3109EBF, 0xA0406D4B, 0x522BEE48, 0x86E18AA3, 0x748A09A0, 0x67DAFA54, 0x95B17957,
16
+ 0xCBA24573, 0x39C9C670, 0x2A993584, 0xD8F2B687, 0x0C38D26C, 0xFE53516F, 0xED03A29B, 0x1F682198,
17
+ 0x5125DAD3, 0xA34E59D0, 0xB01EAA24, 0x42752927, 0x96BF4DCC, 0x64D4CECF, 0x77843D3B, 0x85EFBE38,
18
+ 0xDBFC821C, 0x2997011F, 0x3AC7F2EB, 0xC8AC71E8, 0x1C661503, 0xEE0D9600, 0xFD5D65F4, 0x0F36E6F7,
19
+ 0x61C69362, 0x93AD1061, 0x80FDE395, 0x72966096, 0xA65C047D, 0x5437877E, 0x4767748A, 0xB50CF789,
20
+ 0xEB1FCBAD, 0x197448AE, 0x0A24BB5A, 0xF84F3859, 0x2C855CB2, 0xDEEEDFB1, 0xCDBE2C45, 0x3FD5AF46,
21
+ 0x7198540D, 0x83F3D70E, 0x90A324FA, 0x62C8A7F9, 0xB602C312, 0x44694011, 0x5739B3E5, 0xA55230E6,
22
+ 0xFB410CC2, 0x092A8FC1, 0x1A7A7C35, 0xE811FF36, 0x3CDB9BDD, 0xCEB018DE, 0xDDE0EB2A, 0x2F8B6829,
23
+ 0x82F63B78, 0x709DB87B, 0x63CD4B8F, 0x91A6C88C, 0x456CAC67, 0xB7072F64, 0xA457DC90, 0x563C5F93,
24
+ 0x082F63B7, 0xFA44E0B4, 0xE9141340, 0x1B7F9043, 0xCFB5F4A8, 0x3DDE77AB, 0x2E8E845F, 0xDCE5075C,
25
+ 0x92A8FC17, 0x60C37F14, 0x73938CE0, 0x81F80FE3, 0x55326B08, 0xA759E80B, 0xB4091BFF, 0x466298FC,
26
+ 0x1871A4D8, 0xEA1A27DB, 0xF94AD42F, 0x0B21572C, 0xDFEB33C7, 0x2D80B0C4, 0x3ED04330, 0xCCBBC033,
27
+ 0xA24BB5A6, 0x502036A5, 0x4370C551, 0xB11B4652, 0x65D122B9, 0x97BAA1BA, 0x84EA524E, 0x7681D14D,
28
+ 0x2892ED69, 0xDAF96E6A, 0xC9A99D9E, 0x3BC21E9D, 0xEF087A76, 0x1D63F975, 0x0E330A81, 0xFC588982,
29
+ 0xB21572C9, 0x407EF1CA, 0x532E023E, 0xA145813D, 0x758FE5D6, 0x87E466D5, 0x94B49521, 0x66DF1622,
30
+ 0x38CC2A06, 0xCAA7A905, 0xD9F75AF1, 0x2B9CD9F2, 0xFF56BD19, 0x0D3D3E1A, 0x1E6DCDEE, 0xEC064EED,
31
+ 0xC38D26C4, 0x31E6A5C7, 0x22B65633, 0xD0DDD530, 0x0417B1DB, 0xF67C32D8, 0xE52CC12C, 0x1747422F,
32
+ 0x49547E0B, 0xBB3FFD08, 0xA86F0EFC, 0x5A048DFF, 0x8ECEE914, 0x7CA56A17, 0x6FF599E3, 0x9D9E1AE0,
33
+ 0xD3D3E1AB, 0x21B862A8, 0x32E8915C, 0xC083125F, 0x144976B4, 0xE622F5B7, 0xF5720643, 0x07198540,
34
+ 0x590AB964, 0xAB613A67, 0xB831C993, 0x4A5A4A90, 0x9E902E7B, 0x6CFBAD78, 0x7FAB5E8C, 0x8DC0DD8F,
35
+ 0xE330A81A, 0x115B2B19, 0x020BD8ED, 0xF0605BEE, 0x24AA3F05, 0xD6C1BC06, 0xC5914FF2, 0x37FACCF1,
36
+ 0x69E9F0D5, 0x9B8273D6, 0x88D28022, 0x7AB90321, 0xAE7367CA, 0x5C18E4C9, 0x4F48173D, 0xBD23943E,
37
+ 0xF36E6F75, 0x0105EC76, 0x12551F82, 0xE03E9C81, 0x34F4F86A, 0xC69F7B69, 0xD5CF889D, 0x27A40B9E,
38
+ 0x79B737BA, 0x8BDCB4B9, 0x988C474D, 0x6AE7C44E, 0xBE2DA0A5, 0x4C4623A6, 0x5F16D052, 0xAD7D5351,
39
+ ]
40
+
41
+
42
+ def calculate_crc32(data: bytes) -> int:
43
+ crc = 0xFFFF_FFFF
44
+ for b in data:
45
+ crc = CRC_TABLE[(crc ^ b) & 0xFF] ^ (crc >> 8)
46
+
47
+ return crc ^ 0xFFFF_FFFF
48
+
49
+
50
+ def register_crc32_filters(router: CspRouter) -> None:
51
+ async def incoming_filter(packet: CspPacket) -> CspPacket | None:
52
+ if (packet.packet_id.flags & 1) == 0:
53
+ return packet
54
+
55
+ payload = packet.data[:-4]
56
+
57
+ if len(payload) < 4:
58
+ return None
59
+
60
+ received_checksum = struct.unpack('!I', packet.data[-4:])[0]
61
+ calculated_checksum = calculate_crc32(payload)
62
+
63
+ if received_checksum != calculated_checksum:
64
+ return None
65
+
66
+ return packet.with_data(payload)
67
+
68
+ async def outgoing_filter(packet: CspPacket) -> CspPacket:
69
+ if (packet.packet_id.flags & 1) == 0:
70
+ return packet
71
+
72
+ payload = packet.data
73
+ checksum = calculate_crc32(payload)
74
+
75
+ return packet.with_data(payload + struct.pack('!I', checksum))
76
+
77
+ router.incoming_packet_filters.append(incoming_filter)
78
+ router.outgoing_packet_filters.append(outgoing_filter)
csp_py/interface.py ADDED
@@ -0,0 +1,19 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Protocol
3
+
4
+ from .packet import CspPacket
5
+
6
+
7
+ class CspPacketSink(Protocol):
8
+ def __call__(self, packet: CspPacket) -> None:
9
+ raise NotImplementedError()
10
+
11
+
12
+ class ICspInterface(ABC):
13
+ @abstractmethod
14
+ def set_packet_sink(self, sink: CspPacketSink) -> None:
15
+ pass
16
+
17
+ @abstractmethod
18
+ async def send(self, packet: CspPacket) -> None:
19
+ raise NotImplementedError()
@@ -0,0 +1,6 @@
1
+ from .can_interface import CspCanInterface
2
+
3
+
4
+ __all__ = [
5
+ 'CspCanInterface',
6
+ ]
@@ -0,0 +1,180 @@
1
+ from dataclasses import dataclass
2
+ import struct
3
+ from typing import Any, Awaitable, Callable
4
+
5
+ from csp_py import CspPacket, CspId, CspPacketPriority, CspPacketFlags
6
+ from csp_py.interface import ICspInterface, CspPacketSink
7
+
8
+
9
+ @dataclass
10
+ class CfpIdFields:
11
+ begin: bool
12
+ end: bool
13
+ priority: CspPacketPriority
14
+ dst: int
15
+ sender: int
16
+ sc: int
17
+ fc: int
18
+
19
+ def as_key(self) -> tuple[int, int, CspPacketPriority, int]:
20
+ return (self.dst, self.sender, self.priority, self.sc)
21
+
22
+ def as_num(self) -> int:
23
+ def pack_field(value: int, *, mask: int, offset: int) -> int:
24
+ return (value & mask) << offset
25
+
26
+ r = 0
27
+ r |= pack_field(self.begin, mask=0x1, offset=1)
28
+ r |= pack_field(self.end, mask=0x1, offset=0)
29
+ r |= pack_field(self.priority.value, mask=0x3, offset=27)
30
+ r |= pack_field(self.dst, mask=0x3FFF, offset=13)
31
+ r |= pack_field(self.sender, mask=0x3F, offset=7)
32
+ r |= pack_field(self.sc, mask=0x3, offset=5)
33
+ r |= pack_field(self.fc, mask=0x7, offset=2)
34
+ return r
35
+
36
+ @dataclass
37
+ class CfpHeaderFields:
38
+ src: int
39
+ dport: int
40
+ sport: int
41
+ flags: CspPacketFlags
42
+
43
+ def as_bytes(self) -> bytes:
44
+ def pack_field(value: int, *, mask: int, offset: int) -> int:
45
+ return (value & mask) << offset
46
+
47
+ r = 0
48
+ r |= pack_field(self.src, mask=0x3FFF, offset=18)
49
+ r |= pack_field(self.dport, mask=0x3F, offset=12)
50
+ r |= pack_field(self.sport, mask=0x3F, offset=6)
51
+ r |= pack_field(self.flags, mask=0x3F, offset=0)
52
+ return struct.pack('!I', r)
53
+
54
+
55
+ def parse_csp_can_frame_id(can_id: int) -> CfpIdFields:
56
+ def extract_field(*, offset: int, mask: int) -> int:
57
+ return (can_id >> offset) & mask
58
+
59
+ return CfpIdFields(
60
+ begin=extract_field(mask=0x1, offset=1) == 1,
61
+ end=extract_field(mask=0x1, offset=0) == 1,
62
+ priority=CspPacketPriority(extract_field(mask=0x3, offset=27)),
63
+ dst=extract_field(mask=0x3FFF, offset=13),
64
+ sender=extract_field(mask=0x3F, offset=7),
65
+ sc=extract_field(mask=0x3, offset=5),
66
+ fc=extract_field(mask=0x7, offset=2),
67
+ )
68
+
69
+ def parse_csp_can_header(data: bytes) -> tuple[CfpHeaderFields, bytes]:
70
+ assert len(data) >= 4
71
+ num, = struct.unpack('!I', data[:4])
72
+
73
+ def extract_field(*, offset: int, mask: int) -> int:
74
+ return int((num >> offset) & mask)
75
+
76
+ return CfpHeaderFields(
77
+ src=extract_field(mask=0x3FFF, offset=18),
78
+ dport=extract_field(mask=0x3F, offset=12),
79
+ sport=extract_field(mask=0x3F, offset=6),
80
+ flags=CspPacketFlags(extract_field(mask=0x3F, offset=0)),
81
+ ), data[4:]
82
+
83
+ class CfpReassemblyTracker:
84
+ def __init__(self, id_header: CfpIdFields, header: CfpHeaderFields) -> None:
85
+ self._data = bytearray()
86
+ self._id_header = id_header
87
+ self._header = header
88
+
89
+ def append(self, data: bytes) -> None:
90
+ self._data.extend(data)
91
+
92
+ def capture(self) -> CspPacket:
93
+ return CspPacket(
94
+ packet_id=CspId(
95
+ priority=self._id_header.priority,
96
+ flags=self._header.flags,
97
+ src=self._header.src,
98
+ dst=self._id_header.dst,
99
+ dport=self._header.dport,
100
+ sport=self._header.sport,
101
+ ),
102
+ data=bytes(self._data),
103
+ )
104
+
105
+ class CspCanInterface(ICspInterface):
106
+ def __init__(self) -> None:
107
+ self._in_flight: dict[Any, CfpReassemblyTracker] = {}
108
+ self._packet_sink: CspPacketSink | None = None
109
+ self._sender_counter = 0
110
+
111
+ self.send_can_frame: Callable[[int, bytes], Awaitable[None]] | None = None
112
+
113
+ def set_packet_sink(self, sink: CspPacketSink) -> None:
114
+ self._packet_sink = sink
115
+
116
+ async def send(self, packet: CspPacket) -> None:
117
+ data = packet.data
118
+
119
+ fragments = []
120
+
121
+ fragments.append(data[:4])
122
+ data = data[4:]
123
+
124
+ while len(data) > 0:
125
+ fragments.append(data[:8])
126
+ data = data[8:]
127
+
128
+ if len(fragments) == 1:
129
+ await self._send_singleton_frame(packet)
130
+ else:
131
+ await self._send_multi_frame(packet, fragments)
132
+
133
+ async def _send_singleton_frame(self, packet: CspPacket) -> None:
134
+ raise NotImplementedError()
135
+
136
+ async def _send_multi_frame(self, packet: CspPacket, fragments: list[bytes]) -> None:
137
+ [begin, *middle, end] = fragments
138
+
139
+ header = CfpHeaderFields(
140
+ src=packet.packet_id.src,
141
+ dport=packet.packet_id.dport,
142
+ sport=packet.packet_id.sport,
143
+ flags=packet.packet_id.flags,
144
+ )
145
+
146
+ begin = header.as_bytes() + begin
147
+
148
+ await self._send_frame(CfpIdFields(begin=True, end=False, priority=packet.packet_id.priority, dst=packet.packet_id.dst, sender=packet.packet_id.src, sc=self._sender_counter, fc=0), begin)
149
+
150
+ for idx, f in enumerate(middle):
151
+ await self._send_frame(CfpIdFields(begin=False, end=False, priority=packet.packet_id.priority, dst=packet.packet_id.dst, sender=packet.packet_id.src, sc=self._sender_counter, fc=idx + 1), f)
152
+
153
+ await self._send_frame(CfpIdFields(begin=False, end=True, priority=packet.packet_id.priority, dst=packet.packet_id.dst, sender=packet.packet_id.src, sc=self._sender_counter, fc=len(fragments) - 1), end)
154
+
155
+ async def _send_frame(self, id_fields: CfpIdFields, data: bytes) -> None:
156
+ can_id = id_fields.as_num()
157
+ assert self.send_can_frame is not None
158
+ await self.send_can_frame(can_id, data)
159
+
160
+
161
+ async def on_can_frame(self, can_id: int, data: bytes) -> None:
162
+ parsed_id = parse_csp_can_frame_id(can_id)
163
+
164
+ key = parsed_id.as_key()
165
+
166
+ if parsed_id.begin:
167
+ if len(data) < 4:
168
+ print('[csp.py] Invalid data length')
169
+ return
170
+
171
+ header, data = parse_csp_can_header(data)
172
+
173
+ self._in_flight[key] = CfpReassemblyTracker(parsed_id, header)
174
+
175
+ self._in_flight[key].append(data)
176
+
177
+ if parsed_id.end:
178
+ full_packet = self._in_flight.pop(key).capture()
179
+ assert self._packet_sink is not None
180
+ self._packet_sink(full_packet)
@@ -0,0 +1,39 @@
1
+ from csp_py.interface import ICspInterface, CspPacketSink
2
+ from csp_py.packet import CspPacket
3
+
4
+
5
+ class InterfacePair:
6
+ def __init__(self) -> None:
7
+ self._iface1 = InterfacePair._InterfaceOrPair()
8
+ self._iface2 = InterfacePair._InterfaceOrPair()
9
+
10
+ self._iface1.set_other_iface(self._iface2)
11
+ self._iface2.set_other_iface(self._iface1)
12
+
13
+ @property
14
+ def iface1(self) -> ICspInterface:
15
+ return self._iface1
16
+
17
+ @property
18
+ def iface2(self) -> ICspInterface:
19
+ return self._iface2
20
+
21
+
22
+ class _InterfaceOrPair(ICspInterface):
23
+ def __init__(self) -> None:
24
+ self._packet_sink: CspPacketSink | None = None
25
+ self._other_iface: InterfacePair._InterfaceOrPair | None = None
26
+
27
+ def set_packet_sink(self, sink: CspPacketSink) -> None:
28
+ self._packet_sink = sink
29
+
30
+ def set_other_iface(self, other_iface: 'InterfacePair._InterfaceOrPair') -> None:
31
+ self._other_iface = other_iface
32
+
33
+ async def send(self, packet: CspPacket) -> None:
34
+ assert self._other_iface is not None
35
+ self._other_iface.incoming_packet(packet)
36
+
37
+ def incoming_packet(self, packet: CspPacket) -> None:
38
+ assert self._packet_sink is not None
39
+ self._packet_sink(packet)
@@ -0,0 +1,16 @@
1
+ from csp_py.interface import ICspInterface, CspPacketSink
2
+ from csp_py.packet import CspPacket
3
+
4
+
5
+ class LoInterface(ICspInterface):
6
+ def __init__(self) -> None:
7
+ super().__init__()
8
+ self._packet_sink: CspPacketSink | None = None
9
+
10
+ def set_packet_sink(self, sink: CspPacketSink) -> None:
11
+ self._packet_sink = sink
12
+
13
+ async def send(self, packet: CspPacket) -> None:
14
+ sink = self._packet_sink
15
+ assert sink is not None
16
+ sink(packet)
@@ -0,0 +1,81 @@
1
+ from dataclasses import dataclass
2
+ import struct
3
+ from typing import Protocol
4
+
5
+ from ..interface import ICspInterface, CspPacketSink
6
+ from csp_py import CspPacket, CspId, CspPacketPriority, CspPacketFlags
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class FieldInfo:
11
+ mask: int
12
+ offset: int
13
+
14
+ def extract(self, value: int) -> int:
15
+ return (value >> self.offset) & self.mask
16
+
17
+ def pack(self, value: int) -> int:
18
+ return (value & self.mask) << self.offset
19
+
20
+
21
+ class CspIdLayout:
22
+ Priority = FieldInfo(3, 46)
23
+ Destination = FieldInfo(0x3FFF, 32)
24
+ Source = FieldInfo(0x3FFF, 18)
25
+ DestinationPort = FieldInfo(0x3F, 12)
26
+ SourcePort = FieldInfo(0x3F, 6)
27
+ Flags = FieldInfo(0x3F, 0)
28
+
29
+ @staticmethod
30
+ def from_int(value: int) -> CspId:
31
+ return CspId(
32
+ priority=CspPacketPriority(CspIdLayout.Priority.extract(value)),
33
+ dst=CspIdLayout.Destination.extract(value),
34
+ src=CspIdLayout.Source.extract(value),
35
+ dport=CspIdLayout.DestinationPort.extract(value),
36
+ sport=CspIdLayout.SourcePort.extract(value),
37
+ flags=CspPacketFlags(CspIdLayout.Flags.extract(value)),
38
+ )
39
+
40
+ @staticmethod
41
+ def to_int(id: CspId) -> int:
42
+ return (
43
+ CspIdLayout.Priority.pack(id.priority.value) |
44
+ CspIdLayout.Destination.pack(id.dst) |
45
+ CspIdLayout.Source.pack(id.src) |
46
+ CspIdLayout.DestinationPort.pack(id.dport) |
47
+ CspIdLayout.SourcePort.pack(id.sport) |
48
+ CspIdLayout.Flags.pack(id.flags.value)
49
+ )
50
+
51
+ class SerializedFrameSink(Protocol):
52
+ async def __call__(self, frame: bytes) -> None:
53
+ ...
54
+
55
+
56
+ class CspSerializingInterface(ICspInterface):
57
+ def __init__(self, on_frame: SerializedFrameSink) -> None:
58
+ self._frame_sink = on_frame
59
+ self._packet_sink: CspPacketSink | None = None
60
+
61
+ def set_packet_sink(self, sink: CspPacketSink) -> None:
62
+ self._packet_sink = sink
63
+
64
+ async def send(self, packet: CspPacket) -> None:
65
+ header_num = CspIdLayout.to_int(packet.packet_id)
66
+ header = struct.pack('!Q', header_num)[2:]
67
+ frame = header + packet.data
68
+ await self._frame_sink(frame)
69
+
70
+ async def on_incoming_frame(self, frame: bytes) -> None:
71
+ header = b'\x00\x00' + frame[:6]
72
+ payload = frame[6:]
73
+ header_num, = struct.unpack('!Q', header)
74
+ incoming_id = CspIdLayout.from_int(header_num)
75
+ packet = CspPacket(
76
+ packet_id=incoming_id,
77
+ data=payload,
78
+ )
79
+ sink = self._packet_sink
80
+ assert sink is not None
81
+ sink(packet)
csp_py/node.py ADDED
@@ -0,0 +1,54 @@
1
+ from .router import CspRouter
2
+ from .packet import CspPacket, CspPacketFlags
3
+ from .packet_handler import IPacketHandler
4
+ from .services.ping import CspPingHandler
5
+ from .socket import CspSocketHandler, CspBoundSocket, CspListeningSocket, CspClientConnection
6
+ from .interfaces.lo_interface import LoInterface
7
+ from .crc32 import register_crc32_filters
8
+
9
+
10
+ class CspNode:
11
+ def __init__(self, *, default_send_flags: CspPacketFlags = CspPacketFlags.Zero) -> None:
12
+ self._default_send_flags = default_send_flags
13
+ self.router = CspRouter()
14
+ self.router.local_packet_handler = self._on_local_packet
15
+
16
+ self.router.add_interface(LoInterface(), address=0, netmask_bits=14)
17
+
18
+ register_crc32_filters(self.router)
19
+
20
+ self._socket_handler = CspSocketHandler()
21
+
22
+ self._handlers: list[IPacketHandler] = []
23
+
24
+ self.add_packet_handler(CspPingHandler())
25
+ self.add_packet_handler(self._socket_handler)
26
+
27
+ def add_packet_handler(self, handler: IPacketHandler) -> None:
28
+ handler.set_send_packet(self._send_packet)
29
+ self._handlers.append(handler)
30
+
31
+ def bound_socket(self, port: int | None, *, send_flags: CspPacketFlags=CspPacketFlags.Inherit) -> CspBoundSocket:
32
+ return self._socket_handler.bound_socket(port, send_flags=send_flags)
33
+
34
+ def listen(self, port: int | None, *, send_flags: CspPacketFlags=CspPacketFlags.Inherit) -> CspListeningSocket:
35
+ return self._socket_handler.listen(port, send_flags=send_flags)
36
+
37
+ async def connect(self, *, dst: int, port: int, local_port: int | None = None, send_flags: CspPacketFlags=CspPacketFlags.Inherit) -> CspClientConnection:
38
+ return await self._socket_handler.connect(dst, port, local_port, send_flags=send_flags)
39
+
40
+ async def _on_local_packet(self, packet: CspPacket) -> None:
41
+ try:
42
+ for handler in self._handlers:
43
+ if await handler.on_packet(packet):
44
+ return
45
+
46
+ print('No handler for packet', packet)
47
+ except Exception as e:
48
+ print('Exception in packet handler', e)
49
+ raise e
50
+
51
+ async def _send_packet(self, packet: CspPacket) -> None:
52
+ resolved_flags = packet.packet_id.flags.resolve(self._default_send_flags)
53
+ packet = packet.with_id(packet.packet_id.with_flags(resolved_flags))
54
+ await self.router.send_packet(packet)
csp_py/packet.py ADDED
@@ -0,0 +1,85 @@
1
+ from dataclasses import dataclass
2
+ from enum import Enum, IntFlag
3
+
4
+
5
+ class CspPacketPriority(Enum):
6
+ Critical = 0
7
+ High = 1
8
+ Normal = 2
9
+ Low = 3
10
+
11
+
12
+ class CspPacketFlags(IntFlag):
13
+ Zero = 0
14
+ CRC32 = 1
15
+
16
+ Inherit = 0xFF00_0000
17
+
18
+ def resolve(self, inherit: 'CspPacketFlags') -> 'CspPacketFlags':
19
+ if CspPacketFlags.Inherit in self:
20
+ v = self - CspPacketFlags.Inherit
21
+ v |= inherit
22
+ return CspPacketFlags(v).resolve(CspPacketFlags.Zero)
23
+ else:
24
+ return CspPacketFlags(self)
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class CspId:
29
+ src: int
30
+ dst: int
31
+ dport: int
32
+ sport: int
33
+ flags: CspPacketFlags = CspPacketFlags.Zero
34
+ priority: CspPacketPriority = CspPacketPriority.Normal
35
+
36
+ def reply_id(self) -> 'CspId':
37
+ return CspId(
38
+ priority=self.priority,
39
+ flags=self.flags,
40
+ src=self.dst,
41
+ dst=self.src,
42
+ dport=self.sport,
43
+ sport=self.dport,
44
+ )
45
+
46
+ def with_source(self, src: int) -> 'CspId':
47
+ return CspId(
48
+ priority=self.priority,
49
+ flags=self.flags,
50
+ src=src,
51
+ dst=self.dst,
52
+ dport=self.dport,
53
+ sport=self.sport,
54
+ )
55
+
56
+ def with_flags(self, flags: CspPacketFlags) -> 'CspId':
57
+ return CspId(
58
+ priority=self.priority,
59
+ flags=flags,
60
+ src=self.src,
61
+ dst=self.dst,
62
+ dport=self.dport,
63
+ sport=self.sport,
64
+ )
65
+
66
+
67
+ @dataclass(frozen=True)
68
+ class CspPacket:
69
+ packet_id: CspId
70
+ header: bytes = b''
71
+ data: bytes = b''
72
+
73
+ def with_id(self, new_id: CspId) -> 'CspPacket':
74
+ return CspPacket(
75
+ packet_id=new_id,
76
+ header=self.header,
77
+ data=self.data,
78
+ )
79
+
80
+ def with_data(self, new_data: bytes) -> 'CspPacket':
81
+ return CspPacket(
82
+ packet_id=self.packet_id,
83
+ header=self.header,
84
+ data=new_data,
85
+ )
@@ -0,0 +1,24 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Protocol
3
+
4
+ from .packet import CspPacket
5
+
6
+
7
+ class IPacketHandler(ABC):
8
+ class SendPacket(Protocol):
9
+ async def __call__(self, packet: CspPacket) -> None:
10
+ raise NotImplementedError()
11
+
12
+ def __init__(self) -> None:
13
+ self._send_packet: IPacketHandler.SendPacket | None = None
14
+
15
+ def set_send_packet(self, send_packet: SendPacket) -> None:
16
+ self._send_packet = send_packet
17
+
18
+ async def send_packet(self, packet: CspPacket) -> None:
19
+ assert self._send_packet is not None
20
+ await self._send_packet(packet)
21
+
22
+ @abstractmethod
23
+ async def on_packet(self, packet: CspPacket) -> bool:
24
+ raise NotImplementedError()
csp_py/router.py ADDED
@@ -0,0 +1,149 @@
1
+ import asyncio
2
+ from dataclasses import dataclass
3
+ from typing import Awaitable, Callable, Protocol
4
+
5
+ from .interface import ICspInterface
6
+ from .packet import CspPacket
7
+ from .rtable import CspRoutingTable
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class CspInterfaceAddress:
12
+ address: int
13
+ network_address_bits: int
14
+
15
+ @property
16
+ def network_mask(self) -> int:
17
+ # TODO: const for address length
18
+ return ((1 << self.network_address_bits) - 1) << (14 - self.network_address_bits)
19
+
20
+ @property
21
+ def broadcast_address(self) -> int:
22
+ return (~self.network_mask) & 0x3FFF
23
+
24
+ @property
25
+ def network_address(self) -> int:
26
+ return self.address & self.network_mask
27
+
28
+ def contains_address(self, address: int) -> bool:
29
+ other = CspInterfaceAddress(address=address, network_address_bits=self.network_address_bits)
30
+ return self.network_address == other.network_address
31
+
32
+
33
+ class CspRouterFilter(Protocol):
34
+ async def __call__(self, packet: CspPacket) -> CspPacket | None:
35
+ ...
36
+
37
+
38
+ class CspRouter:
39
+ def __init__(self) -> None:
40
+ self._interfaces: list[tuple[CspInterfaceAddress, ICspInterface]] = []
41
+ self._incoming_packets = asyncio.Queue[tuple[ICspInterface, CspPacket]]()
42
+ self.rtable = CspRoutingTable()
43
+ self.local_packet_handler: Callable[[CspPacket], Awaitable[None]] | None = None
44
+
45
+ self.incoming_packet_filters: list[CspRouterFilter] = []
46
+ self.routed_packet_filters: list[CspRouterFilter] = []
47
+ self.outgoing_packet_filters: list[CspRouterFilter] = []
48
+
49
+ def push_packet(self, iface: ICspInterface, packet: CspPacket) -> None:
50
+ self._incoming_packets.put_nowait((iface, packet))
51
+
52
+ async def arun(self) -> None:
53
+ while True:
54
+ await self.process_one_incoming_packet()
55
+
56
+ async def process_one_incoming_packet(self) -> None:
57
+ src_iface, packet = await self._incoming_packets.get()
58
+
59
+ [src_address] = [addr for addr, iface in self._interfaces if iface == src_iface]
60
+
61
+ to_localhost = packet.packet_id.dst in [src_address.address, src_address.broadcast_address]
62
+ if to_localhost:
63
+ await self._process_incoming_packet(packet)
64
+ return
65
+
66
+ target = self._find_outgoing_interface(packet.packet_id.dst)
67
+
68
+ if target is None:
69
+ return
70
+
71
+ target_address, target_iface = target
72
+
73
+ if target_iface == src_iface:
74
+ return
75
+
76
+ if target_address.address == packet.packet_id.dst:
77
+ return
78
+
79
+ await self._process_routed_packet(packet, target_iface)
80
+
81
+ async def _on_packet_to_local(self, packet: CspPacket) -> None:
82
+ assert self.local_packet_handler is not None
83
+ await self.local_packet_handler(packet)
84
+
85
+ def add_interface(self, interface: ICspInterface, *, address: int, netmask_bits: int) -> None:
86
+ iface_address = CspInterfaceAddress(address=address, network_address_bits=netmask_bits)
87
+
88
+ if any(addr.network_address == iface_address.network_address and addr.network_address_bits != 14 for addr, _ in self._interfaces):
89
+ raise ValueError('An interface with the same network address already exists')
90
+
91
+ self._interfaces.append((iface_address, interface))
92
+
93
+
94
+ def sink_packet(packet: CspPacket) -> None:
95
+ self.push_packet(interface, packet)
96
+
97
+ interface.set_packet_sink(sink_packet)
98
+
99
+ async def send_packet(self, packet: CspPacket) -> None:
100
+ assert isinstance(packet.data, bytes) or isinstance(packet.data, bytearray)
101
+
102
+ target = self._find_outgoing_interface(packet.packet_id.dst)
103
+
104
+ if target is None:
105
+ raise ValueError("no interface matched the packet destination")
106
+
107
+ iface_addr, iface = target
108
+
109
+ packet = packet.with_id(packet.packet_id.with_source(iface_addr.address))
110
+ await self._process_outgoing_packet(packet, iface)
111
+
112
+ def _find_outgoing_interface(self, target: int) -> tuple[CspInterfaceAddress, ICspInterface] | None:
113
+ ifaces = [(addr, iface) for addr, iface in self._interfaces if addr.contains_address(target)]
114
+
115
+ if len(ifaces) == 1:
116
+ return ifaces[0]
117
+
118
+ if len(ifaces) > 1:
119
+ raise ValueError('More than one interface matched the packet destination')
120
+
121
+ iface_by_route = self.rtable.iface_for_address(target)
122
+
123
+ if iface_by_route is None:
124
+ return None
125
+
126
+ [iface_addr] = [addr for (addr, iface) in self._interfaces if iface == iface_by_route]
127
+ return iface_addr, iface_by_route
128
+
129
+ async def _process_outgoing_packet(self, packet: CspPacket, iface: ICspInterface) -> None:
130
+ for f in self.outgoing_packet_filters:
131
+ filter_result = await f(packet)
132
+ if filter_result is None:
133
+ return
134
+ packet = filter_result
135
+ await iface.send(packet)
136
+
137
+ async def _process_routed_packet(self, packet: CspPacket, to_matching_interface: ICspInterface) -> None:
138
+ await to_matching_interface.send(packet)
139
+
140
+ async def _process_incoming_packet(self, packet: CspPacket) -> None:
141
+ for f in self.incoming_packet_filters:
142
+ filter_result = await f(packet)
143
+ if filter_result is None:
144
+ return
145
+ packet = filter_result
146
+ if packet is None:
147
+ return
148
+
149
+ await self._on_packet_to_local(packet)
csp_py/rtable.py ADDED
@@ -0,0 +1,49 @@
1
+ from dataclasses import dataclass
2
+
3
+ from .interface import ICspInterface
4
+
5
+
6
+ @dataclass(frozen=True)
7
+ class Route:
8
+ network_address: int
9
+ netmask_bits: int
10
+ iface: ICspInterface
11
+
12
+ def __post_init__(self) -> None:
13
+ assert (self.network_address & ~self.netmask) == 0, 'Network address must have node address set to 0'
14
+
15
+ @property
16
+ def netmask(self) -> int:
17
+ return ((1 << self.netmask_bits) - 1) << (14 - self.netmask_bits)
18
+
19
+ def routes_to_address(self, target: int) -> bool:
20
+ target_network = target & self.netmask
21
+ return target_network == self.network_address
22
+
23
+
24
+ class CspRoutingTable:
25
+ def __init__(self) -> None:
26
+ self._routes: list[Route] = []
27
+
28
+ def add_entry(self, *, network_address: int, netmask_bits: int, iface: ICspInterface) -> None:
29
+ if any(route for route in self._routes if route.network_address == network_address and route.netmask_bits == netmask_bits):
30
+ raise ValueError('Duplicate route entry')
31
+ # TODO: check for duplicate entry
32
+ self._routes.append(Route(
33
+ network_address=network_address,
34
+ netmask_bits=netmask_bits,
35
+ iface=iface
36
+ ))
37
+
38
+ def iface_for_address(self, address: int) -> ICspInterface | None:
39
+ matching_routes = [route for route in self._routes if route.routes_to_address(address)]
40
+
41
+ if len(matching_routes) == 0:
42
+ return None
43
+
44
+ if len(matching_routes) == 1:
45
+ return matching_routes[0].iface
46
+
47
+ matching_routes = list(sorted(matching_routes, key=lambda r: r.netmask_bits, reverse=True))
48
+
49
+ return matching_routes[0].iface
@@ -0,0 +1,17 @@
1
+ from ..packet_handler import IPacketHandler
2
+ from ..packet import CspPacket, CspPacketFlags
3
+
4
+
5
+ class CspPingHandler(IPacketHandler):
6
+ async def on_packet(self, packet: CspPacket) -> bool:
7
+ if packet.packet_id.dport == 1:
8
+ response = CspPacket(
9
+ packet_id=packet.packet_id.reply_id(),
10
+ header=packet.header,
11
+ data=packet.data,
12
+ )
13
+ await self.send_packet(response)
14
+ return True
15
+ else:
16
+ return False
17
+
@@ -0,0 +1,16 @@
1
+ import time
2
+
3
+ from ..node import CspNode
4
+ from ..packet import CspPacketFlags
5
+
6
+
7
+ async def ping(node: CspNode, *, dst: int, size: int = 100, send_flags: CspPacketFlags = CspPacketFlags.Inherit) -> float:
8
+ sock = await node.connect(dst=dst, port=1, send_flags=send_flags)
9
+ data = bytes([i % 256 for i in range(size)])
10
+
11
+ start = time.monotonic()
12
+ await sock.send(data)
13
+ response = await sock.recv()
14
+ stop = time.monotonic()
15
+ assert response.data == data, f'{response!r} != {data!r}'
16
+ return stop - start
csp_py/socket.py ADDED
@@ -0,0 +1,269 @@
1
+ from abc import ABC, abstractmethod
2
+ import asyncio
3
+ from csp_py.packet_handler import IPacketHandler
4
+ from .packet import CspPacket, CspId, CspPacketPriority, CspPacketFlags
5
+
6
+
7
+ # TODO: separate impl
8
+
9
+ class BindAnyPort:
10
+ pass
11
+
12
+
13
+ Port = int | BindAnyPort
14
+
15
+ class CspPortHandler(ABC):
16
+ @abstractmethod
17
+ async def _add_incoming_packet(self, packet: CspPacket) -> None:
18
+ pass
19
+
20
+ class CspServerConnection:
21
+ def __init__(self, owner: 'CspListeningSocket', dst: int, port: int, local_port: int) -> None:
22
+ self._owner = owner
23
+ self._dst: int | None = dst
24
+ self._port: int | None = port
25
+ self._local_port: int | None = local_port
26
+ self._pending_packets = asyncio.Queue[CspPacket]()
27
+
28
+ @property
29
+ def remote_address(self) -> int:
30
+ assert self._dst is not None
31
+ return self._dst
32
+
33
+ @property
34
+ def remote_port(self) -> int:
35
+ assert self._port is not None
36
+ return self._port
37
+
38
+ @property
39
+ def local_port(self) -> int:
40
+ assert self._local_port is not None
41
+ return self._local_port
42
+
43
+ def __del__(self) -> None:
44
+ self.close()
45
+
46
+ def close(self) -> None:
47
+ if self._dst is not None and self._port is not None:
48
+ self._owner._delete_connection(self._dst, self._port)
49
+ self._dst = None
50
+ self._port = None
51
+
52
+ async def send(self, data: bytes) -> None:
53
+ assert self._dst is not None
54
+ assert self._port is not None
55
+ assert self._local_port is not None
56
+ await self._owner._send_to(dst=self._dst, port=self._port, local_port=self._local_port, data=data)
57
+
58
+ async def recv(self) -> CspPacket:
59
+ return await self._pending_packets.get()
60
+
61
+ async def _add_incoming_packet(self, packet: CspPacket) -> None:
62
+ await self._pending_packets.put(packet)
63
+
64
+
65
+ class CspBoundSocket(CspPortHandler):
66
+ def __init__(self, handler: 'CspSocketHandler', port: Port, send_flags: CspPacketFlags) -> None:
67
+ self._handler = handler
68
+ self._port: Port | None = None
69
+ self._pending_packets = asyncio.Queue[CspPacket]()
70
+ self._handler.bind(port, self)
71
+ self._port = port
72
+ self._send_flags = send_flags
73
+
74
+ def __del__(self) -> None:
75
+ self.close()
76
+
77
+ def close(self) -> None:
78
+ if self._port is not None:
79
+ self._handler.unbind(self._port)
80
+
81
+ self._port = None
82
+
83
+ async def send_to(self, *, dst: int, port: int, data: bytes) -> None:
84
+ assert self._port is not None
85
+ assert not isinstance(self._port, BindAnyPort)
86
+ packet = CspPacket(
87
+ packet_id=CspId(
88
+ priority=CspPacketPriority.Normal, # TODO
89
+ flags=self._send_flags,
90
+ src=0,
91
+ sport=self._port,
92
+ dst=dst,
93
+ dport=port,
94
+ ),
95
+ data=data,
96
+ )
97
+ await self._handler.send_packet(packet)
98
+
99
+ async def _add_incoming_packet(self, packet: CspPacket) -> None:
100
+ await self._pending_packets.put(packet)
101
+
102
+ async def send_reply(self, request: CspPacket, response: bytes) -> None:
103
+ assert self._port is not None
104
+
105
+ if not isinstance(self._port, BindAnyPort) and request.packet_id.dport != self._port:
106
+ raise ValueError(f'Invalid destination port in request (port in request {request.packet_id.dport} but bound to {self._port})')
107
+
108
+ packet = CspPacket(
109
+ packet_id=CspId(
110
+ priority=CspPacketPriority.Normal, # TODO
111
+ flags=self._send_flags,
112
+ src=0,
113
+ sport=request.packet_id.dport,
114
+ dst=request.packet_id.src,
115
+ dport=request.packet_id.sport,
116
+ ),
117
+ data=response,
118
+ )
119
+ await self._handler.send_packet(packet)
120
+
121
+ async def recv_from(self) -> CspPacket:
122
+ return await self._pending_packets.get()
123
+
124
+
125
+ class CspListeningSocket(CspPortHandler):
126
+ ConnectionId = tuple[int, int]
127
+
128
+ def __init__(self, owner: 'CspSocketHandler', local_port: Port, send_flags: CspPacketFlags) -> None:
129
+ self._owner = owner
130
+ self._local_port: Port | None = None
131
+ owner.bind(local_port, self)
132
+ self._local_port = local_port
133
+ self._send_flags = send_flags
134
+ self._connections: dict[CspListeningSocket.ConnectionId, CspServerConnection] = {}
135
+ self._pending_packets = asyncio.Queue[CspPacket]()
136
+ self._pending_connections = asyncio.Queue[CspListeningSocket.ConnectionId]()
137
+
138
+ def __del__(self) -> None:
139
+ self.close()
140
+
141
+ async def accept(self) -> CspServerConnection:
142
+ conn_id = await self._pending_connections.get()
143
+ return self._connections[conn_id]
144
+
145
+ def close(self) -> None:
146
+ if self._local_port is not None:
147
+ self._owner.unbind(self._local_port)
148
+ self._local_port = None
149
+
150
+ async def _add_incoming_packet(self, packet: CspPacket) -> None:
151
+ connection_id = self._conn_id(packet)
152
+ connection = self._connections.get(connection_id, None)
153
+ if connection is not None:
154
+ await connection._add_incoming_packet(packet)
155
+ else:
156
+ connection = CspServerConnection(self, packet.packet_id.src, packet.packet_id.sport, packet.packet_id.dport)
157
+ self._connections[connection_id] = connection
158
+ await connection._add_incoming_packet(packet)
159
+ await self._pending_connections.put(connection_id)
160
+
161
+ def _delete_connection(self, address: int, port: int) -> None:
162
+ del self._connections[(address, port)]
163
+
164
+ async def _send_to(self, *, dst: int, port: int, local_port: int, data: bytes) -> None:
165
+ assert self._local_port is not None
166
+
167
+ packet = CspPacket(
168
+ packet_id=CspId(
169
+ dst=dst,
170
+ dport=port,
171
+ src=0,
172
+ sport=local_port,
173
+ flags=self._send_flags,
174
+ priority=CspPacketPriority.Normal
175
+ ),
176
+ header=b'',
177
+ data=data
178
+ )
179
+ await self._owner.send_packet(packet)
180
+
181
+ @staticmethod
182
+ def _conn_id(packet: CspPacket) -> ConnectionId:
183
+ return (packet.packet_id.src, packet.packet_id.sport)
184
+
185
+
186
+ class CspClientConnection:
187
+ def __init__(self, socket: CspBoundSocket, remote_address: int, remote_port: int) -> None:
188
+ super().__init__()
189
+ self._socket = socket
190
+ self._remote_address = remote_address
191
+ self._remote_port = remote_port
192
+
193
+ async def send(self, data: bytes) -> None:
194
+ await self._socket.send_to(dst=self._remote_address, port=self._remote_port, data=data)
195
+
196
+ async def recv(self) -> CspPacket:
197
+ while True:
198
+ packet = await self._socket.recv_from()
199
+
200
+ if packet.packet_id.src == self._remote_address and packet.packet_id.sport == self._remote_port:
201
+ return packet
202
+
203
+ def close(self) -> None:
204
+ self._socket.close()
205
+
206
+ @staticmethod
207
+ async def _connect(local_socket: CspBoundSocket, dst: int, port: int, local_port: int) -> 'CspClientConnection':
208
+ return CspClientConnection(local_socket, dst, port)
209
+
210
+
211
+ class CspSocketHandler(IPacketHandler):
212
+ ANY_PORT = 0xFFFF_FFFF
213
+
214
+ def __init__(self) -> None:
215
+ super().__init__()
216
+ self._ports: dict[int, CspPortHandler] = {}
217
+
218
+ def bound_socket(self, port: int | None, *, send_flags: CspPacketFlags) -> CspBoundSocket:
219
+ bind_to: Port | None = port
220
+ if bind_to is None:
221
+ bind_to = BindAnyPort()
222
+ socket = CspBoundSocket(self, bind_to, send_flags=send_flags)
223
+ return socket
224
+
225
+ def listen(self, port: int | None, *, send_flags: CspPacketFlags) -> CspListeningSocket:
226
+ bind_to: Port | None = port
227
+ if bind_to is None:
228
+ bind_to = BindAnyPort()
229
+ socket = CspListeningSocket(self, bind_to, send_flags=send_flags)
230
+ return socket
231
+
232
+ async def connect(self, remote_address: int, remote_port: int, local_port: int | None, *, send_flags: CspPacketFlags) -> CspClientConnection:
233
+ if local_port is None:
234
+ local_port = self._find_free_port()
235
+ local_socket = self.bound_socket(local_port, send_flags=send_flags)
236
+ connection = await CspClientConnection._connect(local_socket, remote_address, remote_port, local_port)
237
+ return connection
238
+
239
+ async def on_packet(self, packet: CspPacket) -> bool:
240
+ handler = self._ports.get(packet.packet_id.dport)
241
+ if handler is None:
242
+ handler = self._ports.get(self.ANY_PORT)
243
+
244
+ if handler is None:
245
+ return False
246
+
247
+ await handler._add_incoming_packet(packet)
248
+ return True
249
+
250
+ def bind(self, port: Port, handler: CspPortHandler) -> None:
251
+ if isinstance(port, BindAnyPort):
252
+ port = self.ANY_PORT
253
+
254
+ if port in self._ports:
255
+ raise ValueError('Port already bound')
256
+
257
+ self._ports[port] = handler
258
+
259
+ def unbind(self, port: Port) -> None:
260
+ if isinstance(port, BindAnyPort):
261
+ port = self.ANY_PORT
262
+ del self._ports[port]
263
+
264
+ def _find_free_port(self) -> int:
265
+ for i in range(32, 64): # TODO: global consts?
266
+ if i not in self._ports:
267
+ return i
268
+
269
+ raise ValueError('No free port found')
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: cubesat-space-protocol-py
3
+ Version: 1.0.0
4
+ Summary: Cubsat Space Protocol (CSP) native Python implementation
5
+ Author: KP Labs
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Requires-Python: >=3.12
@@ -0,0 +1,20 @@
1
+ csp_py/__init__.py,sha256=g-S_psXooUNB_qiPIY12XATsbYFYRq6bIULuR5WGnxM,370
2
+ csp_py/crc32.py,sha256=t0bARi9zfnGkQr55qWebFLvXE7d985VjmgFP4CHhnqA,4309
3
+ csp_py/interface.py,sha256=M_lRma0JnNvolMcV05KptAV2DpcS8-N2C9qfnlcxDWQ,446
4
+ csp_py/node.py,sha256=2XWv21PzOG8xm-jfPGkX8Ret3c9WNeuvNRMWzvxKVF4,2389
5
+ csp_py/packet.py,sha256=29lRdWlPMHUso-lllh7noGcTnu6UTtuYbrOFApsWcLs,2000
6
+ csp_py/packet_handler.py,sha256=mckU9wCGhMCwbxKxyBOsOFl4rz7uOB-uw_Aiy8VdYKY,723
7
+ csp_py/router.py,sha256=GkyQY49RaJgO7srhKwWafL5Vrquyj_VsspT0n-7aBUA,5425
8
+ csp_py/rtable.py,sha256=uTstBRf-5A6nkcH4Qn3T-LhWLENY4AF6zfGgR8oereU,1657
9
+ csp_py/socket.py,sha256=UwcIYWYZedzxC4xRkKKVhUhYH19-pVsUqkaemk0kRNc,9307
10
+ csp_py/interfaces/interface_pair.py,sha256=twds2GSnWV9N4SiRi9EWfOtAu4fuDqRDuHn2OrIArog,1300
11
+ csp_py/interfaces/lo_interface.py,sha256=4-bj17qDePQ6XVvifXUlYU9PanQmUs7NjXztOLYPP_s,479
12
+ csp_py/interfaces/serializing_interface.py,sha256=Z4epr_YSeLhz8PoDIfgkKX1-NcWc29cjp4bdSqksvKM,2617
13
+ csp_py/interfaces/can/__init__.py,sha256=E5bpXhyRsl-yavIDq6tWtLl_9IrdJx_3FfL6SAD4meU,82
14
+ csp_py/interfaces/can/can_interface.py,sha256=jDEZouu5Qqdt_51ZYJAfmKcwZtXjuph45ldVy3h5naU,6231
15
+ csp_py/services/ping.py,sha256=WFtmm4i6uyA5S955YMON5BBjvbQbo9zzTOnaCkglG1w,513
16
+ csp_py/services/ping_client.py,sha256=FR4kS3YCBqtbLOTMcx-LwwKkj6Wa-o0NVc_BYSPL0sk,527
17
+ cubesat_space_protocol_py-1.0.0.dist-info/METADATA,sha256=ET6ChTlOsrAS9oqJI4LRQv3Dz2JFIPJXqmaMIlsaikM,319
18
+ cubesat_space_protocol_py-1.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
19
+ cubesat_space_protocol_py-1.0.0.dist-info/licenses/LICENSE,sha256=uuYQfDvWdCQlm3e_bkuGvNeJg_t9KMVnGQPQO1toMK0,1063
20
+ cubesat_space_protocol_py-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 KP Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.