While ancient and modern mariners alike look up to the stars for direction, oceanographers look to the sea. Our research in particular involves moving with the water wherever it may happen to take us since we want to measure how a patch of water develops and changes over time.
On my latest cruise I found myself put into the role of de facto computer tech in charge of the navigation program we use for our scientific gear. Throughout the cruise two pieces of gear are nearly constantly in the water (the sediment trap and the drifter array), and in order for our science to work we need to be within a few hundred meters (less than a mile) from them at all times. This requires up-to-date GPS fixes and a reliable system for relaying these fixes to the bridge.
Historically we’ve used a custom piece of Matlab programing to read out the GPS coordinates and to provide a visual map of where the gear is relative to the ship. Although the Matlab program would freeze up from time to time and require a restart, it provided the necessary information in a convenient package. This works, at least most of the time, until you get a captain who wants it done their way. Our captain was over it and wanted us to come up with a new solution of providing locations and the task fell to me. So enter NMEA.
The National Marine Electronics Association (NMEA) writes up specifications on how computers and electronics should communicate with one another aboard ships. By ensuring a single standard, ship captains can be sure that the data they’re receiving are accurate and reliable regardless of what combination or brands their navigational systems use.
The standard that NMEA came up with allows all these systems to communicate using standard NMEA sentences which are encoded messages that contain information such as the longitude and latitude of the ship. For example, here are a few taken from this wonderful site:
$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47 $GPGSA,A,3,04,05,,09,12,,,24,,,,,2.5,1.3,2.1*39 $GPWPL,4807.038,N,01131.000,E,WPTNME*5C
The first one is a GPS fix location including the full 3D location and accuracy information, the second contains information about the physical GPS signal (which satellites were used for the above), and the last sentence provides waypoint information including position and its name.
For our tracking program I only needed to figure out how to encode my own GPGGA sentences (the position ones) since that is all we care about. With some Googling and a whole lot of help from online sources I get this script working which reads in the latitude and longitude of the drifter, computes the NMEA sentence and broadcasts it on the network.
import socket import time import base64 import re UDP_IP = "127.0.0.1" UDP_PORT = 5006 #MESSAGE = "$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47" def checksum(sentence): """ Remove any newlines """ if re.search("\n$", sentence): sentence = sentence[:-1] sentence = sentence.replace("$", "") nmeadata, cksum = re.split('\*', sentence) print(nmeadata) calc_cksum = 0 for s in nmeadata: calc_cksum ^= ord(s) chk = str(hex(calc_cksum))[-2:] return "$" + nmeadata + "*" + chk while(1): a = open("/Users/euler/Desktop/OHM-MS-0001") temp = a.readline() temp = a.readline() #check = checksum(temp) MESSAGE = checksum(temp) MESSAGE = bytes(MESSAGE, 'utf-8') sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # UDP sock.sendto(MESSAGE, (UDP_IP, UDP_PORT)) print(MESSAGE) a.close() time.sleep(1)
While the script may look complicated, it’s pretty straightforward.
The script will broadcast the current drift location, but it will do so as if the ship and the drifter were one. So instead of telling the navigational computer “the drifter is at location X” we are broadcasting that “the ship is at position X”. To fix this, we actually have to resort to another, even more obfuscating encoding called AIS.
Just as NMEA is used for within ship communication, AIS is used for between ship communications and essentially consists of NMEA sentences repackaged for radio transmission. Here is an example AIS sentence
!AIVDM,1,1,,A,13aEOK?P00PD2wVMdLDRhgvL289?,0*26
Although the idea is similar to NMEA, the level of encoding is more complex and, to be honest, I am still a bit lost in the details of it. But nevertheless, Google and online forums proved helpful is developing the following (hack of a) script that takes latitude and longitude of the drifter and broadcasts it via AIS sentences.
#!/usr/bin/python import socket import time import base64 import re UDP_IP = "127.0.0.1" UDP_PORT = 5010 import math CharTable =[["0", "@", "000000"], ["1", "A", "000001"], ["2", "B", "000010"], ["3", "C", "000011"], ["4", "D", "000100"], ["5", "E", "000101"], ["6", "F", "000110"], ["7", "G", "000111"], ["8", "H", "001000"], ["9", "I", "001001"], [":", "J", "001010"], [";", "K", "001011"], ["<", "L", "001100"], ["=", "M", "001101"], [">", "N", "001110"], ["?", "O", "001111"], ["@", "P", "010000"], ["A", "Q", "010001"], ["B", "R", "010010"], ["C", "S", "010011"], ["D", "T", "010100"], ["E", "U", "010101"], ["F", "V", "010110"], ["G", "W", "010111"], ["H", "X", "011000"], ["I", "Y", "011001"], ["J", "Z", "011010"], ["K", "[", "011011"], ["L", "\\", "011100"], ["M", "]", "011101"], ["N", "^", "011110"], ["O", "_", "011111"], ["P", " ", "100000"], ["Q", "!", "100001"], ["R", "\"", "100010"], ["S", "#", "100011"], ["T", "$", "100100"], ["U", "%", "100101"], ["V", "&", "100110"], ["W", "'", "100111"], ["`", "(", "101000"], ["a", ")", "101001"], ["b", "*", "101010"], ["c", "+", "101011"], ["d", ",", "101100"], ["e", "-", "101101"], ["f", ".", "101110"], ["g", "/", "101111"], ["h", "0", "110000"], ["i", "1", "110001"], ["j", "2", "110010"], ["k", "3", "110011"], ["l", "4", "110100"], ["m", "5", "110101"], ["n", "6", "110110"], ["o", "7", "110111"], ["p", "8", "111000"], ["q", "9", "111001"], ["r", ":", "111010"], ["s", ";", "111011"] ["t", "<", "111100"], ["u", "=", "111101"], ["v", ">", "111110"], ["w", "?", "111111"]] PT = "AIVDM" CHAN = "A" MT = '000001' RI = '00' FILL = '000000' NS = 2 def Invert(BinStr): # Oh shit! I'm a lame programmer and can't find a function to swap bits in binary! return BinStr.replace("0", "A").replace("1", "0").replace("A", "1") def convert(val,bits): # This magic function converts dec to bin in right format if (val < 0 ): val = -val val = (str(bin(~val)))[3:].zfill(bits) val = Invert (val) else: val = (str(bin(val)))[2:].zfill(bits) return str(val) def checksum(s): c = 0 for ch in s: c ^= ord(ch) c = hex(c).upper()[2:] return c def GenAis(PT, CHAN, MT, RI, MMSI, NS, ROT, SOG, PA, LON, LAT, COG, HDG, TS, FILL, CommState): # Preparing Variables MMSI = str((bin(MMSI)))[2:].zfill(30) NS = str(bin(NS))[2:].zfill(4) SOG = str(bin(SOG))[2:].zfill(10) # Converting Vars to bin LON = convert(LON,28) LAT = convert(LAT,27) HDG = convert(HDG,9) COG = convert(COG,12) ROT = convert(ROT,8) TS = convert(TS,6) CommState = convert(CommState,19) MESSAGE_BIN = MT + RI + MMSI + NS + ROT + SOG + PA + LON + LAT + COG + HDG + TS + FILL + CommState mess = MT + ' ' + RI + ' ' + MMSI + ' ' + NS + ' ' + ROT + ' ' + SOG + ' ' + PA + ' ' + LON + ' ' + LAT + ' ' + COG + ' ' + HDG + ' ' + TS + ' ' + FILL + ' ' + CommState print (mess) MESSENC = "" LEN = 28 #28 6-byte words = 168 bits P = 0 # Starting encoding binary string to 6-byte word: while P <= LEN: TMPSTR = MESSAGE_BIN[(6*P):(6*P+6)] # print (TMPSTR) P += 1 for PAIR in CharTable: if PAIR[2] == TMPSTR: MESSENC += PAIR[0] # print (MESSENC) MESSAIS = (PT + ',1,1,,' + CHAN + ',' + MESSENC + ',0') # CS = '4E' CS = checksum(MESSAIS) print(CS) if (len(CS) < 2): CS = '0' + CS return ("!" + MESSAIS + '*' + CS) # So here the program starts MMSI = 0 #MMSI = 000000000 PA = '0' #LON = -122.39253 #LAT = 37.803803 HDG = 511 COG = 3600 SOG = 1023 ROT = 0 ACC = 0 TS = 60 CommState = 67427 #print (GenAis(PT, CHAN, MT, RI, MMSI, NS, ROT, SOG, PA, LON, LAT, COG, HDG, TS, FILL, CommState)) while(1): a = open("/Users/euler/Desktop/OHM-MS-0001") temp = a.readline() temp = a.readline().split(",") print(temp) LON = int(temp[1]) LAT = int(temp[0]) #check = checksum(temp) MESSAGE = GenAis(PT, CHAN, MT, RI, MMSI, NS, ROT, SOG, PA, LON, LAT, COG, HDG, TS, FILL, CommState) MESSAGE = bytes(MESSAGE, 'utf-8') sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # UDP sock.sendto(MESSAGE, (UDP_IP, UDP_PORT)) print(MESSAGE) a.close() time.sleep(1)
Now what this script does is tell the navigation computer “Hey, drifter1 is at position X”. Although far from perfect, the script did work reliably for the duration of the cruise (about a month) and it allowed me to get some well needed sleep once in a while.