Edge SDK (Python) — Edge Adapter
Implementing an edge adapter in Python boils down to subclassing EdgeAdapter and overriding the methods your hardware supports. Methods you don't override automatically return EdgeResponse.not_supported(...) and are reported as unavailable in get_capabilities.
Full method-by-method reference, every real method and signature: Edge Adapter API Reference.
For Java, see edge-sdk-adapter.md.
The contract
from edge_sdk import EdgeAdapter
class MyAdapter(EdgeAdapter):
async def get_capabilities(self, sn: str, asset_id: str | None) -> Capabilities:
...
get_capabilities is the only abstract method. Use the helper:
return self._auto_capabilities(sn, AssetType.DOCK)
_auto_capabilities introspects which methods you've overridden and produces a Capabilities payload that mirrors that exactly. You only maintain the implementation list — the capability list stays in sync automatically.
RequestContext
Every command receives a RequestContext as the first argument:
@dataclass
class RequestContext:
tid: str # transaction id (correlate response/progress to request)
sn: str # asset serial number
timestamp: datetime
Always pass ctx.tid and ctx.sn back into your EdgeResponse to keep the platform's correlation working.
EdgeResponse
The unified response type used by every unary command. Factories are named ok/fail, not success/error:
EdgeResponse.ok(tid, sn, message="Takeoff initiated")
EdgeResponse.fail(tid, sn, ErrorMessage(message="Hardware fault", code=ErrorCode.ASSET_ERROR))
EdgeResponse.not_supported(tid, sn) # default for un-overridden methods
EdgeResponse.ok(tid, sn, progress=CommandProgress(progress=42.0, state="climbing", left_time_seconds=30.0))
ok(...) also accepts external_execution_id — set it when the command you just accepted keeps running asynchronously, so its command execution events can be correlated back to it (see Reporting progress).
For long-running commands you can stream multiple EdgeResponse objects via the streaming variants (see below).
Method groups
The base class organises methods into groups — you only override what your hardware supports. See the reference for every method's exact signature; the categories are:
- Capability management (required) —
get_capabilities,_auto_capabilities - Flight control —
take_off,go_to,return_to_home - Manual control —
enter_manual_control,exit_manual_control,manual_control_input(client-streaming) - Camera and gimbal —
look_at,take_photo,capture_photo,change_lens,change_zoom,enable_gimbal_tracking - Live streaming and recording —
start_live_stream,stop_live_stream,start_recording,stop_recording - Detection —
get_detections(the one server-streaming, async-generator method) - Dock and asset operations —
open_cover,close_cover,start_charging,stop_charging,reboot_asset,boot_up_sub_asset,boot_down_sub_asset,register_asset,deregister_asset - Debug and maintenance —
enter_or_close_remote_debug_mode,change_ac_mode - Tasks —
prepare_task,start_task,stop_task— see Tasks - Custom commands —
register_command, andsend_custom_command, which dispatches to what you registered
class MyDroneAdapter(EdgeAdapter):
async def get_capabilities(self, sn, asset_id):
return self._auto_capabilities(sn, AssetType.AIRCRAFT)
async def take_off(self, ctx, coordinates):
await hardware.take_off(coordinates.latitude, coordinates.longitude)
return EdgeResponse.ok(ctx.tid, ctx.sn)
Custom commands
Declare each command your adapter runs with register_command. One registration does both halves:
the command appears in _auto_capabilities, and the default send_custom_command dispatches to its
handler. A well-known command (mission.waypoint.execute, ...) takes its description and input
schema from the platform's catalog, so registering it is one line; a hardware-specific command goes
under a vendor. prefix and brings its own input_schema (built with schema(...)). A handler takes
the RequestContext and the command's params, and returns a CustomCommandResponse — see the
example below.
Tasks
prepare_task/start_task/stop_task still exist, and by default delegate to the registered
commands mission.prepare/mission.start/mission.stop. The 2.0 platform never calls prepare_task
or start_task. It calls stop_task to cancel a running command, passing that command's
external_execution_id as task_id — so implement stop_task if your long-running commands can be
aborted. See Upgrading from 1.3.
Reporting progress for long-running commands
How a custom command's handler answers decides when the platform considers it finished — which is
when a Skill execution moves on to its next step. (take_off, go_to, look_at and return_to_home
always count as still running: report their outcome with events as below.)
- Finished at once: return
CustomCommandResponse.ok(ctx.tid, ctx.sn, command_id, result={...}). Theresultis the step's output. - Still running: return
ok(...)with anexternal_execution_idand noresult, and report the outcome as command execution events under that id:RUNNINGwithprogress(0.0–1.0) while it runs, then exactly one ofSUCCEEDED(optionally withoutput),FAILEDorCANCELLED. Without a terminal event the Skill waits until it times out.
import asyncio
import uuid
from edge_sdk import (
AssetType, CommandExecutionEvent, CommandExecutionStatus, EdgeAdapter, LiveDataService, RequestContext,
)
from edge_sdk.models.common import CustomCommandResponse
class MyDroneAdapter(EdgeAdapter):
def __init__(self, live: LiveDataService):
super().__init__()
self._live = live
# One registration: advertised in get_capabilities, and dispatched by send_custom_command
self.register_command("mission.waypoint.execute", self._fly_route)
async def get_capabilities(self, sn, asset_id):
return self._auto_capabilities(sn, AssetType.AIRCRAFT)
async def _fly_route(self, ctx: RequestContext, params: dict) -> CustomCommandResponse:
execution_id = str(uuid.uuid4())
asyncio.create_task(self._fly(ctx.sn, execution_id, params["waypoints"]))
# Accepted, not finished: the outcome follows as command execution events
return CustomCommandResponse.ok(ctx.tid, ctx.sn, "mission.waypoint.execute",
external_execution_id=execution_id)
async def _fly(self, sn: str, execution_id: str, waypoints: list[dict]):
for i, waypoint in enumerate(waypoints):
await hardware.fly_to(waypoint)
await self._report(sn, execution_id, CommandExecutionStatus.RUNNING, (i + 1) / len(waypoints))
await self._report(sn, execution_id, CommandExecutionStatus.SUCCEEDED)
async def _report(self, sn: str, execution_id: str, status: CommandExecutionStatus, progress=None):
await self._live.produce_notification(CommandExecutionEvent(
external_execution_id=execution_id,
status=status,
sn=sn,
command_id="mission.waypoint.execute",
progress=progress,
))
produce_notification is on LiveDataService (see Live Data);
2.0 no longer processes task events. The one real async-generator method on EdgeAdapter is
get_detections, used for pull-based detection streaming — it is not a progress-reporting mechanism.
Best practices
- Keep methods async-friendly. Wrap blocking SDK calls with
asyncio.to_thread(...)or use the vendor SDK's async API. - Always honour
ctx.tidandctx.snwhen constructing responses; the platform correlates by these. - Don't catch broad exceptions silently. Convert known hardware errors to
EdgeResponse.fail(tid, sn, ErrorMessage(...))with a meaningfulErrorCode; let the gRPC server surface the rest. - Don't keep state on the adapter for per-request lifetimes. Use the
tidas a key into a per-command map if you must. - Use
_auto_capabilitiesrather than maintaining the capability list manually.
Testing your adapter
Adapter methods are plain coroutines, so a unit test calls them directly:
from datetime import datetime
import pytest
from edge_sdk import Coordinates, RequestContext
from my_edge_adapter.adapter import MyDeviceAdapter
@pytest.mark.asyncio
async def test_takeoff():
adapter = MyDeviceAdapter()
ctx = RequestContext(tid="t-1", sn="DOCK-1", timestamp=datetime.now())
response = await adapter.take_off(ctx, Coordinates(latitude=47.37, longitude=8.54, altitude=100.0))
assert response.success
assert response.tid == "t-1"
For an end-to-end check against a running adapter, see the grpcurl call in the
Quickstart. The SDK's own tests/
directory has more examples.