From 5a56fe997b1cec937ff873aabadb33f61a93604f Mon Sep 17 00:00:00 2001 From: Ricardo Salgado Date: Fri, 20 Nov 2020 19:04:47 +0000 Subject: [PATCH 1/4] Added support for MISB0903 elements. Made numerous changes to support new base element types. --- klvdata/__init__.py | 3 +- klvdata/common.py | 44 ++++++-- klvdata/element.py | 4 +- klvdata/elementparser.py | 148 ++++++++++++++++++++++++-- klvdata/klvparser.py | 17 +-- klvdata/misb0102.py | 181 ++++++++++++++++++++++++-------- klvdata/misb0601.py | 221 +++++++++++++++++++++++++-------------- klvdata/misb0903.py | 204 ++++++++++++++++++++++++++++++++++++ klvdata/seriesparser.py | 104 ++++++++++++++++++ klvdata/setparser.py | 31 +++--- klvdata/streamparser.py | 8 +- test/test_misb_0601.py | 10 +- 12 files changed, 813 insertions(+), 162 deletions(-) mode change 100644 => 100755 klvdata/__init__.py create mode 100755 klvdata/misb0903.py create mode 100755 klvdata/seriesparser.py diff --git a/klvdata/__init__.py b/klvdata/__init__.py old mode 100644 new mode 100755 index b633ed9..e85f582 --- a/klvdata/__init__.py +++ b/klvdata/__init__.py @@ -1,3 +1,4 @@ from .streamparser import StreamParser from . import misb0601 -from . import misb0102 \ No newline at end of file +from . import misb0102 +from . import misb0903 \ No newline at end of file diff --git a/klvdata/common.py b/klvdata/common.py index 063921b..f39b5ab 100755 --- a/klvdata/common.py +++ b/klvdata/common.py @@ -28,6 +28,7 @@ from datetime import datetime from datetime import timezone from binascii import hexlify, unhexlify +from math import log, ceil, floor def datetime_to_bytes(value): """Return bytes representing UTC time in microseconds.""" @@ -77,13 +78,16 @@ def ber_encode(value): return int_to_bytes(byte_length + 128) + int_to_bytes(value, length=byte_length) -def bytes_to_str(value): - """Return UTF-8 formatted string from bytes object.""" - return bytes(value).decode('UTF-8') +def bytes_to_str(value, encoding='UTF-8'): + """Return string from bytes object.""" + if isinstance(value, str): + return value + else: + return bytes(value).decode(encoding) -def str_to_bytes(value): - """Return bytes object from UTF-8 formatted string.""" +def str_to_bytes(value, encoding='UTF-8'): + """Return bytes object from string.""" return bytes(str(value), 'UTF-8') @@ -124,13 +128,11 @@ def linear_map(src_value, src_domain, dst_range): return dst_value - def bytes_to_float(value, _domain, _range): """Convert the fixed point value self.value to a floating point value.""" src_value = int().from_bytes(value, byteorder='big', signed=(min(_domain) < 0)) return linear_map(src_value, _domain, _range) - def float_to_bytes(value, _domain, _range): """Convert the fixed point value self.value to a floating point value.""" # Some classes like MappedElement are calling float_to_bytes with arguments _domain @@ -142,6 +144,34 @@ def float_to_bytes(value, _domain, _range): dst_value = linear_map(value, src_domain=src_domain, dst_range=dst_range) return round(dst_value).to_bytes(length, byteorder='big', signed=(dst_min < 0)) +def float_to_imapb(value, _length, _range): + _min, _max = _range + if value < _min or value > _max: + return b'' + + bPow = ceil(log(_max - _min, 2)) + dPow = 8 * length - 1 + sF = 2**(dPow - bPow) + zOffset = 0.0 + if _min < 0 and _max > 0: + zOffset = sF * _min - floor(sF * _min) + + y = int(sF * (value - _min) + zOffset) + return int_to_bytes(y, _length) + +def imapb_to_float(value, _range): + _min, _max = _range + length = len(value) + + bPow = ceil(log(_max - _min, 2)) + dPow = 8 * length - 1 + sR = 2**(bPow - dPow) + zOffset = 0.0 + if _min < 0 and _max > 0: + zOffset = sR * _min - floor(sR * _min) + + y = bytes_to_int(value) + return sR * (y - zOffset) + _min def packet_checksum(data): """Return two byte checksum from a SMPTE ST 336 KLV structured bytes object.""" diff --git a/klvdata/element.py b/klvdata/element.py index d9393f9..f944146 100755 --- a/klvdata/element.py +++ b/klvdata/element.py @@ -26,7 +26,7 @@ from abc import ABCMeta from abc import abstractmethod from klvdata.common import ber_encode - +from klvdata.common import int_to_bytes # Proposed alternate names, "BaseElement" of modules "bases". class Element(metaclass=ABCMeta): @@ -65,6 +65,8 @@ def __bytes__(self): def __len__(self): """Return the byte length of self.value.""" + if isinstance(self.value, int): + return len(int_to_bytes(self.value)) return len(bytes(self.value)) @abstractmethod diff --git a/klvdata/elementparser.py b/klvdata/elementparser.py index aaa38de..8900549 100755 --- a/klvdata/elementparser.py +++ b/klvdata/elementparser.py @@ -31,9 +31,12 @@ from klvdata.common import bytes_to_float from klvdata.common import bytes_to_hexstr from klvdata.common import bytes_to_str +from klvdata.common import imapb_to_float from klvdata.common import datetime_to_bytes from klvdata.common import float_to_bytes from klvdata.common import str_to_bytes +from klvdata.common import int_to_bytes +from klvdata.common import float_to_imapb class ElementParser(Element, metaclass=ABCMeta): @@ -80,13 +83,24 @@ class BytesElementParser(ElementParser, metaclass=ABCMeta): def __init__(self, value): super().__init__(BytesValue(value)) + def __bytes__(self): + """Return the MISB encoded representation of a Key, Length, Value element.""" + if isinstance(self.value, int): + bytesvalue = int_to_bytes(self.value) + else: + bytesvalue = bytes(self.value) + return bytes(self.key) + bytes(self.length) + bytesvalue + class BytesValue(BaseValue): def __init__(self, value): self.value = value def __bytes__(self): - return bytes(self.value) + if isinstance(self.value, int): + return int_to_bytes(self.value, length=1 if self.value <= 255 else 2) + else: + return bytes(self.value) def __str__(self): return bytes_to_hexstr(self.value, start='0x', sep='') @@ -110,26 +124,79 @@ def __str__(self): class StringElementParser(ElementParser, metaclass=ABCMeta): def __init__(self, value): - super().__init__(StringValue(value)) + super().__init__(StringValue(value, self._encoding)) + + @property + def _encoding(cls): + return 'UTF-8' class StringValue(BaseValue): - def __init__(self, value): + def __init__(self, value, _encoding = 'UTF-8'): + self._encoding = _encoding try: - self.value = bytes_to_str(value) - except TypeError: + self.value = bytes_to_str(value, _encoding) + except UnicodeDecodeError: self.value = value def __bytes__(self): - return str_to_bytes(self.value) + return str_to_bytes(self.value, self._encoding) def __str__(self): return str(self.value) +class IntegerElementParser(ElementParser, metaclass=ABCMeta): + def __init__(self, value): + super().__init__(IntegerValue(value, self._size, self._signed)) + + @property + @classmethod + @abstractmethod + def _signed(cls): + pass + + @property + @classmethod + @abstractmethod + def _size(cls): + pass + +class IntegerValue(BaseValue): + def __init__(self, value, _size, _signed): + self._size = _size + self._signed = _signed + if isinstance(value, int): + self.value = value + else: + try: + self.value = bytes_to_int(value, _signed) + except TypeError: + self.value = value + + def __bytes__(self): + return int_to_bytes(self.value, self._size, self._signed) + + def __str__(self): + return str(self.value) + class MappedElementParser(ElementParser, metaclass=ABCMeta): def __init__(self, value): - super().__init__(MappedValue(value, self._domain, self._range)) + if isinstance(value, float): + localvalue = float_to_bytes(value, self._domain, self._range) + elif isinstance(value, int): + localvalue = float_to_bytes(float(value), self._domain, self._range) + else: + localvalue = value + if int.from_bytes(localvalue, byteorder='big', signed=True) == self._error: + super().__init__(StringValue(('%s (%s)'%(bytes_to_hexstr(localvalue, start='0x', sep=''),'Standard error indicator')).encode('UTF-8'))) + else: + super().__init__(MappedValue(localvalue, self._domain, self._range)) + + def __bytes__(self): + """Return the MISB encoded representation of a Key, Length, Value element.""" + return bytes(self.key) + bytes(self.length) + bytes(self.value) + @property @classmethod @@ -143,6 +210,10 @@ def _domain(cls): def _range(cls): pass + @property + def _error(cls): + pass + class MappedValue(BaseValue): def __init__(self, value, _domain, _range): self._domain = _domain @@ -162,6 +233,69 @@ def __str__(self): def __float__(self): return self.value +class EnumElementParser(ElementParser, metaclass=ABCMeta): + def __init__(self, value): + super().__init__(EnumValue(value, self._enum)) + + @property + @classmethod + @abstractmethod + def _enum(cls): + pass + +class EnumValue(BaseValue): + def __init__(self, value, _enum): + if isinstance(value, bytes): + self.value = bytes_to_int(value) + else: + self.value = value + self._enum = _enum + def __bytes__(self): + return int_to_bytes(self.value) + + def __str__(self): + try: + return self._enum[self.value] + except KeyError: + return str(self.value) +class IMAPBElementParser(ElementParser, metaclass=ABCMeta): + def __init__(self, value): + super().__init__(IMAPBValue(value, self._range)) + @property + @classmethod + @abstractmethod + def _range(cls): + pass + +class IMAPBValue(BaseValue): + def __init__(self, value, _range): + self._range = _range + self._length = len(value) + self.value = imapb_to_float(value, self._range) + + def __bytes__(self): + return float_to_imapb(self.value, self._length, self._range) + + def __str__(self): + return str(self.value) + +class LocationElementParser(ElementParser, metaclass=ABCMeta): + def __init__(self, value): + super().__init__(LocationValue(value)) + +class LocationValue(BaseValue): + def __init__(self, value): + self.value = (imapb_to_float(value[0:4], (-90, 90)), + imapb_to_float(value[4:8], (-180, 180)), + imapb_to_float(value[8:10], (-900, 19000))) + + def __bytes__(self): + return (float_to_imapb(self.value(0), 4, (-90, 90)) + + float_to_imapb(self.value(1), 4, (-180, 180)) + + float_to_imapb(self.value(1), 2, (-900, 19000))) + + def __str__(self): + return str(self.value) \ No newline at end of file diff --git a/klvdata/klvparser.py b/klvdata/klvparser.py index 08062d2..2aca813 100755 --- a/klvdata/klvparser.py +++ b/klvdata/klvparser.py @@ -23,10 +23,11 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +from sys import maxsize from io import BytesIO from io import IOBase from klvdata.common import bytes_to_int - +from klvdata.common import bytes_to_hexstr class KLVParser(object): """Return key, value pairs parsed from an SMPTE ST 336 source.""" @@ -43,7 +44,7 @@ def __iter__(self): def __next__(self): key = self.__read(self.key_length) - + byte_length = bytes_to_int(self.__read(1)) if byte_length < 128: @@ -53,16 +54,20 @@ def __next__(self): # BER Long Form length = bytes_to_int(self.__read(byte_length - 128)) - value = self.__read(length) - + try: + value = self.__read(length) + except OverflowError: + return key, None + return key, value def __read(self, size): + if size < 0 or size > maxsize: + raise OverflowError + if size == 0: return b'' - assert size > 0 - data = self.source.read(size) if data: diff --git a/klvdata/misb0102.py b/klvdata/misb0102.py index af09973..e5a2245 100755 --- a/klvdata/misb0102.py +++ b/klvdata/misb0102.py @@ -24,50 +24,12 @@ # SOFTWARE. from klvdata.element import UnknownElement -from klvdata.elementparser import BytesElementParser +from klvdata.elementparser import EnumElementParser +from klvdata.elementparser import StringElementParser +from klvdata.elementparser import IntegerElementParser from klvdata.misb0601 import UASLocalMetadataSet from klvdata.setparser import SetParser -_classifying_country_coding = { - b'\x01': 'ISO-3166 Two Letter', - b'\x02': 'ISO-3166 Three Letter', - b'\x03': 'FIPS 10-4 Two Letter', - b'\x04': 'FIPS 10-4 Four Letter', - b'\x05': 'ISO-3166 Numeric', - b'\x06': '1059 Two Letter', - b'\x07': '1059 Three Letter', - b'\x08': 'Omitted Value', - b'\x09': 'Omitted Value', - b'\x0A': 'FIPS 10-4 Mixed', - b'\x0B': 'ISO 3166 Mixed', - b'\x0C': 'STANAG 1059 Mixed', - b'\x0D': 'GENC Two Letter', - b'\x0E': 'GENC Three Letter', - b'\x0F': 'GENC Numeric', - b'\x10': 'GENC Mixed', -} - - -_object_country_coding = { - b'\x01': 'ISO-3166 Two Letter', - b'\x02': 'ISO-3166 Three Letter', - b'\x03': 'ISO-3166 Numeric', - b'\x04': 'FIPS 10-4 Two Letter', - b'\x05': 'FIPS 10-4 Four Letter', - b'\x06': '1059 Two Letter', - b'\x07': '1059 Three Letter', - b'\x08': 'Omitted Value', - b'\x09': 'Omitted Value', - b'\x0A': 'Omitted Value', - b'\x0B': 'Omitted Value', - b'\x0C': 'Omitted Value', - b'\x0D': 'GENC Two Letter', - b'\x0E': 'GENC Three Letter', - b'\x0F': 'GENC Numeric', - b'\x40': 'GENC AdminSub', -} - - class UnknownElement(UnknownElement): pass @@ -82,14 +44,15 @@ class SecurityLocalMetadataSet(SetParser): Must be a subclass of Element or duck type Element. """ - key, name = b'\x30', "Security Local Metadata Set" + key = b'\x30' + name = "Security Local Metadata Set" parsers = {} _unknown_element = UnknownElement @SecurityLocalMetadataSet.add_parser -class SecurityClassification(BytesElementParser): +class SecurityClassification(EnumElementParser): """MISB ST0102 Security Classification value interpretation parser. The Security Classification metadata element contains a value @@ -97,11 +60,141 @@ class SecurityClassification(BytesElementParser): accordance with U.S. and NATO classification guidance. """ key = b'\x01' + TAG = 1 + UDSKey = "-" + LDSName = "Security Classification" + ESDName = "Security Classification" + UDSName = "" - _classification = { + _enum = { b'\x01': 'UNCLASSIFIED', b'\x02': 'RESTRICTED', b'\x03': 'CONFIDENTIAL', b'\x04': 'SECRET', b'\x05': 'TOP SECRET', } + +@SecurityLocalMetadataSet.add_parser +class ClassifyingCountryCoding(EnumElementParser): + key = b'\x02' + TAG = 2 + UDSKey = "-" + LDSName = "Classifying Country and Releasing Instructions Coding" + ESDName = "Classifying Country Coding" + UDSName = "" + + _enum = { + b'\x01': 'ISO-3166 Two Letter', + b'\x02': 'ISO-3166 Three Letter', + b'\x03': 'FIPS 10-4 Two Letter', + b'\x04': 'FIPS 10-4 Four Letter', + b'\x05': 'ISO-3166 Numeric', + b'\x06': '1059 Two Letter', + b'\x07': '1059 Three Letter', + b'\x08': 'Omitted Value', + b'\x09': 'Omitted Value', + b'\x0A': 'FIPS 10-4 Mixed', + b'\x0B': 'ISO 3166 Mixed', + b'\x0C': 'STANAG 1059 Mixed', + b'\x0D': 'GENC Two Letter', + b'\x0E': 'GENC Three Letter', + b'\x0F': 'GENC Numeric', + b'\x10': 'GENC Mixed', + } + +@SecurityLocalMetadataSet.add_parser +class ClassifyingCountry(StringElementParser): + key = b'\x03' + TAG = 3 + UDSKey = "-" + LDSName = "Classifying Country" + ESDName = "Classifying Country" + UDSName = "" + min_length, max_length = 0, 6 + +@SecurityLocalMetadataSet.add_parser +class SecurityInformation(StringElementParser): + key = b'\x04' + TAG = 4 + UDSKey = "-" + LDSName = "Security-SCI/SHI Information" + ESDName = "Security-SCI/SHI Information" + UDSName = "" + + _encoding = 'iso646_us' + min_length, max_length = 0, 40 + +@SecurityLocalMetadataSet.add_parser +class Caveats(StringElementParser): + key = b'\x05' + TAG = 5 + UDSKey = "-" + LDSName = "Caveats" + ESDName = "Caveats" + UDSName = "" + + _encoding = 'iso646_us' + min_length, max_length = 0, 32 + +@SecurityLocalMetadataSet.add_parser +class ReleasingInstructions(StringElementParser): + key = b'\x06' + TAG = 6 + UDSKey = "-" + LDSName = "Releasing Instructions" + ESDName = "Releasing Instructions" + UDSName = "" + + _encoding = 'iso646_us' + min_length, max_length = 0, 40 + +@SecurityLocalMetadataSet.add_parser +class ObjectCountryMethod(EnumElementParser): + key = b'\x0C' + TAG = 12 + UDSKey = "-" + LDSName = "Object Country Method" + ESDName = "Object Country Method" + UDSName = "" + _enum = { + b'\x01': 'ISO-3166 Two Letter', + b'\x02': 'ISO-3166 Three Letter', + b'\x03': 'ISO-3166 Numeric', + b'\x04': 'FIPS 10-4 Two Letter', + b'\x05': 'FIPS 10-4 Four Letter', + b'\x06': '1059 Two Letter', + b'\x07': '1059 Three Letter', + b'\x08': 'Omitted Value', + b'\x09': 'Omitted Value', + b'\x0A': 'Omitted Value', + b'\x0B': 'Omitted Value', + b'\x0C': 'Omitted Value', + b'\x0D': 'GENC Two Letter', + b'\x0E': 'GENC Three Letter', + b'\x0F': 'GENC Numeric', + b'\x40': 'GENC AdminSub', + } + +@SecurityLocalMetadataSet.add_parser +class ObjectCountryCodes(StringElementParser): + key = b'\x0D' + TAG = 13 + UDSKey = "-" + LDSName = "Object Country Codes" + ESDName = "Object Country Codes" + UDSName = "" + + _encoding = 'UTF-16-BE' + min_length, max_length = 0, 40 + +@SecurityLocalMetadataSet.add_parser +class SecurityMetadataVersion(IntegerElementParser): + key = b'\x16' + TAG = 22 + UDSKey = "-" + LDSName = "Security Metadata Version" + ESDName = "Security Metadata Version" + UDSName = "" + + _signed = False + _size = 2 \ No newline at end of file diff --git a/klvdata/misb0601.py b/klvdata/misb0601.py index 159924e..1560457 100755 --- a/klvdata/misb0601.py +++ b/klvdata/misb0601.py @@ -27,8 +27,11 @@ from klvdata.element import UnknownElement from klvdata.elementparser import BytesElementParser from klvdata.elementparser import DateTimeElementParser +from klvdata.elementparser import IntegerElementParser from klvdata.elementparser import MappedElementParser +from klvdata.elementparser import EnumElementParser from klvdata.elementparser import StringElementParser +from klvdata.elementparser import IMAPBElementParser from klvdata.setparser import SetParser from klvdata.streamparser import StreamParser @@ -96,6 +99,8 @@ class MissionID(StringElementParser): LDSName = "Mission ID" ESDName = "Mission Number" UDSName = "Episode Number" + + _encoding = 'iso646_us' min_length, max_length = 0, 127 @@ -107,6 +112,8 @@ class PlatformTailNumber(StringElementParser): LDSName = "Platform Tail Number" ESDName = "Platform Tail Number" UDSName = "" + + _encoding = 'iso646_us' min_length, max_length = 0, 127 @@ -120,6 +127,7 @@ class PlatformHeadingAngle(MappedElementParser): UDSName = "Platform Heading Angle" _domain = (0, 2**16-1) _range = (0, 360) + units = 'degrees' @UASLocalMetadataSet.add_parser @@ -132,6 +140,8 @@ class PlatformPitchAngle(MappedElementParser): UDSName = "Platform Pitch Angle" _domain = (-(2**15-1), 2**15-1) _range = (-20, 20) + _error = -2**15 + units = 'degrees' @UASLocalMetadataSet.add_parser @@ -144,32 +154,34 @@ class PlatformRollAngle(MappedElementParser): UDSName = "Platform Roll Angle" _domain = (-(2**15-1), 2**15-1) _range = (-50, 50) + _error = -2**15 units = 'degrees' + @UASLocalMetadataSet.add_parser -class PlatformTrueAirspeed(MappedElementParser): +class PlatformTrueAirspeed(IntegerElementParser): key = b'\x08' TAG = 8 UDSKey = "-" LDSName = "Platform True Airspeed" ESDName = "True Airspeed" UDSName = "" - _domain = (0, 2**8-1) - _range = (0, 255) + _signed = False + _size = 1 units = 'meters/second' @UASLocalMetadataSet.add_parser -class PlatformIndicatedAirspeed(MappedElementParser): +class PlatformIndicatedAirspeed(IntegerElementParser): key = b'\x09' TAG = 9 UDSKey = "-" LDSName = "Platform Indicated Airspeed" ESDName = "Indicated Airspeed" UDSName = "" - _domain = (0, 2**8-1) - _range = (0, 255) + _signed = False + _size = 1 units = 'meters/second' @@ -181,6 +193,8 @@ class PlatformDesignation(StringElementParser): LDSName = "Platform Designation" ESDName = "Project ID Code" UDSName = "Device Designation" + + _encoding = 'iso646_us' min_length, max_length = 0, 127 @@ -192,6 +206,8 @@ class ImageSourceSensor(StringElementParser): LDSName = "Image Source Sensor" ESDName = "Sensor Name" UDSName = "Image Source Device" + + _encoding = 'iso646_us' min_length, max_length = 0, 127 @@ -203,6 +219,8 @@ class ImageCoordinateSystem(StringElementParser): LDSName = "Image Coordinate System" ESDName = "Image Coordinate System" UDSName = "Image Coordinate System" + + _encoding = 'iso646_us' min_length, max_length = 0, 127 @@ -216,6 +234,7 @@ class SensorLatitude(MappedElementParser): UDSName = "Device Latitude" _domain = (-(2**31-1), 2**31-1) _range = (-90, 90) + _error = -2**31 units = 'degrees' @@ -229,6 +248,7 @@ class SensorLongitude(MappedElementParser): UDSName = "Device Longitude" _domain = (-(2**31-1), 2**31-1) _range = (-180, 180) + _error = -2**31 units = 'degrees' @@ -294,6 +314,7 @@ class SensorRelativeElevationAngle(MappedElementParser): UDSName = "" _domain = (-(2**31 - 1), 2**31 - 1) _range = (-180, 180) + _error = -2**31 units = 'degrees' @@ -307,6 +328,7 @@ class SensorRelativeRollAngle(MappedElementParser): UDSName = "" _domain = (0, 2**32-1) _range = (0, 360) + _error = -2**31 units = 'degrees' @@ -346,6 +368,7 @@ class FrameCenterLatitude(MappedElementParser): UDSName = "Frame Center Latitude" _domain = (-(2**31 - 1), 2**31 - 1) _range = (-90, 90) + _error = -2**31 units = 'degrees' @@ -359,6 +382,7 @@ class FrameCenterLongitude(MappedElementParser): UDSName = "Frame Center Longitude" _domain = (-(2**31 - 1), 2**31 - 1) _range = (-180, 180) + _error = -2**31 units = 'degrees' @@ -385,6 +409,7 @@ class OffsetCornerLatitudePoint1(MappedElementParser): UDSName = "Corner Latitude Point 1" _domain = (-(2**15 - 1), 2**15 - 1) _range = (-0.075, +0.075) + _error = -2**15 units = 'degrees' @@ -398,6 +423,7 @@ class OffsetCornerLongitudePoint1(MappedElementParser): UDSName = "Corner Longitude Point 1" _domain = (-(2**15 - 1), 2**15 - 1) _range = (-0.075, 0.075) + _error = -2**15 units = 'degrees' @@ -411,6 +437,7 @@ class OffsetCornerLatitudePoint2(MappedElementParser): UDSName = "Corner Latitude Point 2" _domain = (-(2**15 - 1), 2**15 - 1) _range = (-0.075, 0.075) + _error = -2**15 units = 'degrees' @@ -424,6 +451,7 @@ class OffsetCornerLongitudePoint2(MappedElementParser): UDSName = "Corner Longitude Point 2" _domain = (-(2**15 - 1), 2**15 - 1) _range = (-0.075, 0.075) + _error = -2**15 units = 'degrees' @@ -437,6 +465,7 @@ class OffsetCornerLatitudePoint3(MappedElementParser): UDSName = "Corner Latitude Point 3" _domain = (-(2**15 - 1), 2**15 - 1) _range = (-0.075, 0.075) + _error = -2**15 units = 'degrees' @@ -450,6 +479,7 @@ class OffsetCornerLongitudePoint3(MappedElementParser): UDSName = "Corner Longitude Point 3" _domain = (-(2**15 - 1), 2**15 - 1) _range = (-0.075, 0.075) + _error = -2**15 units = 'degrees' @@ -463,6 +493,7 @@ class OffsetCornerLatitudePoint4(MappedElementParser): UDSName = "Corner Latitude Point 4" _domain = (-(2**15 - 1), 2**15 - 1) _range = (-0.075, 0.075) + _error = -2**15 units = 'degrees' @@ -476,20 +507,23 @@ class OffsetCornerLongitudePoint4(MappedElementParser): UDSName = "Corner Longitude Point 4" _domain = (-(2**15 - 1), 2**15 - 1) _range = (-0.075, 0.075) + _error = -2**15 units = 'degrees' @UASLocalMetadataSet.add_parser -class IcingDetected(MappedElementParser): +class IcingDetected(EnumElementParser): key = b'\x22' TAG = 34 UDSKey = "" LDSName = "Icing Detected" ESDName = "Icing Detected" UDSName = "" - _domain = (0, 2**8-1) - _range = (0, 2**8-1) - units = 'flag' + _enum = { + b'\x00': 'DETECTOR OFF', + b'\x01': 'NO ICING DETECTED', + b'\x02': 'ICING DETECTED', + } @UASLocalMetadataSet.add_parser @@ -545,16 +579,16 @@ class DensityAltitude(MappedElementParser): @UASLocalMetadataSet.add_parser -class OutsideAirTemperature(MappedElementParser): +class OutsideAirTemperature(IntegerElementParser): key = b'\x27' TAG = 39 UDSKey = "-" LDSName = "Outside Air Temperature" ESDName = "Air Temperature" UDSName = "" - _domain = (0, 2**8-1) - _range = (0, 2**8-1) - units = 'celcius' + _signed = True + _size = 1 + units = 'celsius' @UASLocalMetadataSet.add_parser @@ -567,6 +601,7 @@ class TargetLocationLatitude(MappedElementParser): UDSName = "" _domain = (-(2**31 - 1), 2**31 - 1) _range = (-90, 90) + _error = -2**31 units = 'degrees' @@ -580,6 +615,7 @@ class TargetLocationLongitude(MappedElementParser): UDSName = "" _domain = (-(2**31 - 1), 2**31 - 1) _range = (-180, 180) + _error = -2**31 units = 'degrees' @@ -649,19 +685,20 @@ class TargetErrorEstimateLE90(MappedElementParser): @UASLocalMetadataSet.add_parser -class GenericFlagData01(MappedElementParser): +class GenericFlagData01(IntegerElementParser): key = b'\x2F' TAG = 47 UDSKey = "-" LDSName = "Generic Flag Data 01" ESDName = "" UDSName = "" - _domain = (0, 2**8-1) - _range = (0, 2**8-1) - + _signed = False + _size = 1 +# SEE misb0102.py +# # @UASLocalMetadataSet.add_parser -# class SecurityLocalMetadataSet(MappedElementParser): +# class SecurityLocalMetadataSet(BytesElementParser): # key = b'\x30' # TAG = 48 # UDSKey = "06 0E 2B 34 02 03 01 01 0E 01 03 03 02 00 00 00" @@ -669,7 +706,6 @@ class GenericFlagData01(MappedElementParser): # ESDName = "" # UDSName = "Security Local Set" - @UASLocalMetadataSet.add_parser class DifferentialPressure(MappedElementParser): key = b'\x31' @@ -693,6 +729,7 @@ class PlatformAngleOfAttack(MappedElementParser): UDSName = "" _domain = (-(2**15 - 1), 2**15 - 1) _range = (-20, 20) + _error = -2**15 units = 'degrees' @@ -706,6 +743,7 @@ class PlatformVerticalSpeed(MappedElementParser): UDSName = "" _domain = (-(2**15 - 1), 2**15 - 1) _range = (-180, 180) + _error = -2**15 units = 'meters/second' @@ -719,6 +757,7 @@ class PlatformSideslipAngle(MappedElementParser): UDSName = "" _domain = (-(2**15 - 1), 2**15 - 1) _range = (-20, 20) + _error = -2**15 units = 'degrees' @@ -762,15 +801,15 @@ class RelativeHumidity(MappedElementParser): @UASLocalMetadataSet.add_parser -class PlatformGroundSpeed(MappedElementParser): +class PlatformGroundSpeed(IntegerElementParser): key = b'\x38' TAG = 56 UDSKey = "-" LDSName = "Platform Ground Speed" ESDName = "Platform Ground Speed" UDSName = "" - _domain = (0, 2**8-1) - _range = (0, 255) + _signed = False + _size = 1 units = 'meters/second' @@ -783,7 +822,7 @@ class GroundRange(MappedElementParser): ESDName = "Ground Range" UDSName = "" _domain = (0, 2**32-1) - _range = (0, 5000000) + _range = (0, +5e6) units = 'meters' @@ -796,7 +835,7 @@ class PlatformFuelRemaining(MappedElementParser): ESDName = "Platform Fuel Remaining" UDSName = "" _domain = (0, 2**16-1) - _range = (0, 10000) + _range = (0, 10e3) units = 'kilograms' @@ -809,51 +848,55 @@ class PlatformCallSign(StringElementParser): ESDName = "Platform Call Sign" UDSName = "" + _encoding = 'iso646_us' @UASLocalMetadataSet.add_parser -class WeaponLoad(MappedElementParser): +class WeaponLoad(BytesElementParser): key = b'\x3C' TAG = 60 UDSKey = "-" LDSName = "Weapon Load" ESDName = "Weapon Load" UDSName = "" - _domain = (0, 2**16-1) - _range = (0, 2**16-1) @UASLocalMetadataSet.add_parser -class WeaponFired(MappedElementParser): +class WeaponFired(BytesElementParser): key = b'\x3D' TAG = 61 UDSKey = "-" LDSName = "Weapon Fired" ESDName = "Weapon Fired" UDSName = "" - _domain = (0, 2**8-1) - _range = (0, 2**8-1) - @UASLocalMetadataSet.add_parser -class LaserPRFCode(MappedElementParser): +class LaserPRFCode(IntegerElementParser): key = b'\x3E' TAG = 62 UDSKey = "-" LDSName = "Laser PRF Code" ESDName = "Laser PRF Code" UDSName = "" - _domain = (0, 2**16-1) - _range = (0, 65535) + _signed = False + _size = 2 @UASLocalMetadataSet.add_parser -class SensorFieldOfViewName(MappedElementParser): +class SensorFieldOfViewName(EnumElementParser): key = b'\x3F' TAG = 63 UDSKey = "-" LDSName = "Sensor Field of View Name" ESDName = "Sensor Field of View Name" UDSName = "" - _domain = (0, 2**8-1) - _range = (0, 2**8-1) + _enum = { + b'\x00': 'Ultranarrow', + b'\x01': 'Narrow', + b'\x02': 'Medium', + b'\x03': 'Wide', + b'\x04': 'Ultrawide', + b'\x05': 'Narrow Medium', + b'\x06': '2x Ultranarrow', + b'\x07': '4x Ultranarrow', + } @UASLocalMetadataSet.add_parser class PlatformMagneticHeading(MappedElementParser): @@ -869,18 +912,17 @@ class PlatformMagneticHeading(MappedElementParser): @UASLocalMetadataSet.add_parser -class UASLSVersionNumber(MappedElementParser): +class UASLSVersionNumber(IntegerElementParser): key = b'\x41' TAG = 65 UDSKey = "-" LDSName = "UAS Datalink LS Version Number" ESDName = "ESD ICD Version" UDSName = "" - _domain = (0, 2**8-1) - _range = (0, 2**8-1) + _signed = False + _size = 1 units = 'number' - @UASLocalMetadataSet.add_parser class AlternatePlatformLatitude(MappedElementParser): key = b'\x43' @@ -891,6 +933,7 @@ class AlternatePlatformLatitude(MappedElementParser): UDSName = "" _domain = (-(2**31 - 1), 2**31 - 1) _range = (-90, 90) + _error = -2**31 units = 'degrees' @@ -904,6 +947,7 @@ class AlternatePlatformLongitude(MappedElementParser): UDSName = "" _domain = (-(2**31 - 1), 2**31 - 1) _range = (-180, 180) + _error = -2**31 units = 'degrees' @@ -928,6 +972,8 @@ class AlternatePlatformName(StringElementParser): LDSName = "Alternate Platform Name" ESDName = "" UDSName = "" + + _encoding = 'iso646_us' min_length, max_length = 0, 127 @@ -955,7 +1001,7 @@ class EventStartTime(DateTimeElementParser): @UASLocalMetadataSet.add_parser -class RVTLocalSet(MappedElementParser): +class RVTLocalSet(BytesElementParser): key = b'\x49' TAG = 73 UDSKey = "06 0E 2B 34 01 01 01 01 07 02 01 02 07 01 00 00" @@ -964,18 +1010,20 @@ class RVTLocalSet(MappedElementParser): UDSName = "Remote Video Terminal Local Set" -@UASLocalMetadataSet.add_parser -class VMTILocalSet(MappedElementParser): - key = b'\x4A' - TAG = 74 - UDSKey = "06 0E 2B 34 02 0B 01 01 0E 01 03 03 06 00 00 00" - LDSName = "VMTI Local Set" - ESDName = "" - UDSName = "Video Moving Target Indicator Local Set" +# SEE misb0903.py +# +# @UASLocalMetadataSet.add_parser +# class VMTILocalSet(BytesElementParser): +# key = b'\x4A' +# TAG = 74 +# UDSKey = "06 0E 2B 34 02 0B 01 01 0E 01 03 03 06 00 00 00" +# LDSName = "VMTI Local Set" +# ESDName = "" +# UDSName = "Video Moving Target Indicator Local Set" @UASLocalMetadataSet.add_parser -class SensorEllipsoidHeightConversion(MappedElementParser): +class SensorEllipsoidHeight(MappedElementParser): key = b'\x4B' TAG = 75 UDSKey = "-" @@ -1001,14 +1049,21 @@ class AlternatePlatformEllipsoidHeight(MappedElementParser): @UASLocalMetadataSet.add_parser -class OperationalMode(StringElementParser): +class OperationalMode(EnumElementParser): key = b'\x4D' TAG = 77 UDSKey = "-" LDSName = "Operational Mode" ESDName = "" UDSName = "" - + _enum = { + b'\x00': 'Other', + b'\x01': 'Operational', + b'\x02': 'Training', + b'\x03': 'Exercise', + b'\x04': 'Maintenance', + b'\x05': 'Test', + } @UASLocalMetadataSet.add_parser class FrameCenterHeightAboveEllipsoid(MappedElementParser): @@ -1033,6 +1088,7 @@ class SensorNorthVelocity(MappedElementParser): UDSName = "" _domain = (-(2**15 - 1), 2**15 - 1) _range = (-327, 327) + _error = -2**15 units = 'meters/second' @@ -1046,10 +1102,11 @@ class SensorEastVelocity(MappedElementParser): UDSName = "" _domain = (-(2**15 - 1), 2**15 - 1) _range = (-327, 327) + _error = -2**15 units = 'meters/second' # @UASLocalMetadataSet.add_parser -# class ImageHorizonPixelPack(MappedElementParser): +# class ImageHorizonPixelPack(BytesElementParser): # key = b'\x51' # TAG = 81 # UDSKey = "-" @@ -1068,6 +1125,7 @@ class CornerLatitudePoint1Full(MappedElementParser): UDSName = "Corner Latitude Point 1 (Decimal Degrees)" _domain = (-(2**31 - 1), 2**31 - 1) _range = (-90, 90) + _error = -2**31 #b'\x80\x00\x00\x00' units = 'degrees' @@ -1081,6 +1139,7 @@ class CornerLongitudePoint1Full(MappedElementParser): UDSName = "Corner Longitude Point 1 (Decimal Degrees)" _domain = (-(2**31 - 1), 2**31 - 1) _range = (-180, 180) + _error = -2**31 #b'\x80\x00\x00\x00' units = 'degrees' @@ -1094,6 +1153,7 @@ class CornerLatitudePoint2Full(MappedElementParser): UDSName = "Corner Latitude Point 2 (Decimal Degrees)" _domain = (-(2**31 - 1), 2**31 - 1) _range = (-90, 90) + _error = -2**31 #b'\x80\x00\x00\x00' units = 'degrees' @@ -1107,6 +1167,7 @@ class CornerLongitudePoint2Full(MappedElementParser): UDSName = "Corner Longitude Point 2 (Decimal Degrees)" _domain = (-(2**31 - 1), 2**31 - 1) _range = (-180, 180) + _error = -2**31 #b'\x80\x00\x00\x00' units = 'degrees' @@ -1120,6 +1181,7 @@ class CornerLatitudePoint3Full(MappedElementParser): UDSName = "Corner Latitude Point 3 (Decimal Degrees)" _domain = (-(2**31 - 1), 2**31 - 1) _range = (-90, 90) + _error = -2**31 #b'\x80\x00\x00\x00' units = 'degrees' @@ -1133,6 +1195,7 @@ class CornerLongitudePoint3Full(MappedElementParser): UDSName = "Corner Longitude Point 3 (Decimal Degrees)" _domain = (-(2**31 - 1), 2**31 - 1) _range = (-180, 180) + _error = -2**31 #b'\x80\x00\x00\x00' units = 'degrees' @@ -1146,6 +1209,7 @@ class CornerLatitudePoint4Full(MappedElementParser): UDSName = "Corner Latitude Point 4 (Decimal Degrees)" _domain = (-(2**31 - 1), 2**31 - 1) _range = (-90, 90) + _error = -2**31 #b'\x80\x00\x00\x00' units = 'degrees' @@ -1159,6 +1223,7 @@ class CornerLongitudePoint4Full(MappedElementParser): UDSName = "Corner Longitude Point 4 (Decimal Degrees)" _domain = (-(2**31 - 1), 2**31 - 1) _range = (-180, 180) + _error = -2**31 #b'\x80\x00\x00\x00' units = 'degrees' @@ -1172,6 +1237,7 @@ class PlatformPitchAngleFull(MappedElementParser): UDSName = "Platform Pitch Angle" _domain = (-(2**31-1), 2**31-1) _range = (-90, 90) + _error = -2**31 units = 'degrees' @@ -1185,6 +1251,7 @@ class PlatformRollAngleFull(MappedElementParser): UDSName = "Platform Roll Angle" _domain = (-(2**31-1), 2**31-1) _range = (-90, 90) + _error = -2**31 units = 'degrees' @@ -1198,6 +1265,7 @@ class PlatformAngleOfAttackFull(MappedElementParser): UDSName = "" _domain = (-(2**31-1), 2**31-1) _range = (-90, 90) + _error = -2**31 units = 'degrees' @@ -1211,21 +1279,22 @@ class PlatformSideslipAngleFull(MappedElementParser): UDSName = "" _domain = (-(2**31-1), 2**31-1) _range = (-90, 90) + _error = -2**31 units = 'degrees' -#@UASLocalMetadataSet.add_parser -# class MIISCoreIdentifier(StringElementParser): -# key = b'\x5E' -# TAG = 94 -# UDSKey = "06 0E 2B 34 01 01 01 01 0E 01 04 05 03 00 00 00" -# LDSName = "MIIS Core Identifier" -# ESDName = "" -# UDSName = "Motion Imagery Identification System Core" +@UASLocalMetadataSet.add_parser +class MIISCoreIdentifier(BytesElementParser): + key = b'\x5E' + TAG = 94 + UDSKey = "06 0E 2B 34 01 01 01 01 0E 01 04 05 03 00 00 00" + LDSName = "MIIS Core Identifier" + ESDName = "" + UDSName = "Motion Imagery Identification System Core" #@UASLocalMetadataSet.add_parser -# class SARMotionImageryLocalSet(StringElementParser): +# class SARMotionImageryLocalSet(BytesElementParser): # key = b'\x5F' # TAG = 95 # UDSKey = "06 0E 2B 34 02 0B 01 01 0E 01 03 03 0D 00 00 00" @@ -1235,51 +1304,51 @@ class PlatformSideslipAngleFull(MappedElementParser): @UASLocalMetadataSet.add_parser -class TargetWidthExtended(MappedElementParser): +class TargetWidthExtended(IMAPBElementParser): key = b'\x60' TAG = 96 UDSKey = "06 0E 2B 34 01 01 01 01 07 01 09 02 01 00 00 00" LDSName = "Target Width Extended" ESDName = "Target Width" UDSName = "Target Width" - _domain = (0, 2**8-1) - _range = (0, 2**8-1) + + _range = (0, 1.5e6) units = 'meters' @UASLocalMetadataSet.add_parser -class DensityAltitudeExtended(MappedElementParser): +class DensityAltitudeExtended(IMAPBElementParser): key = b'\x67' TAG = 103 UDSKey = "06 0E 2B 34 01 01 01 01 0E 01 01 01 10 00 00 00" LDSName = "Density Altitude Extended" ESDName = "Density Altitude" UDSName = "" - _domain = (0, 2**16-1) - _range = (-900, 40000) + + _range = (-900, 40e3) units = 'meters' @UASLocalMetadataSet.add_parser -class SensorEllipsoidHeightExtended(MappedElementParser): +class SensorEllipsoidHeightExtended(IMAPBElementParser): key = b'\x68' TAG = 104 UDSKey = "06 0E 2B 34 01 01 01 01 0E 01 02 01 82 47 00 00" LDSName = "Sensor Ellipsoid Height Extended" ESDName = "" UDSName = "" - _domain = (0, 2**16-1) - _range = (-900, 40000) + + _range = (-900, 40e3) units = 'meters' @UASLocalMetadataSet.add_parser -class AlternatePlatformEllipsoidHeightExtended(MappedElementParser): +class AlternatePlatformEllipsoidHeightExtended(IMAPBElementParser): key = b'\x69' TAG = 105 UDSKey = "06 0E 2B 34 01 01 01 01 0E 01 02 01 82 48 00 00" LDSName = " Alternate Platform Ellipsoid Height Extended" ESDName = "" UDSName = "" - _domain = (0, 2**16-1) - _range = (-900, 40000) + + _range = (-900, 40e3) units = 'meters' diff --git a/klvdata/misb0903.py b/klvdata/misb0903.py new file mode 100755 index 0000000..0eea8f9 --- /dev/null +++ b/klvdata/misb0903.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from klvdata.element import UnknownElement +from klvdata.elementparser import BytesElementParser +from klvdata.elementparser import DateTimeElementParser +from klvdata.elementparser import StringElementParser +from klvdata.elementparser import IntegerElementParser +from klvdata.elementparser import LocationElementParser +from klvdata.misb0601 import UASLocalMetadataSet +from klvdata.setparser import SetParser +from klvdata.seriesparser import SeriesParser + +@UASLocalMetadataSet.add_parser +class VMTILocalSet(SetParser): + """MISB ST0903 VMTI Metadata nested local set parser. + + Must be a subclass of Element or duck type Element. + """ + key = b'\x4A' + name = "VMTI Local Set" + parsers = {} + + _unknown_element = UnknownElement + + +@VMTILocalSet.add_parser +class Checksum(BytesElementParser): + """Checksum used to detect errors within a UAV Local Set packet. + + Checksum formed as lower 16-bits of summation performed on entire + LS packet, including 16-byte US key and 1-byte checksum length. + + Initialized from bytes value as BytesValue. + """ + key = b'\x01' + TAG = 1 + UDSKey = "-" + LDSName = "Checksum" + ESDName = "" + UDSName = "" + +@VMTILocalSet.add_parser +class PrecisionTimeStamp(DateTimeElementParser): + """Precision Timestamp represented in microseconds. + + Precision Timestamp represented in the number of microseconds elapsed + since midnight (00:00:00), January 1, 1970 not including leap seconds. + + See MISB ST 0601.11 for additional details. + """ + key = b'\x02' + TAG = 2 + UDSKey = "06 0E 2B 34 01 01 01 03 07 02 01 01 01 05 00 00" + LDSName = "Precision Time Stamp" + ESDName = "" + UDSName = "User Defined Time Stamp" + +@VMTILocalSet.add_parser +class NumberDetectedTargets(IntegerElementParser): + key = b'\x05' + TAG = 5 + UDSKey = "-" + LDSName = "Number of Detected Targets" + ESDName = "Number of Detected Targets" + UDSName = "" + + _signed = False + _size = 3 + +@VMTILocalSet.add_parser +class NumberReportedTargets(IntegerElementParser): + key = b'\x06' + TAG = 6 + UDSKey = "-" + LDSName = "Number of Reported Targets" + ESDName = "Number of Reported Targets" + UDSName = "" + + _signed = False + _size = 3 + +@VMTILocalSet.add_parser +class FrameWidth(IntegerElementParser): + key = b'\x08' + TAG = 8 + UDSKey = "-" + LDSName = "Frame Width" + ESDName = "Frame Width" + UDSName = "" + + _signed = False + _size = 3 + +@VMTILocalSet.add_parser +class FrameHeight(IntegerElementParser): + key = b'\x09' + TAG = 9 + UDSKey = "-" + LDSName = "Frame Height" + ESDName = "Frame Height" + UDSName = "" + + _signed = False + _size = 3 + +@VMTILocalSet.add_parser +class SourceSensor(StringElementParser): + key = b'\x0A' + TAG = 10 + UDSKey = "-" + LDSName = "Source Sensor" + ESDName = "Source Sensor" + UDSName = "" + + _encoding = 'UTF-8' + min_length, max_length = 0, 127 + +@VMTILocalSet.add_parser +class VTargetSeries(SeriesParser): + key = b'\x65' + TAG = 101 + + name = "VTarget Series" + parser = None + +@VTargetSeries.set_parser +class VTargetPack(SetParser): + name = "VMTI Target Pack" + parsers = {} + + def __init__(self, value): + """All parser needs is the value, no other information""" + self.key = value[0].to_bytes(1, byteorder='big') + super().__init__(value[1:]) + +@VTargetPack.add_parser +class CentroidPixel(IntegerElementParser): + key = b'\x01' + TAG = 1 + UDSKey = "-" + LDSName = "Centroid Pixel" + ESDName = "Centroid Pixel" + UDSName = "" + + _signed = False + _size = 3 + +@VTargetPack.add_parser +class BoundingBoxTopLeftPixel(IntegerElementParser): + key = b'\x02' + TAG = 2 + UDSKey = "-" + LDSName = "Bounding Box Top Left Pixel" + ESDName = "Bounding Box Top Left Pixel" + UDSName = "" + + _signed = False + _size = 3 + +@VTargetPack.add_parser +class BoundingBoxBottomRightPixel(IntegerElementParser): + key = b'\x03' + TAG = 3 + UDSKey = "-" + LDSName = "Bounding Box Bottom Right Pixel" + ESDName = "Bounding Box Bottom Right Pixel" + UDSName = "" + + _signed = False + _size = 3 + +@VTargetPack.add_parser +class DetectionCount(IntegerElementParser): + key = b'\x06' + TAG = 6 + UDSKey = "-" + LDSName = "Detection Count" + ESDName = "Detection Count" + UDSName = "" + + _signed = False + _size = 2 + +@VTargetPack.add_parser +class TargetIntensity(IntegerElementParser): + key = b'\x09' + TAG = 9 + UDSKey = "-" + LDSName = "Target Intensity" + ESDName = "Target Intensity" + UDSName = "" + + _signed = False + _size = 3 + +@VTargetPack.add_parser +class TargetLocation(LocationElementParser): + key = b'\x11' + TAG = 17 + UDSKey = "-" + LDSName = "Target Location" + ESDName = "Target Location" + UDSName = "" \ No newline at end of file diff --git a/klvdata/seriesparser.py b/klvdata/seriesparser.py new file mode 100755 index 0000000..aee3ba4 --- /dev/null +++ b/klvdata/seriesparser.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from pprint import pformat +from abc import ABCMeta +from abc import abstractmethod +from collections import OrderedDict +from klvdata.element import Element +from klvdata.element import UnknownElement +from klvdata.klvparser import KLVParser + +class SeriesParser(Element, metaclass=ABCMeta): + """Parsable Element. Not intended to be used directly. Always as super class.""" + _unknown_element = UnknownElement + + def __init__(self, value, key_length=1): + super().__init__(self.key, value) + self.key_length = key_length + self.items = OrderedDict() + self.parse() + + def parse(self): + """Parse the parent into items. Called on init and modification of parent value. + + If a known parser is not available for key, parse as generic KLV element. + """ + for i, (_, value) in enumerate(KLVParser(self.value, 0)): + elem = self.parser(value) + self.items[elem.key] = elem + + @classmethod + def set_parser(cls, obj): + """Decorator method used to register a parser to the class parsing repertoire. + + obj is required to implement key attribute supporting bytes as returned by KLVParser key. + """ + + # If sublcass of ElementParser does not implement key, dict accepts key of + # type property object. bytes(obj.key) will raise TypeError. ElementParser + # requires key as abstract property but no raise until instantiation which + # does not occur because the value is never recalled and instantiated from + # parsers. + cls.parser = obj + + return obj + + @property + @classmethod + @abstractmethod + def parser(cls): + # Property must define __getitem__ + pass + + @parser.setter + @classmethod + @abstractmethod + def parser(cls): + # Property must define __setitem__ + pass + + def __repr__(self): + return pformat(self.items, indent=1) + + def __str__(self): + return str_dict(self.items) + + def MetadataList(self): + ''' Return metadata dictionary''' + metadata = {} + for key in self.items: + item = self.items[key] + try: + if hasattr(item, 'items'): + name = item.name if hasattr(item, 'name') else '' + metadata[key] = (name, '', '', item.MetadataList()) + else: + metadata[key] = (item.LDSName, item.ESDName, item.UDSName, str(item.value)) + except: + None + return OrderedDict(metadata) + + def structure(self): + print(str(type(self))) + + def repeat(items, indent=1): + for item in items: + print(indent * "\t" + str(type(item))) + if hasattr(item, 'items'): + repeat(item.items.values(), indent+1) + + repeat(self.items.values()) + + +def str_dict(values): + out = [] + + def per_item(value, indent=0): + for item in value: + out.append(indent * "\t" + str(item)) + if hasattr(item, 'items'): + per_item(item.items, indent + 1) + + per_item(values) + + return '\n'.join(out) diff --git a/klvdata/setparser.py b/klvdata/setparser.py index decbcb1..d0effec 100755 --- a/klvdata/setparser.py +++ b/klvdata/setparser.py @@ -103,15 +103,19 @@ def MetadataList(self): ''' Return metadata dictionary''' metadata = {} - def repeat(items, indent=1): - for item in items: - try: - metadata[item.TAG] = (item.LDSName, item.ESDName, item.UDSName, str(item.value.value)) - except: - None + for key in self.items: + item = self.items[key] + try: if hasattr(item, 'items'): - repeat(item.items.values(), indent + 1) - repeat(self.items.values()) + tag = int.from_bytes(key, byteorder='big') + name = item.name if hasattr(item, 'name') else '' + metadata[tag] = (name, '', '', item.MetadataList()) + else: + metadata[item.TAG] = (item.LDSName, item.ESDName, item.UDSName, str(item.value)) + except Exception as e: + print('Error %d: %s'%(int.from_bytes(key, byteorder='big'),str(e))) + None + return OrderedDict(metadata) def structure(self): @@ -130,12 +134,11 @@ def str_dict(values): out = [] def per_item(value, indent=0): - for item in value: - if isinstance(item): - out.append(indent * "\t" + str(item)) - else: - out.append(indent * "\t" + str(item)) + for key in value: + item = value[key] + out.append(indent * "\t" + str(item)) + if hasattr(item, 'items'): + per_item(item.items, indent + 1) per_item(values) - return '\n'.join(out) diff --git a/klvdata/streamparser.py b/klvdata/streamparser.py index cca231a..629d542 100755 --- a/klvdata/streamparser.py +++ b/klvdata/streamparser.py @@ -43,8 +43,14 @@ def __iter__(self): def __next__(self): key, value = next(self.iter_stream) + if value is None: + return UnknownElement(key, b'') + if key in self.parsers: - return self.parsers[key](value) + try: + return self.parsers[key](value) + except: + return UnknownElement(key, value) else: # Even if KLV is not known, make best effort to parse and preserve. # Element is an abstract super class, do not create instances on Element. diff --git a/test/test_misb_0601.py b/test/test_misb_0601.py index 6b8a711..aa22e49 100755 --- a/test/test_misb_0601.py +++ b/test/test_misb_0601.py @@ -123,7 +123,7 @@ def test_PlatformRollAngle(self): def test_PlatformTrueAirspeed(self): example_value = 147 example_ls_packet = hexstr_to_bytes('08 01 93') - interpretation_string = "147.0" + interpretation_string = "147" from klvdata.misb0601 import PlatformTrueAirspeed self.assertEqual(bytes(PlatformTrueAirspeed(example_value)), example_ls_packet) @@ -133,7 +133,7 @@ def test_PlatformTrueAirspeed(self): def test_PlatformIndicatedAirspeed(self): example_value = 159 example_ls_packet = hexstr_to_bytes('09 01 9f') - interpretation_string = "159.0" + interpretation_string = "159" from klvdata.misb0601 import PlatformIndicatedAirspeed self.assertEqual(bytes(PlatformIndicatedAirspeed(example_value)), example_ls_packet) @@ -714,9 +714,9 @@ def test_SensorEllipsoidHeightConversion(self): example_value = 14190.72 example_ls_packet = hexstr_to_bytes('4B 02 C2 21') - from klvdata.misb0601 import SensorEllipsoidHeightConversion - self.assertEqual(bytes(SensorEllipsoidHeightConversion(example_value)), example_ls_packet) - self.assertEqual(bytes(SensorEllipsoidHeightConversion(example_ls_packet[2:])), example_ls_packet) + from klvdata.misb0601 import SensorEllipsoidHeight + self.assertEqual(bytes(SensorEllipsoidHeight(example_value)), example_ls_packet) + self.assertEqual(bytes(SensorEllipsoidHeight(example_ls_packet[2:])), example_ls_packet) def test_AlternatePlatformEllipsoidHeight(self): # Example value and packet per MISB ST 0601.11, Section 8 "Conversions and Mappings of Metadata Types". From 197e48d79d8505e5c8c8d8912981b6f4eddff775 Mon Sep 17 00:00:00 2001 From: Ricardo Salgado Date: Sat, 21 Nov 2020 01:09:23 +0000 Subject: [PATCH 2/4] Added test to new elements. Corrected errors. --- klvdata/common.py | 10 +-- klvdata/elementparser.py | 7 ++- test/test_common.py | 15 +++++ test/test_misb_0903.py | 130 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 155 insertions(+), 7 deletions(-) create mode 100644 test/test_misb_0903.py diff --git a/klvdata/common.py b/klvdata/common.py index f39b5ab..09e17eb 100755 --- a/klvdata/common.py +++ b/klvdata/common.py @@ -150,14 +150,15 @@ def float_to_imapb(value, _length, _range): return b'' bPow = ceil(log(_max - _min, 2)) - dPow = 8 * length - 1 + dPow = 8 * _length - 1 sF = 2**(dPow - bPow) zOffset = 0.0 if _min < 0 and _max > 0: zOffset = sF * _min - floor(sF * _min) y = int(sF * (value - _min) + zOffset) - return int_to_bytes(y, _length) + + return int_to_bytes(y, _length, signed=True) def imapb_to_float(value, _range): _min, _max = _range @@ -165,12 +166,13 @@ def imapb_to_float(value, _range): bPow = ceil(log(_max - _min, 2)) dPow = 8 * length - 1 + sF = 2**(dPow - bPow) sR = 2**(bPow - dPow) zOffset = 0.0 if _min < 0 and _max > 0: - zOffset = sR * _min - floor(sR * _min) + zOffset = sF * _min - floor(sF * _min) - y = bytes_to_int(value) + y = bytes_to_int(value, signed=True) return sR * (y - zOffset) + _min def packet_checksum(data): diff --git a/klvdata/elementparser.py b/klvdata/elementparser.py index 8900549..e21c21a 100755 --- a/klvdata/elementparser.py +++ b/klvdata/elementparser.py @@ -293,9 +293,10 @@ def __init__(self, value): imapb_to_float(value[8:10], (-900, 19000))) def __bytes__(self): - return (float_to_imapb(self.value(0), 4, (-90, 90)) + - float_to_imapb(self.value(1), 4, (-180, 180)) + - float_to_imapb(self.value(1), 2, (-900, 19000))) + lat, long, alt = self.value + return (float_to_imapb(lat, 4, (-90, 90)) + + float_to_imapb(long, 4, (-180, 180)) + + float_to_imapb(alt, 2, (-900, 19000))) def __str__(self): return str(self.value) \ No newline at end of file diff --git a/test/test_common.py b/test/test_common.py index 0a4cf5b..af999a7 100755 --- a/test/test_common.py +++ b/test/test_common.py @@ -198,6 +198,21 @@ def test_float_signed(self): float_to_bytes(20.0, _domain=(-(2**15-1), 2**15-1), _range=(-20, 20)), b'\x7F\xFF') + def test_float_imapb(self): + from klvdata.common import float_to_imapb + + with self.subTest("IMAP(0.1,0.9,2) x=0.5"): + self.assertEqual( + float_to_imapb(0.5, 2, (0.1,0.9)), + b'\x33\x33') + + def test_float_imapb(self): + from klvdata.common import imapb_to_float + + with self.subTest("IMAP(0.1,0.9,2) y=0x3333"): + self.assertEqual( + imapb_to_float(b'\x33\x33', (0.1,0.9)), + 0.499993896484375) class Checksum(unittest.TestCase): def test_basic1(self): diff --git a/test/test_misb_0903.py b/test/test_misb_0903.py new file mode 100644 index 0000000..c063249 --- /dev/null +++ b/test/test_misb_0903.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 + +import unittest + +from klvdata.common import hexstr_to_bytes + + +class ParserSingleShort(unittest.TestCase): + + def test_checksum(self): + # See MISB ST0903.5 + interpretation = "0xAA43" + tlv_hex_bytes = hexstr_to_bytes('01 02 AA 43') + value = tlv_hex_bytes[2:] + + from klvdata.misb0903 import Checksum + self.assertEqual(str(Checksum(value).value), interpretation) + self.assertEqual(bytes(Checksum(value)), tlv_hex_bytes) + + def test_precisiontimestamp(self): + # See MISB ST0903.5 + interpretation = "2009-01-12 22:08:22+00:00" + tlv_hex_bytes = hexstr_to_bytes('02 08 00 04 60 50 58 4E 01 80') + value = tlv_hex_bytes[2:] + + from klvdata.misb0903 import PrecisionTimeStamp + self.assertEqual(str(PrecisionTimeStamp(value).value), interpretation) + self.assertEqual(bytes(PrecisionTimeStamp(value)), tlv_hex_bytes) + + def test_NumberDetectedTargets(self): + # Example value and packet per MISB ST 0903.5, Section 8.1 "VMTI Local Set". + example_value = 28 + example_ls_packet = hexstr_to_bytes('05 03 00 00 1C') + + from klvdata.misb0903 import NumberDetectedTargets + self.assertEqual(bytes(NumberDetectedTargets(example_value)), example_ls_packet) + self.assertEqual(bytes(NumberDetectedTargets(example_ls_packet[2:])), example_ls_packet) + + + def test_NumberReportedTargets(self): + # Example value and packet per MISB ST 0903.5, Section 8.1 "VMTI Local Set". + example_value = 14 + example_ls_packet = hexstr_to_bytes('06 03 00 00 0E') + + from klvdata.misb0903 import NumberReportedTargets + self.assertEqual(bytes(NumberReportedTargets(example_value)), example_ls_packet) + self.assertEqual(bytes(NumberReportedTargets(example_ls_packet[2:])), example_ls_packet) + + def test_FrameWidth(self): + # Example value and packet per MISB ST 0903.5, Section 8.1 "VMTI Local Set". + example_value = 1920 + example_ls_packet = hexstr_to_bytes('08 03 00 07 80') + + from klvdata.misb0903 import FrameWidth + self.assertEqual(bytes(FrameWidth(example_value)), example_ls_packet) + self.assertEqual(bytes(FrameWidth(example_ls_packet[2:])), example_ls_packet) + + def test_FrameHeight(self): + # Example value and packet per MISB ST 0903.5, Section 8.1 "VMTI Local Set". + example_value = 1080 + example_ls_packet = hexstr_to_bytes('09 03 00 04 38') + + from klvdata.misb0903 import FrameHeight + self.assertEqual(bytes(FrameHeight(example_value)), example_ls_packet) + self.assertEqual(bytes(FrameHeight(example_ls_packet[2:])), example_ls_packet) + + def test_SourceSensor(self): + # Example value and packet per MISB ST 0903.5, Section 8.1 "VMTI Local Set". + example_value = "EO Nose" + example_ls_packet = hexstr_to_bytes('0A 07 45 4F 20 4E 6F 73 65') + + from klvdata.misb0903 import SourceSensor + self.assertEqual(bytes(SourceSensor(example_value)), example_ls_packet) + self.assertEqual(bytes(SourceSensor(example_ls_packet[2:])), example_ls_packet) + + def test_CentroidPixel(self): + # Example value and packet per MISB ST 0903.5, Section 8.2 "VTarget Pack". + example_value = 409600 + example_ls_packet = hexstr_to_bytes('01 03 06 40 00') + + from klvdata.misb0903 import CentroidPixel + self.assertEqual(bytes(CentroidPixel(example_value)), example_ls_packet) + self.assertEqual(bytes(CentroidPixel(example_ls_packet[2:])), example_ls_packet) + + def test_BoundingBoxTopLeftPixel(self): + # Example value and packet per MISB ST 0903.5, Section 8.2 "VTarget Pack". + example_value = 409600 + example_ls_packet = hexstr_to_bytes('02 03 06 40 00') + + from klvdata.misb0903 import BoundingBoxTopLeftPixel + self.assertEqual(bytes(BoundingBoxTopLeftPixel(example_value)), example_ls_packet) + self.assertEqual(bytes(BoundingBoxTopLeftPixel(example_ls_packet[2:])), example_ls_packet) + + def test_BoundingBoxBottomRightPixel(self): + # Example value and packet per MISB ST 0903.5, Section 8.2 "VTarget Pack". + example_value = 409600 + example_ls_packet = hexstr_to_bytes('03 03 06 40 00') + + from klvdata.misb0903 import BoundingBoxBottomRightPixel + self.assertEqual(bytes(BoundingBoxBottomRightPixel(example_value)), example_ls_packet) + self.assertEqual(bytes(BoundingBoxBottomRightPixel(example_ls_packet[2:])), example_ls_packet) + + def test_DetectionCount(self): + # Example value and packet per MISB ST 0903.5, Section 8.2 "VTarget Pack". + example_value = 2765 + example_ls_packet = hexstr_to_bytes('06 02 0A CD') + + from klvdata.misb0903 import DetectionCount + self.assertEqual(bytes(DetectionCount(example_value)), example_ls_packet) + self.assertEqual(bytes(DetectionCount(example_ls_packet[2:])), example_ls_packet) + + def test_TargetIntensity(self): + # Example value and packet per MISB ST 0903.5, Section 8.2 "VTarget Pack". + example_value = 13140 + example_ls_packet = hexstr_to_bytes('09 03 00 33 54') + + from klvdata.misb0903 import TargetIntensity + self.assertEqual(bytes(TargetIntensity(example_value)), example_ls_packet) + self.assertEqual(bytes(TargetIntensity(example_ls_packet[2:])), example_ls_packet) + + def test_TargetLocation(self): + + example_value = (38.725267, -9.150019, 2095) + example_ls_packet = hexstr_to_bytes('40 5c d5 8c 36 ae 6a c6 0B B3') + + from klvdata.misb0903 import TargetLocation + self.assertEqual(bytes(TargetLocation(example_ls_packet))[2:], example_ls_packet) + +if __name__ == '__main__': + unittest.main() From a77a85f80a6b4e8b5746d8ad3d2684a72a91f527 Mon Sep 17 00:00:00 2001 From: Ricardo Salgado Date: Tue, 24 Nov 2020 16:47:23 +0000 Subject: [PATCH 3/4] Added test for real flight data parsing. --- data/realflight.bin | Bin 0 -> 3412 bytes test/test_streamparser.py | 9 +++++++++ 2 files changed, 9 insertions(+) create mode 100644 data/realflight.bin diff --git a/data/realflight.bin b/data/realflight.bin new file mode 100644 index 0000000000000000000000000000000000000000..29866783addb490e09e90ec8c3de6dce6c29547c GIT binary patch literal 3412 zcmd7TYe-XJ7{KxKo*l27OY@Q$$x16TWoM>tPMbHpq><*PmX?`jX_itNCG|mzETSS( zA*E91txU=rB_gFo1Xe=gg$%MJEQJu$q#;)C9uG2f2VAkymxseXY`^EfcYBTbTRcfF zLVUzQh#-h_h$c-5qZ^5}&wlxH9FAq=JUc5-5E>lh9ug<^qG=vKEG_$BT5d+FAU;ek@wA}CGf|CvO0{2HZAn_pT9Kx? zD=GY0gf*3qrwvS7cx4ql+rxAs3Bnnz-9UQEXlH~-5+M#H6h`Zv5P8wg*3lS0zxS zcgp2N3b}ncpR^viGza;W(D|jEH9cd|`wsKhdB08yY@JrR*;?M*WE0l=C7F|WH6M|= zCdP{r7L+mtyQlNimHu{r9oAV5>l}e~>|vc2&2<=RRW~cO_x@K*m1IYR@;0MsCpQ;t# zq3S%f$8>QAdHA4%DfI1F)ECmBvP`Ls4p~ z1ghSHs;zpe+6>epp!NW@A6IpFSd6QRGAEd-0~V}QGgsMvMQwbwgjzj+1=LxpA~4k* zs#ZhQc3rA!T!G8~i>;W{I19rN_%RObbY=_(mQYhn;JeqBsF{na&c5N}zo?f_Rq}vp z0%u{Dnz}t$3$?V2l`1%W6jKu*uo41y=@NMP)XFBHh6B|XsF?{AnmxtAReyh8!&D9M zVWqmHv>Ax{9;lH(4FKu|EmYTctW?@TimBmHwE?QW)Kk@3poRj~5vaNN09co)aMf>H zZ!uM4kF!$6lc|QGDmMdF0n|G{Ezm-Bf67Yr?@(as{KeG{Rp02T>L5^MK$QUXB&OOG z&fuyevL8&<2@xwbti0bq)GI(ep>Y6iYoYcwu~MU=&SL8P#Wes`-|DGqA5bp?^$=8j SgsJv>9dXs6A~{pFq4N*mBI67I literal 0 HcmV?d00001 diff --git a/test/test_streamparser.py b/test/test_streamparser.py index 0790d23..229cbc4 100755 --- a/test/test_streamparser.py +++ b/test/test_streamparser.py @@ -39,6 +39,15 @@ def test_singlepacket(self): # packet.structure() pass + def test_parsefullfile(self): + # Realworld example + with open('./data/klv.bin', 'rb') as f: + packet = f.read() + + from klvdata.streamparser import StreamParser + + for packet in StreamParser(packet): + metadata=packet.MetadataList() if __name__ == "__main__": unittest.main() From afdf5b8ebebdc577b86c426f9b59b90dde563242 Mon Sep 17 00:00:00 2001 From: Ricardo Salgado Date: Tue, 24 Nov 2020 16:50:36 +0000 Subject: [PATCH 4/4] Added test for real flight data parsing. --- test/test_streamparser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_streamparser.py b/test/test_streamparser.py index 229cbc4..af09a87 100755 --- a/test/test_streamparser.py +++ b/test/test_streamparser.py @@ -41,7 +41,7 @@ def test_singlepacket(self): def test_parsefullfile(self): # Realworld example - with open('./data/klv.bin', 'rb') as f: + with open('./data/realflight.bin', 'rb') as f: packet = f.read() from klvdata.streamparser import StreamParser