plato-spw 2024.1.3__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.
egse/spw.py
ADDED
|
@@ -0,0 +1,1480 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module defines classes and functions to work with SpaceWire packets.
|
|
3
|
+
"""
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import struct
|
|
7
|
+
from enum import IntEnum
|
|
8
|
+
from typing import Tuple
|
|
9
|
+
from typing import Union
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
from egse.bits import clear_bit
|
|
14
|
+
from egse.bits import crc_calc
|
|
15
|
+
from egse.bits import set_bit
|
|
16
|
+
from egse.exceptions import Error
|
|
17
|
+
from egse.setup import SetupError
|
|
18
|
+
from egse.state import GlobalState
|
|
19
|
+
|
|
20
|
+
MODULE_LOGGER = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
_ = os.environ["PLATO_CAMERA_IS_EM"]
|
|
24
|
+
TWOS_COMPLEMENT_OFFSET = 32768 if _.capitalize() in ("1", "True", "Yes") else 0
|
|
25
|
+
except KeyError:
|
|
26
|
+
TWOS_COMPLEMENT_OFFSET = 0
|
|
27
|
+
|
|
28
|
+
# RMAP Error Codes and Constants -------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
RMAP_PROTOCOL_ID = 0x01
|
|
31
|
+
RMAP_TARGET_LOGICAL_ADDRESS_DEFAULT = 0xFE
|
|
32
|
+
RMAP_TARGET_KEY = 0xD1
|
|
33
|
+
|
|
34
|
+
# Error and Status Codes
|
|
35
|
+
|
|
36
|
+
RMAP_SUCCESS = 0
|
|
37
|
+
RMAP_GENERAL_ERROR = 1
|
|
38
|
+
RMAP_UNUSED_PACKET_TYPE_COMMAND_CODE = 2
|
|
39
|
+
RMAP_INVALID_KEY = 3
|
|
40
|
+
RMAP_INVALID_DATA_CRC = 4
|
|
41
|
+
RMAP_EARLY_EOP = 5
|
|
42
|
+
RMAP_TOO_MUCH_DATA = 6
|
|
43
|
+
RMAP_EEP = 7
|
|
44
|
+
RMAP_RESERVED = 8
|
|
45
|
+
RMAP_VERIFY_BUFFER_OVERRUN = 9
|
|
46
|
+
RMAP_NOT_IMPLEMENTED_AUTHORISED = 10
|
|
47
|
+
RMAP_RMW_DATA_LENGTH_ERROR = 11
|
|
48
|
+
RMAP_INVALID_TARGET_LOGICAL_ADDRESS = 12
|
|
49
|
+
|
|
50
|
+
# Memory Map layout --------------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
# NOTE: These memory areas are currently equal for N-FEE and F-FEE. Don't know if this will
|
|
53
|
+
# change in the future.
|
|
54
|
+
|
|
55
|
+
CRITICAL_AREA_START = 0x0000_0000
|
|
56
|
+
CRITICAL_AREA_END = 0x0000_00FC
|
|
57
|
+
GENERAL_AREA_START = 0x0000_0100
|
|
58
|
+
GENERAL_AREA_END = 0x0000_06FC
|
|
59
|
+
HK_AREA_START = 0x0000_0700
|
|
60
|
+
HK_AREA_END = 0x0000_07FC
|
|
61
|
+
WINDOWING_AREA_START = 0x0080_0000
|
|
62
|
+
WINDOWING_AREA_END = 0x00FF_FFFC
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class RMAPError(Error):
|
|
66
|
+
"""An RMAP specific Error."""
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class CheckError(RMAPError):
|
|
71
|
+
"""
|
|
72
|
+
Raised when a check fails and you want to pass a status values along with the message.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def __init__(self, message, status):
|
|
76
|
+
self.message = message
|
|
77
|
+
self.status = status
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def update_transaction_identifier(tid: int) -> int:
|
|
81
|
+
"""
|
|
82
|
+
Updates the transaction identifier and returns the new value.
|
|
83
|
+
|
|
84
|
+
FIXME: document more about this identifier, where is it used, when is it checked,
|
|
85
|
+
when does it need to be incremented, who initializes the identifier and
|
|
86
|
+
who updates it, ...
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
tid (int): The current transaction identifier
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
the updated transaction identifier (int).
|
|
93
|
+
"""
|
|
94
|
+
tid = (tid + 1) & 0xFFFF
|
|
95
|
+
return tid
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def create_rmap_read_request_packet(address: int, length: int, tid: int, strict: bool = True) -> bytes:
|
|
99
|
+
"""
|
|
100
|
+
Creates an RMAP Read Request SpaceWire packet.
|
|
101
|
+
|
|
102
|
+
The read request is an RMAP command that read a number of bytes from the FEE register memory.
|
|
103
|
+
|
|
104
|
+
The function returns a ``ctypes`` character array (which is basically a bytes array) that
|
|
105
|
+
can be passed into the EtherSpaceLink library function ``esl_write_packet()``.
|
|
106
|
+
|
|
107
|
+
Address shall be within the 0x0000_0000 and 0x00FF_FFFC. The memory map (register) is divided
|
|
108
|
+
in the following areas:
|
|
109
|
+
|
|
110
|
+
0x0000_0000 - 0x0000_00FC Critical Configuration Area (verified write)
|
|
111
|
+
0x0000_0100 - 0x0000_06FC General Configuration Area (unverified write)
|
|
112
|
+
0x0000_0700 - 0x0000_07FC Housekeeping area
|
|
113
|
+
0x0000_0800 - 0x007F_FFFC Not Supported
|
|
114
|
+
0x0080_0000 - 0x00FF_FFFC Windowing Area (unverified write)
|
|
115
|
+
0x0010_0000 - 0xFFFF_FFFC Not Supported
|
|
116
|
+
|
|
117
|
+
All read requests to the critical area shall have a fixed data length of 4 bytes.
|
|
118
|
+
All read requests to a general area shall have a maximum data length of 256 bytes.
|
|
119
|
+
All read requests to the housekeeping area shall have a maximum data length of 256 bytes.
|
|
120
|
+
All read requests to the windowing area shall have a maximum data length of 4096 bytes.
|
|
121
|
+
|
|
122
|
+
The transaction identifier shall be incremented for each read request. This shall be done by
|
|
123
|
+
the calling function!
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
address (int): the FEE register memory address
|
|
127
|
+
length (int): the data length
|
|
128
|
+
tid (int): transaction identifier
|
|
129
|
+
strict (bool): perform strict checking of address and length
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
a bytes object containing the full RMAP Read Request packet.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
check_address_and_data_length(address, length, strict=strict)
|
|
136
|
+
|
|
137
|
+
buf = bytearray(16)
|
|
138
|
+
|
|
139
|
+
# NOTE: The first bytes would each carry the target SpW address or a destination port,
|
|
140
|
+
# but this is not used for point-to-point connections, so we're safe.
|
|
141
|
+
|
|
142
|
+
buf[0] = 0x51 # Target N-FEE or F-FEE
|
|
143
|
+
buf[1] = 0x01 # RMAP Protocol ID
|
|
144
|
+
buf[2] = 0x4C # Instruction: 0b1001100, RMAP Request, Read, Incrementing address, reply address
|
|
145
|
+
buf[3] = 0xD1 # Destination Key
|
|
146
|
+
buf[4] = 0x50 # Initiator is always the DPU
|
|
147
|
+
buf[5] = (tid >> 8) & 0xFF # MSB of the Transition ID
|
|
148
|
+
buf[6] = tid & 0xFF # LSB of the Transition ID
|
|
149
|
+
buf[7] = 0x00 # Extended address is not used
|
|
150
|
+
buf[8] = (address >> 24) & 0xFF # address (MSB)
|
|
151
|
+
buf[9] = (address >> 16) & 0xFF # address
|
|
152
|
+
buf[10] = (address >> 8) & 0xFF # address
|
|
153
|
+
buf[11] = address & 0xFF # address (LSB)
|
|
154
|
+
buf[12] = (length >> 16) & 0xFF # data length (MSB)
|
|
155
|
+
buf[13] = (length >> 8) & 0xFF # data length
|
|
156
|
+
buf[14] = length & 0xFF # data length (LSB)
|
|
157
|
+
buf[15] = rmap_crc_check(buf, 0, 15) & 0xFF
|
|
158
|
+
return bytes(buf)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def create_rmap_read_request_reply_packet(
|
|
162
|
+
instruction_field: int, tid: int, status: int, buffer: bytes, buffer_length: int) -> bytes:
|
|
163
|
+
"""
|
|
164
|
+
Creates an RMAP Reply to a RMAP Read Request packet.
|
|
165
|
+
|
|
166
|
+
The function returns a ``ctypes`` character array (which is basically a bytes array) that
|
|
167
|
+
can be passed into the EtherSpaceLink library function ``esl_write_packet()``.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
instruction_field (int): the instruction field of the RMAP read request packet
|
|
171
|
+
tid (int): the transaction identifier of the read request packet
|
|
172
|
+
status (int): the status field, 0 on success
|
|
173
|
+
buffer (bytes): the data that was read as indicated by the read request
|
|
174
|
+
buffer_length (int): the data length
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
packet: a bytes object containing the full RMAP Reply packet.
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
buf = bytearray(12 + buffer_length + 1)
|
|
181
|
+
|
|
182
|
+
buf[0] = 0x50 # Initiator address N-DPU or F-DPU
|
|
183
|
+
buf[1] = 0x01 # RMAP Protocol ID
|
|
184
|
+
buf[2] = instruction_field & 0x3F # Clear the command bit as this is a reply
|
|
185
|
+
buf[3] = status & 0xFF # Status field: 0 on success
|
|
186
|
+
buf[4] = 0x51 # Target address is always the N-FEE or F-FEE
|
|
187
|
+
buf[5] = (tid >> 8) & 0xFF # MSB of the Transition ID
|
|
188
|
+
buf[6] = tid & 0xFF # LSB of the Transition ID
|
|
189
|
+
buf[7] = 0x00 # Reserved
|
|
190
|
+
buf[8] = (buffer_length >> 16) & 0xFF # data length (MSB)
|
|
191
|
+
buf[9] = (buffer_length >> 8) & 0xFF # data length
|
|
192
|
+
buf[10] = buffer_length & 0xFF # data length (LSB)
|
|
193
|
+
buf[11] = rmap_crc_check(buf, 0, 11) & 0xFF # Header CRC
|
|
194
|
+
|
|
195
|
+
# Note that we assume here that len(buffer) == buffer_length.
|
|
196
|
+
|
|
197
|
+
if len(buffer) != buffer_length:
|
|
198
|
+
MODULE_LOGGER.warning(
|
|
199
|
+
f"While creating an RMAP read reply packet, the length of the buffer ({len(buffer)}) "
|
|
200
|
+
f"not equals the buffer_length ({buffer_length})"
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
for idx, value in enumerate(buffer):
|
|
204
|
+
buf[12 + idx] = value
|
|
205
|
+
|
|
206
|
+
buf[12 + buffer_length] = rmap_crc_check(buffer, 0, buffer_length) & 0xFF # data CRC
|
|
207
|
+
|
|
208
|
+
return bytes(buf)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def create_rmap_verified_write_packet(address: int, data: bytes, tid: int) -> bytes:
|
|
212
|
+
"""
|
|
213
|
+
Create an RMAP packet for a verified write request on the FEE. The length of the data is
|
|
214
|
+
by convention always 4 bytes and therefore not passed as an argument.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
address: the start memory address on the FEE register map
|
|
218
|
+
data: the data to be written in the register map at address [4 bytes]
|
|
219
|
+
tid (int): transaction identifier
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
packet: a bytes object containing the SpaceWire packet.
|
|
223
|
+
"""
|
|
224
|
+
|
|
225
|
+
if len(data) < 4:
|
|
226
|
+
raise ValueError(
|
|
227
|
+
f"The data argument should be at least 4 bytes, but it is only {len(data)} bytes: {data=}.")
|
|
228
|
+
|
|
229
|
+
if address > CRITICAL_AREA_END:
|
|
230
|
+
raise ValueError("The address range for critical configuration is [0x00 - 0xFC].")
|
|
231
|
+
|
|
232
|
+
tid = update_transaction_identifier(tid)
|
|
233
|
+
|
|
234
|
+
# Buffer length is fixed at 24 bytes since the data length is fixed
|
|
235
|
+
# at 4 bytes (32 bit addressing)
|
|
236
|
+
|
|
237
|
+
buf = bytearray(21)
|
|
238
|
+
|
|
239
|
+
# The values below are taken from the PLATO N-FEE to N-DPU
|
|
240
|
+
# Interface Requirements Document [PLATO-DLR-PL-ICD-0010]
|
|
241
|
+
|
|
242
|
+
buf[0] = 0x51 # Logical Address
|
|
243
|
+
buf[1] = 0x01 # Protocol ID
|
|
244
|
+
buf[2] = 0x7C # Instruction
|
|
245
|
+
buf[3] = 0xD1 # Key
|
|
246
|
+
buf[4] = 0x50 # Initiator Address
|
|
247
|
+
buf[5] = (tid >> 8) & 0xFF # MSB of the Transition ID
|
|
248
|
+
buf[6] = tid & 0xFF # LSB of the Transition ID
|
|
249
|
+
buf[7] = 0x00 # Extended address
|
|
250
|
+
buf[8] = (address >> 24) & 0xFF # address (MSB)
|
|
251
|
+
buf[9] = (address >> 16) & 0xFF # address
|
|
252
|
+
buf[10] = (address >> 8) & 0xFF # address
|
|
253
|
+
buf[11] = address & 0xFF # address (LSB)
|
|
254
|
+
buf[12] = 0x00 # data length (MSB)
|
|
255
|
+
buf[13] = 0x00 # data length
|
|
256
|
+
buf[14] = 0x04 # data length (LSB)
|
|
257
|
+
buf[15] = rmap_crc_check(buf, 0, 15) & 0xFF # header CRC
|
|
258
|
+
buf[16] = data[0]
|
|
259
|
+
buf[17] = data[1]
|
|
260
|
+
buf[18] = data[2]
|
|
261
|
+
buf[19] = data[3]
|
|
262
|
+
buf[20] = rmap_crc_check(buf, 16, 4) & 0xFF # data CRC
|
|
263
|
+
|
|
264
|
+
return bytes(buf)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def create_rmap_unverified_write_packet(address: int, data: bytes, length: int, tid: int) -> bytes:
|
|
268
|
+
"""
|
|
269
|
+
Create an RMAP packet for a unverified write request on the FEE.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
address: the start memory address on the FEE register map
|
|
273
|
+
data: the data to be written in the register map at address
|
|
274
|
+
length: the length of the data
|
|
275
|
+
tid (int): transaction identifier
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
packet: a bytes object containing the SpaceWire packet.
|
|
279
|
+
"""
|
|
280
|
+
|
|
281
|
+
# We can only handle data for which the length >= the given length argument.
|
|
282
|
+
|
|
283
|
+
if len(data) < length:
|
|
284
|
+
raise ValueError(
|
|
285
|
+
f"The length of the data argument ({len(data)}) is smaller than "
|
|
286
|
+
f"the given length argument ({length})."
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
if len(data) > length:
|
|
290
|
+
MODULE_LOGGER.warning(
|
|
291
|
+
f"The length of the data argument ({len(data)}) is larger than "
|
|
292
|
+
f"the given length argument ({length}). The data will be truncated "
|
|
293
|
+
f"when copied into the packet."
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
if address <= CRITICAL_AREA_END:
|
|
297
|
+
raise ValueError(
|
|
298
|
+
f"The given address (0x{address:08X}) is in the range for critical configuration is "
|
|
299
|
+
f"[0x00 - 0xFC]. Use the verified write function for this."
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
tid = update_transaction_identifier(tid)
|
|
303
|
+
|
|
304
|
+
# Buffer length is fixed at 24 bytes since the data length
|
|
305
|
+
# is fixed at 4 bytes (32 bit addressing)
|
|
306
|
+
|
|
307
|
+
buf = bytearray(16 + length + 1)
|
|
308
|
+
offset = 0
|
|
309
|
+
|
|
310
|
+
buf[offset + 0] = 0x51 # Logical Address
|
|
311
|
+
buf[offset + 1] = 0x01 # Protocol ID
|
|
312
|
+
buf[offset + 2] = 0x6C # Instruction
|
|
313
|
+
buf[offset + 3] = 0xD1 # Key
|
|
314
|
+
buf[offset + 4] = 0x50 # Initiator Address
|
|
315
|
+
buf[offset + 5] = (tid >> 8) & 0xFF # MSB of the Transition ID
|
|
316
|
+
buf[offset + 6] = tid & 0xFF # LSB of the Transition ID
|
|
317
|
+
buf[offset + 7] = 0x00 # Extended address
|
|
318
|
+
buf[offset + 8] = (address >> 24) & 0xFF # address (MSB)
|
|
319
|
+
buf[offset + 9] = (address >> 16) & 0xFF # address
|
|
320
|
+
buf[offset + 10] = (address >> 8) & 0xFF # address
|
|
321
|
+
buf[offset + 11] = address & 0xFF # address (LSB)
|
|
322
|
+
buf[offset + 12] = (length >> 16) & 0xFF # data length (MSB)
|
|
323
|
+
buf[offset + 13] = (length >> 8) & 0xFF # data length
|
|
324
|
+
buf[offset + 14] = length & 0xFF # data length (LSB)
|
|
325
|
+
buf[offset + 15] = rmap_crc_check(buf, 0, 15) & 0xFF # header CRC
|
|
326
|
+
|
|
327
|
+
offset += 16
|
|
328
|
+
|
|
329
|
+
for idx, value in enumerate(data):
|
|
330
|
+
buf[offset + idx] = value
|
|
331
|
+
|
|
332
|
+
buf[offset + length] = rmap_crc_check(buf, offset, length) & 0xFF # data CRC
|
|
333
|
+
|
|
334
|
+
return bytes(buf)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def create_rmap_write_request_reply_packet(instruction_field: int, tid: int, status: int) -> bytes:
|
|
338
|
+
buf = bytearray(8)
|
|
339
|
+
|
|
340
|
+
buf[0] = 0x50 # Initiator address N-DPU or F-DPU
|
|
341
|
+
buf[1] = 0x01 # RMAP Protocol ID
|
|
342
|
+
buf[2] = instruction_field & 0x3F # Clear the command bit as this is a reply
|
|
343
|
+
buf[3] = status & 0xFF # Status field: 0 on success
|
|
344
|
+
buf[4] = 0x51 # Target address is always the N-FEE or F-FEE
|
|
345
|
+
buf[5] = (tid >> 8) & 0xFF # MSB of the Transition ID
|
|
346
|
+
buf[6] = tid & 0xFF # LSB of the Transition ID
|
|
347
|
+
buf[7] = rmap_crc_check(buf, 0, 7) & 0xFF # Header CRC
|
|
348
|
+
|
|
349
|
+
return bytes(buf)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def check_address_and_data_length(address: int, length: int, strict: bool = True) -> None:
|
|
353
|
+
"""
|
|
354
|
+
Checks the address and length in the range of memory areas used by the FEE.
|
|
355
|
+
|
|
356
|
+
The ranges are taken from the PLATO-DLR-PL-ICD-0010 N-FEE to N-DPU IRD.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
address (int): the memory address of the FEE Register
|
|
360
|
+
length (int): the number of bytes requested
|
|
361
|
+
strict (bool): strictly apply the rules
|
|
362
|
+
|
|
363
|
+
Raises:
|
|
364
|
+
RMAPError: when address + length fall outside any specified area.
|
|
365
|
+
"""
|
|
366
|
+
|
|
367
|
+
if not strict:
|
|
368
|
+
# All these restrictions have been relaxed on the N-FEE.
|
|
369
|
+
# We are returning here immediately instead of removing or commenting out the code.
|
|
370
|
+
# These reason is that we can then bring back restriction easier and gradually.
|
|
371
|
+
|
|
372
|
+
MODULE_LOGGER.warning(
|
|
373
|
+
"Address and data length checks have been disabled, because the N-FEE "
|
|
374
|
+
"does not enforce restrictions in the critical memory area.")
|
|
375
|
+
return
|
|
376
|
+
|
|
377
|
+
if length % 4:
|
|
378
|
+
raise RMAPError(
|
|
379
|
+
"The requested data length shall be a multiple of 4 bytes.", address, length
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
if address % 4:
|
|
383
|
+
raise RMAPError("The address shall be a multiple of 4 bytes.", address, length)
|
|
384
|
+
|
|
385
|
+
# Note that when checking the given data length, at the defined area end,
|
|
386
|
+
# we can still read 4 bytes.
|
|
387
|
+
|
|
388
|
+
if CRITICAL_AREA_START <= address <= CRITICAL_AREA_END:
|
|
389
|
+
if length != 4:
|
|
390
|
+
raise RMAPError(
|
|
391
|
+
"Read requests to the critical area have a fixed data length of 4 bytes.",
|
|
392
|
+
address, length
|
|
393
|
+
)
|
|
394
|
+
elif GENERAL_AREA_START <= address <= GENERAL_AREA_END:
|
|
395
|
+
if length > 256:
|
|
396
|
+
raise RMAPError(
|
|
397
|
+
"Read requests to the general area have a maximum data length of 256 bytes.",
|
|
398
|
+
address, length
|
|
399
|
+
)
|
|
400
|
+
if address + length > GENERAL_AREA_END + 4:
|
|
401
|
+
raise RMAPError(
|
|
402
|
+
"The requested data length for the general area is too large. "
|
|
403
|
+
"The address + length exceeds the general area boundaries.",
|
|
404
|
+
address, length
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
elif HK_AREA_START <= address <= HK_AREA_END:
|
|
408
|
+
if length > 256:
|
|
409
|
+
raise RMAPError(
|
|
410
|
+
"Read requests to the housekeeping area have a maximum data length of 256 bytes.",
|
|
411
|
+
address, length
|
|
412
|
+
)
|
|
413
|
+
if address + length > HK_AREA_END + 4:
|
|
414
|
+
raise RMAPError(
|
|
415
|
+
"The requested data length for the housekeeping area is too large. "
|
|
416
|
+
"The address + length exceeds the housekeeping area boundaries.",
|
|
417
|
+
address, length
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
elif WINDOWING_AREA_START <= address <= WINDOWING_AREA_END:
|
|
421
|
+
if length > 4096:
|
|
422
|
+
raise RMAPError(
|
|
423
|
+
"Read requests to the windowing area have a maximum data length of 4096 bytes.",
|
|
424
|
+
address, length
|
|
425
|
+
)
|
|
426
|
+
if address + length > WINDOWING_AREA_END + 4:
|
|
427
|
+
raise RMAPError(
|
|
428
|
+
"The requested data length for the windowing area is too large. "
|
|
429
|
+
"The address + length exceeds the windowing area boundaries.", address, length
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
else:
|
|
433
|
+
raise RMAPError("Register address for RMAP read requests is invalid.", address, length)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
class PacketType(IntEnum):
|
|
437
|
+
"""Enumeration type that defines the SpaceWire packet type."""
|
|
438
|
+
|
|
439
|
+
DATA_PACKET = 0
|
|
440
|
+
OVERSCAN_DATA = 1
|
|
441
|
+
HOUSEKEEPING_DATA = 2 # N-FEE
|
|
442
|
+
DEB_HOUSEKEEPING_DATA = 2 # F-FEE
|
|
443
|
+
AEB_HOUSEKEEPING_DATA = 3 # F-FEE
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
class DataPacketType:
|
|
447
|
+
"""
|
|
448
|
+
Defines the Data Packet Field: Type, which is a bit-field of 16 bits.
|
|
449
|
+
|
|
450
|
+
Properties:
|
|
451
|
+
* value: returns the data type as an integer
|
|
452
|
+
* packet_type: the type of data packet, defined in PacketType enum.
|
|
453
|
+
* mode: the FEE mode, defined in n_fee_mode and f_fee_mode enum
|
|
454
|
+
* last_packet: flag which defines the last packet of a type in the current readout cycle
|
|
455
|
+
* ccd_side: 0 for E-side (left), 1 for F-side (right), see egse.fee.fee_side
|
|
456
|
+
* ccd_number: CCD number [0, 3]
|
|
457
|
+
* frame_number: the frame number after sync
|
|
458
|
+
"""
|
|
459
|
+
|
|
460
|
+
def __init__(self, data_type: int = 0):
|
|
461
|
+
self._data_type: int = data_type
|
|
462
|
+
# self.n_fee_side = GlobalState.setup.camera.fee.ccd_sides.enum
|
|
463
|
+
|
|
464
|
+
@property
|
|
465
|
+
def value(self) -> int:
|
|
466
|
+
"""Returns the data packet type as an int."""
|
|
467
|
+
return self._data_type
|
|
468
|
+
|
|
469
|
+
@property
|
|
470
|
+
def packet_type(self):
|
|
471
|
+
"""Returns the packet type: 0 = data packet, 1 = overscan data, 2 = housekeeping packet."""
|
|
472
|
+
return self._data_type & 0b0011
|
|
473
|
+
|
|
474
|
+
@packet_type.setter
|
|
475
|
+
def packet_type(self, value):
|
|
476
|
+
if not 0 <= value < 3:
|
|
477
|
+
raise ValueError(f"Packet Type can only have the value 0, 1, or 2, {value=} given.")
|
|
478
|
+
x = self._data_type
|
|
479
|
+
for idx, bit in enumerate([0, 1]):
|
|
480
|
+
x = set_bit(x, bit) if value & (1 << idx) else clear_bit(x, bit)
|
|
481
|
+
self._data_type = x
|
|
482
|
+
|
|
483
|
+
@property
|
|
484
|
+
def mode(self) -> int:
|
|
485
|
+
return (self._data_type & 0b1111_0000_0000) >> 8
|
|
486
|
+
|
|
487
|
+
@mode.setter
|
|
488
|
+
def mode(self, value: int):
|
|
489
|
+
x = self._data_type
|
|
490
|
+
for idx, bit in enumerate([8, 9, 10, 11]):
|
|
491
|
+
x = set_bit(x, bit) if value & (1 << idx) else clear_bit(x, bit)
|
|
492
|
+
self._data_type = x
|
|
493
|
+
|
|
494
|
+
@property
|
|
495
|
+
def last_packet(self) -> bool:
|
|
496
|
+
return bool(self._data_type & 0b1000_0000)
|
|
497
|
+
|
|
498
|
+
@last_packet.setter
|
|
499
|
+
def last_packet(self, flag: bool):
|
|
500
|
+
self._data_type = set_bit(self._data_type, 7) if flag else clear_bit(self._data_type, 7)
|
|
501
|
+
|
|
502
|
+
@property
|
|
503
|
+
def ccd_side(self) -> int:
|
|
504
|
+
return (self._data_type & 0b0100_0000) >> 6
|
|
505
|
+
|
|
506
|
+
@ccd_side.setter
|
|
507
|
+
def ccd_side(self, value: int):
|
|
508
|
+
self._data_type = set_bit(self._data_type, 6) if value & 0b0001 else clear_bit(self._data_type, 6)
|
|
509
|
+
|
|
510
|
+
@property
|
|
511
|
+
def ccd_number(self) -> int:
|
|
512
|
+
return (self._data_type & 0b0011_0000) >> 4
|
|
513
|
+
|
|
514
|
+
@ccd_number.setter
|
|
515
|
+
def ccd_number(self, value):
|
|
516
|
+
x = self._data_type
|
|
517
|
+
for idx, bit in enumerate([4, 5]):
|
|
518
|
+
x = set_bit(x, bit) if value & (1 << idx) else clear_bit(x, bit)
|
|
519
|
+
self._data_type = x
|
|
520
|
+
|
|
521
|
+
@property
|
|
522
|
+
def frame_number(self) -> int:
|
|
523
|
+
return (self._data_type & 0b1100) >> 2
|
|
524
|
+
|
|
525
|
+
@frame_number.setter
|
|
526
|
+
def frame_number(self, value):
|
|
527
|
+
x = self._data_type
|
|
528
|
+
for idx, bit in enumerate([2, 3]):
|
|
529
|
+
x = set_bit(x, bit) if value & (1 << idx) else clear_bit(x, bit)
|
|
530
|
+
self._data_type = x
|
|
531
|
+
|
|
532
|
+
def __str__(self) -> str:
|
|
533
|
+
from egse.fee import n_fee_mode
|
|
534
|
+
n_fee_side = GlobalState.setup.camera.fee.ccd_sides.enum
|
|
535
|
+
|
|
536
|
+
return (
|
|
537
|
+
f"mode:{n_fee_mode(self.mode).name}, last_packet:{self.last_packet}, "
|
|
538
|
+
f"CCD side:{n_fee_side(self.ccd_side).name}, CCD number:{self.ccd_number}, "
|
|
539
|
+
f"Frame number:{self.frame_number}, Packet Type:{PacketType(self.packet_type).name}"
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def to_string(data: Union[DataPacketType]) -> str:
|
|
544
|
+
"""Returns a 'user-oriented' string representation of the SpW DataPacketType.
|
|
545
|
+
|
|
546
|
+
The purpose of this function is to represent the N-FEE information in a user-oriented way.
|
|
547
|
+
That means for certain values that they will be converted into the form the a user understands
|
|
548
|
+
and that may be different or reverse from the original N-FEE definition. An example is the
|
|
549
|
+
CCD number which is different from the user perspective with respect to the N-FEE.
|
|
550
|
+
|
|
551
|
+
If any other object type is passed, the data.__str__() method will be returned without
|
|
552
|
+
processing or conversion.
|
|
553
|
+
|
|
554
|
+
Args:
|
|
555
|
+
data: a DataPacketType
|
|
556
|
+
"""
|
|
557
|
+
from egse.fee import n_fee_mode
|
|
558
|
+
n_fee_side = GlobalState.setup.camera.fee.ccd_sides.enum
|
|
559
|
+
|
|
560
|
+
if isinstance(data, DataPacketType):
|
|
561
|
+
try:
|
|
562
|
+
ccd_bin_to_id = GlobalState.setup.camera.fee.ccd_numbering.CCD_BIN_TO_ID
|
|
563
|
+
except AttributeError:
|
|
564
|
+
raise SetupError("No entry in the setup for camera.fee.ccd_numbering.CCD_BIN_TO_ID")
|
|
565
|
+
return (
|
|
566
|
+
f"mode:{n_fee_mode(data.mode).name}, last_packet:{data.last_packet}, "
|
|
567
|
+
f"CCD side:{n_fee_side(data.ccd_side).name}, CCD number:"
|
|
568
|
+
f"{ccd_bin_to_id[data.ccd_number]}, "
|
|
569
|
+
f"Frame number:{data.frame_number}, Packet Type:{PacketType(data.packet_type).name}"
|
|
570
|
+
)
|
|
571
|
+
else:
|
|
572
|
+
return data.__str__()
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
class DataPacketHeader:
|
|
576
|
+
"""
|
|
577
|
+
Defines the header of a data packet.
|
|
578
|
+
|
|
579
|
+
The full header can be retrieved as a bytes object with the `data_as_bytes()` method.
|
|
580
|
+
|
|
581
|
+
Properties:
|
|
582
|
+
* logical_address: fixed value of 0x50
|
|
583
|
+
* protocol_id: fixed value of 0xF0
|
|
584
|
+
* length: length of the data part of the packet, i.e. the packet length - size of the header
|
|
585
|
+
* type: data packet type as defined by DataPacketType
|
|
586
|
+
* frame_counter:
|
|
587
|
+
* sequence_counter: a packet sequence counter per CCD
|
|
588
|
+
"""
|
|
589
|
+
def __init__(self, header_data: bytes = None):
|
|
590
|
+
self.header_data = bytearray(
|
|
591
|
+
header_data or bytes([0x50, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]))
|
|
592
|
+
|
|
593
|
+
if len(self.header_data) != 10:
|
|
594
|
+
raise ValueError(f"The length of the header for a data packet shall be 10 bytes, "
|
|
595
|
+
f"got {len(self.header_data)}.")
|
|
596
|
+
|
|
597
|
+
self.n_fee_side = GlobalState.setup.camera.fee.ccd_sides.enum
|
|
598
|
+
|
|
599
|
+
def data_as_bytes(self) -> bytes:
|
|
600
|
+
"""Returns the full header as a bytes object."""
|
|
601
|
+
return bytes(self.header_data)
|
|
602
|
+
|
|
603
|
+
@property
|
|
604
|
+
def logical_address(self) -> int:
|
|
605
|
+
return self.header_data[0]
|
|
606
|
+
|
|
607
|
+
@logical_address.setter
|
|
608
|
+
def logical_address(self, value: int):
|
|
609
|
+
self.header_data[0] = value
|
|
610
|
+
|
|
611
|
+
@property
|
|
612
|
+
def protocol_id(self) -> int:
|
|
613
|
+
return self.header_data[1]
|
|
614
|
+
|
|
615
|
+
@protocol_id.setter
|
|
616
|
+
def protocol_id(self, value: int):
|
|
617
|
+
self.header_data[1] = value
|
|
618
|
+
|
|
619
|
+
@property
|
|
620
|
+
def length(self) -> int:
|
|
621
|
+
return int.from_bytes(self.header_data[2:4], byteorder='big')
|
|
622
|
+
|
|
623
|
+
@length.setter
|
|
624
|
+
def length(self, value: int):
|
|
625
|
+
self.header_data[2:4] = value.to_bytes(2, 'big')
|
|
626
|
+
|
|
627
|
+
@property
|
|
628
|
+
def type(self):
|
|
629
|
+
return int.from_bytes(self.header_data[4:6], byteorder='big')
|
|
630
|
+
|
|
631
|
+
@type.setter
|
|
632
|
+
def type(self, value: Union[int, bytes, DataPacketType]):
|
|
633
|
+
if isinstance(value, bytes):
|
|
634
|
+
self.header_data[4:6] = value
|
|
635
|
+
elif isinstance(value, DataPacketType):
|
|
636
|
+
self.header_data[4:6] = value.value.to_bytes(2, 'big')
|
|
637
|
+
else:
|
|
638
|
+
self.header_data[4:6] = value.to_bytes(2, 'big')
|
|
639
|
+
|
|
640
|
+
@property
|
|
641
|
+
def type_as_object(self):
|
|
642
|
+
return DataPacketType(self.type)
|
|
643
|
+
|
|
644
|
+
@property
|
|
645
|
+
def packet_type(self):
|
|
646
|
+
return self.type_as_object.packet_type
|
|
647
|
+
|
|
648
|
+
@packet_type.setter
|
|
649
|
+
def packet_type(self, value: int):
|
|
650
|
+
type_obj = self.type_as_object
|
|
651
|
+
type_obj.packet_type = value
|
|
652
|
+
self.type = type_obj
|
|
653
|
+
|
|
654
|
+
@property
|
|
655
|
+
def last_packet(self):
|
|
656
|
+
return self.type_as_object.last_packet
|
|
657
|
+
|
|
658
|
+
@last_packet.setter
|
|
659
|
+
def last_packet(self, flag: bool):
|
|
660
|
+
type_obj = self.type_as_object
|
|
661
|
+
type_obj.last_packet = flag
|
|
662
|
+
self.type = type_obj
|
|
663
|
+
|
|
664
|
+
@property
|
|
665
|
+
def frame_counter(self):
|
|
666
|
+
return int.from_bytes(self.header_data[6:8], byteorder='big')
|
|
667
|
+
|
|
668
|
+
@frame_counter.setter
|
|
669
|
+
def frame_counter(self, value):
|
|
670
|
+
self.header_data[6:8] = value.to_bytes(2, 'big')
|
|
671
|
+
|
|
672
|
+
@property
|
|
673
|
+
def sequence_counter(self):
|
|
674
|
+
return int.from_bytes(self.header_data[8:10], byteorder='big')
|
|
675
|
+
|
|
676
|
+
@sequence_counter.setter
|
|
677
|
+
def sequence_counter(self, value):
|
|
678
|
+
self.header_data[8:10] = value.to_bytes(2, 'big')
|
|
679
|
+
|
|
680
|
+
def as_dict(self):
|
|
681
|
+
from egse.fee import n_fee_mode
|
|
682
|
+
|
|
683
|
+
data_packet_type = DataPacketType(self.type)
|
|
684
|
+
return dict(
|
|
685
|
+
logical_address=f"0x{self.logical_address:02X}",
|
|
686
|
+
protocol_id=f"0x{self.protocol_id:02X}",
|
|
687
|
+
length=self.length,
|
|
688
|
+
type=f"0x{self.type:04X}",
|
|
689
|
+
frame_counter=self.frame_counter,
|
|
690
|
+
sequence_counter=self.sequence_counter,
|
|
691
|
+
packet_type=data_packet_type.packet_type,
|
|
692
|
+
frame_number=data_packet_type.frame_number,
|
|
693
|
+
ccd_number=data_packet_type.ccd_number,
|
|
694
|
+
ccd_side=self.n_fee_side(data_packet_type.ccd_side).name,
|
|
695
|
+
last_packet=data_packet_type.last_packet,
|
|
696
|
+
mode=n_fee_mode(data_packet_type.mode).name,
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
class SpaceWirePacket:
|
|
701
|
+
"""Base class for any packet transmitted over a SpaceWire cable."""
|
|
702
|
+
|
|
703
|
+
# these settings are used by this class and its sub-classes to configure the print options
|
|
704
|
+
# for the numpy arrays.
|
|
705
|
+
|
|
706
|
+
_threshold = 300 # sys.maxsize
|
|
707
|
+
_edgeitems = 10
|
|
708
|
+
_linewidth = 120
|
|
709
|
+
|
|
710
|
+
def __init__(self, data: Union[bytes, np.ndarray]):
|
|
711
|
+
"""
|
|
712
|
+
Args:
|
|
713
|
+
data: a bytes object or a numpy array of type np.uint8 (not enforced)
|
|
714
|
+
"""
|
|
715
|
+
self._bytes = bytes(data)
|
|
716
|
+
|
|
717
|
+
def __repr__(self):
|
|
718
|
+
options = np.get_printoptions()
|
|
719
|
+
np.set_printoptions(
|
|
720
|
+
formatter={"int": lambda x: f"0x{x:02x}"},
|
|
721
|
+
threshold=self._threshold,
|
|
722
|
+
edgeitems=self._edgeitems,
|
|
723
|
+
linewidth=self._linewidth,
|
|
724
|
+
)
|
|
725
|
+
msg = f"{self.__class__.__name__}({self._bytes})"
|
|
726
|
+
np.set_printoptions(**options)
|
|
727
|
+
return msg
|
|
728
|
+
|
|
729
|
+
@property
|
|
730
|
+
def packet_as_bytes(self):
|
|
731
|
+
return self._bytes
|
|
732
|
+
|
|
733
|
+
@property
|
|
734
|
+
def packet_as_ndarray(self):
|
|
735
|
+
return np.frombuffer(self._bytes, dtype=np.uint8)
|
|
736
|
+
|
|
737
|
+
@property
|
|
738
|
+
def logical_address(self):
|
|
739
|
+
# TODO: what about a timecode, that has no logical address?
|
|
740
|
+
return self._bytes[0]
|
|
741
|
+
|
|
742
|
+
@property
|
|
743
|
+
def protocol_id(self):
|
|
744
|
+
# TODO: what about a timecode, that has no protocol id?
|
|
745
|
+
return self._bytes[1]
|
|
746
|
+
|
|
747
|
+
def header_as_bytes(self) -> bytes:
|
|
748
|
+
# TODO: what about timecode, this has no header, except maybe the first byte: 0x91
|
|
749
|
+
raise NotImplementedError
|
|
750
|
+
|
|
751
|
+
@staticmethod
|
|
752
|
+
def create_packet(data: Union[bytes, np.ndarray]):
|
|
753
|
+
"""
|
|
754
|
+
Factory method that returns a SpaceWire packet of the correct type based on the information
|
|
755
|
+
in the header.
|
|
756
|
+
"""
|
|
757
|
+
if TimecodePacket.is_timecode_packet(data):
|
|
758
|
+
return TimecodePacket(data)
|
|
759
|
+
if HousekeepingPacket.is_housekeeping_packet(data):
|
|
760
|
+
return HousekeepingPacket(data)
|
|
761
|
+
if DataDataPacket.is_data_data_packet(data):
|
|
762
|
+
return DataDataPacket(data)
|
|
763
|
+
if OverscanDataPacket.is_overscan_data_packet(data):
|
|
764
|
+
return OverscanDataPacket(data)
|
|
765
|
+
if WriteRequest.is_write_request(data):
|
|
766
|
+
return WriteRequest(data)
|
|
767
|
+
if WriteRequestReply.is_write_reply(data):
|
|
768
|
+
return WriteRequestReply(data)
|
|
769
|
+
if ReadRequest.is_read_request(data):
|
|
770
|
+
return ReadRequest(data)
|
|
771
|
+
if ReadRequestReply.is_read_reply(data):
|
|
772
|
+
return ReadRequestReply(data)
|
|
773
|
+
return SpaceWirePacket(data)
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
class DataPacket(SpaceWirePacket):
|
|
777
|
+
"""
|
|
778
|
+
Base class for proprietary SpaceWire data packets that are exchanged between FEE and DPU.
|
|
779
|
+
|
|
780
|
+
.. note::
|
|
781
|
+
This class should not be instantiated directly. Use the SpaceWirePacket.create_packet()
|
|
782
|
+
factory method or the constructors of one of the sub-classes of this DataPacket class.
|
|
783
|
+
"""
|
|
784
|
+
|
|
785
|
+
DATA_HEADER_LENGTH = 10
|
|
786
|
+
|
|
787
|
+
def __init__(self, data: Union[bytes, np.ndarray]):
|
|
788
|
+
"""
|
|
789
|
+
Args:
|
|
790
|
+
data: a bytes object or a numpy array
|
|
791
|
+
"""
|
|
792
|
+
if not self.is_data_packet(data):
|
|
793
|
+
raise ValueError(
|
|
794
|
+
f"Can not create a DataPacket from the given data {[f'0x{x:02x}' for x in data]}"
|
|
795
|
+
)
|
|
796
|
+
|
|
797
|
+
super().__init__(data)
|
|
798
|
+
|
|
799
|
+
if (data[2] == 0x00 and data[3] == 0x00) or len(data) == self.DATA_HEADER_LENGTH:
|
|
800
|
+
MODULE_LOGGER.warning(
|
|
801
|
+
f"SpaceWire data packet without data found, packet={[f'0x{x:02x}' for x in data]}"
|
|
802
|
+
)
|
|
803
|
+
|
|
804
|
+
self._length = (data[2] << 8) + data[3]
|
|
805
|
+
|
|
806
|
+
if len(data) != self._length + self.DATA_HEADER_LENGTH:
|
|
807
|
+
MODULE_LOGGER.warning(
|
|
808
|
+
f"The length of the data argument ({len(data)}) given to "
|
|
809
|
+
f"the constructor of {self.__class__.__name__} (or sub-classes) is inconsistent "
|
|
810
|
+
f"with the length data field ({self._length} + 10) in the packet header."
|
|
811
|
+
)
|
|
812
|
+
raise ValueError(
|
|
813
|
+
f"{self.__class__.__name__} header: data-length field ({self._length}) not "
|
|
814
|
+
f"consistent with packet length ({len(data)}). Difference should be "
|
|
815
|
+
f"{self.DATA_HEADER_LENGTH}."
|
|
816
|
+
)
|
|
817
|
+
|
|
818
|
+
self._type = DataPacketType((data[4] << 8) + data[5])
|
|
819
|
+
self._data = None # lazy loading of data from self._bytes
|
|
820
|
+
|
|
821
|
+
@property
|
|
822
|
+
def length(self) -> int:
|
|
823
|
+
"""Returns the data length in bytes.
|
|
824
|
+
|
|
825
|
+
.. note:: length == len(data_nd_array) * 2
|
|
826
|
+
This length property returns the length of the data area in bytes. This value is
|
|
827
|
+
taken from the header of the data packet. If you want to compare this with the size
|
|
828
|
+
of the data_as_ndarray property, multiply the length by 2 because the data is 16-bit
|
|
829
|
+
integers, not bytes.
|
|
830
|
+
|
|
831
|
+
Returns:
|
|
832
|
+
the size of the data area of the packet in bytes.
|
|
833
|
+
"""
|
|
834
|
+
return self._length
|
|
835
|
+
|
|
836
|
+
@property
|
|
837
|
+
def data_as_ndarray(self):
|
|
838
|
+
"""
|
|
839
|
+
Returns the data from this data packet as a 16-bit integer Numpy array.
|
|
840
|
+
|
|
841
|
+
.. note::
|
|
842
|
+
The data has been converted from the 8-bit packet data into 16-bit integers. That
|
|
843
|
+
means the length of this data array will be half the length of the data field the
|
|
844
|
+
packet, i.e. ``len(data) == length // 2``.
|
|
845
|
+
The reason for this is that pixel data has a size of 16-bit.
|
|
846
|
+
|
|
847
|
+
.. todo::
|
|
848
|
+
check if the data-length of HK packets should also be a multiple of 16.
|
|
849
|
+
|
|
850
|
+
Returns:
|
|
851
|
+
data: Numpy array with the data from this packet (type is np.uint16)
|
|
852
|
+
|
|
853
|
+
"""
|
|
854
|
+
|
|
855
|
+
# We decided to lazy load/construct the data array. The reason is that the packet may be
|
|
856
|
+
# created / transferred without the need to unpack the data field into a 16-bit numpy array.
|
|
857
|
+
|
|
858
|
+
if self._data is None:
|
|
859
|
+
# The data is in two's-complement. The most significant bit (msb) shall be inverted
|
|
860
|
+
# according to Sampie Smit. That is done in the following line where the msb in each
|
|
861
|
+
# byte on an even index is inverted.
|
|
862
|
+
|
|
863
|
+
# data = [toggle_bit(b, 7) if not idx % 2 else b for idx, b in enumerate(self._bytes)]
|
|
864
|
+
# data = bytearray(data)
|
|
865
|
+
# data_1 = np.frombuffer(data, offset=10, dtype='>u2')
|
|
866
|
+
|
|
867
|
+
# Needs further confirmation, but the following line should have the same effect as
|
|
868
|
+
# the previous three lines.
|
|
869
|
+
data_2 = np.frombuffer(self._bytes, offset=10, dtype='>i2') + TWOS_COMPLEMENT_OFFSET
|
|
870
|
+
|
|
871
|
+
# Test if the results are identical, left the code in until we are fully confident
|
|
872
|
+
# if diff := np.sum(np.cumsum(data_1 - data_2)):
|
|
873
|
+
# MODULE_LOGGER.info(f"cumsum={diff}")
|
|
874
|
+
|
|
875
|
+
self._data = data_2.astype('uint16')
|
|
876
|
+
return self._data
|
|
877
|
+
|
|
878
|
+
@property
|
|
879
|
+
def data(self) -> bytes:
|
|
880
|
+
return self._bytes[10: 10 + self._length]
|
|
881
|
+
|
|
882
|
+
@property
|
|
883
|
+
def type(self) -> DataPacketType:
|
|
884
|
+
return self._type
|
|
885
|
+
|
|
886
|
+
@property
|
|
887
|
+
def frame_counter(self):
|
|
888
|
+
return (self._bytes[6] << 8) + self._bytes[7]
|
|
889
|
+
|
|
890
|
+
@property
|
|
891
|
+
def sequence_counter(self):
|
|
892
|
+
return (self._bytes[8] << 8) + self._bytes[9]
|
|
893
|
+
|
|
894
|
+
@property
|
|
895
|
+
def header(self) -> DataPacketHeader:
|
|
896
|
+
return DataPacketHeader(self.header_as_bytes())
|
|
897
|
+
|
|
898
|
+
def header_as_bytes(self):
|
|
899
|
+
return self._bytes[:10]
|
|
900
|
+
|
|
901
|
+
@classmethod
|
|
902
|
+
def is_data_packet(cls, data: np.ndarray) -> bool:
|
|
903
|
+
if len(data) < 10 or data[0] != 0x50 or data[1] != 0xF0:
|
|
904
|
+
return False
|
|
905
|
+
return True
|
|
906
|
+
|
|
907
|
+
def __str__(self):
|
|
908
|
+
options = np.get_printoptions()
|
|
909
|
+
np.set_printoptions(
|
|
910
|
+
formatter={"int": lambda x: f"0x{x:04x}"},
|
|
911
|
+
threshold=super()._threshold,
|
|
912
|
+
edgeitems=super()._edgeitems,
|
|
913
|
+
linewidth=super()._linewidth,
|
|
914
|
+
)
|
|
915
|
+
msg = (
|
|
916
|
+
f"{self.__class__.__name__}:\n"
|
|
917
|
+
f" Logical Address = 0x{self.logical_address:02X}\n"
|
|
918
|
+
f" Protocol ID = 0x{self.protocol_id:02X}\n"
|
|
919
|
+
f" Length = {self.length}\n"
|
|
920
|
+
f" Type = {self._type}\n"
|
|
921
|
+
f" Frame Counter = {self.frame_counter}\n"
|
|
922
|
+
f" Sequence Counter = {self.sequence_counter}\n"
|
|
923
|
+
f" Data = \n{self.data}"
|
|
924
|
+
)
|
|
925
|
+
np.set_printoptions(**options)
|
|
926
|
+
return msg
|
|
927
|
+
|
|
928
|
+
|
|
929
|
+
class DataDataPacket(DataPacket):
|
|
930
|
+
"""Proprietary Data Packet for N-FEE and F-FEE CCD image data."""
|
|
931
|
+
|
|
932
|
+
@classmethod
|
|
933
|
+
def is_data_data_packet(cls, data: Union[bytes, np.ndarray]) -> bool:
|
|
934
|
+
if len(data) <= 10:
|
|
935
|
+
return False
|
|
936
|
+
if data[0] != 0x50:
|
|
937
|
+
return False
|
|
938
|
+
if data[1] != 0xF0:
|
|
939
|
+
return False
|
|
940
|
+
type_ = DataPacketType((data[4] << 8) + data[5])
|
|
941
|
+
if type_.packet_type == PacketType.DATA_PACKET:
|
|
942
|
+
return True
|
|
943
|
+
return False
|
|
944
|
+
|
|
945
|
+
|
|
946
|
+
class OverscanDataPacket(DataPacket):
|
|
947
|
+
"""Proprietary Overscan Data Packet for N-FEE and F-FEE CCD image data."""
|
|
948
|
+
|
|
949
|
+
@classmethod
|
|
950
|
+
def is_overscan_data_packet(cls, data: Union[bytes, np.ndarray]) -> bool:
|
|
951
|
+
if len(data) <= 10:
|
|
952
|
+
return False
|
|
953
|
+
if data[0] != 0x50:
|
|
954
|
+
return False
|
|
955
|
+
if data[1] != 0xF0:
|
|
956
|
+
return False
|
|
957
|
+
type_ = DataPacketType((data[4] << 8) + data[5])
|
|
958
|
+
if type_.packet_type == PacketType.OVERSCAN_DATA:
|
|
959
|
+
return True
|
|
960
|
+
return False
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
class HousekeepingPacket(DataPacket):
|
|
964
|
+
"""Proprietary Housekeeping data packet for the N-FEE and F-FEE."""
|
|
965
|
+
|
|
966
|
+
def __init__(self, data: Union[bytes, np.ndarray]):
|
|
967
|
+
"""
|
|
968
|
+
Args:
|
|
969
|
+
data: a numpy array of type np.uint8 (not enforced)
|
|
970
|
+
"""
|
|
971
|
+
if not self.is_housekeeping_packet(data):
|
|
972
|
+
raise ValueError(f"Can not create a HousekeepingPacket from the given data {data}")
|
|
973
|
+
|
|
974
|
+
# The __init__ method of DataPacket already checks e.g. data-length against packet length,
|
|
975
|
+
# so there is no need for these tests here.
|
|
976
|
+
|
|
977
|
+
super().__init__(data)
|
|
978
|
+
|
|
979
|
+
@classmethod
|
|
980
|
+
def is_housekeeping_packet(cls, data: Union[bytes, np.ndarray]) -> bool:
|
|
981
|
+
if len(data) <= 10:
|
|
982
|
+
return False
|
|
983
|
+
if data[0] != 0x50:
|
|
984
|
+
return False
|
|
985
|
+
if data[1] != 0xF0:
|
|
986
|
+
return False
|
|
987
|
+
type_ = DataPacketType((data[4] << 8) + data[5])
|
|
988
|
+
if type_.packet_type == PacketType.HOUSEKEEPING_DATA:
|
|
989
|
+
return True
|
|
990
|
+
return False
|
|
991
|
+
|
|
992
|
+
|
|
993
|
+
class TimecodePacket(SpaceWirePacket):
|
|
994
|
+
"""A Timecode Packet.
|
|
995
|
+
|
|
996
|
+
This packet really is an extended packet which is generated by the Diagnostic SpaceWire
|
|
997
|
+
Interface (DSI) to forward a SpaceWire timecode over the Ethernet connection.
|
|
998
|
+
"""
|
|
999
|
+
|
|
1000
|
+
def __init__(self, data: Union[bytes, np.ndarray]):
|
|
1001
|
+
super().__init__(data)
|
|
1002
|
+
|
|
1003
|
+
@property
|
|
1004
|
+
def timecode(self) -> int:
|
|
1005
|
+
return self._bytes[1] & 0x3F
|
|
1006
|
+
|
|
1007
|
+
def header_as_bytes(self) -> bytes:
|
|
1008
|
+
return self._bytes[0:1]
|
|
1009
|
+
|
|
1010
|
+
@classmethod
|
|
1011
|
+
def is_timecode_packet(cls, data: Union[bytes, np.ndarray]) -> bool:
|
|
1012
|
+
return data[0] == 0x91
|
|
1013
|
+
|
|
1014
|
+
def __str__(self):
|
|
1015
|
+
return f"Timecode Packet: timecode = 0x{self.timecode:x}"
|
|
1016
|
+
|
|
1017
|
+
|
|
1018
|
+
class RMAPPacket(SpaceWirePacket):
|
|
1019
|
+
"""Base class for RMAP SpaceWire packets."""
|
|
1020
|
+
|
|
1021
|
+
def __init__(self, data: Union[bytes, np.ndarray]):
|
|
1022
|
+
if not self.is_rmap_packet(data):
|
|
1023
|
+
raise ValueError(f"Can not create a RMAPPacket from the given data {data}")
|
|
1024
|
+
super().__init__(data)
|
|
1025
|
+
|
|
1026
|
+
def __str__(self):
|
|
1027
|
+
return (
|
|
1028
|
+
f"{self.__class__.__name__}:\n"
|
|
1029
|
+
f" Logical Address = 0x{self.logical_address:02X}\n"
|
|
1030
|
+
f" Data = {self.data}\n"
|
|
1031
|
+
)
|
|
1032
|
+
|
|
1033
|
+
@property
|
|
1034
|
+
def instruction(self):
|
|
1035
|
+
return get_instruction_field(self._bytes)
|
|
1036
|
+
|
|
1037
|
+
@property
|
|
1038
|
+
def transaction_id(self):
|
|
1039
|
+
return get_transaction_identifier(self._bytes)
|
|
1040
|
+
|
|
1041
|
+
@classmethod
|
|
1042
|
+
def is_rmap_packet(cls, data: Union[bytes, np.ndarray]):
|
|
1043
|
+
if data[1] == 0x01: # Protocol ID
|
|
1044
|
+
return True
|
|
1045
|
+
return False
|
|
1046
|
+
|
|
1047
|
+
|
|
1048
|
+
class WriteRequest(RMAPPacket):
|
|
1049
|
+
"""A Write Request SpaceWire RMAP Packet."""
|
|
1050
|
+
|
|
1051
|
+
def __init__(self, data: Union[bytes, np.ndarray]):
|
|
1052
|
+
super().__init__(data)
|
|
1053
|
+
|
|
1054
|
+
def is_verified(self):
|
|
1055
|
+
return self._bytes[2] == 0x7C
|
|
1056
|
+
|
|
1057
|
+
def is_unverified(self):
|
|
1058
|
+
return self._bytes[2] == 0x6C
|
|
1059
|
+
|
|
1060
|
+
@property
|
|
1061
|
+
def address(self):
|
|
1062
|
+
return get_address(self._bytes)
|
|
1063
|
+
|
|
1064
|
+
@property
|
|
1065
|
+
def data_length(self):
|
|
1066
|
+
return get_data_length(self._bytes)
|
|
1067
|
+
|
|
1068
|
+
@property
|
|
1069
|
+
def data(self) -> bytes:
|
|
1070
|
+
return get_data(self._bytes)
|
|
1071
|
+
|
|
1072
|
+
@classmethod
|
|
1073
|
+
def is_write_request(cls, data: Union[bytes, np.ndarray]):
|
|
1074
|
+
if not RMAPPacket.is_rmap_packet(data):
|
|
1075
|
+
return False
|
|
1076
|
+
if data[0] != 0x51:
|
|
1077
|
+
return False
|
|
1078
|
+
if (data[2] == 0x7C or data[2] == 0x6C) and data[3] == 0xD1:
|
|
1079
|
+
return True
|
|
1080
|
+
return False
|
|
1081
|
+
|
|
1082
|
+
def __str__(self):
|
|
1083
|
+
prefix = "Verified" if self.is_verified() else "Unverified"
|
|
1084
|
+
return f"{prefix} Write Request: {self.transaction_id=}, data=0x{self.data.hex()}"
|
|
1085
|
+
|
|
1086
|
+
|
|
1087
|
+
class WriteRequestReply(RMAPPacket):
|
|
1088
|
+
"""An RMAP Reply packet to a Write Request."""
|
|
1089
|
+
|
|
1090
|
+
def __init__(self, data: Union[bytes, np.ndarray]):
|
|
1091
|
+
super().__init__(data)
|
|
1092
|
+
self._status = data[3]
|
|
1093
|
+
|
|
1094
|
+
@classmethod
|
|
1095
|
+
def is_write_reply(cls, data: Union[bytes, np.ndarray]):
|
|
1096
|
+
if not RMAPPacket.is_rmap_packet(data):
|
|
1097
|
+
return False
|
|
1098
|
+
if data[0] != 0x50:
|
|
1099
|
+
return False
|
|
1100
|
+
if (data[2] == 0x3C or data[2] == 0x2C) and data[4] == 0x51:
|
|
1101
|
+
return True
|
|
1102
|
+
|
|
1103
|
+
@property
|
|
1104
|
+
def status(self):
|
|
1105
|
+
return self._status
|
|
1106
|
+
|
|
1107
|
+
def __str__(self):
|
|
1108
|
+
return f"Write Request Reply: status={self.status}"
|
|
1109
|
+
|
|
1110
|
+
|
|
1111
|
+
class ReadRequest(RMAPPacket):
|
|
1112
|
+
"""A Read Request SpaceWire RMAP Packet."""
|
|
1113
|
+
|
|
1114
|
+
def __init__(self, data: Union[bytes, np.ndarray]):
|
|
1115
|
+
super().__init__(data)
|
|
1116
|
+
|
|
1117
|
+
@classmethod
|
|
1118
|
+
def is_read_request(cls, data: Union[bytes, np.ndarray]):
|
|
1119
|
+
if not RMAPPacket.is_rmap_packet(data):
|
|
1120
|
+
return False
|
|
1121
|
+
if data[0] != 0x51:
|
|
1122
|
+
return False
|
|
1123
|
+
if data[2] == 0x4C and data[3] == 0xD1:
|
|
1124
|
+
return True
|
|
1125
|
+
return False
|
|
1126
|
+
|
|
1127
|
+
@property
|
|
1128
|
+
def address(self):
|
|
1129
|
+
return get_address(self._bytes)
|
|
1130
|
+
|
|
1131
|
+
@property
|
|
1132
|
+
def data_length(self):
|
|
1133
|
+
return get_data_length(self._bytes)
|
|
1134
|
+
|
|
1135
|
+
def __str__(self):
|
|
1136
|
+
return (
|
|
1137
|
+
f"Read Request: tid={self.transaction_id}, address=0x{self.address:04x}, "
|
|
1138
|
+
f"data length={self.data_length}"
|
|
1139
|
+
)
|
|
1140
|
+
|
|
1141
|
+
|
|
1142
|
+
class ReadRequestReply(RMAPPacket):
|
|
1143
|
+
"""An RMAP Reply packet to a Read Request."""
|
|
1144
|
+
|
|
1145
|
+
def __init__(self, data: Union[bytes, np.ndarray]):
|
|
1146
|
+
super().__init__(data)
|
|
1147
|
+
|
|
1148
|
+
@classmethod
|
|
1149
|
+
def is_read_reply(cls, data: Union[bytes, np.ndarray]):
|
|
1150
|
+
if not RMAPPacket.is_rmap_packet(data):
|
|
1151
|
+
return False
|
|
1152
|
+
if data[0] != 0x50:
|
|
1153
|
+
return False
|
|
1154
|
+
if data[2] == 0x0C and data[4] == 0x51:
|
|
1155
|
+
return True
|
|
1156
|
+
|
|
1157
|
+
@property
|
|
1158
|
+
def data(self) -> bytes:
|
|
1159
|
+
return get_data(self._bytes)
|
|
1160
|
+
|
|
1161
|
+
@property
|
|
1162
|
+
def data_length(self):
|
|
1163
|
+
return get_data_length(self._bytes)
|
|
1164
|
+
|
|
1165
|
+
def __str__(self):
|
|
1166
|
+
data_length = self.data_length
|
|
1167
|
+
return f"Read Request Reply: data length={data_length}, data={self.data[:20]} " \
|
|
1168
|
+
f"{'(data is cut to max 20 bytes)' if data_length > 20 else ''}\n"
|
|
1169
|
+
|
|
1170
|
+
|
|
1171
|
+
class SpaceWireInterface:
|
|
1172
|
+
"""
|
|
1173
|
+
This interface defines methods that are used by the DPU to communicate with the FEE over
|
|
1174
|
+
SpaceWire.
|
|
1175
|
+
"""
|
|
1176
|
+
|
|
1177
|
+
def __enter__(self):
|
|
1178
|
+
self.connect()
|
|
1179
|
+
|
|
1180
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
1181
|
+
self.disconnect()
|
|
1182
|
+
|
|
1183
|
+
def connect(self):
|
|
1184
|
+
raise NotImplementedError
|
|
1185
|
+
|
|
1186
|
+
def disconnect(self):
|
|
1187
|
+
raise NotImplementedError
|
|
1188
|
+
|
|
1189
|
+
def configure(self):
|
|
1190
|
+
raise NotImplementedError
|
|
1191
|
+
|
|
1192
|
+
def flush(self):
|
|
1193
|
+
raise NotImplementedError
|
|
1194
|
+
|
|
1195
|
+
def send_timecode(self, timecode: int):
|
|
1196
|
+
raise NotImplementedError
|
|
1197
|
+
|
|
1198
|
+
def read_packet(self, timeout: int = None) -> Tuple[int, bytes]:
|
|
1199
|
+
"""
|
|
1200
|
+
Read a full packet from the SpaceWire transport layer.
|
|
1201
|
+
|
|
1202
|
+
Args:
|
|
1203
|
+
timeout (int): timeout in milliseconds [default=None]
|
|
1204
|
+
Returns:
|
|
1205
|
+
A tuple with the terminator value and a bytes object containing the packet.
|
|
1206
|
+
"""
|
|
1207
|
+
raise NotImplementedError
|
|
1208
|
+
|
|
1209
|
+
def write_packet(self, packet: bytes):
|
|
1210
|
+
"""
|
|
1211
|
+
Write a full packet to the SpaceWire transport layer.
|
|
1212
|
+
|
|
1213
|
+
Args:
|
|
1214
|
+
packet (bytes): a bytes object containing the SpaceWire packet
|
|
1215
|
+
|
|
1216
|
+
Returns:
|
|
1217
|
+
None.
|
|
1218
|
+
"""
|
|
1219
|
+
raise NotImplementedError
|
|
1220
|
+
|
|
1221
|
+
def read_register(self, address: int, length: int = 4, strict: bool = True) -> bytes:
|
|
1222
|
+
"""
|
|
1223
|
+
Reads the data for the given register from the FEE memory map.
|
|
1224
|
+
|
|
1225
|
+
This function sends an RMAP read request for the register to the FEE.
|
|
1226
|
+
|
|
1227
|
+
Args:
|
|
1228
|
+
address: the start address (32-bit aligned) in the remote memory
|
|
1229
|
+
length: the number of bytes to read from the remote memory [default = 4]
|
|
1230
|
+
strict: perform strict checking of address and length
|
|
1231
|
+
|
|
1232
|
+
Returns:
|
|
1233
|
+
data: the 32-bit data that was read from the FEE.
|
|
1234
|
+
"""
|
|
1235
|
+
raise NotImplementedError
|
|
1236
|
+
|
|
1237
|
+
def write_register(self, address: int, data: bytes):
|
|
1238
|
+
"""
|
|
1239
|
+
Writes the data from the given register to the N-FEE memory map.
|
|
1240
|
+
|
|
1241
|
+
The function reads the data for the registry from the local register map
|
|
1242
|
+
and then sends an RMAP write request for the register to the N-FEE.
|
|
1243
|
+
|
|
1244
|
+
.. note:: it is assumed that the local register map is up-to-date.
|
|
1245
|
+
|
|
1246
|
+
Args:
|
|
1247
|
+
address: the start address (32-bit aligned) in the remote memory
|
|
1248
|
+
data: the data that will be written into the remote memory
|
|
1249
|
+
|
|
1250
|
+
Raises:
|
|
1251
|
+
RMAPError: when data can not be written on the target, i.e. the N-FEE.
|
|
1252
|
+
"""
|
|
1253
|
+
|
|
1254
|
+
raise NotImplementedError
|
|
1255
|
+
|
|
1256
|
+
def read_memory_map(self, address: int, size: int):
|
|
1257
|
+
"""
|
|
1258
|
+
Read (part of) the memory map from the N-FEE.
|
|
1259
|
+
|
|
1260
|
+
Args:
|
|
1261
|
+
address: start address
|
|
1262
|
+
size: number of bytes to read
|
|
1263
|
+
|
|
1264
|
+
Returns:
|
|
1265
|
+
a bytes object containing the requested memory map.
|
|
1266
|
+
"""
|
|
1267
|
+
|
|
1268
|
+
raise NotImplementedError
|
|
1269
|
+
|
|
1270
|
+
|
|
1271
|
+
# General RMAP helper functions ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
1272
|
+
|
|
1273
|
+
def rmap_crc_check(data, start, length) -> int:
|
|
1274
|
+
"""Calculate the checksum for the given data."""
|
|
1275
|
+
return crc_calc(data, start, length)
|
|
1276
|
+
|
|
1277
|
+
|
|
1278
|
+
def get_protocol_id(data: bytes) -> int:
|
|
1279
|
+
"""
|
|
1280
|
+
Returns the protocol identifier field. The protocol ID is 1 (0x01) for the RMAP protocol.
|
|
1281
|
+
"""
|
|
1282
|
+
return data[1]
|
|
1283
|
+
|
|
1284
|
+
|
|
1285
|
+
def get_reply_address_field_length(rx_buffer) -> int:
|
|
1286
|
+
"""Returns the size of reply address field.
|
|
1287
|
+
|
|
1288
|
+
This function returns the actual size of the reply address field. It doesn't return
|
|
1289
|
+
the content of the reply address length field. If you need that information, use the
|
|
1290
|
+
reply_address_length() function that work on the instruction field.
|
|
1291
|
+
|
|
1292
|
+
Returns:
|
|
1293
|
+
length: the size of the reply address field.
|
|
1294
|
+
"""
|
|
1295
|
+
instruction = get_instruction_field(rx_buffer)
|
|
1296
|
+
return reply_address_length(instruction) * 4
|
|
1297
|
+
|
|
1298
|
+
|
|
1299
|
+
def get_data(rxbuf) -> bytes:
|
|
1300
|
+
"""
|
|
1301
|
+
Return the data from the RMAP packet.
|
|
1302
|
+
|
|
1303
|
+
Raises:
|
|
1304
|
+
ValueError: if there is no data section in the packet (TODO: not yet implemented)
|
|
1305
|
+
"""
|
|
1306
|
+
instruction_field = get_instruction_field(rxbuf)
|
|
1307
|
+
address_length = get_reply_address_field_length(rxbuf)
|
|
1308
|
+
data_length = get_data_length(rxbuf)
|
|
1309
|
+
|
|
1310
|
+
offset = 12 if is_read(instruction_field) else 16
|
|
1311
|
+
|
|
1312
|
+
return rxbuf[offset + address_length:offset + address_length + data_length]
|
|
1313
|
+
|
|
1314
|
+
|
|
1315
|
+
def check_data_crc(rxbuf):
|
|
1316
|
+
instruction_field = get_instruction_field(rxbuf)
|
|
1317
|
+
address_length = get_reply_address_field_length(rxbuf)
|
|
1318
|
+
data_length = get_data_length(rxbuf)
|
|
1319
|
+
|
|
1320
|
+
offset = 12 if is_read(instruction_field) else 16
|
|
1321
|
+
idx = offset + address_length
|
|
1322
|
+
|
|
1323
|
+
d_crc = rxbuf[idx + data_length]
|
|
1324
|
+
c_crc = rmap_crc_check(rxbuf, idx, data_length) & 0xFF
|
|
1325
|
+
if d_crc != c_crc:
|
|
1326
|
+
raise CheckError(
|
|
1327
|
+
f"Data CRC doesn't match calculated CRC, d_crc=0x{d_crc:02X} & c_crc=0x{c_crc:02X}",
|
|
1328
|
+
RMAP_GENERAL_ERROR
|
|
1329
|
+
)
|
|
1330
|
+
|
|
1331
|
+
|
|
1332
|
+
def check_header_crc(rxbuf):
|
|
1333
|
+
instruction_field = get_instruction_field(rxbuf)
|
|
1334
|
+
if is_command(instruction_field):
|
|
1335
|
+
offset = 15
|
|
1336
|
+
elif is_write(instruction_field):
|
|
1337
|
+
offset = 7
|
|
1338
|
+
else:
|
|
1339
|
+
offset = 11
|
|
1340
|
+
|
|
1341
|
+
idx = offset + get_reply_address_field_length(rxbuf)
|
|
1342
|
+
h_crc = rxbuf[idx]
|
|
1343
|
+
c_crc = rmap_crc_check(rxbuf, 0, idx)
|
|
1344
|
+
if h_crc != c_crc:
|
|
1345
|
+
raise CheckError(
|
|
1346
|
+
f"Header CRC doesn't match calculated CRC, h_crc=0x{h_crc:02X} & c_crc=0x{c_crc:02X}",
|
|
1347
|
+
RMAP_GENERAL_ERROR
|
|
1348
|
+
)
|
|
1349
|
+
|
|
1350
|
+
|
|
1351
|
+
def get_data_length(rxbuf) -> int:
|
|
1352
|
+
"""
|
|
1353
|
+
Returns the length of the data in bytes.
|
|
1354
|
+
|
|
1355
|
+
Raises:
|
|
1356
|
+
TypeError: when this method is used on a Write Request Reply packet (which has no
|
|
1357
|
+
data length).
|
|
1358
|
+
"""
|
|
1359
|
+
instruction_field = get_instruction_field(rxbuf)
|
|
1360
|
+
|
|
1361
|
+
if not is_command(instruction_field) and is_write(instruction_field):
|
|
1362
|
+
raise TypeError("There is no data length field for Write Request Reply packets, "
|
|
1363
|
+
"asking for the data length is an invalid operation.")
|
|
1364
|
+
|
|
1365
|
+
offset = 12 if is_command(instruction_field) else 8
|
|
1366
|
+
idx = offset + get_reply_address_field_length(rxbuf)
|
|
1367
|
+
|
|
1368
|
+
# We could use two alternative decoding methods here:
|
|
1369
|
+
# int.from_bytes(rxbuf[idx:idx+3], byteorder='big') (timeit=1.166s)
|
|
1370
|
+
# struct.unpack('>L', b'\x00' + rxbuf[idx:idx+3])[0] (timeit=0.670s)
|
|
1371
|
+
data_length = struct.unpack('>L', b'\x00' + rxbuf[idx:idx + 3])[0]
|
|
1372
|
+
return data_length
|
|
1373
|
+
|
|
1374
|
+
|
|
1375
|
+
def get_address(rxbuf) -> int:
|
|
1376
|
+
"""
|
|
1377
|
+
Returns the address field (including the extended address field if the address is 40-bits).
|
|
1378
|
+
|
|
1379
|
+
Raises:
|
|
1380
|
+
TypeError: when this method is used on a Reply packet (which has no address field).
|
|
1381
|
+
"""
|
|
1382
|
+
instruction_field = get_instruction_field(rxbuf)
|
|
1383
|
+
|
|
1384
|
+
if not is_command(instruction_field):
|
|
1385
|
+
raise TypeError("There is no address field for Reply packets, asking for the address is "
|
|
1386
|
+
"an invalid operation.")
|
|
1387
|
+
|
|
1388
|
+
idx = 7 + get_reply_address_field_length(rxbuf)
|
|
1389
|
+
extended_address = rxbuf[idx]
|
|
1390
|
+
idx += 1
|
|
1391
|
+
address = struct.unpack('>L', rxbuf[idx:idx + 4])[0]
|
|
1392
|
+
if extended_address:
|
|
1393
|
+
address = address + (extended_address << 32)
|
|
1394
|
+
return address
|
|
1395
|
+
|
|
1396
|
+
|
|
1397
|
+
def get_instruction_field(rxbuf):
|
|
1398
|
+
idx = 2
|
|
1399
|
+
return rxbuf[idx]
|
|
1400
|
+
|
|
1401
|
+
|
|
1402
|
+
def get_transaction_identifier(rxbuf):
|
|
1403
|
+
idx = 5 + get_reply_address_field_length(rxbuf)
|
|
1404
|
+
tid = struct.unpack('>h', rxbuf[idx:idx + 2])[0]
|
|
1405
|
+
return tid
|
|
1406
|
+
|
|
1407
|
+
|
|
1408
|
+
# Functions to interpret the Instrument Field
|
|
1409
|
+
|
|
1410
|
+
def is_reserved(instruction):
|
|
1411
|
+
"""The reserved bit of the 2-bit packet type field from the instruction field.
|
|
1412
|
+
|
|
1413
|
+
For PLATO this bit shall be zero as the 0b10 and 0b11 packet field values are reserved.
|
|
1414
|
+
|
|
1415
|
+
Returns:
|
|
1416
|
+
bit value: 1 or 0.
|
|
1417
|
+
"""
|
|
1418
|
+
return (instruction & 0b10000000) >> 7
|
|
1419
|
+
|
|
1420
|
+
|
|
1421
|
+
def is_command(instruction):
|
|
1422
|
+
"""Returns True if the RMAP packet is a command packet."""
|
|
1423
|
+
return (instruction & 0b01000000) >> 6
|
|
1424
|
+
|
|
1425
|
+
|
|
1426
|
+
def is_reply(instruction):
|
|
1427
|
+
"""Returns True if the RMAP packet is a reply to a previous command packet."""
|
|
1428
|
+
return not is_command(instruction)
|
|
1429
|
+
|
|
1430
|
+
|
|
1431
|
+
def is_write(instruction):
|
|
1432
|
+
"""Returns True if the RMAP packet is a write request command packet."""
|
|
1433
|
+
return (instruction & 0b00100000) >> 5
|
|
1434
|
+
|
|
1435
|
+
|
|
1436
|
+
def is_read(instruction):
|
|
1437
|
+
"""Returns True if the RMAP packet is a read request command packet."""
|
|
1438
|
+
return not is_write(instruction)
|
|
1439
|
+
|
|
1440
|
+
|
|
1441
|
+
def is_verify(instruction):
|
|
1442
|
+
"""Returns True if the RMAP packet needs to do a verify before write."""
|
|
1443
|
+
return (instruction & 0b00010000) >> 4
|
|
1444
|
+
|
|
1445
|
+
|
|
1446
|
+
def is_reply_required(instruction):
|
|
1447
|
+
"""Returns True if the reply bit is set in the instruction field.
|
|
1448
|
+
|
|
1449
|
+
Args:
|
|
1450
|
+
instruction (int): the instruction field of an RMAP packet
|
|
1451
|
+
|
|
1452
|
+
.. note:: the name of this function might be confusing.
|
|
1453
|
+
|
|
1454
|
+
This function does **not** test if the packet is a reply packet, but it checks
|
|
1455
|
+
if the command requests a reply from the target. If you need to test if the
|
|
1456
|
+
packet is a command or a reply, use the is_command() or is_reply() function.
|
|
1457
|
+
|
|
1458
|
+
"""
|
|
1459
|
+
return (instruction & 0b00001000) >> 3
|
|
1460
|
+
|
|
1461
|
+
|
|
1462
|
+
def is_increment(instruction):
|
|
1463
|
+
"""Returns True if the data is written to sequential memory addresses."""
|
|
1464
|
+
return (instruction & 0b00000100) >> 2
|
|
1465
|
+
|
|
1466
|
+
|
|
1467
|
+
def reply_address_length(instruction):
|
|
1468
|
+
"""Returns the content of the reply address length field.
|
|
1469
|
+
|
|
1470
|
+
The size of the reply address field is then decoded from the following table:
|
|
1471
|
+
|
|
1472
|
+
Address Field Length | Size of Address Field
|
|
1473
|
+
----------------------+-----------------------
|
|
1474
|
+
0b00 | 0 bytes
|
|
1475
|
+
0b01 | 4 bytes
|
|
1476
|
+
0b10 | 8 bytes
|
|
1477
|
+
0b11 | 12 bytes
|
|
1478
|
+
|
|
1479
|
+
"""
|
|
1480
|
+
return (instruction & 0b00000011) << 2
|