diff --git a/examples/dis_receiver.py b/examples/dis_receiver.py index 9f17705..25b118f 100644 --- a/examples/dis_receiver.py +++ b/examples/dis_receiver.py @@ -5,8 +5,7 @@ import socket import time -import sys -import array +from typing import cast from opendis.dis7 import * from opendis.RangeCoordinates import * @@ -20,7 +19,7 @@ print("Listening for DIS on UDP socket {}".format(UDP_PORT)) -gps = GPS(); +gps = GPS() def recv(): print('Reading from socket...') @@ -31,6 +30,7 @@ def recv(): pduTypeName = pdu.__class__.__name__ if pdu.pduType == 1: # PduTypeDecoders.EntityStatePdu: + pdu = cast(EntityStatePdu, pdu) # for static checkers loc = (pdu.entityLocation.x, pdu.entityLocation.y, pdu.entityLocation.z, @@ -42,7 +42,7 @@ def recv(): body = gps.ecef2llarpy(*loc) print("Received {}\n".format(pduTypeName) - + " Id : {}\n".format(pdu.entityID.entityID) + + " Id : {}\n".format(pdu.entityID.entityNumber) + " Latitude : {:.2f} degrees\n".format(rad2deg(body[0])) + " Longitude : {:.2f} degrees\n".format(rad2deg(body[1])) + " Altitude : {:.0f} meters\n".format(body[2]) diff --git a/examples/dis_sender.py b/examples/dis_sender.py index c7a0734..c600785 100644 --- a/examples/dis_sender.py +++ b/examples/dis_sender.py @@ -8,7 +8,7 @@ from io import BytesIO -from opendis.DataOutputStream import DataOutputStream +from opendis.stream import DataOutputStream from opendis.dis7 import EntityStatePdu from opendis.RangeCoordinates import * @@ -23,9 +23,9 @@ def send(): pdu = EntityStatePdu() - pdu.entityID.entityID = 42 - pdu.entityID.siteID = 17 - pdu.entityID.applicationID = 23 + pdu.entityID.entityNumber = 42 + pdu.entityID.simulationAddress.site = 17 + pdu.entityID.simulationAddress.application = 23 pdu.marking.setString('Igor3d') # Entity in Monterey, CA, USA facing North, no roll or pitch diff --git a/opendis/DataInputStream.py b/opendis/DataInputStream.py deleted file mode 100644 index 4d95edc..0000000 --- a/opendis/DataInputStream.py +++ /dev/null @@ -1,103 +0,0 @@ -""" -Reading from Java DataInputStream format. -From https://github.com/arngarden/python_java_datastream -This uses big endian (network) format. -""" - -from io import BufferedIOBase -import struct - -from .types import ( - int8, - int16, - int32, - int64, - uint8, - uint16, - uint32, - uint64, - float32, - float64, - char8, - char16, -) - - -class DataInputStream: - def __init__(self, stream: BufferedIOBase): - self.stream = stream - - def read_boolean(self) -> bool: - return struct.unpack('?', self.stream.read(1))[0] - - def read_byte(self) -> int8: - return struct.unpack('b', self.stream.read(1))[0] - - def read_unsigned_byte(self) -> uint8: - return struct.unpack('B', self.stream.read(1))[0] - - def read_char(self) -> char16: - return chr(struct.unpack('>H', self.stream.read(2))[0]) - - def read_double(self) -> float64: - return struct.unpack('>d', self.stream.read(8))[0] - - def read_float(self) -> float32: - return struct.unpack('>f', self.stream.read(4))[0] - - def read_short(self) -> int16: - return struct.unpack('>h', self.stream.read(2))[0] - - def read_unsigned_short(self) -> uint16: - return struct.unpack('>H', self.stream.read(2))[0] - - def read_long(self) -> int64: - return struct.unpack('>q', self.stream.read(8))[0] - - def read_utf(self) -> bytes: - utf_length = struct.unpack('>H', self.stream.read(2))[0] - return self.stream.read(utf_length) - - def read_int(self) -> int32: - return struct.unpack('>i', self.stream.read(4))[0] - - 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) - - # Aliases for convenience - def read_char8(self) -> char8: - return char8(self.read_char()) - - def read_float32(self) -> float32: - return float32(self.read_float()) - - def read_float64(self) -> float64: - return float64(self.read_double()) - - def read_int8(self) -> int8: - return int8(self.read_byte()) - - def read_int16(self) -> int16: - return int16(self.read_short()) - - def read_int32(self) -> int32: - return int32(self.read_int()) - - def read_int64(self) -> int64: - return int64(self.read_long()) - - def read_uint8(self) -> uint8: - return uint8(self.read_unsigned_byte()) - - def read_uint16(self) -> uint16: - return uint16(self.read_unsigned_short()) - - def read_uint32(self) -> uint32: - return uint32(self.read_unsigned_int()) - - def read_uint64(self) -> uint64: - return uint64(self.read_long()) diff --git a/opendis/DataOutputStream.py b/opendis/DataOutputStream.py deleted file mode 100644 index e3cb3db..0000000 --- a/opendis/DataOutputStream.py +++ /dev/null @@ -1,103 +0,0 @@ -""" -Writing to Java DataInputStream format. -From https://github.com/arngarden/python_java_datastream/blob/master/data_output_stream.py -This uses big endian (network) format -""" - -from io import BufferedIOBase -import struct - -from .types import ( - int8, - int16, - int32, - int64, - uint8, - uint16, - uint32, - uint64, - float32, - float64, - char8, - char16, -) - - -class DataOutputStream: - def __init__(self, stream: BufferedIOBase): - self.stream = stream - - def write_boolean(self, boolean: bool) -> None: - self.stream.write(struct.pack('?', boolean)) - - def write_byte(self, val: int) -> None: - self.stream.write(struct.pack('b', val)) - - def write_unsigned_byte(self, val: int) -> None: - self.stream.write(struct.pack('B', val)) - - def write_char(self, val: str) -> None: - self.stream.write(struct.pack('>H', ord(val))) - - def write_double(self, val: float) -> None: - self.stream.write(struct.pack('>d', val)) - - def write_float(self, val: float) -> None: - self.stream.write(struct.pack('>f', val)) - - def write_short(self, val: int) -> None: - self.stream.write(struct.pack('>h', val)) - - def write_unsigned_short(self, val: int) -> None: - self.stream.write(struct.pack('>H', val)) - - def write_long(self, val: int) -> None: - self.stream.write(struct.pack('>q', val)) - - def write_utf(self, string: bytes) -> None: - self.stream.write(struct.pack('>H', len(string))) - self.stream.write(string) - - def write_int(self, val: int) -> None: - self.stream.write(struct.pack('>i', val)) - - 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) - - # Aliases for convenience - def write_char8(self, val: char8) -> None: - self.write_byte(ord(val)) - - def write_float32(self, val: float32) -> None: - self.write_float(val) - - def write_float64(self, val: float64) -> None: - self.write_double(val) - - def write_int8(self, val: int8) -> None: - self.write_byte(val) - - def write_int16(self, val: int16) -> None: - self.write_short(val) - - def write_int32(self, val: int32) -> None: - self.write_int(val) - - def write_int64(self, val: int64) -> None: - self.write_long(val) - - def write_uint8(self, val: uint8) -> None: - self.write_unsigned_byte(val) - - def write_uint16(self, val: uint16) -> None: - self.write_unsigned_short(val) - - def write_uint32(self, val: uint32) -> None: - self.write_unsigned_int(val) - - def write_uint64(self, val: uint64) -> None: - self.stream.write(struct.pack('>Q', val)) diff --git a/opendis/dis7.py b/opendis/dis7.py index 781cc48..1b0036b 100644 --- a/opendis/dis7.py +++ b/opendis/dis7.py @@ -11,14 +11,22 @@ UnknownRadio, UnknownAntennaPattern, EulerAngles, + Vector3Float, + DEFireFlags, + WorldCoordinates, + EntityIdentifier, + EventIdentifier, + SimulationAddress, BeamAntennaPattern, GenericRadio, SimpleIntercomRadio, BasicHaveQuickMP, CCTTSincgarsMP, VariableTransmitterParametersRecord, - HighFidelityHAVEQUICKRadio, - UnknownVariableTransmitterParameters, + DamageDescriptionRecord, + StandardVariableRecord, + getStandardVariableClass, + base ) from .stream import DataInputStream, DataOutputStream from .types import ( @@ -38,6 +46,28 @@ ) +def parseStandardVariableRecord( + inputStream: DataInputStream, + expectedType: type[base.StandardVariableRecord], +) -> base.StandardVariableRecord: + """Parse a single Standard Variable Record from the input stream. + These rely on recordType enums from [UID 66]. + The mapping of recordType to class is defined in opendis.radio + + Args: + inputStream: The DataInputStream to read from. + + Returns: + An instance of a StandardVariableRecord subclass. + """ + recordType = inputStream.read_uint32() + recordLength = inputStream.read_uint16() + svClass = getStandardVariableClass(recordType, expectedType) + sv_instance = svClass() + sv_instance.parse(inputStream, recordLength) + return sv_instance + + class DataQueryDatumSpecification: """Section 6.2.17 @@ -311,8 +341,8 @@ class DeadReckoningParameters: def __init__(self, deadReckoningAlgorithm: enum8 = 0, # [UID 44] parameters=None, - entityLinearAcceleration: "Vector3Float | None" = None, - entityAngularVelocity: "Vector3Float | None" = None): + entityLinearAcceleration: Vector3Float | None = None, + entityAngularVelocity: Vector3Float | None = None): self.deadReckoningAlgorithm = deadReckoningAlgorithm """Algorithm to use in computing dead reckoning. See EBV doc.""" self.parameters = parameters or [0] * 15 @@ -464,7 +494,7 @@ def __init__(self, """Indicates association status between two entities; 8 bit enum""" self.associationType = associationType """Type of association; 8 bit enum""" - self.entityID = entityID or EntityID() + self.entityID = entityID or EntityIdentifier() """Object ID of entity associated with this entity""" self.ownStationLocation = ownStationLocation """Station location on one's own entity. EBV doc.""" @@ -687,89 +717,6 @@ def parse(self, inputStream): self.stationNumber = inputStream.read_unsigned_short() -class DirectedEnergyPrecisionAimpoint: - """Section 6.2.20.3 - - DE Precision Aimpoint Record. - """ - recordType: enum32 = 4000 - recordLength: uint16 = 88 - - def __init__(self, - targetSpotLocation: "Vector3Double | None" = None, - targetSpotEntityLocation: "Vector3Float | None" = None, - targetSpotVelocity: "Vector3Float | None" = None, - targetSpotAcceleration: "Vector3Float | None" = None, - targetEntityID: "EntityID | None" = None, - targetComponentID: enum8 = 0, # [UID 314] - beamSpotType: enum8 = 0, # [UID 311] - beamSpotCrossSectionSemiMajorAxis: float32 = 0.0, # in meters - beamSpotCrossSectionSemiMinorAxis: float32 = 0.0, # in meters - beamSpotCrossSectionOrientationAngle: float32 = 0.0, # in radians - peakIrradiance: float32 = 0.0): # in W/m^2 - self.padding: uint16 = 0 - self.targetSpotLocation = targetSpotLocation or Vector3Double() - """Position of Target Spot in World Coordinates.""" - self.targetSpotEntityLocation = targetSpotEntityLocation or Vector3Float( - ) - """Position (meters) of Target Spot relative to Entity Position.""" - self.targetSpotVelocity = targetSpotVelocity or Vector3Float() - """Velocity (meters/sec) of Target Spot.""" - self.targetSpotAcceleration = targetSpotAcceleration or Vector3Float() - """Acceleration (meters/sec/sec) of Target Spot.""" - self.targetEntityID = targetEntityID or EntityID() - """Unique ID of the target entity.""" - self.targetComponentID = targetComponentID - """Target Component ID ENUM, same as in DamageDescriptionRecord.""" - self.beamSpotType = beamSpotType - """Spot Shape ENUM.""" - self.beamSpotCrossSectionSemiMajorAxis = beamSpotCrossSectionSemiMajorAxis - """Beam Spot Cross Section Semi-Major Axis.""" - self.beamSpotCrossSectionSemiMinorAxis = beamSpotCrossSectionSemiMinorAxis - """Beam Spot Cross Section Semi-Major Axis.""" - self.beamSpotCrossSectionOrientationAngle = beamSpotCrossSectionOrientationAngle - """Beam Spot Cross Section Orientation Angle.""" - self.peakIrradiance = peakIrradiance - """Peak irradiance""" - self.padding2: uint32 = 0 - - def serialize(self, outputStream): - """serialize the class""" - outputStream.write_unsigned_int(self.recordType) - outputStream.write_unsigned_short(self.recordLength) - outputStream.write_unsigned_short(self.padding) - self.targetSpotLocation.serialize(outputStream) - self.targetSpotEntityLocation.serialize(outputStream) - self.targetSpotVelocity.serialize(outputStream) - self.targetSpotAcceleration.serialize(outputStream) - self.targetEntityID.serialize(outputStream) - outputStream.write_unsigned_byte(self.targetComponentID) - outputStream.write_unsigned_byte(self.beamSpotType) - outputStream.write_float(self.beamSpotCrossSectionSemiMajorAxis) - outputStream.write_float(self.beamSpotCrossSectionSemiMinorAxis) - outputStream.write_float(self.beamSpotCrossSectionOrientationAngle) - outputStream.write_float(self.peakIrradiance) - outputStream.write_unsigned_int(self.padding2) - - def parse(self, inputStream): - """Parse a message. This may recursively call embedded objects.""" - self.recordType = inputStream.read_unsigned_int() # TODO: validate - self.recordLength = inputStream.read_unsigned_short() # TODO: validate - self.padding = inputStream.read_unsigned_short() - self.targetSpotLocation.parse(inputStream) - self.targetSpotEntityLocation.parse(inputStream) - self.targetSpotVelocity.parse(inputStream) - self.targetSpotAcceleration.parse(inputStream) - self.targetEntityID.parse(inputStream) - self.targetComponentID = inputStream.read_unsigned_byte() - self.beamSpotType = inputStream.read_unsigned_byte() - self.beamSpotCrossSectionSemiMajorAxis = inputStream.read_float() - self.beamSpotCrossSectionSemiMinorAxis = inputStream.read_float() - self.beamSpotCrossSectionOrientationAngle = inputStream.read_float() - self.peakIrradiance = inputStream.read_float() - self.padding2 = inputStream.read_unsigned_int() - - class IFFDataSpecification: """Section 6.2.43 @@ -804,8 +751,10 @@ class OwnershipStatus: Used to convey entity and conflict status information associated with transferring ownership of an entity. """ - def __init__(self, entityId=None, ownershipStatus=0): - self.entityId = entityId or EntityID() + def __init__(self, + entityId: EntityIdentifier | None = None, + ownershipStatus=0): + self.entityId = entityId or EntityIdentifier() """EntityID""" self.ownershipStatus = ownershipStatus """The ownership and/or ownership conflict status of the entity represented by the Entity ID field.""" @@ -1006,13 +955,13 @@ class Association: def __init__(self, associationType: enum8 = 0, # [UID 330] - associatedEntityID: "EntityID | None" = None, - associatedLocation: "Vector3Double | None" = None): + associatedEntityID: EntityIdentifier | None = None, + associatedLocation: WorldCoordinates | None = None): self.associationType = associationType self.padding4: uint8 = 0 - self.associatedEntityID = associatedEntityID or EntityID() + self.associatedEntityID = associatedEntityID or EntityIdentifier() """identity of associated entity. If none, NO_SPECIFIC_ENTITY""" - self.associatedLocation = associatedLocation or Vector3Double() + self.associatedLocation = associatedLocation or WorldCoordinates() """location, in world coordinates""" def serialize(self, outputStream): @@ -1080,9 +1029,9 @@ class AntennaLocation: """ def __init__(self, - antennaLocation: "Vector3Double | None" = None, - relativeAntennaLocation: "Vector3Float | None" = None): - self.antennaLocation = antennaLocation or Vector3Double() + antennaLocation: WorldCoordinates | None = None, + relativeAntennaLocation: Vector3Float | None = None): + self.antennaLocation = antennaLocation or WorldCoordinates() """Location of the radiating portion of the antenna in world coordinates""" self.relativeAntennaLocation = relativeAntennaLocation or Vector3Float( ) @@ -1342,75 +1291,6 @@ def parse(self, inputStream): self.beamwidthDownElevation = inputStream.read_float() -class DirectedEnergyDamage: - """Section 6.2.15.2 - - Damage sustained by an entity due to directed energy. Location of the - damage based on a relative x,y,z location from the center of the entity. - """ - recordType: enum32 = 4500 # [UID 66] Variable Record Type - recordLength: uint16 = 40 # in bytes - - def __init__( - self, - damageLocation: "Vector3Float | None" = None, - damageDiameter: float32 = 0.0, # in metres - temperature: float32 = -273.15, # in degrees Celsius - componentIdentification: enum8 = 0, # [UID 314] - componentDamageStatus: enum8 = 0, # [UID 315] - componentVisualDamageStatus: struct8 = 0, # [UID 317] - componentVisualSmokeColor: enum8 = 0, # [UID 316] - fireEventID: "EventIdentifier | None" = None): - self.padding: uint16 = 0 - self.damageLocation = damageLocation or Vector3Float() - """location of damage, relative to center of entity""" - self.damageDiameter = damageDiameter - """Size of damaged area, in meters.""" - self.temperature = temperature - """average temp of the damaged area, in degrees celsius. If firing entitty does not model this, use a value of -273.15""" - self.componentIdentification = componentIdentification - """enumeration""" - self.componentDamageStatus = componentDamageStatus - """enumeration""" - self.componentVisualDamageStatus = componentVisualDamageStatus - """enumeration""" - self.componentVisualSmokeColor = componentVisualSmokeColor - """enumeration""" - self.fireEventID = fireEventID or EventIdentifier() - """For any component damage resulting this field shall be set to the fire event ID from that PDU.""" - self.padding2: uint16 = 0 - - def serialize(self, outputStream): - """serialize the class""" - outputStream.write_unsigned_int(self.recordType) - outputStream.write_unsigned_short(self.recordLength) - outputStream.write_unsigned_short(self.padding) - self.damageLocation.serialize(outputStream) - outputStream.write_float(self.damageDiameter) - outputStream.write_float(self.temperature) - outputStream.write_unsigned_byte(self.componentIdentification) - outputStream.write_unsigned_byte(self.componentDamageStatus) - outputStream.write_unsigned_byte(self.componentVisualDamageStatus) - outputStream.write_unsigned_byte(self.componentVisualSmokeColor) - self.fireEventID.serialize(outputStream) - outputStream.write_unsigned_short(self.padding2) - - def parse(self, inputStream): - """Parse a message. This may recursively call embedded objects.""" - self.recordType = inputStream.read_unsigned_int() # TODO: validate - self.recordLength = inputStream.read_unsigned_short() # TODO: validate - self.padding = inputStream.read_unsigned_short() - self.damageLocation.parse(inputStream) - self.damageDiameter = inputStream.read_float() - self.temperature = inputStream.read_float() - self.componentIdentification = inputStream.read_unsigned_byte() - self.componentDamageStatus = inputStream.read_unsigned_byte() - self.componentVisualDamageStatus = inputStream.read_unsigned_byte() - self.componentVisualSmokeColor = inputStream.read_unsigned_byte() - self.fireEventID.parse(inputStream) - self.padding2 = inputStream.read_unsigned_short() - - class ExplosionDescriptor: """Section 6.2.19.3 @@ -1757,92 +1637,6 @@ def parse(self, inputStream): self.variableDatumRecords.append(element) -class DirectedEnergyAreaAimpoint: - """Section 6.2.20.2 - - DE Precision Aimpoint Record. NOT COMPLETE - """ - recordType: enum32 = 4001 # [UID 66] - - def __init__(self, - recordLength: uint16 = 0, - beamAntennaParameters: list | None = None, - directedEnergyTargetEnergyDepositions: list | None -= None): - """Type of Record enumeration""" - self.recordLength = recordLength - """Length of Record""" - self.padding: uint16 = 0 - self.beamAntennaParameters = beamAntennaParameters or [] - """list of beam antenna records. See 6.2.9.2""" - self.directedEnergyTargetEnergyDepositionRecordList = directedEnergyTargetEnergyDepositions or [] - """list of DE target deposition records. See 6.2.21.4""" - - @property - def beamAntennaPatternRecordCount(self) -> uint16: - return len(self.beamAntennaParameters) - - @property - def directedEnergyTargetEnergyDepositionRecordCount(self) -> uint16: - return len(self.directedEnergyTargetEnergyDepositionRecordList) - - def serialize(self, outputStream): - """serialize the class""" - outputStream.write_unsigned_int(self.recordType) - outputStream.write_unsigned_short(self.recordLength) - outputStream.write_unsigned_short(self.padding) - outputStream.write_unsigned_short(self.beamAntennaPatternRecordCount) - outputStream.write_unsigned_short( - self.directedEnergyTargetEnergyDepositionRecordCount - ) - for anObj in self.beamAntennaParameters: - anObj.serialize(outputStream) - - for anObj in self.directedEnergyTargetEnergyDepositionRecordList: - anObj.serialize(outputStream) - - def parse(self, inputStream): - """Parse a message. This may recursively call embedded objects.""" - self.recordType = inputStream.read_unsigned_int() # TODO: validate - self.recordLength = inputStream.read_unsigned_short() # TODO: validate - self.padding = inputStream.read_unsigned_short() - beamAntennaPatternRecordCount = inputStream.read_unsigned_short() - directedEnergyTargetEnergyDepositionRecordCount = inputStream.read_unsigned_short() - for idx in range(0, beamAntennaPatternRecordCount): - element = null() - element.parse(inputStream) - self.beamAntennaParameters.append(element) - - for idx in range(0, directedEnergyTargetEnergyDepositionRecordCount): - element = null() - element.parse(inputStream) - self.directedEnergyTargetEnergyDepositionRecordList.append(element) - - -class Vector3Float: - """Section 6.2.95 - - Three floating point values, x, y, and z. - """ - - def __init__(self, x: float32 = 0.0, y: float32 = 0.0, z: float32 = 0.0): - self.x = x - self.y = y - self.z = z - - def serialize(self, outputStream): - """serialize the class""" - outputStream.write_float(self.x) - outputStream.write_float(self.y) - outputStream.write_float(self.z) - - def parse(self, inputStream): - """Parse a message. This may recursively call embedded objects.""" - self.x = inputStream.read_float() - self.y = inputStream.read_float() - self.z = inputStream.read_float() - - class Expendable: """Section 6.2.36 @@ -1923,8 +1717,8 @@ def __init__(self, segmentModification: enum8 = 0, # [UID 241] generalSegmentAppearance: struct16 = 0, # [UID 229] specificSegmentAppearance: struct32 = 0, # TODO: find reference - segmentLocation: "Vector3Double | None" = None, - segmentOrientation: "EulerAngles | None" = None, + segmentLocation: WorldCoordinates | None = None, + segmentOrientation: EulerAngles | None = None, segmentLength: float32 = 0.0, # in meters segmentWidth: float32 = 0.0, # in meters segmentHeight: float32 = 0.0, # in meters @@ -1937,7 +1731,7 @@ def __init__(self, """general dynamic appearance attributes of the segment. This record shall be defined as a 16-bit record of enumerations. The values defined for this record are included in Section 12 of SISO-REF-010.""" self.specificSegmentAppearance = specificSegmentAppearance """This field shall specify specific dynamic appearance attributes of the segment. This record shall be defined as a 32-bit record of enumerations.""" - self.segmentLocation = segmentLocation or Vector3Double() + self.segmentLocation = segmentLocation or WorldCoordinates() """This field shall specify the location of the linear segment in the simulated world and shall be represented by a World Coordinates record""" self.segmentOrientation = segmentOrientation or EulerAngles() """orientation of the linear segment about the segment location and shall be represented by a Euler Angles record""" @@ -1980,32 +1774,6 @@ def parse(self, inputStream): self.padding = inputStream.read_unsigned_int() -class SimulationAddress: - """Section 6.2.79 - - A Simulation Address record shall consist of the Site Identification number - and the Application Identification number. - """ - - def __init__(self, - site: uint16 = 0, - application: uint16 = 0): - self.site = site - """A site is defined as a facility, installation, organizational unit or a geographic location that has one or more simulation applications capable of participating in a distributed event.""" - self.application = application - """An application is defined as a software program that is used to generate and process distributed simulation data including live, virtual and constructive data.""" - - def serialize(self, outputStream): - """serialize the class""" - outputStream.write_unsigned_short(self.site) - outputStream.write_unsigned_short(self.application) - - def parse(self, inputStream): - """Parse a message. This may recursively call embedded objects.""" - self.site = inputStream.read_unsigned_short() - self.application = inputStream.read_unsigned_short() - - class SystemIdentifier: """Section 6.2.87 @@ -2048,10 +1816,10 @@ class TrackJamData: """ def __init__(self, - entityID: "EntityID | None" = None, + entityID: EntityIdentifier | None = None, emitterNumber: uint8 = 0, beamNumber: uint8 = 0): - self.entityID = entityID or EntityID() + self.entityID = entityID or EntityIdentifier() """the entity tracked or illumated, or an emitter beam targeted with jamming""" self.emitterNumber = emitterNumber """Emitter system associated with the entity""" @@ -2129,8 +1897,8 @@ class SimulationManagementPduHeader: def __init__(self, pduHeader: "PduHeader | None" = None, - originatingID: "SimulationIdentifier | EntityID | None" = None, - receivingID: "SimulationIdentifier | EntityID | None" = None): + originatingID: "SimulationIdentifier | EntityIdentifier | None" = None, + receivingID: "SimulationIdentifier | EntityIdentifier | None" = None): self.pduHeader = pduHeader or PduHeader() """Conventional PDU header""" self.originatingID = originatingID or SimulationIdentifier() @@ -2474,31 +2242,6 @@ def parse(self, inputStream): self.aggregateType.parse(inputStream) -class EventIdentifier: - """Section 6.2.34 - - Identifies an event in the world. Use this format for every PDU EXCEPT - the LiveEntityPdu. - """ - - def __init__(self, - simulationAddress: "SimulationAddress | None" = None, - eventNumber: uint16 = 0): - self.simulationAddress = simulationAddress or SimulationAddress() - """Site and application IDs""" - self.eventNumber = eventNumber - - def serialize(self, outputStream): - """serialize the class""" - self.simulationAddress.serialize(outputStream) - outputStream.write_unsigned_short(self.eventNumber) - - def parse(self, inputStream): - """Parse a message. This may recursively call embedded objects.""" - self.simulationAddress.parse(inputStream) - self.eventNumber = inputStream.read_unsigned_short() - - class BlankingSector: """Section 6.2.21.2 @@ -2574,17 +2317,17 @@ class LaunchedMunitionRecord: """ def __init__(self, - fireEventID: "EventIdentifier | None" = None, - firingEntityID: "EventIdentifier | None" = None, - targetEntityID: "EventIdentifier | None" = None, - targetLocation: "Vector3Double | None" = None): + fireEventID: EventIdentifier | None = None, + firingEntityID: EventIdentifier | None = None, + targetEntityID: EventIdentifier | None = None, + targetLocation: WorldCoordinates | None = None): self.fireEventID = fireEventID or EventIdentifier() self.padding: uint16 = 0 self.firingEntityID = firingEntityID or EventIdentifier() self.padding2: uint16 = 0 self.targetEntityID = targetEntityID or EventIdentifier() self.padding3: uint16 = 0 - self.targetLocation = targetLocation or Vector3Double() + self.targetLocation = targetLocation or WorldCoordinates() def serialize(self, outputStream): """serialize the class""" @@ -3294,85 +3037,6 @@ def parse(self, inputStream): self.maximumQuantityReloadTime = inputStream.read_unsigned_int() -class EntityIdentifier: - """Section 6.2.28 - - Entity Identifier. Unique ID for entities in the world. Consists of a - simulation address and a entity number. - """ - - def __init__(self, - simulationAddress: "SimulationAddress | None" = None, - entityNumber: uint16 = 0): - self.simulationAddress = simulationAddress or SimulationAddress() - """Site and application IDs""" - self.entityNumber = entityNumber - """Entity number""" - - def serialize(self, outputStream): - """serialize the class""" - self.simulationAddress.serialize(outputStream) - outputStream.write_unsigned_short(self.entityNumber) - - def parse(self, inputStream): - """Parse a message. This may recursively call embedded objects.""" - self.simulationAddress.parse(inputStream) - self.entityNumber = inputStream.read_unsigned_short() - - -class DirectedEnergyTargetEnergyDeposition: - """Section 6.2.20.4 - - DE energy deposition properties for a target entity. - """ - - def __init__(self, - targetEntityID: "EntityIdentifier | None" = None, - peakIrradiance: float32 = 0.0): # in W/m^2 - self.targetEntityID = targetEntityID or EntityID() - """Unique ID of the target entity.""" - self.padding: uint16 = 0 - self.peakIrradiance = peakIrradiance - """Peak irrandiance""" - - def serialize(self, outputStream): - """serialize the class""" - self.targetEntityID.serialize(outputStream) - outputStream.write_unsigned_short(self.padding) - outputStream.write_float(self.peakIrradiance) - - def parse(self, inputStream): - """Parse a message. This may recursively call embedded objects.""" - self.targetEntityID.parse(inputStream) - self.padding = inputStream.read_unsigned_short() - self.peakIrradiance = inputStream.read_float() - - -class EntityID: - """more laconically named EntityIdentifier""" - - def __init__(self, siteID=0, applicationID=0, entityID=0): - self.siteID = siteID - """Site ID""" - self.applicationID = applicationID - """application number ID""" - self.entityID = entityID - """Entity number ID""" - - def serialize(self, outputStream): - """serialize the class""" - outputStream.write_unsigned_short(self.siteID) - outputStream.write_unsigned_short(self.applicationID) - outputStream.write_unsigned_short(self.entityID) - - def parse(self, inputStream): - """Parse a message. This may recursively call embedded objects.""" - - self.siteID = inputStream.read_unsigned_short() - self.applicationID = inputStream.read_unsigned_short() - self.entityID = inputStream.read_unsigned_short() - - class EngineFuelReload: """For each type or location of engine fuel, this record specifies the type, location, fuel measurement units, and reload quantity and maximum quantity. Section 6.2.25.""" @@ -3545,34 +3209,6 @@ def parse(self, inputStream): self.padding2 = inputStream.read_unsigned_byte() -class Vector3Double: - """Section 6.2.97 - - Three double precision floating point values, x, y, and z. - Used for world coordinates. - """ - - def __init__(self, x: float32 = 0.0, y: float32 = 0.0, z: float32 = 0.0): - self.x = x - """X value""" - self.y = y - """y Value""" - self.z = z - """Z value""" - - def serialize(self, outputStream): - """serialize the class""" - outputStream.write_double(self.x) - outputStream.write_double(self.y) - outputStream.write_double(self.z) - - def parse(self, inputStream): - """Parse a message. This may recursively call embedded objects.""" - self.x = inputStream.read_double() - self.y = inputStream.read_double() - self.z = inputStream.read_double() - - class GridAxis: """Section 6.2.41 @@ -3951,14 +3587,14 @@ class SeparationVP: def __init__(self, reasonForSeparation: enum8 = 0, # [UID 282] preEntityIndicator: enum8 = 0, # [UID 283] - parentEntityID: "EntityID | None" = None, + parentEntityID: EntityIdentifier | None = None, stationLocation: "NamedLocationIdentification | None" = None): self.reasonForSeparation = reasonForSeparation """Reason for separation. EBV""" self.preEntityIndicator = preEntityIndicator """Whether the entity existed prior to separation EBV""" self.padding1: uint8 = 0 - self.parentEntityID = parentEntityID or EntityID() + self.parentEntityID = parentEntityID or EntityIdentifier() """ID of parent""" self.padding2: uint16 = 0 self.stationLocation = stationLocation or NamedLocationIdentification() @@ -4130,19 +3766,19 @@ class EntityStateUpdatePdu(EntityInformationFamilyPdu): pduType: enum8 = 67 # [UID 4] def __init__(self, - entityID=None, - entityLinearVelocity: "Vector3Float | None" = None, - entityLocation: "Vector3Double | None" = None, - entityOrientation: "EulerAngles | None" = None, + entityID: EntityIdentifier | None = None, + entityLinearVelocity: Vector3Float | None = None, + entityLocation: WorldCoordinates | None = None, + entityOrientation: EulerAngles | None = None, entityAppearance: struct32 = 0, # [UID 31-43] - variableParameters: list["VariableParameter"] | None = None): + variableParameters: list[VariableParameter] | None = None): super(EntityStateUpdatePdu, self).__init__() - self.entityID = entityID or EntityID() + self.entityID = entityID or EntityIdentifier() """This field shall identify the entity issuing the PDU, and shall be represented by an Entity Identifier record (see 6.2.28).""" self.padding1: uint8 = 0 self.entityLinearVelocity = entityLinearVelocity or Vector3Float() """This field shall specify an entitys linear velocity. The coordinate system for an entitys linear velocity depends on the dead reckoning algorithm used. This field shall be represented by a Linear Velocity Vector record [see 6.2.95 item c)]).""" - self.entityLocation = entityLocation or Vector3Double() + self.entityLocation = entityLocation or WorldCoordinates() """This field shall specify an entitys physical location in the simulated world and shall be represented by a World Coordinates record (see 6.2.97).""" self.entityOrientation = entityOrientation or EulerAngles() """This field shall specify an entitys orientation and shall be represented by an Euler Angles record (see 6.2.33).""" @@ -4193,14 +3829,14 @@ class ServiceRequestPdu(LogisticsFamilyPdu): pduType: enum8 = 5 # [UID 4] def __init__(self, - requestingEntityID: "EntityID | None" = None, - servicingEntityID: "EntityID | None" = None, + requestingEntityID: EntityIdentifier | None = None, + servicingEntityID: EntityIdentifier | None = None, serviceTypeRequested: enum8 = 0, # [UID 63] supplies: list["SupplyQuantity"] | None = None): super(ServiceRequestPdu, self).__init__() - self.requestingEntityID = requestingEntityID or EntityID() + self.requestingEntityID = requestingEntityID or EntityIdentifier() """Entity that is requesting service (see 6.2.28), Section 7.4.2""" - self.servicingEntityID = servicingEntityID or EntityID() + self.servicingEntityID = servicingEntityID or EntityIdentifier() """Entity that is providing the service (see 6.2.28), Section 7.4.2""" self.serviceTypeRequested = serviceTypeRequested """Type of service requested, Section 7.4.2""" @@ -4244,13 +3880,13 @@ class RepairCompletePdu(LogisticsFamilyPdu): pduType: enum8 = 9 # [UID 4] def __init__(self, - receivingEntityID: "EntityID | None" = None, - repairingEntityID: "EntityID | None" = None, + receivingEntityID: EntityIdentifier | None = None, + repairingEntityID: EntityIdentifier | None = None, repair: enum16 = 0): # [UID 64] super(RepairCompletePdu, self).__init__() - self.receivingEntityID = receivingEntityID or EntityID() + self.receivingEntityID = receivingEntityID or EntityIdentifier() """Entity that is receiving service. See 6.2.28""" - self.repairingEntityID = repairingEntityID or EntityID() + self.repairingEntityID = repairingEntityID or EntityIdentifier() """Entity that is supplying. See 6.2.28""" self.repair = repair """Enumeration for type of repair. See 6.2.74""" @@ -4302,17 +3938,17 @@ class CollisionPdu(EntityInformationFamilyPdu): pduType: enum8 = 4 # [UID 4] def __init__(self, - issuingEntityID: "EntityID | None" = None, - collidingEntityID: "EntityID | None" = None, + issuingEntityID: EntityIdentifier | None = None, + collidingEntityID: EntityIdentifier | None = None, eventID: "EventIdentifier | None" = None, collisionType: enum8 = 0, # [UID 189] - velocity: "Vector3Float | None" = None, + velocity: Vector3Float | None = None, mass: float32 = 0.0, # in kg - location: "Vector3Float | None" = None): + location: Vector3Float | None = None): super(CollisionPdu, self).__init__() - self.issuingEntityID = issuingEntityID or EntityID() + self.issuingEntityID = issuingEntityID or EntityIdentifier() """This field shall identify the entity that is issuing the PDU, and shall be represented by an Entity Identifier record (see 6.2.28).""" - self.collidingEntityID = collidingEntityID or EntityID() + self.collidingEntityID = collidingEntityID or EntityIdentifier() """This field shall identify the entity that has collided with the issuing entity (see 5.3.3.4). This field shall be represented by an Entity Identifier record (see 6.2.28).""" self.eventID = eventID or EventIdentifier() """This field shall contain an identification generated by the issuing simulation application to associate related collision events. This field shall be represented by an Event Identifier record (see 6.2.34).""" @@ -4359,13 +3995,13 @@ class RepairResponsePdu(LogisticsFamilyPdu): pduType: enum8 = 10 # [UID 4] def __init__(self, - receivingEntityID: "EntityID | None" = None, - repairingEntityID: "EntityID | None" = None, + receivingEntityID: EntityIdentifier | None = None, + repairingEntityID: EntityIdentifier | None = None, repairResult: enum8 = 0): # [UID 65] super(RepairResponsePdu, self).__init__() - self.receivingEntityID = receivingEntityID or EntityID() + self.receivingEntityID = receivingEntityID or EntityIdentifier() """Entity that requested repairs. See 6.2.28""" - self.repairingEntityID = repairingEntityID or EntityID() + self.repairingEntityID = repairingEntityID or EntityIdentifier() """Entity that is repairing. See 6.2.28""" self.repairResult = repairResult """Result of repair operation""" @@ -4399,12 +4035,12 @@ class SimulationManagementFamilyPdu(Pdu): protocolFamily: enum8 = 5 # [UID 5] def __init__(self, - originatingEntityID: "EntityID | None" = None, - receivingEntityID: "EntityID | None" = None): + originatingEntityID: EntityIdentifier | None = None, + receivingEntityID: EntityIdentifier | None = None): super(SimulationManagementFamilyPdu, self).__init__() - self.originatingEntityID = originatingEntityID or EntityID() + self.originatingEntityID = originatingEntityID or EntityIdentifier() """Entity that is sending message""" - self.receivingEntityID = receivingEntityID or EntityID() + self.receivingEntityID = receivingEntityID or EntityIdentifier() """Entity that is intended to receive message""" def serialize(self, outputStream): @@ -4481,8 +4117,8 @@ class LinearObjectStatePdu(SyntheticEnvironmentFamilyPdu): pduType: enum8 = 44 # [UID 4] def __init__(self, - objectID: "EntityID | None" = None, - referencedObjectID: "EntityID | None" = None, + objectID: EntityIdentifier | None = None, + referencedObjectID: EntityIdentifier | None = None, updateNumber: uint16 = 0, forceID: enum8 = 0, # [UID 6] requesterID: "SimulationAddress | None" = None, @@ -4490,9 +4126,9 @@ def __init__(self, objectType: "ObjectType | None" = None, linearSegmentParameters: list["LinearSegmentParameter"] | None = None): super(LinearObjectStatePdu, self).__init__() - self.objectID = objectID or EntityID() + self.objectID = objectID or EntityIdentifier() """Object in synthetic environment""" - self.referencedObjectID = referencedObjectID or EntityID() + self.referencedObjectID = referencedObjectID or EntityIdentifier() """Object with which this point object is associated""" self.updateNumber = updateNumber """unique update number of each state transition of an object""" @@ -4589,7 +4225,7 @@ class IntercomSignalPdu(RadioCommunicationsFamilyPdu): pduType: enum8 = 31 # [UID 4] def __init__(self, - entityID: "EntityID | ObjectID | UnattachedIdentifier | None" = None, + entityID: "EntityIdentifier | ObjectIdentifier | UnattachedIdentifier | None" = None, communicationsDeviceID: uint16 = 0, encodingScheme: struct16 = 00, tdlType: uint16 = 0, # [UID 178] @@ -4597,7 +4233,7 @@ def __init__(self, samples: uint16 = 0, data: list[bytes] | None = None): super(IntercomSignalPdu, self).__init__() - self.entityID = entityID or EntityID() + self.entityID = entityID or EntityIdentifier() self.communicationsDeviceID = communicationsDeviceID self.encodingScheme = encodingScheme self.tdlType = tdlType @@ -4677,13 +4313,13 @@ class ResupplyReceivedPdu(LogisticsFamilyPdu): pduType: enum8 = 7 # [UID 4] def __init__(self, - receivingEntityID: "EntityID | None" = None, - supplyingEntityID: "EntityID | None" = None, + receivingEntityID: EntityIdentifier | None = None, + supplyingEntityID: EntityIdentifier | None = None, supplies: list["SupplyQuantity"] | None = None): super(ResupplyReceivedPdu, self).__init__() - self.receivingEntityID = receivingEntityID or EntityID() + self.receivingEntityID = receivingEntityID or EntityIdentifier() """Entity that is receiving service. Shall be represented by Entity Identifier record (see 6.2.28)""" - self.supplyingEntityID = supplyingEntityID or EntityID() + self.supplyingEntityID = supplyingEntityID or EntityIdentifier() """Entity that is supplying. Shall be represented by Entity Identifier record (see 6.2.28)""" self.padding1: uint8 = 0 self.padding2: uint16 = 0 @@ -4728,12 +4364,12 @@ class WarfareFamilyPdu(Pdu): protocolFamily: enum8 = 2 # [UID 5] def __init__(self, - firingEntityID: "EntityID | None" = None, - targetEntityID: "EntityID | None" = None): + firingEntityID: EntityIdentifier | None = None, + targetEntityID: EntityIdentifier | None = None): super(WarfareFamilyPdu, self).__init__() - self.firingEntityID = firingEntityID or EntityID() + self.firingEntityID = firingEntityID or EntityIdentifier() """ID of the entity that shot""" - self.targetEntityID = targetEntityID or EntityID() + self.targetEntityID = targetEntityID or EntityIdentifier() """ID of the entity that is being shot at""" def serialize(self, outputStream): @@ -4758,24 +4394,24 @@ class CollisionElasticPdu(EntityInformationFamilyPdu): pduType: enum8 = 66 # [UID 4] def __init__(self, - issuingEntityID: "EntityID | None" = None, - collidingEntityID: "EntityID | None" = None, + issuingEntityID: EntityIdentifier | None = None, + collidingEntityID: EntityIdentifier | None = None, collisionEventID: "EventIdentifier | None" = None, - contactVelocity: "Vector3Float | None" = None, + contactVelocity: Vector3Float | None = None, mass: float32 = 0.0, # in kg - locationOfImpact: "Vector3Float | None" = None, + locationOfImpact: Vector3Float | None = None, collisionIntermediateResultXX: float32 = 0.0, collisionIntermediateResultXY: float32 = 0.0, collisionIntermediateResultXZ: float32 = 0.0, collisionIntermediateResultYY: float32 = 0.0, collisionIntermediateResultYZ: float32 = 0.0, collisionIntermediateResultZZ: float32 = 0.0, - unitSurfaceNormal: "Vector3Float | None" = None, + unitSurfaceNormal: Vector3Float | None = None, coefficientOfRestitution: float32 = 0.0): super(CollisionElasticPdu, self).__init__() - self.issuingEntityID = issuingEntityID or EntityID() + self.issuingEntityID = issuingEntityID or EntityIdentifier() """This field shall identify the entity that is issuing the PDU and shall be represented by an Entity Identifier record (see 6.2.28)""" - self.collidingEntityID = collidingEntityID or EntityID() + self.collidingEntityID = collidingEntityID or EntityIdentifier() """This field shall identify the entity that has collided with the issuing entity. This field shall be a valid identifier of an entity or server capable of responding to the receipt of this Collision-Elastic PDU. This field shall be represented by an Entity Identifier record (see 6.2.28).""" self.collisionEventID = collisionEventID or EventIdentifier() """This field shall contain an identification generated by the issuing simulation application to associate related collision events. This field shall be represented by an Event Identifier record (see 6.2.34).""" @@ -4953,12 +4589,12 @@ class SimulationManagementWithReliabilityFamilyPdu(Pdu): protocolFamily: enum8 = 10 # [UID 5] def __init__(self, - originatingEntityID: "EntityID | None" = None, - receivingEntityID: "EntityID | None" = None): + originatingEntityID: EntityIdentifier | None = None, + receivingEntityID: EntityIdentifier | None = None): super(SimulationManagementWithReliabilityFamilyPdu, self).__init__() - self.originatingEntityID = originatingEntityID or EntityID() + self.originatingEntityID = originatingEntityID or EntityIdentifier() """Object originating the request""" - self.receivingEntityID = receivingEntityID or EntityID() + self.receivingEntityID = receivingEntityID or EntityIdentifier() """Object with which this point object is associated""" def serialize(self, outputStream): @@ -5047,23 +4683,23 @@ class DesignatorPdu(DistributedEmissionsFamilyPdu): pduType: enum8 = 24 # [UID 4] def __init__(self, - designatingEntityID: "EntityID | None" = None, + designatingEntityID: EntityIdentifier | None = None, codeName: enum16 = 0, # [UID 80] - designatedEntityID: "EntityID | None" = None, + designatedEntityID: EntityIdentifier | None = None, designatorCode: enum16 = 0, # [UID 81] designatorPower: float32 = 0.0, # in W designatorWavelength: float32 = 0.0, # in microns - designatorSpotWrtDesignated: "Vector3Float | None" = None, - designatorSpotLocation: "Vector3Double | None" = None, + designatorSpotWrtDesignated: Vector3Float | None = None, + designatorSpotLocation: WorldCoordinates | None = None, # Dead Reckoning Parameters deadReckoningAlgorithm: enum8 = 0, # [UID 44] - entityLinearAcceleration: "Vector3Float | None" = None): + entityLinearAcceleration: Vector3Float | None = None): super(DesignatorPdu, self).__init__() - self.designatingEntityID = designatingEntityID or EntityID() + self.designatingEntityID = designatingEntityID or EntityIdentifier() """ID of the entity designating""" self.codeName = codeName """This field shall specify a unique emitter database number assigned to differentiate between otherwise similar or identical emitter beams within an emitter system.""" - self.designatedEntityID = designatedEntityID or EntityID() + self.designatedEntityID = designatedEntityID or EntityIdentifier() """ID of the entity being designated""" self.designatorCode = designatorCode """This field shall identify the designator code being used by the designating entity""" @@ -5074,7 +4710,7 @@ def __init__(self, self.designatorSpotWrtDesignated = designatorSpotWrtDesignated or Vector3Float( ) """designator spot wrt the designated entity""" - self.designatorSpotLocation = designatorSpotLocation or Vector3Double() + self.designatorSpotLocation = designatorSpotLocation or WorldCoordinates() """designator spot wrt the designated entity""" self.deadReckoningAlgorithm = deadReckoningAlgorithm """Dead reckoning algorithm""" @@ -5167,20 +4803,20 @@ class EntityStatePdu(EntityInformationFamilyPdu): pduType: enum8 = 1 # [UID 4] def __init__(self, - entityID: "EntityID | None" = None, + entityID: EntityIdentifier | None = None, forceId: enum8 = 0, # [UID 6] - entityType: "EntityType | None" = None, - alternativeEntityType: "EntityType | None" = None, - entityLinearVelocity: "Vector3Float | None" = None, - entityLocation: "Vector3Double | None" = None, - entityOrientation: "EulerAngles | None" = None, + entityType: EntityType | None = None, + alternativeEntityType: EntityType | None = None, + entityLinearVelocity: Vector3Float | None = None, + entityLocation: WorldCoordinates | None = None, + entityOrientation: EulerAngles | None = None, entityAppearance: uint32 = 0, # [UID 31-43] - deadReckoningParameters: "DeadReckoningParameters | None" = None, - marking: "EntityMarking | None" = None, + deadReckoningParameters: DeadReckoningParameters | None = None, + marking: EntityMarking | None = None, capabilities: uint32 = 0, # [UID 55] - variableParameters: list["VariableParameter"] | None = None): + variableParameters: list[VariableParameter] | None = None): super(EntityStatePdu, self).__init__() - self.entityID = entityID or EntityID() + self.entityID = entityID or EntityIdentifier() """Unique ID for an entity that is tied to this state information""" self.forceId = forceId """What force this entity is affiliated with, eg red, blue, neutral, etc""" @@ -5189,7 +4825,7 @@ def __init__(self, self.alternativeEntityType = alternativeEntityType or EntityType() self.entityLinearVelocity = entityLinearVelocity or Vector3Float() """Describes the speed of the entity in the world""" - self.entityLocation = entityLocation or Vector3Double() + self.entityLocation = entityLocation or WorldCoordinates() """describes the location of the entity in the world""" self.entityOrientation = entityOrientation or EulerAngles() """describes the orientation of the entity, in euler angles""" @@ -5314,32 +4950,32 @@ class TransmitterPdu(RadioCommunicationsFamilyPdu): pduType: enum8 = 25 # [UID 4] def __init__(self, - radioReferenceID: "EntityID | ObjectIdentifier | None" = None, + radioReferenceID: EntityIdentifier | ObjectIdentifier | None = None, radioNumber: uint16 = 0, - radioEntityType: "EntityType | None" = None, + radioEntityType: EntityType | None = None, transmitState: enum8 = 0, # [UID 164] inputSource: enum8 = 0, # [UID 165] - antennaLocation: "Vector3Double | None" = None, - relativeAntennaLocation: "Vector3Float | None" = None, + antennaLocation: WorldCoordinates | None = None, + relativeAntennaLocation: Vector3Float | None = None, antennaPatternType: enum16 = 0, # [UID 167] frequency: uint64 = 0, # in Hz transmitFrequencyBandwidth: float32 = 0.0, # in Hz power: float32 = 0.0, # in decibel-milliwatts - modulationType: "ModulationType | None" = None, + modulationType: ModulationType | None = None, cryptoSystem: enum16 = 0, # [UID 166] cryptoKeyId: struct16 = 0, # See Table 175 modulationParameters: ModulationParametersRecord | None = None, antennaPattern: AntennaPatternRecord | None = None, variableTransmitterParameters: Sequence[VariableTransmitterParametersRecord] | None = None): super(TransmitterPdu, self).__init__() - self.radioReferenceID = radioReferenceID or EntityID() + self.radioReferenceID = radioReferenceID or EntityIdentifier() """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 self.transmitState = transmitState self.inputSource = inputSource - self.antennaLocation = antennaLocation or Vector3Double() + self.antennaLocation = antennaLocation or WorldCoordinates() self.relativeAntennaLocation = relativeAntennaLocation or Vector3Float( ) self.antennaPatternType = antennaPatternType @@ -5414,7 +5050,6 @@ def serialize(self, outputStream: DataOutputStream) -> None: vtp.serialize(outputStream) 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_uint16() @@ -5473,16 +5108,11 @@ def parse(self, inputStream: DataInputStream) -> None: else: self.antennaPattern = None - ## TODO: Variable Transmitter Parameters for _ in range(0, variableTransmitterParameterCount): - recordType = inputStream.read_uint32() - if recordType == 3000: # High Fidelity HAVE QUICK/SATURN Radio - vtp = HighFidelityHAVEQUICKRadio() - vtp.parse(inputStream) - else: # Unknown VTP record type - vtp = UnknownVariableTransmitterParameters() - vtp.recordType = recordType - vtp.parse(inputStream) + vtp = parseStandardVariableRecord( + inputStream, + VariableTransmitterParametersRecord + ) self.variableTransmitterParameters.append(vtp) @@ -5495,12 +5125,12 @@ class ElectromagneticEmissionsPdu(DistributedEmissionsFamilyPdu): pduType: enum8 = 23 # [UID 4] def __init__(self, - emittingEntityID: "EntityID | None" = None, + emittingEntityID: EntityIdentifier | None = None, eventID: "EventIdentifier | None" = None, stateUpdateIndicator: enum8 = 0, # [UID 77] systems: list["EmissionSystemRecord"] | None = None): super(ElectromagneticEmissionsPdu, self).__init__() - self.emittingEntityID = emittingEntityID or EntityID() + self.emittingEntityID = emittingEntityID or EntityIdentifier() """ID of the entity emitting""" self.eventID = eventID or EventIdentifier() self.stateUpdateIndicator = stateUpdateIndicator @@ -5610,8 +5240,8 @@ class EmissionSystemRecord: def __init__(self, systemDataLength: uint8 = 0, # length in 32-bit words, 0 if exceed 255 emitterSystem: "EmitterSystem | None" = None, - location: "Vector3Float | None" = None, - beamRecords: list["EmissionSystemBeamRecord"] | None = None): + location: Vector3Float | None = None, + beamRecords: list[EmissionSystemBeamRecord] | None = None): self.systemDataLength = systemDataLength """this field shall specify the length of this emitter system's data in 32-bit words.""" self.paddingForEmissionsPdu: uint8 = 0 @@ -5655,13 +5285,13 @@ class ResupplyOfferPdu(LogisticsFamilyPdu): pduType: enum8 = 6 # [UID 4] def __init__(self, - receivingEntityID: "EntityID | None" = None, - supplyingEntityID: "EntityID | None" = None, + receivingEntityID: "EntityIdentifier | None" = None, + supplyingEntityID: "EntityIdentifier | None" = None, supplies: list["SupplyQuantity"] | None = None): super(ResupplyOfferPdu, self).__init__() - self.receivingEntityID = receivingEntityID or EntityID() + self.receivingEntityID = receivingEntityID or EntityIdentifier() """Field identifies the Entity and respective Entity Record ID that is receiving service (see 6.2.28), Section 7.4.3""" - self.supplyingEntityID = supplyingEntityID or EntityID() + self.supplyingEntityID = supplyingEntityID or EntityIdentifier() """Identifies the Entity and respective Entity ID Record that is supplying (see 6.2.28), Section 7.4.3""" self.padding1: uint8 = 0 self.padding2: uint16 = 0 @@ -5881,22 +5511,22 @@ class PointObjectStatePdu(SyntheticEnvironmentFamilyPdu): pduType: enum8 = 43 # [UID 4] def __init__(self, - objectID: "EntityID | None" = None, - referencedObjectID: "EntityID | None" = None, + objectID: EntityIdentifier | None = None, + referencedObjectID: EntityIdentifier | None = None, updateNumber: uint16 = 0, forceID: enum8 = 0, # [UID 6] modifications : enum8 = 0, # [UID 240] - objectType: "ObjectType | None" = None, - objectLocation: "Vector3Double | None" = None, - objectOrientation: "EulerAngles | None" = None, + objectType: ObjectType | None = None, + objectLocation: WorldCoordinates | None = None, + objectOrientation: EulerAngles | None = None, objectAppearance: struct32 | struct16 = 0, # [UID 229] - requesterID: "SimulationAddress | None" = None, - receivingID: "SimulationAddress | None" = None): + requesterID: SimulationAddress | None = None, + receivingID: SimulationAddress | None = None): super(PointObjectStatePdu, self).__init__() # TODO: Validate ObjectID? - self.objectID = objectID or EntityID() + self.objectID = objectID or EntityIdentifier() """Object in synthetic environment""" - self.referencedObjectID = referencedObjectID or EntityID() + self.referencedObjectID = referencedObjectID or EntityIdentifier() """Object with which this point object is associated""" self.updateNumber = updateNumber """unique update number of each state transition of an object""" @@ -5906,7 +5536,7 @@ def __init__(self, """modifications""" self.objectType = objectType or ObjectType() """Object type""" - self.objectLocation = objectLocation or Vector3Double() + self.objectLocation = objectLocation or WorldCoordinates() """Object location""" self.objectOrientation = objectOrientation or EulerAngles() """Object orientation""" @@ -6326,22 +5956,22 @@ class ArealObjectStatePdu(SyntheticEnvironmentFamilyPdu): pduType: enum8 = 45 # [UID 4] def __init__(self, - objectID: "EntityID | None" = None, - referencedObjectID: "EntityID | None" = None, + objectID: EntityIdentifier | None = None, + referencedObjectID: EntityIdentifier | None = None, updateNumber: uint16 = 0, forceId: enum8 = 0, # [UID 6] modifications: enum8 = 0, # [UID 242] - objectType: "ObjectType | None" = None, + objectType: ObjectType | None = None, specificObjectAppearance: struct32 = 0, generalObjectAppearance: struct16 = 0, # [UID 229] - requesterID: "SimulationAddress | None" = None, - receivingID: "SimulationAddress | None" = None, - objectLocation: list["Vector3Double"] | None = None): + requesterID: SimulationAddress | None = None, + receivingID: SimulationAddress | None = None, + objectLocation: list[WorldCoordinates] | None = None): super(ArealObjectStatePdu, self).__init__() # TODO: validate object ID? - self.objectID = objectID or EntityID() + self.objectID = objectID or EntityIdentifier() """Object in synthetic environment""" - self.referencedObjectID = referencedObjectID or EntityID() + self.referencedObjectID = referencedObjectID or EntityIdentifier() """Object with which this point object is associated""" self.updateNumber = updateNumber """unique update number of each state transition of an object""" @@ -6390,7 +6020,7 @@ def parse(self, inputStream): self.requesterID.parse(inputStream) self.receivingID.parse(inputStream) for idx in range(0, numberOfPoints): - element = Vector3Double() + element = WorldCoordinates() element.parse(inputStream) self.objectLocation.append(element) @@ -6469,19 +6099,19 @@ def __init__(self, minefieldID: "MinefieldIdentifier | None" = None, minefieldSequence: uint16 = 0, forceID: enum8 = 0, # [UID 6] - minefieldType: "EntityType | None" = None, - minefieldLocation: "Vector3Double | None" = None, - minefieldOrientation: "EulerAngles | None" = None, + minefieldType: EntityType | None = None, + minefieldLocation: WorldCoordinates | None = None, + minefieldOrientation: EulerAngles | None = None, appearance: struct16 = 0, # [UID 190] protocolMode: struct16 = 0, # See 6.2.69 - perimeterPoints: list["Vector2Float"] | None = None, - mineTypes: list["EntityType"] | None = None): + perimeterPoints: list[Vector2Float] | None = None, + mineTypes: list[EntityType] | None = None): super(MinefieldStatePdu, self).__init__() self.minefieldID = minefieldID or MinefieldIdentifier() self.minefieldSequence = minefieldSequence self.forceID = forceID self.minefieldType = minefieldType or EntityType() - self.minefieldLocation = minefieldLocation or Vector3Double() + self.minefieldLocation = minefieldLocation or WorldCoordinates() """location of center of minefield in world coords""" self.minefieldOrientation = minefieldOrientation or EulerAngles() self.appearance = appearance @@ -6697,93 +6327,80 @@ def __init__(self, munitionType: "EntityType | None" = None, shotStartTime: "ClockTime | None" = None, cumulativeShotTime: float32 = 0.0, # in seconds - apertureEmitterLocation: "Vector3Float | None" = None, + apertureEmitterLocation: Vector3Float | None = None, # in meters apertureDiameter: float32 = 0.0, # in meters wavelength: float32 = 0.0, # in meters - peakIrradiance=0.0, pulseRepetitionFrequency: float32 = 0.0, # in Hz pulseWidth: float32 = 0, # in seconds - flags: struct16 = 0, # [UID 313] + flags: DEFireFlags | None = None, # [UID 313] pulseShape: enum8 = 0, # [UID 312] - dERecords: list | None = None): + dERecords: list[DamageDescriptionRecord] | None = None): super(DirectedEnergyFirePdu, self).__init__() # TODO: validate entity type? self.munitionType = munitionType or EntityType() - """Field shall identify the munition type enumeration for the DE weapon beam, Section 7.3.4""" self.shotStartTime = shotStartTime or ClockTime() - """Field shall indicate the simulation time at start of the shot, Section 7.3.4""" self.cumulativeShotTime = cumulativeShotTime - """Field shall indicate the current cumulative duration of the shot, Section 7.3.4""" self.apertureEmitterLocation = (apertureEmitterLocation or Vector3Float()) - """Field shall identify the location of the DE weapon aperture/emitter, Section 7.3.4""" self.apertureDiameter = apertureDiameter - """Field shall identify the beam diameter at the aperture/emitter, Section 7.3.4""" self.wavelength = wavelength - """Field shall identify the emissions wavelength in units of meters, Section 7.3.4""" - self.peakIrradiance = peakIrradiance - """Field shall identify the current peak irradiance of emissions in units of Watts per square meter, Section 7.3.4""" + self.padding1: uint32 = 0 self.pulseRepetitionFrequency = pulseRepetitionFrequency - """field shall identify the current pulse repetition frequency in units of cycles per second (Hertz), Section 7.3.4""" self.pulseWidth = pulseWidth - """field shall identify the pulse width emissions in units of seconds, Section 7.3.4""" - self.flags = flags - """16bit Boolean field shall contain various flags to indicate status information needed to process a DE, Section 7.3.4""" + self.flags = flags or DEFireFlags() self.pulseShape = pulseShape - """Field shall identify the pulse shape and shall be represented as an 8-bit enumeration, Section 7.3.4""" - self.padding1: uint8 = 0 - self.padding2: uint32 = 0 - self.padding3: uint16 = 0 - self.dERecords = dERecords or [] - """Fields shall contain one or more DE records, records shall conform to the variable record format (Section6.2.82), Section 7.3.4""" + self.padding2: uint8 = 0 + self.padding3: uint32 = 0 + self.padding4: uint16 = 0 + self.dERecords: list[DamageDescriptionRecord] = dERecords or [] @property def numberOfDERecords(self) -> uint16: return len(self.dERecords) - def serialize(self, outputStream): - """serialize the class""" + def serialize(self, outputStream: DataOutputStream) -> None: super(DirectedEnergyFirePdu, self).serialize(outputStream) self.munitionType.serialize(outputStream) self.shotStartTime.serialize(outputStream) - outputStream.write_float(self.commulativeShotTime) + outputStream.write_float32(self.commulativeShotTime) self.apertureEmitterLocation.serialize(outputStream) - outputStream.write_float(self.apertureDiameter) - outputStream.write_float(self.wavelength) - outputStream.write_float(self.peakIrradiance) - outputStream.write_float(self.pulseRepetitionFrequency) - outputStream.write_int(self.pulseWidth) - outputStream.write_int(self.flags) - outputStream.write_byte(self.pulseShape) - outputStream.write_unsigned_byte(self.padding1) - outputStream.write_unsigned_int(self.padding2) - outputStream.write_unsigned_short(self.padding3) - outputStream.write_unsigned_short(self.numberOfDERecords) - for anObj in self.dERecords: - anObj.serialize(outputStream) + outputStream.write_float32(self.apertureDiameter) + outputStream.write_float32(self.wavelength) + outputStream.write_uint32(self.padding1) + outputStream.write_float32(self.pulseRepetitionFrequency) + outputStream.write_float32(self.pulseWidth) + self.flags.serialize(outputStream) + outputStream.write_uint8(self.pulseShape) + outputStream.write_uint8(self.padding2) + outputStream.write_uint32(self.padding3) + outputStream.write_uint16(self.padding4) + outputStream.write_uint16(self.numberOfDERecords) + for record in self.dERecords: + record.serialize(outputStream) - def parse(self, inputStream): - """Parse a message. This may recursively call embedded objects.""" + def parse(self, inputStream: DataInputStream) -> None: super(DirectedEnergyFirePdu, self).parse(inputStream) self.munitionType.parse(inputStream) self.shotStartTime.parse(inputStream) - self.commulativeShotTime = inputStream.read_float() + self.commulativeShotTime = inputStream.read_float32() self.apertureEmitterLocation.parse(inputStream) - self.apertureDiameter = inputStream.read_float() - self.wavelength = inputStream.read_float() - self.peakIrradiance = inputStream.read_float() - self.pulseRepetitionFrequency = inputStream.read_float() - self.pulseWidth = inputStream.read_int() - self.flags = inputStream.read_int() - self.pulseShape = inputStream.read_byte() - self.padding1 = inputStream.read_unsigned_byte() - self.padding2 = inputStream.read_unsigned_int() - self.padding3 = inputStream.read_unsigned_short() - numberOfDERecords = inputStream.read_unsigned_short() - for idx in range(0, numberOfDERecords): - element = null() - element.parse(inputStream) - self.dERecords.append(element) + self.apertureDiameter = inputStream.read_float32() + self.wavelength = inputStream.read_float32() + self.padding1 = inputStream.read_uint32() + self.pulseRepetitionFrequency = inputStream.read_float32() + self.pulseWidth = inputStream.read_float32() + self.flags.parse(inputStream) + self.pulseShape = inputStream.read_uint8() + self.padding2 = inputStream.read_uint8() + self.padding3 = inputStream.read_uint32() + self.padding4 = inputStream.read_uint16() + numberOfDERecords = inputStream.read_uint16() + for _ in range(0, numberOfDERecords): + vtp = parseStandardVariableRecord( + inputStream, + DamageDescriptionRecord + ) + self.dERecords.append(vtp) class DetonationPdu(WarfareFamilyPdu): @@ -6796,21 +6413,21 @@ class DetonationPdu(WarfareFamilyPdu): pduType: enum8 = 3 # [UID 4] def __init__(self, - explodingEntityID: "EntityID | None" = None, - eventID: "EventIdentifier | None" = None, - velocity: "Vector3Float | None" = None, - location: "Vector3Double | None" = None, - descriptor: "MunitionDescriptor | None" = None, - locationInEntityCoordinates: "Vector3Float | None" = None, + explodingEntityID: EntityIdentifier | None = None, + eventID: EventIdentifier | None = None, + velocity: Vector3Float | None = None, + location: WorldCoordinates | None = None, + descriptor: MunitionDescriptor | None = None, + locationInEntityCoordinates: Vector3Float | None = None, detonationResult: enum8 = 0, # [UID 62] - variableParameters: list["VariableParameter"] | None = None): + variableParameters: list[VariableParameter] | None = None): super(DetonationPdu, self).__init__() - self.explodingEntityID = explodingEntityID or EntityID() + self.explodingEntityID = explodingEntityID or EntityIdentifier() """ID of the expendable entity, Section 7.3.3""" self.eventID = eventID or EventIdentifier() self.velocity = velocity or Vector3Float() """velocity of the munition immediately before detonation/impact, Section 7.3.3""" - self.location = location or Vector3Double( + self.location = location or WorldCoordinates( ) """location of the munition detonation, the expendable detonation, Section 7.3.3""" self.descriptor = descriptor or MunitionDescriptor() @@ -7019,47 +6636,45 @@ class EntityDamageStatusPdu(WarfareFamilyPdu): """Section 7.3.5 Shall be used to communicate detailed damage information sustained by an - entity regardless of the source of the damage. COMPLETE + entity regardless of the source of the damage. """ pduType: enum8 = 69 # [UID 4] def __init__(self, - damagedEntityID: "EntityID | None" = None, - damageDescriptionRecords=None): + damagedEntityID: EntityIdentifier | None = None, + damageDescriptions: list[DamageDescriptionRecord] | None = None): super(EntityDamageStatusPdu, self).__init__() - self.damagedEntityID = damagedEntityID or EntityID() - """Field shall identify the damaged entity (see 6.2.28), Section 7.3.4 COMPLETE""" + self.damagedEntityID = damagedEntityID or EntityIdentifier() self.padding1: uint16 = 0 self.padding2: uint16 = 0 # TODO: Look into using StandardVariableSpecification to compose this - self.damageDescriptionRecords = damageDescriptionRecords or [] - """Fields shall contain one or more Damage Description records (see 6.2.17) and may contain other Standard Variable records, Section 7.3.5""" + self.damageDescriptions: list[DamageDescriptionRecord] = damageDescriptions or [] @property - def numberOfDamageDescriptions(self) -> uint16: - return len(self.damageDescriptionRecords) + def damageDescriptionCount(self) -> uint16: + return len(self.damageDescriptions) - def serialize(self, outputStream): - """serialize the class""" + def serialize(self, outputStream: DataOutputStream) -> None: super(EntityDamageStatusPdu, self).serialize(outputStream) self.damagedEntityID.serialize(outputStream) - outputStream.write_unsigned_short(self.padding1) - outputStream.write_unsigned_short(self.padding2) - outputStream.write_unsigned_short(self.numberOfDamageDescriptions) - for anObj in self.damageDescriptionRecords: - anObj.serialize(outputStream) + outputStream.write_uint16(self.padding1) + outputStream.write_uint16(self.padding2) + outputStream.write_uint16(self.damageDescriptionCount) + for record in self.damageDescriptions: + record.serialize(outputStream) - def parse(self, inputStream): - """Parse a message. This may recursively call embedded objects.""" + def parse(self, inputStream: DataInputStream) -> None: super(EntityDamageStatusPdu, self).parse(inputStream) self.damagedEntityID.parse(inputStream) - self.padding1 = inputStream.read_unsigned_short() - self.padding2 = inputStream.read_unsigned_short() - numberOfDamageDescriptions = inputStream.read_unsigned_short() - for idx in range(0, numberOfDamageDescriptions): - element = null() - element.parse(inputStream) - self.damageDescriptionRecords.append(element) + self.padding1 = inputStream.read_uint16() + self.padding2 = inputStream.read_uint16() + damageDescriptionCount = inputStream.read_uint16() + for _ in range(0, damageDescriptionCount): + record = parseStandardVariableRecord( + inputStream, + DamageDescriptionRecord + ) + self.damageDescriptions.append(record) class FirePdu(WarfareFamilyPdu): @@ -7071,21 +6686,21 @@ class FirePdu(WarfareFamilyPdu): pduType: enum8 = 2 # [UID 4] def __init__(self, - munitionExpendableID: "EntityID | None" = None, - eventID: "EventIdentifier | None" = None, + munitionExpendableID: EntityIdentifier | None = None, + eventID: EventIdentifier | None = None, fireMissionIndex: uint32 = 0, - location: "Vector3Double | None" = None, - descriptor: "MunitionDescriptor | None" = None, - velocity: "Vector3Float | None" = None, + location: WorldCoordinates | None = None, + descriptor: MunitionDescriptor | None = None, + velocity: Vector3Float | None = None, range_: float32 = 0.0): # in meters super(FirePdu, self).__init__() - self.munitionExpendableID = munitionExpendableID or EntityID() + self.munitionExpendableID = munitionExpendableID or EntityIdentifier() """This field shall specify the entity identification of the fired munition or expendable. This field shall be represented by an Entity Identifier record (see 6.2.28).""" self.eventID = eventID or EventIdentifier() """This field shall contain an identification generated by the firing entity to associate related firing and detonation events. This field shall be represented by an Event Identifier record (see 6.2.34).""" self.fireMissionIndex = fireMissionIndex """This field shall identify the fire mission (see 5.4.3.3). This field shall be representedby a 32-bit unsigned integer.""" - self.location = location or Vector3Double( + self.location = location or WorldCoordinates( ) """This field shall specify the location, in world coordinates, from which the munition was launched, and shall be represented by a World Coordinates record (see 6.2.97).""" self.descriptor = descriptor or MunitionDescriptor() @@ -7128,7 +6743,7 @@ class ReceiverPdu(RadioCommunicationsFamilyPdu): def __init__(self, receiverState: enum16 = 0, # [UID 179] receivedPower: float32 = 0.0, # in decibel milliwatts - transmitterEntityID: "EntityID | ObjectIdentifier | UnattachedIdentifier | None" = None, + transmitterEntityID: EntityIdentifier | ObjectIdentifier | UnattachedIdentifier | None = None, transmitterRadioId: uint16 = 0): super(ReceiverPdu, self).__init__() self.receiverState = receiverState @@ -7136,7 +6751,7 @@ def __init__(self, self.padding1: uint16 = 0 self.receivedPower = receivedPower """received power""" - self.transmitterEntityID = transmitterEntityID or EntityID() + self.transmitterEntityID = transmitterEntityID or EntityIdentifier() """ID of transmitter""" self.transmitterRadioId = transmitterRadioId """ID of transmitting radio""" @@ -7170,8 +6785,8 @@ class UaPdu(DistributedEmissionsFamilyPdu): pduType: enum8 = 29 # [UID 4] def __init__(self, - emittingEntityID: "EntityID | None" = None, - eventID: "EventIdentifier | None" = None, + emittingEntityID: EntityIdentifier | None = None, + eventID: EventIdentifier | None = None, stateChangeIndicator: enum8 = 0, # [UID 143] passiveParameterIndex: enum16 = 0, # [UID 148] propulsionPlantConfiguration: struct8 = 0, # [UID 149] @@ -7179,7 +6794,7 @@ def __init__(self, apaData: list | None = None, emitterSystems: list | None = None): super(UaPdu, self).__init__() - self.emittingEntityID = emittingEntityID or EntityID() + self.emittingEntityID = emittingEntityID or EntityIdentifier() """ID of the entity that is the source of the emission""" self.eventID = eventID or EventIdentifier() """ID of event""" @@ -7268,19 +6883,19 @@ class IntercomControlPdu(RadioCommunicationsFamilyPdu): def __init__(self, controlType: enum8 = 0, # [UID 180] communicationsChannelType: struct8 = 0, # [UID 416], [UID 181] - sourceEntityID: "EntityID | UnattachedIdentifier | None" = None, + sourceEntityID: "EntityIdentifier | UnattachedIdentifier | None" = None, sourceCommunicationsDeviceID: uint16 = 0, sourceLineID: uint8 = 0, transmitPriority: uint8 = 0, transmitLineState: enum8 = 0, # [UID 183] command: enum8 = 0, # [UID 182] - masterEntityID: "EntityID | UnattachedIdentifier | None" = None, + masterEntityID: "EntityIdentifier | UnattachedIdentifier | None" = None, masterCommunicationsDeviceID: uint16 = 0, intercomParameters: "IntercomCommunicationsParameters | None" = None): super(IntercomControlPdu, self).__init__() self.controlType = controlType self.communicationsChannelType = communicationsChannelType - self.sourceEntityID = sourceEntityID or EntityID() + self.sourceEntityID = sourceEntityID or EntityIdentifier() self.sourceCommunicationsDeviceID = sourceCommunicationsDeviceID """The specific intercom device being simulated within an entity.""" self.sourceLineID = sourceLineID @@ -7291,7 +6906,7 @@ def __init__(self, """current transmit state of the line""" self.command = command """detailed type requested.""" - self.masterEntityID = masterEntityID or EntityID() + self.masterEntityID = masterEntityID or EntityIdentifier() """eid of the entity that has created this intercom channel.""" self.masterCommunicationsDeviceID = masterCommunicationsDeviceID """specific intercom device that has created this intercom channel""" @@ -7349,7 +6964,7 @@ class SignalPdu(RadioCommunicationsFamilyPdu): pduType: enum8 = 26 # [UID 4] def __init__(self, - entityID: "EntityID | ObjectIdentifier | UnattachedIdentifier | None" = None, + entityID: EntityIdentifier | ObjectIdentifier | UnattachedIdentifier | None = None, radioID: uint16 = 0, encodingScheme: struct16 = 0, # (Table 177), [UID 271], [UID 270] tdlType: enum16 = 0, # [UID 178] @@ -7357,7 +6972,7 @@ def __init__(self, samples: uint16 = 0, data: list[bytes] | None = None): super(SignalPdu, self).__init__() - self.entityID = entityID or EntityID() + self.entityID = entityID or EntityIdentifier() self.radioID = radioID self.encodingScheme = encodingScheme self.tdlType = tdlType @@ -7442,14 +7057,14 @@ class SeesPdu(DistributedEmissionsFamilyPdu): pduType: enum8 = 30 # [UID 4] def __init__(self, - originatingEntityID: "EntityID | None" = None, + originatingEntityID: EntityIdentifier | None = None, infraredSignatureRepresentationIndex: uint16 = 0, acousticSignatureRepresentationIndex: uint16 = 0, radarCrossSectionSignatureRepresentationIndex: uint16 = 0, propulsionSystemData: list | None = None, vectoringSystemData: list | None = None): super(SeesPdu, self).__init__() - self.orginatingEntityID = originatingEntityID or EntityID() + self.orginatingEntityID = originatingEntityID or EntityIdentifier() self.infraredSignatureRepresentationIndex = infraredSignatureRepresentationIndex self.acousticSignatureRepresentationIndex = acousticSignatureRepresentationIndex self.radarCrossSectionSignatureRepresentationIndex = radarCrossSectionSignatureRepresentationIndex @@ -7643,14 +7258,14 @@ class MinefieldResponseNackPdu(MinefieldFamilyPdu): pduType: enum8 = 40 # [UID 4] def __init__(self, - minefieldID: "EntityID | None" = None, - requestingEntityID: "EntityID | None" = None, + minefieldID: EntityIdentifier | None = None, + requestingEntityID: EntityIdentifier | None = None, requestID: uint32 = 0, missingPduSequenceNumbers: list[uint8] | None = None): super(MinefieldResponseNackPdu, self).__init__() # TODO: validate EntityID? - self.minefieldID = minefieldID or EntityID() - self.requestingEntityID = requestingEntityID or EntityID() + self.minefieldID = minefieldID or EntityIdentifier() + self.requestingEntityID = requestingEntityID or EntityIdentifier() self.requestID = requestID self.missingPduSequenceNumbers = missingPduSequenceNumbers or [] """PDU sequence numbers that were missing""" @@ -7743,16 +7358,16 @@ class IsPartOfPdu(EntityManagementFamilyPdu): pduType: enum8 = 36 # [UID 4] def __init__(self, - originatingEntityID: "EntityID | None" = None, - receivingEntityID: "EntityID | None" = None, + originatingEntityID: EntityIdentifier | None = None, + receivingEntityID: EntityIdentifier | None = None, relationship: "Relationship | None" = None, - partLocation: "Vector3Float | None" = None, + partLocation: Vector3Float | None = None, namedLocationID: "NamedLocationIdentification | None" = None, partEntityType: "EntityType | None" = None): super(IsPartOfPdu, self).__init__() - self.orginatingEntityID = originatingEntityID or EntityID() + self.orginatingEntityID = originatingEntityID or EntityIdentifier() """ID of entity originating PDU""" - self.receivingEntityID = receivingEntityID or EntityID() + self.receivingEntityID = receivingEntityID or EntityIdentifier() """ID of entity receiving PDU""" self.relationship = relationship or Relationship() """relationship of joined parts""" @@ -7797,15 +7412,15 @@ def __init__(self, aggregateID: "AggregateIdentifier | None" = None, aggregateType: "AggregateType | None" = None, formation: enum32 = 0, aggregateMarking: "AggregateMarking | None" = None, - dimensions: "Vector3Float | None" = None, - orientation: "EulerAngles | None" = None, - centerOfMass: "Vector3Float | None" = None, - velocity: "Vector3Double | None" = None, - aggregateIDs: list["AggregateIdentifier"] | None = None, - entityIDs: list["EntityIdentifier"] | None = None, - silentAggregateSystems: list["SilentAggregateSystem"] | None = None, - silentEntitySystems: list["SilentEntitySystem"] | None = None, - variableDatumRecords: list["VariableDatum"] | None = None): + dimensions: Vector3Float | None = None, + orientation: EulerAngles | None = None, + centerOfMass: Vector3Float | None = None, + velocity: WorldCoordinates | None = None, + aggregateIDs: list[AggregateIdentifier] | None = None, + entityIDs: list[EntityIdentifier] | None = None, + silentAggregateSystems: list[SilentAggregateSystem] | None = None, + silentEntitySystems: list[SilentEntitySystem] | None = None, + variableDatumRecords: list[VariableDatum] | None = None): super(AggregateStatePdu, self).__init__() """Identifier of the aggregate issuing the PDU""" self.aggregateID = aggregateID or AggregateIdentifier() @@ -7822,7 +7437,7 @@ def __init__(self, aggregateID: "AggregateIdentifier | None" = None, self.orientation = orientation or EulerAngles() self.centerOfMass = centerOfMass or Vector3Float() """Aggregates linear velocity. The coordinate system is dependent on the dead reckoning algorithm""" - self.velocity = velocity or Vector3Double() + self.velocity = velocity or WorldCoordinates() """Identify subaggregates that are transmitting Aggregate State PDUs""" self.aggregateIDs = aggregateIDs or [] """Constituent entities transmitting Entity State PDUs""" @@ -7904,7 +7519,7 @@ def parse(self, inputStream): element.parse(inputStream) self.aggregateIDs.append(element) for idx in range(0, numberOfEntityIDs): - element = EntityID() + element = EntityIdentifier() element.parse(inputStream) self.entityIDs.append(element) for idx in range(0, numberOfSilentAggregateSystems): diff --git a/opendis/record/__init__.py b/opendis/record/__init__.py index 04a4039..50f57b7 100644 --- a/opendis/record/__init__.py +++ b/opendis/record/__init__.py @@ -3,11 +3,10 @@ This module defines classes for various record types used in DIS PDUs. """ -from abc import ABC, abstractmethod +from typing import TypeVar -from . import bitfield -from ..stream import DataInputStream, DataOutputStream -from ..types import ( +from opendis.stream import DataInputStream, DataOutputStream +from opendis.types import ( enum8, enum16, enum32, @@ -15,552 +14,72 @@ bf_int, bf_uint, float32, + float64, + struct8, uint8, uint16, uint32, ) - -class EulerAngles: - """Section 6.2.32 Euler Angles record - - Three floating point values representing an orientation, psi, theta, - and phi, aka the euler angles, in radians. - These angles shall be specified with respect to the entity's coordinate - system. - """ - - def __init__(self, - psi: float32 = 0.0, - theta: float32 = 0.0, - phi: float32 = 0.0): # in radians - self.psi = psi - self.theta = theta - self.phi = phi - - def marshalledSize(self) -> int: - return 12 - - def serialize(self, outputStream): - """serialize the class""" - outputStream.write_float32(self.psi) - outputStream.write_float32(self.theta) - outputStream.write_float32(self.phi) - - def parse(self, inputStream): - """Parse a message. This may recursively call embedded objects.""" - self.psi = inputStream.read_float32() - self.theta = inputStream.read_float32() - self.phi = inputStream.read_float32() - - -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 - - Represents an Operational Net in the format of NXX.XYY, where: - N = Mode - XXX = Net Number - YY = Frequency Table - """ - - _struct = bitfield.bitfield(name="NetId", fields=[ - ("netNumber", bitfield.INTEGER, 10), - ("frequencyTable", bitfield.INTEGER, 2), - ("mode", bitfield.INTEGER, 2), - ("padding", bitfield.INTEGER, 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.bitfield(name="SpreadSpectrum", fields=[ - ("frequencyHopping", bitfield.INTEGER, 1), - ("pseudoNoise", bitfield.INTEGER, 1), - ("timeHopping", bitfield.INTEGER, 1), - ("padding", bitfield.INTEGER, 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) - - -class ModulationParametersRecord(ABC): - """6.2.58 Modulation Parameters record - - Base class for modulation parameters records, as defined in Annex C. - 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 UnknownRadio(ModulationParametersRecord): - """Placeholder for unknown or unimplemented radio 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: - self.data = inputStream.read_bytes(bytelength) - - -class GenericRadio(ModulationParametersRecord): - """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(ModulationParametersRecord): - """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(ModulationParametersRecord): - """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() - - -class CCTTSincgarsMP(ModulationParametersRecord): - """Annex C 6.2.3, Table C.7 — CCTT SINCGARS MP record""" - - def __init__(self, - fh_net_id: uint16 = 0, - hop_set_id: uint16 = 0, - lockout_set_id: uint16 = 0, - start_of_message: enum8 = 0, - clear_channel: enum8 = 0, - fh_sync_time_offset: uint32 = 0, - transmission_security_key: uint16 = 0): - self.fh_net_id = fh_net_id - self.hop_set_id = hop_set_id - self.lockout_set_id = lockout_set_id - self.start_of_message = start_of_message - self.clear_channel = clear_channel - self.fh_sync_time_offset = fh_sync_time_offset - self.transmission_security_key = transmission_security_key - self.padding: uint16 = 0 - - def marshalledSize(self) -> int: - return 16 # bytes - - def serialize(self, outputStream: DataOutputStream) -> None: - outputStream.write_uint16(self.fh_net_id) - outputStream.write_uint16(self.hop_set_id) - outputStream.write_uint16(self.lockout_set_id) - outputStream.write_uint8(self.start_of_message) - outputStream.write_uint8(self.clear_channel) - outputStream.write_uint32(self.fh_sync_time_offset) - outputStream.write_uint16(self.transmission_security_key) - outputStream.write_uint16(self.padding) - - def parse(self, inputStream: DataInputStream) -> None: - self.fh_net_id = inputStream.read_uint16() - self.hop_set_id = inputStream.read_uint16() - self.lockout_set_id = inputStream.read_uint16() - self.start_of_message = inputStream.read_uint8() - self.clear_channel = inputStream.read_uint8() - self.fh_sync_time_offset = inputStream.read_uint32() - self.transmission_security_key = inputStream.read_uint16() - self.padding = inputStream.read_uint16() - - -class AntennaPatternRecord(ABC): - """6.2.8 Antenna Pattern record - - 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) - - -class BeamAntennaPattern(AntennaPatternRecord): - """6.2.8.2 Beam Antenna Pattern record - - Used when the antenna pattern type field has a value of 1. Specifies the - direction, pattern, and polarization of radiation from an antenna. - """ - - def __init__(self, - beamDirection: "EulerAngles | None" = None, - azimuthBeamwidth: float32 = 0.0, # in radians - elevationBeamwidth: float32 = 0.0, # in radians - referenceSystem: enum8 = 0, # [UID 168] - ez: float32 = 0.0, - ex: float32 = 0.0, - phase: float32 = 0.0): # in radians - self.beamDirection = beamDirection or EulerAngles() - """The rotation that transforms the reference coordinate sytem into the beam coordinate system. Either world coordinates or entity coordinates may be used as the reference coordinate system, as specified by the reference system field of the antenna pattern record.""" - self.azimuthBeamwidth = azimuthBeamwidth - self.elevationBeamwidth = elevationBeamwidth - self.referenceSystem = referenceSystem - self.padding1: uint8 = 0 - self.padding2: uint16 = 0 - self.ez = ez - """This field shall specify the magnitude of the Z-component (in beam coordinates) of the Electrical field at some arbitrary single point in the main beam and in the far field of the antenna.""" - self.ex = ex - """This field shall specify the magnitude of the X-component (in beam coordinates) of the Electrical field at some arbitrary single point in the main beam and in the far field of the antenna.""" - self.phase = phase - """This field shall specify the phase angle between EZ and EX in radians. If fully omni-directional antenna is modeled using beam pattern type one, the omni-directional antenna shall be represented by beam direction Euler angles psi, theta, and phi of zero, an azimuth beamwidth of 2PI, and an elevation beamwidth of PI""" - self.padding3: uint32 = 0 - - def marshalledSize(self) -> int: - return 40 - - def serialize(self, outputStream: DataOutputStream) -> None: - self.beamDirection.serialize(outputStream) - outputStream.write_float32(self.azimuthBeamwidth) - outputStream.write_float32(self.elevationBeamwidth) - outputStream.write_uint8(self.referenceSystem) - outputStream.write_uint8(self.padding1) - outputStream.write_uint16(self.padding2) - outputStream.write_float32(self.ez) - outputStream.write_float32(self.ex) - outputStream.write_float32(self.phase) - outputStream.write_uint32(self.padding3) - - def parse(self, inputStream: DataInputStream) -> None: - self.beamDirection.parse(inputStream) - self.azimuthBeamwidth = inputStream.read_float32() - self.elevationBeamwidth = inputStream.read_float32() - self.referenceSystem = inputStream.read_uint8() - self.padding1 = inputStream.read_uint8() - self.padding2 = inputStream.read_uint16() - self.ez = inputStream.read_float32() - self.ex = inputStream.read_float32() - self.phase = inputStream.read_float32() - self.padding3 = inputStream.read_uint32() - - -class VariableTransmitterParametersRecord(ABC): - """6.2.95 Variable Transmitter Parameters record - - One or more VTP records may be associated with a radio system, and the same - VTP record may be associated with multiple radio systems. - Specific VTP records applicable to a radio system are identified in the - subclause that defines the radio system’s unique requirements in Annex C. - The total length of each record shall be a multiple of 64 bits. - """ - recordType: enum32 - - @abstractmethod - def marshalledSize(self) -> int: - """Return the size of the record when serialized.""" - - @abstractmethod - def serialize(self, outputStream: DataOutputStream) -> None: - """Serialize the record to the output stream.""" - - @abstractmethod - def parse(self, inputStream: DataInputStream) -> None: - """Parse the record from the input stream. - - The recordType field is assumed to have been read, so as to identify - the type of VTP record to be parsed, before this method is called. - """ - - -class UnknownVariableTransmitterParameters(VariableTransmitterParametersRecord): - """Placeholder for unknown or unimplemented variable transmitter parameter - types. - """ - recordType: enum32 = 0 - - def __init__(self, recordType: enum32 = 0, data: bytes = b""): - self.recordType = recordType # [UID 66] Variable Parameter Record Type - self.data = data - - def marshalledSize(self) -> int: - return 6 + len(self.data) - - @property - def recordLength(self) -> uint16: - return self.marshalledSize() - - def serialize(self, outputStream: DataOutputStream) -> None: - outputStream.write_uint32(self.recordType) - outputStream.write_uint16(self.recordLength) - outputStream.write_bytes(self.data) - - def parse(self, inputStream: DataInputStream) -> None: - self.recordType = inputStream.read_uint32() - recordLength = inputStream.read_uint16() - self.data = inputStream.read_bytes(recordLength) - - -class HighFidelityHAVEQUICKRadio(VariableTransmitterParametersRecord): - """Annex C C4.2.3, Table C.4 — High Fidelity HAVE QUICK Radio record""" - recordType: enum32 = 3000 - - def __init__(self, - netId: NetId | None = None, - todTransmitIndicator: enum8 = 0, - todDelta: uint32 = 0, - wod1: uint32 = 0, - wod2: uint32 = 0, - wod3: uint32 = 0, - wod4: uint32 = 0, - wod5: uint32 = 0, - wod6: uint32 = 0): - self.padding1: uint16 = 0 - self.netId = netId or NetId() - self.todTransmitIndicator = todTransmitIndicator - self.padding2: uint8 = 0 - self.todDelta = todDelta - self.wod1 = wod1 - self.wod2 = wod2 - self.wod3 = wod3 - self.wod4 = wod4 - self.wod5 = wod5 - self.wod6 = wod6 - - @property - def recordLength(self) -> uint16: - return self.marshalledSize() - - def marshalledSize(self) -> int: - return 40 - - def serialize(self, outputStream: DataOutputStream) -> None: - outputStream.write_uint16(self.padding1) - self.netId.serialize(outputStream) - outputStream.write_uint8(self.todTransmitIndicator) - outputStream.write_uint8(self.padding2) - outputStream.write_uint32(self.todDelta) - outputStream.write_uint32(self.wod1) - outputStream.write_uint32(self.wod2) - outputStream.write_uint32(self.wod3) - outputStream.write_uint32(self.wod4) - outputStream.write_uint32(self.wod5) - outputStream.write_uint32(self.wod6) - - def parse(self, inputStream: DataInputStream) -> None: - self.padding1 = inputStream.read_uint16() - self.netId.parse(inputStream) - self.todTransmitIndicator = inputStream.read_uint8() - self.padding2 = inputStream.read_uint8() - self.todDelta = inputStream.read_uint32() - self.wod1 = inputStream.read_uint32() - self.wod2 = inputStream.read_uint32() - self.wod3 = inputStream.read_uint32() - self.wod4 = inputStream.read_uint32() - self.wod5 = inputStream.read_uint32() - self.wod6 = inputStream.read_uint32() +from . import base, bitfield, symbolic_names as sym +from .base import StandardVariableRecord +from .common import * +from .radio import * +from .warfare import * + +SV = TypeVar('SV', bound=base.StandardVariableRecord) + + +__variableRecordClasses: dict[int, type[base.StandardVariableRecord]] = { + 3000: HighFidelityHAVEQUICKRadio, + 4000: DirectedEnergyPrecisionAimpoint, + 4001: DirectedEnergyAreaAimpoint, + 4500: DirectedEnergyDamage, +} + +def getStandardVariableClass( + recordType: int, + expectedType: type[SV] = base.StandardVariableRecord +) -> type[SV]: + """Return a StandardVariableRecord subclass for the given recordType.""" + + # Declare a local class since the recordType class variable will need to be + # set for each new unrecognised record type. + class UnknownStandardVariableRecord(base.StandardVariableRecord): + """A placeholder class for unrecognised Standard Variable Records.""" + recordType: enum32 + + def __init__(self, data: bytes = b'') -> None: + self.data = data + + def marshalledSize(self) -> uint16: + return 6 + len(self.data) + + def serialize(self, outputStream: DataOutputStream) -> None: + super().serialize(outputStream) + outputStream.write_uint32(self.recordType) + outputStream.write_uint16(self.recordLength) + outputStream.write_bytes(self.data) + + def parse(self, + inputStream: DataInputStream, + bytelength: int) -> None: + super().parse(inputStream, bytelength) + # Subtract 6 bytes for type and length + self.data = inputStream.read_bytes(bytelength - 6) + + if not isinstance(recordType, int) or recordType < 0: + raise ValueError( + f"recordType must be a non-negative integer, got {recordType!r}" + ) + UnknownStandardVariableRecord.recordType = recordType + vrClass = __variableRecordClasses.get( + recordType, + UnknownStandardVariableRecord + ) + if not issubclass(vrClass, expectedType): + raise TypeError( + f"Record Type {recordType}: Record class {vrClass.__name__} is not " + f"a subclass of {expectedType.__name__}" + ) + return vrClass diff --git a/opendis/record/base.py b/opendis/record/base.py new file mode 100644 index 0000000..735025f --- /dev/null +++ b/opendis/record/base.py @@ -0,0 +1,107 @@ +"""Base classes for all Record types.""" + +__all__ = ["Record", "StandardVariableRecord", "VariableRecord"] + +from abc import abstractmethod +from typing import Any, Protocol, TypeGuard, runtime_checkable + +from opendis.stream import DataInputStream, DataOutputStream +from opendis.types import enum32 + + +@runtime_checkable +class Record(Protocol): + """Base class for all Record types with fixed sizes.""" + + @abstractmethod + def marshalledSize(self) -> int: + """Return the size (in bytes) of the record when serialized.""" + + @abstractmethod + def serialize(self, outputStream: DataOutputStream) -> None: + """Serialize the record to the output stream.""" + + @abstractmethod + def parse(self, inputStream: DataInputStream) -> None: + """Parse the record from the input stream.""" + + +@runtime_checkable +class VariableRecord(Protocol): + """Base class for all Record types with variable sizes.""" + + @staticmethod + def is_positive_int(value: Any) -> TypeGuard[int]: + """Check if a value is a positive integer.""" + return isinstance(value, int) and value >= 0 + + @abstractmethod + def marshalledSize(self) -> int: + """Return the size (in bytes) of the record when serialized.""" + + @abstractmethod + def serialize(self, outputStream: DataOutputStream) -> None: + """Serialize the record to the output stream.""" + + @abstractmethod + def parse(self, + inputStream: DataInputStream, + bytelength: int | None = None) -> None: + """Parse the record from the input stream. + + If bytelength is provided, it indicates the expected length of the record. Some records may require this information to parse correctly. + """ + if bytelength is not None and not self.is_positive_int(bytelength): + raise ValueError( + f"bytelength must be a non-negative integer, got {bytelength!r}" + ) + # TODO: Implement padding handling + + +class StandardVariableRecord(VariableRecord): + """6.2.83 Standard Variable (SV) Record + + This base class defines the interface for DIS records with variable sizes. + First SV record of a Standard Variable Specification record shall start on + a 64-bit boundary. + Padding shall be explicitly included in each record as necessary to make + the record length a multiple of 8 octets (64 bits) so that the following + record is automatically aligned. The record length requirement may be + achieved by placing padding fields anywhere in the SV record as deemed appropriate, not necessarily at the end of the record. + """ + recordType: enum32 # [UID 66] + + @staticmethod + def is_positive_int(value: Any) -> TypeGuard[int]: + """Check if a value is a positive integer.""" + return isinstance(value, int) and value >= 0 + + @property + def recordLength(self) -> int: + return self.marshalledSize() + + @abstractmethod + def serialize(self, outputStream: DataOutputStream) -> None: + """Serialize the record to the output stream.""" + super().serialize(outputStream) + outputStream.write_uint32(self.recordType) + outputStream.write_uint32(self.recordLength) + + @abstractmethod + def parse(self, # pyright: ignore [reportIncompatibleMethodOverride] + inputStream: DataInputStream, + bytelength: int) -> None: + """Parse the record from the input stream. + + If bytelength is provided, it indicates the expected length of the record. Some records may require this information to parse correctly. + Assume that recordType and recordLength have already been read from the + stream. + recordLength will usually be passed to this method to assist in parsing. + """ + # Validate bytelength + super().parse(inputStream, bytelength) + if not bytelength % 8 == 0: + raise ValueError( + f"bytelength must be a multiple of 8, got {bytelength!r}" + ) + # TODO: Implement padding handling diff --git a/opendis/record/common.py b/opendis/record/common.py new file mode 100644 index 0000000..3f35f87 --- /dev/null +++ b/opendis/record/common.py @@ -0,0 +1,198 @@ +"""Common record types used across multiple protocol families""" + +from opendis.record import base, bitfield +from opendis.stream import DataInputStream, DataOutputStream +from opendis.types import ( + enum8, + enum16, + enum32, + bf_enum, + bf_int, + bf_uint, + float32, + float64, + struct8, + uint8, + uint16, + uint32, +) + +from . import symbolic_names as sym + +__all__ = [ + "Vector3Float", + "WorldCoordinates", + "EntityIdentifier", + "EulerAngles", + "SimulationAddress", + "EventIdentifier", +] + + +class EntityIdentifier(base.Record): + """Section 6.2.28 Entity Identifier record + + Unique designation of each entity in an event or exercise that is not + contained in a Live Entity PDU. + """ + + def __init__(self, + simulationAddress: "SimulationAddress | None" = None, + entityNumber: uint16 = 0): + self.simulationAddress = simulationAddress or SimulationAddress() + self.entityNumber = entityNumber + + def marshalledSize(self) -> int: + return self.simulationAddress.marshalledSize() + 2 + + def serialize(self, outputStream: DataOutputStream) -> None: + self.simulationAddress.serialize(outputStream) + outputStream.write_uint16(self.entityNumber) + + def parse(self, inputStream: DataInputStream) -> None: + self.simulationAddress.parse(inputStream) + self.entityNumber = inputStream.read_uint16() + + +class EulerAngles(base.Record): + """6.2.32 Euler Angles record + + Three floating point values representing an orientation, psi, theta, + and phi, aka the euler angles, in radians. + These angles shall be specified with respect to the entity's coordinate + system. + """ + + def __init__(self, + psi: float32 = 0.0, + theta: float32 = 0.0, + phi: float32 = 0.0): # in radians + self.psi = psi + self.theta = theta + self.phi = phi + + def marshalledSize(self) -> int: + return 12 + + def serialize(self, outputStream: DataOutputStream) -> None: + outputStream.write_float32(self.psi) + outputStream.write_float32(self.theta) + outputStream.write_float32(self.phi) + + def parse(self, inputStream: DataInputStream) -> None: + self.psi = inputStream.read_float32() + self.theta = inputStream.read_float32() + self.phi = inputStream.read_float32() + + +class EventIdentifier(base.Record): + """6.2.33 Event Identifier record + + Identifies an event in the world. Use this format for every PDU EXCEPT + the LiveEntityPdu. + """ + # TODO: Distinguish EventIdentifier and LiveEventIdentifier + + def __init__(self, + simulationAddress: "SimulationAddress | None" = None, + eventNumber: uint16 = 0): + self.simulationAddress = simulationAddress or SimulationAddress() + """Site and application IDs""" + self.eventNumber = eventNumber + + def marshalledSize(self) -> int: + return self.simulationAddress.marshalledSize() + 2 + + def serialize(self, outputStream: DataOutputStream) -> None: + self.simulationAddress.serialize(outputStream) + outputStream.write_unsigned_short(self.eventNumber) + + def parse(self, inputStream: DataInputStream) -> None: + self.simulationAddress.parse(inputStream) + self.eventNumber = inputStream.read_unsigned_short() + + +class SimulationAddress(base.Record): + """6.2.80 Simulation Address record + + Simulation designation associated with all object identifiers except + those contained in Live Entity PDUs. + """ + + def __init__(self, + site: uint16 = 0, + application: uint16 = 0): + self.site = site + """A site is defined as a facility, installation, organizational unit or a geographic location that has one or more simulation applications capable of participating in a distributed event.""" + self.application = application + """An application is defined as a software program that is used to generate and process distributed simulation data including live, virtual and constructive data.""" + + def marshalledSize(self) -> int: + return 4 + + def serialize(self, outputStream: DataOutputStream) -> None: + outputStream.write_unsigned_short(self.site) + outputStream.write_unsigned_short(self.application) + + def parse(self, inputStream: DataInputStream) -> None: + self.site = inputStream.read_unsigned_short() + self.application = inputStream.read_unsigned_short() + + +class Vector3Float(base.Record): + """6.2.96 Vector record + + Vector values for entity coordinates, linear acceleration, and linear + velocity shall be represented using a Vector record. This record shall + consist of three fields, each a 32-bit floating point number. + The unit of measure represented by these fields shall depend on the + information represented. + """ + + def __init__(self, x: float32 = 0.0, y: float32 = 0.0, z: float32 = 0.0): + self.x = x + self.y = y + self.z = z + + def marshalledSize(self) -> int: + return 12 + + def serialize(self, outputStream: DataOutputStream) -> None: + outputStream.write_float(self.x) + outputStream.write_float(self.y) + outputStream.write_float(self.z) + + def parse(self, inputStream: DataInputStream) -> None: + self.x = inputStream.read_float() + self.y = inputStream.read_float() + self.z = inputStream.read_float() + + +class WorldCoordinates(base.Record): + """6.2.98 World Coordinates record + + Location of the origin of the entity's or object's coordinate system, + target locations, detonation locations, and other points shall be specified + by a set of three coordinates: X, Y, and Z, represented by 64-bit floating + point numbers. + """ + + def __init__(self, x: float64 = 0.0, y: float64 = 0.0, z: float64 = 0.0): + self.x = x + self.y = y + self.z = z + + def marshalledSize(self) -> int: + return 24 + + def serialize(self, outputStream: DataOutputStream) -> None: + outputStream.write_float64(self.x) + outputStream.write_float64(self.y) + outputStream.write_float64(self.z) + + def parse(self, inputStream: DataInputStream) -> None: + self.x = inputStream.read_float64() + self.y = inputStream.read_float64() + self.z = inputStream.read_float64() + + diff --git a/opendis/record/radio/__init__.py b/opendis/record/radio/__init__.py new file mode 100644 index 0000000..8667ef5 --- /dev/null +++ b/opendis/record/radio/__init__.py @@ -0,0 +1,510 @@ +"""Radio Family PDU record types""" + +__all__ = [ + "AntennaPatternRecord", + "BasicHaveQuickMP", + "BeamAntennaPattern", + "CCTTSincgarsMP", + "GenericRadio", + "HighFidelityHAVEQUICKRadio", + "ModulationParametersRecord", + "ModulationType", + "NetId", + "SimpleIntercomRadio", + "SpreadSpectrum", + "UnknownAntennaPattern", + "UnknownRadio", + "VariableTransmitterParametersRecord", +] + +from opendis.record import base, bitfield +from opendis.record.common import * +from opendis.stream import DataInputStream, DataOutputStream +from opendis.types import ( + enum8, + enum16, + enum32, + bf_enum, + bf_int, + bf_uint, + float32, + float64, + struct8, + uint8, + uint16, + uint32, +) + + +# Interfaces + +class AntennaPatternRecord(base.VariableRecord): + """6.2.8 Antenna Pattern record + + The total length of each record shall be a multiple of 64 bits. + """ + + +class ModulationParametersRecord(base.VariableRecord): + """6.2.58 Modulation Parameters record + + Base class for modulation parameters records, as defined in Annex C. + The total length of each record shall be a multiple of 64 bits. + """ + + +# Placeholders + +class UnknownRadio(ModulationParametersRecord): + """Placeholder for unknown or unimplemented radio types.""" + + def __init__(self, data: bytes = b''): + self.data = data + + def marshalledSize(self) -> int: + return len(self.data) + + def serialize(self, outputStream: DataOutputStream) -> None: + super().serialize(outputStream) + outputStream.write_bytes(self.data) + + def parse(self, + inputStream: DataInputStream, + bytelength: int | None = None) -> None: + # Validate bytelength argument by calling base method + super().parse(inputStream, bytelength) + assert isinstance(bytelength, int) + self.data = inputStream.read_bytes(bytelength) + + +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: + super().serialize(outputStream) + outputStream.write_bytes(self.data) + + def parse(self, # pyright: ignore[reportIncompatibleMethodOverride] + inputStream: DataInputStream, + bytelength: int) -> None: + # Validate bytelength argument by calling base method + super().parse(inputStream, bytelength) + # Read the remaining bytes in the record + self.data = inputStream.read_bytes(bytelength - 6) + + +# Implementations + +class BeamAntennaPattern(AntennaPatternRecord): + """6.2.8.2 Beam Antenna Pattern record + + Used when the antenna pattern type field has a value of 1. Specifies the + direction, pattern, and polarization of radiation from an antenna. + """ + + def __init__(self, + beamDirection: "EulerAngles | None" = None, + azimuthBeamwidth: float32 = 0.0, # in radians + elevationBeamwidth: float32 = 0.0, # in radians + referenceSystem: enum8 = 0, # [UID 168] + ez: float32 = 0.0, + ex: float32 = 0.0, + phase: float32 = 0.0): # in radians + self.beamDirection = beamDirection or EulerAngles() + """The rotation that transforms the reference coordinate sytem into the beam coordinate system. Either world coordinates or entity coordinates may be used as the reference coordinate system, as specified by the reference system field of the antenna pattern record.""" + self.azimuthBeamwidth = azimuthBeamwidth + self.elevationBeamwidth = elevationBeamwidth + self.referenceSystem = referenceSystem + self.padding1: uint8 = 0 + self.padding2: uint16 = 0 + self.ez = ez + """This field shall specify the magnitude of the Z-component (in beam coordinates) of the Electrical field at some arbitrary single point in the main beam and in the far field of the antenna.""" + self.ex = ex + """This field shall specify the magnitude of the X-component (in beam coordinates) of the Electrical field at some arbitrary single point in the main beam and in the far field of the antenna.""" + self.phase = phase + """This field shall specify the phase angle between EZ and EX in radians. If fully omni-directional antenna is modeled using beam pattern type one, the omni-directional antenna shall be represented by beam direction Euler angles psi, theta, and phi of zero, an azimuth beamwidth of 2PI, and an elevation beamwidth of PI""" + self.padding3: uint32 = 0 + + def marshalledSize(self) -> int: + return 40 + + def serialize(self, outputStream: DataOutputStream) -> None: + super().serialize(outputStream) + self.beamDirection.serialize(outputStream) + outputStream.write_float32(self.azimuthBeamwidth) + outputStream.write_float32(self.elevationBeamwidth) + outputStream.write_uint8(self.referenceSystem) + outputStream.write_uint8(self.padding1) + outputStream.write_uint16(self.padding2) + outputStream.write_float32(self.ez) + outputStream.write_float32(self.ex) + outputStream.write_float32(self.phase) + outputStream.write_uint32(self.padding3) + + def parse(self, + inputStream: DataInputStream, + bytelength: int | None = 40) -> None: + # Validate bytelength argument by calling base method + super().parse(inputStream, bytelength) + self.beamDirection.parse(inputStream) + self.azimuthBeamwidth = inputStream.read_float32() + self.elevationBeamwidth = inputStream.read_float32() + self.referenceSystem = inputStream.read_uint8() + self.padding1 = inputStream.read_uint8() + self.padding2 = inputStream.read_uint16() + self.ez = inputStream.read_float32() + self.ex = inputStream.read_float32() + self.phase = inputStream.read_float32() + self.padding3 = inputStream.read_uint32() + + +class SpreadSpectrum(base.Record): + """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.bitfield(name="SpreadSpectrum", fields=[ + ("frequencyHopping", bitfield.INTEGER, 1), + ("pseudoNoise", bitfield.INTEGER, 1), + ("timeHopping", bitfield.INTEGER, 1), + ("padding", bitfield.INTEGER, 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) + + +class ModulationType(base.Record): + """6.2.59 Modulation Type Record + + 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 VariableTransmitterParametersRecord(base.StandardVariableRecord): + """6.2.95 Variable Transmitter Parameters record + + One or more VTP records may be associated with a radio system, and the same + VTP record may be associated with multiple radio systems. + Specific VTP records applicable to a radio system are identified in the + subclause that defines the radio system's unique requirements in Annex C. + The total length of each record shall be a multiple of 64 bits. + """ + + +class NetId(base.Record): + """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.bitfield(name="NetId", fields=[ + ("netNumber", bitfield.INTEGER, 10), + ("frequencyTable", bitfield.INTEGER, 2), + ("mode", bitfield.INTEGER, 2), + ("padding", bitfield.INTEGER, 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 GenericRadio(ModulationParametersRecord): + """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, + bytelength: int | None = None) -> None: + pass + + +class SimpleIntercomRadio(ModulationParametersRecord): + """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, + bytelength: int | None = None) -> None: + pass + + +# C.4 HAVE QUICK Radios + +class BasicHaveQuickMP(ModulationParametersRecord): + """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: + super().serialize(outputStream) + 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, + bytelength: int | None = None) -> None: + # Validate bytelength argument by calling base method + super().parse(inputStream, bytelength) + 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() + + +class CCTTSincgarsMP(ModulationParametersRecord): + """Annex C 6.2.3, Table C.7 — CCTT SINCGARS MP record""" + + def __init__(self, + fh_net_id: uint16 = 0, + hop_set_id: uint16 = 0, + lockout_set_id: uint16 = 0, + start_of_message: enum8 = 0, + clear_channel: enum8 = 0, + fh_sync_time_offset: uint32 = 0, + transmission_security_key: uint16 = 0): + self.fh_net_id = fh_net_id + self.hop_set_id = hop_set_id + self.lockout_set_id = lockout_set_id + self.start_of_message = start_of_message + self.clear_channel = clear_channel + self.fh_sync_time_offset = fh_sync_time_offset + self.transmission_security_key = transmission_security_key + self.padding: uint16 = 0 + + def marshalledSize(self) -> int: + return 16 # bytes + + def serialize(self, outputStream: DataOutputStream) -> None: + super().serialize(outputStream) + outputStream.write_uint16(self.fh_net_id) + outputStream.write_uint16(self.hop_set_id) + outputStream.write_uint16(self.lockout_set_id) + outputStream.write_uint8(self.start_of_message) + outputStream.write_uint8(self.clear_channel) + outputStream.write_uint32(self.fh_sync_time_offset) + outputStream.write_uint16(self.transmission_security_key) + outputStream.write_uint16(self.padding) + + def parse(self, + inputStream: DataInputStream, + bytelength: int | None = None) -> None: + # Validate bytelength argument by calling base method + super().parse(inputStream, bytelength) + self.fh_net_id = inputStream.read_uint16() + self.hop_set_id = inputStream.read_uint16() + self.lockout_set_id = inputStream.read_uint16() + self.start_of_message = inputStream.read_uint8() + self.clear_channel = inputStream.read_uint8() + self.fh_sync_time_offset = inputStream.read_uint32() + self.transmission_security_key = inputStream.read_uint16() + self.padding = inputStream.read_uint16() + + +class HighFidelityHAVEQUICKRadio(VariableTransmitterParametersRecord): + """Annex C C4.2.3, Table C.4 — High Fidelity HAVE QUICK Radio record""" + recordType: enum32 = 3000 + + def __init__(self, + netId: NetId | None = None, + todTransmitIndicator: enum8 = 0, + todDelta: uint32 = 0, + wod1: uint32 = 0, + wod2: uint32 = 0, + wod3: uint32 = 0, + wod4: uint32 = 0, + wod5: uint32 = 0, + wod6: uint32 = 0): + self.padding1: uint16 = 0 + self.netId = netId or NetId() + self.todTransmitIndicator = todTransmitIndicator + self.padding2: uint8 = 0 + self.todDelta = todDelta + self.wod1 = wod1 + self.wod2 = wod2 + self.wod3 = wod3 + self.wod4 = wod4 + self.wod5 = wod5 + self.wod6 = wod6 + + def marshalledSize(self) -> int: + return 40 + + def serialize(self, outputStream: DataOutputStream) -> None: + super().serialize(outputStream) + outputStream.write_uint16(self.padding1) + self.netId.serialize(outputStream) + outputStream.write_uint8(self.todTransmitIndicator) + outputStream.write_uint8(self.padding2) + outputStream.write_uint32(self.todDelta) + outputStream.write_uint32(self.wod1) + outputStream.write_uint32(self.wod2) + outputStream.write_uint32(self.wod3) + outputStream.write_uint32(self.wod4) + outputStream.write_uint32(self.wod5) + outputStream.write_uint32(self.wod6) + + def parse(self, + inputStream: DataInputStream, + bytelength: int) -> None: + # Validate bytelength argument by calling base method + super().parse(inputStream, bytelength) + self.padding1 = inputStream.read_uint16() + self.netId.parse(inputStream) + self.todTransmitIndicator = inputStream.read_uint8() + self.padding2 = inputStream.read_uint8() + self.todDelta = inputStream.read_uint32() + self.wod1 = inputStream.read_uint32() + self.wod2 = inputStream.read_uint32() + self.wod3 = inputStream.read_uint32() + self.wod4 = inputStream.read_uint32() + self.wod5 = inputStream.read_uint32() + self.wod6 = inputStream.read_uint32() diff --git a/opendis/record/symbolic_names.py b/opendis/record/symbolic_names.py new file mode 100644 index 0000000..8cf858a --- /dev/null +++ b/opendis/record/symbolic_names.py @@ -0,0 +1,136 @@ +"""6.1.8 Table 25: Constants for DIS7 symbolic names""" + +AGG_RESPONSE_DFLT = 10 # Default: 10 s +ALL_AGGS = 0xFFFF # Hexadecimal +ALL_APPLIC = 0xFFFF # Hexadecimal +ALL_BEAMS = 0xFF # Hexadecimal +ALL_EMITTERS = 0xFF # Hexadecimal +ALL_ENTITIES = 0xFFFF # Hexadecimal +ALL_OBJECTS = 0xFFFF # Hexadecimal +ALL_SITES = 0xFFFF # Hexadecimal +COLLISION_ELASTIC_TIMEOUT = 5 # Default: 5 s +COLLISION_THRSH = 0.1 # Default: 0.1 m/s +DE_AREA_AIMING_THRSH = 10 # Default: 10° +DE_ENERGY_THRSH = 1.0 # Default: 1.0% +DE_PRECISION_AIMING_THRSH = 0.5 # Default: 0.5 m +DRA_ORIENT_THRSH = 3 # Default: 3° +DRA_POS_THRSH = 1 # Default: 1 m +D_SPOT_NO_ENTITY = None # No entity +EE_AD_PULRAT_THRSH = 0.017 # Default: 0.017 rad/s +EE_AD_PULACC_THRSH = 0.017 # Default: 0.017 rad/s² +EE_AZ_THRSH = 1 # Default: 1° +EE_EL_THRSH = 1 # Default: 1° +EE_ERP_THRSH = 1.0 # Default: 1.0 dBm +EE_FREQ_THRSH = 1 # Default: 1 Hz +EE_FRNG_THRSH = 1 # Default: 1 Hz +EE_FT_VEL_THRSH = 1.0 # Default: 1.0 m/s +EE_FT_ACC_THRSH = 1.0 # Default: 1.0 m/s² +EE_FT_MWD_THRSH = 10000 # Default: 10000 m +EE_FT_KT_THRSH = 10 # Default: 10 s +EE_FT_ESP_THRSH = 10 # Default: 10 m +EE_HIGH_DENSITY_THRSH = 10 # Default: 10 entities/beam +EE_PRF_THRSH = 1 # Default: 1 Hz +EE_PW_THRSH = 1 # Default: 1 +ENTITY_ID_UNKNOWN = None # No entity +EP_DIMENSION_THRSH = 1 # Default: 1 m +EP_NO_SEQUENCE = 0xFFFF # Hexadecimal +EP_POS_THRSH = 1 # Default: 1 m shift +EP_STATE_THRSH = 10 # Default: ±10% +GD_GEOMETRY_CHANGE = 10 # Default: ±10% +GD_STATE_CHANGE = 10 # Default: ±10% +HBT_DAMAGE_TIMEOUT_MPLIER = 2.4 # Default: 2.4 +HBT_ESPDU_KIND_CULTURAL_FEATURE = 5 # Default: 5 s, Tolerance: ±10% +HBT_ESPDU_KIND_ENVIRONMENTAL = 5 # Default: 5 s, Tolerance: ±10% +HBT_ESPDU_KIND_EXPENDABLE = 5 # Default: 5 s, Tolerance: ±10% +HBT_ESPDU_KIND_LIFE_FORM = 5 # Default: 5 s, Tolerance: ±10% +HBT_ESPDU_KIND_MUNITION = 5 # Default: 5 s, Tolerance: ±10% +HBT_ESPDU_KIND_RADIO = 5 # Default: 5 s, Tolerance: ±10% +HBT_ESPDU_KIND_SENSOR_EMITTER = 5 # Default: 5 s, Tolerance: ±10% +HBT_ESPDU_KIND_SUPPLY = 5 # Default: 5 s, Tolerance: ±10% +HBT_ESPDU_PLATFORM_AIR = 5 # Default: 5 s, Tolerance: ±10% +HBT_ESPDU_PLATFORM_LAND = 5 # Default: 5 s, Tolerance: ±10% +HBT_ESPDU_PLATFORM_SPACE = 5 # Default: 5 s, Tolerance: ±10% +HBT_ESPDU_PLATFORM_SUBSURFACE = 5 # Default: 5 s, Tolerance: ±10% +HBT_ESPDU_PLATFORM_SURFACE = 5 # Default: 5 s, Tolerance: ±10% +HBT_PDU_AGGREGATE_STATE = 30 # Default: 30 s, Tolerance: ±10% +HBT_PDU_APPEARANCE = 60 # Default: 60 s, Tolerance: ±10% +HBT_PDU_DE_FIRE = 0.5 # Default: 0.5 s, Tolerance: ±10% +HBT_PDU_DESIGNATOR = 5 # Default: 5 s, Tolerance: ±10% +HBT_PDU_EE = 5 # Default: 5 s, Tolerance: ±10% +HBT_PDU_ENTITY_DAMAGE = 10 # Default: 10 s, Tolerance: ±10% +HBT_PDU_ENVIRONMENTAL_PROCESS = 15 # Default: 15 s, Tolerance: ±10% +HBT_PDU_GRIDDED_DATA = 900 # Default: 15 min, Tolerance: ±10% +HBT_PDU_IFF = 10 # Default: 10 s, Tolerance: ±10% +HBT_PDU_ISGROUPOF = 5 # Default: 5 s, Tolerance: ±10% +HBT_PDU_MINEFIELD_DATA = 5 # Default: 5 s, Tolerance: ±10% +HBT_PDU_MINEFIELD_STATE = 5 # Default: 5 s, Tolerance: ±10% +HBT_PDU_RECEIVER = 5 # Default: 5 s, Tolerance: ±10% +HBT_PDU_SEES = 180 # Default: 3 min, Tolerance: ±10% +HBT_PDU_TRANSMITTER = 2 # Default: 2 s, Tolerance: ±10% +HBT_PDU_TSPI = 30 # Default: 30 s, Tolerance: ±10% +HBT_PDU_UA = 180 # Default: 3 min, Tolerance: ±10% +HBT_STATIONARY = 60 # Default: 1 min, Tolerance: ±10% +HBT_TIMEOUT_MPLIER = 2.4 # Default: 2.4 +HQ_TOD_DIFF_THRSH = 20 # Default: 20 ms +IFF_AZ_THRSH = 3 # Default: 3° +IFF_CHG_LATENCY = 2 # Default: 2 s +IFF_EL_THRSH = 3 # Default: 3° +IFF_IP_REPLY_TIMER = 30 # Default: 30 s +IFF_PDU_FINAL = 10 # Default: 10 s +IFF_PDU_RESUME = 10 # Default: 10 s +IO_UNTIL_FURTHER_NOTICE = 65535 # Fixed +MAX_PDU_SIZE_BITS = 65536 # Fixed +MAX_PDU_SIZE_OCTETS = 8192 # Fixed +MINEFIELD_CHANGE = 2.5 # Default: 2.5 s +MINEFIELD_RESPONSE_TIMER = 1 # Default: 1 s +MULTIPLES_PRESENT = 0 # Fixed +NO_AGG = 0 # Fixed +NO_APPLIC = 0 # Fixed +NO_BEAM = 0 # Fixed +NO_CATEGORY = 0 # Fixed +NO_EMITTER = 0 # Fixed +NO_ENTITY = 0 # Fixed +NO_FIRE_MISSION = 0 # Fixed +NO_KIND = 0 # Fixed +NO_OBJECT = 0 # Fixed +NO_PATTERN = 0.0 # Fixed +NO_REF_NUMBER = 0 # Fixed +NO_SITE = 0 # Fixed +NO_SPECIFIC = 0 # Fixed +NO_SPECIFIC_ENTITY = None # No entity +NO_SUBCAT = 0 # Fixed +NO_VALUE = 0 # Fixed +NON_SYNC_THRSH = 60 # Default: 1 min +POWER_ENGINE_OFF = -100.0 # Fixed +POWER_IDLE = 0.0 # Fixed +POWER_MAX_AFTERBURNER = 100.0 # Fixed +POWER_MILITARY = 50.0 # Fixed +POWER_MIN_AFTERBURNER = 51.0 # Fixed +REPAR_REC_T1 = 5 # Default: 5 s +REPAR_SUP_T1 = 12 # Default: 12 s +REPAR_SUP_T2 = 12 # Default: 12 s +RESUP_REC_T1 = 5 # Default: 5 s +RESUP_REC_T2 = 55 # Default: 55 s +RESUP_SUP_T1 = 60 # Default: 1 min +RQST_ASSIGN_ID = 0xFFFE # Hexadecimal +SEES_NDA_THRSH = 2 # Default: ±2° in the axis of deflection +SEES_PS_THRSH = 10 # Default: ±10% of the maximum value of the Power Setting +SEES_RPM_THRSH = 5 # Default: ±5% of the maximum engine speed in RPM +SMALLEST_MTU_OCTETS = 1400 # Default: 1400 octets for IPv4 networks +SM_REL_RETRY_CNT = 3 # Default: 3 +SM_REL_RETRY_DELAY = 2 # Default: 2 s +TARGET_ID_UNKNOWN = None # No entity +TIMESTAMP_AHEAD = 5 # Default: 5 s +TIMESTAMP_BEHIND = 5 # Default: 5 s +TI_TIMER1_DFLT = 2 # Default: 2 s +TI_TIMER2_DFLT = 12 # Default: 12 s +TO_AUTO_RESPONSE_TIMER = 5 # Default: 5 s +TO_MAN_RESPONSE_TIMER = 120 # Default: 120 s +TR_TIMER1_DFLT = 5 # Default: 5 s +TR_TIMER2_DFLT = 60 # Default: 60 s +TRANS_ORIENT_THRSH = 180 # Default: 180° +TRANS_POS_THRSH = 500 # Default: 500 m +UA_ORIENT_THRSH = 2 # Default: 2° +UA_POS_THRSH = 10 # Default: 10 m +UA_SRPM_ROC_THRSH = 10 # Default: ±10% of maximum rate of change +UA_SRPM_THRSH = 5 # Default: ±5% of maximum shaft rate in RPM diff --git a/opendis/record/warfare/__init__.py b/opendis/record/warfare/__init__.py new file mode 100644 index 0000000..d0ea1dd --- /dev/null +++ b/opendis/record/warfare/__init__.py @@ -0,0 +1,280 @@ +"""Warfare Family PDU record types""" + +__all__ = [ + "DEFireFlags", + "DamageDescriptionRecord", + "DirectedEnergyDamage", + "DirectedEnergyAreaAimpoint", + "DirectedEnergyPrecisionAimpoint", + "DirectedEnergyTargetEnergyDeposition", +] + +from opendis.record import base, bitfield +from opendis.record.common import * +from opendis.stream import DataInputStream, DataOutputStream +from opendis.types import ( + enum8, + enum16, + enum32, + bf_enum, + bf_int, + bf_uint, + float32, + float64, + struct8, + uint8, + uint16, + uint32, +) + +from .enums import DEFireFlags +from opendis.record.radio import BeamAntennaPattern + + +class DamageDescriptionRecord(base.StandardVariableRecord): + """6.2.15 Damage Description record + + Damage Description records shall use the Standard Variable record format of + the Standard Variable Specification record (see 6.2.83). + New Damage Description records may be defined at some future date as needed. + """ + + +class DirectedEnergyDamage(DamageDescriptionRecord): + """6.2.15.2 Directed Energy Damage Description record + + Damage sustained by an entity due to directed energy. Location of the + damage based on a relative x, y, z location from the center of the entity. + """ + recordType: enum32 = 4500 # [UID 66] Variable Record Type + + def __init__( + self, + damageLocation: Vector3Float | None = None, + damageDiameter: float32 = 0.0, # in metres + temperature: float32 = -273.15, # in degrees Celsius + componentIdentification: enum8 = 0, # [UID 314] + componentDamageStatus: enum8 = 0, # [UID 315] + componentVisualDamageStatus: struct8 = 0, # [UID 317] + componentVisualSmokeColor: enum8 = 0, # [UID 316] + fireEventID: EventIdentifier | None = None): + self.padding: uint16 = 0 + self.damageLocation = damageLocation or Vector3Float() + self.damageDiameter = damageDiameter + self.temperature = temperature + self.componentIdentification = componentIdentification + self.componentDamageStatus = componentDamageStatus + self.componentVisualDamageStatus = componentVisualDamageStatus + self.componentVisualSmokeColor = componentVisualSmokeColor + self.fireEventID = fireEventID or EventIdentifier() + self.padding2: uint16 = 0 + + def marshalledSize(self) -> int: + return 40 + + def serialize(self, outputStream: DataOutputStream) -> None: + super().serialize(outputStream) + outputStream.write_uint32(self.recordType) + outputStream.write_uint16(self.recordLength) + outputStream.write_uint16(self.padding) + self.damageLocation.serialize(outputStream) + outputStream.write_float32(self.damageDiameter) + outputStream.write_float32(self.temperature) + outputStream.write_uint8(self.componentIdentification) + outputStream.write_uint8(self.componentDamageStatus) + outputStream.write_uint8(self.componentVisualDamageStatus) + outputStream.write_uint8(self.componentVisualSmokeColor) + self.fireEventID.serialize(outputStream) + outputStream.write_uint16(self.padding2) + + def parse(self, + inputStream: DataInputStream, + bytelength: int) -> None: + # Validate bytelength argument by calling base method + super().parse(inputStream, bytelength) + self.padding = inputStream.read_unsigned_short() + self.damageLocation.parse(inputStream) + self.damageDiameter = inputStream.read_float32() + self.temperature = inputStream.read_float32() + self.componentIdentification = inputStream.read_uint8() + self.componentDamageStatus = inputStream.read_uint8() + self.componentVisualDamageStatus = inputStream.read_uint8() + self.componentVisualSmokeColor = inputStream.read_uint8() + self.fireEventID.parse(inputStream) + self.padding2 = inputStream.read_uint16() + + +class DirectedEnergyAreaAimpoint(DamageDescriptionRecord): + """6.2.20.2 DE Area Aimpoint record + + Targeting information when the target of the directed energy weapon is an + area. The area may or may not be associated with one or more target + entities. + """ + recordType: enum32 = 4001 # [UID 66] + + def __init__(self, + beamAntennaPatterns: list["BeamAntennaPattern"] | None = None, + directedEnergyTargetEnergyDepositions: list["DirectedEnergyTargetEnergyDeposition"] | None = None): + self.padding: uint16 = 0 + self.beamAntennaPatterns: list["BeamAntennaPattern"] = beamAntennaPatterns or [] + self.directedEnergyTargetEnergyDepositions: list["DirectedEnergyTargetEnergyDeposition"] = directedEnergyTargetEnergyDepositions or [] + + @property + def beamAntennaPatternCount(self) -> uint16: + return len(self.beamAntennaPatterns) + + @property + def directedEnergyTargetEnergyDepositionCount(self) -> uint16: + return len(self.directedEnergyTargetEnergyDepositions) + + def marshalledSize(self) -> int: + size = 8 # recordType, recordLength, padding + for record in self.beamAntennaPatterns: + size += record.marshalledSize() + for record in self.directedEnergyTargetEnergyDepositions: + size += record.marshalledSize() + return size + + def serialize(self, outputStream: DataOutputStream) -> None: + super().serialize(outputStream) + outputStream.write_uint32(self.recordType) + outputStream.write_uint16(self.recordLength) + outputStream.write_uint16(self.padding) + outputStream.write_uint16(self.beamAntennaPatternCount) + outputStream.write_uint16( + self.directedEnergyTargetEnergyDepositionCount + ) + for record in self.beamAntennaPatterns: + record.serialize(outputStream) + + for record in self.directedEnergyTargetEnergyDepositions: + record.serialize(outputStream) + + def parse(self, + inputStream: DataInputStream, + bytelength: int) -> None: + # Validate bytelength argument by calling base method + super().parse(inputStream, bytelength) + self.padding = inputStream.read_uint16() + beamAntennaPatternCount = inputStream.read_uint16() + directedEnergyTargetEnergyDepositionCount = inputStream.read_uint16() + for _ in range(0, beamAntennaPatternCount): + record = BeamAntennaPattern() + record.parse(inputStream) + self.beamAntennaPatterns.append(record) + for _ in range(0, directedEnergyTargetEnergyDepositionCount): + record = DirectedEnergyTargetEnergyDeposition() + record.parse(inputStream) + self.directedEnergyTargetEnergyDepositions.append(record) + + +class DirectedEnergyPrecisionAimpoint(DamageDescriptionRecord): + """6.2.20.3 DE Precision Aimpoint record + + Targeting information when the target of the directed energy weapon is not + an area but a specific target entity. Use of this record assumes that the DE + weapon would not fire unless a target is known and is currently tracked. + """ + recordType: enum32 = 4000 + + def __init__(self, + targetSpotLocation: WorldCoordinates | None = None, + targetSpotEntityLocation: Vector3Float | None = None, + targetSpotVelocity: Vector3Float | None = None, # in m/s + targetSpotAcceleration: Vector3Float | None = None, # in m/s^2 + targetEntityID: EntityIdentifier | None = None, + targetComponentID: enum8 = 0, # [UID 314] + beamSpotType: enum8 = 0, # [UID 311] + beamSpotCrossSectionSemiMajorAxis: float32 = 0.0, # in meters + beamSpotCrossSectionSemiMinorAxis: float32 = 0.0, # in meters + beamSpotCrossSectionOrientationAngle: float32 = 0.0, # in radians + peakIrradiance: float32 = 0.0): # in W/m^2 + self.padding: uint16 = 0 + self.targetSpotLocation = targetSpotLocation or WorldCoordinates() + self.targetSpotEntityLocation = targetSpotEntityLocation or Vector3Float() + self.targetSpotVelocity = targetSpotVelocity or Vector3Float() + self.targetSpotAcceleration = targetSpotAcceleration or Vector3Float() + self.targetEntityID = targetEntityID or EntityIdentifier() + self.targetComponentID = targetComponentID + self.beamSpotType = beamSpotType + self.beamSpotCrossSectionSemiMajorAxis = beamSpotCrossSectionSemiMajorAxis + self.beamSpotCrossSectionSemiMinorAxis = beamSpotCrossSectionSemiMinorAxis + self.beamSpotCrossSectionOrientationAngle = beamSpotCrossSectionOrientationAngle + self.peakIrradiance = peakIrradiance + self.padding2: uint32 = 0 + + def marshalledSize(self) -> int: + return 96 + + def serialize(self, outputStream: DataOutputStream) -> None: + super().serialize(outputStream) + outputStream.write_uint32(self.recordType) + outputStream.write_uint16(self.recordLength) + outputStream.write_uint16(self.padding) + self.targetSpotLocation.serialize(outputStream) + self.targetSpotEntityLocation.serialize(outputStream) + self.targetSpotVelocity.serialize(outputStream) + self.targetSpotAcceleration.serialize(outputStream) + self.targetEntityID.serialize(outputStream) + outputStream.write_uint8(self.targetComponentID) + outputStream.write_uint8(self.beamSpotType) + outputStream.write_float32(self.beamSpotCrossSectionSemiMajorAxis) + outputStream.write_float32(self.beamSpotCrossSectionSemiMinorAxis) + outputStream.write_float32(self.beamSpotCrossSectionOrientationAngle) + outputStream.write_float32(self.peakIrradiance) + outputStream.write_uint32(self.padding2) + + def parse(self, + inputStream: DataInputStream, + bytelength: int) -> None: + """recordType and recordLength are assumed to have been read before + this method is called. + """ + # Validate bytelength argument by calling base method + super().parse(inputStream, bytelength) + self.padding = inputStream.read_uint16() + self.targetSpotLocation.parse(inputStream) + self.targetSpotEntityLocation.parse(inputStream) + self.targetSpotVelocity.parse(inputStream) + self.targetSpotAcceleration.parse(inputStream) + self.targetEntityID.parse(inputStream) + self.targetComponentID = inputStream.read_uint8() + self.beamSpotType = inputStream.read_uint8() + self.beamSpotCrossSectionSemiMajorAxis = inputStream.read_float32() + self.beamSpotCrossSectionSemiMinorAxis = inputStream.read_float32() + self.beamSpotCrossSectionOrientationAngle = inputStream.read_float32() + self.peakIrradiance = inputStream.read_float32() + self.padding2 = inputStream.read_uint32() + + +class DirectedEnergyTargetEnergyDeposition(base.Record): + """6.2.20.4 DE Target Energy Deposition record + + Directed energy deposition properties for a target entity shall be + communicated using the DE Target Energy Deposition record. This record is + required to be included inside another DE record as it does not have a + record type. + """ + + def __init__(self, + targetEntityID: EntityIdentifier | None = None, + peakIrradiance: float32 = 0.0): # in W/m^2 + self.targetEntityID = targetEntityID or EntityIdentifier() + self.padding: uint16 = 0 + self.peakIrradiance = peakIrradiance + + def marshalledSize(self) -> int: + return self.targetEntityID.marshalledSize() + 6 + + def serialize(self, outputStream: DataOutputStream) -> None: + super().serialize(outputStream) + self.targetEntityID.serialize(outputStream) + outputStream.write_uint16(self.padding) + outputStream.write_float32(self.peakIrradiance) + + def parse(self, inputStream: DataInputStream) -> None: + super().parse(inputStream) + self.targetEntityID.parse(inputStream) + self.padding = inputStream.read_uint16() + self.peakIrradiance = inputStream.read_float32() diff --git a/opendis/record/warfare/enums.py b/opendis/record/warfare/enums.py new file mode 100644 index 0000000..01d188a --- /dev/null +++ b/opendis/record/warfare/enums.py @@ -0,0 +1,38 @@ +"""Warfare Family PDU record types: SISO enumeration classes""" + +from opendis.record import base, bitfield +from opendis.stream import DataInputStream, DataOutputStream +from opendis.types import bf_enum, bf_uint + + +class DEFireFlags(base.Record): + """SISO-REF-010-2025 18.5.2 DE Fire Flags [UID 313]""" + + _struct = bitfield.bitfield(name="DEFireFlags", fields=[ + ("weaponOn", bitfield.INTEGER, 1), # state of the DE Weapon + ("stateUpdateFlag", bitfield.INTEGER, 1), # DE Weapon State Change + ("padding", bitfield.INTEGER, 14) # unused bits + ]) + + def __init__(self, + weaponOn: bool = False, + stateUpdateFlag: bf_enum = 0): # [UID 299] + # Net number ranging from 0 to 999 decimal + self.weaponOn = weaponOn + self.stateUpdateFlag = stateUpdateFlag + self.padding: bf_uint = 0 + + def marshalledSize(self) -> int: + return self._struct.marshalledSize() + + def serialize(self, outputStream: DataOutputStream) -> None: + self._struct( + self.weaponOn, + self.stateUpdateFlag, + ).serialize(outputStream) + + def parse(self, inputStream: DataInputStream) -> None: + record_bitfield = self._struct.parse(inputStream) + self.weaponOn = record_bitfield.weaponOn + self.stateUpdateFlag = record_bitfield.stateUpdateFlag + self.padding = record_bitfield.padding diff --git a/opendis/stream.py b/opendis/stream.py index 45731d8..908e976 100644 --- a/opendis/stream.py +++ b/opendis/stream.py @@ -5,5 +5,180 @@ __all__ = ['DataInputStream', 'DataOutputStream'] -from .DataInputStream import DataInputStream -from .DataOutputStream import DataOutputStream +from io import BufferedIOBase +import struct + +from .types import ( + int8, + int16, + int32, + int64, + uint8, + uint16, + uint32, + uint64, + float32, + float64, + char8, + char16, +) + + +class DataInputStream: + def __init__(self, stream: BufferedIOBase): + self.stream = stream + + def read_boolean(self) -> bool: + return struct.unpack('?', self.stream.read(1))[0] + + def read_byte(self) -> int8: + return struct.unpack('b', self.stream.read(1))[0] + + def read_unsigned_byte(self) -> uint8: + return struct.unpack('B', self.stream.read(1))[0] + + def read_char(self) -> char16: + return chr(struct.unpack('>H', self.stream.read(2))[0]) + + def read_double(self) -> float64: + return struct.unpack('>d', self.stream.read(8))[0] + + def read_float(self) -> float32: + return struct.unpack('>f', self.stream.read(4))[0] + + def read_short(self) -> int16: + return struct.unpack('>h', self.stream.read(2))[0] + + def read_unsigned_short(self) -> uint16: + return struct.unpack('>H', self.stream.read(2))[0] + + def read_long(self) -> int64: + return struct.unpack('>q', self.stream.read(8))[0] + + def read_utf(self) -> bytes: + utf_length = struct.unpack('>H', self.stream.read(2))[0] + return self.stream.read(utf_length) + + def read_int(self) -> int32: + return struct.unpack('>i', self.stream.read(4))[0] + + 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) + + # Aliases for convenience + def read_char8(self) -> char8: + return char8(self.read_char()) + + def read_float32(self) -> float32: + return float32(self.read_float()) + + def read_float64(self) -> float64: + return float64(self.read_double()) + + def read_int8(self) -> int8: + return int8(self.read_byte()) + + def read_int16(self) -> int16: + return int16(self.read_short()) + + def read_int32(self) -> int32: + return int32(self.read_int()) + + def read_int64(self) -> int64: + return int64(self.read_long()) + + def read_uint8(self) -> uint8: + return uint8(self.read_unsigned_byte()) + + def read_uint16(self) -> uint16: + return uint16(self.read_unsigned_short()) + + def read_uint32(self) -> uint32: + return uint32(self.read_unsigned_int()) + + def read_uint64(self) -> uint64: + return uint64(self.read_long()) + + +class DataOutputStream: + def __init__(self, stream: BufferedIOBase): + self.stream = stream + + def write_boolean(self, boolean: bool) -> None: + self.stream.write(struct.pack('?', boolean)) + + def write_byte(self, val: int) -> None: + self.stream.write(struct.pack('b', val)) + + def write_unsigned_byte(self, val: int) -> None: + self.stream.write(struct.pack('B', val)) + + def write_char(self, val: str) -> None: + self.stream.write(struct.pack('>H', ord(val))) + + def write_double(self, val: float) -> None: + self.stream.write(struct.pack('>d', val)) + + def write_float(self, val: float) -> None: + self.stream.write(struct.pack('>f', val)) + + def write_short(self, val: int) -> None: + self.stream.write(struct.pack('>h', val)) + + def write_unsigned_short(self, val: int) -> None: + self.stream.write(struct.pack('>H', val)) + + def write_long(self, val: int) -> None: + self.stream.write(struct.pack('>q', val)) + + def write_utf(self, string: bytes) -> None: + self.stream.write(struct.pack('>H', len(string))) + self.stream.write(string) + + def write_int(self, val: int) -> None: + self.stream.write(struct.pack('>i', val)) + + 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) + + # Aliases for convenience + def write_char8(self, val: char8) -> None: + self.write_byte(ord(val)) + + def write_float32(self, val: float32) -> None: + self.write_float(val) + + def write_float64(self, val: float64) -> None: + self.write_double(val) + + def write_int8(self, val: int8) -> None: + self.write_byte(val) + + def write_int16(self, val: int16) -> None: + self.write_short(val) + + def write_int32(self, val: int32) -> None: + self.write_int(val) + + def write_int64(self, val: int64) -> None: + self.write_long(val) + + def write_uint8(self, val: uint8) -> None: + self.write_unsigned_byte(val) + + def write_uint16(self, val: uint16) -> None: + self.write_unsigned_short(val) + + def write_uint32(self, val: uint32) -> None: + self.write_unsigned_int(val) + + def write_uint64(self, val: uint64) -> None: + self.stream.write(struct.pack('>Q', val)) diff --git a/tests/test_ElectromageneticEmissionPdu.py b/tests/test_ElectromageneticEmissionPdu.py index 841489f..943ab09 100644 --- a/tests/test_ElectromageneticEmissionPdu.py +++ b/tests/test_ElectromageneticEmissionPdu.py @@ -22,9 +22,9 @@ def test_parse(self): #self.assertEqual(0, pdu.timestamp) self.assertEqual(108, pdu.length) - self.assertEqual(23, pdu.emittingEntityID.siteID) - self.assertEqual(1, pdu.emittingEntityID.applicationID) - self.assertEqual(2, pdu.emittingEntityID.entityID) + self.assertEqual(23, pdu.emittingEntityID.simulationAddress.site) + self.assertEqual(1, pdu.emittingEntityID.simulationAddress.application) + self.assertEqual(2, pdu.emittingEntityID.entityNumber) self.assertEqual(23, pdu.eventID.simulationAddress.site) self.assertEqual(1, pdu.eventID.simulationAddress.application) @@ -55,9 +55,9 @@ def test_parse(self): self.assertEqual(0, pdu.systems[0].beamRecords[0].highDensityTrackJam) self.assertEqual(0, pdu.systems[0].beamRecords[0].jammingModeSequence) - self.assertEqual(23, pdu.systems[0].beamRecords[0].trackJamRecords[0].entityID.siteID) - self.assertEqual(1, pdu.systems[0].beamRecords[0].trackJamRecords[0].entityID.applicationID) - self.assertEqual(1, pdu.systems[0].beamRecords[0].trackJamRecords[0].entityID.entityID) + self.assertEqual(23, pdu.systems[0].beamRecords[0].trackJamRecords[0].entityID.simulationAddress.site) + self.assertEqual(1, pdu.systems[0].beamRecords[0].trackJamRecords[0].entityID.simulationAddress.application) + self.assertEqual(1, pdu.systems[0].beamRecords[0].trackJamRecords[0].entityID.entityNumber) self.assertEqual(0, pdu.systems[0].beamRecords[0].trackJamRecords[0].emitterNumber) self.assertEqual(0, pdu.systems[0].beamRecords[0].trackJamRecords[0].beamNumber) diff --git a/tests/test_EntityStatePdu.py b/tests/test_EntityStatePdu.py index b7227e6..1cd8c26 100644 --- a/tests/test_EntityStatePdu.py +++ b/tests/test_EntityStatePdu.py @@ -24,9 +24,9 @@ def test_parse(self): self.assertEqual(0, pdu.padding) # Entity ID - self.assertEqual(42, pdu.entityID.siteID) - self.assertEqual(4, pdu.entityID.applicationID) - self.assertEqual(26, pdu.entityID.entityID) + self.assertEqual(42, pdu.entityID.simulationAddress.site) + self.assertEqual(4, pdu.entityID.simulationAddress.application) + self.assertEqual(26, pdu.entityID.entityNumber) # Force ID self.assertEqual(1, pdu.forceId) diff --git a/tests/test_SignalPdu.py b/tests/test_SignalPdu.py index d703d71..300a2e7 100644 --- a/tests/test_SignalPdu.py +++ b/tests/test_SignalPdu.py @@ -6,7 +6,7 @@ from opendis.dis7 import * from opendis.PduFactory import * -from opendis.DataOutputStream import DataOutputStream +from opendis.stream import DataOutputStream class TestSignalPdu(unittest.TestCase): @@ -25,9 +25,9 @@ def test_parse_and_serialize(self): #self.assertEqual(0, pdu.timestamp) self.assertEqual(1056, pdu.length) - self.assertEqual(1677, pdu.entityID.siteID) - self.assertEqual(1678, pdu.entityID.applicationID) - self.assertEqual(169, pdu.entityID.entityID ) + self.assertEqual(1677, pdu.entityID.simulationAddress.site) + self.assertEqual(1678, pdu.entityID.simulationAddress.application) + self.assertEqual(169, pdu.entityID.entityNumber) self.assertEqual(1, pdu.radioID) self.assertEqual(4, pdu.encodingScheme) self.assertEqual(0, pdu.tdlType) diff --git a/tests/test_TransmitterPdu.py b/tests/test_TransmitterPdu.py index a34df0f..4cc36f5 100644 --- a/tests/test_TransmitterPdu.py +++ b/tests/test_TransmitterPdu.py @@ -22,9 +22,9 @@ def test_parse(self): #self.assertEqual(0, pdu.timestamp) self.assertEqual(104, pdu.length) - self.assertEqual(1677, pdu.radioReferenceID.siteID) - self.assertEqual(1678, pdu.radioReferenceID.applicationID) - self.assertEqual(169, pdu.radioReferenceID.entityID ) + self.assertEqual(1677, pdu.radioReferenceID.simulationAddress.site) + self.assertEqual(1678, pdu.radioReferenceID.simulationAddress.application) + self.assertEqual(169, pdu.radioReferenceID.entityNumber) self.assertEqual(1, pdu.radioNumber) self.assertEqual(2, pdu.transmitState) self.assertEqual(10000000000, pdu.frequency)