SecureTrack Java API

Note

The API is publicly available via TCP under the address api.secureadsb.com:4201.

Prerequisites

Step 1: Create a Maven Project

To use the SecureTrack API, add gRPC dependencies to your project first. The official gRPC Java documentation is available here:

The downloadable examples below show complete client code. For your own project, configure your build to compile SeRoAPI.proto and generate the gRPC bindings.

Note

We do not recommend using pre-compiled fat JARs for new projects. Generate Java bindings from SeRoAPI.proto in your own build instead. This avoids dependency and Java-version compatibility issues.

Step 2: Generate Java Bindings

Download SeRoAPI.proto and place it in your Maven project’s src/main/proto/ directory. Configure the protobuf Maven plugin to run protoc and protoc-gen-grpc-java during the build.

The documentation build also provides generated Java source files here:

These source files can be useful for inspection or simple experiments, but for production clients prefer generating them from SeRoAPI.proto as part of your own build.

Step 3: Build Your Project

After adding SeRoAPI.proto to your Maven project and configuring protobuf generation, build your project:

mvn clean package

This compiles SeRoAPI.proto and your client code. The generated classes are written under Maven’s generated-source and target directories.

Javadoc

The javadoc for the Java bindings can be found here. Pay special attention to the SeRoAPIGrpc.SeRoAPIStub and SeRoAPIGrpc.SeRoAPIBlockingStub classes which provide the methods to access the SecureTrack API.

Step 4: Connect and Use the API

Note

Before you start, see Authentication for information on user authentication. We also use TLS for server authentication and encryption. See Transport Layer Security for information on Transport Layer Security.

We will now show the typical steps that are performed by clients in order to access the SecureTrack API. For the sake of simplicity, we will use a synchronous (blocking) example. For asynchronous (non-blocking) clients, please refer to the Javadoc of the SeRoAPIGrpc.SeRoAPIStub class or see the Asynchronous (Non-blocking) below.

The first step is to create a TLS channel to the API endpoint. Note that you first have to download and provide the server certificate ca.crt, which you can download under Transport Layer Security.

final ChannelCredentials credentials;
try {
    credentials = TlsChannelCredentials.newBuilder()
            .trustManager(new File("path/to/ca.crt"))
            .build();
} catch (IOException e) {
    System.err.println("Could not load TLS certificate: " + e.getMessage());
    return;
}
ManagedChannel ch = Grpc.newChannelBuilderForAddress("api.secureadsb.com", 4201, credentials)
        .build();

Using this channel, we can now create a (blocking in this example) stub for the SecureTrack API:

SeRoAPIGrpc.SeRoAPIBlockingStub stub = SeRoAPIGrpc.newBlockingStub(ch);

Now we are all set to request some data from the API. Since all SecureTrack API calls require a request object as argument, we will need to create that one first. We start with a simple retrieval of information on our sensors:

SeRoAPIProto.SensorInfoRequest sensorRequest = SeRoAPIProto.SensorInfoRequest.newBuilder()
        .setToken("INSERT YOUR TOKEN HERE") // mandatory!
        .build();

With this request, we can now use the stub to do the call and retrieve the information:

SeRoAPIProto.SensorInfoResponse sensorResponse = stub.getSensorInfo(sensorRequest);

The response object now contains all kinds of information on the sensors that are associated with this token.

Warning

If the token is not valid or if you request data for a sensor that does not exist or for which the token lacks permissions, stub.getSensorInfo(sensorRequest) will throw a StatusRuntimeException. So it is generally advisable to surround calls to stub methods with a try-catch block.

To access a stream of data such as all Mode S replies that are received by your receivers, the blocking stub uses iterators. But as always, we will have to define our request first. When requesting data, we are often able to set specific filters for the data stream. In the case of Mode S replies, we can use filters to only retrieve data that was received by a specific receiver and we can subscribe to data from a specific Mode S transponder only. The following example just shows a fictional example for both filters:

SeRoAPIProto.ModeSDownlinkFramesRequest modeSRequest = SeRoAPIProto.ModeSDownlinkFramesRequest.newBuilder()
    .setToken("INSERT YOUR TOKEN HERE") // mandatory!
    .addSensorFilter(SeRoAPIProto.Sensor.newBuilder()
            .setSerial(123456L)
            .setType(SeRoAPIProto.Sensor.Type.GRX1090)
            .build())
    .addAircraftFilter(0xc0ffee)
    .build();

