Creating, sharing and running a Docker image to decode AIS messages

Recently, I showed how to use AISMessages to quickly build a Spring Boot based HTTP/JSON-service capable of converting NMEA-armoured AIS messages into JSON-based parsed AIS messages (what are AIS messages?). Now we want to make it even easier to get this AIS decoder service running, by building and sharing a Docker image of the service, which can easily be downloaded and spun up by anyone using Docker.

Getting ready

To get going we first want to make sure, that Docker is installed. For our purposes, we will use Docker for Mac. So go and grab that if you don’t already have it. If you are preferring a certain package manager or are using a different operating system, you will have to get Docker for that – take a look at “Get Started with Docker”.

With Docker properly installed, you should be able to run:

$ docker version

and get a sensible output.

Second we clone the source code of our AISdecoder into a folder on the local harddrive:

$ git clone https://github.com/tbsalling/aisdecoder.git

For the sake of getting the repository in the right starting state, we will rewind it a bit to the following commit:

$ git checkout 7c02cbcef2ff273ab157e41fa71b193ae3304a93

And finally we compile the project in order to produce the binary artifact that we will run using Docker:

$ ./gradlew build
...
BUILD SUCCESSFUL in 4s
5 actionable tasks: 5 executed
$

Now we are ready – and this is the file that we want to run in a Docker container:

$ ls -lh build/libs/
total 32000
-rw-r--r--  1 tbsalling  staff    16M 24 Sep 11:30 aisdecoder-0.0.1-SNAPSHOT.jar

Adding the Dockerfile

With all prerequisites in place, the first thing we want to do is to add a Dockerfile to the project. The Dockerfile describes how Docker should build the Docker image. It could look like this:

FROM openjdk:11-jre-slim
MAINTAINER Thomas Borg Salling "tbsalling@tbsalling.dk"
COPY build/libs/aisdecoder-0.0.1-SNAPSHOT.jar /app/aisdecoder.war
ENTRYPOINT ["java", "-jar", "/app/aisdecoder.war"]
EXPOSE 8080/tcp

The FROM keyword specifies the Docker base image. The rest of the Dockerfile can roughly be considered to be changes or additions to this base image. Base images can be self-built or they can be searched or browsed on e.g. Docker Hub, where contributors upload and share images. We choose to use the openjdk:11-jre-slim image from Docker Hub, which is a Linux-based image with the OpenJDK version of Java 11 pre-installed. For a Spring Boot-based Java SE application this is a good start.

The MAINTAINER keyword mainly adds meta information to the image and is not very important here.

COPY on the other hand is quite important here. It copies the compiled .war-file from your local developer machine into the generated Docker image – and places it in folder /app as aisdecoder.war.

ENTRYPOINT defines the executable command, that will be fired by default when running a container based on this image using docker run ... later on. As you can probably see, this is equivalent to java -jar /app/aisdecoder.war – i.e. a command line based launch of our Java SE application.

Finally, EXPOSE 8080/tcp tells Docker, that a container launched from this image listens on the specified network port at runtime. In this case in listens for TCP-based network traffic on port 8080 – which is exactly where the embedded Tomcat in our Java SE application listens. So any traffic going to port 8080 of this container will go to our own embedded Tomcat.

To learn more Dockerfile keywords – and details of those used here – it is a good idea to familiarize yourself with the reference documentation for Dockerfiles.

Building the Docker image

With the Dockerfile in place it is time to actually create the Docker image. This can be done like this:

$ docker build .
Sending build context to Docker daemon  16.95MB
Step 1/5 : FROM openjdk:11-jre-slim
 ---> 422e4d3c11a7
Step 2/5 : MAINTAINER Thomas Borg Salling "tbsalling@tbsalling.dk"
 ---> Using cache
 ---> 4ce7f868ea8b
