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