from __future__ import annotations import functools import dataclasses import zigpy_znp.types as t from zigpy_znp.exceptions import InvalidFrame @dataclasses.dataclass(frozen=True) class GeneralFrame: header: t.CommandHeader data: t.Bytes def __post_init__(self) -> None: # We're frozen so `self.header = ...` is disallowed if not isinstance(self.header, t.CommandHeader): object.__setattr__(self, "header", t.CommandHeader(self.header)) if not isinstance(self.data, t.Bytes): object.__setattr__(self, "data", t.Bytes(self.data)) if self.length > 250: raise InvalidFrame( f"Frame length cannot exceed 250 bytes. Got: {self.length}" ) @property def length(self) -> t.uint8_t: """Length of the frame.""" return t.uint8_t(len(self.data)) @classmethod def deserialize(cls, data): """Deserialize frame and sanity checks.""" length, data = t.uint8_t.deserialize(data) if length > 250: raise InvalidFrame(f"Frame length cannot exceed 250 bytes. Got: {length}") if len(data) < length + 2: raise InvalidFrame(f"Data is too short for {cls.__name__}") header, data = t.CommandHeader.deserialize(data) payload, data = data[:length], data[length:] return cls(header, payload), data def serialize(self) -> bytes: return self.length.serialize() + self.header.serialize() + self.data.serialize() @dataclasses.dataclass class TransportFrame: """Transport frame.""" SOF = t.uint8_t(0xFE) # Start of frame marker payload: GeneralFrame @classmethod def deserialize(cls, data: bytes) -> tuple[TransportFrame, bytes]: sof, data = t.uint8_t.deserialize(data) if sof != cls.SOF: raise InvalidFrame( f"Expected frame to start with SOF 0x{cls.SOF:02X}, got 0x{sof:02X}" ) gen_frame, data = GeneralFrame.deserialize(data) checksum, data = t.uint8_t.deserialize(data) frame = cls(gen_frame) if frame.checksum() != checksum: raise InvalidFrame( f"Invalid frame checksum for data {gen_frame}: " f"expected 0x{frame.checksum():02X}, got 0x{checksum:02X}" ) return frame, data def checksum(self) -> t.uint8_t: """ Calculates the FCS of the payload. """ checksum = functools.reduce(lambda a, b: a ^ b, self.payload.serialize()) return t.uint8_t(checksum) def serialize(self) -> bytes: return ( self.SOF.serialize() + self.payload.serialize() + self.checksum().serialize() )