IRI® FILLER PRO · DOCUMENTS
‹ back · zurück · 返回

Device <-> Licence Register — API Contract v1.0 (FINAL, verified against live code)

Status: server side is LIVE (YAVU licence register, backend/iot_router.py, IoT-Router v2). The manufacturer implements client calls only — no server development is needed or in scope. Principles (from the server): "dumb device, smart server" · fail-open (server silence NEVER locks a device — grace default 72 h) · settings can only throttle, never exceed hardware limits · the 60 µl limit is mechanical and has NO server-side lever.

Base & authentication

0. POST /enroll — cable-free pairing (device has no token yet)

No bearer header (the device has nothing yet); the pinned CA still applies. Firmware calls this every poll_seconds while unprovisioned (LicenceClient::enroll, driven by DeviceController::servicePairing, only in LOCKED/UNCOCKED/READY).

Send: { "device_serial": "IRI-PRO-000123", "device_type": "IRI-FP", "hardware_rev": "A1", "fw_version": "0.1.0", "enroll_nonce": "<from the first reply, then always>" } Receive while waiting: { "status": "pending", "pairing_code": "483920", "enroll_nonce": "…", "poll_seconds": 15 } — the device shows the 6 digits (IDisplayUi::showPairing). Receive once the admin has paired serial + code to a licence in the register: { "status": "approved", "device_token": "…", "unlock_secret_hex": "<64 hex>" }once; the device stores both in NVS (dev_token, unlock_key) and re-runs the licence check. 403 = nonce mismatch (device forgets its nonce and starts over), 409 = serial already provisioned another way. Pending entries expire after 24 h.

No USB, no factory secret: the pairing code is the proof of possession, the nonce keeps a third party from collecting the token after approval. Manufacturers may still use the classic POST /admin/devices/provision (token + secret returned once) and write both at the factory.

1. POST /heartbeat — the one roundtrip that tells the device everything

Send (default every 15 min, configurable 5–60 via settings):

{ "fw_version": "0.1.0", "rssi": -61, "battery_pct": 87,
  "shots_total": 1234, "dosing_heads_used": 17, "error_code": null, "counters": {} }

Receive:

{ "action": "ok | remind | lock",
  "settings": { "max_level": 20, "heartbeat_minutes": 15 },
  "settings_version": "<hash>",
  "content_version": 3,
  "balances": { "S600": 12345 },
  "grace_hours": 72,
  "fw_update_available": false,
  "server_time": "2026-07-28T..." }

Device behaviour: apply settings (they are pre-clamped server-side); on action=lock enter LOCKED state; on remind show balance reminder on display; if the server is unreachable, keep treating for grace_hours since last_online (fail-open), then offline-unlock codes apply.

2. POST /events/shots — offline-capable metering (IRI-FP)

Queue shots locally (flash ring buffer), send in batches (max 500):

{ "shots": [ { "ts": "2026-07-28T10:00:00Z", "seq": 1235, "level": 12,
               "volume_ul": 36.0, "product": "S600" } ] }

3. GET /license — on-demand status

Returns action, licence status/tier, studio_name, balances. Use at boot and before entering LOCKED to double-check.

4. GET /fw/manifest — OTA

Returns fw_latest, fw_url, fw_sha256, update_available. The device MUST verify the sha256 of the downloaded image before flashing (A/B slots recommended).

5. GET /content/manifest — display playlist

Returns version + playlist (tier-dependent; GOLD/DIAMOND may include the studio's own promo video first). Cache by content_version from heartbeat.

6. Emergency offline unlock codes (spec — /core implementation)

Bridges outages beyond the 72 h grace: register/support generates a TIME-LIMITED code (HMAC-SHA256 over device serial + date window, truncated to 8-10 digits); studio enters it on the device (or via studio portal + BLE). Device verifies OFFLINE with its per-device secret, extends operation for a bounded window (e.g. 7 days), logs the event and reports it on reconnect. Codes are device-bound and expire — never a permanent bypass.

Server side (YAVU, done 2026-08-18, branch feature/offline-unlock-code): POST /admin/devices/provision now also returns unlock_secret_hex (32 bytes, ONCE) — provisioning must write it into the device's secure NVS/eFuse and pass it to ControllerConfig::unlockSecret. POST /admin/devices/{id}/unlock-code computes the current (or next) week's code; POST /admin/devices/{id}/unlock-secret/rotate issues a new secret. The device serial used at provisioning must be the exact string the firmware uses as cfg.serial — it is part of the HMAC message.

Algorithm (identical on both sides): window = (epoch/86400)/7; msg = "<serial>|<window>"; mac = HMAC-SHA256(secret, msg); off = mac[31] & 0x0f; bin = ((mac[off]&0x7f)<<24)|(mac[off+1]<<16)|(mac[off+2]<<8)|mac[off+3]; code = bin mod 10^8, printed with leading zeros. Device accepts window ±1. Test vector (verified against the C++ truncation and the Python generator): secret = 000102…1e1f (bytes 0..31), serial = IRI-PRO-000001, window = 2947 → code 53641176.

Device side: the code is typed on the four panel keys while LOCKED (confirm opens the dialog, dose ± sets a digit, confirm advances / submits, end-treatment cancels); five wrong codes lock the dialog for 15 minutes, persisted across power cycles.

7. POST /events/treatments — generic metering for FUTURE device types

IRI-FP keeps using /events/shots. New platform devices (CELLIQ, AIRBOX, ...) report here — same seq/replay logic, generic consumption list. Listed for completeness.

Device duties summary (firmware /core)

  1. Persist device_token + per-device secret in encrypted NVS.
  2. Heartbeat scheduler (settings-driven interval) + fail-open grace timer since last success.
  3. Shot queue with monotonic seq, batch upload + last_seq pruning; volume_ul from dose setting.
  4. Enforce action (ok/remind/lock) + software dose mirror (<= 60 µl) — mechanical stop stays primary.
  5. Offline unlock code verification + logging.
  6. OTA: manifest poll, sha256 verify, A/B flash.

Source: firmware repo docs/API_CONTRACT.md · confidential · NDA