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.

Table of Contents


How It Works

When your Quarkus application starts, the SDK registers a gRPC service (EdgeAdapterGrpcServiceImpl) that receives commands from the platform and delegates them to your EdgeAdapterService bean. 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.zequent.framework.edge.sdk.interfaces.EdgeAdapterService;
import com.zequent.framework.edge.sdk.models.*;
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.


Command Reference

Flight Control

MethodParametersDescription
takeOff(TakeOffRequest)sn, tid, coordinatesInitiate takeoff at the given coordinates
returnToHome(ReturnToHomeRequest)sn, tid, altitudeReturn the sub-asset to its home position
goTo(GoToRequest)sn, tid, coordinatesNavigate to the specified coordinates

Example:

@Override
public CompletableFuture<CommandResult> goTo(GoToRequest request) {
    deviceApi.flyTo(
        request.getCoordinates().getLatitude(),
        request.getCoordinates().getLongitude(),
        request.getCoordinates().getAltitude()
    );
    return CompletableFuture.completedFuture(
        CommandResult.success("Navigation started", request.getTid(), request.getSn())
    );
}

Dock Operations

MethodParametersDescription
openCover(String sn)snOpen the dock cover
closeCover(String sn, Boolean force)sn, forceClose the dock cover, optionally forcing it
startCharging(String sn)snStart charging the sub-asset
stopCharging(String sn)snStop charging the sub-asset
rebootAsset(String sn)snReboot the asset (dock)
bootUpSubAsset(String sn)snPower on the sub-asset (drone)
bootDownSubAsset(String sn)snPower off the sub-asset (drone)

Example:

@Override
public CompletableFuture<CommandResult> openCover(String sn) {
    dockApi.sendCommand("open_cover");
    return CompletableFuture.completedFuture(
        CommandResult.success("Cover opening", sn)
    );
}

Camera and Gimbal

MethodParametersDescription
lookAt(LookAtRequest)sn, lat, lon, alt, locked, payloadIndexPoint the camera at coordinates
changeLens(ChangeLensRequest)sn, lens, videoIdSwitch the active camera lens
changeZoom(ChangeZoomRequest)sn, lens, payloadIndex, zoomAdjust the camera zoom level
enableGimbalTracking(String sn, boolean enabled)sn, enabledEnable or disable gimbal tracking mode

Example:

@Override
public CompletableFuture<CommandResult> lookAt(LookAtRequest request) {
    cameraApi.pointAt(
        request.getLatitude(),
        request.getLongitude(),
        request.getAltitude(),
        request.getLocked()
    );
    return CompletableFuture.completedFuture(
        CommandResult.success("Camera pointing", request.getSn())
    );
}

Manual Control

MethodParametersDescription
enterManualControl(String sn)snEnter manual (joystick) control mode
exitManualControl(String sn)snExit manual control mode
manualControlInput(Multi<ManualControlInput>)input streamReceive a continuous stream of joystick inputs

The manualControlInput method receives a reactive Multi stream of ManualControlInput objects, each containing roll, pitch, yaw, throttle, and gimbalPitch values. This is used for real-time joystick or virtual stick control.

Example:

@Override
public CompletableFuture<CommandResult> enterManualControl(String sn) {
    deviceApi.enableDrcMode(sn);
    return CompletableFuture.completedFuture(
        CommandResult.success("Manual control mode entered", sn)
    );
}

@Override
public CompletableFuture<CommandResult> manualControlInput(Multi<ManualControlInput> inputStream) {
    inputStream.subscribe().with(
        input -> deviceApi.sendStickCommand(
            input.getRoll(), input.getPitch(),
            input.getYaw(), input.getThrottle()
        ),
        error -> log.error("Manual control stream error", error),
        () -> log.info("Manual control stream completed")
    );
    return CompletableFuture.completedFuture(
        CommandResult.success("Manual control stream started", "")
    );
}

Live Streaming

MethodParametersDescription
startLiveStream(LiveStreamStartRequest)sn, tid, videoId, streamServer, videoTypeStart a video live stream
stopLiveStream(LiveStreamStopRequest)sn, tid, videoIdStop a video live stream

Example:

@Override
public CompletableFuture<CommandResult> startLiveStream(LiveStreamStartRequest request) {
    deviceApi.startStream(request.getVideoId(), request.getStreamServer());
    return CompletableFuture.completedFuture(
        CommandResult.success("Live stream started", request.getSn())
    );
}

Debug and Maintenance

MethodParametersDescription
enterRemoteDebugMode(String sn)snEnter remote debug mode on the device
closeRemoteDebugMode(String sn)snExit remote debug mode
changeAcMode(String sn, String mode)sn, modeChange the air conditioner mode of the asset

Task Execution

