Source code for pihat.eeprom.layout

"""Pi Hat EEPROM layout"""

from abc import abstractmethod
from ctypes import (Array, LittleEndianStructure, c_uint8, c_uint16,
                    c_uint32, sizeof)
from dataclasses import dataclass, field
from typing import ClassVar, Dict, List, Optional, Type, Union
from uuid import UUID
from fdt import FDT, parse_dtb
from .constants import (EepromSignature, EepromVersion, EepromAtomType,
                        EepromGpioDrive, EepromGpioSlew, EepromGpioHysteresis,
                        EepromGpioBackPower, EepromGpioFunction,
                        EepromGpioPull)
from .crc import crc16
from .exceptions import EepromSignatureError, EepromLengthError, EepromCrcError

__all__ = [
    'EepromHeader',
    'EepromVendorInfo',
    'EepromGpioBank',
    'EepromGpioPower',
    'EepromGpioPin',
    'EepromGpioPins',
    'EepromGpioMap',
    'EepromAtom',
    'Eeprom',
]

CRC_SEED = 0
GPIO_COUNT = 28


class EepromStructure:
    """EEPROM data structure abstract base class"""

    def __bytes__(self):
        return self.pack()

    def __len__(self):
        return len(self.pack(fixup=False))

    def __eq__(self, other):
        if isinstance(self, type(other)):
            return self.pack(fixup=False) == other.pack(fixup=False)
        return NotImplemented

    def fixup(self):
        """Fix up fields for consistency"""

    def pack(self, fixup=True):
        """Pack structure to binary data"""
        if fixup:
            self.fixup()

    @abstractmethod
    def unpack(self, raw):
        """Unpack structure from binary data"""


@dataclass
class EepromTypedField:
    """A typed field within an EEPROM structure"""

    type: Type
    field: Optional[str] = None

    def __set_name__(self, owner, name):
        if self.field is None:
            self.field = '_%s' % name
        owner.register_typed_field(name)


@dataclass
class EepromEnumField(EepromTypedField):
    """An enumerated type field within an EEPROM structure"""

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return self.type(getattr(instance, self.field))

    def __set__(self, instance, value):
        setattr(instance, self.field, self.type(value))


@dataclass
class EepromUuidField(EepromTypedField):
    """A UUID field within an EEPROM structure"""

    type: Type = UUID

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return self.type(bytes=bytes(getattr(instance, self.field))[::-1])

    def __set__(self, instance, value):
        if isinstance(value, self.type):
            value = value.bytes[::-1]
        ftype = type(getattr(instance, self.field))
        setattr(instance, self.field, ftype(*bytes(value)))


@dataclass
class EepromStringField(EepromTypedField):
    """A string field within an EEPROM structure"""

    type: Type = bytes
    length_field: Optional[str] = None

    def __set_name__(self, owner, name):
        super().__set_name__(owner, name)
        if self.length_field is None:
            self.length_field = '%s_len' % self.field

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return self.type(getattr(instance, self.field))

    def __set__(self, instance, value):
        value = self.type(value)
        setattr(instance, self.field, value)
        setattr(instance, self.length_field, len(value))


class EepromLittleEndianStructure(EepromStructure, LittleEndianStructure):
    """An EEPROM data structure"""

    _typed_fields_: List[str] = []
    """List of attribute names for typed field attributes"""

    _repr_fields_: List[str] = []
    """List of attribute names to appear in `repr` output"""

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        cls._repr_fields_ = cls._typed_fields_ + [
            f[0] for f in getattr(cls, '_fields_', ())
            if not f[0].startswith('_')
        ]

    def __init__(self, **kwargs):
        typed_kwargs = {
            k: kwargs.pop(k) for k in self._typed_fields_ if k in kwargs
        }
        super().__init__(**kwargs)
        for k, v in typed_kwargs.items():
            setattr(self, k, v)

    def __repr__(self):
        return '%s(%s)' % (self.__class__.__name__, ', '.join(
            '%s=%r' % (f, getattr(self, f)) for f in self._repr_fields_
        ))

    def __len__(self):
        return sizeof(self)

    @classmethod
    def register_typed_field(cls, name):
        """Register a typed field attribute"""
        cls._typed_fields_ = cls._typed_fields_ + [name]

    def pack(self, fixup=True):
        """Pack structure to binary data"""
        super().pack(fixup=fixup)
        return memoryview(self).tobytes()

    def unpack(self, raw):
        """Unpack structure from binary data"""
        hlen = sizeof(self)
        if len(raw) < hlen:
            raise EepromLengthError("Underlength structure")
        memoryview(self).cast('B')[:] = raw[:hlen]
        return self


class EepromArray(Array):
    """An EEPROM data structure array"""

    _type_: ClassVar[Type] = c_uint8
    _length_: ClassVar[int] = 0

    def __repr__(self):
        return '[%s]' % ', '.join('%r' % x for x in self)


