Edge SDK — Edge Adapter Service
The EdgeAdapterService interface is the core contract of the Edge SDK. Every edge adapter must provide a CDI bean that implements this interface. The SDK ships with a default implementation (EdgeAdapterServiceImpl) whose methods all return NOT_IMPLEMENTED, so you only need to override the commands that your particular hardware supports.
Full method-by-method reference: Edge Adapter API Reference.
Table of Contents
How It Works
The SDK's gRPC service (EdgeAdapterGrpcServiceImpl) receives commands from the platform and delegates them to your EdgeAdapterService bean. Your adapter registers it, by subclassing it with @GrpcService (see gRPC Layer). The flow is:
Platform Services --(gRPC)--> EdgeAdapterGrpcServiceImpl --(delegates)--> Your EdgeAdapterService implementation
Every command method returns CompletableFuture<CommandResult>, which means your implementation can be fully asynchronous. The gRPC layer wraps it in a Mutiny Uni automatically.
Creating an Adapter
Step 1: Implement the Interface
Create a CDI bean that implements EdgeAdapterService. The @ApplicationScoped annotation ensures there is a single instance across the application lifecycle.
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 java.util.concurrent.CompletableFuture;
@ApplicationScoped
public class MyDeviceAdapter implements EdgeAdapterService {
@Override
public CompletableFuture<CommandResult> takeOff(TakeOffRequest request) {
// Call your device SDK/API here
boolean success = myDevice.initiateTakeoff(
request.getCoordinates().getLatitude(),
request.getCoordinates().getLongitude(),
request.getCoordinates().getAltitude()
);
if (success) {
return CompletableFuture.completedFuture(
CommandResult.success("Takeoff initiated", request.getTid(), request.getSn())
);
} else {
return CompletableFuture.completedFuture(
CommandResult.error("Takeoff failed: device busy", request.getSn())
);
}
}
}
Step 2: Asynchronous Implementation
If your device SDK provides asynchronous or callback-based APIs, use CompletableFuture accordingly:
@Override
public CompletableFuture<CommandResult> takeOff(TakeOffRequest request) {
CompletableFuture<CommandResult> future = new CompletableFuture<>();
myDevice.takeoffAsync(request.getCoordinates(), new DeviceCallback() {
@Override
public void onSuccess() {
future.complete(CommandResult.success("Takeoff complete", request.getSn()));
}
@Override
public void onError(String errorMsg) {
future.complete(CommandResult.error(errorMsg, request.getSn()));
}
});
return future;
}
Step 3: Override Only What You Support
Any method that you do not override will automatically return a NOT_IMPLEMENTED result to the caller. This is by design -- a dock adapter may support openCover and startCharging but not takeOff (which is a drone-level command), and that is perfectly fine.
See the API Reference for the full command surface, grouped
by area (Flight Control, Dock Operations, Camera and Gimbal, Manual Control, Live Streaming, Debug
and Maintenance, Task Execution — of which 2.0 only still calls the cancel path —, Capability
Reporting), plus CommandResult, the default
convenience-method overloads, and the error-code mapping.
Custom Commands
For a command that doesn't map to a standard EdgeAdapterService method, override sendCustomCommand.
This is how a waypoint route reaches an adapter in 2.0: the waypoints and configuration arrive inline in
params as mission.waypoint.execute (see Waypoint Missions).
The platform waits for the outcome. A successful custom command is answered as accepted, not as
finished: when it runs as part of a Skill execution, the step completes only when your adapter reports a
command execution event for it. The same holds for takeOff, goTo, lookAt and returnToHome. So return CommandResult.accepted(...) with your own execution id,
and publish the outcome under that id through LiveDataService:
@Override
public CompletableFuture<CommandResult> sendCustomCommand(String sn, String componentId,
String commandType, Map<String, Object> params) {
if (!"mission.waypoint.execute".equals(commandType)) {
return CompletableFuture.completedFuture(CommandResult.notImplemented("Unknown command " + commandType, sn));
}
String executionId = UUID.randomUUID().toString();
deviceApi.startWaypointMission(params, () ->
report(sn, executionId, commandType, CommandExecutionStatus.COMMAND_EXECUTION_STATUS_SUCCEEDED));
return CompletableFuture.completedFuture(
CommandResult.accepted("Waypoint mission started", executionId, sn));
}
@Override
public CompletableFuture<CommandResult> cancelExecution(String sn, String externalExecutionId) {
deviceApi.abortMission(externalExecutionId);
return CompletableFuture.completedFuture(CommandResult.success("Mission aborted", sn));
}
private void report(String sn, String executionId, String commandId, CommandExecutionStatus status) {
liveData.produceNotificationData(NotificationRequestData.builder()
.sn(sn)
.tid(UUID.randomUUID().toString())
.timestamp(Instant.now())
.eventType(NotificationEventType.NOTIFICATION_EVENT_COMMAND_EXECUTION)
.commandExecutionEvent(NotificationRequestData.CommandExecutionEventData.builder()
.externalExecutionId(executionId)
.commandId(commandId)
.assetSn(sn)
.status(status)
.occurredAt(Instant.now())
.build())
.build());
}
- Publish
COMMAND_EXECUTION_STATUS_RUNNINGwithprogresswhile it runs, and exactly one terminal status:SUCCEEDED,FAILED(witherror) orCANCELLED. Report the outcome even for a command that finishes at once, or the Skill waits until it times out. - The platform cancels a running command by calling
cancelExecution(sn, externalExecutionId)with the id you returned. Its default callsstopTask, so an adapter that only implementsstopTaskkeeps working. - The task methods (
prepareTask,startTask,pauseTask,resumeTask) are still on the interface, but 2.0 never calls them — see Upgrading from 1.3.
Command ID naming convention
Every built-in command maps to a well-known, vendor-neutral command_id string. Custom commands should follow the same convention:
- Vendor-neutral — never encode a vendor name (
dji.takeoffis wrong); the same id should be implementable by any adapter. domain.actionfor a single atomic command — a domain (flight,navigation,dock,asset,camera,gimbal,stream) and a snake_case action (return_to_home,go_to,change_lens).domain.subtype.actiononly when a domain genuinely has distinct execution variants, e.g.mission.waypoint.execute.custom.prefix for tenant/user-defined commands, so they never collide with a future built-in of the same name.- Commands under
flight.,navigation.,dock.,mission., andasset.rebootare treated as high-risk by the platform's execution safety checks (e.g. may require human approval) — keep new movement-capable commands under one of those prefixes rather than introducing an unrecognized domain.
gRPC Layer
The EdgeAdapterGrpcServiceImpl class is not registered on its own: subclass it in your adapter and
annotate the subclass with @GrpcService, as in
Quickstart — Wire the SDK. It:
- Receives incoming gRPC requests from the platform (Remote Control, Mission Autonomy and Live Data services).
- Maps Proto request messages to SDK model POJOs using
ProtoJsonMapper. - Delegates to your
EdgeAdapterServiceimplementation. - Converts
CommandResultback to aCommandResponseProto message. - Handles errors with proper gRPC error codes and
GlobalErrorMessage.
Beyond that subclass, you typically do not need to interact with this class directly.
Best Practices
-
Override selectively. Only implement the commands your hardware actually supports. The default
NOT_IMPLEMENTEDresponse gives callers a clear signal that a given command is not available. -
Use CompletableFuture properly. Do not block inside your adapter methods. If your device SDK is blocking, wrap the call in
CompletableFuture.supplyAsync(...). -
Report capabilities. Override
getCapabilitiesto give the platform and its users an accurate view of what your device can do at any given moment. -
Use transaction IDs. Pass the
tidfrom the request through to theCommandResultso that command execution can be correlated across the system. -
Handle timeouts. If your device takes time to respond, use
CompletableFuture.orTimeout(...)or custom timeout logic so that callers are not left waiting indefinitely. -
Log meaningfully. The gRPC layer already logs incoming commands. Focus your adapter logs on device-level events, errors, and state changes.