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 +18 -0
- csp_py/crc32.py +78 -0
- csp_py/interface.py +19 -0
- csp_py/interfaces/can/__init__.py +6 -0
- csp_py/interfaces/can/can_interface.py +180 -0
- csp_py/interfaces/interface_pair.py +39 -0
- csp_py/interfaces/lo_interface.py +16 -0
- csp_py/interfaces/serializing_interface.py +81 -0
- csp_py/node.py +54 -0
- csp_py/packet.py +85 -0
- csp_py/packet_handler.py +24 -0
- csp_py/router.py +149 -0
- csp_py/rtable.py +49 -0
- csp_py/services/ping.py +17 -0
- csp_py/services/ping_client.py +16 -0
- csp_py/socket.py +269 -0
- cubesat_space_protocol_py-1.0.0.dist-info/METADATA +10 -0
- cubesat_space_protocol_py-1.0.0.dist-info/RECORD +20 -0
- cubesat_space_protocol_py-1.0.0.dist-info/WHEEL +4 -0
- cubesat_space_protocol_py-1.0.0.dist-info/licenses/LICENSE +21 -0
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,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
|
+
)
|
csp_py/packet_handler.py
ADDED
|
@@ -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
|
csp_py/services/ping.py
ADDED
|
@@ -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,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.
|