[docs]class EepromHeader(EepromLittleEndianStructure): """EEPROM header""" _fields_ = [ ('_signature', c_uint32), ('_version', c_uint8), ('_reserved', c_uint8), ('numatoms', c_uint16), ('eeplen', c_uint32), ] signature = EepromEnumField(EepromSignature) version = EepromEnumField(EepromVersion)
class EepromAtomData(EepromStructure): """EEPROM atom data""" # pylint: disable=abstract-method type: ClassVar[EepromAtomType] types: ClassVar[Dict[EepromAtomType, Type]] = {} def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) if cls.type is not None: cls.types[cls.type] = cls
[docs]class EepromVendorInfo(EepromAtomData, EepromLittleEndianStructure): """EEPROM vendor information data""" type: ClassVar[EepromAtomType] = EepromAtomType.INFO _vstr: bytes = b'' _pstr: bytes = b'' _fields_ = [ ('_uuid', c_uint8 * 16), ('pid', c_uint16), ('pver', c_uint16), ('_vslen', c_uint8), ('_pslen', c_uint8), ] uuid = EepromUuidField() vstr = EepromStringField(length_field='_vslen') pstr = EepromStringField(length_field='_pslen') def __len__(self): return sizeof(self) + len(self._vstr) + len(self._pstr)
[docs] def fixup(self): # pylint: disable=attribute-defined-outside-init super().fixup() self._vslen = len(self.vstr) self._pslen = len(self.pstr)
[docs] def pack(self, fixup=True): return super().pack(fixup=fixup) + self._vstr + self._pstr
[docs] def unpack(self, raw): super().unpack(raw) hlen = sizeof(self) vslen = self._vslen pslen = self._pslen if len(raw) < hlen + vslen + pslen: raise EepromLengthError("Underlength vendor information data") self._vstr = raw[hlen:(hlen + vslen)] self._pstr = raw[(hlen + vslen):(hlen + vslen + pslen)] return self
[docs]class EepromGpioBank(EepromLittleEndianStructure): """EEPROM GPIO bank configuration""" _fields_ = [ ('_drive', c_uint8, 4), ('_slew', c_uint8, 2), ('_hysteresis', c_uint8, 2), ] drive = EepromEnumField(EepromGpioDrive) slew = EepromEnumField(EepromGpioSlew) hysteresis = EepromEnumField(EepromGpioHysteresis)
[docs]class EepromGpioPower(EepromLittleEndianStructure): """EEPROM GPIO powering""" _fields_ = [ ('_back_power', c_uint8, 2), ('_reserved', c_uint8, 6), ] back_power = EepromEnumField(EepromGpioBackPower)
[docs]class EepromGpioPin(EepromLittleEndianStructure): """EEPROM GPIO pin description""" _fields_ = [ ('_function', c_uint8, 3), ('_reserved', c_uint8, 2), ('_pull', c_uint8, 2), ('_used', c_uint8, 1), ] function = EepromEnumField(EepromGpioFunction) pull = EepromEnumField(EepromGpioPull) used = EepromEnumField(bool)
[docs]class EepromGpioPins(EepromArray): """EEPROM GPIO pin descriptions""" _type_ = EepromGpioPin _length_ = GPIO_COUNT
[docs]class EepromGpioMap(EepromAtomData, EepromLittleEndianStructure): """EEPROM GPIO map""" type: ClassVar[EepromAtomType] = EepromAtomType.GPIO _fields_ = [ ('bank', EepromGpioBank), ('power', EepromGpioPower), ('pins', EepromGpioPins), ]
@dataclass class EepromDeviceTreeBlob(EepromAtomData): """EEPROM device tree blob""" type: ClassVar[EepromAtomType] = EepromAtomType.DTBO fdt: FDT = field(default_factory=FDT) def __len__(self): return len(self.pack(fixup=True)) def pack(self, fixup=True): super().pack(fixup=fixup) if fixup and self.fdt.header.version is None: self.fdt.header.version = self.fdt.header.MAX_VERSION return self.fdt.to_dtb() def unpack(self, raw): super().unpack(raw) self.fdt = parse_dtb(raw) return self class EepromAtomChecksum(EepromLittleEndianStructure): """EEPROM atom checksum""" _fields_ = [ ("crc", c_uint16), ]
[docs]class EepromAtom(EepromLittleEndianStructure): """EEPROM atom""" data: Union[bytes, EepromAtomData] = b'' _typed_fields_ = ['data'] _fields_ = [ ('_type', c_uint16), ('count', c_uint16), ('_dlen', c_uint32), ] type = EepromEnumField(EepromAtomType) def __len__(self): return sizeof(self) + len(self.data) + sizeof(EepromAtomChecksum) @property def unfixed_len(self): """Recorded length (ignoring any potential data changes)""" return sizeof(self) + self._dlen
[docs] def fixup(self): # pylint: disable=attribute-defined-outside-init super().fixup() self._dlen = len(self.data) + sizeof(EepromAtomChecksum)
[docs] def pack(self, fixup=True): header = super().pack(fixup=fixup) data = (self.data.pack(fixup=fixup) if hasattr(self.data, 'pack') else bytes(self.data)) body = header + data checksum = EepromAtomChecksum(crc=crc16(body, CRC_SEED)) return body + checksum
[docs] def unpack(self, raw): super().unpack(raw) hlen = sizeof(self) dlen = self._dlen clen = sizeof(EepromAtomChecksum) if dlen < clen: raise EepromLengthError("Underlength atom CRC") if len(raw) < hlen + dlen: raise EepromLengthError("Underlength atom data") atom = raw[:hlen + dlen] if crc16(atom, CRC_SEED) != 0: raise EepromCrcError( "Invalid atom CRC 0x%04x (expected 0x%04x)" % ( EepromAtomChecksum().unpack(atom[-clen:]).crc, crc16(atom[:-clen]) ) ) self.data = atom[hlen:-clen] subcls = EepromAtomData.types.get(self.type) if subcls is not None: self.data = subcls().unpack(self.data) return self
@dataclass class EepromAtomAttribute: """An EEPROM attribute located inside an atom""" atom: str attribute: Optional[str] = None def __set_name__(self, owner, name): if self.attribute is None: self.attribute = name def __get__(self, instance, owner): if instance is None: return self atom = getattr(instance, self.atom) return getattr(atom.data, self.attribute) def __set__(self, instance, value): atom = getattr(instance, self.atom) setattr(atom.data, self.attribute, value) @dataclass class EepromInfoAttribute(EepromAtomAttribute): """An EEPROM attribute located inside the vendor information atom""" atom: str = 'info' @dataclass class EepromGpioAttribute(EepromAtomAttribute): """An EEPROM attribute located inside the GPIO map atom""" atom: str = 'gpio' @dataclass class EepromDtboAttribute(EepromAtomAttribute): """An EEPROM attribute located inside the device tree blob atom""" atom: str = 'dtbo'
[docs]@dataclass class Eeprom(EepromStructure): """EEPROM content""" # pylint: disable=too-many-instance-attributes header: EepromHeader = field(default_factory=lambda: EepromHeader( signature=EepromSignature.RPI, version=EepromVersion.V1, )) atoms: List[EepromAtom] = field(default_factory=lambda: [ EepromAtom(type=EepromAtomType.INFO, count=0, data=EepromVendorInfo()), EepromAtom(type=EepromAtomType.GPIO, count=1, data=EepromGpioMap()), ]) uuid = EepromInfoAttribute() pid = EepromInfoAttribute() pver = EepromInfoAttribute() vstr = EepromInfoAttribute() pstr = EepromInfoAttribute() bank = EepromGpioAttribute() power = EepromGpioAttribute() pins = EepromGpioAttribute() fdt = EepromDtboAttribute() def __len__(self): return len(self.header) + sum(len(x) for x in self.atoms)
[docs] def fixup(self): super().fixup() for i, atom in enumerate(self.atoms): atom.count = i atom.fixup() self.header.numatoms = len(self.atoms) self.header.eeplen = len(self) self.header.fixup()
[docs] def pack(self, fixup=True): super().pack(fixup=fixup) atoms = [x.pack(fixup=False) for x in self.atoms] return self.header.pack(fixup=False) + b''.join(atoms)
[docs] def unpack(self, raw): self.header = EepromHeader().unpack(raw) if self.header.signature != EepromSignature.RPI: raise EepromSignatureError("Invalid EEPROM signature") if len(raw) < self.header.eeplen: raise EepromLengthError("Underlength EEPROM content") remaining = raw[len(self.header):self.header.eeplen] self.atoms = [] while remaining: atom = EepromAtom().unpack(remaining) self.atoms.append(atom) remaining = remaining[atom.unfixed_len:] if self.header.numatoms != len(self.atoms): raise EepromLengthError("Atom count mismatch") return self
[docs] def atom(self, type): """Find first atom of a specified type""" # pylint: disable=redefined-builtin return next((x for x in self.atoms if x.type == type), None)
@property def info(self): """Vendor information atom""" atom = self.atom(EepromAtomType.INFO) if atom is None: atom = EepromAtom(type=EepromAtomType.INFO, data=EepromVendorInfo()) self.atoms.insert(0, atom) return atom @property def gpio(self): """GPIO map atom""" atom = self.atom(EepromAtomType.GPIO) if atom is None: atom = EepromAtom(type=EepromAtomType.GPIO, data=EepromGpioMap()) self.atoms.insert(self.atoms.index(self.info) + 1, atom) return atom @property def dtbo(self): """Device tree overlay atom""" atom = self.atom(EepromAtomType.DTBO) if atom is None: atom = EepromAtom(type=EepromAtomType.DTBO, data=EepromDeviceTreeBlob()) self.atoms.insert(self.atoms.index(self.gpio) + 1, atom) return atom @property def has_dtbo(self): """Check for presence of device tree overlay atom""" return self.atom(EepromAtomType.DTBO) is not None