"""I2C system definition: scaling, data formats, fields, registers, devices."""
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from enum import Enum
from typing import Callable, Literal, Optional
[docs]
class ScalingFunction(ABC):
"""Convert between raw register integers and physical units (forward and inverse)."""
[docs]
@abstractmethod
def to_physical(self, raw: int) -> float:
"""Raw register value → physical units."""
pass
[docs]
@abstractmethod
def to_raw(self, physical: float) -> int:
"""Physical value → raw integer. May raise ``NotImplementedError`` if inverse is not supported."""
pass
[docs]
class LinearScaling(ScalingFunction):
"""Linear scaling: ``physical = offset + gain * raw``.
Example:
>>> # Temperature sensor: 0.1°C per count, -40°C offset
>>> LinearScaling(gain=0.1, offset=-40.0).to_physical(500)
10.0
"""
def __init__(self, gain: float = 1.0, offset: float = 0.0):
self.gain = gain
self.offset = offset
[docs]
def to_physical(self, raw: int) -> float:
return self.offset + self.gain * float(raw)
[docs]
def to_raw(self, physical: float) -> int:
return int(round((physical - self.offset) / self.gain))
[docs]
class CustomScaling(ScalingFunction):
"""User-defined scaling. ``to_raw_fn`` is optional; ``to_raw`` raises ``NotImplementedError`` without it.
Example:
>>> CustomScaling(
... to_physical_fn=lambda x: x / 4095 * 5.0 * 7.2,
... to_raw_fn=lambda v: int(v / 7.2 / 5.0 * 4095),
... )
"""
def __init__(
self,
to_physical_fn: Callable[[int], float],
to_raw_fn: Callable[[float], int] | None = None,
):
self._to_physical_fn = to_physical_fn
self._to_raw_fn = to_raw_fn
[docs]
def to_physical(self, raw: int) -> float:
return self._to_physical_fn(raw)
[docs]
def to_raw(self, physical: float) -> int:
if self._to_raw_fn is None:
raise NotImplementedError("Inverse transformation not provided for this CustomScaling")
return self._to_raw_fn(physical)
[docs]
@dataclass(frozen=True)
class FieldDef:
"""A contiguous bit field within a register (e.g. ``FieldDef("mode", lsb=3, width_bits=2)`` for bits [4:3])."""
name: str
lsb: int
width_bits: int = 1
[docs]
def mask(self) -> int:
"""Bitmask covering this field's bits (e.g. ``lsb=3, width=2`` → ``0b00011000``)."""
return ((1 << self.width_bits) - 1) << self.lsb
[docs]
@dataclass(frozen=True)
class RegisterDef:
"""I2C register: address, data format, power-on default, byte order, and named bit fields."""
alias: str
register: int
default_value: int = 0x00
format: DataFormat = field(default_factory=lambda: DataFormat(transfer_bits=8))
endianness: Literal["little", "big"] = "big"
fields: dict[str, FieldDef] = field(default_factory=dict)
@property
def data_width_bytes(self) -> int:
return self.format.data_width_bytes
[docs]
def field(self, field_name: str) -> FieldDef:
return self.fields[field_name]
[docs]
def pack_data(self, data_raw: int) -> int:
return self.format.pack_data(data_raw)
[docs]
def float_from_raw(self, transfer_raw: int) -> float:
return self.format.float_from_raw(transfer_raw)
[docs]
def raw_from_float(self, physical: float) -> int:
return self.format.raw_from_float(physical)
[docs]
@dataclass
class CommandDef:
"""A named command enum whose members are OR'd together when sent."""
name: str
values: type[Enum]
[docs]
@dataclass
class RegisterDevice:
"""Register-based I2C device (e.g. a GPIO expander). Read/write by register alias."""
name: str
address: int # 7-bit I2C address
addr_width_bytes: int = 1
registers: dict[str, RegisterDef] = field(default_factory=dict)
[docs]
def register(self, register_alias: str) -> RegisterDef:
return self.registers[register_alias]
[docs]
@dataclass
class CommandDevice:
"""Command-based I2C device (e.g. an ADC). Sends a command byte; no register addressing."""
name: str
address: int # 7-bit I2C address
data_format: DataFormat # Required field, no default
valid_commands: dict[str, CommandDef] = field(default_factory=dict)
batch_commands: dict[str, list[Enum]] = field(default_factory=dict)
endianness: Literal["little", "big"] = "big"
_valid_enum_types: set[type] = field(default_factory=set, init=False)
def __post_init__(self):
"""Cache the set of enum types accepted by ``valid_commands`` for fast validation."""
self._valid_enum_types = {cmd_def.values for cmd_def in self.valid_commands.values()}
[docs]
def command(self, command_alias: str) -> CommandDef:
return self.valid_commands[command_alias]
@property
def data_width_bytes(self) -> int:
return self.data_format.data_width_bytes
Device = RegisterDevice | CommandDevice # Union type for device variants
[docs]
@dataclass
class SystemDefinition:
"""Top-level container of all I2C device definitions, passed to ``I2CInterface``.
>>> system = SystemDefinition()
>>> system.add_device(RegisterDevice(name="power_gpio", address=0x20))
>>> system.add_device(CommandDevice(name="adc", address=0x09, data_format=DataFormat(...)))
>>> i2c = I2CInterface(
... name="main_i2c",
... driver=Aardvark(serial_number="123456"),
... system_definition=system,
... )
>>> i2c.write("power_gpio", "LED_OUTPUT_STATE", 0xFF)
"""
devices: dict[str, Device] = field(default_factory=dict) # periph -> Device
[docs]
def add_device(self, dev: Device) -> None:
self.devices[dev.name] = dev
[docs]
def device(self, periph: str) -> Device:
return self.devices[periph]