Step 3/5 : COPY build/libs/aisdecoder-0.0.1-SNAPSHOT.jar /app/aisdecoder.war
 ---> Using cache
 ---> 7a277936c416
Step 4/5 : ENTRYPOINT ["java", "-jar", "/app/aisdecoder.war"]
 ---> Using cache
 ---> 97e0dee65253
Step 5/5 : EXPOSE 8080/tcp
 ---> Running in a10e415ccf76
Removing intermediate container a10e415ccf76
 ---> 9f37cd551132
Successfully built 9f37cd551132

This means, that Docker has now successfully built an image identified by 9f37cd551132.

Running the Docker image as a container

To spawn a container based on this image, we can issue the following command line:

$ docker run -p 8080:8080 9f37cd551132

This instructs Docker to launch a new container based on image 9f37cd551132. Thanks to the -p option docker will bind to port 8080 on the local host and forward traffic on this port to port 8080 inside the Docker container. In effect, this causes traffic on port 8080 on the host machine to be forwarded to the embedded Tomcat inside our Java SE application running in the Docker container.

Using the container

So, with our Docker container running, we can now reach its functionality by sending HTTP traffic to port 8080 on our host machine. Like this:

$ curl -X POST http://localhost:8080/decode -H 'Content-Type: application/json' -d '[ "!AIVDM,1,1,,A,18UG;P0012G?Uq4EdHa=c;7@051@,0*53" ]'

… which (as we have seen previously) will result in this output:

[{"nmeaMessages":[{"rawMessage":"!AIVDM,1,1,,A,18UG;P0012G?Uq4EdHa=c;7@051@,0*53","valid":true,"sequenceNumber":null,"radioChannelCode":"A","checksum":83,"numberOfFragments":1,"fragmentNumber":1,"messageType":"AIVDM","encodedPayload":"18UG;P0012G?Uq4EdHa=c;7@051@","fillBits":0}],"metadata":{"source":"SRC1","received":"2018-09-24T09:33:45.690187Z","decoderVersion":"2.2.2","category":"AIS"},"repeatIndicator":0,"sourceMmsi":{"mmsi":576048000},"navigationStatus":"UnderwayUsingEngine","rateOfTurn":0,"speedOverGround":6.6,"positionAccuracy":false,"latitude":37.912167,"longitude":-122.42299,"courseOverGround":350.0,"trueHeading":355,"second":40,"specialManeuverIndicator":"NotAvailable","raimFlag":false,"communicationState":{"syncState":"UTCDirect","slotTimeout":1,"numberOfReceivedStations":null,"slotNumber":null,"utcHour":8,"utcMinute":20,"slotOffset":null},"messageType":"PositionReportClassAScheduled","transponderClass":"A","valid":true}]

Building a Docker image with a tag

You may have experienced that 9f37cd551132 is not a terribly easy “name” to remember. Actually it is a hash value – and it changes dramatically with every little change to our image. To ease this, Docker supports associating descriptive – easier-to-remember – names with these hashes. So Docker images can be built with a name like this:

$ docker build -t tbsalling/aisdecoder:latest .
...
Successfully built 9f37cd551132
Successfully tagged tbsalling/aisdecoder:latest

Here tbsalling/aisdecoder is the repository name, and latest is the actual tag name. So now tbsalling/aisdecoder:latest points to the image with hash value 9f37cd551132.

Read more about Docker’s valid tags.

Running a Docker container using a tag

With a tag name in place, a Docker container can be spun up like this:

$ docker run -p 8080:8080 tbsalling/aisdecoder:latest

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.0.5.RELEASE)

2018-09-24 11:05:20.737  INFO 1 --- [           main] d.t.a.d.a.AisdecoderApplication          : Starting AisdecoderApplication on 8c6771c64973 with PID 1 (/app/aisdecoder.war started by root in /)
2018-09-24 11:05:2
...

Uploading an image to Docker hub

