Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 46 additions & 32 deletions space_packet_parser/xtce/parameter_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
class ParameterType(common.AttrComparable, common.XmlObject, metaclass=ABCMeta):
"""Abstract base class for XTCE parameter types"""

def __init__(self, name: str, encoding: encodings.DataEncoding, unit: str | None = None):
def __init__(self, name: str, encoding: encodings.DataEncoding, unit: str | tuple[str, ...] | None = None):
"""Constructor

Parameters
Expand All @@ -25,8 +25,8 @@ def __init__(self, name: str, encoding: encodings.DataEncoding, unit: str | None
Parameter type name. Usually something like 'MSN__PARAM_Type'
encoding : DataEncoding
How the data is encoded. e.g. IntegerDataEncoding, StringDataEncoding, etc.
unit : Optional[str]
String describing the unit for the stored value.
unit : Optional[Union[str, tuple[str, ...]]]
Unit for stored value. String for a single unit or tuple of strings for compound units.
"""
if name is None:
raise ValueError("Parameter Type name attribute is required.")
Expand Down Expand Up @@ -96,39 +96,46 @@ def to_xml(self, *, elmaker: ElementMaker) -> ElementTree.Element:
param_type_element = getattr(elmaker, self.__class__.__name__)(name=self.name)

if self.unit:
param_type_element.append(elmaker.UnitSet(elmaker.Unit(self.unit)))
if isinstance(self.unit, tuple):
unit_elements = [elmaker.Unit(u) for u in self.unit]
param_type_element.append(elmaker.UnitSet(*unit_elements))
else:
param_type_element.append(elmaker.UnitSet(elmaker.Unit(self.unit)))

param_type_element.append(self.encoding.to_xml(elmaker=elmaker))
return param_type_element

@staticmethod
def get_units(parameter_type_element: ElementTree.Element) -> str | None:
"""Finds the units associated with a parameter type element and parsed them to return a unit string.
We assume only one <xtce:Unit> but this could be extended to support multiple units.
See section 4.3.2.2.4 of CCSDS 660.1-G-1
def get_units(parameter_type_element: ElementTree.Element) -> str | tuple[str, ...] | None:
"""Finds the units associated with a parameter type element and parses them.

Supports both single and multiple <xtce:Unit> elements (compound units).
See section 4.3.2.2.4 of CCSDS 660.1-G-1.

Parameters
----------
parameter_type_element : ElementTree.Element
The parameter type element
The parameter type element.

Returns
-------
: Union[str, None]
Unit string or None if no units are defined
: Union[str, tuple[str, ...], None]
- A string if a single unit is defined
- A tuple of strings if multiple units are defined
- None if no units are defined
"""
# Assume we are not parsing a Time Parameter Type, which stores units differently

units = parameter_type_element.findall("UnitSet/Unit")
# TODO: Implement multiple unit elements for compound unit definitions
if len(units) > 1:
raise NotImplementedError(
f"Found {len(units)} <xtce:Unit> elements in a single <xtce:UnitSet>."
f"This is supported in the standard but is not yet supported by this library."
)
if units:
return " ".join([u.text for u in units])
# Units are optional so return None if they aren't specified
return None
units = [u.text for u in units if u.text]

if not units:
# Units are optional so return None if they aren't specified
return None
elif len(units) == 1:
return units[0]
else:
return tuple(units)

@staticmethod
def get_data_encoding(parameter_type_element: ElementTree.Element) -> encodings.DataEncoding | None:
Expand Down Expand Up @@ -181,7 +188,7 @@ def parse_value(self, packet: spp.SpacePacket) -> common.ParameterDataTypes:
class StringParameterType(ParameterType):
"""<xtce:StringParameterType>"""

def __init__(self, name: str, encoding: encodings.StringDataEncoding, unit: str | None = None):
def __init__(self, name: str, encoding: encodings.StringDataEncoding, unit: str | tuple[str, ...] | None = None):
"""Constructor

