├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── examples ├── __init__.py ├── dis_receiver.py └── dis_sender.py ├── opendis ├── DataInputStream.py ├── DataOutputStream.py ├── PduFactory.py ├── RangeCoordinates.py ├── __init__.py ├── dis7.py └── types.py ├── poetry.lock ├── pyproject.toml └── tests ├── ElectromagneticEmissionPdu-single-system.raw ├── EntityStatePdu-26.raw ├── SetDataPdu-multi-variable-datums.raw ├── SetDataPdu-vbs-script-cmd.raw ├── SignalPdu.raw ├── TransmitterPdu.raw ├── __init__.py ├── testElectromageneticEmissionPdu.py ├── testEntityStatePdu.py ├── testRangeCoordinates.py ├── testSetDataPdu.py ├── testSignalPdu.py └── testTransmitterPdu.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache* 2 | *egg-info* 3 | build/* 4 | dist/* 5 | docs/* 6 | .local/* 7 | .* 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | script: 3 | # check syntax of all files 4 | - python -m py_compile */*.py 5 | # run unit tests 6 | - pip install -e . 7 | - cd tests && python -m unittest discover 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2023, Open DIS 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## open-dis-python 2 | 3 | [![Build Status](https://app.travis-ci.com/open-dis/open-dis-python.svg?branch=master)](https://app.travis-ci.com/open-dis/open-dis-python) 4 | [![PyPI Version](https://shields.mitmproxy.org/pypi/v/opendis.svg)](https://pypi.org/project/opendis/) 5 | 6 | A Python 3 implementation of the Distributed Interactive Simulation (DIS) 7 standard. 7 | Initially generated by [xmlpg](https://github.com/open-dis/xmlpg). 8 | 9 | ## Library installation 10 | 11 | From source: 12 | 13 | ```bash 14 | pip install . 15 | ``` 16 | 17 | For developers of this library (This installs a symlink to the sources so they can be edited and referenced in `tests` and `examples` without a reinstall): 18 | ```bash 19 | pip install -e . 20 | ``` 21 | 22 | ## Run examples 23 | 24 | Run a receiver: 25 | 26 | ```bash 27 | cd examples 28 | python3 dis_receiver.py 29 | ``` 30 | 31 | In another terminal, run the sender: 32 | 33 | ```bash 34 | python3 dis_sender.py 35 | ``` 36 | 37 | You should also see the traffic on the net in Wireshark on your localhost interface. 38 | 39 | Press `Ctrl+\` to stop the process. 40 | 41 | ## Documentation 42 | 43 | You can auto generate API docs from the project source code: 44 | ```bash 45 | pip install pdoc 46 | pdoc --html --html-dir docs opendis 47 | ``` 48 | 49 | The docs will be generated in the `docs/opendis` folder. 50 | 51 | ## Poetry setup 52 | 1. `poetry install` or `poetry update` 53 | 1. `poetry run python examples/dis_receiver.py` or `poetry shell` && `python examples/dis_receiver.py` 54 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = "mcgredo" 2 | __date__ = "$Jun 23, 2015 10:26:43 AM$" -------------------------------------------------------------------------------- /examples/dis_receiver.py: -------------------------------------------------------------------------------- 1 | #!python 2 | 3 | __author__ = "mcgredo" 4 | __date__ = "$Jun 25, 2015 12:10:26 PM$" 5 | 6 | import socket 7 | import time 8 | import sys 9 | import array 10 | 11 | from opendis.dis7 import * 12 | from opendis.RangeCoordinates import * 13 | from opendis.PduFactory import createPdu 14 | 15 | UDP_PORT = 3001 16 | 17 | udpSocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 18 | udpSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 19 | udpSocket.bind(("", UDP_PORT)) 20 | 21 | print("Listening for DIS on UDP socket {}".format(UDP_PORT)) 22 | 23 | gps = GPS(); 24 | 25 | def recv(): 26 | data = udpSocket.recv(1024) # buffer size in bytes 27 | pdu = createPdu(data); 28 | pduTypeName = pdu.__class__.__name__ 29 | 30 | if pdu.pduType == 1: # PduTypeDecoders.EntityStatePdu: 31 | loc = (pdu.entityLocation.x, 32 | pdu.entityLocation.y, 33 | pdu.entityLocation.z, 34 | pdu.entityOrientation.psi, 35 | pdu.entityOrientation.theta, 36 | pdu.entityOrientation.phi 37 | ) 38 | 39 | body = gps.ecef2llarpy(*loc) 40 | 41 | print("Received {}\n".format(pduTypeName) 42 | + " Id : {}\n".format(pdu.entityID.entityID) 43 | + " Latitude : {:.2f} degrees\n".format(rad2deg(body[0])) 44 | + " Longitude : {:.2f} degrees\n".format(rad2deg(body[1])) 45 | + " Altitude : {:.0f} meters\n".format(body[2]) 46 | + " Yaw : {:.2f} degrees\n".format(rad2deg(body[3])) 47 | + " Pitch : {:.2f} degrees\n".format(rad2deg(body[4])) 48 | + " Roll : {:.2f} degrees\n".format(rad2deg(body[5])) 49 | ) 50 | else: 51 | print("Received {}, {} bytes".format(pduTypeName, len(data)), flush=True) 52 | 53 | 54 | while True: 55 | recv() 56 | -------------------------------------------------------------------------------- /examples/dis_sender.py: -------------------------------------------------------------------------------- 1 | #!python 2 | 3 | __author__ = "DMcG" 4 | __date__ = "$Jun 23, 2015 10:27:29 AM$" 5 | 6 | import socket 7 | import time 8 | 9 | from io import BytesIO 10 | 11 | from opendis.DataOutputStream import DataOutputStream 12 | from opendis.dis7 import EntityStatePdu 13 | from opendis.RangeCoordinates import * 14 | 15 | UDP_PORT = 3001 16 | DESTINATION_ADDRESS = "127.0.0.1" 17 | 18 | udpSocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 19 | udpSocket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) 20 | 21 | gps = GPS() # conversion helper 22 | 23 | def send(): 24 | pdu = EntityStatePdu() 25 | pdu.entityID.entityID = 42 26 | pdu.entityID.siteID = 17 27 | pdu.entityID.applicationID = 23 28 | pdu.marking.setString('Igor3d') 29 | 30 | # Entity in Monterey, CA, USA facing North, no roll or pitch 31 | montereyLocation = gps.llarpy2ecef(deg2rad(36.6), # longitude (radians) 32 | deg2rad(-121.9), # latitude (radians) 33 | 1, # altitude (meters) 34 | 0, # roll (radians) 35 | 0, # pitch (radians) 36 | 0 # yaw (radians) 37 | ) 38 | 39 | pdu.entityLocation.x = montereyLocation[0] 40 | pdu.entityLocation.y = montereyLocation[1] 41 | pdu.entityLocation.z = montereyLocation[2] 42 | pdu.entityOrientation.psi = montereyLocation[3] 43 | pdu.entityOrientation.theta = montereyLocation[4] 44 | pdu.entityOrientation.phi = montereyLocation[5] 45 | 46 | 47 | memoryStream = BytesIO() 48 | outputStream = DataOutputStream(memoryStream) 49 | pdu.serialize(outputStream) 50 | data = memoryStream.getvalue() 51 | 52 | while True: 53 | udpSocket.sendto(data, (DESTINATION_ADDRESS, UDP_PORT)) 54 | print("Sent {}. {} bytes".format(pdu.__class__.__name__, len(data))) 55 | time.sleep(60) 56 | 57 | send() 58 | -------------------------------------------------------------------------------- /opendis/DataInputStream.py: -------------------------------------------------------------------------------- 1 | """ 2 | Reading from Java DataInputStream format. 3 | From https://github.com/arngarden/python_java_datastream 4 | This uses big endian (network) format. 5 | """ 6 | 7 | from io import BufferedIOBase 8 | import struct 9 | 10 | from .types import ( 11 | int8, 12 | int16, 13 | int32, 14 | int64, 15 | uint8, 16 | uint16, 17 | uint32, 18 | float32, 19 | float64, 20 | char16, 21 | ) 22 | 23 | 24 | class DataInputStream: 25 | def __init__(self, stream: BufferedIOBase): 26 | self.stream = stream 27 | 28 | def read_boolean(self) -> bool: 29 | return struct.unpack('?', self.stream.read(1))[0] 30 | 31 | def read_byte(self) -> int8: 32 | return struct.unpack('b', self.stream.read(1))[0] 33 | 34 | def read_unsigned_byte(self) -> uint8: 35 | return struct.unpack('B', self.stream.read(1))[0] 36 | 37 | def read_char(self) -> char16: 38 | return chr(struct.unpack('>H', self.stream.read(2))[0]) 39 | 40 | def read_double(self) -> float64: 41 | return struct.unpack('>d', self.stream.read(8))[0] 42 | 43 | def read_float(self) -> float32: 44 | return struct.unpack('>f', self.stream.read(4))[0] 45 | 46 | def read_short(self) -> int16: 47 | return struct.unpack('>h', self.stream.read(2))[0] 48 | 49 | def read_unsigned_short(self) -> uint16: 50 | return struct.unpack('>H', self.stream.read(2))[0] 51 | 52 | def read_long(self) -> int64: 53 | return struct.unpack('>q', self.stream.read(8))[0] 54 | 55 | def read_utf(self) -> bytes: 56 | utf_length = struct.unpack('>H', self.stream.read(2))[0] 57 | return self.stream.read(utf_length) 58 | 59 | def read_int(self) -> int32: 60 | return struct.unpack('>i', self.stream.read(4))[0] 61 | 62 | def read_unsigned_int(self) -> uint32: 63 | return struct.unpack('>I', self.stream.read(4))[0] 64 | -------------------------------------------------------------------------------- /opendis/DataOutputStream.py: -------------------------------------------------------------------------------- 1 | """ 2 | Writing to Java DataInputStream format. 3 | From https://github.com/arngarden/python_java_datastream/blob/master/data_output_stream.py 4 | This uses big endian (network) format 5 | """ 6 | 7 | from io import BufferedIOBase 8 | import struct 9 | 10 | 11 | class DataOutputStream: 12 | def __init__(self, stream: BufferedIOBase): 13 | self.stream = stream 14 | 15 | def write_boolean(self, boolean: bool) -> None: 16 | self.stream.write(struct.pack('?', boolean)) 17 | 18 | def write_byte(self, val: bytes) -> None: 19 | self.stream.write(struct.pack('b', val)) 20 | 21 | def write_unsigned_byte(self, val: bytes) -> None: 22 | self.stream.write(struct.pack('B', val)) 23 | 24 | def write_char(self, val: str) -> None: 25 | self.stream.write(struct.pack('>H', ord(val))) 26 | 27 | def write_double(self, val: float) -> None: 28 | self.stream.write(struct.pack('>d', val)) 29 | 30 | def write_float(self, val: float) -> None: 31 | self.stream.write(struct.pack('>f', val)) 32 | 33 | def write_short(self, val: int) -> None: 34 | self.stream.write(struct.pack('>h', val)) 35 | 36 | def write_unsigned_short(self, val: int) -> None: 37 | self.stream.write(struct.pack('>H', val)) 38 | 39 | def write_long(self, val: int) -> None: 40 | self.stream.write(struct.pack('>q', val)) 41 | 42 | def write_utf(self, string: bytes) -> None: 43 | self.stream.write(struct.pack('>H', len(string))) 44 | self.stream.write(string) 45 | 46 | def write_int(self, val: int) -> None: 47 | self.stream.write(struct.pack('>i', val)) 48 | 49 | def write_unsigned_int(self, val: int) -> None: 50 | self.stream.write(struct.pack('>I', val)) 51 | 52 | -------------------------------------------------------------------------------- /opendis/PduFactory.py: -------------------------------------------------------------------------------- 1 | __author__ = "mcgredo" 2 | __date__ = "$Jun 25, 2015 11:31:42 AM$" 3 | 4 | from .DataInputStream import DataInputStream 5 | from .dis7 import * 6 | from io import BytesIO 7 | from os import PathLike 8 | import binascii 9 | import io 10 | 11 | PduTypeDecoders = { 12 | 1 : EntityStatePdu 13 | , 2 : FirePdu 14 | , 3 : DetonationPdu 15 | , 4 : CollisionPdu 16 | , 5 : ServiceRequestPdu 17 | , 6 : CollisionElasticPdu 18 | , 7 : ResupplyReceivedPdu 19 | , 9 : RepairCompletePdu 20 | , 10 : RepairResponsePdu 21 | , 11 : CreateEntityPdu 22 | , 12 : RemoveEntityPdu 23 | , 13 : StartResumePdu 24 | , 14 : StopFreezePdu 25 | , 15 : AcknowledgePdu 26 | , 16 : ActionRequestPdu 27 | , 17 : ActionResponsePdu 28 | , 18 : DataQueryPdu 29 | , 19 : SetDataPdu 30 | , 20 : DataPdu 31 | , 21 : EventReportPdu 32 | , 22 : CommentPdu 33 | , 23 : ElectromagneticEmissionsPdu 34 | , 24 : DesignatorPdu 35 | , 25 : TransmitterPdu 36 | , 26 : SignalPdu 37 | , 27 : ReceiverPdu 38 | , 29 : UaPdu 39 | , 31 : IntercomSignalPdu 40 | , 32 : IntercomControlPdu 41 | , 36 : IsPartOfPdu 42 | , 37 : MinefieldStatePdu 43 | , 40 : MinefieldResponseNackPdu 44 | , 41 : PointObjectStatePdu 45 | , 43 : PointObjectStatePdu 46 | , 44 : LinearObjectStatePdu 47 | , 45 : ArealObjectStatePdu 48 | , 51 : CreateEntityReliablePdu 49 | , 52 : RemoveEntityReliablePdu 50 | , 54 : StopFreezeReliablePdu 51 | , 55 : AcknowledgeReliablePdu 52 | , 56 : ActionRequestReliablePdu 53 | , 57 : ActionResponseReliablePdu 54 | , 58 : DataQueryReliablePdu 55 | , 59 : SetDataReliablePdu 56 | , 60 : DataReliablePdu 57 | , 61 : EventReportReliablePdu 58 | , 62 : CommentReliablePdu 59 | , 63 : RecordQueryReliablePdu 60 | , 66 : CollisionElasticPdu 61 | , 67 : EntityStateUpdatePdu 62 | , 69 : EntityDamageStatusPdu 63 | } 64 | 65 | 66 | def getPdu(inputStream: DataInputStream) -> PduSuperclass | None: 67 | inputStream.stream.seek(2, 0) # Skip ahead to PDU type enum field 68 | pduType = inputStream.read_byte() 69 | inputStream.stream.seek(0, 0) # Rewind to start 70 | 71 | if pduType in PduTypeDecoders.keys(): 72 | Decoder = PduTypeDecoders[pduType] 73 | pdu = Decoder() 74 | pdu.parse(inputStream) 75 | return pdu 76 | 77 | # Punt and return none if we don't have a match on anything 78 | # print("Unable to find a PDU corresponding to PduType {}".format(pduType)) 79 | 80 | return None 81 | 82 | 83 | def createPdu(data: bytes) -> PduSuperclass | None: 84 | """ Create a PDU of the correct type when passed an array of binary data 85 | input: a bytebuffer of DIS data 86 | output: a python DIS pdu instance of the correct class""" 87 | 88 | memoryStream = BytesIO(data) 89 | inputStream = DataInputStream(memoryStream) 90 | 91 | return getPdu(inputStream) 92 | 93 | 94 | def createPduFromFilePath(filePath: PathLike) -> PduSuperclass | None: 95 | """ Utility written for unit tests, but could have other uses too.""" 96 | with io.open(filePath, "rb") as f: 97 | inputStream = DataInputStream(f) 98 | pdu = getPdu(inputStream) 99 | return pdu 100 | -------------------------------------------------------------------------------- /opendis/RangeCoordinates.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python 2 | 3 | # Various coordinate system transform utilities. DIS uses 4 | # an Earth-Centered, Earth-Fixed coordinate system, with the 5 | # origin at the center of the (WGS84) earth, positive x out 6 | # at the equator and prime meridian, z out through the north 7 | # pole, and y out at the equator and 90 deg east. We often want 8 | # to convert those coordinates to latitude, longitude, and altitude 9 | # on the WGS84 globe. This utility does that. (It's swiped from 10 | # the net, specifically the stoqs project at MBARI) 11 | 12 | __author__ = "" 13 | __credits = "https://github.com/GAVLab/fhwa2_viz/blob/master/fhwa2_gui/src/util.py" 14 | 15 | """ 16 | Container for general GPS functions and classes 17 | 18 | Functions: 19 | deg2rad 20 | rad2deg 21 | euclideanDistance 22 | 23 | Classes: 24 | GPS - includes functions: 25 | ecef2lla 26 | ecef2llarpy 27 | ecef2ned 28 | ecef2pae 29 | ecef2utm 30 | ecefvec2nedvec 31 | lla2ecef 32 | lla2gcc 33 | lla2utm 34 | llarpy2ecef 35 | ned2ecef 36 | ned2pae 37 | rotate_3x3 38 | transpose 39 | utmLetterDesignator 40 | 41 | WGS84 - constant parameters for GPS class - includes function: 42 | g0 43 | """ 44 | #Import required packages 45 | from math import sqrt, pi, sin, cos, tan, atan, atan2, asin 46 | from numpy import array, dot, identity 47 | 48 | 49 | def deg2rad(deg: float): 50 | """Converts degrees to radians""" 51 | return deg * pi / 180 52 | 53 | 54 | def rad2deg(rad: float): 55 | """Converts radians to degrees""" 56 | return rad * 180 / pi 57 | 58 | 59 | def euclideanDistance(data, dataRef=None): 60 | """Calculates the Euclidian distance between the given data and zero. 61 | This works out to be equivalent to the distance between two points if their 62 | difference is given as the input""" 63 | total = 0 64 | for index in range(len(data)): 65 | if dataRef is None: 66 | total += data[index]**2 67 | else: 68 | total += (data[index] - dataRef[index])**2 69 | return sqrt(total) 70 | 71 | 72 | class WGS84: 73 | """General parameters defined by the WGS84 system""" 74 | 75 | # Semimajor axis length (m) 76 | a = 6378137.0 77 | 78 | # Semiminor axis length (m) 79 | b = 6356752.3142 80 | 81 | # Ellipsoid flatness (unitless) 82 | f = (a - b) / a 83 | 84 | # Eccentricity (unitless) 85 | e = sqrt(f * (2 - f)) 86 | 87 | # Speed of light (m/s) 88 | c = 299792458. 89 | 90 | # Relativistic constant 91 | F = -4.442807633e-10 92 | 93 | # Earth's universal gravitational constant 94 | mu = 3.986005e14 95 | 96 | # Earth rotation rate (rad/s) 97 | omega_ie = 7.2921151467e-5 98 | 99 | def g0(self, L): 100 | """ 101 | acceleration due to gravity at the ellipsoid surface at latitude L 102 | 103 | Note: this is not the WGS84 models for gravity at latitude, 104 | it is an approximation from another source. 105 | """ 106 | return 9.7803267715 * (1 + 0.001931851353 * sin(L)**2) / \ 107 | sqrt(1 - 0.0066943800229 * sin(L)**2) 108 | 109 | 110 | class GPS: 111 | """Working class for GPS module""" 112 | 113 | wgs84 = WGS84() 114 | 115 | def ecef2lla(self, ecef, tolerance=1e-9): 116 | """Convert Earth-centered, Earth-fixed coordinates to lat, lon, alt. 117 | Input: ecef - (x, y, z) in (m, m, m) 118 | Output: lla - (lat, lon, alt) in (decimal degrees, decimal degrees, m) 119 | """ 120 | # Decompose the input 121 | x = ecef[0] 122 | y = ecef[1] 123 | z = ecef[2] 124 | 125 | # Calculate lon 126 | lon = atan2(y, x) 127 | 128 | # Initialize the variables to calculate lat and alt 129 | alt = 0 130 | N = self.wgs84.a 131 | p = sqrt(x**2 + y**2) 132 | lat = 0 133 | previousLat = 90 134 | 135 | # Special case for poles (note: any longitude value is valid here) 136 | if x == 0 and y == 0: 137 | if z > 0: 138 | return (90.00, 69.00, round(z - self.wgs84.b, 2)) 139 | 140 | elif z < 0: 141 | return (-90.00, 69.00, round(-z - self.wgs84.b, 2)) 142 | 143 | else: 144 | raise Exception("The Exact Center of the Earth has no lat/lon") 145 | 146 | # Iterate until tolerance is reached 147 | while abs(lat - previousLat) >= tolerance: 148 | previousLat = lat 149 | sinLat = z / (N * (1 - self.wgs84.e**2) + alt) 150 | lat = atan((z + self.wgs84.e**2 * N * sinLat) / p) 151 | N = self.wgs84.a / sqrt(1 - (self.wgs84.e * sinLat)**2) 152 | alt = p / cos(lat) - N 153 | 154 | # Return the lla coordinates 155 | return (rad2deg(lat), rad2deg(lon), alt) 156 | 157 | 158 | def ecef2llarpy(self, X, Y, Z, psi, theta, phi): 159 | """ 160 | Convert an entities DIS standard ECEF coordinates and Euler angles into 161 | latitude, longitude, altitude, pitch, roll, and yaw. 162 | 163 | Inputs: 164 | X : entities ECEF X coordinate in meters 165 | Y : entities ECEF Y coordinate in meters 166 | Z : entities ECEF Z coordinate in meters 167 | psi : entities rotation around the ECEF Z axis 168 | theta : entities rotation around the ECEF Y axis 169 | phi : entities rotation around the ECEF X axis 170 | 171 | Output: 172 | (lat, lon, alt, roll, pitch, yaw) where: 173 | lat : latitude of the entity in radians 174 | lon : longitude of the entity in radians 175 | alt : altitude of the entity in meters 176 | roll : roll of the entity in radians 177 | pitch : pitch of the entity in radians 178 | yaw : yaw (heading) of the entity in radians 179 | """ 180 | 181 | # get lat, lon, alt from ECEF X, Y, Z 182 | lat, lon, alt = self.ecef2lla((X, Y, Z)) 183 | 184 | # Note: X = x0, Y = y0, Z = z0 185 | x0 = array([1, 0, 0]) 186 | y0 = array([0, 1, 0]) 187 | z0 = array([0, 0, 1]) 188 | 189 | # first rotation array, z0 by psi rads 190 | rot_array_1 = self.rotate_3x3(psi, z0) 191 | 192 | # rotate 193 | x1 = dot(rot_array_1, x0) 194 | y1 = dot(rot_array_1, y0) 195 | z1 = dot(rot_array_1, z0) 196 | 197 | # second rotation array, y1 by theta rads 198 | rot_array_2 = self.rotate_3x3(theta, y1) 199 | 200 | # rotate 201 | x2 = dot(rot_array_2, x1) 202 | y2 = dot(rot_array_2, y1) 203 | z2 = dot(rot_array_2, z1) 204 | 205 | # third rotation array, x2 by phi rads 206 | rot_array_3 = self.rotate_3x3(phi, x2) 207 | 208 | # rotate 209 | x3 = dot(rot_array_3, x2) 210 | y3 = dot(rot_array_3, y2) 211 | z3 = dot(rot_array_3, z2) 212 | 213 | # get the local NED unit vectors wrt the ECEF coordinate system 214 | a0 = self.ecefvec2nedvec(deg2rad(lat), deg2rad(lon)) 215 | X0 = a0[0] 216 | Y0 = a0[1] 217 | Z0 = a0[2] 218 | 219 | # calculate yaw and pitch 220 | yaw = atan2( dot(x3, Y0), dot(x3, X0) ) 221 | pitch = atan2( -dot(x3, Z0), sqrt( (dot(x3, X0)) ** 2 + (dot(x3, Y0)) **2 ) ) 222 | 223 | # calculate roll 224 | Y2 = dot(self.rotate_3x3(yaw, Z0), Y0) 225 | Z2 = dot(self.rotate_3x3(pitch, Y2), Z0) 226 | 227 | roll = atan2( dot(y3, Z2), dot(y3, Y2)) 228 | 229 | 230 | return (deg2rad(lat), deg2rad(lon), alt, roll, pitch, yaw) 231 | 232 | 233 | def ecef2ned(self, ecef, origin): 234 | """Converts ecef coordinates into local tangent plane where the 235 | origin is the origin in ecef coordinates. 236 | Input: ecef - (x, y, z) in (m, m, m) 237 | origin - (x0, y0, z0) in (m, m, m) 238 | Output: ned - (north, east, down) in (m, m, m) 239 | """ 240 | llaOrigin = self.ecef2lla(origin) 241 | lat = deg2rad(llaOrigin[0]) 242 | lon = deg2rad(llaOrigin[1]) 243 | Re2t = array([[-sin(lat)*cos(lon), -sin(lat)*sin(lon), cos(lat)], 244 | [-sin(lon), cos(lon), 0], 245 | [-cos(lat)*cos(lon), -cos(lat)*sin(lon), -sin(lat)]]) 246 | return list(dot(Re2t, array(ecef) - array(origin))) 247 | 248 | 249 | def ecef2pae(self, ecef, origin): 250 | """Converts the ecef coordinates into a tangent plane with the origin 251 | privided, returning the range, azimuth, and elevation angles. 252 | This is a convenience function combining ecef2ned and ned2pae. 253 | Input: ecef - (x, y, z) in (m, m, m) 254 | origin - (x0, y0, z0) in (m, m, m) 255 | Output: pae - (p, alpha, epsilon) in (m, degrees, degrees) 256 | """ 257 | ned = self.ecef2ned(ecef, origin) 258 | return self.ned2pae(ned) 259 | 260 | 261 | def ecef2utm(self, ecef): 262 | lla = self.ecef2lla(ecef) 263 | utm, info = self.lla2utm(lla) 264 | return utm, info 265 | 266 | 267 | def ecefvec2nedvec(self, lat, lon): 268 | """ 269 | Returns the rotation matrix to translate coordinate systems between 270 | ECEF and NED. 271 | 272 | Inputs: latitude and longitude in radians 273 | 274 | Outputs: the unit vector matrix of the NED coordinate system with 275 | respect to the ECEF coordinate system: 276 | 277 | np.array( [ [ N0, N1, N2], 278 | [ E0, E1, E2], 279 | [ D0, D1, D2] 280 | ] 281 | ) 282 | """ 283 | 284 | return array([[-sin(lat)*cos(lon), -sin(lat)*sin(lon), cos(lat) ], 285 | [-sin(lon) , cos(lon) , 0 ], 286 | [-cos(lat)*cos(lon), -cos(lat)*sin(lon), -sin(lat)] 287 | ] 288 | ) 289 | 290 | 291 | def lla2ecef(self, lla): 292 | """ 293 | Convert lat, lon, alt to Earth-centered, Earth-fixed coordinates. 294 | Input: lla - (lat, lon, alt) in (decimal degrees, decimal degees, m) 295 | Output: ecef - (x, y, z) in (m, m, m) 296 | """ 297 | # Decompose the input 298 | lat = deg2rad(lla[0]) 299 | lon = deg2rad(lla[1]) 300 | alt = lla[2] 301 | 302 | # Calculate length of the normal to the ellipsoid 303 | N = self.wgs84.a / sqrt(1 - (self.wgs84.e * sin(lat))**2) 304 | 305 | # Calculate ecef coordinates 306 | x = (N + alt) * cos(lat) * cos(lon) 307 | y = (N + alt) * cos(lat) * sin(lon) 308 | z = (N * (1 - self.wgs84.e**2) + alt) * sin(lat) 309 | 310 | # Return the ecef coordinates 311 | return (x, y, z) 312 | 313 | 314 | def lla2gcc(self, lla, geoOrigin=''): 315 | """ 316 | Same as lls2ecef, but accepts an X3D-style geoOrigin string for 317 | subtraction of it in ecef (gcc) cooridinates 318 | """ 319 | if geoOrigin: 320 | lon0, lat0, a0 = [float(c) for c in geoOrigin.split()] 321 | x0, y0, z0 = self.lla2ecef((lat0, lon0, a0)) 322 | else: 323 | x0, y0, z0 = 0, 0, 0 324 | 325 | x, y, z = self.lla2ecef(lla) 326 | 327 | return (x - x0, y - y0, z -z0) 328 | 329 | 330 | def lla2utm(self, lla): 331 | """Converts lat, lon, alt to Universal Transverse Mercator coordinates 332 | Input: lla - (lat, lon, alt) in (decimal degrees, decimal degrees, m) 333 | Output: utm - (easting, northing, upping) in (m, m, m) 334 | info - (zone, scale factor) 335 | Algorithm from: 336 | Snyder, J. P., Map Projections-A Working Manual, U.S. Geol. Surv. 337 | Prof. Pap., 1395, 1987 338 | Code segments from pygps project, Russ Nelson""" 339 | #Decompose lla 340 | lat = lla[0] 341 | lon = lla[1] 342 | alt = lla[2] 343 | #Determine the zone number 344 | zoneNumber = int((lon+180.)/6) + 1 345 | #Special zone for Norway 346 | if (56. <= lat < 64.) and (3. <= lon < 12.): 347 | zoneNumber = 32 348 | #Special zones for Svalbard 349 | if 72. <= lat < 84.: 350 | if 0. <= lon < 9.: zoneNumber = 31 351 | elif 9. <= lon < 21.: zoneNumber = 33 352 | elif 21. <= lon < 33.: zoneNumber = 35 353 | elif 33. <= lon < 42.: zoneNumber = 37 354 | #Format the zone 355 | zone = "%d%c" % (zoneNumber, self.utmLetterDesignator(lat)) 356 | #Determine longitude origin 357 | lonOrigin = (zoneNumber - 1) * 6 - 180 + 3 358 | #Convert to radians 359 | latRad = deg2rad(lat) 360 | lonRad = deg2rad(lon) 361 | lonOriginRad = deg2rad(lonOrigin) 362 | #Conversion constants 363 | k0 = 0.9996 364 | eSquared = self.wgs84.e**2 365 | ePrimeSquared = eSquared/(1.-eSquared) 366 | N = self.wgs84.a/sqrt(1.-eSquared*sin(latRad)**2) 367 | T = tan(latRad)**2 368 | C = ePrimeSquared*cos(latRad)**2 369 | A = (lonRad - lonOriginRad)*cos(latRad) 370 | M = self.wgs84.a*( \ 371 | (1. - \ 372 | eSquared/4. - \ 373 | 3.*eSquared**2/64. - \ 374 | 5.*eSquared**3/256)*latRad - \ 375 | (3.*eSquared/8. + \ 376 | 3.*eSquared**2/32. + \ 377 | 45.*eSquared**3/1024.)*sin(2.*latRad) + \ 378 | (15.*eSquared**2/256. + \ 379 | 45.*eSquared**3/1024.)*sin(4.*latRad) - \ 380 | (35.*eSquared**3/3072.)*sin(6.*latRad)) 381 | M0 = 0. 382 | #Calculate coordinates 383 | x = k0*N*( \ 384 | A+(1-T+C)*A**3/6. + \ 385 | (5.-18.*T+T**2+72.*C-58.*ePrimeSquared)*A**5/120.) + 500000. 386 | y = k0*( \ 387 | M-M0+N*tan(latRad)*( \ 388 | A**2/2. + \ 389 | (5.-T+9.*C+4.*C**2)*A**4/24. + \ 390 | (61.-58.*T+T**2+600.*C-330.*ePrimeSquared)*A**6/720.)) 391 | #Calculate scale factor 392 | k = k0*(1 + \ 393 | (1+C)*A**2/2. + \ 394 | (5.-4.*T+42.*C+13.*C**2-28.*ePrimeSquared)*A**4/24. + \ 395 | (61.-148.*T+16.*T**2)*A**6/720.) 396 | utm = [x, y, alt] 397 | info = [zone, k] 398 | return utm, info 399 | 400 | 401 | def llarpy2ecef(self, lat, lon, alt, roll, pitch, yaw): 402 | """ 403 | Convert an entities latitude, longitude, altitude, pitch, roll, and yaw 404 | into DIS standard ECEF coordinates and Euler angles. 405 | 406 | Inputs: 407 | lat : latitude of the entity in radians 408 | lon : longitude of the entity in radians 409 | alt : altitude of the entity in meters 410 | roll : roll of the entity in radians 411 | pitch : pitch of the entity in radians 412 | yaw : yaw (heading) of the entity in radians 413 | 414 | Output: 415 | (X, Y, Z, psi, theta, phi) where: 416 | X : entities ECEF X coordinate in meters 417 | Y : entities ECEF Y coordinate in meters 418 | Z : entities ECEF Z coordinate in meters 419 | psi : entities rotation around the ECEF Z axis 420 | theta : entities rotation around the ECEF Y axis 421 | phi : entities rotation around the ECEF X axis 422 | """ 423 | 424 | # get ECEF X, Y, Z from lat, lon, alt 425 | X, Y, Z = self.lla2ecef((rad2deg(lat), rad2deg(lon), alt)) 426 | 427 | # get the local NED unit vectors wrt the ECEF coordinate system 428 | a0 = self.ecefvec2nedvec(lat, lon) 429 | 430 | # Note: N = x0, E = y0, D = z0 431 | x0 = a0[0] 432 | y0 = a0[1] 433 | z0 = a0[2] 434 | 435 | # first rotation array, z0 by yaw rads 436 | rot_array_1 = self.rotate_3x3(yaw, z0) 437 | 438 | # rotate 439 | x1 = dot(rot_array_1, x0) 440 | y1 = dot(rot_array_1, y0) 441 | z1 = dot(rot_array_1, z0) 442 | 443 | # second rotation array, y1 by pitch rads 444 | rot_array_2 = self.rotate_3x3(pitch, y1) 445 | 446 | # rotate 447 | x2 = dot(rot_array_2, x1) 448 | y2 = dot(rot_array_2, y1) 449 | z2 = dot(rot_array_2, z1) 450 | 451 | # third rotation array, x2 by roll rads 452 | rot_array_3 = self.rotate_3x3(roll, x2) 453 | 454 | # rotate 455 | x3 = dot(rot_array_3, x2) 456 | y3 = dot(rot_array_3, y2) 457 | z3 = dot(rot_array_3, z2) 458 | 459 | # define ECEF unit vectors 460 | X0 = array([1, 0, 0]) 461 | Y0 = array([0, 1, 0]) 462 | Z0 = array([0, 0, 1]) 463 | 464 | # calculate psi and theta 465 | psi = atan2( dot(x3, Y0), dot(x3, X0) ) 466 | theta = atan2( -dot(x3, Z0), sqrt( (dot(x3, X0)) ** 2 + (dot(x3, Y0)) **2 ) ) 467 | 468 | # calculate phi 469 | Y2 = dot(self.rotate_3x3(psi, Z0), Y0) 470 | Z2 = dot(self.rotate_3x3(theta, Y2), Z0) 471 | 472 | phi = atan2( dot(y3, Z2), dot(y3, Y2)) 473 | 474 | return (X, Y, Z, psi, theta, phi) 475 | 476 | 477 | def ned2ecef(self, ned, origin): 478 | """Converts ned local tangent plane coordinates into ecef coordinates 479 | using origin as the ecef point of tangency. 480 | Input: ned - (north, east, down) in (m, m, m) 481 | origin - (x0, y0, z0) in (m, m, m) 482 | Output: ecef - (x, y, z) in (m, m, m) 483 | """ 484 | llaOrigin = self.ecef2lla(origin) 485 | lat = deg2rad(llaOrigin[0]) 486 | lon = deg2rad(llaOrigin[1]) 487 | Rt2e = array([[-sin(lat)*cos(lon), -sin(lon), -cos(lat)*cos(lon)], 488 | [-sin(lat)*sin(lon), cos(lon), -cos(lat)*sin(lon)], 489 | [cos(lat), 0., -sin(lat)]]) 490 | return list(dot(Rt2e, array(ned)) + array(origin)) 491 | 492 | def ned2pae(self, ned): 493 | """Converts the local north, east, down coordinates into range, azimuth, 494 | and elevation angles 495 | Input: ned - (north, east, down) in (m, m, m) 496 | Output: pae - (p, alpha, epsilon) in (m, degrees, degrees) 497 | """ 498 | p = euclideanDistance(ned) 499 | alpha = atan2(ned[1], ned[0]) 500 | epsilon = atan2(-ned[2], sqrt(ned[0]**2 + ned[1]**2)) 501 | return [p, rad2deg(alpha), rad2deg(epsilon)] 502 | 503 | 504 | def rotate_3x3(self, theta, normal_vec): 505 | """ 506 | Returns a rotation matrix to rotate a 3x3 matrix theta degrees around 507 | normal_vec axis of rotation. Theta is in rads and normal vec is a 3x1 508 | matrix pointing along the axis of rotation. 509 | 510 | Example: get the rotation matrix to rotate a vector 30 degrees around the 511 | z-axis: 512 | 513 | rot_mat = rotate_3x3(30 * pi / 180, np.array([[0], [0], [1]])) 514 | 515 | Note: dot product a vector with the result of this function to perform 516 | the rotation. 517 | """ 518 | 519 | n_v_t = normal_vec.transpose() 520 | 521 | n_x = array([[ 0 , -normal_vec[2], normal_vec[1] ], 522 | [normal_vec[2] , 0 , -normal_vec[0]], 523 | [-normal_vec[1], normal_vec[0] , 0 ] 524 | ] 525 | ) 526 | 527 | 528 | I = identity(3) 529 | 530 | nnt = normal_vec * self.transpose(normal_vec) 531 | 532 | return (1 - cos(theta)) * nnt + cos(theta) * I + sin(theta) * n_x 533 | 534 | 535 | def transpose(self, arr): 536 | """ 537 | numpy doesn't want to transpose a 3x1 vector into a 1x3 vector. So 538 | we'll do it ourselves. 539 | """ 540 | 541 | return array( [[arr[0]], [arr[1]], [arr[2]]]) 542 | 543 | 544 | def utmLetterDesignator(self, lat): 545 | """Returns the latitude zone of the UTM coordinates""" 546 | if -80 <= lat < -72: return 'C' 547 | elif -72 <= lat < -64: return 'D' 548 | elif -64 <= lat < -56: return 'E' 549 | elif -56 <= lat < -48: return 'F' 550 | elif -48 <= lat < -40: return 'G' 551 | elif -40 <= lat < -32: return 'H' 552 | elif -32 <= lat < -24: return 'J' 553 | elif -24 <= lat < -16: return 'K' 554 | elif -16 <= lat < -8: return 'L' 555 | elif -8 <= lat < 0: return 'M' 556 | elif 0 <= lat < 8: return 'N' 557 | elif 8 <= lat < 16: return 'P' 558 | elif 16 <= lat < 24: return 'Q' 559 | elif 24 <= lat < 32: return 'R' 560 | elif 32 <= lat < 40: return 'S' 561 | elif 40 <= lat < 48: return 'T' 562 | elif 48 <= lat < 56: return 'U' 563 | elif 56 <= lat < 64: return 'V' 564 | elif 64 <= lat < 72: return 'W' 565 | elif 72 <= lat < 80: return 'X' 566 | else: return 'Z' 567 | 568 | 569 | if __name__ == "__main__": 570 | wgs84 = WGS84() 571 | gps = GPS() 572 | lla = (34. + 0/60. + 0.00174/3600., 573 | -117. - 20./60. - 0.84965/3600., 574 | 251.702) 575 | print("lla: {}".format(lla)) 576 | ecef = gps.lla2ecef(lla) 577 | print("ecef: {}".format(ecef)) 578 | print("lla: {}".format(gps.ecef2lla(ecef))) 579 | -------------------------------------------------------------------------------- /opendis/__init__.py: -------------------------------------------------------------------------------- 1 | # Marker for a python package directory 2 | name = "opendis" 3 | -------------------------------------------------------------------------------- /opendis/types.py: -------------------------------------------------------------------------------- 1 | """types.py 2 | 3 | This module contains type aliases to document the size of attributes required/ 4 | used by classes in dis7.py and elsewhere. It should not import other modules in 5 | the opendis package. 6 | 7 | Note: if imported directly, this may shadow the `types` built-in Python module, 8 | which is rarely used. 9 | """ 10 | 11 | # Type aliases (for readability) 12 | enum8 = int 13 | enum16 = int 14 | enum32 = int 15 | int8 = int 16 | int16 = int 17 | int32 = int 18 | int64 = int 19 | uint8 = int 20 | uint16 = int 21 | uint32 = int 22 | uint64 = int 23 | float32 = float 24 | float64 = float 25 | struct8 = bytes 26 | struct16 = bytes 27 | struct32 = bytes 28 | char8 = str 29 | char16 = str 30 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "numpy" 5 | version = "1.26.4" 6 | description = "Fundamental package for array computing in Python" 7 | optional = false 8 | python-versions = ">=3.9" 9 | files = [ 10 | {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, 11 | {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, 12 | {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, 13 | {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, 14 | {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, 15 | {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, 16 | {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, 17 | {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, 18 | {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, 19 | {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, 20 | {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, 21 | {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, 22 | {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, 23 | {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, 24 | {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, 25 | {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, 26 | {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, 27 | {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, 28 | {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, 29 | {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, 30 | {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, 31 | {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, 32 | {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, 33 | {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, 34 | {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"}, 35 | {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"}, 36 | {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"}, 37 | {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"}, 38 | {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"}, 39 | {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"}, 40 | {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"}, 41 | {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"}, 42 | {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, 43 | {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, 44 | {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, 45 | {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, 46 | ] 47 | 48 | [metadata] 49 | lock-version = "2.0" 50 | python-versions = "^3.9" 51 | content-hash = "73104148745e647961282501b5fbe50c548b7829b9369051b6f32c8ba02cad44" 52 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "opendis" 3 | version = "1.0" 4 | description='implementation of DIS, IEEE-1278.1' 5 | authors = ["Don McGregor "] 6 | readme = "README.md" 7 | repository = 'https://github.com/open-dis/open-dis-python' 8 | documentation = "https://open-dis.org/" 9 | classifiers=[ 10 | "Programming Language :: Python :: 3", 11 | "License :: OSI Approved :: BSD License", 12 | "Operating System :: OS Independent", 13 | "Topic :: Scientific/Engineering :: Interface Engine/Protocol Translator" 14 | ] 15 | 16 | [tool.poetry.dependencies] 17 | python = "^3.9" 18 | numpy = "^1.26.4" 19 | 20 | 21 | [build-system] 22 | requires = ["poetry-core"] 23 | build-backend = "poetry.core.masonry.api" 24 | -------------------------------------------------------------------------------- /tests/ElectromagneticEmissionPdu-single-system.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-dis/open-dis-python/bf95104a45b3def1eb7466c436178b1684117b04/tests/ElectromagneticEmissionPdu-single-system.raw -------------------------------------------------------------------------------- /tests/EntityStatePdu-26.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-dis/open-dis-python/bf95104a45b3def1eb7466c436178b1684117b04/tests/EntityStatePdu-26.raw -------------------------------------------------------------------------------- /tests/SetDataPdu-multi-variable-datums.raw: -------------------------------------------------------------------------------- 1 | phversionNumberhversionNumberhversionNumber -------------------------------------------------------------------------------- /tests/SetDataPdu-vbs-script-cmd.raw: -------------------------------------------------------------------------------- 1 | 8 @allunits -------------------------------------------------------------------------------- /tests/SignalPdu.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-dis/open-dis-python/bf95104a45b3def1eb7466c436178b1684117b04/tests/SignalPdu.raw -------------------------------------------------------------------------------- /tests/TransmitterPdu.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-dis/open-dis-python/bf95104a45b3def1eb7466c436178b1684117b04/tests/TransmitterPdu.raw -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-dis/open-dis-python/bf95104a45b3def1eb7466c436178b1684117b04/tests/__init__.py -------------------------------------------------------------------------------- /tests/testElectromageneticEmissionPdu.py: -------------------------------------------------------------------------------- 1 | #!python 2 | 3 | import unittest 4 | import io 5 | import os 6 | 7 | from opendis.dis7 import * 8 | from opendis.PduFactory import * 9 | 10 | class TestElectromagneticEmissionPdu(unittest.TestCase): 11 | 12 | def setUp(self): 13 | testdir = os.path.dirname(os.path.abspath(__file__)) 14 | os.chdir(testdir) 15 | 16 | def test_parse(self): 17 | pdu = createPduFromFilePath("ElectromagneticEmissionPdu-single-system.raw") 18 | self.assertEqual(6, pdu.protocolVersion) 19 | self.assertEqual(1, pdu.exerciseID) 20 | self.assertEqual(23, pdu.pduType) 21 | self.assertEqual(6, pdu.protocolFamily) 22 | #self.assertEqual(0, pdu.timestamp) 23 | self.assertEqual(108, pdu.length) 24 | 25 | self.assertEqual(23, pdu.emittingEntityID.siteID) 26 | self.assertEqual(1, pdu.emittingEntityID.applicationID) 27 | self.assertEqual(2, pdu.emittingEntityID.entityID) 28 | 29 | self.assertEqual(23, pdu.eventID.simulationAddress.site) 30 | self.assertEqual(1, pdu.eventID.simulationAddress.application) 31 | self.assertEqual(8, pdu.eventID.eventNumber) 32 | 33 | self.assertEqual(0, pdu.stateUpdateIndicator) 34 | self.assertEqual(1, pdu.numberOfSystems) 35 | self.assertEqual(0, pdu.paddingForEmissionsPdu) 36 | self.assertEqual(1, pdu.systems[0].numberOfBeams) 37 | self.assertEqual(0, pdu.systems[0].location.x) 38 | self.assertEqual(0, pdu.systems[0].location.y) 39 | self.assertEqual(0, pdu.systems[0].location.z) 40 | self.assertEqual(15, pdu.systems[0].beamRecords[0].beamDataLength) 41 | self.assertEqual(1, pdu.systems[0].beamRecords[0].beamIDNumber) 42 | self.assertEqual(0, pdu.systems[0].beamRecords[0].beamParameterIndex) 43 | #self.assertAlmostEqual(9000000000, pdu.systems[0].fundamentalParameterData.frequency) 44 | self.assertEqual(0, pdu.systems[0].beamRecords[0].fundamentalParameterData.frequencyRange) 45 | self.assertEqual(70000, pdu.systems[0].beamRecords[0].fundamentalParameterData.effectiveRadiatedPower) 46 | self.assertEqual(0, pdu.systems[0].beamRecords[0].fundamentalParameterData.pulseRepetitionFrequency) 47 | self.assertEqual(0, pdu.systems[0].beamRecords[0].fundamentalParameterData.pulseWidth) 48 | self.assertEqual(0, pdu.systems[0].beamRecords[0].fundamentalParameterData.beamAzimuthCenter) 49 | #self.assertAlmostEqual(3.14159, pdu.systems[0].fundamentalParameterData.beamAzimuthSweep) 50 | #self.assertEqual(0.741765, pdu.systems[0].fundamentalParameterData.beamElevationCenter ) 51 | #self.assertEqual(0.829031, pdu.systems[0].fundamentalParameterData.beamElevationSweep) 52 | self.assertEqual(1, pdu.systems[0].beamRecords[0].fundamentalParameterData.beamSweepSync) 53 | self.assertEqual(4, pdu.systems[0].beamRecords[0].beamFunction) 54 | self.assertEqual(1, pdu.systems[0].beamRecords[0].numberOfTargetsInTrackJam) 55 | self.assertEqual(0, pdu.systems[0].beamRecords[0].highDensityTrackJam) 56 | self.assertEqual(0, pdu.systems[0].beamRecords[0].jammingModeSequence) 57 | 58 | self.assertEqual(23, pdu.systems[0].beamRecords[0].trackJamRecords[0].entityID.siteID) 59 | self.assertEqual(1, pdu.systems[0].beamRecords[0].trackJamRecords[0].entityID.applicationID) 60 | self.assertEqual(1, pdu.systems[0].beamRecords[0].trackJamRecords[0].entityID.entityID) 61 | self.assertEqual(0, pdu.systems[0].beamRecords[0].trackJamRecords[0].emitterNumber) 62 | self.assertEqual(0, pdu.systems[0].beamRecords[0].trackJamRecords[0].beamNumber) 63 | 64 | 65 | if __name__ == '__main__': 66 | unittest.main() 67 | -------------------------------------------------------------------------------- /tests/testEntityStatePdu.py: -------------------------------------------------------------------------------- 1 | #!python 2 | 3 | import unittest 4 | import io 5 | import os 6 | 7 | from opendis.dis7 import * 8 | from opendis.PduFactory import * 9 | 10 | class TestEntityStatePdu(unittest.TestCase): 11 | 12 | def setUp(self): 13 | testdir = os.path.dirname(os.path.abspath(__file__)) 14 | os.chdir(testdir) 15 | 16 | def test_parse(self): 17 | pdu = createPduFromFilePath("EntityStatePdu-26.raw") 18 | self.assertEqual(6, pdu.protocolVersion) 19 | self.assertEqual(7, pdu.exerciseID) 20 | self.assertEqual(1, pdu.pduType) 21 | self.assertEqual(1, pdu.protocolFamily) 22 | #self.assertEqual(0, pdu.timestamp) 23 | self.assertEqual(144, pdu.length) 24 | self.assertEqual(0, pdu.padding) 25 | 26 | # Entity ID 27 | self.assertEqual(42, pdu.entityID.siteID) 28 | self.assertEqual(4, pdu.entityID.applicationID) 29 | self.assertEqual(26, pdu.entityID.entityID) 30 | 31 | # Force ID 32 | self.assertEqual(1, pdu.forceId) 33 | 34 | # Articulation Parameters 35 | self.assertEqual(0, pdu.numberOfVariableParameters) 36 | 37 | # Entity Type (aka DIS Enumeration) 38 | self.assertEqual(1, pdu.entityType.entityKind) 39 | self.assertEqual(1, pdu.entityType.domain) 40 | self.assertEqual(39, pdu.entityType.country) 41 | self.assertEqual(7, pdu.entityType.category) 42 | self.assertEqual(2, pdu.entityType.subcategory) 43 | self.assertEqual(1, pdu.entityType.specific) 44 | self.assertEqual(0, pdu.entityType.extra) 45 | 46 | # Alternative Entity Type 47 | self.assertEqual(1, pdu.alternativeEntityType.entityKind) 48 | self.assertEqual(1, pdu.alternativeEntityType.domain) 49 | self.assertEqual(39, pdu.alternativeEntityType.country) 50 | self.assertEqual(7, pdu.alternativeEntityType.category) 51 | self.assertEqual(2, pdu.alternativeEntityType.subcategory) 52 | self.assertEqual(1, pdu.alternativeEntityType.specific) 53 | self.assertEqual(0, pdu.alternativeEntityType.extra) 54 | 55 | # Entity Linear Velocity 56 | self.assertEqual(0, pdu.entityLinearVelocity.x, 0) 57 | self.assertEqual(0, pdu.entityLinearVelocity.y, 0) 58 | self.assertEqual(0, pdu.entityLinearVelocity.z, 0) 59 | 60 | # Entity Location 61 | self.assertAlmostEqual(4374082.80485589, pdu.entityLocation.x) 62 | self.assertAlmostEqual(1667679.95730107, pdu.entityLocation.y) 63 | self.assertAlmostEqual(4318284.36890269, pdu.entityLocation.z) 64 | 65 | # Entity Orientation 66 | self.assertAlmostEqual(1.93505, pdu.entityOrientation.psi, 5) 67 | self.assertAlmostEqual(0, pdu.entityOrientation.theta) 68 | self.assertAlmostEqual(-2.31924, pdu.entityOrientation.phi, 5) 69 | 70 | # Entity Appearance 71 | #self.assertEqual(0, pdu.entityAppearance_paintScheme) 72 | #self.assertEqual(0, pdu.entityAppearance_mobility) 73 | #self.assertEqual(0, pdu.entityAppearance_firepower) 74 | 75 | # Dead Reckoning Parameters 76 | # TODO self.assertEqual(???, pdu.deadReckoningParameters) 77 | 78 | # Entity Marking 79 | self.assertEqual("26",pdu.marking.charactersString()) 80 | 81 | 82 | if __name__ == '__main__': 83 | unittest.main() 84 | -------------------------------------------------------------------------------- /tests/testRangeCoordinates.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Unit tests for RangeCoordinates.py 5 | 6 | 7 | 8 | Tests the following functions: 9 | 10 | Test 1 : body_to_ecef_and_back which tests the following functions: 11 | * ecef2lla 12 | * ecef2llarpy 13 | * ecefvec2nedvec 14 | * lla2ecef 15 | * llarpy2ecef 16 | * rotate_3x3 17 | * transpose 18 | 19 | """ 20 | #----------------------------------------------------------------------------- 21 | # IMPORTS 22 | #----------------------------------------------------------------------------- 23 | import unittest as _unittest 24 | from random import randint 25 | from random import random 26 | from opendis.RangeCoordinates import * 27 | #----------------------------------------------------------------------------- 28 | # CONSTANTS 29 | #----------------------------------------------------------------------------- 30 | gps = GPS() 31 | wgs84 = WGS84() 32 | #----------------------------------------------------------------------------- 33 | # FUNCTIONS 34 | #----------------------------------------------------------------------------- 35 | 36 | #----------------------------------------------------------------------------- 37 | # CLASSES 38 | #----------------------------------------------------------------------------- 39 | class SimpleTest(_unittest.TestCase): 40 | 41 | # set up the test 42 | def setUp(self): 43 | pass 44 | 45 | # Test 1 : llarpy2ecef and ecef2llarpy 46 | def test01_body_ecef_conversions(self): 47 | """ 48 | Tests the following functions: 49 | ecef2lla 50 | ecef2llarpy 51 | ecefvec2nedvec 52 | lla2ecef 53 | llarpy2ecef 54 | rotate_3x3 55 | transpose 56 | 57 | First tests some simple cases and examples from the internet, then 58 | tests random values. 59 | """ 60 | 61 | def body_to_ecef_and_back(body, ecef): 62 | """ 63 | Verify llarpy2ecef() translates given body coordinates to given 64 | ecef coordinates. Verify ecef2llarpy() translates those coordinates 65 | back to the given body coordinates. 66 | 67 | Input: body = (lat, lon, alt, roll, pitch, yaw) 68 | ecef = (X, Y, Z, psi, theta, phi) 69 | 70 | Output: None 71 | """ 72 | 73 | # body to ECEF 74 | a = gps.llarpy2ecef(*body) 75 | 76 | # assert to nearest 2 decimal places 77 | self.assertAlmostEqual(a[0], ecef[0], 2) 78 | self.assertAlmostEqual(a[1], ecef[1], 2) 79 | self.assertAlmostEqual(a[2], ecef[2], 2) 80 | self.assertAlmostEqual(a[3], ecef[3], 2) 81 | self.assertAlmostEqual(a[4], ecef[4], 2) 82 | self.assertAlmostEqual(a[5], ecef[5], 2) 83 | 84 | # ECEF back to body 85 | b = gps.ecef2llarpy(*a) 86 | 87 | # assert to nearest 2 decimal places (altitude to nearest number) 88 | self.assertAlmostEqual(b[0], body[0], 2) 89 | self.assertAlmostEqual(b[1], body[1], 2) 90 | self.assertAlmostEqual(b[2], body[2], 0) 91 | self.assertAlmostEqual(b[3], body[3], 2) 92 | self.assertAlmostEqual(b[4], body[4], 2) 93 | self.assertAlmostEqual(b[5], body[5], 2) 94 | 95 | ###################################################################### 96 | # test ground vehicle at prime meridian and equator pointed north 97 | ###################################################################### 98 | # define body coordinates 99 | lat, lon, alt, roll, pitch, yaw = 0, 0, 0, 0, 0, 0 100 | 101 | # define ECEF coordinates 102 | X, Y, Z, psi, theta, phi = wgs84.a, 0, 0, 0, -pi/2, 0 103 | 104 | # test 105 | body_to_ecef_and_back((lat, lon, alt, roll, pitch, yaw), 106 | (X, Y, Z, psi, theta, phi) 107 | ) 108 | 109 | 110 | ###################################################################### 111 | # test ground vehicle at prime meridian and equator pointed south 112 | ###################################################################### 113 | # define body coordinates 114 | lat, lon, alt, roll, pitch, yaw = 0, 0, 0, 0, 0, deg2rad(180) 115 | 116 | # define ECEF coordinates 117 | X, Y, Z, psi, theta, phi = wgs84.a, 0, 0, pi/2, pi/2, -pi/2 118 | 119 | # test 120 | body_to_ecef_and_back((lat, lon, alt, roll, pitch, yaw), 121 | (X, Y, Z, psi, theta, phi) 122 | ) 123 | 124 | 125 | ###################################################################### 126 | # test ground vehicle at north pole pointed south @ 180 127 | ###################################################################### 128 | # define body coordinates 129 | lat, lon, alt, roll, pitch, yaw = deg2rad(90), 0, 0, 0, 0, deg2rad(180) 130 | 131 | # define ECEF coordinates 132 | X, Y, Z, psi, theta, phi = 0, 0, wgs84.b, 0, 0, -pi 133 | 134 | # test 135 | body_to_ecef_and_back((lat, lon, alt, roll, pitch, yaw), 136 | (X, Y, Z, psi, theta, phi) 137 | ) 138 | 139 | 140 | ###################################################################### 141 | # test aircraft flying 10000 m above Adeliaide, Australia heading SE, 142 | # climbing at 20 deg while holding a 30 deg roll 143 | ###################################################################### 144 | # define body coordinates 145 | lat = deg2rad(-34.9) 146 | lon = deg2rad(138.5) 147 | alt = 10000 148 | pitch = deg2rad(20) 149 | roll = deg2rad(30) 150 | yaw = deg2rad(135) 151 | 152 | # define ECEF coordinates 153 | X = -3928260.52 154 | Y = 3475431.33 155 | Z = -3634495.17 156 | psi = deg2rad(-122.97) 157 | theta = deg2rad(47.79) 158 | phi = deg2rad(-29.67) 159 | 160 | # test 161 | body_to_ecef_and_back((lat, lon, alt, roll, pitch, yaw), 162 | (X, Y, Z, psi, theta, phi) 163 | ) 164 | 165 | 166 | ###################################################################### 167 | # test body to ecef and back with random values in range: 168 | # latitude : -89.9999 to 89.9999 deg 169 | # longitude : -179.9999 to 179.9999 deg 170 | # altitude : -1000000 to 1000000 m 171 | # pitch : -89.9999 to 89.9999 deg 172 | # roll : -179.9999 to 179.9999 deg 173 | # yaw : -179.9999 to 179.9999 deg 174 | ###################################################################### 175 | for _ in range(10000): 176 | 177 | # define body coordinates 178 | lat = deg2rad(randint(-89, 89) + random()) 179 | lon = deg2rad(randint(-179, 179) + random()) 180 | alt = randint(-1000000, 1000000) 181 | pitch = deg2rad(randint(-89, 89) + random()) 182 | roll = deg2rad(randint(-179, 179) + random()) 183 | yaw = deg2rad(randint(-179, 179) + random()) 184 | 185 | # body to ECEF 186 | a = gps.llarpy2ecef(lat, lon, alt, roll, pitch, yaw) 187 | 188 | # ECEF back to body 189 | b = gps.ecef2llarpy(*a) 190 | 191 | # assert to nearest 2 decimal places (altitude within 5 m) 192 | self.assertAlmostEqual(b[0], lat, 2) 193 | self.assertAlmostEqual(b[1], lon, 2) 194 | self.assertAlmostEqual(b[2], alt, delta=5) 195 | self.assertAlmostEqual(b[3], roll, 2) 196 | self.assertAlmostEqual(b[4], pitch, 2) 197 | self.assertAlmostEqual(b[5], yaw, 2) 198 | 199 | ###################################################################### 200 | # test ecef to body and back with random values in range: 201 | # X : -7500000 to 7500000 m 202 | # Y : -7500000 to 7500000 m 203 | # Z : -7500000 to 7500000 m 204 | # psi : -179.9999 to 179.9999 deg 205 | # theta : -89.9999 to 89.9999 deg 206 | # phi : -179.9999 to 179.9999 deg 207 | ###################################################################### 208 | for _ in range(10000): 209 | 210 | # define body coordinates 211 | X = randint(-7500000, 7500000) 212 | Y = randint(-7500000, 7500000) 213 | Z = randint(-7500000, 7500000) 214 | psi = deg2rad(randint(-179, 179) + random()) 215 | theta = deg2rad(randint(-89, 89) + random()) 216 | phi = deg2rad(randint(-179, 179) + random()) 217 | 218 | # body to ECEF 219 | a = gps.ecef2llarpy(X, Y, Z, psi, theta, phi) 220 | 221 | # ECEF back to body 222 | b = gps.llarpy2ecef(*a) 223 | 224 | # assert location to nearest decimal place and Euler angles to 225 | # nearest 2 226 | self.assertAlmostEqual(b[0], X, 0) 227 | self.assertAlmostEqual(b[1], Y, 0) 228 | self.assertAlmostEqual(b[2], Z, 0) 229 | self.assertAlmostEqual(b[3], psi, 2) 230 | self.assertAlmostEqual(b[4], theta, 2) 231 | self.assertAlmostEqual(b[5], phi, 2) 232 | 233 | 234 | #----------------------------------------------------------------------------- 235 | # DO THE THING 236 | #----------------------------------------------------------------------------- 237 | if __name__ == "__main__": 238 | _unittest.main() 239 | #----------------------------------------------------------------------------- 240 | -------------------------------------------------------------------------------- /tests/testSetDataPdu.py: -------------------------------------------------------------------------------- 1 | #!python 2 | 3 | import unittest 4 | import io 5 | import os 6 | 7 | from opendis.dis7 import * 8 | from opendis.PduFactory import * 9 | 10 | class TestSetDataPdu(unittest.TestCase): 11 | 12 | def setUp(self): 13 | testdir = os.path.dirname(os.path.abspath(__file__)) 14 | os.chdir(testdir) 15 | 16 | def test_parse(self): 17 | pdu = createPduFromFilePath("SetDataPdu-vbs-script-cmd.raw") 18 | self.assertEqual(6, pdu.protocolVersion) 19 | self.assertEqual(1, pdu.exerciseID) 20 | self.assertEqual(19, pdu.pduType) 21 | self.assertEqual(5, pdu.protocolFamily) 22 | self.assertEqual(0, pdu.timestamp) 23 | self.assertEqual(56, pdu.length) 24 | 25 | self.assertEqual(0, pdu.numberOfFixedDatumRecords) 26 | self.assertEqual(1, pdu.numberOfVariableDatumRecords) 27 | self.assertEqual(0, len(pdu.fixedDatumRecords)) 28 | self.assertEqual(1, len(pdu.variableDatumRecords)) 29 | 30 | datum = pdu.variableDatumRecords[0] 31 | self.assertEqual(1, datum.variableDatumID) 32 | self.assertEqual(64, datum.variableDatumLength) 33 | self.assertEqual(b'allunits', bytes(datum.variableData)) 34 | 35 | def test_parse_multi_variable_datums(self): 36 | pdu = createPduFromFilePath("SetDataPdu-multi-variable-datums.raw") 37 | self.assertEqual(112, pdu.length) 38 | 39 | self.assertEqual(3, pdu.numberOfVariableDatumRecords) 40 | self.assertEqual(3, len(pdu.variableDatumRecords)) 41 | 42 | 43 | if __name__ == '__main__': 44 | unittest.main() 45 | -------------------------------------------------------------------------------- /tests/testSignalPdu.py: -------------------------------------------------------------------------------- 1 | #!python 2 | 3 | import unittest 4 | import io 5 | import os 6 | 7 | from opendis.dis7 import * 8 | from opendis.PduFactory import * 9 | from opendis.DataOutputStream import DataOutputStream 10 | 11 | class TestSignalPdu(unittest.TestCase): 12 | 13 | def setUp(self): 14 | testdir = os.path.dirname(os.path.abspath(__file__)) 15 | os.chdir(testdir) 16 | 17 | def test_parse_and_serialize(self): 18 | with open('SignalPdu.raw', 'rb') as f: 19 | data = f.read() 20 | pdu = createPdu(data) 21 | self.assertEqual(6, pdu.protocolVersion) 22 | self.assertEqual(1, pdu.exerciseID) 23 | self.assertEqual(26, pdu.pduType) 24 | self.assertEqual(4, pdu.protocolFamily) 25 | #self.assertEqual(0, pdu.timestamp) 26 | self.assertEqual(1056, pdu.length) 27 | 28 | self.assertEqual(1677, pdu.entityID.siteID) 29 | self.assertEqual(1678, pdu.entityID.applicationID) 30 | self.assertEqual(169, pdu.entityID.entityID ) 31 | self.assertEqual(1, pdu.radioID) 32 | self.assertEqual(4, pdu.encodingScheme) 33 | self.assertEqual(0, pdu.tdlType) 34 | self.assertEqual(22050, pdu.sampleRate) 35 | self.assertEqual(8192, pdu.dataLength) 36 | self.assertEqual(512, pdu.samples) 37 | self.assertEqual(8192/8, len(pdu.data)) 38 | 39 | memoryStream = io.BytesIO() 40 | outputStream = DataOutputStream(memoryStream) 41 | pdu.serialize(outputStream) 42 | 43 | self.assertEqual(data, memoryStream.getvalue()) 44 | 45 | 46 | if __name__ == '__main__': 47 | unittest.main() 48 | -------------------------------------------------------------------------------- /tests/testTransmitterPdu.py: -------------------------------------------------------------------------------- 1 | #!python 2 | 3 | import unittest 4 | import io 5 | import os 6 | 7 | from opendis.dis7 import * 8 | from opendis.PduFactory import * 9 | 10 | class TestTransmitterPdu(unittest.TestCase): 11 | 12 | def setUp(self): 13 | testdir = os.path.dirname(os.path.abspath(__file__)) 14 | os.chdir(testdir) 15 | 16 | def test_parse(self): 17 | pdu = createPduFromFilePath("TransmitterPdu.raw") 18 | self.assertEqual(6, pdu.protocolVersion) 19 | self.assertEqual(1, pdu.exerciseID) 20 | self.assertEqual(25, pdu.pduType) 21 | self.assertEqual(4, pdu.protocolFamily) 22 | #self.assertEqual(0, pdu.timestamp) 23 | self.assertEqual(104, pdu.length) 24 | 25 | self.assertEqual(1677, pdu.radioReferenceID.siteID) 26 | self.assertEqual(1678, pdu.radioReferenceID.applicationID) 27 | self.assertEqual(169, pdu.radioReferenceID.entityID ) 28 | self.assertEqual(1, pdu.radioNumber) 29 | self.assertEqual(2, pdu.transmitState) 30 | self.assertEqual(10000000000, pdu.frequency) 31 | self.assertEqual(20000, pdu.transmitFrequencyBandwidth) 32 | self.assertEqual(35, pdu.power) 33 | 34 | if __name__ == '__main__': 35 | unittest.main() 36 | --------------------------------------------------------------------------------