"""TLV configuration serializer for OpenDisplay devices.""" from __future__ import annotations import struct from typing import TYPE_CHECKING if TYPE_CHECKING: from ..models.config import ( BinaryInputs, DataBus, DisplayConfig, GlobalConfig, LedConfig, ManufacturerData, PassiveBuzzer, PowerOption, SecurityConfig, SensorData, SystemConfig, TouchController, WifiConfig, ) # Packet type IDs (same as config_parser.py) PACKET_TYPE_SYSTEM = 0x01 PACKET_TYPE_MANUFACTURER = 0x02 PACKET_TYPE_POWER = 0x04 PACKET_TYPE_DISPLAY = 0x20 PACKET_TYPE_LED = 0x21 PACKET_TYPE_SENSOR = 0x23 PACKET_TYPE_DATABUS = 0x24 PACKET_TYPE_BINARY_INPUT = 0x25 PACKET_TYPE_WIFI_CONFIG = 0x26 PACKET_TYPE_SECURITY_CONFIG = 0x27 PACKET_TYPE_TOUCH_CONTROLLER = 0x28 PACKET_TYPE_PASSIVE_BUZZER = 0x29 def calculate_config_crc(data: bytes) -> int: """Calculate CRC32 and return lower 16 bits. Uses standard CRC32 algorithm (same as zlib/firmware) but only returns the lower 16 bits for backwards compatibility with firmware. Firmware source: main.cpp:1543-1556, 1912-1914 The firmware calculates full CRC32 but only uses lower 16 bits. Args: data: Config data to calculate CRC over Returns: Lower 16 bits of CRC32 value """ crc = 0xFFFFFFFF for byte in data: crc ^= byte for _ in range(8): if crc & 1: crc = (crc >> 1) ^ 0xEDB88320 else: crc = crc >> 1 crc32 = (~crc) & 0xFFFFFFFF return crc32 & 0xFFFF # Return lower 16 bits only def serialize_system_config(config: SystemConfig) -> bytes: """Serialize SystemConfig to 22 bytes. Format (little-endian): - ic_type: uint16 - communication_modes: uint8 - device_flags: uint8 - pwr_pin: uint8 - reserved: 15 bytes - pwr_pin_2: uint8 - pwr_pin_3: uint8 Args: config: SystemConfig instance Returns: 22 bytes of serialized data """ data = struct.pack( " bytes: """Serialize ManufacturerData to 22 bytes. Format (little-endian): - manufacturer_id: uint16 - board_type: uint8 - board_revision: uint8 - reserved: 18 bytes Args: config: ManufacturerData instance Returns: 22 bytes of serialized data """ data = struct.pack( " bytes: """Serialize PowerOption to 30 bytes. Format (little-endian): - power_mode: uint8 - battery_capacity_mah: 3 bytes (24-bit LE) - sleep_timeout_ms: uint16 - tx_power: int8 (signed) - sleep_flags: uint8 - battery_sense_pin: uint8 - battery_sense_enable_pin: uint8 - battery_sense_flags: uint8 - capacity_estimator: uint8 - voltage_scaling_factor: uint16 - deep_sleep_current_ua: uint32 - deep_sleep_time_seconds: uint16 - reserved: 10 bytes Args: config: PowerOption instance Returns: 30 bytes of serialized data """ # Start with power_mode data = bytes([config.power_mode]) # Add 3-byte battery capacity (little-endian) if isinstance(config.battery_capacity_mah, bytes): data += config.battery_capacity_mah[:3] else: # Convert int to 3 bytes little-endian capacity_bytes = config.battery_capacity_mah.to_bytes(3, byteorder="little") data += capacity_bytes # Pack remaining fields data += struct.pack( " bytes: """Serialize DisplayConfig to 46 bytes. Format (little-endian): - instance_number: uint8 - display_technology: uint8 - panel_ic_type: uint16 - pixel_width: uint16 - pixel_height: uint16 - active_width_mm: uint16 - active_height_mm: uint16 - tag_type: uint16 - rotation: uint8 - reset_pin: uint8 - busy_pin: uint8 - dc_pin: uint8 - cs_pin: uint8 - data_pin: uint8 - partial_update_support: uint8 - color_scheme: uint8 - transmission_modes: uint8 - clk_pin: uint8 - reserved_pins: 7 bytes - reserved: 15 bytes Args: config: DisplayConfig instance Returns: 46 bytes of serialized data """ data = struct.pack( " bytes: """Serialize LedConfig to 22 bytes. Format: - instance_number: uint8 - led_type: uint8 - led_1_r: uint8 - led_2_g: uint8 - led_3_b: uint8 - led_4: uint8 - led_flags: uint8 - reserved: 15 bytes Args: config: LedConfig instance Returns: 22 bytes of serialized data """ data = struct.pack( " bytes: """Serialize SensorData to 30 bytes. Format (little-endian): - instance_number: uint8 - sensor_type: uint16 - bus_id: uint8 - i2c_addr_7bit: uint8 - msd_data_start_byte: uint8 - reserved: 24 bytes Args: config: SensorData instance Returns: 30 bytes of serialized data """ data = struct.pack( " bytes: """Serialize DataBus to 30 bytes. Format (little-endian): - instance_number: uint8 - bus_type: uint8 - pin_1 through pin_7: 7x uint8 - bus_speed_hz: uint32 - bus_flags: uint8 - pullups: uint8 - pulldowns: uint8 - reserved: 14 bytes Args: config: DataBus instance Returns: 30 bytes of serialized data """ data = struct.pack( " bytes: """Serialize BinaryInputs to 30 bytes. Format: - instance_number: uint8 - input_type: uint8 - display_as: uint8 - reserved_pins: 8 bytes - input_flags: uint8 - invert: uint8 - pullups: uint8 - pulldowns: uint8 - button_data_byte_index: uint8 - reserved: 14 bytes Args: config: BinaryInputs instance Returns: 30 bytes of serialized data """ data = struct.pack( " bytes: """Serialize SecurityConfig to 64 bytes.""" data = bytes([config.encryption_enabled & 0xFF]) data += config.encryption_key[:16].ljust(16, b"\x00") data += config.session_timeout_seconds.to_bytes(2, "little") data += bytes([config.flags & 0xFF, config.reset_pin & 0xFF]) reserved = config.reserved if config.reserved else b"\x00" * 43 return data + reserved[:43] def serialize_touch_controller(config: TouchController) -> bytes: """Serialize TouchController to 32 bytes.""" data = struct.pack( " bytes: """Serialize PassiveBuzzer to 32 bytes.""" data = struct.pack( " bytes: """Serialize WifiConfig to 160 bytes.""" return config.to_bytes() def serialize_config(config: GlobalConfig) -> bytes: """Serialize complete GlobalConfig to TLV binary format. Format: [2 bytes: padding/reserved] [1 byte: version] [TLV packets...] [2 bytes: CRC16 (lower 16 bits of CRC32)] TLV Packet Format: [1 byte: packet_number] # 0-3 for repeatable types [1 byte: packet_type] # 0x01, 0x02, 0x04, 0x20-0x26 [N bytes: fixed-size data] Args: config: GlobalConfig to serialize Returns: Complete config data ready to send to device Raises: ValueError: If config exceeds maximum size (4096 bytes) """ if config.system is None or config.manufacturer is None or config.power is None: raise ValueError("Config is missing required packets") # Start with 2 bytes padding and 1 byte version packet_data = b"\x00\x00" # 2 bytes padding packet_data += bytes([config.version]) # 1 byte version # Serialize single-instance packets packet_data += bytes([0, PACKET_TYPE_SYSTEM]) packet_data += serialize_system_config(config.system) packet_data += bytes([0, PACKET_TYPE_MANUFACTURER]) packet_data += serialize_manufacturer_data(config.manufacturer) packet_data += bytes([0, PACKET_TYPE_POWER]) packet_data += serialize_power_option(config.power) # Serialize repeatable packets for i, display in enumerate(config.displays): if i >= 4: # Max 4 instances break packet_data += bytes([i, PACKET_TYPE_DISPLAY]) packet_data += serialize_display_config(display) for i, led in enumerate(config.leds): if i >= 4: # Max 4 instances break packet_data += bytes([i, PACKET_TYPE_LED]) packet_data += serialize_led_config(led) for i, sensor in enumerate(config.sensors): if i >= 4: # Max 4 instances break packet_data += bytes([i, PACKET_TYPE_SENSOR]) packet_data += serialize_sensor_data(sensor) for i, bus in enumerate(config.data_buses): if i >= 4: # Max 4 instances break packet_data += bytes([i, PACKET_TYPE_DATABUS]) packet_data += serialize_data_bus(bus) for i, binary_input in enumerate(config.binary_inputs): if i >= 4: # Max 4 instances break packet_data += bytes([i, PACKET_TYPE_BINARY_INPUT]) packet_data += serialize_binary_inputs(binary_input) if config.wifi_config is not None: packet_data += bytes([0, PACKET_TYPE_WIFI_CONFIG]) packet_data += serialize_wifi_config(config.wifi_config) if config.security_config is not None: packet_data += bytes([0, PACKET_TYPE_SECURITY_CONFIG]) packet_data += serialize_security_config(config.security_config) for i, tc in enumerate(config.touch_controllers): if i >= 4: break packet_data += bytes([i, PACKET_TYPE_TOUCH_CONTROLLER]) packet_data += serialize_touch_controller(tc) for i, bz in enumerate(config.buzzers): if i >= 4: break packet_data += bytes([i, PACKET_TYPE_PASSIVE_BUZZER]) packet_data += serialize_passive_buzzer(bz) # Validate size (max 4096 bytes including wrapper and CRC) total_size = len(packet_data) + 2 # +2 for CRC if total_size > 4096: raise ValueError(f"Config size {total_size} bytes exceeds maximum 4096 bytes") # Calculate CRC over packet data (excluding CRC itself) crc16 = calculate_config_crc(packet_data) # Append CRC as 2 bytes little-endian packet_data += crc16.to_bytes(2, byteorder="little") return packet_data