diff --git a/opendis/dis7.py b/opendis/dis7.py index add05b2..1a7e9bb 100644 --- a/opendis/dis7.py +++ b/opendis/dis7.py @@ -6,8 +6,6 @@ enum8, enum16, enum32, - int8, - int16, int32, uint8, uint16, @@ -19,6 +17,7 @@ struct16, struct32, ) +from .record import SpreadSpectrum class DataQueryDatumSpecification: @@ -2025,11 +2024,11 @@ class ModulationType: """ def __init__(self, - spreadSpectrum: struct16 = 0, # See RPR Enumerations + spreadSpectrum: SpreadSpectrum | None = None, # See RPR Enumerations majorModulation: enum16 = 0, # [UID 155] - detail: enum16 =0, # [UID 156-162] - radioSystem: enum16 =0): # [UID 163] - self.spreadSpectrum = spreadSpectrum + detail: enum16 = 0, # [UID 156-162] + radioSystem: enum16 = 0): # [UID 163] + self.spreadSpectrum = spreadSpectrum or SpreadSpectrum() """This field shall indicate the spread spectrum technique or combination of spread spectrum techniques in use. Bit field. 0=freq hopping, 1=psuedo noise, time hopping=2, reamining bits unused""" self.majorModulation = majorModulation """the major classification of the modulation type.""" @@ -5477,7 +5476,7 @@ def __init__(self, antennaPatternList=None): super(TransmitterPdu, self).__init__() self.radioReferenceID = radioReferenceID or EntityID() - """ID of the entitythat is the source of the communication""" + """ID of the entity that is the source of the communication""" self.radioNumber = radioNumber """particular radio within an entity""" self.radioEntityType = radioEntityType or EntityType() # TODO: validation @@ -5491,18 +5490,15 @@ def __init__(self, self.frequency = frequency self.transmitFrequencyBandwidth = transmitFrequencyBandwidth self.power = power - """transmission power""" self.modulationType = modulationType or ModulationType() self.cryptoSystem = cryptoSystem self.cryptoKeyId = cryptoKeyId - # FIXME: Refactpr modulation parameters into its own record class + # FIXME: Refactor modulation parameters into its own record class self.modulationParameterCount = modulationParameterCount self.padding2 = 0 self.padding3 = 0 self.modulationParametersList = modulationParametersList or [] - """variable length list of modulation parameters""" self.antennaPatternList = antennaPatternList or [] - """variable length list of antenna pattern records""" # TODO: zero or more Variable Transmitter Parameters records (see 6.2.95) @property diff --git a/opendis/record.py b/opendis/record.py new file mode 100644 index 0000000..7c8b082 --- /dev/null +++ b/opendis/record.py @@ -0,0 +1,149 @@ +"""Record type classes for OpenDIS7. + +This module defines classes for various record types used in DIS PDUs. +""" + +from collections.abc import Sequence +from ctypes import _SimpleCData, BigEndianStructure, c_uint +from .types import ( + bf_enum, + bf_int, + bf_uint, +) + +from .DataInputStream import DataInputStream +from .DataOutputStream import DataOutputStream + + +def _bitfield( + name: str, + bytesize: int, + fields: Sequence[ + tuple[str, type[_SimpleCData]] | tuple[str, type[_SimpleCData], int] + ], + ): + """Factory function for bitfield structs, which are subclasses of + ctypes.Structure. + These are used in records that require them to unpack non-octet-sized fields. + + Args: + name: Name of the bitfield struct. + bytesize: Size of the bitfield in bytes. + fields: Sequence of tuples defining the fields of the bitfield. + See https://docs.python.org/3/library/ctypes.html#ctypes.Structure._fields_ + """ + if bytesize <= 0: + raise ValueError("Cannot create bitfield with less than one byte") + + class Bitfield(BigEndianStructure): + _fields_ = fields + + @staticmethod + def marshalledSize() -> int: + return bytesize + + def serialize(self, outputStream: DataOutputStream) -> None: + outputStream.write_bytes(bytes(self)) + + @classmethod + def parse(cls, inputStream: DataInputStream) -> "Bitfield": + return cls.from_buffer_copy(inputStream.read_bytes(bytesize)) + Bitfield.__name__ = name + return Bitfield + + +class NetId: + """Annex C, Table C.5 + + Represents an Operational Net in the format of NXX.XYY, where: + N = Mode + XXX = Net Number + YY = Frequency Table + """ + + _struct = _bitfield(name="NetId", bytesize=2, fields=[ + ("netNumber", c_uint, 10), + ("frequencyTable", c_uint, 2), + ("mode", c_uint, 2), + ("padding", c_uint, 2) + ]) + + def __init__(self, + netNumber: bf_uint = 0, + frequencyTable: bf_enum = 0, # [UID 299] + mode: bf_enum = 0, # [UID 298] + padding: bf_uint = 0): + # Net number ranging from 0 to 999 decimal + self.netNumber = netNumber + self.frequencyTable = frequencyTable + self.mode = mode + self.padding = padding + + def marshalledSize(self) -> int: + return self._struct.marshalledSize() + + def serialize(self, outputStream: DataOutputStream) -> None: + self._struct( + self.netNumber, + self.frequencyTable, + self.mode, + self.padding + ).serialize(outputStream) + + def parse(self, inputStream: DataInputStream) -> None: + record_bitfield = self._struct.parse(inputStream) + self.netNumber = record_bitfield.netNumber + self.frequencyTable = record_bitfield.frequencyTable + self.mode = record_bitfield.mode + self.padding = record_bitfield.padding + + +class SpreadSpectrum: + """6.2.59 Modulation Type Record, Table 90 + + Modulation used for radio transmission is characterized in a generic + fashion by the Spread Spectrum, Major Modulation, and Detail fields. + + Each independent type of spread spectrum technique shall be represented by + a single element of this array. + If a particular spread spectrum technique is in use, the corresponding array + element shall be set to one; otherwise it shall be set to zero. + All unused array elements shall be set to zero. + + In Python, the presence or absence of each technique is indicated by a bool. + """ + + _struct = _bitfield("SpreadSpectrum", 2, [ + ("frequencyHopping", c_uint, 1), + ("pseudoNoise", c_uint, 1), + ("timeHopping", c_uint, 1), + ("padding", c_uint, 13) + ]) + + def __init__(self, + frequencyHopping: bool = False, + pseudoNoise: bool = False, + timeHopping: bool = False, + padding: bf_uint = 0): + self.frequencyHopping = frequencyHopping + self.pseudoNoise = pseudoNoise + self.timeHopping = timeHopping + self.padding = padding + + def marshalledSize(self) -> int: + return self._struct.marshalledSize() + + def serialize(self, outputStream: DataOutputStream) -> None: + # Bitfield expects int input + self._struct( + int(self.frequencyHopping), + int(self.pseudoNoise), + int(self.timeHopping), + self.padding + ).serialize(outputStream) + + def parse(self, inputStream: DataInputStream) -> None: + record_bitfield = self._struct.parse(inputStream) + self.frequencyHopping = bool(record_bitfield.frequencyHopping) + self.pseudoNoise = bool(record_bitfield.pseudoNoise) + self.timeHopping = bool(record_bitfield.timeHopping) diff --git a/opendis/types.py b/opendis/types.py index cbb7c83..38de5ff 100644 --- a/opendis/types.py +++ b/opendis/types.py @@ -27,3 +27,8 @@ struct32 = int char8 = str char16 = str + +# Non-octet-size types for bitfields +bf_enum = int +bf_int = int +bf_uint = int