SecureTrack Java API

Note

You need Java 17 or higher to use the API.

Note

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

Installation

The Clean Approach

To use the SecureTrack API you will need to add gRPC dependencies to your project first. Please follow the instructions of the official gRPC Java implementation:

If you are using Maven, simply add the dependencies mentioned under the above link to your project. Alternatively you can also add the individual jars (also provided under the above link) to your classpath.

Once you added the dependencies to your project, you can add the two Java source code files SeRoAPIGrpc.java and SeRoAPIProto.java to your project. They are the output of the protocol buffer compiler protoc. These files contain the serialization/deserialization code for the data that is exchanged between the API and the client (you) as well as the stub classes that you will use to talk to the SecureTrack API server.

You can either generate these files yourself (see this documentation) or download them here:

The Fat Jar Approach

For those of you who are not using Maven or Gradle and do not want to bother themselves with adding all dependencies, we also provide a fat JAR that contains all dependencies and pre-compiled API bindings here:

Just add this file to your project’s classpath and you are all set for using the SecureTrack API. Note that if you are using Eclipse, make sure you add the library to your Modulepath rather than the Classpath.

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.

Quick Start

Note

See Token Authentication for information on authentication.

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 channel to the API endpoint:

ManagedChannel ch = ManagedChannelBuilder.forAddress("api.secureadsb.com", 4200)
        .usePlaintext()
        .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 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 to 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 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 shutdown 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 shutdown 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

Synchronous (Blocking)

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

import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.StatusRuntimeException;

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.sero-systems.de";
    private static final int API_PORT = 31071;
    private static final UUID token = UUID.fromString("INSERT YOUR TOKEN HERE");

    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("Using token '%s' for authentication.\n", token);

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

        // first create channel
        ManagedChannel ch = ManagedChannelBuilder.forAddress(API_ADDRESS, API_PORT)
                .usePlaintext()
                .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)

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

import io.grpc.Context;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.stub.StreamObserver;

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.sero-systems.de";
    private static final int API_PORT = 31071;
    private static final UUID token = UUID.fromString("INSERT YOUR TOKEN HERE");

    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;
                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("Using token '%s' for authentication.\n", token);

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

        // first create channel
        ManagedChannel ch = ManagedChannelBuilder.forAddress(API_ADDRESS, API_PORT)
                .usePlaintext()
                .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());
    }

}