1
1
"""Module for parsing XTCE xml files to specify packet format"""
2
2
# Standard
3
- from abc import ABCMeta
3
+ from abc import ABCMeta , abstractmethod
4
4
from collections import namedtuple
5
5
from dataclasses import dataclass , field
6
6
import inspect
@@ -111,6 +111,7 @@ class MatchCriteria(AttrComparable, metaclass=ABCMeta):
111
111
}
112
112
113
113
@classmethod
114
+ @abstractmethod
114
115
def from_match_criteria_xml_element (cls , element : ElementTree .Element , ns : dict ):
115
116
"""Abstract classmethod to create a match criteria object from an XML element.
116
117
@@ -127,6 +128,7 @@ def from_match_criteria_xml_element(cls, element: ElementTree.Element, ns: dict)
127
128
"""
128
129
raise NotImplementedError ()
129
130
131
+ @abstractmethod
130
132
def evaluate (self , parsed_data : dict , current_parsed_value : Optional [Union [int , float ]] = None ) -> bool :
131
133
"""Evaluate match criteria down to a boolean.
132
134
@@ -609,6 +611,7 @@ class Calibrator(AttrComparable, metaclass=ABCMeta):
609
611
"""Abstract base class for XTCE calibrators"""
610
612
611
613
@classmethod
614
+ @abstractmethod
612
615
def from_calibrator_xml_element (cls , element : ElementTree .Element , ns : dict ) -> 'Calibrator' :
613
616
"""Abstract classmethod to create a default_calibrator object from an XML element.
614
617
@@ -625,6 +628,7 @@ def from_calibrator_xml_element(cls, element: ElementTree.Element, ns: dict) ->
625
628
"""
626
629
return NotImplemented
627
630
631
+ @abstractmethod
628
632
def calibrate (self , uncalibrated_value : Union [int , float ]) -> Union [int , float ]:
629
633
"""Takes an integer-encoded or float-encoded value and returns a calibrated version.
630
634
@@ -1035,6 +1039,7 @@ class DataEncoding(AttrComparable, metaclass=ABCMeta):
1035
1039
"""Abstract base class for XTCE data encodings"""
1036
1040
1037
1041
@classmethod
1042
+ @abstractmethod
1038
1043
def from_data_encoding_xml_element (cls , element : ElementTree .Element , ns : dict ) -> 'DataEncoding' :
1039
1044
"""Abstract classmethod to create a data encoding object from an XML element.
1040
1045
@@ -1456,6 +1461,7 @@ def __init__(self, size_in_bits: int,
1456
1461
def _calculate_size (self , packet : Packet ) -> int :
1457
1462
return self .size_in_bits
1458
1463
1464
+ @abstractmethod
1459
1465
def _get_raw_value (self , packet : Packet ) -> Union [int , float ]:
1460
1466
"""Read the raw value from the packet data
1461
1467
@@ -1472,6 +1478,16 @@ def _get_raw_value(self, packet: Packet) -> Union[int, float]:
1472
1478
"""
1473
1479
raise NotImplementedError ()
1474
1480
1481
+ @staticmethod
1482
+ def twos_complement (val : int , bit_width : int ) -> int :
1483
+ """Take the twos complement of val
1484
+
1485
+ Used when parsing ints and some floats
1486
+ """
1487
+ if (val & (1 << (bit_width - 1 ))) != 0 : # if sign bit is set e.g., 8bit: 128-255
1488
+ return val - (1 << bit_width ) # compute negative value
1489
+ return val
1490
+
1475
1491
def parse_value (self ,
1476
1492
packet : Packet ,
1477
1493
** kwargs ) -> Tuple [Union [int , float ], Union [int , float ]]:
@@ -1522,10 +1538,8 @@ def _get_raw_value(self, packet: Packet) -> int:
1522
1538
)
1523
1539
if self .encoding == 'unsigned' :
1524
1540
return val
1525
- # It is a signed integer and we need to take into account the first bit
1526
- if (val & (1 << (self .size_in_bits - 1 ))) != 0 : # if sign bit is set e.g., 8bit: 128-255
1527
- return val - (1 << self .size_in_bits ) # compute negative value
1528
- return val # return positive value as is
1541
+ # It is a signed integer, and we need to take into account the first bit
1542
+ return self .twos_complement (val , self .size_in_bits )
1529
1543
1530
1544
@classmethod
1531
1545
def from_data_encoding_xml_element (cls , element : ElementTree .Element , ns : dict ) -> 'IntegerDataEncoding' :
@@ -1561,8 +1575,6 @@ def __init__(self, size_in_bits: int, encoding: str = 'IEEE-754',
1561
1575
context_calibrators : Optional [List [ContextCalibrator ]] = None ):
1562
1576
"""Constructor
1563
1577
1564
- # TODO: Implement MIL-1650A encoding option
1565
-
1566
1578
Parameters
1567
1579
----------
1568
1580
size_in_bits : int
@@ -1581,33 +1593,69 @@ def __init__(self, size_in_bits: int, encoding: str = 'IEEE-754',
1581
1593
if encoding not in self ._supported_encodings :
1582
1594
raise ValueError (f"Invalid encoding type { encoding } for float data. "
1583
1595
f"Must be one of { self ._supported_encodings } ." )
1584
- if encoding == 'MIL-1750A' :
1585
- raise NotImplementedError ("MIL-1750A encoded floats are not supported by this library yet." )
1596
+ if encoding == 'MIL-1750A' and size_in_bits != 32 :
1597
+ raise ValueError ("MIL-1750A encoded floats must be 32 bits, per the MIL-1750A spec. See "
1598
+ "https://www.xgc-tek.com/manuals/mil-std-1750a/c191.html#AEN324" )
1586
1599
if encoding == 'IEEE-754' and size_in_bits not in (16 , 32 , 64 ):
1587
1600
raise ValueError (f"Invalid size_in_bits value for IEEE-754 FloatDataEncoding, { size_in_bits } . "
1588
1601
"Must be 16, 32, or 64." )
1589
1602
super ().__init__ (size_in_bits , encoding = encoding , byte_order = byte_order ,
1590
1603
default_calibrator = default_calibrator , context_calibrators = context_calibrators )
1591
1604
1592
- if self .byte_order == "leastSignificantByteFirst" :
1593
- self ._struct_format = "<"
1605
+ if self .encoding == "MIL-1750A" :
1606
+ def _mil_parse_func (mil_bytes : bytes ):
1607
+ """Parsing function for MIL-1750A floats"""
1608
+ # MIL 1750A floats are always 32 bit
1609
+ # See: https://www.xgc-tek.com/manuals/mil-std-1750a/c191.html#AEN324
1610
+ #
1611
+ # MSB LSB MSB LSB
1612
+ # ------------------------------------------------------------------
1613
+ # | S| Mantissa | Exponent |
1614
+ # ------------------------------------------------------------------
1615
+ # 0 1 23 24 31
1616
+ bytes_as_int = int .from_bytes (mil_bytes , byteorder = 'big' )
1617
+ exponent = bytes_as_int & 0xFF # last 8 bits
1618
+ mantissa = (bytes_as_int >> 8 ) & 0xFFFFFFFF # bits 0 through 23 (24 bits)
1619
+ # We include the sign bit with the mantissa because we can just take the twos complement
1620
+ # of it directly and use it in the final calculation for the value
1621
+
1622
+ # Both mantissa and exponent are stored as twos complement with no bias
1623
+ exponent = self .twos_complement (exponent , 8 )
1624
+ mantissa = self .twos_complement (mantissa , 24 )
1625
+
1626
+ # Calculate float value using native Python floats, which are more precise
1627
+ return mantissa * (2.0 ** (exponent - (24 - 1 )))
1628
+
1629
+ # Set up the parsing function just once, so we can use it repeatedly with _get_raw_value
1630
+ self .parse_func = _mil_parse_func
1594
1631
else :
1595
- # Big-endian is the default
1596
- self ._struct_format = ">"
1632
+ if self .byte_order == "leastSignificantByteFirst" :
1633
+ self ._struct_format = "<"
1634
+ else :
1635
+ # Big-endian is the default
1636
+ self ._struct_format = ">"
1637
+
1638
+ if self .size_in_bits == 16 :
1639
+ self ._struct_format += "e"
1640
+ elif self .size_in_bits == 32 :
1641
+ self ._struct_format += "f"
1642
+ elif self .size_in_bits == 64 :
1643
+ self ._struct_format += "d"
1644
+
1645
+ def ieee_parse_func (data : bytes ):
1646
+ """Parsing function for IEEE floats"""
1647
+ # The packet data we got back is always extracted in big-endian order
1648
+ # but the struct format code contains the endianness of the float data
1649
+ return struct .unpack (self ._struct_format , data )[0 ]
1597
1650
1598
- if self .size_in_bits == 16 :
1599
- self ._struct_format += "e"
1600
- elif self .size_in_bits == 32 :
1601
- self ._struct_format += "f"
1602
- elif self .size_in_bits == 64 :
1603
- self ._struct_format += "d"
1651
+ # Set up the parsing function just once, so we can use it repeatedly with _get_raw_value
1652
+ self .parse_func : callable = ieee_parse_func
1604
1653
1605
1654
def _get_raw_value (self , packet ):
1606
1655
"""Read the data in as bytes and return a float representation."""
1607
1656
data = packet .read_as_bytes (self .size_in_bits )
1608
- # The packet data we got back is always extracted in big-endian order
1609
- # but the struct format code contains the endianness of the float data
1610
- return struct .unpack (self ._struct_format , data )[0 ]
1657
+ # The parsing function is fully set during initialization to save time during parsing
1658
+ return self .parse_func (data )
1611
1659
1612
1660
@classmethod
1613
1661
def from_data_encoding_xml_element (cls , element : ElementTree .Element , ns : dict ) -> 'FloatDataEncoding' :
0 commit comments