You can of course also add more sensors and/or aircraft filters. If you add no sensor and aircraft filters at all, the SecureTrack API will simply return data for all aircraft and all sensors that are associated with your token.

Warning

Depending on the number of sensors associated with your token and their coverage, the data volume of the stream can be very high. We recommend to apply as many filters as possible to narrow down the data stream to what’s really important to you.

For example: a sensor that is located in a high traffic density area with a 450 km range in all directions can produce up to about 2500 Mode S replies per second. If you are only interested in ADS-B data, however, add a filter for downlink format 17 to enable server-side filtering.

The stream of data can now be accessed as follows:

Iterator<SeRoAPIProto.ModeSDownlinkFrame> iter = stub.getModeSDownlinkFrames(modeSRequest);
while (iter.hasNext()) {
    SeRoAPIProto.ModeSDownlinkFrame frame = iter.next();

    // process Mode S signal
}

Note that the iterator will keep going until either the connection is interrupted, the server restarted or your client decides that it received enough data.

Once we are done, we simply close the connection to the server by shutting down the channel:

ch.shutdownNow();

Detailed examples for both Synchronous (Blocking) and Asynchronous (Non-blocking) can be found below.

Graceful Shutdown

Warning

It is important to shut down streams gracefully if your program keeps running after reading from a stream or if you want to change the data subscription (filters). Otherwise the stream will just continue receiving and buffering data in the background which will occupy unnecessary memory and network resources.

There are two ways to accomplish a graceful shutdown:

Option 1 is to shut down the channel and create a new channel and stub for subsequent calls. Shutting down the channel can simply be done by calling:

ch.shutdownNow();

Option 2 is using a cancellable context to explicitly tell the server that you are done with a specific stream. This is unfortunately not well implemented in the Java API and not really documented by the gRPC folks. However, here’s a simple example:

// attach context
Context.CancellableContext withCancellation = Context.current().withCancellation();
Context ctx = withCancellation.attach();

Iterator<SeRoAPIProto.ModeSDownlinkFrame> iter = stub.getModeSDownlinkFrames(modeSRequest);
// do processing

// close context
withCancellation.close();
withCancellation.detach(ctx);

Further Reading

Examples

The following examples can be downloaded individually and are embedded below. Build your Maven project before running them.

Synchronous (Blocking)

BlockingExample.java retrieves sensor information and target reports with a blocking gRPC client.

Download BlockingExample.java

package de.serosystems.proto.v3.backend.api;

import io.grpc.*;

import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.UUID;

/**
 * This example shows how to use the SecureTrack backend API in
 * a blocking (synchronous) fashion.
 *
 * @author Matthias Schäfer (schaefer@sero-systems.de)
 */
public class BlockingExample {

    private static final String API_ADDRESS = "api.secureadsb.com";
    private static final int API_PORT = 4201;
    private static final UUID token = UUID.fromString("INSERT YOUR TOKEN HERE");
    private static final String TLS_CA = "ca.crt"; // point this to your ca.crt

    private static final long DATA_TIMEOUT_MS = 15_000; // 15 seconds

    public static void printSensorInfo(Collection<SeRoAPIProto.SensorInformation> sensors) {
        // print response
        System.out.printf("We got information about %d sensors:\n", sensors.size());

        for (SeRoAPIProto.SensorInformation info : sensors)
            // check if location is available
            switch (info.getGnss().getPosition().getFixType()) {
                case Pos2D:
                case Pos3D:
                case TimeOnly:
                    // sensor location available
                    System.out.printf("\t%d (%s, alias %s) at %.4f,%.4f.\n",
                            info.getSensor().getSerial(), info.getSensor().getType(), info.getAlias(),
                            info.getGnss().getPosition().getLatitude(), info.getGnss().getPosition().getLongitude());
                    break;
                case None:
                case UNRECOGNIZED:
                    // sensor location unknown
                    System.out.printf("\t%d (%s alias %s) at an unknown location.\n",
                            info.getSensor().getSerial(), info.getSensor().getType(), info.getAlias());
                    break;
            }
    }

