Skip to content

Serving real-time decoded data

Output in the terminal

  • The --verbose option dumps JSON entries to the terminal as messages are received.
  • The --interactive option is similar to the dump1090 --interactive and provides a table view of the visible aircraft. For example the following snapshot can be obtained with a locally connected dongle with jet1090 --interactive rtlsdr://

jet1090 table view

Both options are incompatible and --interactive has the priority.

In the table view, aircraft can be sorted by:

key
. number of messages (count), default
A altitude
C callsign
F first timestamp
L last timestamp
V vertical rate
- ascending/descending order

Output as a JSON file

The -o or --output option enables the dumping of decoded messages to a file in the JSON line format.

If the filename ends in .jsonl.zst, output is automatically compressed with Zstandard. All other filenames keep the existing uncompressed behavior.

jet1090 --output capture.jsonl.zst tcp://127.0.0.1:30005
decode1090 follow capture.jsonl.zst

The compressor and file buffers are flushed approximately every second, even when no new messages arrive, so the file can be followed while it is still being written. decode1090 follow reads from the first byte, displaying existing records before following additions without re-decoding their frames. It accepts plain JSONL too and stops with Ctrl-C. The file must already exist; rotation/replacement and truncation are not supported.

A plain tail -f cannot start arbitrarily inside a compressed frame. The tail -c +1 -f file.jsonl.zst | zstd -dc pipeline reads from the beginning but can lag at low traffic rates because the zstd CLI buffers piped input. decode1090 follow instead reads incrementally and flushes decoded output immediately. Further downstream pipes may still add their own buffering.

jet1090 retains its append behavior: restarting with the same output filename adds another Zstandard frame. The zstd CLI and decode1090 file accept these concatenated frames. Graceful shutdown (including SIGINT/SIGTERM in headless mode) finalizes the current frame. Forced termination such as SIGKILL or power loss may leave an unfinished final frame. decode1090 also supports automatic .jsonl.zst output, but overwrites its output file as before rather than appending.

That format is easily accessible from several libraries across programming languages:

import pandas as pd

df = pd.read_json("output.jsonl", lines=True)
import polars as pl

df = pl.read_ndjson("output.jsonl")

You will need the lazy and ndjson (new line delimited JSON) features:

[dependencies]
polars = { version = "0.34", features = ["lazy", "ndjson"] }
use polars::prelude::*;

let df = LazyFrame::scan_ndjson("output.jsonl", ScanArgsNdJson::default())?
    .collect()?;
data = fetch("output.jsonl")
  .then(response => response.text())
  .then(text => text.split("\n").filter(line => line.trim() !== "").map(JSON.parse));
library(ndjson)

df <- ndjson::stream_in("output.jsonl")

REST API

If a --serve-port option is set, a REST API is set on 0.0.0.0 on the port of your choice.

The following endpoint are provided:

  • /: returns an index page with the available routes
  • /icao24: returns a list of all visible icao24 identifiers
  • /all: returns a list of all state vectors (the last valid field for each aircraft)
  • /track?icao24=xxx: returns a list of all received messages for a given aircraft.
  • /sensors: returns information about all sensors
  • /airports?q=xxx: returns a list of airports matching the query string
  • /stream: a live feed of decoded messages as server-sent events (see below)

Warning

By default, all the history remains accessible until the program exits but this behaviour can drain your RAM memory quickly if you see many aircraft. You can use the --history-expire parameter to limit the history to a number of minutes before the current time.

You can also completely deactivate the storing of messages with the --history-expire 0 option.

Live stream

The /stream endpoint pushes every decoded message as it arrives, in the same JSON format as the .jsonl output and the Redis feed. It uses server-sent events, so any browser can consume it without extra libraries:

const source = new EventSource("http://localhost:8080/stream");
source.onmessage = (event) => console.log(JSON.parse(event.data));

The --df-filter and --aircraft-filter options apply before the stream, so a message excluded there never reaches any subscriber.

A client that reads too slowly is moved to the live edge of the feed and misses the messages in between. The decoder never waits for a client.

WebSocket

(not yet supported)

Redis pub/sub

If the --redis-url is set, all messages are sent to the Redis pub/sub instance.

The default topic is jet1090 but it can be overriden with the --redis-topic option.

You may run a Redis server as a Docker or podman instance:

docker run -d --rm --name redis -p 6379:6379 -p 8001:8001 redis/redis-stack:latest
podman run -d --rm --name redis -p 6379:6379 -p 8001:8001 redis/redis-stack:latest

You may check messages are incoming through the web interface on http://localhost:8001 or with a redis CLI client, for instance:

$ uvx iredis
Installed 11 packages in 51ms
iredis  1.15.0 (Python 3.13.1)
redis-server  7.4.1
Home:   https://iredis.xbin.io/
Issues: https://github.com/laixintao/iredis/issues
127.0.0.1:6379> psubscribe jet1090
# messages should be incoming here