Zequent Edge SDK (Go) — Adapters on 3.0 (preview)
Preview. This page describes the 3.0 adapter API of the Go Edge SDK. 3.0 is not released yet: the API below is available as a preview. Names may still change before the release. For the current 2.0 line, see Go Edge Adapter and the Go Quickstart.
On 3.0 an adapter registers each command once — dotted id, input and output JSON Schema,
handler. The advertised capabilities, the v3 ExecuteCommand the platform calls and the v2 typed
RPCs a 2.x platform still calls all come from that registration, so the same adapter works against
2.x and 3.0 platforms.
Java: Adapters on 3.0 (Java) · Python: Adapters on 3.0 (Python) · Platform: Upgrading the platform to 3.0.
Step 1: Add the preview module
From 3.0 the Go SDKs are one module, github.com/zequent/zqnt-sdk-go/v3 (/v3, lower-case
owner). The Edge SDK is its edge/ tree: package edgesdk at github.com/zequent/zqnt-sdk-go/v3/edge,
with edge/adapter, edge/conformance and the other packages below it; the Client SDK is the
client/ tree. Its generated types and the telemetry mapping come from the public module
github.com/zequent/zqnt-api/go/v3 (Apache 2.0), which replaces zqnt-utils-golang; the
packages below gen/ keep their paths.
# preview branch, not a release
go get github.com/zequent/zqnt-sdk-go/v3@feature/v3-agentic
Go resolves the branch to a pseudo-version; run go get again to pick up later preview commits and
move to v3.0.0 once it is out — do not run a preview in production. Requires Go 1.25+.
Moving an adapter from 2.x:
grep -rl zqnt-edge-sdk-go/v2 --include='*.go' . | xargs sed -i \
's#github.com/Zequent/zqnt-edge-sdk-go/v2#github.com/zequent/zqnt-sdk-go/v3/edge#g'
go get github.com/zequent/zqnt-sdk-go/v3@feature/v3-agentic && go mod tidy
Imports of generated types move to github.com/zequent/zqnt-api/go/v3/gen/.... Authentication
(ZQNT_EDGE_TOKEN / WithEdgeToken, ZQNT_PLATFORM_PUBLIC_KEY / WithPlatformPublicKey,
ZQNT_EDGE_AUTH_DISABLED / WithoutPlatformAuth()) is unchanged from 2.0.
Step 2: Declare the commands
Embed adapter.Base and register the commands:
package main
import (
"context"
"github.com/zequent/zqnt-sdk-go/v3/edge/adapter"
"github.com/zequent/zqnt-sdk-go/v3/edge/adapter/domains"
)
var altitudeInput = map[string]any{
"type": "object",
"properties": map[string]any{"altitude": map[string]any{"type": "number", "minimum": 2}},
}
type Drone struct {
adapter.Base
events adapter.CommandEventPublisher
}
func newDrone() *Drone {
d := &Drone{}
d.SetAssetType("ASSET_TYPE_DRONE")
d.MustRegisterCommand("flight.takeoff", altitudeInput, nil, d.takeOff,
adapter.WithDescription("Take off and climb to the given altitude above the takeoff point"),
adapter.WithCompletion(domains.CompletionAsynchronous, "flight.takeoff.completed"))
if gear, ok := domains.StandardTelemetryField("drone.gear"); ok {
d.DeclareTelemetryField(gear)
}
d.DeclareTelemetryField(domains.TelemetryField{
Key: "vendor.acme.tank_level", Type: domains.TelemetryValueNumber, Unit: "%"})
return d
}
func (d *Drone) takeOff(ctx context.Context, req *domains.CustomCommandRequest) (*domains.CommandResult, error) {
altitude, _ := req.Params["altitude"].(float64) // validated against the input schema
// start the climb on the device, report the outcome later (Step 4)
_ = altitude
return domains.Success("climbing", req.SN), nil
}
- Ids. Reuse the platform's ids for shared actions (
flight.takeoff,navigation.go_to,dock.open_cover, …); a device-specific command goes undervendor.<vendor>.<name>. - Handler.
adapter.CommandHandlerisfunc(ctx, *domains.CustomCommandRequest) (*domains.CommandResult, error). The request carriesSN,CommandID,TargetRef,ParamsandCommandExecutionID(the platform's id of this run on v3; empty over v2, whereTIDcarries it). - Validation. Params are validated against the input schema before the handler runs; whole
numbers under an
integerproperty arrive asint64, every other number asfloat64. A mismatch isREJECTEDwith error codecommand.invalid_paramsand a message naming each field. A schema that does not compile failsRegisterCommand(MustRegisterCommandpanics). - Result.
domains.Success(...)/SuccessWithOutput(...),domains.Accepted(message, externalExecutionID, sn),domains.Rejected(code, message, sn),domains.Error(...). A success is reportedACCEPTEDon v3 — the outcome follows as an event — when it carries an external execution id or the command declaresCompletionAsynchronous; otherwiseSUCCEEDED. - Options.
WithDescription,WithCompletion(mode, completionEvent),WithTarget(type, ref),Unavailable(reason),WithCapability(func(*domains.Capability))for any other field. - Changing what the asset can do.
RegisterCommandagain replaces an id,UnregisterCommand(id)withdraws it; every registry change is reported to the platform.
Adapters written against the typed methods (adapter.UnimplementedEdgeAdapter + TakeOff, …) keep
working — the v3 server runs a built-in id through the typed method that implements it — but those
methods are deprecated in favour of the registry.
Command risk (CommandSafety)
The 3.0 contract rates every command with a CommandSafety (COMMAND_RISK_OBSERVE, _ADJUST,
_MOVE, _CRITICAL, plus reversibility and an undo command). A standard id is advertised with
the command catalog's rating (domains.CatalogSafety(id): flight.takeoff MOVE, flight.land
CRITICAL, ...). Declare your own at registration:
d.MustRegisterCommand("vendor.acme.spray", sprayInput, nil, d.spray,
adapter.WithRisk(domains.CommandRiskCritical))
// or adapter.WithSafety(domains.CommandSafety{Risk: ..., UndoCommandID: ..., Preconditions: ...})
A vendor.* command that moves or affects the asset must declare its own risk. An id outside the catalog
that declares none counts as critical (a client must give a reason to run it).
Payloads
A device mounted on the asset (camera, gimbal, speaker, spotlight, sprayer, sensor) is reported with
the capabilities. Set the asset's payloads on the registry, and target a payload's commands at its
Ref:
slot := int32(0)
_ = d.SetPayloads("SN-1", []domains.Payload{
{Ref: "speaker-1", Kind: "speaker", Vendor: "Acme", Model: "S2", SlotIndex: &slot},
})
d.MustRegisterCommand("speaker.audio.play", playInput, nil, d.play,
adapter.WithTarget(domains.CapabilityTargetPayload, "speaker-1"))
SetPayloads reports again when the snapshot changed; MountedOn names the sub-asset carrying
the payload (empty: the asset itself). A report is a full snapshot: a payload missing from the next report is detached. The platform keeps
the payloads on the asset (the console's payload list) and stores one mounted on a sub-asset (a
drone of a dock) on that sub-asset. kind is free text (camera, speaker, spotlight,
sprayer, sensor, ...); metadata carries anything else, library_version is stored as such.
The conformance kit
(Report.Payloads) flags a payload without Ref, two with the same one, and a command targeting a
payload that is not reported.
Step 3: Serve and connect
client, err := edgesdk.NewEdgeClient(backendAddr, deviceSN, drone,
edgesdk.WithRemoteControlAddr(remoteControlAddr))
if err != nil {
return err
}
drone.events = client.LiveData()
lis, _ := net.Listen("tcp", ":9090")
return client.StartServing(ctx, lis)
StartServing serves the v2 and the v3 EdgeAdapterService from the same adapter and reports the
capabilities. remote-control serves EdgeGatewayService (capability reports, v3 command events):
point the SDK at it with WithRemoteControlAddr unless the main endpoint multiplexes it
(WithConnectorAddr, WithLiveDataAddr, WithMissionAutonomyAddr do the same for the other
services).
| What | 3.0 platform | 2.x platform (answers UNIMPLEMENTED) |
|---|---|---|
| Commands from the platform | v3 ExecuteCommand | v2 typed RPCs, served alongside |
Capabilities (on start, on every registry change, client.RefreshCapabilities()) | ReportCapabilities, with the declared telemetry fields | v2 ReportAssetRuntime |
Command events (LiveData().PublishCommandExecutionEvent) | PublishCommandEvent under the platform's command_execution_id | v2 notification stream |
LiveData().PublishTelemetrySample | TelemetryIngestService.PublishTelemetry | v2 ProduceTelemetry, catalog keys mapped — see Step 5 |
LiveData().PublishDetections | TelemetryIngestService.PublishDetections | v2 ProduceDetection |
LiveData().PublishAlert | TelemetryIngestService.PublishAlerts | dropped (no v2 counterpart) |
Automatic fallback. After UNIMPLEMENTED the SDK stays on v2 for 10 minutes, then tries v3
again — an adapter picks up a platform upgrade without a restart. The first item sent to a v2-only
platform can be lost before the SDK switches.
An adapter that serves more assets than its own SN (a fleet behind one endpoint) adds them with
client.TrackCapabilities(sn) (UntrackCapabilities(sn) to stop).
Step 4: Report command events
err := d.events.PublishCommandExecutionEvent(ctx, &domains.CommandExecutionEvent{
SN: req.SN,
CommandExecutionID: req.CommandExecutionID,
ExternalExecutionID: req.TID,
CommandID: req.CommandID,
Status: domains.CommandExecutionSucceeded,
Output: map[string]any{"altitude": 40.0},
})
On v3 the event goes to PublishCommandEvent under CommandExecutionID (it takes precedence over
ExternalExecutionID); on v2 over the notification stream. OccurredAt defaults to the publish
time. Progress (0..1) belongs on a CommandExecutionRunning event; finish every accepted command
with exactly one terminal status (CommandExecutionSucceeded, …Failed with ErrorCode,
…Cancelled). Publishing through the adapter.CommandEventPublisher interface lets the conformance
kit's Recorder stand in for the client in tests.
Step 5: Publish telemetry
battery := 81.0
err := client.LiveData().PublishTelemetrySample(ctx, &domains.TelemetrySample{
SN: deviceSN,
Position: &domains.GeoPoint{Lat: 47.5, Lon: 9.7},
BatteryPercent: &battery,
Details: map[string]any{"drone.gear": 1, "vendor.acme.tank_level": 63.0},
})
TelemetrySample has the fields every asset shares — Position (Alt above sea level),
RelativeAltitude (above the takeoff point), HeadingDegrees, HorizontalSpeed, VerticalSpeed,
BatteryPercent — and Details for everything device-specific. ObservedAt defaults to now.
PublishDetections(ctx, *domains.DetectionBatch) and PublishAlert(ctx, *domains.Alert) (severity
domains.AlertSeverityInfo / Warning / Critical) publish detections and alerts. The v3 streams
are long-lived and reconnect with backoff; an item sent while a stream waits for its next attempt
gets livedata.ErrReconnecting.
Standard keys. The platform's telemetry catalog
(github.com/zequent/zqnt-api/go/v3/telemetry) has 64 detail keys — listed in
The telemetry catalog — as constants
(telemetry.DockCoverState, telemetry.DroneGear, …) with telemetry.Fields() and
telemetry.Field(key). domains.StandardTelemetryField(key) turns one into a domains.TelemetryField
with the platform's type, unit and allowed values (and reports false for a key outside the catalog).
On a 2.x platform a sample goes over v2 ProduceTelemetry: catalog keys in Details land in
their v2 fields, other keys are dropped. The v2 telemetry API (ProduceTelemetryData,
ProduceTelemetry) is unchanged.
Pairing
client.EnsureAsset(ctx, asset) binds to an asset the platform already knows and redeems a
one-time claim code (edgesdk.WithClaimCode, default ZQNT_CLAIM_CODE, minted in the console)
only for an unknown serial. The claim decides the organization. Connector().RedeemAssetClaim(ctx, code, asset) and Connector().DescribeAssetClaim(ctx, code) (the organization's name, without
spending the code) are there for custom flows. A refused code returns (nil, nil) without saying
why. RegisterAsset is deprecated, and the platform's organization-UUID bind is planned to be
removed in 3.0.
Step 6: Check the adapter with the conformance kit
import "github.com/zequent/zqnt-sdk-go/v3/edge/conformance"
func TestConformance(t *testing.T) {
recorder := &conformance.Recorder{}
drone := newDrone()
drone.events = recorder
conformance.Run(t, drone, conformance.Options{Events: recorder})
}
Run reports one subtest per rule: schemas parse, advertised ids are executable, executable ids
are advertised, and an ACCEPTED command is completed by an event with occurred_at (within
CompletionTimeout, default 5 s). It executes commands — with Options.Params per id, or params
generated from the input schema — so run it against fakes or a simulator. conformance.Check
returns the Report instead of failing a test.