    public static void printTargetReports(Collection<SeRoAPIProto.TargetReport> reports) {
        // some counters
        int adsb = 0, mlat = 0, both = 0, none = 0;
        int noADSB = 0, ADSBv0 = 0, ADSBv1 = 0, ADSBv2 = 0, ADSBv3 = 0;
        boolean hasValidADSB, hasValidMLAT;
        for (SeRoAPIProto.TargetReport t : reports) {
            // do we have recent ADS-B info?
            hasValidADSB = t.hasAdsb() &&
                    System.currentTimeMillis() - t.getAdsb().getPositionLastSeen() < DATA_TIMEOUT_MS;

            // do we have recent MLAT info?
            hasValidMLAT = t.hasMlat() &&
                    System.currentTimeMillis() - (t.getMlat().getTxTimestamp() / 1_000_000L) < DATA_TIMEOUT_MS;

            // do we have ADS-B information on the target and is it up to date?
            if (hasValidADSB) {
                adsb++;

                // count versions
                switch (t.getAdsb().getAdsbVersion()) {
                    case 0:
                        ADSBv0++;
                        break;
                    case 1:
                        ADSBv1++;
                        break;
                    case 2:
                        ADSBv2++;
                        break;
                    case 3:
                        ADSBv3++;
                        break;
                    default: // ignore
                }
            } else noADSB++;

            // do we have MLAT information on the target and is it up to date?
            if (hasValidMLAT)
                mlat++;

            if (hasValidADSB && hasValidMLAT)
                both++;

            if (!hasValidADSB && !hasValidMLAT)
                none++;
        }

        System.out.printf("%d targets were equipped with ADS-B.\n", adsb);
        System.out.printf("%d targets were not tracked with ADS-B.\n", noADSB);
        System.out.printf("%d targets were tracked by MLAT.\n", mlat);
        System.out.printf("%d targets were tracked by both ADS-B and MLAT.\n", both);
        System.out.printf("%d targets had neither valid ADS-B nor MLAT info.\n", none);

        System.out.printf("Out of the %d ADS-B equipped targets, we saw the following versions:\n", adsb);
        System.out.printf("\t%d had ADS-B version 0 transponders\n", ADSBv0);
        System.out.printf("\t%d had ADS-B version 1 transponders\n", ADSBv1);
        System.out.printf("\t%d had ADS-B version 2 transponders\n", ADSBv2);
        System.out.printf("\t%d had ADS-B version 3 transponders\n", ADSBv3);
    }

    public static void main(String[] args) {

        System.out.println("Hello world! This is an example for using the SecureTrack API with blocking gRPC calls!");

        System.out.printf("Connecting to SeRo API at %s:%d.\n", API_ADDRESS, API_PORT);

        // first create channel
        final ChannelCredentials credentials;
        try {
            credentials = TlsChannelCredentials.newBuilder()
                    .trustManager(new File(TLS_CA))
                    .build();
        } catch (IOException e) {
            System.err.println("Could not load TLS certificate: " + e.getMessage());
            return;
        }
        ManagedChannel ch = Grpc.newChannelBuilderForAddress(API_ADDRESS, API_PORT, credentials)
                .build();

        // then create stub
        SeRoAPIGrpc.SeRoAPIBlockingStub stub = SeRoAPIGrpc.newBlockingStub(ch);

        System.out.println("Retrieving info about all sensors that are available to this token.");

        // prepare request
        SeRoAPIProto.SensorInfoRequest sensorRequest = SeRoAPIProto.SensorInfoRequest.newBuilder()
                .setToken(token.toString())
                .build();

        // retrieve response
        SeRoAPIProto.SensorInfoResponse sensorResponse = stub.getSensorInfo(sensorRequest);

        // print response
        printSensorInfo(sensorResponse.getSensorInfoList());

        System.out.println("Let's see what happens when we request info for a sensor that does not belong to us!");

        sensorRequest = SeRoAPIProto.SensorInfoRequest.newBuilder()
                .setToken(token.toString())
                .addSensors(SeRoAPIProto.Sensor.newBuilder()
                        .setSerial(12345L)
                        .setType(SeRoAPIProto.Sensor.Type.GRX1090)
                        .build())
                .build();

        try {
            stub.getSensorInfo(sensorRequest);
            System.out.println("It worked?!"); // you shouldn't see this message
        } catch (StatusRuntimeException e) {
            System.out.printf("Retrieving sensor info failed (%s).\n", e.getStatus().getCode());
            System.out.println("Cause was: " + e.getStatus().getDescription());
        }

        System.out.println("Now we are going to retrieve target state reports for about 10 seconds.");

        SeRoAPIProto.TargetReportsRequest targetReportsRequest = SeRoAPIProto.TargetReportsRequest.newBuilder()
                .setToken(token.toString())
                .build();

        // some data handling
        HashMap<Integer, SeRoAPIProto.TargetReport> targetReports = new HashMap<>();
        long start = System.currentTimeMillis();
        long count = 0L;

        // open stream
        Iterator<SeRoAPIProto.TargetReport> it = stub.getTargetReports(targetReportsRequest);
        while (it.hasNext() && System.currentTimeMillis() - start < 10_000L) {
            SeRoAPIProto.TargetReport target = it.next();
            count++;

            // store in target map (replaces older data of the same target)
            targetReports.put(target.getTarget().getAddress(), target);
        }

        System.out.printf("Done! We received %d target reports from %d different aircraft!\n",
                count, targetReports.size());

        printTargetReports(targetReports.values());

        // close connection gracefully
        ch.shutdownNow();

    }

}