With all this work done – wouldn’t it be nice if we could share the result? This would allow anyone to run aisdecoder locally by just downloading our Docker image and run it in a Docker container? Luckily this is possible by uploading our image to Docker Hub.

To do that first make sure, that you have a valid account on https://hub.docker.com. Then, from the command line login like this:

$ docker login --username=tbsalling
Password: 
Login Succeeded

With the image already built (see above) we can now push (upload) it to Docker Hub using the repository and tag names like this:

$ docker push tbsalling/aisdecoder:latest
The push refers to repository [docker.io/tbsalling/aisdecoder]
308058d2da0d: Pushed 
7d2319767e1d: Mounted from library/openjdk 
36fdef6aaa51: Mounted from library/openjdk 
0854ef12fba3: Mounted from library/openjdk 
9a27a9751438: Mounted from library/openjdk 
f9af8abefa4e: Mounted from library/openjdk 
latest: digest: sha256:0f93b0c3b65b794ce628d135515732c0f8a0fa826a8ccb0df9882086cd0d29dd size: 1577

Now the image is uploaded to Docker Hub. You can see that for yourself by visiting https://hub.docker.com/r/tbsalling/aisdecoder/.

Now let us logout of Docker Hub so that we could be anyone in the public:

$ docker logout
Removing login credentials for https://index.docker.io/v1/

Any random person (with Docker installed…) can now pull (download) and run this image as simple as:

$ docker pull tbsalling/aisdecoder:latest
latest: Pulling from tbsalling/aisdecoder
Digest: sha256:0f93b0c3b65b794ce628d135515732c0f8a0fa826a8ccb0df9882086cd0d29dd
Status: Image is up to date for tbsalling/aisdecoder:latest

$ docker run tbsalling/aisdecoder:latest

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.0.5.RELEASE)

2018-09-24 11:12:30.158  INFO 1 --- [           main] d.t.a.d.a.AisdecoderApplication          : Starting AisdecoderApplication on 4ab60bf292ce with PID 1 (/app/aisdecoder.war started by root in /)

Conclusion

In this post I have shown how to create a Docker image containing an HTTP/JSON-based decoder for NMEA messages with AIS-contents. I showed how to upload and share this image via Docker Hub – and demonstrated how anyone can pull this image and run the AIS decoder from scratch with just 2 command line instructions (provided Docker is already installed).

Have fun 🙂

Creating a Spring Boot based AIS message decoder

To demonstrate how easy it is to parse AIS messages (what is an AIS message?) with my open source library AISmessages, this post shows how to create a Spring Boot based microservice which can receive NMEA strings via HTTP and respond with the decoded AIS messages in JSON format.

So – for an HTTP request with a JSON array of NMEA strings like this:

POST http://localhost:8080/decode

Content-Type: application/json

[ 
  "!AIVDM,1,1,,A,18UG;P0012G?Uq4EdHa=c;7@051@,0*53",
  "!AIVDM,2,1,0,B,539S:k40000000c3G04PPh63<00000000080000o1PVG2uGD:00000000000,0*34",
  "!AIVDM,2,2,0,B,00000000000,2*27"
]

… we would like a response like this:

[
  {
    "repeatIndicator":0,
    "sourceMmsi": { "mmsi":576048000 },
    "navigationStatus":"UnderwayUsingEngine",
    "rateOfTurn":0,
    "speedOverGround":6.6,
    "positionAccuracy":false,
    "latitude":37.912167,
    "longitude":-122.42299,
    "courseOverGround":350.0
    "trueHeading":355,
    "second":40,
    "specialManeuverIndicator":"NotAvailable",
    "raimFlag":false,
    "communicationState": {
      "syncState":"UTCDirect",
      "slotTimeout":1,
      "numberOfReceivedStations":null,
      "slotNumber":null,
      "utcHour":8,
      "utcMinute":20,
      "slotOffset":null
    },
    "messageType":"PositionReportClassAScheduled",
    "transponderClass":"A",
    "valid":true
  }
]

