Skip to content

Commit

Permalink
Merge pull request #29 from GlobalFishingWatch/type-9
Browse files Browse the repository at this point in the history
add support for type 9
  • Loading branch information
pwoods25443 authored Sep 15, 2022
2 parents df073b5 + 1bf9be2 commit 7bb13e5
Show file tree
Hide file tree
Showing 8 changed files with 159 additions and 79 deletions.
5 changes: 4 additions & 1 deletion ais_tools/ais.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from ais_tools.transcode import DecodeError
from ais_tools import ais8
from ais_tools.transcode import ASCII8toAIS6
from ais_tools import ais8
from ais_tools import ais9
from ais_tools import ais18
from ais_tools import ais19
from ais_tools import ais24
Expand All @@ -9,6 +10,7 @@

encode_fn = {
8: ais8.ais8_encode,
9: ais9.ais9_encode,
18: ais18.ais18_encode,
19: ais19.ais19_encode,
24: ais24.ais24_encode,
Expand All @@ -17,6 +19,7 @@

decode_fn = {
8: ais8.ais8_decode,
9: ais9.ais9_decode,
18: ais18.ais18_decode,
19: ais19.ais19_decode,
24: ais24.ais24_decode,
Expand Down
79 changes: 6 additions & 73 deletions ais_tools/ais18.py
Original file line number Diff line number Diff line change
@@ -1,64 +1,32 @@
from ais_tools.transcode import DecodeError
from ais_tools.transcode import NmeaBits
from ais_tools.transcode import NmeaStruct as Struct
from ais_tools.transcode import UintField as Uint
from ais_tools.transcode import Uint10Field as Uint10
from ais_tools.transcode import LatLonField as LatLon
from ais_tools.transcode import BoolField as Bool
from ais_tools.transcode import BitField as Bits
from ais_tools.ais_commstate import ais_commstate_decode
from ais_tools.ais_commstate import ais_commstate_encode
from ais_tools.ais_commstate import ais_commstate_CS


def ais18_decode(body, pad):
bits = NmeaBits.from_nmea(body, pad)
message = bits.unpack(ais18_fields)

cs, fields = ais18_commstate_fields(message)
message.update(bits.unpack(fields))

if cs == 'SOTDMA':
fields = sotdma_timeout_fields(message)
message.update(bits.unpack(fields))
ais_commstate_decode(bits, message)

return message


def ais18_encode(message):
bits = NmeaBits(ais18_fields.nbits + ais18_commstate_CS.nbits)
bits = NmeaBits(ais18_fields.nbits + ais_commstate_CS.nbits)
bits.pack(ais18_fields, message)

cs, commstate_fields = ais18_commstate_fields(message)
timeout_fields = sotdma_timeout_fields(message)

bits.pack(commstate_fields, message)
if cs == 'SOTDMA':
bits.pack(timeout_fields, message)
ais_commstate_encode(bits, message)

return bits.to_nmea()


def ais18_commstate_fields(message):
if message.get('unit_flag', 0):
return 'CS', ais18_commstate_CS
elif message.get('commstate_flag', 0):
return 'ITDMA', ais18_commstate_ITDMA
else:
return 'SOTDMA', ais18_commstate_SOTDMA


def sotdma_timeout_fields(message):
slot_timeout = message.get('slot_timeout', 0)
if slot_timeout == 0:
return ais18_commstate_SOTDMA_timeout_0
elif slot_timeout == 1:
return ais18_commstate_SOTDMA_timeout_1
elif slot_timeout in (2, 4, 6):
return ais18_commstate_SOTDMA_timeout_2_4_6
elif slot_timeout in (3, 5, 7):
return ais18_commstate_SOTDMA_timeout_3_5_7
else:
raise DecodeError(f'AIS18: unknown slot_timeout value {slot_timeout}')


ais18_fields = Struct(
Uint(name='id', nbits=6, default=18),
Uint(name='repeat_indicator', nbits=2, default=0),
Expand All @@ -82,38 +50,3 @@ def sotdma_timeout_fields(message):
Bool(name='raim', nbits=1, default=0),
Uint(name='commstate_flag', nbits=1, default=0)
)


ais18_commstate_CS = Struct(
Bits(name='commstate', nbits=19, default='1100000000000000110')
)

ais18_commstate_ITDMA = Struct(
Uint(name='sync_state', nbits=2, default=0),
Uint(name='slot_increment', nbits=13, default=0),
Uint(name='slots_to_allocate', nbits=3, default=0),
Bool(name='keep_flag', nbits=1, default=0)
)

ais18_commstate_SOTDMA = Struct(
Uint(name='sync_state', nbits=2, default=0),
Uint(name='slot_timeout', nbits=3, default=0),
)

ais18_commstate_SOTDMA_timeout_0 = Struct(
Uint(name='slot_offset', nbits=14, default=0),
)

ais18_commstate_SOTDMA_timeout_1 = Struct(
Uint(name='utc_hour', nbits=5, default=0),
Uint(name='utc_min', nbits=7, default=0),
Uint(name='utc_spare', nbits=2, default=0),
)

ais18_commstate_SOTDMA_timeout_2_4_6 = Struct(
Uint(name='slot_number', nbits=14, default=0),
)

ais18_commstate_SOTDMA_timeout_3_5_7 = Struct(
Uint(name='received_stations', nbits=14, default=0),
)
2 changes: 0 additions & 2 deletions ais_tools/ais19.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
from ais_tools.transcode import DecodeError
from ais_tools.transcode import NmeaBits
from ais_tools.transcode import NmeaStruct as Struct
from ais_tools.transcode import UintField as Uint
from ais_tools.transcode import Uint10Field as Uint10
from ais_tools.transcode import LatLonField as LatLon
from ais_tools.transcode import BoolField as Bool
from ais_tools.transcode import BitField as Bits
from ais_tools.transcode import ASCII6Field as ASCII6


Expand Down
48 changes: 48 additions & 0 deletions ais_tools/ais9.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from ais_tools.transcode import NmeaBits
from ais_tools.transcode import NmeaStruct as Struct
from ais_tools.transcode import UintField as Uint
from ais_tools.transcode import Uint10Field as Uint10
from ais_tools.transcode import LatLonField as LatLon
from ais_tools.transcode import BoolField as Bool
from ais_tools.ais_commstate import ais_commstate_decode
from ais_tools.ais_commstate import ais_commstate_encode
from ais_tools.ais_commstate import ais_commstate_CS


def ais9_decode(body, pad):
bits = NmeaBits.from_nmea(body, pad)
message = bits.unpack(ais9_fields)

ais_commstate_decode(bits, message)

return message


def ais9_encode(message):
bits = NmeaBits(ais9_fields.nbits + ais_commstate_CS.nbits)
bits.pack(ais9_fields, message)

ais_commstate_encode(bits, message)

return bits.to_nmea()


ais9_fields = Struct(
Uint(name='id', nbits=6, default=18),
Uint(name='repeat_indicator', nbits=2, default=0),
Uint(name='mmsi', nbits=30),
Uint(name='alt', nbits=12, default=4095),
Uint(name='sog', nbits=10, default=1023),
Uint(name='position_accuracy', nbits=1, default=0),
LatLon(name='x', nbits=28, default=181),
LatLon(name='y', nbits=27, default=91),
Uint10(name='cog', nbits=12, default=360),
Uint(name='timestamp', nbits=6, default=60),
Uint(name='alt_sensor', nbits=1, default=0),
Uint(name='spare', nbits=7, default=0),
Uint(name='dte', nbits=1, default=0),
Uint(name='spare2', nbits=3, default=0),
Bool(name='assigned_mode', nbits=1, default=0),
Bool(name='raim', nbits=1, default=0),
Uint(name='commstate_flag', nbits=1, default=0)
)
81 changes: 81 additions & 0 deletions ais_tools/ais_commstate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
from ais_tools.transcode import DecodeError
from ais_tools.transcode import UintField as Uint
from ais_tools.transcode import BitField as Bits
from ais_tools.transcode import NmeaStruct as Struct
from ais_tools.transcode import BoolField as Bool


def ais_commstate_decode(bits, message):
cs, fields = ais_commstate_fields(message)
message.update(bits.unpack(fields))

if cs == 'SOTDMA':
fields = sotdma_timeout_fields(message)
message.update(bits.unpack(fields))


def ais_commstate_encode(bits, message):
cs, commstate_fields = ais_commstate_fields(message)
timeout_fields = sotdma_timeout_fields(message)

bits.pack(commstate_fields, message)
if cs == 'SOTDMA':
bits.pack(timeout_fields, message)


def ais_commstate_fields(message):
if message.get('unit_flag', 0):
return 'CS', ais_commstate_CS
elif message.get('commstate_flag', 0):
return 'ITDMA', ais_commstate_ITDMA
else:
return 'SOTDMA', ais_commstate_SOTDMA


def sotdma_timeout_fields(message):
slot_timeout = message.get('slot_timeout', 0)
if slot_timeout == 0:
return ais_commstate_SOTDMA_timeout_0
elif slot_timeout == 1:
return ais_commstate_SOTDMA_timeout_1
elif slot_timeout in (2, 4, 6):
return ais_commstate_SOTDMA_timeout_2_4_6
elif slot_timeout in (3, 5, 7):
return ais_commstate_SOTDMA_timeout_3_5_7
else:
raise DecodeError(f'AIS18: unknown slot_timeout value {slot_timeout}')


ais_commstate_CS = Struct(
Bits(name='commstate', nbits=19, default='1100000000000000110')
)

ais_commstate_ITDMA = Struct(
Uint(name='sync_state', nbits=2, default=0),
Uint(name='slot_increment', nbits=13, default=0),
Uint(name='slots_to_allocate', nbits=3, default=0),
Bool(name='keep_flag', nbits=1, default=0)
)

ais_commstate_SOTDMA = Struct(
Uint(name='sync_state', nbits=2, default=0),
Uint(name='slot_timeout', nbits=3, default=0),
)

ais_commstate_SOTDMA_timeout_0 = Struct(
Uint(name='slot_offset', nbits=14, default=0),
)

ais_commstate_SOTDMA_timeout_1 = Struct(
Uint(name='utc_hour', nbits=5, default=0),
Uint(name='utc_min', nbits=7, default=0),
Uint(name='utc_spare', nbits=2, default=0),
)

ais_commstate_SOTDMA_timeout_2_4_6 = Struct(
Uint(name='slot_number', nbits=14, default=0),
)

ais_commstate_SOTDMA_timeout_3_5_7 = Struct(
Uint(name='received_stations', nbits=14, default=0),
)
2 changes: 2 additions & 0 deletions sample/type8.nmea
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
!AIVDM,1,1,,B,8Nj<9D0000ttt0<D04@<tt<8`H0H@@l44L`<40<@tT`<4T0`=h0,2*47

17 changes: 15 additions & 2 deletions tests/test_ais.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
('H>cSnNP@4eEL544000000000000', 2, {}),
('H>cSnNTU7B=40058qpmjhh000004', 0, {'spare'}),
('I0000027FtlE01000VNJ;0`:h`0', 5, {}),
('C7m@5n004qtr0wtdVL9GSwrPFK04BL`2L?042@2>B310?1052120', 0, {'assigned_mode'})
('C7m@5n004qtr0wtdVL9GSwrPFK04BL`2L?042@2>B310?1052120', 0, {'assigned_mode'}),
('9001?BP=h:qJ9vb;:f7EN1h240Rb', 0, {})
])
def test_nmea_vs_libais(body, pad, ignore):
is_close_fields = {'x', 'y', 'cog', 'sog'}
Expand Down Expand Up @@ -80,6 +81,17 @@ def test_ais8_wrong_pad():
assert ('83am8S@j<d8dtfMEuj9loFOM6@0', 2) == AISMessageTranscoder.encode_nmea(msg)