Asynchronous (Non-blocking)

AsynchronousExample.java retrieves sensor information and target reports with asynchronous gRPC callbacks.

Download AsynchronousExample.java

package de.serosystems.proto.v3.backend.api;

import io.grpc.*;
import io.grpc.stub.StreamObserver;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.UUID;

import static de.serosystems.proto.v3.backend.api.BlockingExample.printSensorInfo;
import static de.serosystems.proto.v3.backend.api.BlockingExample.printTargetReports;

/**
 * This example shows how to use the SecureTrack backend API in
 * a non-blocking (asynchronous) fashion.
 *
 * @author Matthias Schäfer (schaefer@sero-systems.de)
 */
public class AsynchronousExample {

    private static final String API_ADDRESS = "api.secureadsb.com";
    private static final int API_PORT = 4201;
    private static final UUID token = UUID.fromString("INSERT YOUR TOKEN HERE");
    private static final String TLS_CA = "ca.crt"; // point this to your ca.crt

    private static class SensorInfoSink implements StreamObserver<SeRoAPIProto.SensorInfoResponse> {
        boolean finished = false;

        @Override
        public void onNext(SeRoAPIProto.SensorInfoResponse response) {
            // called when data is returned
            printSensorInfo(response.getSensorInfoList());
        }

        @Override
        public void onError(Throwable t) {
            // called when an error occurs
            finished = true;
            System.err.println("Retrieving sensor information failed!");
            System.err.println("Reason: " + t.getMessage());
        }

        @Override
        public void onCompleted() {
            // called when the request is completed
            finished = true;
            System.out.println("Retrieving sensor data completed!");
        }

        public boolean isRunning() {
            return !finished;
        }
    }

    private static class TargetReportsSink implements StreamObserver<SeRoAPIProto.TargetReport> {
        boolean finished = false;
        HashMap<Integer, SeRoAPIProto.TargetReport> targetReports = new HashMap<>();
        long count = 0L;

        @Override
        public void onNext(SeRoAPIProto.TargetReport report) {
            // called when new data arrives
            count++;
            targetReports.put(report.getTarget().getAddress(), report);
        }

        @Override
        public void onError(Throwable t) {
            // called when an error occurs
            synchronized (this) {
                finished = true;
                if (Status.fromThrowable(t).getCode() == Status.Code.CANCELLED) {
                    System.out.println("Target report stream was cancelled.");
                } else {
                    System.err.println("An error occurred while retrieving target reports or stream was closed!");
                    System.err.println("Reason: " + t.getMessage());
                }
                this.notifyAll(); // wake up whoever is waiting
            }
        }