MethodParametersDescription
prepareTask(String taskId, String tid)taskId, tidPrepare a task for execution (e.g., generate wayline files, upload resources)
startTask(String taskId, String tid)taskId, tidStart executing a previously prepared task
stopTask(String taskId)taskIdStop a running task

Example:

@Override
public CompletableFuture<CommandResult> prepareTask(String taskId, String tid) {
    // Fetch waypoints, generate flight plan, upload to device
    missionService.generateAndUpload(taskId);
    return CompletableFuture.completedFuture(
        CommandResult.success("Task prepared", tid, taskId)
    );
}

@Override
public CompletableFuture<CommandResult> startTask(String taskId, String tid) {
    deviceApi.executeFlightPlan(taskId);
    return CompletableFuture.completedFuture(
        CommandResult.success("Task started", tid, taskId)
    );
}

Capability Reporting

MethodParametersDescription
getCapabilities(String sn)snReturn the set of capabilities this adapter supports

Override this method to tell the platform exactly which commands your adapter supports and any metadata about them:

@Override
public CompletableFuture<CurrentCapabilities> getCapabilities(String sn) {
    Set<Capability> capabilities = Set.of(
        new Capability("takeOff", "Initiate drone takeoff", true, null, Map.of()),
        new Capability("openCover", "Open dock cover", true, null, Map.of()),
        new Capability("goTo", "Navigate to coordinates", true, null, Map.of()),
        new Capability("manualControlInput", "Joystick control", false,
            "DRC mode not available", Map.of())
    );

    return CompletableFuture.completedFuture(
        CurrentCapabilities.of(sn, AssetTypeEnum.ASSET_TYPE_DOCK, capabilities)
    );
}

CommandResult

Every adapter command returns a CommandResult. Use the static factory methods to create results:

// Success without transaction ID
CommandResult.success("Message", sn);

// Success with transaction ID
CommandResult.success("Message", tid, sn);

// Error without transaction ID
CommandResult.error("Error description", sn);

// Error with transaction ID
CommandResult.error("Error description", tid, sn);

// Not Implemented (used by default methods)
CommandResult.notImplemented("Command not supported", sn);

The CommandResult.ResultType enum has three values:

  • SUCCESS -- command executed successfully
  • ERROR -- command failed
  • NOT_IMPLEMENTED -- command is not supported by this adapter

Default Implementation Convenience Methods

The provided EdgeAdapterServiceImpl class extends the interface with convenience methods that automatically use the configured serial number from EdgeClientConfig.sn(). These include:

  • openCover() / closeCover() (no sn parameter)
  • startCharging() / stopCharging()
  • rebootAsset()
  • bootUpSubAsset() / bootDownSubAsset()
  • enterManualControl() / exitManualControl()
  • getCapabilities()
  • enableGimbalTracking(boolean)
  • changeAcMode(String mode)

If your adapter extends EdgeAdapterServiceImpl instead of implementing EdgeAdapterService directly, these methods are available automatically.


gRPC Layer

The EdgeAdapterGrpcServiceImpl class is registered as a @GrpcService. It:

  1. Receives incoming gRPC requests from the platform (Remote Control Service, Client SDK).
  2. Maps Proto request messages to SDK model POJOs using ProtoJsonMapper.
  3. Delegates to your EdgeAdapterService implementation.
  4. Converts CommandResult back to an EdgeResponse Proto message.
  5. Handles errors with proper gRPC error codes and GlobalErrorMessage.

You typically do not need to interact with this class directly. It is wired automatically by the CDI container and the Quarkus gRPC extension.


Error Handling

Exceptions thrown by your adapter code are caught by the gRPC layer and mapped to error responses:

Exception TypegRPC Error Code
IllegalArgumentExceptionCLIENT_ERROR
UnsupportedOperationExceptionCLIENT_ERROR
TimeoutExceptionSYSTEM_ERROR
All other exceptionsSYSTEM_ERROR

You can also return explicit error results using CommandResult.error(...) instead of throwing exceptions for expected failure conditions.


Best Practices

  1. Override selectively. Only implement the commands your hardware actually supports. The default NOT_IMPLEMENTED response gives callers a clear signal that a given command is not available.

  2. Use CompletableFuture properly. Do not block inside your adapter methods. If your device SDK is blocking, wrap the call in CompletableFuture.supplyAsync(...).

  3. Report capabilities. Override getCapabilities to give the platform and its users an accurate view of what your device can do at any given moment.

  4. Use transaction IDs. Pass the tid from the request through to the CommandResult so that command execution can be correlated across the system.

  5. Handle timeouts. If your device takes time to respond, use CompletableFuture.orTimeout(...) or custom timeout logic so that callers are not left waiting indefinitely.

  6. Log meaningfully. The gRPC layer already logs incoming commands. Focus your adapter logs on device-level events, errors, and state changes.

Was this page helpful?