diff --git a/README.md b/README.md index 6c28139..c6fc81c 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Each example is one sketch or script with a short README. - Devices bring their own MQTT or HTTP client. infinimesh has no device SDK. - MQTT topics: a device publishes to `devices//state/reported` and subscribes to `devices//state/desired`. `` is the device uuid. - Boards without TLS (for example ATmega Arduinos) should talk to a local gateway, such as a Raspberry Pi or an ESP32, which then connects to infinimesh over TLS. See the guide above. -- The HTTP path `POST /devices/states` may change to a smaller `/v1/ingest` endpoint later. We will update the examples when it does. +- HTTP devices send their own JSON with `POST /v1/ingest` and a device token, and read desired state with `GET /v1/ingest/desired`. The v1 API is stable, see [API versioning](https://github.com/infinimesh/core/blob/main/docs/api-versioning.md). - The boards themselves have not been tested yet. What was tested is listed in each README. ## Check before you commit diff --git a/examples/curl/README.md b/examples/curl/README.md index bb8de20..e94ba90 100644 --- a/examples/curl/README.md +++ b/examples/curl/README.md @@ -5,8 +5,8 @@ A shell script that sends reported state to a device over the REST API and reads ## What it does 1. Gets a device token. Either you pass one, or the script makes one from an account login. -2. Sends `{"temp": 21.5, "source": "curl"}` as reported state with `POST /devices/states`. -3. Reads the state with `GET /devices/states` and prints this device's shadow. +2. Sends `{"temp": 21.5, "source": "curl"}` as reported state with `POST /v1/ingest`. +3. Reads the desired state with `GET /v1/ingest/desired` and prints it. ## Config @@ -32,13 +32,13 @@ inf devices token --allow-post With the API (this is what the script does): ```bash -ACCOUNT=$(curl -s -X POST $INF_API/token \ +ACCOUNT=$(curl -s -X POST $INF_API/v1/token \ -d '{"auth":{"type":"standard","data":["",""]}}' | jq -r .token) -curl -s -X POST $INF_API/devices/token -H "Authorization: Bearer $ACCOUNT" \ +curl -s -X POST $INF_API/v1/devices/token -H "Authorization: Bearer $ACCOUNT" \ -d '{"devices":{"":"MGMT"},"post":true}' | jq -r .token ``` -Device tokens do not expire today. Keep them secret. +The token must name exactly one device and be allowed to post (`"post":true`). Without `expires_at` it expires after 24 hours. Keep it secret. ## Run @@ -46,24 +46,31 @@ Device tokens do not expire today. Keep them secret. INF_API=https://api.example.com DEVICE_ID= DEVICE_TOKEN= ./infinimesh.sh ``` +## The ingest API + +`POST /v1/ingest` with `Authorization: Bearer ` takes your own JSON object as the body. It becomes the reported state of the device in the token. + +```bash +curl -X POST $INF_API/v1/ingest -H "Authorization: Bearer $DEVICE_TOKEN" \ + -H "Content-Type: application/json" -d '{"temp":21.5}' +``` + +- `204 No Content`: stored. +- `400`: the body is not a JSON object, or it is larger than 16 KB. +- `401`: the bearer token is missing or invalid. +- `403`: the token names more than one device, or it may not post. +- `429`: more than 60 requests a minute for this device. + +`GET /v1/ingest/desired` returns the desired JSON with an `ETag`. Send that `ETag` back as `If-None-Match` and add `?wait=30` (0 to 60 seconds) to wait for a change. It returns `304` when nothing changed. + ## Expected output ```text Posting reported state: temp=21.5 -Reading state back -{ - "device": "335ef65d-b82e-4b05-9c44-c14218713ebc", - "reported": { - "timestamp": "2026-10-12T11:39:33.291579569Z", - "data": { - "source": "curl", - "temp": 21.5 - } - }, - ... -} +Reading desired state +{} ``` ## Tested -Run live against infinimesh core (docker compose, REST gateway) on 12 October 2026, with both token paths. +The request and response codes follow core `pkg/ingest/handler.go` and [docs/api-versioning.md](https://github.com/infinimesh/core/blob/main/docs/api-versioning.md). On 12 October 2026 these requests were run against core's ingest handler from main: `204` for a post, `200` with the desired JSON, `400` for a non-object body, `401` without a token and `429` over the rate limit. The token calls were not rerun on the `/v1` paths. diff --git a/examples/curl/infinimesh.sh b/examples/curl/infinimesh.sh index e488fdb..c2f3713 100755 --- a/examples/curl/infinimesh.sh +++ b/examples/curl/infinimesh.sh @@ -23,11 +23,11 @@ if [ -z "$DEVICE_TOKEN" ]; then : "${INF_USER:?set DEVICE_TOKEN, or INF_USER and INF_PASS}" : "${INF_PASS:?set INF_PASS}" echo "Getting an account token for $INF_USER" >&2 - account_token=$(curl -fsS -X POST "$INF_API/token" \ + account_token=$(curl -fsS -X POST "$INF_API/v1/token" \ -d "{\"auth\":{\"type\":\"standard\",\"data\":[\"$INF_USER\",\"$INF_PASS\"]}}" \ | json_get 'd["token"]') echo "Getting a device token for $DEVICE_ID" >&2 - DEVICE_TOKEN=$(curl -fsS -X POST "$INF_API/devices/token" \ + DEVICE_TOKEN=$(curl -fsS -X POST "$INF_API/v1/devices/token" \ -H "Authorization: Bearer $account_token" \ -d "{\"devices\":{\"$DEVICE_ID\":\"MGMT\"},\"post\":true}" \ | json_get 'd["token"]') @@ -35,11 +35,12 @@ fi temp="${TEMP_VALUE:-21.5}" echo "Posting reported state: temp=$temp" >&2 -curl -fsS -X POST "$INF_API/devices/states" \ +# The body is your own JSON object, at most 16 KB. Success is 204 No Content. +curl -fsS -X POST "$INF_API/v1/ingest" \ -H "Authorization: Bearer $DEVICE_TOKEN" \ - -d "{\"device\":\"$DEVICE_ID\",\"reported\":{\"data\":{\"temp\":$temp,\"source\":\"curl\"}}}" \ - > /dev/null + -H "Content-Type: application/json" \ + -d "{\"temp\":$temp,\"source\":\"curl\"}" -echo "Reading state back" >&2 -curl -fsS "$INF_API/devices/states" -H "Authorization: Bearer $DEVICE_TOKEN" \ - | json_get 'json.dumps(next((s for s in d.get("shadows", []) if s.get("device") == "'"$DEVICE_ID"'"), {}), indent=2)' +echo "Reading desired state" >&2 +curl -fsS "$INF_API/v1/ingest/desired" -H "Authorization: Bearer $DEVICE_TOKEN" +echo diff --git a/examples/esp32-http/README.md b/examples/esp32-http/README.md index b3a80a9..63526fe 100644 --- a/examples/esp32-http/README.md +++ b/examples/esp32-http/README.md @@ -5,8 +5,8 @@ An Arduino sketch for the ESP32 that talks to infinimesh over HTTPS with a devic ## What it does - Connects to WiFi and sets the clock with NTP. -- Every 30 seconds sends reported state with `POST /devices/states`. -- Then reads `GET /devices/states` and prints the desired part. +- Every 30 seconds sends reported state with `POST /v1/ingest`. +- Then reads `GET /v1/ingest/desired` and prints it. Good for boards that wake up, send one value and go back to sleep. @@ -28,13 +28,13 @@ Edit the top of `esp32-http.ino`: ## Request body -`POST /devices/states` takes a Shadow: +`POST /v1/ingest` takes your own JSON object, at most 16 KB: ```json -{"device": "", "reported": {"data": {"temp": 21.5}}} +{"temp": 21.5, "uptime": 5} ``` -Your own JSON goes inside `reported.data`. `GET /devices/states` returns `{"shadows": [...]}` for every device in the token. +It becomes the reported state of the device named in the token. Success is `204`. `400` means the body is not a JSON object or is too large, `401` a bad token, `403` a token that names more than one device or may not post, and `429` more than 60 requests a minute. `GET /v1/ingest/desired` returns the desired JSON, for example `{"led": true}`. ## Get a device token @@ -42,7 +42,7 @@ Your own JSON goes inside `reported.data`. `GET /devices/states` returns `{"shad inf devices token --allow-post ``` -or with the API, see [../curl/README.md](../curl/README.md). Device tokens do not expire today. +or with the API, see [../curl/README.md](../curl/README.md). The token must name only this device. Without `expires_at` it expires after 24 hours, so long-running devices need a token with a later `expires_at`. ## Build @@ -54,16 +54,12 @@ arduino-cli compile --fqbn esp32:esp32:esp32 examples/esp32-http ```text WiFi..... connected -POST /devices/states -> 200, sent {"device":"","reported":{"data":{"temp":24.1,"uptime":5}}} -GET /devices/states -> 200, desired: "desired":{"timestamp":"...","data":{"led":true} +POST /v1/ingest -> 204, sent {"temp":24.1,"uptime":5} +GET /v1/ingest/desired -> 200, desired: {"led":true} ``` -## Note - -This endpoint may move to a smaller `/v1/ingest` endpoint later. - ## Tested - Compiles in CI for `esp32:esp32:esp32`. -- The same requests (body, headers, token) were tested live against core with the [curl](../curl) script on 12 October 2026. +- The same requests (body, headers, token) are the ones the [curl](../curl) script sends. - It has not been run on a real ESP32 yet. diff --git a/examples/esp32-http/esp32-http.ino b/examples/esp32-http/esp32-http.ino index acd21b5..c7b6663 100644 --- a/examples/esp32-http/esp32-http.ino +++ b/examples/esp32-http/esp32-http.ino @@ -1,6 +1,6 @@ // infinimesh ESP32 HTTP example. // Posts reported state with a device token every 30 seconds over HTTPS, -// then reads the device state back and prints the desired part. +// then reads the desired state and prints it. // // Uses only the Arduino esp32 core (WiFi, WiFiClientSecure, HTTPClient). @@ -28,35 +28,29 @@ String auth; void postReported() { HTTPClient http; - http.begin(net, String(API_URL) + "/devices/states"); + http.begin(net, String(API_URL) + "/v1/ingest"); http.addHeader("Authorization", auth); http.addHeader("Content-Type", "application/json"); float temp = 20.0 + (esp_random() % 100) / 10.0; - // Body is a Shadow: device id plus reported.data with your own JSON. - String body = String("{\"device\":\"") + DEVICE_ID + - "\",\"reported\":{\"data\":{\"temp\":" + String(temp, 1) + - ",\"uptime\":" + String(millis() / 1000) + "}}}"; + // Body is your own JSON object (max 16 KB). It becomes the reported state. + String body = + String("{\"temp\":") + String(temp, 1) + ",\"uptime\":" + String(millis() / 1000) + "}"; int code = http.POST(body); - Serial.printf("POST /devices/states -> %d, sent %s\n", code, body.c_str()); - if (code != 200) Serial.println(http.getString()); + Serial.printf("POST /v1/ingest -> %d, sent %s\n", code, body.c_str()); + if (code != 204) Serial.println(http.getString()); http.end(); } void readDesired() { HTTPClient http; - http.begin(net, String(API_URL) + "/devices/states"); + http.begin(net, String(API_URL) + "/v1/ingest/desired"); http.addHeader("Authorization", auth); int code = http.GET(); if (code == 200) { - // Response: {"shadows":[{"device":..., "reported":{...}, "desired":{"data":{...}}}]} - String res = http.getString(); - int i = res.indexOf("\"desired\""); - Serial.printf("GET /devices/states -> 200, desired: %s\n", - i >= 0 - ? res.substring(i, res.indexOf('}', res.indexOf("\"data\"", i)) + 1).c_str() - : "none"); + // Response: the desired JSON, for example {"led":true} + Serial.printf("GET /v1/ingest/desired -> 200, desired: %s\n", http.getString().c_str()); } else { - Serial.printf("GET /devices/states -> %d\n", code); + Serial.printf("GET /v1/ingest/desired -> %d\n", code); } http.end(); }