        @Override
        public void onCompleted() {
            // Note: this should only happen if client closes the stream or the server is restarted
            synchronized (this) {
                finished = true;
                System.out.println("Stream was closed!");
                this.notifyAll(); // wake up whoever is waiting
            }
        }

        /**
         * Blocks until the stream is closed or timeout fires
         * @param timeout in milliseconds (0 disables timeout)
         * @return true if stream finished
         */
        public boolean waitUntilFinished(long timeout) throws InterruptedException {
            synchronized (this) {
                if (finished) return true;
                this.wait(timeout); // wait
            }

            return finished;
        }
    }

    public static void main(String[] args) {

        System.out.println("Hello world! This is an example for using the SecureTrack API with asynchronous gRPC calls!");

        System.out.printf("Connecting to SeRo API at %s:%d.\n", API_ADDRESS, API_PORT);

        // first create channel
        final ChannelCredentials credentials;
        try {
            credentials = TlsChannelCredentials.newBuilder()
                    .trustManager(new File(TLS_CA))
                    .build();
        } catch (IOException e) {
            System.err.println("Could not load TLS certificate: " + e.getMessage());
            return;
        }
        ManagedChannel ch = Grpc.newChannelBuilderForAddress(API_ADDRESS, API_PORT, credentials)
                .build();

        // then create stub
        SeRoAPIGrpc.SeRoAPIStub stub = SeRoAPIGrpc.newStub(ch);

        System.out.println("Retrieving info about all sensors that are available to this token.");

        // prepare request
        SeRoAPIProto.SensorInfoRequest sensorRequest = SeRoAPIProto.SensorInfoRequest.newBuilder()
                .setToken(token.toString())
                .build();

        SensorInfoSink sensorInfoObserver = new SensorInfoSink();

        // send request and return immediately
        stub.getSensorInfo(sensorRequest, sensorInfoObserver);

        System.out.println("Sleeping in the meantime...");
        try {
            while (sensorInfoObserver.isRunning())
                Thread.sleep(5_000L);
        } catch (InterruptedException e) {
            System.err.println("Something woke me up unexpectedly: " + e.getMessage());
        }

        System.out.println("Ok, that nap was great! Now on to other things...");

        System.out.println("Let's see what happens when we request info for a sensor that does not belong to us!");

        sensorRequest = SeRoAPIProto.SensorInfoRequest.newBuilder()
                .setToken(token.toString())
                .addSensors(SeRoAPIProto.Sensor.newBuilder()
                        .setSerial(12345L)
                        .setType(SeRoAPIProto.Sensor.Type.GRX1090)
                        .build())
                .build();

        sensorInfoObserver = new SensorInfoSink();
        stub.getSensorInfo(sensorRequest, sensorInfoObserver);

        // wait for call to finish
        try {
            while (sensorInfoObserver.isRunning())
                Thread.sleep(100L);
        } catch (InterruptedException e) {
            System.err.println("Something woke me up unexpectedly: " + e.getMessage());
        }

        System.out.println("Now we are going to retrieve target state reports for about 10 seconds.");

        SeRoAPIProto.TargetReportsRequest targetReportsRequest = SeRoAPIProto.TargetReportsRequest.newBuilder()
                .setToken(token.toString())
                .build();

        TargetReportsSink targetReportsSink = new TargetReportsSink();

        // since we want to cancel it after 10 seconds, we need to do the call within a cancellable context
        Context.CancellableContext withCancellation = Context.current().withCancellation();
        Context ctx = withCancellation.attach();
        try {
            stub.getTargetReports(targetReportsRequest, targetReportsSink);
            Thread.sleep(10_000);
        } catch (InterruptedException e) {
            System.err.println("Someone woke me up early :-(");
        } finally {
            withCancellation.close();
            withCancellation.detach(ctx);
        }

        // wait for stream to finish gracefully
        try {
            targetReportsSink.waitUntilFinished(0L);
        } catch (InterruptedException e) {
            System.err.println("Something interrupted me while I was waiting for the stream to finish :-(");
        }

        System.out.printf("Done! We received %d target reports from %d different aircraft!\n",
                targetReportsSink.count, targetReportsSink.targetReports.size());

        printTargetReports(targetReportsSink.targetReports.values());
    }

}