DDI-1 Contract — Device Direct Interface v1¶
Status: Frozen — DDI v1.0
DDI-1 is the wire contract between a device’s firmware and the FlexGalaxy device platform. A conforming device speaks two protocols to a single endpoint host and implements the surfaces below. The contract is frozen: wire-breaking changes go to a new major version (DDI v2), and additive changes within v1 (new optional fields, new error codes, new event types) never break a conforming device — see §10 Versioning. Firmware MUST ignore unknown fields.
Note
This is the device-integrator view of the frozen DDI v1.0 wire contract — the surface a firmware developer builds against. It is a stable external presentation; the versioning policy in §10 governs how it changes. Report an ambiguity or a suspected discrepancy through your integration contact.
Tip
Building firmware? Start with the device SDKs — the polyglot device SDKs (Python, Android/Java, C, C++) implement this contract for you. This page is the underlying wire reference the SDKs conform to.
1. Endpoints + transport¶
A device speaks two protocols to one endpoint host. Per-deployment values (host, CA chain) are configured at bootstrap-burn time — firmware MUST NOT hardcode hostnames.
Protocol |
Port |
TLS |
Used for |
|---|---|---|---|
HTTPS (HTTP/2 + HTTP/3) |
443 |
mTLS with device cert |
Provisioning, credential rotation, command ack, OTA polling, diagnostic-bundle upload, time-sync |
MQTT 5.0 |
8883 |
mTLS with device cert |
Telemetry, attribute report + ack, command receive, durable events, heartbeat, log streaming |
v1 firmware MUST implement HTTPS + MQTT. Telemetry, attribute reports, events, command-receive, and shared-attribute push exist only on the MQTT surface (§4); there is no HTTPS carrier for them in v1. A constrained device MAY implement HTTPS only, forgoing the §4 capabilities (see the capability matrix).
Endpoint hostname convention: things.<deployment-domain> (e.g.
things.flexgalaxy.ai for the managed environment). Configured at burn time.
2. Identity + authentication¶
2.1 Before first identify¶
A factory-burned device starts with:
Attestation token — issued by the factory at burn time; an opaque bearer (~256 bits) presented as
Authorization: Bearer <attestation>on the firstPOST /ddi/v1/identify. Per-device or per-batch, at the factory’s discretion.TLS trust anchors — the platform root CA + deployment intermediate CA, baked into firmware at burn time; used to verify the endpoint’s TLS chain. Rotated via firmware update.
No device certificate — issued during provisioning (§3.2).
2.2 After provisioning¶
Device X.509 certificate + private key (private key generated on-device; CSR sent, signed cert returned). Subject CN =
<serial>, O =<registrar-name>, OU =<model>, SAN URI =urn:fgai:thing:<thing_id>. Valid 1 year.CA chain — presented alongside the device cert in mTLS handshakes.
mTLS with the device cert is the sole auth mechanism for all post-provisioning traffic (HTTPS + MQTT). The attestation token is invalidated once provisioning succeeds.
2.3 Certificate rotation¶
Devices SHOULD rotate ≥ 30 days before expiry via POST /ddi/v1/rotate
(§3.3). A device MUST NOT rotate within 1 hour of its last successful rotation
(loop guard).
3. HTTPS surface — /ddi/v1/*¶
Request bodies are application/json; success responses are application/json,
errors are application/problem+json (RFC 9457). Every endpoint accepts an
Idempotency-Key header (RFC 9529) — 24-hour dedup window.
3.1 POST /ddi/v1/identify — first-boot identification¶
Auth: attestation token. Idempotent: yes (24h, keyed by model + sn + key).
Request:
{
"model": "FlexSwift MAX",
"sn": "SR202606180087",
"registrar_hint": "syrius-robotics",
"attestation_method": "factory-burn",
"firmware_version": "1.4.2",
"hardware_revision": "B"
}
Success (200):
{
"thing_id": "uuid",
"enrollment_token": "opaque-32-byte-base64",
"provision_endpoint": "/ddi/v1/provision",
"server_time": "2026-06-22T14:32:11.500Z",
"metadata": { "registrar_id": "uuid", "factory_service_id": "uuid" }
}
Errors: DDI-1-403-NOT-RECOGNIZED (serial not a registered factory pattern),
DDI-1-403-IDENTITY-BLACKLISTED (terminal — stop retrying, halt provisioning),
DDI-1-422-UNKNOWN-MODEL.
3.2 POST /ddi/v1/provision — provisioning¶
Auth: attestation token + enrollment_token from §3.1. Idempotent: yes.
Production pattern (required for production firmware): the device generates a keypair on-device (TPM / secure enclave), sends a CSR, and receives a signed cert
CA chain. The private key never leaves the device.
Request (CSR pattern):
{
"thing_id": "uuid",
"enrollment_token": "from /identify response",
"csr_pem": "-----BEGIN CERTIFICATE REQUEST-----\n...",
"device_attributes": {
"mac_addresses": ["02:1a:2b:3c:4d:5e"],
"tpm_attestation": "optional-base64",
"uptime_seconds": 17
}
}
Success (200):
{
"cert_pem": "-----BEGIN CERTIFICATE-----\n...",
"ca_chain_pem": "-----BEGIN CERTIFICATE-----\n...",
"expires_at": "2027-06-22T14:32:11Z",
"serial_number": "0x1a2b...",
"thing_id": "uuid",
"registrar_id": "uuid",
"shared_config": {
"mqtt_endpoint": "mqtts://things.flexgalaxy.ai:8883",
"polling_interval_seconds": 30,
"telemetry_sample_rate_hz": 1
}
}
After success the device switches to mTLS with the new cert and discards the attestation token.
3.3 POST /ddi/v1/rotate — credential rotation¶
Auth: current device mTLS. Idempotent: yes; rate-limited to 1/hour per device.
Body { "csr_pem": "..." }. Response has the same shape as /provision but omits
shared_config. The old cert stays valid for a 24-hour overlap window.
3.4 GET /ddi/v1/time — time-sync fallback¶
Auth: mTLS (post-provision) or attestation (pre-provision). Returns
{ "server_time": "...", "server_time_epoch_ms": 1782489131500 }. Devices SHOULD
prefer standard SNTP; every other HTTPS response also carries server_time in the
body or the standard Date: header, so a dedicated /time call is rarely needed.
3.5 GET /ddi/v1/ota/* — OTA polling¶
Auth: mTLS. The endpoint speaks the Eclipse hawkBit DDI wire format unchanged — this contract does not redefine it. Firmware SHOULD use the Eclipse Hara reference client.
3.6 POST /ddi/v1/bundles — diagnostic-bundle upload (chunked)¶
Auth: mTLS. Resumable, chunked upload:
POST /ddi/v1/bundles→ create session{kind, content_type, estimated_size, captured_from, captured_to}→{upload_id, chunk_size, presigned_post}.PUT /ddi/v1/bundles/{upload_id}/chunks/{n}→ sequential chunks.POST /ddi/v1/bundles/{upload_id}/complete→{sha256, actual_size}→{bundle_id, state: "available"}.POST /ddi/v1/bundles/{upload_id}/abort→ cancel.
Chunk at ≤ 4 MiB; resume from the last successful chunk on failure.
3.7 POST /ddi/v1/commands/{command_id}/ack — command acknowledgment¶
Auth: mTLS. Idempotent: yes. Commands arrive via MQTT (§4); the device acks over HTTPS:
{
"state": "received | executing | succeeded | failed",
"result_payload": {},
"error": null,
"completed_at": "2026-06-22T14:35:00Z"
}
3.8 POST /ddi/v1/sentry-ingest/* — error-reporting ingest¶
Auth: mTLS. A Sentry-compatible ingest prefix — point the standard Sentry SDK at it. Auth translation is handled platform-side; from the device it just works.
4. MQTT 5.0 surface — device/<thing_id>/*¶
MQTT over TLS on port 8883 with the device cert as the client cert. CONNECT
username = thing_id, password empty (the cert is the credential). MQTT 5 user
properties carry correlation IDs.
Note
Payload encoding. Every MQTT payload is the binary protobuf serialization
of the corresponding fgai.device.v1 message. The JSON shown below is a
logical-schema illustration of the fields, not the wire format — the wire is
protobuf. (HTTPS bodies in §3 remain JSON.)
QoS: 0 for telemetry + heartbeat; 1 for attribute reports, events, and command receive/ack. QoS 2 is never used — QoS 1 with receiver-side dedup is the canonical pattern.
Topic |
Direction |
QoS |
Purpose |
|---|---|---|---|
|
device → cloud |
0 |
Time-series values + optional geo block |
|
device → cloud |
1 |
CLIENT-bucket attributes (device-only-writable) |
|
cloud → device |
1 |
SHARED-bucket updates (subscribe on connect) |
|
device → cloud |
1 |
Ack a SHARED update after applying it |
|
cloud → device |
1 |
Command dispatch (subscribe on connect); ack via §3.7 |
|
device → cloud |
1 |
Durable, structured, high-value events |
|
device → cloud |
0 |
Presence (recommend 30s; missed > 3 → OFFLINE) |
Telemetry (values free-form, SI units preferred; optional canonical geo
block):
{
"ts": "2026-06-22T14:32:11.500Z",
"values": { "battery_pct": 87.3, "temp_c": 28.5, "rpm_left": 421 },
"geo": { "lat": 37.7749, "lng": -122.4194, "alt_m": 12.3, "speed_mps": 1.8, "source": "gnss" }
}
CLIENT attribute report — full snapshot on every new connection, deltas on change:
{ "attributes": { "firmware_version": "1.4.2", "boot_count": 142 }, "reported_at": "2026-06-22T14:32:11Z", "wire_message_id": "uuid" }
SHARED push (subscribe) → apply locally → ack:
{ "key": "telemetry_sample_rate_hz", "value_json": 2, "version": 18, "set_at": "2026-06-22T14:32:00Z", "wire_message_id": "uuid" }
Command (subscribe) — execute, then ack via §3.7:
{ "command_id": "uuid", "command": "upload_logs_bundle", "payload": {}, "deadline": "2026-06-22T14:45:00Z", "idempotency_key": "uuid" }
Event:
{ "event_id": "uuid", "event_type": "alarm.collision_avoided", "severity": "warning", "ts": "2026-06-22T14:32:11Z", "payload": {}, "wire_message_id": "uuid" }
5. Lifecycle¶
Device state |
Wire interactions |
|---|---|
Rolled off |
Boot bootstrap firmware; |
Provisioned |
Received cert from |
Activated |
Normal operation — telemetry / events / heartbeat flow per §4 |
Deactivated |
Cert revoked; the next mTLS attempt fails with TLS alert |
Annihilated |
|
6. Idempotency + retries¶
Every state-mutating call accepts Idempotency-Key (HTTPS) or wire_message_id
(MQTT). Dedup window = 24h. Recommended retry policy:
Network errors / HTTP 5xx: exponential backoff, full jitter, base 1s, max 60s, max 8 attempts.
HTTP 4xx (except 408/429): do NOT retry.
HTTP 429: respect
Retry-After.DDI-1-403-IDENTITY-BLACKLISTED: do NOT retry (terminal).MQTT disconnect: reconnect with backoff; resubscribe to shared-push + commands; republish the CLIENT snapshot.
7. Error codes¶
application/problem+json responses carry error_code of the form
DDI-1-<HTTP_STATUS>-<KIND>. The v1 enumeration (additions are non-breaking):
Code |
HTTP |
Meaning |
|---|---|---|
|
400 |
Body not valid JSON or required fields missing |
|
401 |
mTLS cert or attestation token missing |
|
401 |
mTLS cert signature invalid or chain broken |
|
403 |
Identity not a registered factory pattern |
|
403 |
Identity annihilated — terminal |
|
403 |
Device cert revoked (deactivated) |
|
409 |
Same |
|
422 |
Model not in the registered catalog |
|
422 |
CSR cannot be parsed or its signature is invalid |
|
429 |
Per-device rate limit exceeded; respect |
|
503 |
Backend temporarily unavailable; retry with backoff |
8. Time + clock¶
All
ts/reported_at/set_at/applied_atfields are ISO 8601 UTC, millisecond precision.Every HTTPS response includes a standard
Date:header.Devices with an RTC SHOULD sync via SNTP; DDI-1 time endpoints are a fallback.
Devices without SNTP MUST consume
server_timeorDate:on each response to hold a clock baseline.
9. Conformance¶
Firmware certification requires a green conformance run: 39 scenarios across 14 categories (identify, provision, rotate, MQTT connect, telemetry, attributes, commands, events, heartbeat, bundles, OTA, time, and each enumerated error code). Conformance is per-profile — a device asserts its profile (below) and only the relevant scenarios run.
10. Versioning¶
DDI-1 is DDI v1, frozen. Wire-breaking changes go to DDI v2 (new path
prefix /ddi/v2/*, new MQTT topic prefix); v1 and v2 coexist during a deprecation
window (target 12 months). Non-breaking changes within v1 — new optional response
fields, new error codes, new event types — MAY land without a version bump and are
recorded in the revision history. Devices MUST ignore unknown JSON fields.
Deprecated fields are marked Deprecated for 6+ months before removal.
11. Capability matrix — device profiles¶
Capability |
Full (AMR / robot) |
Embedded (sensor / actuator) |
Constrained (battery / LoRa) |
|---|---|---|---|
HTTPS provisioning (§3.1–§3.3) |
required |
required |
required |
MQTT telemetry (§4) |
required |
required |
required if telemetry reported |
Attributes (§4) |
required |
required |
client-only, requires MQTT |
Commands (§4 + §3.7) |
required |
optional |
requires MQTT to receive; ack via HTTPS |
Events (§4) |
required |
required |
requires MQTT if used |
Heartbeat (§4) |
required |
required |
required |
Logs (§4) |
required |
optional |
none (use error-reporting ingest §3.8) |
Bundles (§3.6) |
required |
optional |
none |
OTA hawkBit DDI (§3.5) |
required |
required |
required |
Error-reporting ingest (§3.8) |
required |
required |
required (low-volume) |
Time / SNTP (§8) |
SNTP preferred |
SNTP preferred |
|