Initializing a new Spring Boot project

A quick way to build such a service is to use Spring MVC. So, first we need to initialize a new Spring Boot project. An easy way to do this is to visit https://start.spring.io and fill in the form like this:


Generate and download the resulting project. Then move it to a suitable directory on your machine and unzip it like this:

$ mv ~/Downloads/aisdecoder.zip .
$ unzip aisdecoder.zip 
Archive:  aisdecoder.zip
   creating: aisdecoder/
  inflating: aisdecoder/gradlew      
   creating: aisdecoder/gradle/
   creating: aisdecoder/gradle/wrapper/
   creating: aisdecoder/src/
   creating: aisdecoder/src/main/
   creating: aisdecoder/src/main/java/
   creating: aisdecoder/src/main/java/dk/
   creating: aisdecoder/src/main/java/dk/tbsalling/
   creating: aisdecoder/src/main/java/dk/tbsalling/ais/
   creating: aisdecoder/src/main/java/dk/tbsalling/ais/decoder/
   creating: aisdecoder/src/main/java/dk/tbsalling/ais/decoder/aisdecoder/
   creating: aisdecoder/src/main/resources/
   creating: aisdecoder/src/main/resources/static/
   creating: aisdecoder/src/main/resources/templates/
   creating: aisdecoder/src/test/
   creating: aisdecoder/src/test/java/
   creating: aisdecoder/src/test/java/dk/
   creating: aisdecoder/src/test/java/dk/tbsalling/
   creating: aisdecoder/src/test/java/dk/tbsalling/ais/
   creating: aisdecoder/src/test/java/dk/tbsalling/ais/decoder/
   creating: aisdecoder/src/test/java/dk/tbsalling/ais/decoder/aisdecoder/
  inflating: aisdecoder/.gitignore   
  inflating: aisdecoder/build.gradle  
  inflating: aisdecoder/gradle/wrapper/gradle-wrapper.jar  
  inflating: aisdecoder/gradle/wrapper/gradle-wrapper.properties  
  inflating: aisdecoder/gradlew.bat  
  inflating: aisdecoder/settings.gradle  
  inflating: aisdecoder/src/main/java/dk/tbsalling/ais/decoder/aisdecoder/AisdecoderApplication.java  
  inflating: aisdecoder/src/main/resources/application.properties  
  inflating: aisdecoder/src/test/java/dk/tbsalling/ais/decoder/aisdecoder/AisdecoderApplicationTests.java 

As a smoke test we will first build new freshly, unmodified project - this is done with Gradle like this:

$ cd aisdecoder
$ ./gradlew build
... <a lot of build information>
BUILD SUCCESSFUL in 15s
5 actionable tasks: 5 executed

With the boiler plate project just built, we should see that it runs:

$ ./gradlew bootRun

> Task :bootRun

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.0.5.RELEASE)

... <a lot of log output omitted>

o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2018-09-13 07:59:01.475  INFO 10989 --- [           main] d.t.a.d.a.AisdecoderApplication          : Started AisdecoderApplication in 1.7 seconds (JVM running for 2.008)
<=========----> 75% EXECUTING [21s]
> :bootRun

All seems well. The Spring MVC web application is running - but not doing much useful yet.

Adding custom code

Adding AISmessages as a dependency

The first thing we will do, is to add AISmessages as a dependency. This is done by adding this line into build.gradle:

...
dependencies {
  compile group: 'dk.tbsalling', name: 'aismessages', version: '2.2.3'   
  ...
}
...

Adding Spring MVC Controller

Next we will add the Spring MVC controller which handle incoming HTTP requests. This controller should be able to receive a JSON array of NMEA strings and output a JSON array of AIS messages.

So, in folder src/main/java/dk/tbsalling/ais/decoder/ we add file AisdecoderController.java like this:

package dk.tbsalling.ais.decoder.aisdecoder;

