ekfsm 0.11.0b1.post3__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.

Potentially problematic release.


This version of ekfsm might be problematic. Click here for more details.

@@ -0,0 +1,1054 @@
1
+ """
2
+ A module containing classes to represent EEPROM devices.
3
+
4
+ Routine Listings
5
+ ----------------
6
+ :py:class:`EEPROM`
7
+ :py:class:`Validatable_EEPROM`
8
+ :py:class:`EKF_EEPROM`
9
+ :py:class:`validated`
10
+ """
11
+
12
+ from abc import ABC, abstractmethod
13
+ from datetime import date
14
+ from typing import Any, Callable, Literal, Sequence
15
+ from functools import wraps
16
+
17
+ from ekfsm.core.components import SystemComponent
18
+ from ekfsm.core.probe import ProbeableDevice
19
+
20
+ from .generic import Device
21
+ from .utils import compute_int_from_bytes, get_crc16_xmodem
22
+ from ekfsm.exceptions import DataCorruptionError
23
+ from hexdump import hexdump
24
+
25
+ from ekfsm.log import ekfsm_logger
26
+
27
+ __all__ = ["EEPROM", "Validatable_EEPROM", "EKF_EEPROM", "validated"]
28
+
29
+ logger = ekfsm_logger(__name__)
30
+
31
+
32
+ def validated(func: Callable[..., Any]) -> Callable[..., Any]:
33
+ """
34
+ A decorator to validate the CRC of the EEPROM content before executing a method.
35
+
36
+ Parameters
37
+ ----------
38
+ func
39
+ The method to validate.
40
+
41
+ Note
42
+ ----
43
+ This decorator should be used on methods that read data from an EEPROM.
44
+ """
45
+
46
+ @wraps(func)
47
+ def validate(self, *args, **kwargs):
48
+ logger.debug(f"Validating EEPROM content for {self.name}")
49
+ if not self.valid:
50
+ raise DataCorruptionError("CRC validation failed")
51
+ logger.debug(f"EEPROM content is valid for {self.name}")
52
+ return func(self, *args, **kwargs)
53
+
54
+ return validate
55
+
56
+
57
+ class EEPROM(Device):
58
+ """
59
+ A class used to represent a generic EEPROM device.
60
+
61
+ Parameters
62
+ ----------
63
+ name
64
+ The name of the EEPROM device.
65
+ parent
66
+ The parent device of the EEPROM in the :py:class:`~.generic.Device` tree.
67
+
68
+
69
+ Caution
70
+ -------
71
+ The following conditions must be met for this class to work properly:
72
+ - EEPROM must be I2C accessable
73
+ - EEPROM must have a sysfs device
74
+
75
+
76
+ Note
77
+ ----
78
+ This class should be inherited by classes representing specific EEPROM devices and defining custom storage schemes.
79
+ """
80
+
81
+ def __init__(
82
+ self,
83
+ name: str,
84
+ parent: SystemComponent | None = None,
85
+ *args,
86
+ **kwargs,
87
+ ):
88
+ super().__init__(name, parent, None, *args, **kwargs)
89
+
90
+ self.addr = self.get_i2c_chip_addr()
91
+ self.sysfs_device = self.get_i2c_sysfs_device(self.addr)
92
+ self._update_content()
93
+
94
+ def _update_content(self) -> None:
95
+ """
96
+ Update the content of the EEPROM device.
97
+
98
+
99
+ Note
100
+ ----
101
+ - This method should be called whenever the content of the EEPROM is updated (after each write op).
102
+ - Inheriting classes should call this method before updating their own attributes.
103
+ """
104
+ logger.debug("Reading data")
105
+ try:
106
+ data = self.read()
107
+ self._content = data
108
+ except Exception as e:
109
+ logger.error(f"Error reading data, {e}")
110
+ self._content = b""
111
+
112
+ def read(self) -> bytes:
113
+ """
114
+ Read the content of the EEPROM.
115
+
116
+ Returns
117
+ -------
118
+ The content of the EEPROM.
119
+
120
+ Raises
121
+ ------
122
+ FileNotFoundError
123
+ If the EEPROM sysfs file is not found.
124
+ RuntimeError
125
+ If the sysfs device is not found.
126
+ """
127
+ try:
128
+ if self.sysfs_device:
129
+ cnt = self.sysfs_device.read_attr_bytes("eeprom")
130
+ else:
131
+ raise RuntimeError("No sysfs device for EEPROM")
132
+ except FileNotFoundError:
133
+ raise FileNotFoundError("EEPROM not found")
134
+
135
+ return cnt
136
+
137
+ def write(self, data: bytes, offset: int = 0) -> None:
138
+ """
139
+ Write data to the EEPROM.
140
+
141
+ Parameters
142
+ ----------
143
+ data
144
+ The data to write to the EEPROM.
145
+ offset
146
+ The offset at which to start writing the data.
147
+
148
+ Raises
149
+ ------
150
+ RuntimeError
151
+ If the sysfs device is not found.
152
+ FileNotFoundError
153
+ If the EEPROM sysfs file is not found.
154
+ DataCorruptionError
155
+ If an error occurs during the write operation.
156
+
157
+ Note
158
+ ----
159
+ Operation is checked for data corruption by reading back the written data.
160
+ """
161
+ try:
162
+ if self.sysfs_device:
163
+ logger.info(f"Writing {len(data)} bytes to EEPROM at offset {offset}")
164
+ self.sysfs_device.write_attr("eeprom", data, offset)
165
+ else:
166
+ raise RuntimeError("No sysfs device for EEPROM")
167
+ except FileNotFoundError:
168
+ raise FileNotFoundError("EEPROM not found")
169
+
170
+ self._update_content()
171
+ written = self._content[offset : offset + len(data)]
172
+ if not written == data:
173
+ raise DataCorruptionError(
174
+ "Error during EEPROM write, data is not the same as read back"
175
+ )
176
+
177
+ def print(self):
178
+ hexdump(self._content)
179
+
180
+
181
+ class Validatable_EEPROM(EEPROM, ABC):
182
+ """
183
+ Abstract class used to represent an EEPROM device using CRC to validate its content.
184
+
185
+ Parameters
186
+ ----------
187
+ crc_pos
188
+ The position of the CRC value in the EEPROM content (`'start'` or `'end'`).
189
+ crc_length
190
+ The length of the CRC value in number of bytes (defaults to 2).
191
+
192
+ Note
193
+ ----
194
+ - Derived classes must implement a method to compute the CRC value of the EEPROM content.
195
+ - If the CRC position differs from the shipped schema `('start' | 'end')`,
196
+ the derived class must override the :meth:`~Validatable_EEPROM._update_content` method.
197
+ - Validity of individual content fields stored/returned by attributes, methods or properties
198
+ can be achieved by using the :py:func:`validated` decorator.
199
+
200
+ See Also
201
+ --------
202
+ Validatable_EEPROM._compute_crc : Method to compute the CRC value of the EEPROM content.
203
+ """
204
+
205
+ def __init__(
206
+ self,
207
+ crc_pos: Literal["start", "end"] = "end",
208
+ crc_length: int = 2,
209
+ *args,
210
+ **kwargs,
211
+ ) -> None:
212
+ self._crc_length: int = crc_length
213
+ self._crc_pos: str = crc_pos
214
+
215
+ super().__init__(*args, **kwargs)
216
+
217
+ self._crc_pos_start = len(self._data) if self._crc_pos == "end" else 0
218
+ self._crc_pos_end = self._crc_pos_start + self._crc_length
219
+
220
+ def _update_content(self) -> None:
221
+ """
222
+ Update the content of the EEPROM device (checksum excluded).
223
+ """
224
+ super()._update_content()
225
+
226
+ # Firmware data without CRC
227
+ self._data: bytes = (
228
+ self._content[self._crc_length :]
229
+ if self._crc_pos == "start"
230
+ else self._content[: -self._crc_length :]
231
+ )
232
+
233
+ def _update_crc(self) -> None:
234
+ """
235
+ Update the CRC value of the EEPROM content.
236
+ """
237
+ self.crc = self._compute_crc()
238
+
239
+ def _get_crc_value(self) -> int:
240
+ return int.from_bytes(
241
+ self._content[self._crc_pos_start : self._crc_pos_end], byteorder="little"
242
+ )
243
+
244
+ @property
245
+ def crc(self) -> int:
246
+ """
247
+ Gets or sets the CRC value of the EEPROM content.
248
+
249
+ Parameters
250
+ ----------
251
+ value: optional
252
+ The CRC value to write to the EEPROM.
253
+
254
+ Returns
255
+ -------
256
+ int
257
+ The CRC value currently stored in the EEPROM if used as *getter*.
258
+ None
259
+ If used as *setter*.
260
+
261
+
262
+ Caution
263
+ -------
264
+ The *setter* actually writes the CRC value to the EEPROM.
265
+
266
+
267
+ Warning
268
+ -------
269
+ The *setter* method should be used with caution as it can lead to data corruption if the CRC value is not correct!
270
+
271
+
272
+ Note
273
+ ----
274
+ The *setter* is usually triggered automatically after a successful write operation
275
+ and in most cases, there is no need to call it manually.
276
+ """
277
+ return self._get_crc_value()
278
+
279
+ @crc.setter
280
+ def crc(self, value: int) -> None:
281
+ crc_bytes = value.to_bytes(self._crc_length, byteorder="little")
282
+ logger.debug(f"Writing CRC value {value} to EEPROM")
283
+
284
+ try:
285
+ super().write(crc_bytes, self._crc_pos_start)
286
+ except Exception as e:
287
+ logger.error(f"Error writing CRC value to EEPROM, {e}")
288
+
289
+ # self._update_content()
290
+
291
+ @property
292
+ def valid(self) -> bool:
293
+ """
294
+ Checks if the EEPROM content is valid by comparing the stored CRC value with the computed CRC value.
295
+
296
+ Returns
297
+ -------
298
+ bool
299
+ `True` if the EEPROM content is valid, `False` otherwise.
300
+ """
301
+ return self._compute_crc() == self.crc
302
+
303
+ @abstractmethod
304
+ def _compute_crc(self) -> int:
305
+ """
306
+ This method should be implemented by derived classes to compute the CRC value of the EEPROM content.
307
+
308
+ Returns
309
+ -------
310
+ The computed CRC value.
311
+ """
312
+ pass
313
+
314
+ def write(self, data: bytes, offset: int = 0) -> None:
315
+ try:
316
+ super().write(data, offset)
317
+ self._update_crc()
318
+ except Exception as e:
319
+ logger.error(f"Error writing to EEPROM, {e}")
320
+
321
+
322
+ class EKF_EEPROM(Validatable_EEPROM, ProbeableDevice):
323
+ """
324
+ A class used to represent an EKF EEPROM device.
325
+
326
+ Structure
327
+ ---------
328
+ The EKF_EEPROM content is structured as follows:
329
+
330
+ - `Serial number` (4 bytes, starts at pos 8):
331
+ The serial number of the device.
332
+ - `Manufactured at` (2 bytes, starts at pos 12):
333
+ The date the device was manufactured.
334
+ - `Repaired at` (2 bytes, starts at pos 14):
335
+ The date the device was repaired.
336
+ - `Customer serial number` (4 bytes, starts at pos 32):
337
+ The customer serial number of the device.
338
+ - `Customer configuration block offset pointer` (4 bytes, starts at pos 36):
339
+ The offset pointer to the customer configuration block.
340
+ - `String array` (78 bytes, starts at pos 48):
341
+ An array of strings containing the model, manufacturer, and custom board data of the device.
342
+ - `CRC` (2 bytes, starts at pos 126):
343
+ The CRC value of the EEPROM content.
344
+ - `Raw content` (80 bytes, starts at pos 128):
345
+ Free customizable content for other purposes.
346
+
347
+
348
+ Note
349
+ ----
350
+ - As the CRC value is stored at the end of the OEM data space, just before the customer configuration block,
351
+ the CRC position is manually set and the :meth:`~ekfsm.devices.eeprom.Validatable_EEPROM._update_content` method is
352
+ overridden.
353
+ - The CRC value is computed using the `CRC-16/XMODEM <https://en.wikipedia.org/wiki/Cyclic_redundancy_check>`_ algorithm.
354
+ - Dates are stored in a proprietary format (2 bytes) and must be decoded using the :meth:`~EKF_EEPROM._decode_date` method.
355
+
356
+
357
+ See Also
358
+ --------
359
+ `crcmod <https://crcmod.sourceforge.net/>`_
360
+
361
+
362
+ Important
363
+ ---------
364
+ All data read from the EEPROM should be validated using the @validated decorator.
365
+ This decorator ensures that the data is not corrupted by checking the CRC value.
366
+
367
+
368
+ Raises
369
+ ------
370
+ DataCorruptionError
371
+ If the CRC validation fails.
372
+ """
373
+
374
+ _sernum_index_start = 8
375
+ _sernum_index_end = 12
376
+
377
+ _date_mft_index_start = 12
378
+ _date_mft_index_end = 14
379
+
380
+ _date_rep_index_start = 14
381
+ _date_rep_index_end = 16
382
+
383
+ _customer_serial_index_start = 32
384
+ _customer_serial_index_end = 36
385
+
386
+ _customer_config_block_offset_pointer_index = 36
387
+
388
+ _str_array_start_offset = 48
389
+ _str_array_end_offset = 126
390
+
391
+ def __init__(
392
+ self,
393
+ *args,
394
+ **kwargs,
395
+ ) -> None:
396
+ super().__init__(*args, **kwargs)
397
+
398
+ def _update_content(self) -> None:
399
+
400
+ super()._update_content()
401
+
402
+ # EKF EEPROM content is restricted to 128 bytes, so strip the rest!
403
+ self._firmware_content = self._content[:128]
404
+
405
+ # The rest is raw content available for other purposes
406
+ self._raw_content: bytes = self._content[128:]
407
+
408
+ # Firmware data without CRC needs to be overriden
409
+ self._data: bytes = (
410
+ self._firmware_content[self._crc_length :]
411
+ if self._crc_pos == "start"
412
+ else self._firmware_content[: -self._crc_length :]
413
+ )
414
+ self._str_list = self._get_string_array()
415
+
416
+ self._crc_pos_start = len(self._data)
417
+ self._crc_pos_end = self._crc_pos_start + self._crc_length
418
+
419
+ @validated
420
+ def serial(self) -> str:
421
+ """
422
+ Get the serial number of the device to which the EEPROM is attached (the root device).
423
+
424
+ Returns
425
+ -------
426
+ The serial number of the root device.
427
+ """
428
+ area = self._content[self._sernum_index_start : self._sernum_index_end]
429
+ sernum = compute_int_from_bytes(area[::-1])
430
+ return str(sernum)
431
+
432
+ def write_serial(self, serial: int) -> None:
433
+ """
434
+ Write serial number of the root device to EEPROM.
435
+
436
+ Parameters
437
+ ----------
438
+ serial
439
+ The serial number to write to the EEPROM.
440
+ """
441
+ # Check serial number is within bounds
442
+ unsigned_upper_bound = (2**32) - 1
443
+ if serial < 0 or serial > unsigned_upper_bound:
444
+ raise ValueError(
445
+ f"Serial number must be between 0 and {unsigned_upper_bound}"
446
+ )
447
+ serial_bytes = serial.to_bytes(4, byteorder="little")
448
+ self.write(serial_bytes, self._sernum_index_start)
449
+
450
+ @validated
451
+ def custom_serial(self) -> str:
452
+ """
453
+ Get the customer serial number of the device to which the EEPROM is attached (the root device).
454
+
455
+ Attention
456
+ ---------
457
+ This is a custom - non-OEM - serial number that can be set by the user.
458
+
459
+ Returns
460
+ -------
461
+ The customer serial number of the root device.
462
+ """
463
+ area = self._content[
464
+ self._customer_serial_index_start : self._customer_serial_index_end
465
+ ]
466
+ sernum = compute_int_from_bytes(area[::-1])
467
+ return str(sernum)
468
+
469
+ def write_custom_serial(self, serial: int) -> None:
470
+ """
471
+ Write customer serial number of the root device to EEPROM.
472
+
473
+ Parameters
474
+ ----------
475
+ serial
476
+ The customer serial number to write to the EEPROM.
477
+
478
+
479
+ Raises
480
+ ------
481
+ ValueError
482
+ If the serial number is not within the bounds of a 32-bit unsigned integer.
483
+
484
+
485
+ Note
486
+ ----
487
+ Due to space restrictions on storage, the serial number must be a 32-bit unsigned integer.
488
+ """
489
+ # Check serial number is within bounds
490
+ unsigned_upper_bound = (2**32) - 1
491
+ if serial < 0 or serial > unsigned_upper_bound:
492
+ raise ValueError(
493
+ f"Serial number must be between 0 and {unsigned_upper_bound}"
494
+ )
495
+
496
+ serial_bytes = serial.to_bytes(4, byteorder="little")
497
+ logger.debug(f"Writing customer serial {serial}")
498
+ self.write(serial_bytes, self._customer_serial_index_start)
499
+
500
+ @validated
501
+ def manufactured_at(self) -> date:
502
+ """
503
+ Get the date the device was manufactured.
504
+
505
+ Returns
506
+ -------
507
+ The date the device was manufactured.
508
+ """
509
+ area = self._content[self._date_mft_index_start : self._date_mft_index_end]
510
+ encoded_mft_date = area[::-1]
511
+ return self._decode_date(encoded_mft_date)
512
+
513
+ @validated
514
+ def repaired_at(self) -> date:
515
+ """
516
+ Get the date the device was repaired.
517
+
518
+ Returns
519
+ -------
520
+ The most recent date the device was repaired.
521
+ """
522
+ area = self._content[self._date_rep_index_start : self._date_rep_index_end]
523
+ encoded_rep_date = area[::-1]
524
+ return self._decode_date(encoded_rep_date)
525
+
526
+ @validated
527
+ def write_repaired_at(self, date: date) -> None:
528
+ """
529
+ Write the date the device was repaired to EEPROM.
530
+
531
+ Parameters
532
+ ----------
533
+ date
534
+ The date the device was repaired.
535
+
536
+ Note
537
+ ----
538
+ The date year must be within the range of 1980-2079.
539
+
540
+ Attention
541
+ ---------
542
+ The date is stored in a proprietary 2-byte format.
543
+
544
+ Raises
545
+ ------
546
+ ValueError
547
+ If the year is not within the range of 1980-2079.
548
+ """
549
+ if date.year < 1980 or date.year > 2079:
550
+ raise ValueError("Year must be within the range of 1980-2079")
551
+ rep_date_bytes = self._encode_date(date)
552
+ logger.debug(f"Writing repair date {date} to EEPROM")
553
+ self.write(rep_date_bytes, self._date_rep_index_start)
554
+
555
+ @validated
556
+ def model(self) -> str | None:
557
+ """
558
+ Get the model name of the device to which the EEPROM is attached to (the root device).
559
+
560
+ Returns
561
+ -------
562
+ The model name of the device.
563
+ """
564
+ return self._str_list[0] if len(self._str_list) > 0 else None
565
+
566
+ @validated
567
+ def vendor(self) -> str | None:
568
+ """
569
+ Get the vendor/manufacturer of the device to which the EEPROM is attached to (the root device).
570
+
571
+ Returns
572
+ -------
573
+ The name of the vendor/manufacturer of the device.
574
+ """
575
+ return self._str_list[1] if len(self._str_list) > 1 else None
576
+
577
+ @validated
578
+ def custom_board_data(self) -> str | None:
579
+ """
580
+ Get the custom board data of the device.
581
+
582
+ Note
583
+ ----
584
+ This is a custom field that can be set by the user.
585
+
586
+
587
+ Attention
588
+ ---------
589
+ This field is optional and may not be present in the EEPROM content.
590
+
591
+
592
+ Returns
593
+ -------
594
+ The custom board data of the device as a string, or `None` if the field is not present.
595
+ """
596
+ return None if len(self._str_list) < 3 else self._str_list[2]
597
+
598
+ def write_custom_board_data(self, data: str) -> None:
599
+ """
600
+ Write custom board data to EEPROM.
601
+
602
+ Important
603
+ ---------
604
+ Due to size limitations, the custom board data should only contain expressive,
605
+ short content like serials, variants or specific codes.
606
+
607
+ Parameters
608
+ ----------
609
+ data
610
+ The custom board data to write to the EEPROM.
611
+
612
+ Attention
613
+ ---------
614
+ The model and vendor fields are mandatory and must be set before writing custom board data.
615
+
616
+ Raises
617
+ ------
618
+ ValueError
619
+ If the model and vendor fields are not set before writing custom board data.
620
+ """
621
+ data_bytes = data.encode("utf-8")
622
+ data_offset = 0
623
+ for s in self._str_list[:2]:
624
+ if s is None:
625
+ raise ValueError(
626
+ "Model and vendor fields must be set before writing custom board data"
627
+ )
628
+ if isinstance(s, str):
629
+ data_offset += len(s) + 1
630
+ logger.info(f"Writing custom board data {data} to EEPROM")
631
+ self.write(data_bytes, self._str_array_start_offset + data_offset)
632
+
633
+ def custom_raw_data(self) -> bytes:
634
+ """
635
+ Get the raw content area data stored in the EEPROM.
636
+
637
+ Returns
638
+ -------
639
+ The data contained in the raw content block of the EEPROM.
640
+
641
+
642
+ Note
643
+ ----
644
+ This area is free for custom data storage and is not included during crc calculations and validations.
645
+
646
+
647
+ Important
648
+ ---------
649
+ If custom raw data should be stored on EEPROM and
650
+ if it should be protected against corruption, it has to be validated manually.
651
+ """
652
+ return self._raw_content
653
+
654
+ def write_custom_raw_data(self, data: bytes) -> None:
655
+ """
656
+ Write custom data to the raw content area of the EEPROM.
657
+
658
+ Parameters
659
+ ----------
660
+ data
661
+ The data to write to the raw content area of the EEPROM.
662
+ """
663
+ logger.info(f"Writing {len(data)} bytes to raw content area of EEPROM")
664
+ self.write(data, 128)
665
+
666
+ def _get_string_array(self) -> list[str | None]:
667
+ str_array = self._content[
668
+ self._str_array_start_offset : self._str_array_end_offset
669
+ ].split(b"\x00")
670
+ try:
671
+ return [s.decode("utf-8") for s in str_array if s]
672
+ except UnicodeDecodeError:
673
+ return [None, None, None]
674
+
675
+ @classmethod
676
+ def _decode_date(cls, encoded_date: Sequence[int]) -> date:
677
+ """
678
+ Decode a date from a proprietary 2-byte format.
679
+
680
+ Parameters
681
+ ----------
682
+ encoded_date
683
+ The date to decode.
684
+
685
+ Raises
686
+ ------
687
+ ValueError
688
+ If the date is invalid (e.g., 30th Feb).
689
+
690
+ Returns
691
+ -------
692
+ date
693
+ The decoded date.
694
+ """
695
+ encoded_date = encoded_date[::-1]
696
+ bdate = compute_int_from_bytes(encoded_date)
697
+
698
+ # Extract the day (bit 0-4)
699
+ day = bdate & 0x1F # 0x1F is 00011111 in binary (5 bits)
700
+
701
+ # Extract the month (bit 5-8)
702
+ month = (bdate >> 5) & 0x0F # Shift right by 5 and mask with 0x0F (4 bits)
703
+
704
+ # Extract the year since 1980 (bit 9-15)
705
+ year = (bdate >> 9) & 0x7F # Shift right by 9 and mask with 0x7F (7 bits)
706
+ year += 1980 # Add base year (1980)
707
+
708
+ # Return a datetime object with the extracted year, month, and day
709
+ try:
710
+ decoded_date = date(year, month, day)
711
+ return decoded_date
712
+ except ValueError:
713
+ raise ValueError(
714
+ f"Invalid date: {day}/{month}/{year}"
715
+ ) # Handle invalid dates, e.g., 30th Feb
716
+
717
+ @classmethod
718
+ def _encode_date(self, date: date) -> bytes:
719
+ """
720
+ Encode a date into a proprietary 2-byte format.
721
+
722
+ Parameters
723
+ ----------
724
+ date
725
+ The date to encode.
726
+
727
+ Returns
728
+ -------
729
+ bytes
730
+ The encoded date.
731
+ """
732
+ year = date.year - 1980
733
+ month = date.month
734
+ day = date.day
735
+
736
+ encoded_date = year << 9 | month << 5 | day
737
+ return encoded_date.to_bytes(2, byteorder="little")
738
+
739
+ def _compute_crc(self) -> int:
740
+ return get_crc16_xmodem(self._data)
741
+
742
+ def probe(self, *args, **kwargs):
743
+ return self.root.id == self.model()
744
+
745
+
746
+ class EKF_CCU_EEPROM(EKF_EEPROM):
747
+ """
748
+ EKF CCU EEPROM - uses the second part of the EEPROM for chassis inventory and customer area
749
+ """
750
+
751
+ _cvendor_index_start = 128
752
+ _cvendor_length = 24
753
+
754
+ _cmodel_index_start = 152
755
+ _cmodel_length = 24
756
+
757
+ _crevision_index_start = 176
758
+ _crevision_length = 9
759
+
760
+ _cserial_index_start = 185
761
+ _cserial_length = 4
762
+
763
+ _unit_index_start = 189
764
+ _unit_length = 1
765
+
766
+ _customer_area_start = 190
767
+ _customer_area_length = 64
768
+
769
+ def __init__(
770
+ self,
771
+ *args,
772
+ **kwargs,
773
+ ):
774
+ super().__init__(*args, **kwargs)
775
+
776
+ def _update_content(self) -> None:
777
+
778
+ super()._update_content()
779
+
780
+ # CCU content is the raw content area of the EEPROM
781
+ self._ccu_content: bytes = self._raw_content
782
+
783
+ # CCU Firmware data without CRC needs to be overriden
784
+ self._cdata: bytes = (
785
+ self._ccu_content[self._crc_length :]
786
+ if self._crc_pos == "start"
787
+ else self._ccu_content[: -self._crc_length :]
788
+ )
789
+ self._ccrc_pos_start = len(self._cdata) + 128
790
+ self._ccrc_pos_end = self._ccrc_pos_start + self._crc_length
791
+
792
+ def _update_ccrc(self) -> None:
793
+ """
794
+ Update the CRC value of the EEPROM content.
795
+ """
796
+ self.ccrc = self._compute_ccrc()
797
+
798
+ def _get_ccrc_value(self) -> int:
799
+ return int.from_bytes(
800
+ self._content[self._ccrc_pos_start : self._ccrc_pos_end], byteorder="little"
801
+ )
802
+
803
+ @property
804
+ def ccrc(self) -> int:
805
+ """
806
+ Gets or sets the CRC value of the EEPROM content.
807
+
808
+ Parameters
809
+ ----------
810
+ value: optional
811
+ The CRC value to write to the EEPROM.
812
+
813
+ Returns
814
+ -------
815
+ int
816
+ The CRC value currently stored in the EEPROM if used as *getter*.
817
+ None
818
+ If used as *setter*.
819
+
820
+
821
+ Caution
822
+ -------
823
+ The *setter* actually writes the CRC value to the EEPROM.
824
+
825
+
826
+ Warning
827
+ -------
828
+ The *setter* method should be used with caution as it can lead to data corruption if the CRC value is not correct!
829
+
830
+
831
+ Note
832
+ ----
833
+ The *setter* is usually triggered automatically after a successful write operation
834
+ and in most cases, there is no need to call it manually.
835
+ """
836
+ return self._get_ccrc_value()
837
+
838
+ @ccrc.setter
839
+ def ccrc(self, value: int) -> None:
840
+ ccrc_bytes = value.to_bytes(self._crc_length, byteorder="little")
841
+ logger.debug(f"Writing chassis CRC value {value}")
842
+ try:
843
+ super(Validatable_EEPROM, self).write(ccrc_bytes, self._ccrc_pos_start)
844
+ except Exception as e:
845
+ logger.error(f"Error writing CRC value, error: {e}")
846
+
847
+ @property
848
+ def valid(self) -> bool:
849
+ """
850
+ Checks if the EEPROM content is valid by comparing the stored CRC value with the computed CRC value.
851
+
852
+ Returns
853
+ -------
854
+ bool
855
+ `True` if the EEPROM content is valid, `False` otherwise.
856
+ """
857
+ return self._compute_ccrc() == self.ccrc and super().valid
858
+
859
+ @validated
860
+ def cvendor(self) -> str:
861
+ """
862
+ Get the chassis vendor.
863
+
864
+ Returns
865
+ -------
866
+ The vendor of the chassis.
867
+ """
868
+ return (
869
+ self._content[
870
+ self._cvendor_index_start : self._cvendor_index_start
871
+ + self._cvendor_length
872
+ ]
873
+ .strip(b"\x00")
874
+ .decode("utf-8")
875
+ )
876
+
877
+ def write_cvendor(self, vendor: str) -> None:
878
+ """
879
+ Write the vendor of the chassis to EEPROM.
880
+
881
+ Parameters
882
+ ----------
883
+ vendor
884
+ The vendor of the chassis.
885
+ """
886
+ vendor_bytes = vendor.encode("utf-8")
887
+ vendor_fill = b"\x00" * (self._cvendor_length - len(vendor_bytes))
888
+ vendor_bytes += vendor_fill
889
+ logger.info(f"Writing vendor {vendor}")
890
+ self.write(vendor_bytes, self._cvendor_index_start)
891
+
892
+ @validated
893
+ def cmodel(self) -> str:
894
+ """
895
+ Get the chassis model.
896
+
897
+ Returns
898
+ -------
899
+ The model of the chassis.
900
+ """
901
+ return (
902
+ self._content[
903
+ self._cmodel_index_start : self._cmodel_index_start
904
+ + self._cmodel_length
905
+ ]
906
+ .strip(b"\x00")
907
+ .decode("utf-8")
908
+ )
909
+
910
+ def write_cmodel(self, model: str) -> None:
911
+ """
912
+ Write the model of the chassis to EEPROM.
913
+
914
+ Parameters
915
+ ----------
916
+ model
917
+ The model of the chassis.
918
+ """
919
+ model_bytes = model.encode("utf-8")
920
+ model_fill = b"\x00" * (self._cmodel_length - len(model_bytes))
921
+ model_bytes += model_fill
922
+ logger.info(f"Writing model {model}")
923
+ self.write(model_bytes, self._cmodel_index_start)
924
+
925
+ @validated
926
+ def cserial(self) -> int:
927
+ """
928
+ Get the chassis serial number.
929
+
930
+ Returns
931
+ -------
932
+ The serial number of the chassis.
933
+ """
934
+ area = self._content[
935
+ self._cserial_index_start : self._cserial_index_start + self._cserial_length
936
+ ]
937
+ cserial = compute_int_from_bytes(area[::-1])
938
+ return cserial
939
+
940
+ def write_cserial(self, serial: int) -> None:
941
+ """
942
+ Write the serial number of the chassis to EEPROM.
943
+
944
+ Parameters
945
+ ----------
946
+ serial
947
+ The serial number of the chassis.
948
+ """
949
+ # Check serial number is within bounds
950
+ unsigned_upper_bound = (2**32) - 1
951
+ if serial < 0 or serial > unsigned_upper_bound:
952
+ raise ValueError(
953
+ f"Serial number must be between 0 and {unsigned_upper_bound}"
954
+ )
955
+ serial_bytes = serial.to_bytes(4, byteorder="little")
956
+ logger.info(f"Writing chassis serial {serial}")
957
+ self.write(serial_bytes, self._cserial_index_start)
958
+
959
+ @validated
960
+ def crevision(self) -> str:
961
+ """
962
+ Get the revision of the chassis.
963
+
964
+ Returns
965
+ -------
966
+ The revision of the chassis.
967
+ """
968
+ return (
969
+ self._content[
970
+ self._crevision_index_start : self._crevision_index_start
971
+ + self._crevision_length
972
+ ]
973
+ .strip(b"\x00")
974
+ .decode("utf-8")
975
+ )
976
+
977
+ def write_crevision(self, revision: str) -> None:
978
+ """
979
+ Write the chassis revision.
980
+
981
+ Parameters
982
+ ----------
983
+ revision
984
+ The revision of the chassis.
985
+ """
986
+ revision_bytes = revision.encode("utf-8")
987
+ revision_fill = b"\x00" * (self._crevision_length - len(revision_bytes))
988
+ revision_bytes += revision_fill
989
+ logger.info(f"Writing chassis revision {revision}")
990
+ self.write(revision_bytes, self._crevision_index_start)
991
+
992
+ @validated
993
+ def unit(self) -> int:
994
+ """
995
+ Get the subsystem unit number.
996
+
997
+ Returns
998
+ -------
999
+ The unit number of the subsystem.
1000
+ """
1001
+ area = self._content[self._unit_index_start]
1002
+ unit = compute_int_from_bytes([area])
1003
+ return unit
1004
+
1005
+ def write_unit(self, unit: int) -> None:
1006
+ """
1007
+ Write the subsystem unit number.
1008
+
1009
+ Parameters
1010
+ ----------
1011
+ unit
1012
+ The unit number of the subsystem.
1013
+ """
1014
+ unit_bytes = unit.to_bytes(1, byteorder="little")
1015
+ logger.info(f"Writing unit {unit}")
1016
+ self.write(unit_bytes, self._unit_index_start)
1017
+
1018
+ @validated
1019
+ def customer_area(self) -> bytes:
1020
+ """
1021
+ Get the customer area of the CCU EEPROM.
1022
+
1023
+ Returns
1024
+ -------
1025
+ The customer area of the CCU EEPROM.
1026
+ """
1027
+ return self._content[
1028
+ self._customer_area_start : self._customer_area_start
1029
+ + self._customer_area_length
1030
+ ]
1031
+
1032
+ def write_customer_area(self, data: bytes) -> None:
1033
+ """
1034
+ Write data to CCU EEPROM customer area.
1035
+ """
1036
+ if len(data) > self._customer_area_length:
1037
+ raise ValueError("Data exceeds customer area length")
1038
+ self.write(data, self._customer_area_start)
1039
+
1040
+ def _compute_ccrc(self) -> int:
1041
+ return get_crc16_xmodem(self._cdata)
1042
+
1043
+ def write(self, data: bytes, offset: int = 0) -> None:
1044
+ try:
1045
+ super(Validatable_EEPROM, self).write(data, offset)
1046
+ except Exception as e:
1047
+ logger.error(f"Error writing to EEPROM, {e}")
1048
+ self._update_ccrc()
1049
+
1050
+ def custom_raw_data(self) -> bytes:
1051
+ raise NotImplementedError("CCU EEPROM does not have a raw content area")
1052
+
1053
+ def write_custom_raw_data(self, data: bytes) -> None:
1054
+ raise NotImplementedError("CCU EEPROM does not have a raw content area")