import ctypes from enum import Enum class Field: """ Base class for 1553 Data Word fields descriptors. Handles the logic to extract specific bits from a 16-bit word following MIL-STD-1553 convention (Bit 0 = MSB, Bit 15 = LSB). """ def __init__(self, word_index, start_bit, width=1): """ Args: word_index (int): Index of the 16-bit word in the message (0-based). start_bit (int): Starting bit index as per ICD (0=MSB, 15=LSB). width (int): Number of bits occupied by the field. """ self.word_index = word_index self.start_bit = start_bit self.width = width # Calculate shift required for Little Endian architecture # to match 1553 Big Endian bit numbering (0=MSB). # Example: Bit 0 (MSB) with width 1 -> Shift 15 self._shift = 16 - (start_bit + width) self._mask = (1 << width) - 1 def _get_raw_value(self, instance): """Extracts the raw unsigned integer from the message data.""" # instance.data is expected to be a ctypes array or list of ints if self.word_index >= len(instance.data): return 0 # Fail safe for incomplete messages word_val = instance.data[self.word_index] return (word_val >> self._shift) & self._mask def _set_raw_value(self, instance, value): """Writes the raw unsigned integer back into the message data.""" if self.word_index >= len(instance.data): return # Fail safe current_word = instance.data[self.word_index] # Clear the bits clear_mask = ~(self._mask << self._shift) current_word &= clear_mask # Set the new bits (masked to width) val_to_write = (int(value) & self._mask) << self._shift current_word |= val_to_write instance.data[self.word_index] = current_word class BitField(Field): """ Represents a generic numeric field (Unsigned). Used for flags, counters, or raw values without scaling. """ def __get__(self, instance, owner): if instance is None: return self return self._get_raw_value(instance) def __set__(self, instance, value): self._set_raw_value(instance, value) class EnumField(Field): """ Represents a field mapped to a Python Enum. Automatically converts between the raw integer and the Enum member. """ def __init__(self, word_index, start_bit, enum_cls, width=None): """ Args: enum_cls (Enum): The Enum class to map to. width (int, optional): Auto-calculated from enum length if not provided. """ # Calculate width based on max enum value if not provided, # but usually ICD defines width explicitly. if width is None: # Minimal width to fit the max value max_val = max([e.value for e in enum_cls]) width = max_val.bit_length() super().__init__(word_index, start_bit, width) self.enum_cls = enum_cls def __get__(self, instance, owner): if instance is None: return self raw = self._get_raw_value(instance) try: return self.enum_cls(raw) except ValueError: # Return raw value if not found in Enum (e.g. reserved values) return raw def __set__(self, instance, value): if isinstance(value, self.enum_cls): val_to_set = value.value else: val_to_set = int(value) self._set_raw_value(instance, val_to_set) class ScaledField(Field): """ Represents a numeric field with a scale factor (LSB). Can be Signed (2's complement) or Unsigned. Value = Raw * LSB """ def __init__(self, word_index, start_bit, width, lsb_value, signed=False): super().__init__(word_index, start_bit, width) self.lsb_value = lsb_value self.signed = signed def __get__(self, instance, owner): if instance is None: return self raw = self._get_raw_value(instance) # Handle 2's complement for signed fields if self.signed: # Check sign bit (highest bit of the field) sign_mask = 1 << (self.width - 1) if raw & sign_mask: # Calculate negative value raw = raw - (1 << self.width) return float(raw * self.lsb_value) def __set__(self, instance, value): # Convert physical value to raw steps raw = int(round(value / self.lsb_value)) if self.signed: # Handle negative input if raw < 0: raw = (1 << self.width) + raw self._set_raw_value(instance, raw) class ASCIIField(Field): """ Represents 2 characters packed into a 16-bit word. MSB (bits 0-7) = Character 1 LSB (bits 8-15) = Character 2 """ def __init__(self, word_index): # Takes the whole word super().__init__(word_index, 0, 16) def __get__(self, instance, owner): if instance is None: return self raw = self._get_raw_value(instance) # In 1553: # High Byte (0xFF00) is first char # Low Byte (0x00FF) is second char char1 = (raw >> 8) & 0xFF char2 = raw & 0xFF # Return string, handling non-printable bytes safely s = "" s += chr(char1) if 32 <= char1 <= 126 else '?' s += chr(char2) if 32 <= char2 <= 126 else '?' return s def __set__(self, instance, value): if not isinstance(value, str) or len(value) != 2: raise ValueError("ASCIIField requires a string of exactly 2 characters") char1 = ord(value[0]) char2 = ord(value[1]) raw = (char1 << 8) | char2 self._set_raw_value(instance, raw)