import dk.tbsalling.aismessages.ais.messages.AISMessage;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.util.Collections;
import java.util.List;

@RestController
public class AisdecoderController {

    @RequestMapping(
        value = "/decode",
        method = RequestMethod.POST,
        consumes = MediaType.APPLICATION_JSON_VALUE,
        produces = MediaType.APPLICATION_JSON_VALUE
    )
    public List<AISMessage> decode(@RequestBody List<String> nmea) {
        return Collections.emptyList();
    }

}

This class is discovered by Spring through classpath scanning at startup, and handles incoming HTTP POST requests headed for URI /decode - such as http://localhost:8080/decode. The current implementation is mostly boiler plate and does nothing useful.

What we want it to do, is to call a service class which can convert the received NMEA strings into AIS messages. Like this:

@RestController
public class AisdecoderController {

    @Autowired
    private AisdecoderService aisdecoderService;

    ...

    public List<AISMessage> decode(@RequestBody List<String> nmea) {
        return aisdecoderService.decode(nmea);
    }

}

Adding AIS decode service

Then we need need the AisdecoderService. This is the most custom part of the code and where the real work happens. It should receive a list of n NMEA messages, convert and return these as a list of m AIS messages.

We start by adding to src/main/java/dk/tbsalling/ais/decoder/ the class AisdecoderService.java:

package dk.tbsalling.ais.decoder.aisdecoder;

import dk.tbsalling.aismessages.ais.messages.AISMessage;

import java.util.List;

@Service
@RequestScope
public class AisdecoderService {
    
    public List<AISMessage> decode(List<String> nmea) {
    }
    
}

So - how do we implement the decode method? The key here is class NMEAMessageHandler from AISmessages. NMEAMessageHandler is a class which can keep consuming NMEA messages and perform a callback whenever the received messages result in the successful decoding of a complete AIS message. Sometimes NMEA messages and AIS messages correspond 1:1 - other times it takes 2 NMEA messages to decode 1 AIS message.

So - we will extend AisdecoderService like this:

...
public class AisdecoderService implements Consumer<AISMessage> {
    
    public List<AISMessage> decode(List<String> nmeaMessagesAsStrings) {
        NMEAMessageHandler nmeaMessageHandler = new NMEAMessageHandler("SRC1", this);
    }
    
    @Override
    public void accept(AISMessage aisMessage) {
        aisMessages.add(aisMessage);
    }
}

Now the decode()-method initializes a NMEAMessageHandler. This handler is handed this (the decoder itself) to that it can make callbacks whenever an AIS message is fully constructed. To be used for callbacks, the AisdecoderService needs to implement the Consumer<AISMessage> interface.

Next, we need to start feeding the NMEA messages to the NMEAMessageHandler. One way to do that is this loop which iterates over all the NMEA strings and passes each one to the NMEAMessageHandler:

...
public class AisdecoderService implements Consumer<AISMessage> {
    ...
    public List<AISMessage> decode(List<String> nmeaMessagesAsStrings) {
       ...
        // Decode all received messages
        nmeaMessagesAsStrings.forEach(nmeaMessageAsString -> {
            try {
                NMEAMessage nmeaMessage = NMEAMessage.fromString(nmeaMessageAsString);
                nmeaMessageHandler.accept(nmeaMessage);
            } catch(NMEAParseException e) {
                System.err.printf(e.getMessage());
            }
        });
    }
    ...
}

Everytime the NMEA message handler can put the NMEA pieces together a complete AIS message, a callback is made to the AisdecoderService#accept(AISMessage msg) method. This method needs to store i AIS messages in a list, so that they can be returned by the decoder later:


public class AisdecoderService implements Consumer<AISMessage> {
    ...
    private final List<AISMessage> aisMessages = new LinkedList<>();

    @Override
    public void accept(AISMessage aisMessage) {
        aisMessages.add(aisMessage);
    }
    ...
}

