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());
}
}