SXXXXXXX_PyBusMonitor1553/pybusmonitor1553/lib1553/fields.py
2025-12-09 15:52:18 +01:00

182 lines
5.9 KiB
Python

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)