def __init__(self, raw: bytes): # Set all values to None initially [setattr(self, name, None) for name in self.__slots__] # Store raw data self.raw = raw # An AIS NMEA message consists of seven, comma separated parts values = raw.split(b",") # Only encapsulated messages are currently supported if values[0][0] != 0x21: return if len(values) != 7: raise InvalidNMEAMessageException("A NMEA message needs to have exactly 7 comma separated entries.") # Unpack NMEA message parts ( head, count, index, seq_id, channel, data, checksum ) = values # The talker is identified by the next 2 characters self.talker = head[1:3].decode('ascii') # The type of message is then identified by the next 3 characters self.msg_type = head[3:].decode('ascii') # Store other important parts self.count = int(count) self.index = int(index) self.seq_id = seq_id self.channel = channel self.data = data self.checksum = int(checksum[2:], 16) # Verify if the checksum is correct if not self.is_valid: raise InvalidChecksumException( f"Invalid Checksum. Expected {self.checksum}, got {compute_checksum(self.data)}.") # Finally decode bytes into bits self.bit_array = decode_into_bit_array(self.data) self.ais_id = get_int(self.bit_array, 0, 6)
def validate_message(msg: bytes) -> None: """ Validates a given message. It checks if the messages complies with the AIS standard. It is based on: 1. https://en.wikipedia.org/wiki/Automatic_identification_system 2. https://en.wikipedia.org/wiki/NMEA_0183 If not errors are found, nothing is returned. Otherwise an InvalidNMEAMessageException is raised. """ values = msg.split(b",") # A message has exactly 7 comma separated values if len(values) != 7: raise InvalidNMEAMessageException( "A NMEA message needs to have exactly 7 comma separated entries.") # The only allowed blank value may be the message ID if not values[0]: raise InvalidNMEAMessageException("The NMEA message type is empty!") if not values[1]: raise InvalidNMEAMessageException("Number of sentences is empty!") if not values[2]: raise InvalidNMEAMessageException("Sentence number is empty!") if not values[4]: raise InvalidNMEAMessageException("The AIS channel (A or B) is empty.") if not values[5]: raise InvalidNMEAMessageException( "The NMEA message body (payload) is empty.") if not values[6]: raise InvalidNMEAMessageException( "NMEA checksum (NMEA 0183 Standard CRC16) is empty.") try: sentence_num = int(values[1]) if sentence_num > 9: raise InvalidNMEAMessageException( "Number of sentences exceeds limit of 9 total sentences.") except ValueError: raise InvalidNMEAMessageException( "Invalid sentence number. No Number.") if values[2]: try: sentence_num = int(values[2]) if sentence_num > 9: raise InvalidNMEAMessageException( " Sentence number exceeds limit of 9 total sentences.") except ValueError: raise InvalidNMEAMessageException( "Invalid Sentence number. No Number.") if values[3]: try: sentence_num = int(values[3]) if sentence_num > 9: raise InvalidNMEAMessageException( "Number of sequential message ID exceeds limit of 9 total sentences." ) except ValueError: raise InvalidNMEAMessageException( "Invalid sequential message ID. No Number.") # It should not have more than 82 chars (including starting $ or ! and <LF>) if len(msg) > 82: raise InvalidNMEAMessageException( f"{msg.decode('utf-8')} has more than 82 characters.") # Only encapsulated messages are currently supported if values[0][0] != 0x21: # https://en.wikipedia.org/wiki/Automatic_identification_system raise InvalidNMEAMessageException( "'NMEAMessage' only supports !AIVDM/!AIVDO encapsulated messages. " f"These start with an '!', but got '{chr(values[0][0])}'")