From 1e2ad0679aac7ffcbf8d83fcb3989b215661ce07 Mon Sep 17 00:00:00 2001 From: JS Ng Date: Wed, 1 Oct 2025 04:07:56 +0000 Subject: [PATCH 01/16] feat: add ModulationType class --- opendis/record.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/opendis/record.py b/opendis/record.py index 211cd8a..7bf88de 100644 --- a/opendis/record.py +++ b/opendis/record.py @@ -16,6 +16,7 @@ from .stream import DataInputStream, DataOutputStream from .types import ( + enum16, bf_enum, bf_int, bf_uint, @@ -103,6 +104,44 @@ def parse(cls, inputStream: DataInputStream) -> "Bitfield": return Bitfield +class ModulationType: + """Section 6.2.59 + + Information about the type of modulation used for radio transmission. + """ + + def __init__(self, + 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 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, remaining bits unused""" + self.majorModulation = majorModulation + self.detail = detail + self.radioSystem = radioSystem + + def marshalledSize(self) -> int: + size = 0 + size += self.spreadSpectrum.marshalledSize() + size += 2 # majorModulation + size += 2 # detail + size += 2 # radioSystem + return size + + def serialize(self, outputStream: DataOutputStream) -> None: + self.spreadSpectrum.serialize(outputStream) + outputStream.write_uint16(self.majorModulation) + outputStream.write_uint16(self.detail) + outputStream.write_uint16(self.radioSystem) + + def parse(self, inputStream: DataInputStream) -> None: + self.spreadSpectrum.parse(inputStream) + self.majorModulation = inputStream.read_uint16() + self.detail = inputStream.read_uint16() + self.radioSystem = inputStream.read_uint16() + + class NetId: """Annex C, Table C.5 From 914f51ed173c71ca11f54b0b14db0cdd0bb025fd Mon Sep 17 00:00:00 2001 From: JS Ng Date: Wed, 1 Oct 2025 04:10:42 +0000 Subject: [PATCH 02/16] refactor: replace ModulationType class in dis7.py This removes the need to use the SpreadSpectrum record directly --- opendis/dis7.py | 37 +------------------------------------ 1 file changed, 1 insertion(+), 36 deletions(-) diff --git a/opendis/dis7.py b/opendis/dis7.py index 1a7e9bb..9403a40 100644 --- a/opendis/dis7.py +++ b/opendis/dis7.py @@ -17,7 +17,7 @@ struct16, struct32, ) -from .record import SpreadSpectrum +from .record import ModulationType class DataQueryDatumSpecification: @@ -2017,41 +2017,6 @@ def parse(self, inputStream): self.communicationsNodeID.parse(inputStream) -class ModulationType: - """Section 6.2.59 - - Information about the type of modulation used for radio transmission. - """ - - def __init__(self, - 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 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.""" - self.detail = detail - """provide certain detailed information depending upon the major modulation type""" - self.radioSystem = radioSystem - """the radio system associated with this Transmitter PDU and shall be used as the basis to interpret other fields whose values depend on a specific radio system.""" - - def serialize(self, outputStream): - """serialize the class""" - outputStream.write_unsigned_short(self.spreadSpectrum) - outputStream.write_unsigned_short(self.majorModulation) - outputStream.write_unsigned_short(self.detail) - outputStream.write_unsigned_short(self.radioSystem) - - def parse(self, inputStream): - """Parse a message. This may recursively call embedded objects.""" - self.spreadSpectrum = inputStream.read_unsigned_short() - self.majorModulation = inputStream.read_unsigned_short() - self.detail = inputStream.read_unsigned_short() - self.radioSystem = inputStream.read_unsigned_short() - - class LinearSegmentParameter: """Section 6.2.52 From 56fef6c0c05e30393535efa9d419aca0279186bd Mon Sep 17 00:00:00 2001 From: JS Ng Date: Wed, 1 Oct 2025 04:17:39 +0000 Subject: [PATCH 03/16] format: organize imports --- opendis/dis7.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/opendis/dis7.py b/opendis/dis7.py index 9403a40..5bb8ca3 100644 --- a/opendis/dis7.py +++ b/opendis/dis7.py @@ -2,6 +2,8 @@ #This code is licensed under the BSD software license # +from .record import ModulationType +from .stream import DataInputStream, DataOutputStream from .types import ( enum8, enum16, @@ -17,7 +19,6 @@ struct16, struct32, ) -from .record import ModulationType class DataQueryDatumSpecification: From bd644a9be8cc5192abb1a6b7c5a8887b1053e76b Mon Sep 17 00:00:00 2001 From: JS Ng Date: Wed, 1 Oct 2025 04:19:04 +0000 Subject: [PATCH 04/16] refactor: add type annotations, use clearer stream methods --- opendis/dis7.py | 62 ++++++++++++++++++++++++------------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/opendis/dis7.py b/opendis/dis7.py index 5bb8ca3..8747ccc 100644 --- a/opendis/dis7.py +++ b/opendis/dis7.py @@ -5474,58 +5474,58 @@ def variableTransmitterParameterCount(self) -> uint16: """ return len(self.modulationParametersList) - def serialize(self, outputStream): + def serialize(self, outputStream: DataOutputStream) -> None: """serialize the class""" super(TransmitterPdu, self).serialize(outputStream) self.radioReferenceID.serialize(outputStream) - outputStream.write_unsigned_short(self.radioNumber) + outputStream.write_uint16(self.radioNumber) self.radioEntityType.serialize(outputStream) - outputStream.write_unsigned_byte(self.transmitState) - outputStream.write_unsigned_byte(self.inputSource) - outputStream.write_unsigned_short( + outputStream.write_uint8(self.transmitState) + outputStream.write_uint8(self.inputSource) + outputStream.write_uint16( self.variableTransmitterParameterCount) self.antennaLocation.serialize(outputStream) self.relativeAntennaLocation.serialize(outputStream) - outputStream.write_unsigned_short(self.antennaPatternType) - outputStream.write_unsigned_short(len(self.antennaPatternList)) - outputStream.write_long(self.frequency) - outputStream.write_float(self.transmitFrequencyBandwidth) - outputStream.write_float(self.power) + outputStream.write_uint16(self.antennaPatternType) + outputStream.write_uint16(len(self.antennaPatternList)) + outputStream.write_uint64(self.frequency) + outputStream.write_float32(self.transmitFrequencyBandwidth) + outputStream.write_float32(self.power) self.modulationType.serialize(outputStream) - outputStream.write_unsigned_short(self.cryptoSystem) - outputStream.write_unsigned_short(self.cryptoKeyId) - outputStream.write_unsigned_byte(len(self.modulationParametersList)) - outputStream.write_unsigned_short(self.padding2) - outputStream.write_unsigned_byte(self.padding3) + outputStream.write_uint16(self.cryptoSystem) + outputStream.write_uint16(self.cryptoKeyId) + outputStream.write_uint8(len(self.modulationParametersList)) + outputStream.write_uint16(self.padding2) + outputStream.write_uint8(self.padding3) for anObj in self.modulationParametersList: anObj.serialize(outputStream) for anObj in self.antennaPatternList: anObj.serialize(outputStream) - def parse(self, inputStream): + def parse(self, inputStream: DataInputStream) -> None: """Parse a message. This may recursively call embedded objects.""" super(TransmitterPdu, self).parse(inputStream) self.radioReferenceID.parse(inputStream) - self.radioNumber = inputStream.read_unsigned_short() + self.radioNumber = inputStream.read_uint16() self.radioEntityType.parse(inputStream) - self.transmitState = inputStream.read_unsigned_byte() - self.inputSource = inputStream.read_unsigned_byte() - variableTransmitterParameterCount = inputStream.read_unsigned_short( - ) + self.transmitState = inputStream.read_uint8() + self.inputSource = inputStream.read_uint8() + variableTransmitterParameterCount = inputStream.read_uint16() self.antennaLocation.parse(inputStream) self.relativeAntennaLocation.parse(inputStream) - self.antennaPatternType = inputStream.read_unsigned_short() - self.antennaPatternCount = inputStream.read_unsigned_short() - self.frequency = inputStream.read_long() - self.transmitFrequencyBandwidth = inputStream.read_float() - self.power = inputStream.read_float() + self.antennaPatternType = inputStream.read_uint16() + self.antennaPatternCount = inputStream.read_uint16() + self.frequency = inputStream.read_uint64() + self.transmitFrequencyBandwidth = inputStream.read_float32() + self.power = inputStream.read_float32() self.modulationType.parse(inputStream) - self.cryptoSystem = inputStream.read_unsigned_short() - self.cryptoKeyId = inputStream.read_unsigned_short() - self.modulationParameterCount = inputStream.read_unsigned_byte() - self.padding2 = inputStream.read_unsigned_short() - self.padding3 = inputStream.read_unsigned_byte() + self.cryptoSystem = inputStream.read_uint16() + self.cryptoKeyId = inputStream.read_uint16() + modulationParametersLength = inputStream.read_uint8() + self.padding2 = inputStream.read_uint16() + self.padding3 = inputStream.read_uint8() + """Vendor product MACE from BattleSpace Inc, only uses 1 byte per modulation param""" """SISO Spec dictates it should be 2 bytes""" """Instead of dumping the packet we can make an assumption that some vendors use 1 byte per param""" From 0bb78cdef0ac07859df6829859f1f59f2742e537 Mon Sep 17 00:00:00 2001 From: JS Ng Date: Wed, 1 Oct 2025 04:25:58 +0000 Subject: [PATCH 05/16] feat: add MP classes for radios These classes are defined in Annex C, and will be used by ModulationParameter (MP) --- opendis/record.py | 84 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/opendis/record.py b/opendis/record.py index 7bf88de..2e27594 100644 --- a/opendis/record.py +++ b/opendis/record.py @@ -20,6 +20,9 @@ bf_enum, bf_int, bf_uint, + uint8, + uint16, + uint32, ) # Type definitions for bitfield field descriptors @@ -237,3 +240,84 @@ def parse(self, inputStream: DataInputStream) -> None: self.frequencyHopping = bool(record_bitfield.frequencyHopping) self.pseudoNoise = bool(record_bitfield.pseudoNoise) self.timeHopping = bool(record_bitfield.timeHopping) + + +class GenericRadio: + """Annex C.2 Generic Radio record + + There are no other specific Transmitter, Signal, or Receiver PDU + requirements unique to a generic radio. + """ + + def marshalledSize(self) -> int: + return 0 + + def serialize(self, outputStream: DataOutputStream) -> None: + pass + + def parse(self, inputStream: DataInputStream) -> None: + pass + + +class SimpleIntercomRadio: + """Annex C.3 Simple Intercom Radio + + A Simple Intercom shall be identified by both the Transmitter PDU + Modulation Type record—Radio System field indicating a system type of + Generic Radio or Simple Intercom (1) and by the Modulation Type + record—Major Modulation field set to No Statement (0). + + This class has specific field requirements for the TransmitterPdu. + """ + + def marshalledSize(self) -> int: + return 0 + + def serialize(self, outputStream: DataOutputStream) -> None: + pass + + def parse(self, inputStream: DataInputStream) -> None: + pass + + +# C.4 HAVE QUICK Radios + +class BasicHaveQuickMP: + """Annex C 4.2.2, Table C.3 — Basic HAVE QUICK MP record""" + + def __init__(self, + net_id: NetId | None = None, + mwod_index: uint16 = 1, + reserved16: uint16 = 0, + reserved8_1: uint8 = 0, + reserved8_2: uint8 = 0, + time_of_day: uint32 = 0, + padding: uint32 = 0): + self.net_id = net_id or NetId() + self.mwod_index = mwod_index + self.reserved16 = reserved16 + self.reserved8_1 = reserved8_1 + self.reserved8_2 = reserved8_2 + self.time_of_day = time_of_day + self.padding = padding + + def marshalledSize(self) -> int: + return 16 # bytes + + def serialize(self, outputStream: DataOutputStream) -> None: + self.net_id.serialize(outputStream) + outputStream.write_uint16(self.mwod_index) + outputStream.write_uint16(self.reserved16) + outputStream.write_uint8(self.reserved8_1) + outputStream.write_uint8(self.reserved8_2) + outputStream.write_uint32(self.time_of_day) + outputStream.write_uint32(self.padding) + + def parse(self, inputStream: DataInputStream) -> None: + self.net_id.parse(inputStream) + self.mwod_index = inputStream.read_uint16() + self.reserved16 = inputStream.read_uint16() + self.reserved8_1 = inputStream.read_uint8() + self.reserved8_2 = inputStream.read_uint8() + self.time_of_day = inputStream.read_uint32() + self.padding = inputStream.read_uint32() From 42a3b1130af8aa9900b4bcbd65d0c5caa14e20f9 Mon Sep 17 00:00:00 2001 From: JS Ng Date: Wed, 1 Oct 2025 05:30:03 +0000 Subject: [PATCH 06/16] feat: Implement ModulationParameters as a wrapper class for MPRecords --- opendis/record.py | 55 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/opendis/record.py b/opendis/record.py index 2e27594..0a02ab7 100644 --- a/opendis/record.py +++ b/opendis/record.py @@ -3,6 +3,7 @@ This module defines classes for various record types used in DIS PDUs. """ +from abc import abstractmethod from collections.abc import Sequence from ctypes import ( _SimpleCData, @@ -242,7 +243,26 @@ def parse(self, inputStream: DataInputStream) -> None: self.timeHopping = bool(record_bitfield.timeHopping) -class GenericRadio: +class ModulationParametersRecord: + """Base class for modulation parameters records, as defined in Annex C.""" + + @abstractmethod + def marshalledSize(self) -> int: + """Return the size of the record when serialized.""" + raise NotImplementedError() + + @abstractmethod + def serialize(self, outputStream: DataOutputStream) -> None: + """Serialize the record to the output stream.""" + raise NotImplementedError() + + @abstractmethod + def parse(self, inputStream: DataInputStream) -> None: + """Parse the record from the input stream.""" + raise NotImplementedError() + + +class GenericRadio(ModulationParametersRecord): """Annex C.2 Generic Radio record There are no other specific Transmitter, Signal, or Receiver PDU @@ -259,7 +279,7 @@ def parse(self, inputStream: DataInputStream) -> None: pass -class SimpleIntercomRadio: +class SimpleIntercomRadio(ModulationParametersRecord): """Annex C.3 Simple Intercom Radio A Simple Intercom shall be identified by both the Transmitter PDU @@ -282,7 +302,7 @@ def parse(self, inputStream: DataInputStream) -> None: # C.4 HAVE QUICK Radios -class BasicHaveQuickMP: +class BasicHaveQuickMP(ModulationParametersRecord): """Annex C 4.2.2, Table C.3 — Basic HAVE QUICK MP record""" def __init__(self, @@ -321,3 +341,32 @@ def parse(self, inputStream: DataInputStream) -> None: self.reserved8_2 = inputStream.read_uint8() self.time_of_day = inputStream.read_uint32() self.padding = inputStream.read_uint32() + + +class ModulationParameters: + """Section 6.2.58 + + Modulation parameters associated with a specific radio system. + + This class is not designed to be instantiated directly with no arguments. + """ + + def __init__(self, + # record must be provided as there is no default value. + record: ModulationParametersRecord): + self.record = record + # ModulationParameters requires padding to 64-bit (8-byte) boundary + self.padding = 8 - (self.record.marshalledSize() % 8) % 8 + + def serialize(self, outputStream: DataOutputStream) -> None: + self.record.serialize(outputStream) + outputStream.write_bytes(b'\x00' * self.padding) + + def parse(self, inputStream: DataInputStream) -> None: + self.record.parse(inputStream) + bytes_to_read = 8 - (self.record.marshalledSize() % 8) % 8 + if bytes_to_read > 0: + self.padding = int.from_bytes( + inputStream.read_bytes(bytes_to_read), + byteorder='big' + ) From 69f8e5ea75d44c4b496576cb4ee65254c8dfd089 Mon Sep 17 00:00:00 2001 From: JS Ng Date: Wed, 1 Oct 2025 09:42:18 +0000 Subject: [PATCH 07/16] refactor: add ModulationParameters.marshalledSize(), replace existing class in dis7.py --- opendis/dis7.py | 18 +----------------- opendis/record.py | 5 +++++ 2 files changed, 6 insertions(+), 17 deletions(-) diff --git a/opendis/dis7.py b/opendis/dis7.py index 8747ccc..60a535c 100644 --- a/opendis/dis7.py +++ b/opendis/dis7.py @@ -2,7 +2,7 @@ #This code is licensed under the BSD software license # -from .record import ModulationType +from .record import ( from .stream import DataInputStream, DataOutputStream from .types import ( enum8, @@ -670,22 +670,6 @@ def parse(self, inputStream): self.stationNumber = inputStream.read_unsigned_short() -class ModulationParameters: - """Section 6.2.58 - - Modulation parameters associated with a specific radio system. INCOMPLETE. - """ - - def __init__(self): - pass - - def serialize(self, outputStream): - """serialize the class""" - - def parse(self, inputStream): - """Parse a message. This may recursively call embedded objects.""" - - class EulerAngles: """Section 6.2.33 diff --git a/opendis/record.py b/opendis/record.py index 0a02ab7..862f1a8 100644 --- a/opendis/record.py +++ b/opendis/record.py @@ -358,6 +358,9 @@ def __init__(self, # ModulationParameters requires padding to 64-bit (8-byte) boundary self.padding = 8 - (self.record.marshalledSize() % 8) % 8 + def marshalledSize(self) -> int: + return self.record.marshalledSize() + self.padding + def serialize(self, outputStream: DataOutputStream) -> None: self.record.serialize(outputStream) outputStream.write_bytes(b'\x00' * self.padding) @@ -370,3 +373,5 @@ def parse(self, inputStream: DataInputStream) -> None: inputStream.read_bytes(bytes_to_read), byteorder='big' ) + else: + self.padding = 0 From 3da6a5585fbf3f83ab940b8c7963eb7cbf4e7f69 Mon Sep 17 00:00:00 2001 From: JS Ng Date: Wed, 1 Oct 2025 09:44:28 +0000 Subject: [PATCH 08/16] feat: add UnknownRadio class This serves as a placeholder for unrecognised radios in ModulationParameters --- opendis/dis7.py | 4 ++++ opendis/record.py | 16 ++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/opendis/dis7.py b/opendis/dis7.py index 60a535c..71ed44b 100644 --- a/opendis/dis7.py +++ b/opendis/dis7.py @@ -3,6 +3,10 @@ # from .record import ( + ModulationType, + ModulationParameters, + UnknownRadio, +) from .stream import DataInputStream, DataOutputStream from .types import ( enum8, diff --git a/opendis/record.py b/opendis/record.py index 862f1a8..8f78de2 100644 --- a/opendis/record.py +++ b/opendis/record.py @@ -262,6 +262,22 @@ def parse(self, inputStream: DataInputStream) -> None: raise NotImplementedError() +class UnknownRadio(ModulationParametersRecord): + """Placeholder for unknown or unimplemented radio types.""" + + def __init__(self, data: bytes): + self.data = data + + def marshalledSize(self) -> int: + return len(self.data) + + def serialize(self, outputStream: DataOutputStream) -> None: + outputStream.write_bytes(self.data) + + def parse(self, inputStream: DataInputStream, bytelength: int = 0) -> None: + self.data = inputStream.read_bytes(bytelength) + + class GenericRadio(ModulationParametersRecord): """Annex C.2 Generic Radio record From aa0b8ac666990fd78ec35da5b0b50e377eee49cd Mon Sep 17 00:00:00 2001 From: JS Ng Date: Wed, 1 Oct 2025 09:53:36 +0000 Subject: [PATCH 09/16] refactor: use ModulationParameters in TransmitterPdu --- opendis/dis7.py | 59 +++++++++++++++++++------------------------------ 1 file changed, 23 insertions(+), 36 deletions(-) diff --git a/opendis/dis7.py b/opendis/dis7.py index 71ed44b..1084f33 100644 --- a/opendis/dis7.py +++ b/opendis/dis7.py @@ -5425,8 +5425,7 @@ def __init__(self, modulationType: "ModulationType | None" = None, cryptoSystem: enum16 = 0, # [UID 166] cryptoKeyId: struct16 = 0, # See Table 175 - modulationParameterCount: uint8 = 0, # in bytes - modulationParametersList=None, + modulationParameters: ModulationParameters | None = None, antennaPatternList=None): super(TransmitterPdu, self).__init__() self.radioReferenceID = radioReferenceID or EntityID() @@ -5447,13 +5446,16 @@ def __init__(self, self.modulationType = modulationType or ModulationType() self.cryptoSystem = cryptoSystem self.cryptoKeyId = cryptoKeyId - # FIXME: Refactor modulation parameters into its own record class - self.modulationParameterCount = modulationParameterCount self.padding2 = 0 self.padding3 = 0 - self.modulationParametersList = modulationParametersList or [] - self.antennaPatternList = antennaPatternList or [] - # TODO: zero or more Variable Transmitter Parameters records (see 6.2.95) + self.modulationParameters = modulationParameters + + @property + def modulationParametersLength(self) -> uint8: + if self.modulationParameters: + return self.modulationParameters.marshalledSize() + else: + return 0 @property def variableTransmitterParameterCount(self) -> uint16: @@ -5485,8 +5487,12 @@ def serialize(self, outputStream: DataOutputStream) -> None: outputStream.write_uint8(len(self.modulationParametersList)) outputStream.write_uint16(self.padding2) outputStream.write_uint8(self.padding3) - for anObj in self.modulationParametersList: - anObj.serialize(outputStream) + + # Serialize parameter records + + ## Modulation Parameters + if self.modulationParameters: + self.modulationParameters.serialize(outputStream) for anObj in self.antennaPatternList: anObj.serialize(outputStream) @@ -5514,33 +5520,14 @@ def parse(self, inputStream: DataInputStream) -> None: self.padding2 = inputStream.read_uint16() self.padding3 = inputStream.read_uint8() - """Vendor product MACE from BattleSpace Inc, only uses 1 byte per modulation param""" - """SISO Spec dictates it should be 2 bytes""" - """Instead of dumping the packet we can make an assumption that some vendors use 1 byte per param""" - """Although we will still send out 2 bytes per param as per spec""" - endsize = self.antennaPatternCount * 39 - mod_bytes = 2 - - if (self.modulationParameterCount > 0): - curr = inputStream.stream.tell() - remaining = inputStream.stream.read(None) - mod_bytes = (len(remaining) - - endsize) / self.modulationParameterCount - inputStream.stream.seek(curr, 0) - - if (mod_bytes > 2): - print("Malformed Packet") - else: - for idx in range(0, self.modulationParameterCount): - if mod_bytes == 2: - element = inputStream.read_unsigned_short() - else: - element = inputStream.read_unsigned_byte() - self.modulationParametersList.append(element) - for idx in range(0, self.antennaPatternCount): - element = BeamAntennaPattern() - element.parse(inputStream) - self.antennaPatternList.append(element) + # Parse parameter records + + ## Modulation Parameters + if modulationParametersLength > 0: + mp_data = inputStream.read_bytes(modulationParametersLength) + self.modulationParameters = ModulationParameters( + UnknownRadio(mp_data) + ) class ElectromagneticEmissionsPdu(DistributedEmissionsFamilyPdu): From 71d9a565d02b1b3442ead0bc0ee4e078f682e11b Mon Sep 17 00:00:00 2001 From: JS Ng Date: Wed, 1 Oct 2025 09:56:29 +0000 Subject: [PATCH 10/16] refactor: use UnknownRadio.parse() for consistency --- opendis/dis7.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/opendis/dis7.py b/opendis/dis7.py index 1084f33..b2c27f9 100644 --- a/opendis/dis7.py +++ b/opendis/dis7.py @@ -5524,10 +5524,9 @@ def parse(self, inputStream: DataInputStream) -> None: ## Modulation Parameters if modulationParametersLength > 0: - mp_data = inputStream.read_bytes(modulationParametersLength) - self.modulationParameters = ModulationParameters( - UnknownRadio(mp_data) - ) + radio = UnknownRadio() + radio.parse(inputStream, bytelength=modulationParametersLength) + self.modulationParameters = ModulationParameters(radio) class ElectromagneticEmissionsPdu(DistributedEmissionsFamilyPdu): From 738cd3a8eb6227958c13868be1d4845431488299 Mon Sep 17 00:00:00 2001 From: JS Ng Date: Wed, 1 Oct 2025 10:10:10 +0000 Subject: [PATCH 11/16] feat: add AntennaPatternRecord type and UnknownAntennaPattern placeholder class --- opendis/dis7.py | 35 ++++++++++++++++++++++++++++++----- opendis/record.py | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 5 deletions(-) diff --git a/opendis/dis7.py b/opendis/dis7.py index b2c27f9..25815cc 100644 --- a/opendis/dis7.py +++ b/opendis/dis7.py @@ -3,9 +3,11 @@ # from .record import ( + AntennaPatternRecord, ModulationType, ModulationParameters, UnknownRadio, + UnknownAntennaPattern, ) from .stream import DataInputStream, DataOutputStream from .types import ( @@ -5449,6 +5451,14 @@ def __init__(self, self.padding2 = 0 self.padding3 = 0 self.modulationParameters = modulationParameters + self.antennaPattern = antennaPattern + + @property + def antennaPatternLength(self) -> uint16: + if self.antennaPattern: + return self.antennaPattern.marshalledSize() + else: + return 0 @property def modulationParametersLength(self) -> uint8: @@ -5477,14 +5487,14 @@ def serialize(self, outputStream: DataOutputStream) -> None: self.antennaLocation.serialize(outputStream) self.relativeAntennaLocation.serialize(outputStream) outputStream.write_uint16(self.antennaPatternType) - outputStream.write_uint16(len(self.antennaPatternList)) + outputStream.write_uint16(self.antennaPatternLength) outputStream.write_uint64(self.frequency) outputStream.write_float32(self.transmitFrequencyBandwidth) outputStream.write_float32(self.power) self.modulationType.serialize(outputStream) outputStream.write_uint16(self.cryptoSystem) outputStream.write_uint16(self.cryptoKeyId) - outputStream.write_uint8(len(self.modulationParametersList)) + outputStream.write_uint8(self.modulationParametersLength) outputStream.write_uint16(self.padding2) outputStream.write_uint8(self.padding3) @@ -5494,8 +5504,9 @@ def serialize(self, outputStream: DataOutputStream) -> None: if self.modulationParameters: self.modulationParameters.serialize(outputStream) - for anObj in self.antennaPatternList: - anObj.serialize(outputStream) + ## Antenna Pattern + if self.antennaPattern: + self.antennaPattern.serialize(outputStream) def parse(self, inputStream: DataInputStream) -> None: """Parse a message. This may recursively call embedded objects.""" @@ -5509,7 +5520,7 @@ def parse(self, inputStream: DataInputStream) -> None: self.antennaLocation.parse(inputStream) self.relativeAntennaLocation.parse(inputStream) self.antennaPatternType = inputStream.read_uint16() - self.antennaPatternCount = inputStream.read_uint16() + antennaPatternLength = inputStream.read_uint16() self.frequency = inputStream.read_uint64() self.transmitFrequencyBandwidth = inputStream.read_float32() self.power = inputStream.read_float32() @@ -5527,6 +5538,20 @@ def parse(self, inputStream: DataInputStream) -> None: radio = UnknownRadio() radio.parse(inputStream, bytelength=modulationParametersLength) self.modulationParameters = ModulationParameters(radio) + else: + self.modulationParameters = None + + ## Antenna Pattern + if antennaPatternLength > 0: + self.antennaPattern = UnknownAntennaPattern() + self.antennaPattern.parse( + inputStream, + bytelength=antennaPatternLength + ) + else: + self.antennaPattern = None + + class ElectromagneticEmissionsPdu(DistributedEmissionsFamilyPdu): diff --git a/opendis/record.py b/opendis/record.py index 8f78de2..e246811 100644 --- a/opendis/record.py +++ b/opendis/record.py @@ -391,3 +391,42 @@ def parse(self, inputStream: DataInputStream) -> None: ) else: self.padding = 0 + + +class AntennaPatternRecord: + """Section 6.2.8 + + The total length of each record shall be a multiple of 64 bits. + """ + + @abstractmethod + def marshalledSize(self) -> int: + """Return the size of the record when serialized.""" + raise NotImplementedError() + + @abstractmethod + def serialize(self, outputStream: DataOutputStream) -> None: + """Serialize the record to the output stream.""" + raise NotImplementedError() + + @abstractmethod + def parse(self, inputStream: DataInputStream) -> None: + """Parse the record from the input stream.""" + raise NotImplementedError() + + +class UnknownAntennaPattern(AntennaPatternRecord): + """Placeholder for unknown or unimplemented antenna pattern types.""" + + def __init__(self, data: bytes = b''): + self.data = data + + def marshalledSize(self) -> int: + return len(self.data) + + def serialize(self, outputStream: DataOutputStream) -> None: + outputStream.write_bytes(self.data) + + def parse(self, inputStream: DataInputStream, bytelength: int = 0) -> None: + """Parse a message. This may recursively call embedded objects.""" + self.data = inputStream.read_bytes(bytelength) From 12dad11ceade75378bf3e9f4d913222c7cde8284 Mon Sep 17 00:00:00 2001 From: JS Ng Date: Wed, 1 Oct 2025 10:15:01 +0000 Subject: [PATCH 12/16] refactor: add placeholder implementation for VariableTransmitterParameters --- opendis/dis7.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/opendis/dis7.py b/opendis/dis7.py index 25815cc..1d1682d 100644 --- a/opendis/dis7.py +++ b/opendis/dis7.py @@ -942,21 +942,28 @@ class VariableTransmitterParameters: Relates to radios. NOT COMPLETE. """ - def __init__(self, recordType: enum32 = 0, recordLength: uint16 = 4): + def __init__(self, recordType: enum32 = 0, data: bytes = b""): self.recordType = recordType # [UID 66] Variable Parameter Record Type - """Type of VTP. Enumeration from EBV""" - self.recordLength = recordLength - """Length, in bytes""" + self.data = data + + def marshalledSize(self) -> int: + return 6 + len(self.data) + + @property + def recordLength(self) -> uint16: + return self.marshalledSize() - def serialize(self, outputStream): + def serialize(self, outputStream: DataOutputStream) -> None: """serialize the class""" - outputStream.write_unsigned_int(self.recordType) - outputStream.write_unsigned_int(self.recordLength) + outputStream.write_uint32(self.recordType) + outputStream.write_uint16(self.recordLength) + outputStream.write_bytes(self.data) - def parse(self, inputStream): + def parse(self, inputStream: DataInputStream) -> None: """Parse a message. This may recursively call embedded objects.""" - self.recordType = inputStream.read_unsigned_int() - self.recordLength = inputStream.read_unsigned_int() + self.recordType = inputStream.read_uint32() + recordLength = inputStream.read_uint16() + self.data = inputStream.read_bytes(recordLength) class Attribute: From 1722d61eabec9751d6a397bd6a81bd9d6f7fc92e Mon Sep 17 00:00:00 2001 From: JS Ng Date: Wed, 1 Oct 2025 10:16:08 +0000 Subject: [PATCH 13/16] refactor: handle VariableTransmitterParameters in TransmitterPdu --- opendis/dis7.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/opendis/dis7.py b/opendis/dis7.py index 1d1682d..6c48a59 100644 --- a/opendis/dis7.py +++ b/opendis/dis7.py @@ -2,6 +2,8 @@ #This code is licensed under the BSD software license # +from typing import Sequence + from .record import ( AntennaPatternRecord, ModulationType, @@ -5423,11 +5425,9 @@ def __init__(self, radioEntityType: "EntityType | None" = None, transmitState: enum8 = 0, # [UID 164] inputSource: enum8 = 0, # [UID 165] - variableTransmitterParameterCount: uint16 = 0, antennaLocation: "Vector3Double | None" = None, relativeAntennaLocation: "Vector3Float | None" = None, antennaPatternType: enum16 = 0, # [UID 167] - antennaPatternCount: uint16 = 0, # in bytes frequency: uint64 = 0, # in Hz transmitFrequencyBandwidth: float32 = 0.0, # in Hz power: float32 = 0.0, # in decibel-milliwatts @@ -5435,7 +5435,8 @@ def __init__(self, cryptoSystem: enum16 = 0, # [UID 166] cryptoKeyId: struct16 = 0, # See Table 175 modulationParameters: ModulationParameters | None = None, - antennaPatternList=None): + antennaPattern: AntennaPatternRecord | None = None, + variableTransmitterParameters: Sequence[VariableTransmitterParameters] | None = None): super(TransmitterPdu, self).__init__() self.radioReferenceID = radioReferenceID or EntityID() """ID of the entity that is the source of the communication""" @@ -5448,7 +5449,6 @@ def __init__(self, self.relativeAntennaLocation = relativeAntennaLocation or Vector3Float( ) self.antennaPatternType = antennaPatternType - self.antennaPatternCount = antennaPatternCount self.frequency = frequency self.transmitFrequencyBandwidth = transmitFrequencyBandwidth self.power = power @@ -5459,6 +5459,7 @@ def __init__(self, self.padding3 = 0 self.modulationParameters = modulationParameters self.antennaPattern = antennaPattern + self.variableTransmitterParameters = variableTransmitterParameters or [] @property def antennaPatternLength(self) -> uint16: @@ -5476,21 +5477,16 @@ def modulationParametersLength(self) -> uint8: @property def variableTransmitterParameterCount(self) -> uint16: - """How many variable transmitter parameters are in the variable length list. - In earlier versions of DIS these were known as articulation parameters. - """ - return len(self.modulationParametersList) + return len(self.variableTransmitterParameters) def serialize(self, outputStream: DataOutputStream) -> None: - """serialize the class""" super(TransmitterPdu, self).serialize(outputStream) self.radioReferenceID.serialize(outputStream) outputStream.write_uint16(self.radioNumber) self.radioEntityType.serialize(outputStream) outputStream.write_uint8(self.transmitState) outputStream.write_uint8(self.inputSource) - outputStream.write_uint16( - self.variableTransmitterParameterCount) + outputStream.write_uint16(self.variableTransmitterParameterCount) self.antennaLocation.serialize(outputStream) self.relativeAntennaLocation.serialize(outputStream) outputStream.write_uint16(self.antennaPatternType) @@ -5515,6 +5511,10 @@ def serialize(self, outputStream: DataOutputStream) -> None: if self.antennaPattern: self.antennaPattern.serialize(outputStream) + ## Variable Transmitter Parameters + for vtp in self.variableTransmitterParameters: + vtp.serialize(outputStream) + def parse(self, inputStream: DataInputStream) -> None: """Parse a message. This may recursively call embedded objects.""" super(TransmitterPdu, self).parse(inputStream) From 61eec458d7eb82e05946e71f1a75b79d8dc62df7 Mon Sep 17 00:00:00 2001 From: JS Ng Date: Wed, 1 Oct 2025 10:17:09 +0000 Subject: [PATCH 14/16] fix: add default value for UnknownRadio --- opendis/record.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendis/record.py b/opendis/record.py index e246811..4631b2b 100644 --- a/opendis/record.py +++ b/opendis/record.py @@ -265,7 +265,7 @@ def parse(self, inputStream: DataInputStream) -> None: class UnknownRadio(ModulationParametersRecord): """Placeholder for unknown or unimplemented radio types.""" - def __init__(self, data: bytes): + def __init__(self, data: bytes = b''): self.data = data def marshalledSize(self) -> int: From a25a460b01fbeb424487bfa2826fecd2f82c5bce Mon Sep 17 00:00:00 2001 From: JS Ng Date: Wed, 1 Oct 2025 20:40:50 +0800 Subject: [PATCH 15/16] refactor: use ModulationParametersRecord directly instead of wrapping in ModulationParameters --- opendis/dis7.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/opendis/dis7.py b/opendis/dis7.py index 6c48a59..290db3b 100644 --- a/opendis/dis7.py +++ b/opendis/dis7.py @@ -8,6 +8,7 @@ AntennaPatternRecord, ModulationType, ModulationParameters, + ModulationParametersRecord, UnknownRadio, UnknownAntennaPattern, ) @@ -5434,7 +5435,7 @@ def __init__(self, modulationType: "ModulationType | None" = None, cryptoSystem: enum16 = 0, # [UID 166] cryptoKeyId: struct16 = 0, # See Table 175 - modulationParameters: ModulationParameters | None = None, + modulationParameters: ModulationParametersRecord | None = None, antennaPattern: AntennaPatternRecord | None = None, variableTransmitterParameters: Sequence[VariableTransmitterParameters] | None = None): super(TransmitterPdu, self).__init__() @@ -5544,7 +5545,7 @@ def parse(self, inputStream: DataInputStream) -> None: if modulationParametersLength > 0: radio = UnknownRadio() radio.parse(inputStream, bytelength=modulationParametersLength) - self.modulationParameters = ModulationParameters(radio) + self.modulationParameters = radio else: self.modulationParameters = None From b13d41eada1f4496b9ae4b0a054d85cc41dce5a3 Mon Sep 17 00:00:00 2001 From: JS Ng Date: Wed, 1 Oct 2025 20:45:29 +0800 Subject: [PATCH 16/16] chore: remove unused import Removed unused import of ModulationParameters. --- opendis/dis7.py | 1 - 1 file changed, 1 deletion(-) diff --git a/opendis/dis7.py b/opendis/dis7.py index 290db3b..765f20d 100644 --- a/opendis/dis7.py +++ b/opendis/dis7.py @@ -7,7 +7,6 @@ from .record import ( AntennaPatternRecord, ModulationType, - ModulationParameters, ModulationParametersRecord, UnknownRadio, UnknownAntennaPattern,