From c498343911c4b777e4aaeeebfa07983f9a60afda Mon Sep 17 00:00:00 2001 From: Leon Morten Richter Date: Sun, 8 May 2022 16:00:19 +0200 Subject: [PATCH 1/4] add a *get_sotdma_comm_state* method --- pyais/constants.py | 10 ++++++ pyais/util.py | 86 +++++++++++++++++++++++++++++++++++++++++++- tests/test_decode.py | 49 +++++++++++++++++++++++-- 3 files changed, 142 insertions(+), 3 deletions(-) diff --git a/pyais/constants.py b/pyais/constants.py index a66f4db..8156c7a 100644 --- a/pyais/constants.py +++ b/pyais/constants.py @@ -311,3 +311,13 @@ def _missing_(cls, value: object) -> int: @classmethod def from_value(cls, v: typing.Optional[typing.Any]) -> typing.Optional["StationIntervals"]: return cls(v) if v is not None else None + + +class SyncState(IntEnum): + """ + https://www.navcen.uscg.gov/?pageName=AISMessagesA#Sync + """ + UTC_DIRECT = 0x00 + UTC_INDIRECT = 0x01 + BASE_DIRECT = 0x02 + BASE_INDIRECT = 0x04 diff --git a/pyais/util.py b/pyais/util.py index 79ca6ca..03dee61 100644 --- a/pyais/util.py +++ b/pyais/util.py @@ -3,10 +3,12 @@ from collections import OrderedDict from functools import partial, reduce from operator import xor -from typing import Any, Generator, Hashable, TYPE_CHECKING, Union +from typing import Any, Generator, Hashable, TYPE_CHECKING, Union, Dict from bitarray import bitarray +from pyais.constants import SyncState + if TYPE_CHECKING: BaseDict = OrderedDict[Hashable, Any] else: @@ -286,3 +288,85 @@ def chk_to_int(chk_str: bytes) -> typing.Tuple[int, int]: except (IndexError, ValueError): checksum = -1 return fill_bits, checksum + + +SYNC_MASK = 0x03 +TIMEOUT_MASK = 0x07 +MSG_MASK = 0x3fff + + +def get_sotdma_comm_state(radio: int) -> Dict[str, int]: + """ + This method returns the SOTDMA communication state for messages, + that have a `radio` field (e.g. 1,2,4,9,18). + + @param msg: The raw (uninterpreted) integer value as returned by + calling `decode` method. + @return: A dictionary holding the interpreted data. The dict has + the following keys: + + - slot_number + - utc_hour + - utc_minute + - slot_offset + - slot_timeout + - sync_state + + The SOTDMA communication state is structured as follows: + +-------------------+----------------------+------------------------------------------------------------------------------------------------+ + | Parameter | Number of bits | Description | + +-------------------+----------------------+------------------------------------------------------------------------------------------------+ + | Sync state | 2 | 0 UTC direct | + | | | 1 UTC indirect | + | | | 2 Station is synchronized to a base station | + | | | 3 Station is synchronized to another station based on the highest number of received stations | + | Slot time-out | 3 | Specifies frames remaining until a new slot is selected | + | | | 0 means that this was the last transmission in this slot | + | | | 1-7 means that 1 to 7 frames respectively are left until slot change | + | Sub message | 14 | 14 The sub message depends on the current value in slot time-out | + +-------------------+----------------------+------------------------------------------------------------------------------------------------+ + + The slot time-out defines how to interpret the sub message: + +-----------------+---------------------------------------------------------------------------+ + | Slot time-out | Description | + +-----------------+---------------------------------------------------------------------------+ + | 3, 5, 7 | Number of receiving stations (not own station) (between 0 and 16 383) | + | 2, 4, 6 | Slot number Slot number used for this transmission (between 0 and 2 249) | + | 1 | UTC hour (bits 13 to 9) and minute (bits 8 to 2) | + | 0 | Next frame | + +-----------------+---------------------------------------------------------------------------+ + + You may refer to: + - https://github.com/M0r13n/pyais/issues/17 + - https://www.itu.int/dms_pubrec/itu-r/rec/m/R-REC-M.1371-1-200108-S!!PDF-E.pdf + - https://www.navcen.uscg.gov/?pageName=AISMessagesA#Sync + """ + result = { + 'received_stations': 0, + 'slot_number': 0, + 'utc_hour': 0, + 'utc_minute': 0, + 'slot_offset': 0, + 'slot_timeout': 0, + 'sync_state': 0, + } + + sync_state = (radio >> 17) & SYNC_MASK # First two (2) bits + slot_timeout = (radio >> 14) & TIMEOUT_MASK # Next three (3) bits + sub_msg = radio & MSG_MASK # Last 14 bits + + if slot_timeout == 0: + result['slot_offset'] = sub_msg + elif slot_timeout == 1: + result['utc_hour'] = (sub_msg >> 9) & 0xf + result['utc_minute'] = (sub_msg >> 2) & 0x3f + elif slot_timeout in (2, 4, 6): + result['slot_number'] = sub_msg + elif slot_timeout in (3, 5, 7): + result['received_stations'] = sub_msg + else: + raise ValueError("Slot timeout can only be an integer between 0 and 7") + + result['sync_state'] = SyncState(sync_state) + result['slot_timeout'] = slot_timeout + return result diff --git a/tests/test_decode.py b/tests/test_decode.py index 1f670b2..b203f55 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -9,7 +9,7 @@ from pyais import NMEAMessage, encode_dict from pyais.ais_types import AISType from pyais.constants import (EpfdType, ManeuverIndicator, NavAid, - NavigationStatus, ShipType, StationType, + NavigationStatus, ShipType, StationType, SyncState, TransmitMode) from pyais.decode import decode from pyais.exceptions import UnknownMessageException @@ -26,7 +26,7 @@ MessageType26BroadcastUnstructured, from_turn, to_turn) from pyais.stream import ByteStream -from pyais.util import b64encode_str, bits2bytes, bytes2bits +from pyais.util import b64encode_str, bits2bytes, bytes2bits, get_sotdma_comm_state def ensure_type_for_msg_dict(msg_dict: typing.Dict[str, typing.Any]) -> None: @@ -1256,3 +1256,48 @@ def test_rot_decode_yields_expected_values(self): assert decode(b"!AIVDM,1,1,,2,13aB:Hhuh0PHjEFNKJg@11sH08J=,0*1E").turn == -4.0 assert decode(b"!AIVDM,1,1,,A,16:VFv0k0I`KQPpFATG4SgvT40:v,0*7B").turn == -121.0 assert decode(b"!AIVDM,1,1,,B,16:D3F0:15`5ogh1Dd2L1<,0*0B").turn == 64.0 + + def test_get_sotdma_comm_state_utc_direct(self): + msg = "!AIVDM,1,1,,A,13HOI:0P0000VOHLCnHQKwvL05Ip,0*23" + decoded = decode(msg) + actual = get_sotdma_comm_state(decoded.radio) + + self.assertEqual(actual, { + 'received_stations': 0, + 'slot_number': 0, + 'utc_hour': 11, + 'utc_minute': 30, + 'slot_offset': 0, + 'slot_timeout': 1, + 'sync_state': SyncState.UTC_DIRECT, + }) + + def test_get_sotdma_comm_state_utc_direct_slot_number(self): + msg = "!AIVDM,1,1,,B,403OtVAv>lba;o?Ia`E`4G?02H6k,0*44" + decoded = decode(msg) + actual = get_sotdma_comm_state(decoded.radio) + + self.assertEqual(actual, { + 'received_stations': 0, + 'slot_number': 435, + 'utc_hour': 0, + 'utc_minute': 0, + 'slot_offset': 0, + 'slot_timeout': 6, + 'sync_state': SyncState.UTC_DIRECT, + }) + + def test_get_sotdma_comm_state_utc_direct_slot_timeout(self): + msg = "!AIVDM,1,1,,B,91b55wi;hbOS@OdQAC062Ch2089h,0*30" + decoded = decode(msg) + actual = get_sotdma_comm_state(decoded.radio) + + self.assertEqual(actual, { + 'received_stations': 0, + 'slot_number': 624, + 'utc_hour': 0, + 'utc_minute': 0, + 'slot_offset': 0, + 'slot_timeout': 2, + 'sync_state': SyncState.UTC_DIRECT, + }) From a97e356598cdfa658eec064d9fd468a3fa7f2cd0 Mon Sep 17 00:00:00 2001 From: Leon Morten Richter Date: Sun, 8 May 2022 16:19:06 +0200 Subject: [PATCH 2/4] make newest mypy happy --- pyais/messages.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyais/messages.py b/pyais/messages.py index a370668..e1896ef 100644 --- a/pyais/messages.py +++ b/pyais/messages.py @@ -481,14 +481,14 @@ def asdict(self, enum_as_int: bool = False) -> typing.Dict[str, typing.Optional[ """ if enum_as_int: d: typing.Dict[str, typing.Optional[NMEA_VALUE]] = {} - for slt in self.__slots__: + for slt in self.__slots__: # type: ignore val = getattr(self, slt) if val is not None and slt in ENUM_FIELDS: val = int(getattr(self, slt)) d[slt] = val return d else: - return {slt: getattr(self, slt) for slt in self.__slots__} + return {slt: getattr(self, slt) for slt in self.__slots__} # type: ignore def to_json(self) -> str: return JSONEncoder(indent=4).encode(self.asdict()) From 00736abbecfc97f2f7dbb3bd578815d182a2c478 Mon Sep 17 00:00:00 2001 From: Leon Morten Richter Date: Fri, 13 May 2022 16:27:31 +0200 Subject: [PATCH 3/4] implement itdma state --- pyais/constants.py | 2 +- pyais/messages.py | 72 +++++++++++++++++++++++---- pyais/util.py | 63 ++++++++++++++++-------- tests/test_decode.py | 114 +++++++++++++++++++++++++++++++++++++------ 4 files changed, 205 insertions(+), 46 deletions(-) diff --git a/pyais/constants.py b/pyais/constants.py index 8156c7a..bbfbe6d 100644 --- a/pyais/constants.py +++ b/pyais/constants.py @@ -320,4 +320,4 @@ class SyncState(IntEnum): UTC_DIRECT = 0x00 UTC_INDIRECT = 0x01 BASE_DIRECT = 0x02 - BASE_INDIRECT = 0x04 + BASE_INDIRECT = 0x03 diff --git a/pyais/messages.py b/pyais/messages.py index e1896ef..56e63b3 100644 --- a/pyais/messages.py +++ b/pyais/messages.py @@ -11,7 +11,7 @@ TransmitMode, StationIntervals from pyais.exceptions import InvalidNMEAMessageException, UnknownMessageException, UnknownPartNoException, \ InvalidDataTypeException -from pyais.util import decode_into_bit_array, compute_checksum, int_to_bin, str_to_bin, \ +from pyais.util import decode_into_bit_array, compute_checksum, get_itdma_comm_state, get_sotdma_comm_state, int_to_bin, str_to_bin, \ encode_ascii_6, from_bytes, from_bytes_signed, decode_bin_as_ascii6, get_int, chk_to_int, coerce_val, \ bits2bytes, bytes2bits, b64encode_str @@ -549,8 +549,62 @@ def from_turn(turn: typing.Optional[typing.Union[int, float]]) -> int: return int(math.copysign(round(4.733 * math.sqrt(abs(turn))), turn)) +class CommunicationStateMixin: + """ + Mixin class to access Communication State values by applicable messages. + + You may refer to 3.3.7.2.1 of: + https://www.itu.int/dms_pubrec/itu-r/rec/m/R-REC-M.1371-1-200108-S!!PDF-E.pdf + """ + + radio: int # Type hint to make mypy happy + + MAX_COMM_STATE_VALUE = 0x7ffff + + def get_communication_state(self) -> Dict[str, typing.Optional[int]]: + result: Dict[str, typing.Optional[int]] = { + 'received_stations': None, + 'slot_number': None, + 'utc_hour': None, + 'utc_minute': None, + 'slot_offset': None, + 'slot_timeout': None, + 'sync_state': None, + 'keep_flag': None, + 'slot_increment': None, + 'num_slots': None, + } + + if self.is_sotdma: + result.update(get_sotdma_comm_state(self.communication_state_raw)) + else: + result.update(get_itdma_comm_state(self.communication_state_raw)) + + return result + + @property + def is_sotdma(self) -> bool: + """The radio status field has it's 20th bit (MSB) set to 0 or has less than 20 bits""" + return self.radio <= self.MAX_COMM_STATE_VALUE + + @property + def is_itdma(self) -> bool: + """The radio status field has it's 20th bit (MSB) set to 1""" + return self.radio > self.MAX_COMM_STATE_VALUE + + @property + def communication_state_raw(self) -> int: + """Get the raw radio status except 20th bit - if present""" + try: + return self.radio & self.MAX_COMM_STATE_VALUE + except AttributeError as err: + raise ValueError( + 'Communication State is only available for messages with radio field' + ) from err + + @attr.s(slots=True) -class MessageType1(Payload): +class MessageType1(Payload, CommunicationStateMixin): """ AIS Vessel position report using SOTDMA (Self-Organizing Time Division Multiple Access) Src: https://gpsd.gitlab.io/gpsd/AIVDM.html#_types_1_2_and_3_position_report_class_a @@ -591,7 +645,7 @@ class MessageType3(MessageType1): @attr.s(slots=True) -class MessageType4(Payload): +class MessageType4(Payload, CommunicationStateMixin): """ AIS Vessel position report using SOTDMA (Self-Organizing Time Division Multiple Access) Src: https://gpsd.gitlab.io/gpsd/AIVDM.html#_type_4_base_station_report @@ -698,7 +752,7 @@ class MessageType8(Payload): @attr.s(slots=True) -class MessageType9(Payload): +class MessageType9(Payload, CommunicationStateMixin): """ Standard SAR Aircraft Position Report Src: https://gpsd.gitlab.io/gpsd/AIVDM.html#_type_9_standard_sar_aircraft_position_report @@ -843,7 +897,7 @@ class MessageType17(Payload): @attr.s(slots=True) -class MessageType18(Payload): +class MessageType18(Payload, CommunicationStateMixin): """ Standard Class B CS Position Report Src: https://gpsd.gitlab.io/gpsd/AIVDM.html#_type_18_standard_class_b_cs_position_report @@ -1247,7 +1301,7 @@ def from_bitarray(cls, bit_arr: bitarray) -> "ANY_MESSAGE": @attr.s(slots=True) -class MessageType26AddressedStructured(Payload): +class MessageType26AddressedStructured(Payload, CommunicationStateMixin): msg_type = bit_field(6, int, default=26, signed=False) repeat = bit_field(2, int, default=0, signed=False) mmsi = bit_field(30, int, from_converter=from_mmsi) @@ -1262,7 +1316,7 @@ class MessageType26AddressedStructured(Payload): @attr.s(slots=True) -class MessageType26BroadcastStructured(Payload): +class MessageType26BroadcastStructured(Payload, CommunicationStateMixin): msg_type = bit_field(6, int, default=26, signed=False) repeat = bit_field(2, int, default=0, signed=False) mmsi = bit_field(30, int, from_converter=from_mmsi) @@ -1276,7 +1330,7 @@ class MessageType26BroadcastStructured(Payload): @attr.s(slots=True) -class MessageType26AddressedUnstructured(Payload): +class MessageType26AddressedUnstructured(Payload, CommunicationStateMixin): msg_type = bit_field(6, int, default=26, signed=False) repeat = bit_field(2, int, default=0, signed=False) mmsi = bit_field(30, int, from_converter=from_mmsi) @@ -1291,7 +1345,7 @@ class MessageType26AddressedUnstructured(Payload): @attr.s(slots=True) -class MessageType26BroadcastUnstructured(Payload): +class MessageType26BroadcastUnstructured(Payload, CommunicationStateMixin): msg_type = bit_field(6, int, default=26, signed=False) repeat = bit_field(2, int, default=0, signed=False) mmsi = bit_field(30, int, from_converter=from_mmsi) diff --git a/pyais/util.py b/pyais/util.py index 03dee61..ec8ce17 100644 --- a/pyais/util.py +++ b/pyais/util.py @@ -293,25 +293,11 @@ def chk_to_int(chk_str: bytes) -> typing.Tuple[int, int]: SYNC_MASK = 0x03 TIMEOUT_MASK = 0x07 MSG_MASK = 0x3fff +SLOT_INCREMENT_MASK = 0x1fff -def get_sotdma_comm_state(radio: int) -> Dict[str, int]: +def get_sotdma_comm_state(radio: int) -> Dict[str, typing.Optional[int]]: """ - This method returns the SOTDMA communication state for messages, - that have a `radio` field (e.g. 1,2,4,9,18). - - @param msg: The raw (uninterpreted) integer value as returned by - calling `decode` method. - @return: A dictionary holding the interpreted data. The dict has - the following keys: - - - slot_number - - utc_hour - - utc_minute - - slot_offset - - slot_timeout - - sync_state - The SOTDMA communication state is structured as follows: +-------------------+----------------------+------------------------------------------------------------------------------------------------+ | Parameter | Number of bits | Description | @@ -342,11 +328,11 @@ def get_sotdma_comm_state(radio: int) -> Dict[str, int]: - https://www.navcen.uscg.gov/?pageName=AISMessagesA#Sync """ result = { - 'received_stations': 0, - 'slot_number': 0, - 'utc_hour': 0, - 'utc_minute': 0, - 'slot_offset': 0, + 'received_stations': None, + 'slot_number': None, + 'utc_hour': None, + 'utc_minute': None, + 'slot_offset': None, 'slot_timeout': 0, 'sync_state': 0, } @@ -370,3 +356,38 @@ def get_sotdma_comm_state(radio: int) -> Dict[str, int]: result['sync_state'] = SyncState(sync_state) result['slot_timeout'] = slot_timeout return result + + +def get_itdma_comm_state(radio: int) -> Dict[str, typing.Optional[int]]: + """ + +-----------------+------+--------------------------------------------------------------------------------+ + | Parameter | Bits | Description | + +-----------------+------+--------------------------------------------------------------------------------+ + | Sync state | 2 | 0 UTC direct | + | | | 1 UTC indirec | + | | | 2 Station is synchronized to a base station | + | | | 3 Station is synchronized to another station | + | Slot increment | 13 | Offset to next slot to be used, or zero (0) if no more transmissions | + | Number of slots | 3 | Number of consecutive slots to allocate. (0 = 1 slot, 1 = 2 slots,2 = 3 slots, | + | | | 3 = 4 slots, 4 = 5 slots) | + | Keep flag | 1 | Set to TRUE = 1 if the slot remains allocated for one additional frame | + +-----------------+------+--------------------------------------------------------------------------------+ + + You may refer to: + - https://github.com/M0r13n/pyais/issues/17 + - https://www.itu.int/dms_pubrec/itu-r/rec/m/R-REC-M.1371-1-200108-S!!PDF-E.pdf + - https://www.navcen.uscg.gov/?pageName=AISMessagesA#Sync + """ + + sync_state = (radio >> 17) & SYNC_MASK # First two (2) bits + slot_increment = (radio >> 4) & SLOT_INCREMENT_MASK # Next 13 bits + num_slots = (radio >> 1) & TIMEOUT_MASK # Next three (3) bits + keep_flag = radio & 0x01 # Last bit + + return { + 'keep_flag': keep_flag, + 'sync_state': sync_state, + 'slot_increment': slot_increment, + 'num_slots': num_slots, + 'keep_flag': keep_flag, + } diff --git a/tests/test_decode.py b/tests/test_decode.py index b203f55..e7e6306 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -26,7 +26,7 @@ MessageType26BroadcastUnstructured, from_turn, to_turn) from pyais.stream import ByteStream -from pyais.util import b64encode_str, bits2bytes, bytes2bits, get_sotdma_comm_state +from pyais.util import b64encode_str, bits2bytes, bytes2bits def ensure_type_for_msg_dict(msg_dict: typing.Dict[str, typing.Any]) -> None: @@ -1260,44 +1260,128 @@ def test_rot_decode_yields_expected_values(self): def test_get_sotdma_comm_state_utc_direct(self): msg = "!AIVDM,1,1,,A,13HOI:0P0000VOHLCnHQKwvL05Ip,0*23" decoded = decode(msg) - actual = get_sotdma_comm_state(decoded.radio) + actual = decoded.get_communication_state() + + assert decoded.is_sotdma + assert not decoded.is_itdma self.assertEqual(actual, { - 'received_stations': 0, - 'slot_number': 0, + 'received_stations': None, + 'slot_number': None, 'utc_hour': 11, 'utc_minute': 30, - 'slot_offset': 0, + 'slot_offset': None, 'slot_timeout': 1, 'sync_state': SyncState.UTC_DIRECT, + 'keep_flag': None, + 'slot_increment': None, + 'num_slots': None, }) def test_get_sotdma_comm_state_utc_direct_slot_number(self): msg = "!AIVDM,1,1,,B,403OtVAv>lba;o?Ia`E`4G?02H6k,0*44" decoded = decode(msg) - actual = get_sotdma_comm_state(decoded.radio) + actual = decoded.get_communication_state() + + assert decoded.is_sotdma + assert not decoded.is_itdma self.assertEqual(actual, { - 'received_stations': 0, + 'received_stations': None, 'slot_number': 435, - 'utc_hour': 0, - 'utc_minute': 0, - 'slot_offset': 0, + 'utc_hour': None, + 'utc_minute': None, + 'slot_offset': None, 'slot_timeout': 6, 'sync_state': SyncState.UTC_DIRECT, + 'keep_flag': None, + 'slot_increment': None, + 'num_slots': None, }) def test_get_sotdma_comm_state_utc_direct_slot_timeout(self): msg = "!AIVDM,1,1,,B,91b55wi;hbOS@OdQAC062Ch2089h,0*30" decoded = decode(msg) - actual = get_sotdma_comm_state(decoded.radio) + actual = decoded.get_communication_state() + + assert decoded.is_sotdma + assert not decoded.is_itdma self.assertEqual(actual, { - 'received_stations': 0, + 'received_stations': None, 'slot_number': 624, - 'utc_hour': 0, - 'utc_minute': 0, - 'slot_offset': 0, + 'utc_hour': None, + 'utc_minute': None, + 'slot_offset': None, 'slot_timeout': 2, 'sync_state': SyncState.UTC_DIRECT, + 'keep_flag': None, + 'slot_increment': None, + 'num_slots': None, }) + + def test_get_comm_state_type_18_itdma_base_indirect(self): + msg = '!AIVDM,1,1,,A,B5NJ;PP005l4ot5Isbl03wsUkP06,0*76' + decoded = decode(msg) + + assert decoded.communication_state_raw == 393222 + assert decoded.is_itdma + assert not decoded.is_sotdma + + comm_state = decoded.get_communication_state() + + assert isinstance(comm_state, dict) + assert comm_state['received_stations'] is None + assert comm_state['slot_number'] is None + assert comm_state['utc_hour'] is None + assert comm_state['utc_minute'] is None + assert comm_state['slot_offset'] is None + assert comm_state['slot_timeout'] is None + assert comm_state['sync_state'] == SyncState.BASE_INDIRECT + assert comm_state['keep_flag'] == 0 + assert comm_state['slot_increment'] == 0 + assert comm_state['num_slots'] == 3 + + def test_get_comm_state_type_18_sotdma_utc_direct(self): + msg = '!AIVDM,1,1,,A,B69A5U@3wk?8mP=18D3Q3wSRPD00,0*5C' + decoded = decode(msg) + + assert decoded.communication_state_raw == 81920 + assert not decoded.is_itdma + assert decoded.is_sotdma + + comm_state = decoded.get_communication_state() + + assert isinstance(comm_state, dict) + assert comm_state['received_stations'] == 0 + assert comm_state['slot_number'] is None + assert comm_state['utc_hour'] is None + assert comm_state['utc_minute'] is None + assert comm_state['slot_offset'] is None + assert comm_state['slot_timeout'] == 5 + assert comm_state['sync_state'] == SyncState.UTC_DIRECT + assert comm_state['keep_flag'] is None + assert comm_state['slot_increment'] is None + assert comm_state['num_slots'] is None + + def test_get_comm_state_type_18_sotdma_base_inidrect(self): + msg = '!AIVDM,1,1,,A,B69Gk3h071tpI02lT2ek?wg61P06,0*1F' + decoded = decode(msg) + + assert decoded.communication_state_raw == 393222 + assert not decoded.is_itdma + assert decoded.is_sotdma + + comm_state = decoded.get_communication_state() + + assert isinstance(comm_state, dict) + assert comm_state['received_stations'] is None + assert comm_state['slot_number'] is None + assert comm_state['utc_hour'] is None + assert comm_state['utc_minute'] is None + assert comm_state['slot_offset'] == 6 + assert comm_state['slot_timeout'] == 0 + assert comm_state['sync_state'] == SyncState.BASE_INDIRECT + assert comm_state['keep_flag'] is None + assert comm_state['slot_increment'] is None + assert comm_state['num_slots'] is None From fdb2ab4cb27aa0a88b163ef1a9a6e00cba610518 Mon Sep 17 00:00:00 2001 From: Leon Morten Richter Date: Sat, 14 May 2022 14:10:09 +0200 Subject: [PATCH 4/4] add missing documentation/examples for SOTDMA/ITDMA --- .github/workflows/main.yml | 2 +- .github/workflows/pythonpublish.yml | 2 +- CHANGELOG.txt | 7 ++++++ docs/communication_state.rst | 39 +++++++++++++++++++++++++++++ docs/conf.py | 4 +-- docs/examples/file.rst | 4 +-- docs/index.rst | 2 +- examples/communication_state.py | 19 ++++++++++++++ pyais/__init__.py | 2 +- tests/test_examples.py | 2 +- 10 files changed, 74 insertions(+), 9 deletions(-) create mode 100644 docs/communication_state.rst create mode 100644 examples/communication_state.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 087c1a3..a1671e7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,7 +9,7 @@ jobs: strategy: fail-fast: false matrix: - python: ['3.7', '3.8', '3.9'] + python: ['3.7', '3.8', '3.9', '3.10'] os: ['ubuntu-latest'] steps: - uses: actions/checkout@master diff --git a/.github/workflows/pythonpublish.yml b/.github/workflows/pythonpublish.yml index bdb40d4..8865702 100644 --- a/.github/workflows/pythonpublish.yml +++ b/.github/workflows/pythonpublish.yml @@ -2,7 +2,7 @@ name: Upload Python Package on: release: - types: [published, created] + types: [published] jobs: deploy: diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 93ccff4..1c59948 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,13 @@ ==================== pyais CHANGELOG ==================== +------------------------------------------------------------------------------- + Version 2.1.2 14 May 2022 +------------------------------------------------------------------------------- +* Closes https://github.com/M0r13n/pyais/issues/17 + * decoded `radio state` fields + * provided functions to access SOTDMA/ITDMA communication state information + ------------------------------------------------------------------------------- Version 2.1.1 24 Apr 2022 ------------------------------------------------------------------------------- diff --git a/docs/communication_state.rst b/docs/communication_state.rst new file mode 100644 index 0000000..1932430 --- /dev/null +++ b/docs/communication_state.rst @@ -0,0 +1,39 @@ +################################## +SOTDMA / ITDMA communication state +################################## + +Certain message types (1,2,4,9,18) contain diagnostic information for the radio system. +This information is encoded in up to 20 bits of data in the `radio` field and is +used in planning for the next transmission in order to avoiding mutual interference. + +There are two different communication states used in AIS messages: + +1. **SOTDMA**: contains information used by the slot allocation algorithm in the SOTDMA concept +2. **ITDMA**: contains information used by the slot allocation algorithm in the ITDMA concept + +The concrete details of these concepts are out of the scope oh this document. +Your may refer to [ITU-R M.1371-1 ](https://www.itu.int/dms_pubrec/itu-r/rec/m/R-REC-M.1371-1-200108-S!!PDF-E.pdf). + +**pyais** offers the following methods: + +.. py:function:: get_communication_state() + + Return the communication state decoded from the `radio` field. + + :return: A dictionary containing the decoded data. Not all fields + are set. Some default to `None` + :rtype: Dict[str, Optional[int]] + + +.. py:function:: is_sotdma + + Return True if the communication state contains SOTDMA data + + :rtype: bool + + +.. py:function:: is_itdma + + Return True if the communication state contains ITDMA data + + :rtype: bool diff --git a/docs/conf.py b/docs/conf.py index 8be137f..4ab1413 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,11 +18,11 @@ # -- Project information ----------------------------------------------------- project = 'Pyais' -copyright = '2021, Leon Morten Richter' +copyright = '2022, Leon Morten Richter' author = 'Leon Morten Richter' # The full version, including alpha/beta/rc tags -release = '1.5.0' +release = '2.1.1' # -- General configuration --------------------------------------------------- diff --git a/docs/examples/file.rst b/docs/examples/file.rst index a4386cb..edc1c45 100644 --- a/docs/examples/file.rst +++ b/docs/examples/file.rst @@ -1,6 +1,6 @@ -############### +######################### Reading and parsing files -############### +######################### Examples diff --git a/docs/index.rst b/docs/index.rst index 0fedc1d..2092275 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,7 +13,7 @@ Welcome to Pyais's documentation! install usage messages - + communication_state Indices and tables diff --git a/examples/communication_state.py b/examples/communication_state.py new file mode 100644 index 0000000..f562f53 --- /dev/null +++ b/examples/communication_state.py @@ -0,0 +1,19 @@ +""" +The following example shows how you can get the communication state +of a message. This works for message types 1, 2, 4, 9, 11 and 18. + +These messages contain diagnostic information for the radio system. +""" +from pyais import decode +import json +import functools + +msg = '!AIVDM,1,1,,A,B69Gk3h071tpI02lT2ek?wg61P06,0*1F' +decoded = decode(msg) + +print("The raw radio value is:", decoded.radio) +print("Communication state is SOTMDA:", decoded.is_sotdma) +print("Communication state is ITDMA:", decoded.is_itdma) + +pretty_json = functools.partial(json.dumps, indent=4) +print("Communication state:", pretty_json(decoded.get_communication_state())) diff --git a/pyais/__init__.py b/pyais/__init__.py index ee21463..13c76d8 100644 --- a/pyais/__init__.py +++ b/pyais/__init__.py @@ -4,7 +4,7 @@ from pyais.decode import decode __license__ = 'MIT' -__version__ = '2.1.1' +__version__ = '2.1.2' __author__ = 'Leon Morten Richter' __all__ = ( diff --git a/tests/test_examples.py b/tests/test_examples.py index a12e055..bbb555d 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -22,4 +22,4 @@ def test_run_every_file(self): if csv_file.exists(): csv_file.unlink() - assert i == 9 + assert i == 10