Direct Device Integration¶
This guide explains how to connect a directly connected device (firmware that speaks to NereidLink itself, with no gateway in between). It is written for device/edge developers, not console operators.
- Transport: MQTT 3.1.1 / 5.0 (primary) or HTTPS long-polling (fallback).
- Payloads: JSON, UTF-8.
- Official SDK: Go —
github.com/nereidai/nereidlink/sdk/device/go. It is a thin framing/transport library; your firmware keeps control of cadence and reconnection. Other languages can implement the wire contract in this document directly.
1. Prerequisites on the platform¶
Before firmware can connect, an operator must prepare the device in the console:
- Create the device and assign a device profile (its measurement descriptors declare point names, value types and units).
- Create at least one credential for the device. Three kinds exist:
TOKEN— one opaque access token, sent as the MQTT username with an empty password (or HTTPAuthorizationheader).- ID + secret — sent as MQTT username/password or HTTP Basic auth.
- X.509 client certificate — mutual TLS.
- A device without a valid credential cannot establish a session.
Telemetry keys should match the measurement keys declared in the device profile. The platform never guesses units or types: only descriptor-defined points get conversions/units; unknown scalar keys are still stored but without transformation.
2. MQTT quick start (Go SDK)¶
go get github.com/nereidai/nereidlink/sdk/device/go@latest
package main
import (
"context"
"github.com/nereidai/nereidlink/sdk/device/go"
"github.com/nereidai/nereidlink/sdk/device/go/devicemqtt"
)
func main() {
ctx := context.Background()
client, err := devicemqtt.Connect(ctx, devicemqtt.Options{
BrokerURL: "tcp://nereidlink.example:1883", // or tls://...:8883
Credential: device.Token("device-access-token"),
ClientID: "sensor-001",
})
if err != nil {
panic(err)
}
defer client.Disconnect(200)
// Receive downlink commands and answer them.
_ = client.SubscribeCommands(ctx, 1, func(cmd device.Command) {
// Downlink topic: v1/me/cmd/<name>; reply path uses the ID.
_ = client.RespondCommand(ctx, cmd.ID,
map[string]any{"ok": true}, 1)
})
// Publish flat scalar measurements.
frame := device.NewFrame().
SetFloat("temperature", 21.5).
SetBool("online", true).
SetString("mode", "AUTO")
_ = client.PublishTelemetry(ctx, frame, 1)
}
device.Basic(id, secret) and device.ClientCert(...) provide the other
credential kinds. A complete runnable program is in the SDK under
examples/simple.
3. MQTT topic contract¶
A directly connected device only ever uses the v1/me/* topics.
Device → platform¶
| Topic | Direction | Payload |
|---|---|---|
v1/me/telemetry |
publish | Flat JSON object of scalar measurements |
v1/me/events |
publish | One domain-event object or an array |
v1/me/attributes |
publish | CLIENT-scope attributes object |
v1/me/attributes/get |
publish | On-demand pull of platform attributes: {"scopes":[...],"response_topic"?}; omitted scopes means SERVER_SCOPE+SHARED_SCOPE |
v1/me/shadow/report |
publish | Reported-state patch: {"state":{...},"version"?:N} |
v1/me/cmdres/{command_id} |
publish | Result of a request-response command |
Platform → device (subscribe)¶
| Filter/topic | Payload |
|---|---|
v1/me/cmd/# |
Command frames; the concrete topic is v1/me/cmd/<command_name> |
v1/me/attributes/down |
SERVER/SHARED attribute pushes |
v1/me/attributes/get/ack |
On-demand pull reply: {"scopes":{"SERVER_SCOPE":{...},"SHARED_SCOPE":{...}}} |
v1/me/shadow/desired |
Complete desired state: {"version":N,"desired":{...}} |
Downlink frames are QoS 0 and non-retained. The firmware chooses uplink QoS; QoS 1 is recommended.
Telemetry frame¶
{
"ts": 1728374400000,
"temperature": 21.5,
"online": true,
"mode": "AUTO"
}
- Every value MUST be a JSON number, boolean or string.
null, nested objects and arrays are rejected before upload. tsis optional (epoch milliseconds). Omit it to let the platform stamp arrival time.- One frame produces one stored record per measurement key.
Attributes and scopes¶
Attributes are relatively static key/value configuration or metadata, not
telemetry: the device only reports the CLIENT scope via
v1/me/attributes; the platform manages SERVER_SCOPE and
SHARED_SCOPE.
In the current version both SERVER_SCOPE and SHARED_SCOPE are delivered to
the device: while online you receive incremental pushes on
v1/me/attributes/down as {"scope":"...","data":{"key":value}}; after every
(re)connect you should publish v1/me/attributes/get once to recover changes
missed while offline, and replace the local cache wholesale with the full
key/value set returned per scope in get/ack. Note this differs from classic
ThingsBoard semantics, where SERVER_SCOPE is private to the server and only
SHARED_SCOPE is visible to the device; do not assume SERVER_SCOPE is private
here. A JSON null value in a downlink data map means the platform deleted
that key, so the device must drop the local cache entry (do not store null).
Omitting scopes in the request pulls both platform scopes; requesting CLIENT
or an unknown scope is rejected.
Device shadow (desired / reported)¶
The shadow holds, per device UUID, a platform-requested desired state and the device-observed reported state, so you can tell "the state I asked for" from "the state it actually reached". Use it only for state that must be maintained and confirmed; use commands for one-shot actions and SHARED attributes for fire-and-forget configuration.
- Desired is always full-state. On
v1/me/shadow/desiredyou receive the complete map as{"version":N,"desired":{...}}; replace your local desired map wholesale, never patch it. The frame carries no tenant/device fields — the connection already identifies the device. Desired is re-pushed in full whenever it changes and once on each ONLINE (re)connect withdesired_version > 0. - Reported is a patch. Publish
v1/me/shadow/reportwith{"state":{...},"version"?:N};stateis merged per top-level key. Addversionto acknowledge the desired version you just applied. A JSONnullvalue deletes that top-level key; values may be scalars, objects or arrays. - The platform computes
delta(desired keys whose reported value differs) and flags the shadowin_sync; read both over REST/WebSocket.
Media: images, documents and other binaries¶
Telemetry can carry binary media — camera images, inspection photos,
scanned documents — not just scalars. The bytes never enter the time-series
row or the message bus: NereidLink stores them in its S3-compatible blob
backend (MinIO, SeaweedFS, AWS S3, …) and the point keeps only
blob://media/<tenant>/<device>/<uuid>. Two uplink forms exist:
- Frame marker — a
{"blob":true,"content_type":…,"filename":…, "data":"<base64>"}object in place of a scalar, over MQTT (v1/me/telemetry) or HTTP (POST /v1/me/telemetry).datais base64 only; the marker can share a frame with ordinary measurements. - Raw HTTP upload —
POST /v1/me/blob?key=<name>streams the raw file (no base64) straight into object storage, then publishes the reference point.
Per-object size (default 10 MiB) and the content-type allowlist (default
image/*, extend with e.g. application/pdf for documents) are enforced
once on the server. Owners download via GET /v1/me/blob; console/API
users resolve a reference via
GET /v1/tenants/{tenant}/blob/sign?key=… to a short-lived signed URL.
Full media contract
The marker field specification, base64 variants, S3/local backends, configuration, error codes and console rendering are documented on a dedicated page: Binary Media and S3 Storage.
Command frame (platform → device)¶
{
"id": "01J8...",
"name": "set-led",
"direction": "write",
"params": { "color": "green" }
}
- The topic suffix is the command
name(e.g.v1/me/cmd/set-led); the reply topic suffix is the commandid(v1/me/cmdres/01J8...). Never swap them. directionisreadorwrite, the south operation to perform, and is orthogonal to whether a reply is expected. Execute the GET/write exactly as directed; do not infer it from the name. A read may still carryparams(query arguments), andone-wayframes never carrydirection=read.paramsis a JSON document validated against the platform catalog; it is delivered verbatim.- For a request-response command, publish an arbitrary JSON result to
v1/me/cmdres/{id}. For a one-way command, do not reply. - If the command cannot be executed, still reply (request-response mode)
with a JSON body describing the failure; the platform marks the command
SUCCEEDEDonce any correlated response arrives, so include an explicit error field for business failures, e.g.{"error": "busy"}.
Domain events¶
{
"event_type": "door_forced",
"severity": "warning",
"ts_ms": 1728374400000,
"payload": { "zone": 3 }
}
severity is one of info, warning, error, critical (default
info). The platform carries no business logic: event_type and payload
are tenant-defined opaque data.
4. HTTP transport¶
For networks that only allow HTTPS, the device channel provides:
POST /v1/me/telemetry— upload one flat telemetry frame (base64 media markers allowed; see the Media section).POST /v1/me/blob?key=<measurement>— upload raw media bytes, andGET /v1/me/blob?key=<key>— download the device's own object (Range supported).GET /v1/me/commands— one long-poll wait; returns either the next queued command or an empty envelope when the wait window expires.POST /v1/me/cmdres/{command_id}— submit a command result.POST /v1/me/shadow/report— report shadow state.GET /v1/me/ota/tasks,GET /v1/me/ota/{task_id}/package(Range resume supported),POST /v1/me/ota/{task_id}/progress— firmware update flow.
httpClient, _ := devicehttp.New(devicehttp.Options{
BaseURL: "https://nereidlink.example",
Credential: device.Token("device-access-token"),
})
_, _ = httpClient.PublishTelemetry(ctx,
device.NewFrame().SetFloat("temperature", 21.5))
for {
cmd, _ := httpClient.PollCommands(ctx) // one long poll; loop to re-arm
if cmd != nil {
_ = httpClient.RespondCommand(ctx, cmd.ID,
map[string]any{"ok": true})
}
}
Authentication uses the same credentials (the token goes in
Authorization).
5. Command timeliness¶
Commands are declared in the platform's command catalog per device; undeclared command names cannot be dispatched (the platform rejects them with 404). Each declaration selects:
- Interaction:
one-way(fire-and-forget, endsDELIVERED) orrequest-response(waits for one correlated result, thenSUCCEEDEDorTIMEOUT). - Timeliness:
realtime(an offline device immediately getsNOT_CONNECTED) orpersistent(queued and delivered after reconnect; undelivered commands past TTL endEXPIRED).
Firmware does not configure these; it only observes whether a reply is expected.
6. Integration checklist¶
- Device created in console with a profile; credential issued and embedded in firmware.
- Firmware connects with the correct credential and unique client ID.
- Telemetry frames are flat scalar JSON, keys match profile descriptors,
tsincluded when sampling time matters. - Binary media uses the blob path: base64 frame marker or
POST /v1/me/blob; never send raw text in markerdata, and verify the content type is allowed (blob.media_allowed_types). - Firmware subscribes to
v1/me/cmd/#, distinguishesname(topic) fromid(reply), and always answers request-response commands — including failure cases. - Reconnection is handled by firmware (the SDK offers auto-reconnect for MQTT); after reconnect, resume publishing with fresh timestamps.