Repository: M0r13n/pyais

Issue:
Determination of the communication status field
## Problem
It seems that the current method of detecting and distinguishing communication status fields is not entirely reliable. For example, the following example incorrectly indicates that it is a SOTDMA communication status, although it is an ITDMA.
```python
decoded = pyais.decode('!AIVDM,1,1,,A,333ga6PP0jPPDUNM3I7DTOwDR59C,0*51')
com = decoded.get_communication_state()
print(f"SOTDMA: {decoded.is_sotdma}, ITDMA: {decoded.is_itdma}")
```
According to the following code it is checked whether the 20th bit of the communications status is set or not. However, as the com state is only 19 bits long, a bit from the previous data fields is checked here and not one from the com state. In the case of a type 1, 2 or 3 message, it would be checked whether the RAIM flag is set. Since 1 is the default value, pyais returns in almost all cases that it is a SOTDMA com state.
```python
@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
```
## Solution
It is better to use the message type rather than the bit length to indicate the type of communication status. Table 46 of the M.1371-5 standard shows which type uses which com state. Type 9, 18 and 26 messages are edge cases, as they can include one of the two com state types. The existing implementation can be used here, as with these message types the bit before the actual com state determines its type.

Discussion:

Patch:
diff --git a/pyais/messages.py b/pyais/messages.py
index 3f6ef48..0acdd68 100644
--- a/pyais/messages.py
+++ b/pyais/messages.py
@@ -797,10 +797,14 @@ class CommunicationStateMixin:
     https://www.itu.int/dms_pubrec/itu-r/rec/m/R-REC-M.1371-1-200108-S!!PDF-E.pdf
     """
 
+    msg_type: int  # Type hint to make mypy happy
     radio: int  # Type hint to make mypy happy
 
     MAX_COMM_STATE_VALUE = 0x7ffff
 
+    SOTDMA_TYPES = (1, 2, 4, 11)
+    SOTDMA_ITDMA_TYPES = (9, 18, 26)
+
     def get_communication_state(self) -> Dict[str, typing.Optional[int]]:
         """Returns information used by the slot allocation algorithm as a dict."""
         result: Dict[str, typing.Optional[int]] = {
@@ -825,13 +829,21 @@ class CommunicationStateMixin:
 
     @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
+        """Messages of type 1, 2, 4, 11 use SOTDMA or 9, 18, 26 if 20th bit is set."""
+        if self.msg_type in self.SOTDMA_TYPES:
+            return True
+        if self.msg_type in self.SOTDMA_ITDMA_TYPES:
+            return self.radio <= self.MAX_COMM_STATE_VALUE
+        return False
 
     @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
+        """Messages of type 3 use ITDMA or 9, 18, 26 if 20th bit is set."""
+        if self.msg_type == 3:
+            return True
+        if self.msg_type in self.SOTDMA_ITDMA_TYPES:
+            return self.radio > self.MAX_COMM_STATE_VALUE
+        return False
 
     @property
     def communication_state_raw(self) -> int:


Test patch:
diff --git a/tests/test_decode.py b/tests/test_decode.py
index 7d8bb49..6bdc0d8 100644
--- a/tests/test_decode.py
+++ b/tests/test_decode.py
@@ -1385,6 +1385,14 @@ class TestAIS(unittest.TestCase):
         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`5ogh<O?bk>1Dd2L1<,0*0B").turn == 71.0
 
+    def test_sotdma_time_conversion(self):
+        """Prevent regressions for: https://github.com/M0r13n/pyais/pull/135"""
+        decoded = decode('!AIVDM,1,1,,B,133ga6PP0lPPE>4M3G@DpOwTR61p,0*33')
+        cs = decoded.get_communication_state()
+
+        assert cs['utc_hour'] == 16
+        assert cs['utc_minute'] == 30
+
     def test_get_sotdma_comm_state_utc_direct(self):
         msg = "!AIVDM,1,1,,A,13HOI:0P0000VOHLCnHQKwvL05Ip,0*23"
         decoded = decode(msg)
@@ -1457,6 +1465,32 @@ class TestAIS(unittest.TestCase):
             },
         )
 
+    def test_is_sotdma_or_itdma(self):
+        """ Verify that messages are correctly identified as either ITDMA or SOTDMA.
+        Details: https://github.com/M0r13n/pyais/issues/136."""
+
+        # 1
+        assert decode(b"!AIVDM,1,1,,A,13n3aW0PCkPJS8>Qhc2<urG02D13,0*18").is_sotdma
+        assert not decode(b"!AIVDM,1,1,,A,13n3aW0PCkPJS8>Qhc2<urG02D13,0*18").is_itdma
+        # 2
+        assert decode(b"!AIVDM,1,1,,A,23aFfl0P00PCR?0MEB@h0?w020S7,0*68").is_sotdma
+        assert not decode(b"!AIVDM,1,1,,A,23aFfl0P00PCR?0MEB@h0?w020S7,0*68").is_itdma
+        # 3
+        assert not decode(b"!AIVDM,1,1,,B,33UTPD5000G<@aTL3:?0010j0000,0*2A").is_sotdma
+        assert decode(b"!AIVDM,1,1,,B,33UTPD5000G<@aTL3:?0010j0000,0*2A").is_itdma
+        # 4
+        assert decode(b"!AIVDM,1,1,,B,4h2=aCAuho;QNOUQrvQ6?a1000S:,0*5D").is_sotdma
+        assert not decode(b"!AIVDM,1,1,,B,4h2=aCAuho;QNOUQrvQ6?a1000S:,0*5D").is_itdma
+        # 9
+        assert decode(b"!AIVDM,1,1,,A,91b55vRCQvOo4PLLww<3cGh20@Br,0*79").is_sotdma
+        assert not decode(b"!AIVDM,1,1,,A,91b55vRCQvOo4PLLww<3cGh20@Br,0*79").is_itdma
+        # 11
+        assert decode(b"!AIVDM,1,1,,A,;03t=KQuho;QM`d:WFAtwnW00000,0*7C").is_sotdma
+        assert not decode(b"!AIVDM,1,1,,A,;03t=KQuho;QM`d:WFAtwnW00000,0*7C").is_itdma
+        # 18
+        assert decode(b" !AIVDM,1,1,,A,B6:a?;03wk?8mP=18D3Q3wP61P06,0*7F").is_sotdma
+        assert not decode(b" !AIVDM,1,1,,A,B6:a?;03wk?8mP=18D3Q3wP61P06,0*7F").is_itdma
+
     def test_get_comm_state_type_18_itdma_base_indirect(self):
         msg = "!AIVDM,1,1,,A,B5NJ;PP005l4ot5Isbl03wsUkP06,0*76"
         decoded = decode(msg)