@pytest.mark.parametrize("body,pad,expected", [
('9001?BP=h:qJ9vb;:f7EN1h240Rb', 0, {'mmsi': 20298, 'alt': 55, 'sog': 10}),
('90009C3dRIM1QSsjSPAa1;h200T4', 0, {'mmsi': 2380, 'alt': 946, 'alt_sensor': 0}),
])
def test_ais9(body, pad, expected):
msg = AISMessageTranscoder.decode_nmea(body, pad)
actual = {k: v for k, v in msg.items() if k in expected}
assert actual == expected
assert AISMessageTranscoder.encode_nmea(msg) == (body, pad)


@pytest.mark.parametrize("body,pad,expected", [
('B:U=ai@09o>61WLb:orRv2010400', 0, {'unit_flag': 0, 'commstate_flag': 0, 'slot_timeout': 1}),
('B52T:q@1C6TOpsUj5@??owTQh85G', 0, {'unit_flag': 0, 'commstate_flag': 0, 'slot_timeout': 2}),
Expand All @@ -102,14 +114,15 @@ def test_ais18(body, pad, expected):

@pytest.mark.parametrize("body,pad,expected", [
('C8k?R4h06mc;FwrwlfQWpTv0PBL>`2BTNL?WSWKQ1gW:00411R2P', 0, {'name': 'PINGTAIRONG313-0 73%'}),
('C8kI2<004V0u6wsPwKH00Qv0PBL>`2BTNL?gkKW1eg:000411R2P',0, {'assigned_mode': False})
('C8kI2<004V0u6wsPwKH00Qv0PBL>`2BTNL?gkKW1eg:000411R2P', 0, {'assigned_mode': False})
])
def test_ais19(body, pad, expected):
msg = AISMessageTranscoder.decode_nmea(body, pad)
actual = {k: v for k, v in msg.items() if k in expected}
assert actual == expected
assert AISMessageTranscoder.encode_nmea(msg) == (body, pad)


@pytest.mark.parametrize("fields", [
{'part_num': 0, 'name': 'ABCDEFGHIJKLMNOP@@@@'},
{'part_num': 1, 'vendor_id_1371_4': 'GRM'},
Expand Down
4 changes: 3 additions & 1 deletion tests/test_aivdm.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
('\\s:66,c:1661782483*3E\\!AIVDM,1,1,,A,35DFuH002>9NHLHCE@MB@AqD07VS,0*57', {'raim': False}),
('\\s:66,c:1661782099*31\\!AIVDM,1,1,,A,33`mOp0P0n0FNg6Mv7seTwvP0S0S,0*5C', {'keep_flag': True}),
('\\s:66,c:1662392695*3D\\!AIVDM,1,1,,A,E>j9dQjas000000000000000000@Ijfb?VJlh00808v>B0,4*0D',
{'assigned_mode': False})
{'assigned_mode': False}),
('\\s:66,c:1663246931*35\\!AIVDM,1,1,,,9001?BP=h:qJ9vb;:f7EN1h240Rb,0*3F',
{'alt_sensor': 0, 'assigned_mode': False})
])
def test_decode(nmea, expected):
decoder = AIVDM()
Expand Down

0 comments on commit 7bb13e5

Please sign in to comment.