ekfsm.devices.eeprom — ekfsm documentation

# Source code for ekfsm.devices.eeprom

    """
    A module containing classes to represent EEPROM devices.

    Routine Listings
    ----------------
        :py:class:`EEPROM`
        :py:class:`Validatable_EEPROM`
        :py:class:`EKF_EEPROM`
        :py:class:`validated`
    """

    from abc import ABC, abstractmethod
    from datetime import date
    from typing import Any, Callable, Literal, Sequence
    from functools import wraps

    from ekfsm.core.components import SysTree
    from ekfsm.core.probe import ProbeableDevice

    from .generic import Device
    from .utils import compute_int_from_bytes, get_crc16_xmodem
    from ekfsm.exceptions import DataCorruptionError
    from hexdump import hexdump

    from ekfsm.log import ekfsm_logger

    logger = ekfsm_logger(__name__)

    def validated(func: Callable[..., Any]) -> Callable[..., Any]:
        """
        A decorator to validate the CRC of the EEPROM content before executing a method.

        Parameters
        ----------
        func
            The method to validate.

        Note
        ----
        This decorator should be used on methods that read data from an EEPROM.
        """

        @wraps(func)
        def validate(self, *args, **kwargs):
            logger.debug(f"Validating EEPROM content for {self.name}")
            if not self.valid:
                raise DataCorruptionError("CRC validation failed")
            logger.debug(f"EEPROM content is valid for {self.name}")
            return func(self, *args, **kwargs)

        return validate

    class EEPROM(Device):
        """
        A class used to represent a generic EEPROM device.

        Parameters
        ----------
        name
            The name of the EEPROM device.
        parent
            The parent device of the EEPROM in the :py:class:`~.generic.Device` tree.

        Caution
        -------
        The following conditions must be met for this class to work properly:
            - EEPROM must be I2C accessable
            - EEPROM must have a sysfs device

        Note
        ----
        This class should be inherited by classes representing specific EEPROM devices and defining custom storage schemes.
        """

        def __init__(
            self,
            name: str,
            parent: SysTree | None = None,
            *args,
            **kwargs,
        ):
            super().__init__(name, parent, None, *args, **kwargs)

            self.addr = self.get_i2c_chip_addr()
            self.sysfs_device = self.get_i2c_sysfs_device(self.addr)
            self._update_content()

        def _update_content(self) -> None:
            """
            Update the content of the EEPROM device.

            Note
            ----
                - This method should be called whenever the content of the EEPROM is updated (after each write op).
                - Inheriting classes should call this method before updating their own attributes.
            """
            logger.debug("Reading data")
            try:
                data = self.read()
                self._content = data
            except Exception as e:
                logger.error(f"Error reading data, {e}")
                self._content = b""

        def read(self) -> bytes:
            """
            Read the content of the EEPROM.

            Returns
            -------
                The content of the EEPROM.

            Raises
            ------
            FileNotFoundError
                If the EEPROM sysfs file is not found.
            RuntimeError
                If the sysfs device is not found.
            """
            try:
                if self.sysfs_device:
                    cnt = self.sysfs_device.read_attr_bytes("eeprom")
                else:
                    raise RuntimeError("No sysfs device for EEPROM")
            except FileNotFoundError:
                raise FileNotFoundError("EEPROM not found")

            return cnt

        def write(self, data: bytes, offset: int = 0) -> None:
            """
            Write data to the EEPROM.

            Parameters
            ----------
            data
                The data to write to the EEPROM.
            offset
                The offset at which to start writing the data.

            Raises
            ------
            RuntimeError
                If the sysfs device is not found.
            FileNotFoundError
                If the EEPROM sysfs file is not found.
            DataCorruptionError
                If an error occurs during the write operation.

            Note
            ----
            Operation is checked for data corruption by reading back the written data.
            """
            try:
                if self.sysfs_device:
                    logger.info(f"Writing {len(data)} bytes to EEPROM at offset {offset}")
                    self.sysfs_device.write_attr("eeprom", data, offset)
                else:
                    raise RuntimeError("No sysfs device for EEPROM")
            except FileNotFoundError:
                raise FileNotFoundError("EEPROM not found")

            self._update_content()
            written = self._content[offset : offset + len(data)]
            if not written == data:
                raise DataCorruptionError(
                    "Error during EEPROM write, data is not the same as read back"
                )

        def print(self):
            hexdump(self._content)

    class Validatable_EEPROM(EEPROM, ABC):
        """
        Abstract class used to represent an EEPROM device using CRC to validate its content.

        Parameters
        ----------
        crc_pos
            The position of the CRC value in the EEPROM content (`'start'` or `'end'`).
        crc_length
            The length of the CRC value in number of bytes (defaults to 2).

        Note
        ----
        - Derived classes must implement a method to compute the CRC value of the EEPROM content.
        - If the CRC position differs from the shipped schema `('start' | 'end')`,
          the derived class must override the :meth:`~Validatable_EEPROM._update_content` method.
        - Validity of individual content fields stored/returned by attributes, methods or properties
          can be achieved by using the :py:func:`validated` decorator.

        See Also
        --------
        Validatable_EEPROM._compute_crc : Method to compute the CRC value of the EEPROM content.
        """

        def __init__(
            self,
            crc_pos: Literal["start", "end"] = "end",
            crc_length: int = 2,
            *args,
            **kwargs,
        ) -> None:
            self._crc_length: int = crc_length
            self._crc_pos: str = crc_pos

            super().__init__(*args, **kwargs)

            self._crc_pos_start = len(self._data) if self._crc_pos == "end" else 0
            self._crc_pos_end = self._crc_pos_start + self._crc_length

        def _update_content(self) -> None:
            """
            Update the content of the EEPROM device (checksum excluded).
            """
            super()._update_content()

            # Firmware data without CRC
            self._data: bytes = (
                self._content[self._crc_length :]
                if self._crc_pos == "start"
                else self._content[: -self._crc_length :]
            )

        def _update_crc(self) -> None:
            """
            Update the CRC value of the EEPROM content.
            """
            self.crc = self._compute_crc()

        def _get_crc_value(self) -> int:
            return int.from_bytes(
                self._content[self._crc_pos_start : self._crc_pos_end], byteorder="little"
            )

        @property
        def crc(self) -> int:
            """
            Gets or sets the CRC value of the EEPROM content.

            Parameters
            ----------
            value: optional
                The CRC value to write to the EEPROM.

            Returns
            -------
                int
                    The CRC value currently stored in the EEPROM if used as *getter*.
                None
                    If used as *setter*.

            Caution
            -------
            The *setter* actually writes the CRC value to the EEPROM.

            Warning
            -------
            The *setter* method should be used with caution as it can lead to data corruption if the CRC value is not correct!

            Note
            ----
            The *setter* is usually triggered automatically after a successful write operation
            and in most cases, there is no need to call it manually.
            """
            return self._get_crc_value()

        @crc.setter
        def crc(self, value: int) -> None:
            crc_bytes = value.to_bytes(self._crc_length, byteorder="little")
            logger.debug(f"Writing CRC value {value} to EEPROM")

            try:
                super().write(crc_bytes, self._crc_pos_start)
            except Exception as e:
                logger.error(f"Error writing CRC value to EEPROM, {e}")

            # self._update_content()

        @property
        def valid(self) -> bool:
            """
            Checks if the EEPROM content is valid by comparing the stored CRC value with the computed CRC value.

            Returns
            -------
            bool
                `True` if the EEPROM content is valid, `False` otherwise.
            """
            return self._compute_crc() == self.crc

        @abstractmethod
        def _compute_crc(self) -> int:
            """
            This method should be implemented by derived classes to compute the CRC value of the EEPROM content.

            Returns
            -------
                The computed CRC value.
            """
            pass

        def write(self, data: bytes, offset: int = 0) -> None:
            try:
                super().write(data, offset)
                self._update_crc()
            except Exception as e:
                logger.error(f"Error writing to EEPROM, {e}")

    class EKF_EEPROM(Validatable_EEPROM, ProbeableDevice):
        """
        A class used to represent an EKF EEPROM device.

        Structure
        ---------
        The EKF_EEPROM content is structured as follows:

        - `Serial number` (4 bytes, starts at pos 8):
            The serial number of the device.
        - `Manufactured at` (2 bytes, starts at pos 12):
            The date the device was manufactured.
        - `Repaired at` (2 bytes, starts at pos 14):
            The date the device was repaired.
        - `Customer serial number` (4 bytes, starts at pos 32):
            The customer serial number of the device.
        - `Customer configuration block offset pointer` (4 bytes, starts at pos 36):
            The offset pointer to the customer configuration block.
        - `String array` (78 bytes, starts at pos 48):
            An array of strings containing the model, manufacturer, and custom board data of the device.
        - `CRC` (2 bytes, starts at pos 126):
            The CRC value of the EEPROM content.
        - `Raw content` (80 bytes, starts at pos 128):
            Free customizable content for other purposes.

        Note
        ----
        - As the CRC value is stored at the end of the OEM data space, just before the customer configuration block,
          the CRC position is manually set and the :meth:`~ekfsm.devices.eeprom.Validatable_EEPROM._update_content` method is
          overridden.
        - The CRC value is computed using the `CRC-16/XMODEM <https://en.wikipedia.org/wiki/Cyclic_redundancy_check>`_ algorithm.
        - Dates are stored in a proprietary format (2 bytes) and must be decoded using the :meth:`~EKF_EEPROM._decode_date` method.

        See Also
        --------
        `crcmod <https://crcmod.sourceforge.net/>`_

        Important
        ---------
        All data read from the EEPROM should be validated using the @validated decorator.
        This decorator ensures that the data is not corrupted by checking the CRC value.

        Raises
        ------
        DataCorruptionError
            If the CRC validation fails.
        """

        _sernum_index_start = 8
        _sernum_index_end = 12

        _date_mft_index_start = 12
        _date_mft_index_end = 14

        _date_rep_index_start = 14
        _date_rep_index_end = 16

        _customer_serial_index_start = 32
        _customer_serial_index_end = 36

        _customer_config_block_offset_pointer_index = 36

        _str_array_start_offset = 48
        _str_array_end_offset = 126

        def __init__(
            self,
            *args,
            **kwargs,
        ) -> None:
            super().__init__(*args, **kwargs)

        def _update_content(self) -> None:

            super()._update_content()

            # EKF EEPROM content is restricted to 128 bytes, so strip the rest!
            self._firmware_content = self._content[:128]

            # The rest is raw content available for other purposes
            self._raw_content: bytes = self._content[128:]

            # Firmware data without CRC needs to be overriden
            self._data: bytes = (
                self._firmware_content[self._crc_length :]
                if self._crc_pos == "start"
                else self._firmware_content[: -self._crc_length :]
            )
            self._str_list = self._get_string_array()

            self._crc_pos_start = len(self._data)
            self._crc_pos_end = self._crc_pos_start + self._crc_length

        @validated
        def serial(self) -> str:
            """
            Get the serial number of the device to which the EEPROM is attached (the root device).

            Returns
            -------
                The serial number of the root device.
            """
            area = self._content[self._sernum_index_start : self._sernum_index_end]
            sernum = compute_int_from_bytes(area[::-1])
            return str(sernum)

        def write_serial(self, serial: int) -> None:
            """
            Write serial number of the root device to EEPROM.

            Parameters
            ----------
            serial
                The serial number to write to the EEPROM.
            """
            # Check serial number is within bounds
            unsigned_upper_bound = (2**32) - 1
            if serial < 0 or serial > unsigned_upper_bound:
                raise ValueError(
                    f"Serial number must be between 0 and {unsigned_upper_bound}"
                )
            serial_bytes = serial.to_bytes(4, byteorder="little")
            self.write(serial_bytes, self._sernum_index_start)

        @validated
        def custom_serial(self) -> str:
            """
            Get the customer serial number of the device to which the EEPROM is attached (the root device).

            Attention
            ---------
            This is a custom - non-OEM - serial number that can be set by the user.

            Returns
            -------
                The customer serial number of the root device.
            """
            area = self._content[
                self._customer_serial_index_start : self._customer_serial_index_end
            ]
            sernum = compute_int_from_bytes(area[::-1])
            return str(sernum)

        def write_custom_serial(self, serial: int) -> None:
            """
            Write customer serial number of the root device to EEPROM.

            Parameters
            ----------
            serial
                The customer serial number to write to the EEPROM.

            Raises
            ------
            ValueError
                If the serial number is not within the bounds of a 32-bit unsigned integer.

            Note
            ----
            Due to space restrictions on storage, the serial number must be a 32-bit unsigned integer.
            """
            # Check serial number is within bounds
            unsigned_upper_bound = (2**32) - 1
            if serial < 0 or serial > unsigned_upper_bound:
                raise ValueError(
                    f"Serial number must be between 0 and {unsigned_upper_bound}"
                )

            serial_bytes = serial.to_bytes(4, byteorder="little")
            logger.debug(f"Writing customer serial {serial}")
            self.write(serial_bytes, self._customer_serial_index_start)

        @validated
        def manufactured_at(self) -> date:
            """
            Get the date the device was manufactured.

            Returns
            -------
                The date the device was manufactured.
            """
            area = self._content[self._date_mft_index_start : self._date_mft_index_end]
            encoded_mft_date = area[::-1]
            return self._decode_date(encoded_mft_date)

        @validated
        def repaired_at(self) -> date:
            """
            Get the date the device was repaired.

            Returns
            -------
                The most recent date the device was repaired.
            """
            area = self._content[self._date_rep_index_start : self._date_rep_index_end]
            encoded_rep_date = area[::-1]
            return self._decode_date(encoded_rep_date)

        @validated
        def write_repaired_at(self, date: date) -> None:
            """
            Write the date the device was repaired to EEPROM.

            Parameters
            ----------
            date
                The date the device was repaired.

            Note
            ----
            The date year must be within the range of 1980-2079.

            Attention
            ---------
            The date is stored in a proprietary 2-byte format.

            Raises
            ------
            ValueError
                If the year is not within the range of 1980-2079.
            """
            if date.year < 1980 or date.year > 2079:
                raise ValueError("Year must be within the range of 1980-2079")
            rep_date_bytes = self._encode_date(date)
            logger.debug(f"Writing repair date {date} to EEPROM")
            self.write(rep_date_bytes, self._date_rep_index_start)

        @validated
        def model(self) -> str | None:
            """
            Get the model name of the device to which the EEPROM is attached to (the root device).

            Returns
            -------
                The model name of the device.
            """
            return self._str_list[0] if len(self._str_list) > 0 else None

        @validated
        def vendor(self) -> str | None:
            """
            Get the vendor/manufacturer of the device to which the EEPROM is attached to (the root device).

            Returns
            -------
                The name of the vendor/manufacturer of the device.
            """
            return self._str_list[1] if len(self._str_list) > 1 else None

        @validated
        def custom_board_data(self) -> str | None:
            """
            Get the custom board data of the device.

            Note
            ----
            This is a custom field that can be set by the user.

            Attention
            ---------
            This field is optional and may not be present in the EEPROM content.

            Returns
            -------
                The custom board data of the device as a string, or `None` if the field is not present.
            """
            return None if len(self._str_list) < 3 else self._str_list[2]

        def write_custom_board_data(self, data: str) -> None:
            """
            Write custom board data to EEPROM.

            Important
            ---------
            Due to size limitations, the custom board data should only contain expressive,
            short content like serials, variants or specific codes.

            Parameters
            ----------
            data
                The custom board data to write to the EEPROM.

            Attention
            ---------
            The model and vendor fields are mandatory and must be set before writing custom board data.

            Raises
            ------
            ValueError
                If the model and vendor fields are not set before writing custom board data.
            """
            data_bytes = data.encode("utf-8")
            data_offset = 0
            for s in self._str_list[:2]:
                if s is None:
                    raise ValueError(
                        "Model and vendor fields must be set before writing custom board data"
                    )
                if isinstance(s, str):
                    data_offset += len(s) + 1
            logger.info(f"Writing custom board data {data} to EEPROM")
            self.write(data_bytes, self._str_array_start_offset + data_offset)

        def custom_raw_data(self) -> bytes:
            """
            Get the raw content area data stored in the EEPROM.

            Returns
            -------
                The data contained in the raw content block of the EEPROM.

            Note
            ----
            This area is free for custom data storage and is not included during crc calculations and validations.

            Important
            ---------
            If custom raw data should be stored on EEPROM and
            if it should be protected against corruption, it has to be validated manually.
            """
            return self._raw_content

        def write_custom_raw_data(self, data: bytes) -> None:
            """
            Write custom data to the raw content area of the EEPROM.

            Parameters
            ----------
            data
                The data to write to the raw content area of the EEPROM.
            """
            logger.info(f"Writing {len(data)} bytes to raw content area of EEPROM")
            self.write(data, 128)

        def _get_string_array(self) -> list[str | None]:
            str_array = self._content[
                self._str_array_start_offset : self._str_array_end_offset
            ].split(b"\x00")
            try:
                return [s.decode("utf-8") for s in str_array if s]
            except UnicodeDecodeError:
                return [None, None, None]

        @classmethod
        def _decode_date(cls, encoded_date: Sequence[int]) -> date:
            """
            Decode a date from a proprietary 2-byte format.

            Parameters
            ----------
            encoded_date
                The date to decode.

            Raises
            ------
            ValueError
                If the date is invalid (e.g., 30th Feb).

            Returns
            -------
            date
                The decoded date.
            """
            encoded_date = encoded_date[::-1]
            bdate = compute_int_from_bytes(encoded_date)

            # Extract the day (bit 0-4)
            day = bdate & 0x1F  # 0x1F is 00011111 in binary (5 bits)

            # Extract the month (bit 5-8)
            month = (bdate >> 5) & 0x0F  # Shift right by 5 and mask with 0x0F (4 bits)

            # Extract the year since 1980 (bit 9-15)
            year = (bdate >> 9) & 0x7F  # Shift right by 9 and mask with 0x7F (7 bits)
            year += 1980  # Add base year (1980)

            # Return a datetime object with the extracted year, month, and day
            try:
                decoded_date = date(year, month, day)
                return decoded_date
            except ValueError:
                raise ValueError(
                    f"Invalid date: {day}/{month}/{year}"
                )  # Handle invalid dates, e.g., 30th Feb

        @classmethod
        def _encode_date(self, date: date) -> bytes:
            """
            Encode a date into a proprietary 2-byte format.

            Parameters
            ----------
            date
                The date to encode.

            Returns
            -------
            bytes
                The encoded date.
            """
            year = date.year - 1980
            month = date.month
            day = date.day

            encoded_date = year << 9 | month << 5 | day
            return encoded_date.to_bytes(2, byteorder="little")

        def _compute_crc(self) -> int:
            return get_crc16_xmodem(self._data)

        def probe(self, *args, **kwargs):
            return self.hw_module.id == self.model()

    class EKF_CCU_EEPROM(EKF_EEPROM):
        """
        EKF CCU EEPROM - uses the second part of the EEPROM for chassis inventory and customer area
        """

        _cvendor_index_start = 128
        _cvendor_length = 24

        _cmodel_index_start = 152
        _cmodel_length = 24

        _crevision_index_start = 176
        _crevision_length = 9

        _cserial_index_start = 185
        _cserial_length = 4

        _unit_index_start = 189
        _unit_length = 1

        _customer_area_start = 190
        _customer_area_length = 63

        def __init__(
            self,
            *args,
            **kwargs,
        ):
            super().__init__(*args, **kwargs)

        def _update_content(self) -> None:

            super()._update_content()

            # CCU content is the raw content area of the EEPROM
            self._ccu_content: bytes = self._raw_content

            # CCU Firmware data without CRC needs to be overriden
            self._cdata: bytes = (
                self._ccu_content[self._crc_length :]
                if self._crc_pos == "start"
                else self._ccu_content[: -self._crc_length :]
            )
            self._ccrc_pos_start = len(self._cdata) + 128
            self._ccrc_pos_end = self._ccrc_pos_start + self._crc_length

        def _update_ccrc(self) -> None:
            """
            Update the CRC value of the EEPROM content.
            """
            self.ccrc = self._compute_ccrc()

        def _get_ccrc_value(self) -> int:
            return int.from_bytes(
                self._content[self._ccrc_pos_start : self._ccrc_pos_end], byteorder="little"
            )

        @property
        def ccrc(self) -> int:
            """
            Gets or sets the CRC value of the EEPROM content.

            Parameters
            ----------
            value: optional
                The CRC value to write to the EEPROM.

            Returns
            -------
                int
                    The CRC value currently stored in the EEPROM if used as *getter*.
                None
                    If used as *setter*.

            Caution
            -------
            The *setter* actually writes the CRC value to the EEPROM.

            Warning
            -------
            The *setter* method should be used with caution as it can lead to data corruption if the CRC value is not correct!

            Note
            ----
            The *setter* is usually triggered automatically after a successful write operation
            and in most cases, there is no need to call it manually.
            """
            return self._get_ccrc_value()

        @ccrc.setter
        def ccrc(self, value: int) -> None:
            ccrc_bytes = value.to_bytes(self._crc_length, byteorder="little")
            logger.debug(f"Writing chassis CRC value {value}")
            try:
                super(Validatable_EEPROM, self).write(ccrc_bytes, self._ccrc_pos_start)
            except Exception as e:
                logger.error(f"Error writing CRC value, error: {e}")

        @property
        def valid(self) -> bool:
            """
            Checks if the EEPROM content is valid by comparing the stored CRC value with the computed CRC value.

            Returns
            -------
            bool
                `True` if the EEPROM content is valid, `False` otherwise.
            """
            return self._compute_ccrc() == self.ccrc and super().valid

        @validated
        def cvendor(self) -> str:
            """
            Get the chassis vendor.

            Returns
            -------
                The vendor of the chassis.
            """

            return (
                self._content[
                    self._cvendor_index_start : self._cvendor_index_start
                    + self._cvendor_length
                ]
                .strip(b"\x00")
                .decode("utf-8")
            )

        def write_cvendor(self, vendor: str) -> None:
            """
            Write the vendor of the chassis to EEPROM.

            Parameters
            ----------
            vendor
                The vendor of the chassis.
            """
            vendor_bytes = vendor.encode("utf-8")
            vendor_fill = b"\x00" * (self._cvendor_length - len(vendor_bytes))
            vendor_bytes += vendor_fill
            logger.info(f"Writing vendor {vendor}")
            self.write(vendor_bytes, self._cvendor_index_start)

        @validated
        def cmodel(self) -> str:
            """
            Get the chassis model.

            Returns
            -------
                The model of the chassis.
            """
            return (
                self._content[
                    self._cmodel_index_start : self._cmodel_index_start
                    + self._cmodel_length
                ]
                .strip(b"\x00")
                .decode("utf-8")
            )

        def write_cmodel(self, model: str) -> None:
            """
            Write the model of the chassis to EEPROM.

            Parameters
            ----------
            model
                The model of the chassis.
            """
            model_bytes = model.encode("utf-8")
            model_fill = b"\x00" * (self._cmodel_length - len(model_bytes))
            model_bytes += model_fill
            logger.info(f"Writing model {model}")
            self.write(model_bytes, self._cmodel_index_start)

        @validated
        def cserial(self) -> int:
            """
            Get the chassis serial number.

            Returns
            -------
                The serial number of the chassis.
            """
            area = self._content[
                self._cserial_index_start : self._cserial_index_start + self._cserial_length
            ]
            cserial = compute_int_from_bytes(area[::-1])
            return cserial

        def write_cserial(self, serial: int) -> None:
            """
            Write the serial number of the chassis to EEPROM.

            Parameters
            ----------
            serial
                The serial number of the chassis.
            """
            # Check serial number is within bounds
            unsigned_upper_bound = (2**32) - 1
            if serial < 0 or serial > unsigned_upper_bound:
                raise ValueError(
                    f"Serial number must be between 0 and {unsigned_upper_bound}"
                )
            serial_bytes = serial.to_bytes(4, byteorder="little")
            logger.info(f"Writing chassis serial {serial}")
            self.write(serial_bytes, self._cserial_index_start)

        @validated
        def crevision(self) -> str:
            """
            Get the revision of the chassis.

            Returns
            -------
                The revision of the chassis.
            """
            return (
                self._content[
                    self._crevision_index_start : self._crevision_index_start
                    + self._crevision_length
                ]
                .strip(b"\x00")
                .decode("utf-8")
            )

        def write_crevision(self, revision: str) -> None:
            """
            Write the chassis revision.

            Parameters
            ----------
            revision
                The revision of the chassis.
            """
            revision_bytes = revision.encode("utf-8")
            revision_fill = b"\x00" * (self._crevision_length - len(revision_bytes))
            revision_bytes += revision_fill
            logger.info(f"Writing chassis revision {revision}")
            self.write(revision_bytes, self._crevision_index_start)

        @validated
        def unit(self) -> int:
            """
            Get the subsystem unit number.

            Returns
            -------
                The unit number of the subsystem.
            """
            area = self._content[self._unit_index_start]
            unit = compute_int_from_bytes([area])
            return unit

        @validated
        def version(self) -> int:
            """
            Get the version of the EEPROM data scheme.

            Note
            ----
            If undefined, the version is set to 255 and then defaults to 0.

            Returns
            -------
                The version of the EEPROM data scheme.
            """
            version = self._content[self._ccrc_pos_start - 1]
            return version

        def write_version(self, version: int) -> None:
            """
            Write the version of the EEPROM data scheme.

            Parameters
            ----------
            version
                The version of the EEPROM data scheme.
            """
            if version < 0 or version > 255:
                raise ValueError("Version must be between 0 and 255")

            if version == 255:
                logger.warning("Version 255 is undefined, setting to 0")
                version = 0

            version_bytes = version.to_bytes(1, byteorder="little")
            logger.info(f"Writing version {version}")
            self.write(version_bytes, self._ccrc_pos_start - 1)

        def write_unit(self, unit: int) -> None:
            """
            Write the subsystem unit number.

            Parameters
            ----------
            unit
                The unit number of the subsystem.
            """
            unit_bytes = unit.to_bytes(1, byteorder="little")
            logger.info(f"Writing unit {unit}")
            self.write(unit_bytes, self._unit_index_start)

        @validated
        def customer_area(self) -> bytes:
            """
            Get the customer area of the CCU EEPROM.

            Returns
            -------
                The customer area of the CCU EEPROM.
            """
            return self._content[
                self._customer_area_start : self._customer_area_start
                + self._customer_area_length
            ]

        def write_customer_area(self, data: bytes) -> None:
            """
            Write data to CCU EEPROM customer area.
            """
            if len(data) > self._customer_area_length:
                raise ValueError("Data exceeds customer area length")
            self.write(data, self._customer_area_start)

        def _compute_ccrc(self) -> int:
            return get_crc16_xmodem(self._cdata)

        def write(self, data: bytes, offset: int = 0) -> None:
            try:
                super(Validatable_EEPROM, self).write(data, offset)
            except Exception as e:
                logger.error(f"Error writing to EEPROM, {e}")
            self._update_ccrc()

        def custom_raw_data(self) -> bytes:
            raise NotImplementedError("CCU EEPROM does not have a raw content area")

        def write_custom_raw_data(self, data: bytes) -> None:
            raise NotImplementedError("CCU EEPROM does not have a raw content area")