From 890786d0e87bbccb37d297d4d39aca2fca11fcf4 Mon Sep 17 00:00:00 2001 From: JS Ng Date: Thu, 18 Sep 2025 14:05:25 +0000 Subject: [PATCH 1/8] feat: add read_bytes() and write_bytes() methods This is required for bitfields to be able to read or write an arbitrary number of bytes from/to the stream. --- opendis/DataInputStream.py | 4 ++++ opendis/DataOutputStream.py | 3 +++ 2 files changed, 7 insertions(+) diff --git a/opendis/DataInputStream.py b/opendis/DataInputStream.py index 4374410..f2d55ca 100644 --- a/opendis/DataInputStream.py +++ b/opendis/DataInputStream.py @@ -61,3 +61,7 @@ def read_int(self) -> int32: def read_unsigned_int(self) -> uint32: return struct.unpack('>I', self.stream.read(4))[0] + + def read_bytes(self, n: int) -> bytes: + """Read n bytes from the stream.""" + return self.stream.read(n) diff --git a/opendis/DataOutputStream.py b/opendis/DataOutputStream.py index c0dd023..c82889a 100644 --- a/opendis/DataOutputStream.py +++ b/opendis/DataOutputStream.py @@ -48,3 +48,6 @@ def write_int(self, val: int) -> None: def write_unsigned_int(self, val: int) -> None: self.stream.write(struct.pack('>I', val)) + def write_bytes(self, val: bytes) -> None: + """Write bytes to the stream.""" + self.stream.write(val) From bd13dcbcad5c5b33a5bf0a669cc16930f64a8a41 Mon Sep 17 00:00:00 2001 From: JS Ng Date: Thu, 18 Sep 2025 14:05:54 +0000 Subject: [PATCH 2/8] feat: add specific non-octet-size type aliases for bitfields This might change in future. --- opendis/types.py | 5 +++++ 1 file changed, 5 insertions(+) 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 From 9fd7c6e144552894bf8741366a5e7131dc10ef29 Mon Sep 17 00:00:00 2001 From: JS Ng Date: Thu, 18 Sep 2025 14:22:33 +0000 Subject: [PATCH 3/8] feat: bitfield factory function, NetId and SpreadSpectrum record classes These two classes require the use of bitfields, which until now has not been implemented. The interface and design of these two classes is not yet final. Discussion and testing of this interface is recommended before finalization and further work on other bitfield records. --- opendis/record.py | 149 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 opendis/record.py diff --git a/opendis/record.py b/opendis/record.py new file mode 100644 index 0000000..c35ae06 --- /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 zero bytes") + + 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, + frequencyTable: bf_enum, # [UID 299] + mode: bf_enum, # [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, + pseudoNoise: bool, + timeHopping: bool, + 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) From 806e53b1b64e9eb1c9f215a07d9a52d1791f015c Mon Sep 17 00:00:00 2001 From: JS Ng Date: Thu, 18 Sep 2025 14:29:10 +0000 Subject: [PATCH 4/8] docs: clean up TransmitterPdu comments --- opendis/dis7.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/opendis/dis7.py b/opendis/dis7.py index add05b2..5135971 100644 --- a/opendis/dis7.py +++ b/opendis/dis7.py @@ -5477,7 +5477,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 +5491,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 From bd2aad479e1da21e44d75603e9e4077bd536e0a3 Mon Sep 17 00:00:00 2001 From: JS Ng Date: Thu, 18 Sep 2025 14:30:03 +0000 Subject: [PATCH 5/8] refactor: Add default values to allow init without initial values This follows the pattern established by the classes in dis7.py --- opendis/record.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/opendis/record.py b/opendis/record.py index c35ae06..5ca52f8 100644 --- a/opendis/record.py +++ b/opendis/record.py @@ -69,9 +69,9 @@ class NetId: ]) def __init__(self, - netNumber: bf_uint, - frequencyTable: bf_enum, # [UID 299] - mode: bf_enum, # [UID 298] + 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 @@ -121,9 +121,9 @@ class SpreadSpectrum: ]) def __init__(self, - frequencyHopping: bool, - pseudoNoise: bool, - timeHopping: bool, + frequencyHopping: bool = False, + pseudoNoise: bool = False, + timeHopping: bool = False, padding: bf_uint = 0): self.frequencyHopping = frequencyHopping self.pseudoNoise = pseudoNoise From 281186139666287221ac5e2919f2c26f498159ff Mon Sep 17 00:00:00 2001 From: JS Ng Date: Thu, 18 Sep 2025 14:31:57 +0000 Subject: [PATCH 6/8] refactor: use SpreadSpectrum in ModulationType Initial test for bitfield class --- opendis/dis7.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/opendis/dis7.py b/opendis/dis7.py index 5135971..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.""" From f2740e8ff0958e5cb877d499cdee57979a7ad444 Mon Sep 17 00:00:00 2001 From: JS Ng Date: Thu, 18 Sep 2025 14:35:02 +0000 Subject: [PATCH 7/8] fix: use relative imports --- opendis/record.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opendis/record.py b/opendis/record.py index 5ca52f8..7debf89 100644 --- a/opendis/record.py +++ b/opendis/record.py @@ -11,8 +11,8 @@ bf_uint, ) -from DataInputStream import DataInputStream -from DataOutputStream import DataOutputStream +from .DataInputStream import DataInputStream +from .DataOutputStream import DataOutputStream def _bitfield( From 93d8e22e0654498f368510f134b5a8d426365101 Mon Sep 17 00:00:00 2001 From: JS Ng Date: Thu, 18 Sep 2025 14:43:03 +0000 Subject: [PATCH 8/8] fix: typo --- opendis/record.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opendis/record.py b/opendis/record.py index 7debf89..7c8b082 100644 --- a/opendis/record.py +++ b/opendis/record.py @@ -32,8 +32,8 @@ def _bitfield( 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 zero bytes") + if bytesize <= 0: + raise ValueError("Cannot create bitfield with less than one byte") class Bitfield(BigEndianStructure): _fields_ = fields