Finally, in the decode() method, we must deal with the situation, where there are no more NMEA messages. This calls for a flush of the NMEAMessageHandler and return of the collected AIS messages:

...
public class AisdecoderService implements Consumer<AISMessage> {
    ...
    public List<AISMessage> decode(List<String> nmeaMessagesAsStrings) {
         ...
        // Flush receiver for unparsed message fragments
        List unparsedMessages = nmeaMessageHandler.flush();
        unparsedMessages.forEach(unparsedMessage -> {
            System.err.println("NMEA message not used: " + unparsedMessage);
        });

        // Return result
        return aisMessages;
    }
    ...
}

Complete code

The complete code resulting from the above can be viewed on Github: https://github.com/tbsalling/aisdecoder/tree/ready/spring-boot-webservice

Run it yourself

The code can then be cloned, compiled, run and invoked like this:

$ git clone https://github.com/tbsalling/aisdecoder.git
Cloning into 'aisdecoder'...
remote: Counting objects: 32, done.
remote: Compressing objects: 100% (18/18), done.
remote: Total 32 (delta 0), reused 32 (delta 0), pack-reused 0
Unpacking objects: 100% (32/32), done.
$ cd aisdecoder/
$ git checkout 7c02cbcef2ff273ab157e41fa71b193ae3304a93
$ ./gradlew build
...
BUILD SUCCESSFUL in 5s
$ ./gradlew bootRun

> Task :bootRun

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.0.5.RELEASE)

...
<=========----> 75% EXECUTING [42s]
> :bootRun

Then, in a separate terminal window the web service can be invoked with e.g. Postman or curl (on Linux or MacOS), like this:

$ curl -X POST http://localhost:8080/decode -H 'Content-Type: application/json' -d '[ "!AIVDM,1,1,,A,18UG;P0012G?Uq4EdHa=c;7@051@,0*53" ]'
[{"nmeaMessages":[{"rawMessage":"!AIVDM,1,1,,A,18UG;P0012G?Uq4EdHa=c;7@051@,0*53","valid":true,"sequenceNumber":null,"numberOfFragments":1,"fragmentNumber":1,"radioChannelCode":"A","checksum":83,"messageType":"AIVDM","encodedPayload":"18UG;P0012G?Uq4EdHa=c;7@051@","fillBits":0}],"metadata":{"source":"SRC1","received":"2018-09-13T10:28:19.661343Z","decoderVersion":"2.2.2","category":"AIS"},"repeatIndicator":0,"sourceMmsi":{"mmsi":576048000},"navigationStatus":"UnderwayUsingEngine","rateOfTurn":0,"speedOverGround":6.6,"positionAccuracy":false,"latitude":37.912167,"longitude":-122.42299,"courseOverGround":350.0,"trueHeading":355,"second":40,"specialManeuverIndicator":"NotAvailable","raimFlag":false,"communicationState":{"syncState":"UTCDirect","slotTimeout":1,"numberOfReceivedStations":null,"slotNumber":null,"utcHour":8,"utcMinute":20,"slotOffset":null},"messageType":"PositionReportClassAScheduled","transponderClass":"A","valid":true}]

That's it 🙂

An introduction to AIS

One of the domains, that I’ve done the most work in is the maritime domain. One of the interesting technologies here is AIS – Automatic Identification System. So – what is AIS?

Every ship larger than 300 BRT and all ships with paying passengers are required by international regulations to transmit “AIS messages” via radio in the VHF band periodically:

These messages can be thought of as small “SMS/text messages” and contain information about the ship’s name, draught, cargo, position, speed and a lot of other details.

Despite its name, AIS is not primarily intended for identification of ships – but to support anti-collision. Even very far out at sea. So AIS works without support from any land-based radio infrastructure.

The bandwidth in the VHF band is very limited – so AIS messages are very brief and sent in a compact format, to ensure that there is airtime for everyone – even in congested areas.