Parameters
Expand All @@ -190,8 +197,8 @@ def __init__(self, name: str, encoding: encodings.StringDataEncoding, unit: str
Parameter type name. Usually something like 'MSN__PARAM_Type'
encoding : StringDataEncoding
Must be a StringDataEncoding object since strings can't be encoded other ways.
unit : Optional[str]
String describing the unit for the stored value.
unit : Optional[Union[str, tuple[str, ...]]]
Unit for stored value. String for a single unit or tuple of strings for compound units.
"""
if not isinstance(encoding, encodings.StringDataEncoding):
raise ValueError("StringParameterType may only be instantiated with a StringDataEncoding encoding.")
Expand All @@ -214,15 +221,17 @@ class FloatParameterType(ParameterType):
class EnumeratedParameterType(ParameterType):
"""<xtce:EnumeratedParameterType>"""

def __init__(self, name: str, encoding: encodings.DataEncoding, enumeration: dict, unit: str | None = None):
def __init__(
self, name: str, encoding: encodings.DataEncoding, enumeration: dict, unit: str | tuple[str, ...] | None = None
):
"""Constructor

Parameters
----------
name : str
Parameter type name.
unit : str
Unit string for stored value.
unit : Optional[Union[str, tuple[str, ...]]]
Unit for stored value. String for a single unit or tuple of strings for compound units.
encoding : DataEncoding
How the data is encoded. e.g. IntegerDataEncoding.
enumeration : dict
Expand Down Expand Up @@ -282,10 +291,15 @@ def to_xml(self, *, elmaker: ElementMaker) -> ElementTree.Element:
-------
: ElementTree.Element
"""

param_type_element = getattr(elmaker, self.__class__.__name__)(name=self.name)

if self.unit:
param_type_element.append(elmaker.UnitSet(elmaker.Unit(self.unit)))
if isinstance(self.unit, tuple):
unit_elements = [elmaker.Unit(u) for u in self.unit]
param_type_element.append(elmaker.UnitSet(*unit_elements))
else:
param_type_element.append(elmaker.UnitSet(elmaker.Unit(self.unit)))

Comment thread
blakedehaas marked this conversation as resolved.
param_type_element.append(self.encoding.to_xml(elmaker=elmaker))

Expand Down Expand Up @@ -378,7 +392,7 @@ def parse_value(self, packet: spp.SpacePacket) -> common.StrParameter:
class BinaryParameterType(ParameterType):
"""<xtce:BinaryParameterType>"""

def __init__(self, name: str, encoding: encodings.BinaryDataEncoding, unit: str | None = None):
def __init__(self, name: str, encoding: encodings.BinaryDataEncoding, unit: str | tuple[str, ...] | None = None):
"""Constructor

Parameters
Expand All @@ -387,8 +401,8 @@ def __init__(self, name: str, encoding: encodings.BinaryDataEncoding, unit: str
Parameter type name. Usually something like 'MSN__PARAM_Type'
encoding : BinaryDataEncoding
Must be a BinaryDataEncoding object since binary data can't be encoded other ways.
unit : Optional[str]
String describing the unit for the stored value.
unit : Optional[Union[str, tuple[str, ...]]]
Unit for stored value. String for a single unit or tuple of strings for compound units.
"""
if not isinstance(encoding, encodings.BinaryDataEncoding):
raise ValueError("BinaryParameterType may only be instantiated with a BinaryDataEncoding encoding.")
Expand All @@ -399,7 +413,7 @@ def __init__(self, name: str, encoding: encodings.BinaryDataEncoding, unit: str
class BooleanParameterType(ParameterType):
"""<xtce:BooleanParameterType>"""

def __init__(self, name: str, encoding: encodings.DataEncoding, unit: str | None = None):
def __init__(self, name: str, encoding: encodings.DataEncoding, unit: str | tuple[str, ...] | None = None):
"""Constructor that just issues a warning if the encoding is String or Binary"""
if isinstance(encoding, (encodings.BinaryDataEncoding, encodings.StringDataEncoding)):
warnings.warn(
Expand Down
93 changes: 93 additions & 0 deletions tests/unit/test_xtce/test_parameter_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,22 @@ def test_string_parameter_type(elmaker, xtce_parser, xml_string: str, expectatio
),
),
),
(
f"""
<xtce:IntegerParameterType xmlns:xtce="{XTCE_1_2_XMLNS}" name="TEST_INT_Type">
<xtce:UnitSet>
<xtce:Unit>m</xtce:Unit>
<xtce:Unit>s</xtce:Unit>
</xtce:UnitSet>
<xtce:IntegerDataEncoding sizeInBits="16" encoding="unsigned"/>
</xtce:IntegerParameterType>
""",
parameter_types.IntegerParameterType(
name="TEST_INT_Type",
unit=("m", "s"),
encoding=encodings.IntegerDataEncoding(size_in_bits=16, encoding="unsigned"),
),
),
],
)
def test_integer_parameter_type(elmaker, xtce_parser, xml_string: str, expectation):
Expand Down Expand Up @@ -289,6 +305,22 @@ def test_integer_parameter_type(elmaker, xtce_parser, xml_string: str, expectati
),
),
),
(
f"""
<xtce:FloatParameterType xmlns:xtce="{XTCE_1_2_XMLNS}" name="TEST_INT_Type">
<xtce:UnitSet>
<xtce:Unit>m</xtce:Unit>
<xtce:Unit>s</xtce:Unit>
</xtce:UnitSet>
<xtce:FloatDataEncoding sizeInBits="16"/>
</xtce:FloatParameterType>
""",
parameter_types.FloatParameterType(
name="TEST_INT_Type",
unit=("m", "s"),
encoding=encodings.FloatDataEncoding(size_in_bits=16, encoding="IEEE754"),
),
),
],
)
def test_float_parameter_type(elmaker, xtce_parser, xml_string: str, expectation):
Expand Down Expand Up @@ -420,6 +452,47 @@ def test_float_parameter_type(elmaker, xtce_parser, xml_string: str, expectation
},
),
),
(
f"""
<xtce:EnumeratedParameterType xmlns:xtce="{XTCE_1_2_XMLNS}" name="TEST_ENUM_Type">
<xtce:UnitSet>
<xtce:Unit>m</xtce:Unit>
<xtce:Unit>s</xtce:Unit>
</xtce:UnitSet>
<xtce:IntegerDataEncoding sizeInBits="2" encoding="unsigned"/>
<xtce:EnumerationList>
<xtce:Enumeration label="BOOT_POR" value="0"/>
<xtce:Enumeration label="BOOT_RETURN" value="1"/>
</xtce:EnumerationList>
</xtce:EnumeratedParameterType>
""",
parameter_types.EnumeratedParameterType(
name="TEST_ENUM_Type",
unit=("m", "s"),
encoding=encodings.IntegerDataEncoding(size_in_bits=2, encoding="unsigned"),
enumeration={0: "BOOT_POR", 1: "BOOT_RETURN"},
),
),
(
f"""
<xtce:EnumeratedParameterType xmlns:xtce="{XTCE_1_2_XMLNS}" name="TEST_ENUM_Type">
<xtce:UnitSet>
<xtce:Unit>meters</xtce:Unit>
</xtce:UnitSet>
<xtce:IntegerDataEncoding sizeInBits="2" encoding="unsigned"/>
<xtce:EnumerationList>
<xtce:Enumeration label="BOOT_POR" value="0"/>
<xtce:Enumeration label="BOOT_RETURN" value="1"/>
</xtce:EnumerationList>
</xtce:EnumeratedParameterType>
""",
parameter_types.EnumeratedParameterType(
name="TEST_ENUM_Type",
unit="meters",
encoding=encodings.IntegerDataEncoding(size_in_bits=2, encoding="unsigned"),
enumeration={0: "BOOT_POR", 1: "BOOT_RETURN"},
),
),
],
)
def test_enumerated_parameter_type(elmaker, xtce_parser, xml_string: str, expectation):
Expand Down Expand Up @@ -517,6 +590,26 @@ def test_enumerated_parameter_type(elmaker, xtce_parser, xml_string: str, expect
encoding=encodings.BinaryDataEncoding(size_reference_parameter="SizeFromThisParameter"),
),
),
(
f"""
<xtce:BinaryParameterType xmlns:xtce="{XTCE_1_2_XMLNS}" name="TEST_PARAM_Type">
<xtce:UnitSet>
<xtce:Unit>m</xtce:Unit>
<xtce:Unit>s</xtce:Unit>
</xtce:UnitSet>
<xtce:BinaryDataEncoding>
<xtce:SizeInBits>
<xtce:FixedValue>128</xtce:FixedValue>
</xtce:SizeInBits>
</xtce:BinaryDataEncoding>
</xtce:BinaryParameterType>
""",
parameter_types.BinaryParameterType(
name="TEST_PARAM_Type",
unit=("m", "s"),
encoding=encodings.BinaryDataEncoding(fixed_size_in_bits=128),
),
),
Comment on lines +593 to +612
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep! Good catch. @blakedehaas Let's get the round tripping of the unit set tested. Both parsing from XML and serialization to XML. You addressed the parsing side above but we should also have a serialization test to prove that we serialize multiple unit elements correctly.

],
)
def test_binary_parameter_type(elmaker, xtce_parser, xml_string: str, expectation):
Expand Down
Loading