SecureTrack Java API
Note
The API is publicly available via TCP under the address api.secureadsb.com:4201.
Prerequisites
Java 17 or later (JDK)
SecureTrack API token
ca.crtcertificate file, available from Transport Layer Security
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.
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());
}
}