Edge SDK — Quickstart Guide
This guide walks you through creating a new edge adapter project from scratch using the Zequent Edge SDK. By the end, you will have an adapter application that can be packaged as a container image, receive commands from the platform, and push telemetry data.
Prerequisites
- Java 25
- Maven 3.9.9 or higher
- Docker or Podman (for running platform services)
- GitHub account with access to the Zequent packages repository
Step 1: Create a New Quarkus Project
mvn io.quarkus:quarkus-maven-plugin:3.17.4:create \
-DprojectGroupId=com.example.edge \
-DprojectArtifactId=my-edge-adapter \
-Dextensions="grpc,smallrye-health,arc"
cd my-edge-adapter
Step 2: Add the Edge SDK Dependency
Open pom.xml and add the Edge SDK and the GitHub Packages repository:
<dependencies>
<!-- Zequent Edge SDK -->
<dependency>
<groupId>com.zqnt.sdk</groupId>
<artifactId>edge-java-sdk</artifactId>
<version>2.0.0</version>
</dependency>
</dependencies>
<repositories>
<repository>
<id>github</id>
<url>https://maven.pkg.github.com/Zequent/zqnt-edge-sdk-java</url>
</repository>
</repositories>
The examples on this page log with Lombok's @Slf4j: add Lombok to your build (on JDK 23+ also as
an annotation processor in maven-compiler-plugin), or use any other logger.
Make sure your ~/.m2/settings.xml has the GitHub credentials:
<settings>
<servers>
<server>
<id>github</id>
<username>YOUR_GITHUB_USERNAME</username>
<password>YOUR_GITHUB_TOKEN</password>
</server>
</servers>
</settings>
Step 3: Configure the Adapter
Create or edit src/main/resources/application.properties:
# Application
quarkus.application.name=my-edge-adapter
quarkus.http.host=0.0.0.0
quarkus.http.port=9001
# Edge Identity
zequent.edge.endpoint=localhost:9001
zequent.edge.sn=YOUR_DEVICE_SERIAL_NUMBER
zequent.edge.asset-type=ASSET_TYPE_DOCK
zequent.edge.asset-vendor=ASSET_VENDOR_DJI
# Platform services (read by EdgeWiring below)
grpc.client.live-data.host=${LIVE_DATA_SERVICE_HOST:localhost}
grpc.client.live-data.port=${LIVE_DATA_SERVICE_PORT:8003}
grpc.client.connector.host=${CONNECTOR_SERVICE_HOST:localhost}
grpc.client.connector.port=${CONNECTOR_SERVICE_PORT:8010}
# gRPC server shares HTTP port
quarkus.grpc.server.use-separate-server=false
And the adapter's credentials, as environment variables:
# Edge credential: the Admin Console, Manage -> Access & Integrations -> Credentials (kind "Edge adapter")
ZQNT_EDGE_TOKEN=<your edge credential>
# The platform's public key, to check the platform's calls into the adapter
ZQNT_PLATFORM_PUBLIC_KEY=<the platform's service public key>
See Configuration for both.
Step 3b: Wire the SDK
The SDK registers no beans of its own. Three small classes connect it to Quarkus: one creates the services your adapter uses (with the edge credential on every channel), one exposes your adapter to the platform over gRPC, and one checks that calls into it come from the platform.
package com.example.edge;
import com.zqnt.sdk.edge.application.ProtoJsonMapper;
import com.zqnt.sdk.edge.auth.EdgeAuthConfig;
import com.zqnt.sdk.edge.auth.EdgeCredentialsClientInterceptor;
import com.zqnt.sdk.edge.connector.application.ConnectorService;
import com.zqnt.sdk.edge.connector.application.impl.ConnectorServiceImpl;
import com.zqnt.sdk.edge.livedata.application.DetectionMapper;
import com.zqnt.sdk.edge.livedata.application.LiveDataService;
import com.zqnt.sdk.edge.livedata.application.NotificationMapper;
import com.zqnt.sdk.edge.livedata.application.TelemetryMapper;
import com.zqnt.sdk.edge.livedata.application.impl.LiveDataServiceImpl;
import com.zqnt.utils.connector.proto.ConnectorServiceGrpc;
import com.zqnt.utils.livedata.proto.LiveDataServiceGrpc;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Produces;
import org.eclipse.microprofile.config.inject.ConfigProperty;
// The SDK registers no beans of its own: this class creates what the adapter uses.
@ApplicationScoped
public class EdgeWiring {
@ConfigProperty(name = "grpc.client.live-data.host") String liveDataHost;
@ConfigProperty(name = "grpc.client.live-data.port") int liveDataPort;
@ConfigProperty(name = "grpc.client.connector.host") String connectorHost;
@ConfigProperty(name = "grpc.client.connector.port") int connectorPort;
// Puts ZQNT_EDGE_TOKEN on every call to the platform
private final EdgeCredentialsClientInterceptor credentials =
new EdgeCredentialsClientInterceptor(EdgeAuthConfig.fromEnv().edgeToken());
@Produces
@ApplicationScoped
ProtoJsonMapper protoJsonMapper() {
return new ProtoJsonMapper();
}
@Produces
@ApplicationScoped
LiveDataService liveDataService() {
return new LiveDataServiceImpl(new TelemetryMapper(), new DetectionMapper(), new NotificationMapper(),
LiveDataServiceGrpc.newStub(channel(liveDataHost, liveDataPort)));
}
@Produces
@ApplicationScoped
ConnectorService connectorService(ProtoJsonMapper mapper) {
return new ConnectorServiceImpl(mapper, ConnectorServiceGrpc.newStub(channel(connectorHost, connectorPort)));
}
private ManagedChannel channel(String host, int port) {
return ManagedChannelBuilder.forAddress(host, port)
.usePlaintext()
.intercept(credentials)
.build();
}
}
package com.example.edge;
import com.zqnt.sdk.edge.adapter.application.EdgeAdapterService;
import com.zqnt.sdk.edge.adapter.grpc.EdgeAdapterGrpcServiceImpl;
import com.zqnt.sdk.edge.application.ProtoJsonMapper;
import io.quarkus.grpc.GrpcService;
// Exposes your EdgeAdapterService to the platform over gRPC
@GrpcService
public class EdgeGrpcService extends EdgeAdapterGrpcServiceImpl {
public EdgeGrpcService(EdgeAdapterService adapter, ProtoJsonMapper mapper) {
super(adapter, mapper);
}
}
package com.example.edge;
import com.zqnt.sdk.edge.auth.EdgeAuthConfig;
import com.zqnt.sdk.edge.auth.PlatformAuthServerInterceptor;
import io.grpc.Metadata;
import io.grpc.ServerCall;
import io.grpc.ServerCallHandler;
import io.grpc.ServerInterceptor;
import io.quarkus.grpc.GlobalInterceptor;
import jakarta.enterprise.context.ApplicationScoped;
// Refuses every call into the adapter that the platform did not sign
@GlobalInterceptor
@ApplicationScoped
public class PlatformAuth implements ServerInterceptor {
private final ServerInterceptor delegate = new PlatformAuthServerInterceptor(EdgeAuthConfig.fromEnv());
@Override
public <Q, R> ServerCall.Listener<Q> interceptCall(ServerCall<Q, R> call, Metadata headers,
ServerCallHandler<Q, R> next) {
return delegate.interceptCall(call, headers, next);
}
}
Health probes stay open. Without ZQNT_PLATFORM_PUBLIC_KEY every command is refused;
ZQNT_EDGE_AUTH_DISABLED=true turns the check off, for a local stack only. Add channels for
Mission Autonomy (MissionAutonomyServiceImpl) the same way when you need it.
Step 4: Implement the EdgeAdapterService
Create your adapter class. You only need to override the commands that your hardware supports:
package com.example.edge;
import com.zqnt.sdk.edge.adapter.application.EdgeAdapterService;
import com.zqnt.sdk.edge.adapter.domains.*;
import jakarta.enterprise.context.ApplicationScoped;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CompletableFuture;
@Slf4j
@ApplicationScoped
public class MyDeviceAdapter implements EdgeAdapterService {
@Override
public CompletableFuture<CommandResult> takeOff(TakeOffRequest request) {
log.info("Takeoff requested for SN: {}", request.getSn());
// Replace with your actual device API call
boolean success = true;
if (success) {
return CompletableFuture.completedFuture(
CommandResult.success("Takeoff initiated", request.getTid(), request.getSn())
);
} else {
return CompletableFuture.completedFuture(
CommandResult.error("Takeoff failed", request.getSn())
);
}
}
@Override
public CompletableFuture<CommandResult> returnToHome(ReturnToHomeRequest request) {
log.info("Return to home for SN: {}", request.getSn());
return CompletableFuture.completedFuture(
CommandResult.success("Returning to home", request.getTid(), request.getSn())
);
}
@Override
public CompletableFuture<CommandResult> openCover(String sn) {
log.info("Opening cover for SN: {}", sn);
return CompletableFuture.completedFuture(
CommandResult.success("Cover opening", sn)
);
}
// All other commands default to NOT_IMPLEMENTED and that is fine.
// Override more as your hardware supports them.
}
Step 5: Push Telemetry Data (Optional)
If your device produces telemetry, inject the LiveDataService and push data:
package com.example.edge;
import com.zqnt.sdk.edge.livedata.application.LiveDataService;
import com.zqnt.sdk.edge.adapter.domains.TelemetryRequestData;
import com.zqnt.utils.edge.sdk.domains.TelemetryData;
import jakarta.enterprise.context.ApplicationScoped;
import lombok.extern.slf4j.Slf4j;
import java.time.Instant;
import java.util.UUID;
@Slf4j
@ApplicationScoped
public class TelemetryProducer {
private final LiveDataService liveDataService;
public TelemetryProducer(LiveDataService liveDataService) {
this.liveDataService = liveDataService;
}
public void sendAssetTelemetry(String sn) {
TelemetryData.AssetDetails assetDetails = TelemetryData.AssetDetails.builder()
.environmentTemp(22.5f)
.humidity(65.0f)
.build();
TelemetryData telemetry = TelemetryData.builder()
.id(UUID.randomUUID().toString())
.timestamp(Instant.now())
.sn(sn)
.latitude(47.3769)
.longitude(8.5417)
.absoluteAltitude(450.0f)
.asset(assetDetails)
.build();
TelemetryRequestData data = TelemetryRequestData.builder()
.sn(sn)
.tid(UUID.randomUUID().toString())
.timestamp(Instant.now())
.telemetry(telemetry)
.build();
liveDataService.produceTelemetryData(data)
.thenRun(() -> log.debug("Telemetry sent for {}", sn))
.exceptionally(err -> {
log.error("Telemetry error", err);
return null;
});
}
}
Step 6: Start Platform Services
Before running your adapter, start the required platform services from the published container images:
docker compose -f docker-compose.customer.yml up -d
The compose file uses one deployment-local .env file through env_file.
Step 7: Run Your Adapter
docker run --env-file .env -p 9001:9001 your-registry/my-edge-adapter:latest
You should see output similar to:
Starting new Quarkus gRPC server (using Vert.x transport)...
my-edge-adapter 1.0.0-SNAPSHOT on JVM (powered by Quarkus 3.x) started in 0.4s. Listening on: http://0.0.0.0:9001
Your adapter is now running and ready to receive commands from the platform via gRPC.
Step 8: Test with grpcurl
You can test your adapter endpoint with grpcurl. Your adapter refuses calls the platform did not
sign, so for this test run it with ZQNT_EDGE_AUTH_DISABLED=true — locally only — and with
quarkus.grpc.server.enable-reflection-service=true (on by default only in dev mode), so grpcurl
can discover the services:
# Install grpcurl if needed
# brew install grpcurl (macOS)
# go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest (Go)
# List available services
grpcurl -plaintext localhost:9001 list
# Call takeOff
grpcurl -plaintext -d '{
"base": {
"sn": "YOUR_DEVICE_SN",
"tid": "test-123",
"timestamp": "2026-01-01T00:00:00Z"
},
"coordinate": {
"latitude": 47.3769,
"longitude": 8.5417,
"altitude": 100.0
}
}' localhost:9001 zqnt.EdgeAdapterService/TakeOff
Project Structure
After completing this guide, your project should look like this:
my-edge-adapter/
pom.xml
src/
main/
java/
com/example/edge/
MyDeviceAdapter.java # Your EdgeAdapterService implementation
EdgeWiring.java # SDK services, with the edge credential
EdgeGrpcService.java # Exposes the adapter over gRPC
PlatformAuth.java # Checks the platform's calls in
TelemetryProducer.java # Optional: telemetry push logic
resources/
application.properties # Configuration
docker-compose.yml # Platform services (dev)
Next Steps
- Edge Adapter Reference -- Full command reference and advanced patterns
- Configuration Guide -- All configuration properties
- Live Data -- In-depth telemetry streaming guide
- Connector -- Asset pairing and the Skill Registry
- Models Reference -- Complete model documentation
For a ready-made DJI deployment, see DJI Adapter Deployment.