Now – we don’t have to be aboard a ship to receive AIS messages. The messages are not encrypted, so anyone with a VHF receiver can receive messages from ships in vicinity of the receiver. Typically a receiver can receive AIS messages from ships in a distance of up to 50 kilometers/30 nautical miles depending on weather and local conditions.

Examples of such receivers are dAISy (cheap, but very limited functionality) and Transas T-55. Other sources of AIS data are subscription services such as VT Explorer and MarineTraffic – or exhange based services such as AIShub.

Most receivers and others sources of AIS data will provide the AIS messages as “NMEA armoured messages”. These messages take a form like this:

!AIVDM,1,1,,A,18UG;P0012G?Uq4EdHa=c;7@051@,0*53

Don’t be scared – we’ll dissect it 🙂

As you can see, the NMEA message is constructed from a number of fields separated by commas:

!AIVDM is the NMEA message type. This tells us that this NMEA message contains AIS data received via AIS. Other possible values are defined in the NMEA 0183 specification.

  • The first “1” tells us the number of NMEA messages which are used to transmit this AIS message. Sometimes it says “2” – but when there’s a “1” like in our example here, things are easy – and we need to consider only this single sentence.
  • The second “1” tells us the sentence number. So this is NMEA message number 1 of 1.
  • The blank field we’ll leave out for now.
  • Then there’s an “A”. This tells us, that this AIS messages was received on AIS channel A. For most uses this is not of great importance.
  • Then we have it: The important part – the payload, which looks like this: “18UG;P0012G?Uq4EdHa=c;7@051@”. This is the actual AIS message which was received via VHF radio – here represented as a string in “6-bit ASCII” format. We could call it the “encoded AIS data”.
  • “0*53” is an NMEA checksum to verify the integrity of this message.

In total, there are 27 different types of messages which can be sent via AIS. These are all defined in great detail in a technical publication by the International Telecommunications Union called “Recommendation ITU-R M.1371-5“. The example above is a type 1 message – or in plain word: A position report. For introductory purposes other messages types of interest are type 2-3 (other variants of position reports) and type 5 (static and voyage related data).

In this blog post I shall not go into detail with how to decode the payload. This process contains a lot of nitty gritty and ugly details. But I maintain a two Java-based open source libraries which are capable of decoding this message into plain old Java objects (POJO’s) so that you do not need to know all these details:

  • AISmessages which I have built from scratch and is used in production in several projects across the planet.
  • AISlib which was built by a client of mine and which I occasionally maintain.

I have deployed AISmessages as an online REST service in a number of different ways (which I will go into detail with in other posts). For instance AISmessages is available on a Heroku service and also an AWS Lambda function.

Pasting the AIS message above into the Heroku implementation on ais.tbsalling.dk reveals that “18UG;P0012G?Uq4EdHa=c;7@051@” can be translated into the following human-readable contents in JSON representation:

{
  "messageType":"PositionReportClassAScheduled",
  "navigationStatus":"UnderwayUsingEngine",
  "rateOfTurn":0,
  "speedOverGround":6.6,
  "positionAccuracy":false,
  "latitude":37.912167,
  "longitude":-122.42299,
  "courseOverGround":350.0,
  "trueHeading":355,
  "specialManeuverIndicator":"NotAvailable",
  "raimFlag":false,
  "communicationState":{
    "syncState":"UTCDirect",
    "slotTimeout":1,
    "numberOfReceivedStations":null,
    "slotNumber":null,
    "utcHour":8,
    "utcMinute":20,
    "slotOffset":null
  },
  "second":40,
  "repeatIndicator":0,
  "sourceMmsi":{
    "mmsi":576048000
  },
  "valid":true
}

Important resources

If you're interested in AIS and software development I recommend you to stay tuned on this blog. Also I can highly recommend to study the following very important specifications, tutorials and views on AIS: