from __future__ import annotations from dataclasses import dataclass @dataclass(frozen=True) class UartTiming: baud: int = 38_400 data_bits: int = 8 parity: str = "N" stop_bits: int = 1 start_bits: int = 1 def __post_init__(self) -> None: parity = self.parity.upper() if parity not in {"N", "E", "O"}: raise ValueError("parity must be N, E, or O") object.__setattr__(self, "parity", parity) @property def parity_bits(self) -> int: return 0 if self.parity == "N" else 1 @property def bits_per_character(self) -> int: return self.start_bits + self.data_bits + self.parity_bits + self.stop_bits def seconds_per_character(self) -> float: return self.bits_per_character / max(1, self.baud) def micros_per_character(self) -> float: return 1_000_000.0 * self.seconds_per_character() def cycles_per_character(self, clock_hz: int) -> int: return max(1, round(max(1, clock_hz) * self.seconds_per_character())) def summary(self, clock_hz: int) -> str: return ( f"uart_{self.data_bits}{self.parity}{self.stop_bits} " f"baud={self.baud} byte_us={self.micros_per_character():.3f} " f"byte_cycles={self.cycles_per_character(clock_hz)}" ) @classmethod def from_format(cls, text: str, *, baud: int = 38_400) -> "UartTiming": normalized = text.strip().upper() if len(normalized) != 3 or normalized[0] not in "78" or normalized[1] not in "NEO" or normalized[2] not in "12": raise ValueError(f"unsupported UART format {text!r}; expected 8E1, 8N1, 8O1, etc.") return cls(baud=baud, data_bits=int(normalized[0]), parity=normalized[1], stop_bits=int(normalized[2])) @classmethod def from_sci_smr(cls, smr: int, *, baud: int = 38_400) -> "UartTiming": data_bits = 7 if smr & 0x40 else 8 if smr & 0x20: parity = "O" if smr & 0x10 else "E" else: parity = "N" stop_bits = 2 if smr & 0x08 else 1 return cls(baud=baud, data_bits=data_bits, parity=parity, stop_bits=stop_bits) __all__ = ["UartTiming"]