Tracking Real-Time Ship Data with Elixir and a WebSocket API
Who This Blog Post Is For: This blog post is for Elixir developers, hobbyists interested in real-time APIs, or anyone, like myself, who has never seen a WebSocket API before.
Living on the bank of the Detroit River, I watch massive ships pass by every day. While MarineTraffic can help identify these vessels, I thought: why not build my own display to show real-time ship data? This project began with exploring the AIS Stream WebSocket API, my first step in combining Elixir with maritime data.
AIS (Automatic Identification System)
Large ships send out their position information over radio using a system called AIS (Automatic idenfitication system). AIS radio receivers collect data from nearby ships and can share it online, creating a global network of maritime traffic data. By consuming this data via APIs, you can gain insights into ship movements worldwide—or in your backyard.
I chose to explore the AIS Stream WebSocket API to process live AIS messages in real-time. This API provides a WebSocket interface for receiving real-time ship data, which makes it an appropriate project to tackle using Elixir. Elixir is particularly well-suited for consuming real-time APIs over WebSockets, and its advantages stem from its robust concurrency model, lightweight processes, and the fault-tolerant runtime of the underlying Erlang VM (BEAM).
Setting Up the Project
First, I created a new Elixir project:
mix new ais_stream_client
cd ais_stream_client
Then, I added the websockex
library to my dependencies for WebSocket communication and poison for JSON encoding and decoding. Here’s how the mix.exs
file looked:
defp deps do
[
{:websockex, "~> 0.4.3"},
{:poison, "~> 6.0"}
]
end
Run mix deps.get
to fetch the dependencies.
Creating the WebSocket Client
Using websockex
, I created a simple WebSocket client module that connects to the AIS Stream WebSocket API and processes incoming messages.
WebSockex works like other Elixir behaviours (like GenServer
or Supervisor
)–you need to implement some callbacks to start using it.
There are only 3 things you need to do to get started with Websockex.
- Call
WebSockex.start_link
with the URL of the API you’re calling. - Authenticate yourself by sending a message (with your API key) over the websocket to the API.
- Implement the
WebSockex
behaviour and in particular, thehandle_frame
callback to handle any messages the API sends you.
Check out the docs for WebSockex, they’re useful.
Here’s the code to implement a WebSockex behaviour, the bare minimum to get started:
defmodule AISStreamClient do
use WebSockex
@url "wss://stream.aisstream.io/v0/stream" # WebSocket URL
# Start the WebSocket client
def start_link(initial_state \\ %{}) do
WebSockex.start_link(@url, __MODULE__, initial_state, name: __MODULE__)
end
# Handle incoming messages
def handle_frame({:binary, msg}, state) do
case Poison.decode(msg) do
{:ok, decoded_msg} ->
IO.inspect(decoded_msg, label: "Decoded AIS Data")
{:ok, state}
{:error, reason} ->
IO.puts("Failed to decode JSON: #{inspect(reason)}")
{:ok, state}
end
end
# Handle other frames not covered by the above pattern matching
def handle_frame(frame, state) do
IO.puts("Unhandled frame: #{inspect(frame)}")
{:ok, state}
end
end
A few notes:
__MODULE__
is a compile-time macro that represents the name of the current module, in this case,AISStreamClient
.- I’m not using the
initial_state
argument in the call tostart_link
, or the changing the state before it’s returned fromhandle_frame
, but this is the convention of managing state found in OTP behaviours. The state is a way to store information between messages—for example, connection settings or processed data. Even though I don’t update it in this example, following this pattern ensures your client can scale to handle more complex tasks later.
Connecting and Sending the Subscription Key
To authenticate with the AIS Stream WebSocket API, I needed a subscription key. To filter AIS data to a specific geographic area, the API requires a bounding box, defined by latitude and longitude coordinates. Here’s how I started the client and sent the key and bounding boxes:
defmodule AISStreamApp do
def start do
{:ok, pid} = AISStreamClient.start_link(%{})
data = %{
# Replace with your actual API key from AISStream
APIKey: "your_subscription_key",
# Bounding box covering the Detroit River
BoundingBoxes: [
[[42.414173, -82.907625], [42.037176, -83.250223]]
]
}
# Send the API key as JSON
WebSockex.send_frame(pid, {:text, Poison.encode!(data)})
end
end
Run the application using:
iex -S mix
AISStreamApp.start()
Processing AIS Data
I was able to successfully receive and process real-time AIS data. Here’s an example of the decoded JSON message:
Decoded AIS Data: %{
"Message" => %{
"PositionReport" => %{
"Cog" => 91,
"CommunicationState" => 59916,
"Latitude" => 42.30840333333333,
"Longitude" => -83.083435,
"MessageID" => 3,
"NavigationalStatus" => 5,
"PositionAccuracy" => true,
"Raim" => false,
"RateOfTurn" => 0,
"RepeatIndicator" => 0,
"Sog" => 0,
"Spare" => 0,
"SpecialManoeuvreIndicator" => 0,
"Timestamp" => 42,
"TrueHeading" => 43,
"UserID" => 538006783,
"Valid" => true
}
},
"MessageType" => "PositionReport",
"MetaData" => %{
"MMSI" => 538006783,
"MMSI_String" => 538006783,
"ShipName" => "FEDERAL OSHIMA ",
"latitude" => 42.30840333333333,
"longitude" => -83.083435,
"time_utc" => "2024-12-09 02:27:43.237370229 +0000 UTC"
}
}
This is great, and exactly what I wanted. However, I was receiving data intermittently; on some days I would receive information and on others I would not. I wonder if this is due to the intermittent running of the AIS radio receivers. (e.g., Maybe it was turned off?)
Oh well! I’ll keep an eye on it, and hopefully it will work out for me. I’ll try another API next time!
Conclusion
While I’m not certain that the API will work for my specific needs, building a WebSocket client in Elixir to consume real-time ship data was a great learning experience. If you’re working with WebSockets in Elixir, libraries like websockex and poison make the process straightforward.
You can check out AIS Stream’s documentation to explore their API further. Let me know if you have any questions or tips for improving WebSocket clients in Elixir!
Note: This is just the beginning! In future posts, I’ll explore other AIS APIs and ways to integrate this data into a custom physical display. Stay tuned.