"""High level object-oriented API for elements that can exist in a packet.
See also:
- packet_types.FixedLength
- packet_types.VariableLength
"""
__author__ = "Daniel da Silva <mail@danieldasilva.org>"
import string
import numpy as np
from .converters import Converter
[docs]
class PacketField:
"""A field contained in a packet."""
def __init__(
self, name, data_type, bit_length, bit_offset=None, byte_order="big", description=None
):
"""
Parameters
----------
name : str
String identifier for the field. The name specified how you may
call upon this data later.
data_type : {'uint', 'int', 'float', 'str', 'fill'}
Data type of the field.
bit_length : int
Number of bits contained in the field.
bit_offset : int, optional
Bit offset into packet, including the primary header which is 48 bits long.
If this is not specified, than the bit offset will the be calculated automatically
from its position inside the packet definition.
byte_order : {'big', 'little'}, or custom string of digits like "4321"
Byte order of the field. Can be "big", "little", or an arbitrary ordering
Specified as a string of digits like "2341". Defaults to big endian.
description: str, optional
Description of the field, used as metadata.
Raises
------
TypeError
If one of the arguments is not of the correct type.
ValueError
data_type or byte_order is invalid
"""
if not isinstance(name, str):
raise TypeError("name parameter must be a str")
if not isinstance(data_type, str):
raise TypeError("data_type parameter must be a str")
if not isinstance(bit_length, (int, np.integer)):
raise TypeError("bit_length parameter must be an int")
if not (bit_offset is None or isinstance(bit_offset, (int, np.integer))):
raise TypeError("bit_offset parameter must be an int")
if not (description is None or isinstance(description, str)):
raise TypeError("description parameter must be a str")
valid_data_types = ("uint", "int", "float", "str", "fill")
if data_type not in valid_data_types:
raise ValueError(f"data_type must be one of {valid_data_types}")
valid_default_byte_orders = ("big", "little")
valid_custom_byte_order = all(char in string.digits for char in byte_order)
if byte_order in valid_default_byte_orders:
byte_order_parse = byte_order
byte_order_post = None
elif valid_custom_byte_order:
byte_order_parse = "big"
byte_order_post = byte_order
else:
raise ValueError(
f"byte_order must be one of {valid_default_byte_orders} or "
"a string like '1234' or '3412'."
)
self._name = name
self._data_type = data_type
self._bit_length = bit_length
self._bit_offset = bit_offset
self._byte_order = byte_order
self._byte_order_parse = byte_order
self._byte_order_post = byte_order_post
self._field_type = "element"
self._array_shape = None
self._array_order = None
self._description = description
def __repr__(self):
values = {k: repr(v) for (k, v) in self.__dict__.items()}
values["cls_name"] = self.__class__.__name__
if values["cls_name"] == "PacketArray":
return (
"{cls_name}(name={_name}, data_type={_data_type}, "
"bit_length={_bit_length}, bit_offset={_bit_offset}, "
"byte_order={_byte_order}, array_shape={_array_shape}, "
"array_order={_array_order})".format(**values)
)
else:
return (
"{cls_name}(name={_name}, data_type={_data_type}, "
"bit_length={_bit_length}, bit_offset={_bit_offset}, "
"byte_order={_byte_order})".format(**values)
)
def __iter__(self):
return iter(
[
("name", self._name),
("dataType", self._data_type),
("bitLength", self._bit_length),
("bitOffset", self._bit_offset),
("byteOrder", self._byte_order),
]
)
@property
def name(self):
"""Name of the field.
Optional metadata. If set, is a string, otherwise is None.
"""
return self._name
@property
def data_type(self):
"""Data type of the field.
String, one of:
- 'uint' (unsigned integer)
- 'int' (signed integer)
- 'float' (IEEE floating point)
- 'str' (string)
- 'fill' (placeholder used to fill space between other fields)
"""
return self._data_type
@property
def bit_length(self):
"""Bit length of the field.
Integer, must be non-negative.
"""
return self._bit_length
@property
def bit_offset(self):
"""Bit offset of the field.
Integer bit offset supplied in definition. If not manually specified, is
None.
"""
return self._bit_offset
@property
def byte_order(self):
"""Byte order of the field.
Applies to integer data types. Either 'big', 'small', ad-hoc byte order
like "4312".
Big endian stores the most significant byte of a word at the smallest
memory address, and little endian stores the least-significant byte at
the smallest address.
For ad-hoc byte orders outside of big or little endian, pass a string like
"4312"
"""
return self._byte_order
@property
def field_type(self):
"""Whether the field is an element or array.
If instance created using `~PacketField`, this will be set to the string
"element". If created using `~PacketArray`, this will be "array".
"""
return self._field_type
@property
def description(self):
"""Description of the field.
Used for metadata purposes. If set, is a string. Otherwise, is None.
"""
return self._description
[docs]
class PacketArray(PacketField):
"""An array contained in a packet, similar to :py:class:`~ccsdspy.PacketField` but with multiple
elements of the same size (e.g. image).
"""
def __init__(self, *args, array_shape=None, array_order="C", **kwargs):
"""
Parameters
----------
name : str
String identifier for the field. The name specified how you may
call upon this data later.
data_type : {'uint', 'int', 'float', 'str', 'fill'}
Data type of the field.
bit_length : int
Number of bits contained in the field.
array_shape : int, tuple of ints, str, 'expand'
Shape of the array as a tuple. For a 1-dimensional array, a single integer
can be supplied. To use another field's value, pass the name of that field. To
grow to fill the packet, use "expand". For details on variable length fields, see
the :py:class:`~ccsdspy.VariableLength` class.
array_order {'C', 'F'}
Row-major (C-style) or column-major (Fortran-style) order.
bit_offset : int, optional
Bit offset into packet, including the primary header which is 48 bits long.
If this is not specified, than the bit offset will the be calculated automatically
from its position inside the packet definition.
byte_order : {'big', 'little'}, optional
Byte order of the field. Defaults to big endian.
Raises
------
TypeError
If one of the arguments is not of the correct type.
ValueError
array_shape, array_order, data_type, or byte_order is invalid
"""
if isinstance(array_shape, str):
if "data_type" not in kwargs:
kwargs["data_type"] = "uint"
elif kwargs["data_type"] != "uint":
raise ValueError("Expanding arrays must be data_type='uint'")
else:
if isinstance(array_shape, int):
array_shape = (array_shape,)
if not isinstance(array_shape, tuple):
raise TypeError("array_shape parameter must be a tuple of ints")
if not all(isinstance(dim, int) for dim in array_shape):
raise TypeError("array_shape parameter must be a tuple of ints")
if not all(dim >= 0 for dim in array_shape):
raise TypeError("array_shape parameter dimensions must be >= 0")
if sum(array_shape) == 0:
raise TypeError("array must have at least one element")
if not isinstance(array_order, str):
raise TypeError("array_order parameter must be string")
if array_order not in {"C", "F"}:
raise TypeError("array_order parameter must be either 'C' or 'F'")
super().__init__(*args, **kwargs)
self._field_type = "array"
self._array_shape = array_shape
self._array_order = array_order
@property
def array_shape(self):
"""Shape of the array.
Tuple shape of the array, or string "expand"
"""
return self._array_shape
@property
def array_order(self):
"""Order of the array.
This is either the string 'C' for row-major order, or 'F' for
column-major order (Fortran-style).
"""
return self._array_order