M0r13n / pyais

AIS message decoding and encoding in Python (AIVDM/AIVDO)
MIT License
176 stars 61 forks source link

Stream NMEA messages containing any type of metadata #145

Closed MyrnsWork closed 1 month ago

MyrnsWork commented 1 month ago

It may be useful to read or stream NMEA messages containing any type of metadata

enhanced_fake_stream = [
    b"[2024-07-19 08:45:27.141] !AIVDM,1,1,,A,13HOI:0P0000VOHLCnHQKwvL05Ip,0*23",
    b"[2024-07-19 08:45:30.074] !AIVDM,1,1,,A,133sVfPP00PD>hRMDH@jNOvN20S8,0*7F",
    b"[2024-07-19 08:45:35.007] !AIVDM,1,1,,B,100h00PP0@PHFV`Mg5gTH?vNPUIp,0*3B",
    b"[2024-07-19 08:45:35.301] !AIVDM,1,1,,B,13eaJF0P00Qd388Eew6aagvH85Ip,0*45",
    b"[2024-07-19 08:45:40.021] !AIVDM,1,1,,A,14eGrSPP00ncMJTO5C6aBwvP2D0?,0*7A",
    b"[2024-07-19 08:45:40.074] !AIVDM,1,1,,A,15MrVH0000KH<:V:NtBLoqFP2H9:,0*2F",
]

# Create a custom parsing function:
# - NMEA message must be always in the first position
# - Always consider that the NMEA message are bytes when parsing
# - The metadata field can be also parsed during the process (any types: string, float, datetime, etc.)
def parse_function(line: bytes) -> Tuple[bytes, Any]:
    nmea_message = re.search(b'.* (.*)', line).group(1)  # NMEA
    metadata_bytes = re.search(b'(.*) .*', line).group(1)  # Metadata
    timestamp = datetime.strptime(metadata_bytes.decode("utf-8"), "[%Y-%m-%d %X.%f]").timestamp()
    return nmea_message, timestamp

# Whatever if enhanced_fake_stream datas are bytes or strings:
for message, infos in IterMessages(enhanced_fake_stream, parse_function):
    print(f"Timestamp: {infos} --", message.decode())

And the result would be:

Timestamp: 1721371527.141 -- MessageType1(msg_type=1, repeat=0, mmsi=227006760, status=<NavigationStatus.UnderWayUsingEngine: 0>, turn=<TurnRate.NO_TI_DEFAULT: -128.0>, speed=0.0, accuracy=False, lon=0.13138, lat=49.475577, course=36.7, heading=511, second=14, maneuver=<ManeuverIndicator.NotAvailable: 0>, spare_1=b'\x00', raim=False, radio=22136)
Timestamp: 1721371530.074 -- MessageType1(msg_type=1, repeat=0, mmsi=205448890, status=<NavigationStatus.UnderWayUsingEngine: 0>, turn=<TurnRate.NO_TI_DEFAULT: -128.0>, speed=0.0, accuracy=True, lon=4.419442, lat=51.237658, course=63.3, heading=511, second=15, maneuver=<ManeuverIndicator.NotAvailable: 0>, spare_1=b'\x00', raim=True, radio=2248)
Timestamp: 1721371535.007 -- MessageType1(msg_type=1, repeat=0, mmsi=786434, status=<NavigationStatus.UnderWayUsingEngine: 0>, turn=<TurnRate.NO_TI_DEFAULT: -128.0>, speed=1.6, accuracy=True, lon=5.320033, lat=51.967037, course=112.0, heading=511, second=15, maneuver=<ManeuverIndicator.NoSpecialManeuver: 1>, spare_1=b'\x00', raim=False, radio=153208)
Timestamp: 1721371535.301 -- MessageType1(msg_type=1, repeat=0, mmsi=249191000, status=<NavigationStatus.UnderWayUsingEngine: 0>, turn=<TurnRate.NO_TI_DEFAULT: -128.0>, speed=0.0, accuracy=True, lon=23.603633, lat=37.955883, course=247.0, heading=511, second=12, maneuver=<ManeuverIndicator.NotAvailable: 0>, spare_1=b'@', raim=False, radio=22136)
Timestamp: 1721371540.021 -- MessageType1(msg_type=1, repeat=0, mmsi=316013198, status=<NavigationStatus.UnderWayUsingEngine: 0>, turn=<TurnRate.NO_TI_DEFAULT: -128.0>, speed=0.0, accuracy=True, lon=-130.316237, lat=54.32111, course=237.9, heading=511, second=16, maneuver=<ManeuverIndicator.NotAvailable: 0>, spare_1=b'\x00', raim=True, radio=81935)
Timestamp: 1721371540.074 -- MessageType1(msg_type=1, repeat=0, mmsi=366913120, status=<NavigationStatus.UnderWayUsingEngine: 0>, turn=0.0, speed=0.0, accuracy=False, lon=-64.620662, lat=18.321188, course=329.5, heading=299, second=16, maneuver=<ManeuverIndicator.NotAvailable: 0>, spare_1=b'\x00', raim=True, radio=98890)

The retro-compatibility is done by a inputs/outputs mapper function:

io_mapper_function = (lambda msg, infos: msg) if parse_function is None else (lambda msg, infos: (msg, infos))

It's the biggest weakness of this new feature: we break the consistency of the output data structure. However, we could imagine for a future version of pyais, with a breaking change (3.y.z), to change the global output to (message, metadata) where metadata can be None.


As far as I'm concerned, all the tests passed except:

If I change the path ‘tests/ais_test_messages’ to ‘ais_test_messages’ in test_decode_from_file and test_parser then :

Anyways, when I git checkout on master (without any modifications), the same tests don't pass. Is this normal for these tests?