Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add a *get_sotdma_comm_state* method #68

Merged
merged 4 commits into from
May 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pythonpublish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Upload Python Package

on:
release:
types: [published, created]
types: [published]

jobs:
deploy:
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.txt
Original file line number Diff line number Diff line change
@@ -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
-------------------------------------------------------------------------------
Expand Down
39 changes: 39 additions & 0 deletions docs/communication_state.rst
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 ---------------------------------------------------

Expand Down
4 changes: 2 additions & 2 deletions docs/examples/file.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
###############
#########################
Reading and parsing files
###############
#########################


Examples
Expand Down
2 changes: 1 addition & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Welcome to Pyais's documentation!
install
usage
messages

communication_state


Indices and tables
Expand Down
19 changes: 19 additions & 0 deletions examples/communication_state.py
Original file line number Diff line number Diff line change
@@ -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()))
2 changes: 1 addition & 1 deletion pyais/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from pyais.decode import decode

__license__ = 'MIT'
__version__ = '2.1.1'
__version__ = '2.1.2'
__author__ = 'Leon Morten Richter'

__all__ = (
Expand Down
10 changes: 10 additions & 0 deletions pyais/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = 0x03
76 changes: 65 additions & 11 deletions pyais/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down
Loading