Skip to main content

protocol

Ingest protocol reference between the ESP32-C3 device and the helmlog server — schema, versioning, idempotency, endpoint contract. TRIGGER when adding fields to the sample payload, bumping the schema version, implementing the helmlog-side endpoint, or debugging an ingest round-trip. DO NOT trigger for BNO085 register-level questions (use /calibrate or /domain).

インストールへ移動

ソース情報

リポジトリ
weaties/imu4helmlog
ソースの最終更新活動
2026年4月14日 21:55
検出された SKILL.md の言語
英語
スター
0
フォーク
0

インストール方法

デフォルトでは、最初にソースを確認する Prompt が選択されています。直接コマンドに切り替えるか、ローカルコピーをダウンロードすることもできます。

ソースファイルを確認

インストールを決める前に、SKILL.md と SkillsMP に表示されている付属ファイルをお読みください。

SKILL.md を表示中

SKILL.md
ソースの指示 · 読み取り専用プレビュー
name
protocol
description
Ingest protocol reference between the ESP32-C3 device and the helmlog server — schema, versioning, idempotency, endpoint contract. TRIGGER when adding fields to the sample payload, bumping the schema version, implementing the helmlog-side endpoint, or debugging an ingest round-trip. DO NOT trigger for BNO085 register-level questions (use /calibrate or /domain).
# Ingest protocol (v1) The wire contract between `imu4helmlog` (device) and `helmlog` (server). Owned jointly — changes to this skill must be mirrored in the helmlog-side route handler and the helmlog schema migration. ## Endpoint ``` POST /api/v1/imu/ingest Content-Type: application/json Content-Encoding: gzip X-Device-Id: <hex> # same as body.device_id; for log routing before parse X-Boot-Id: <hex> ``` The URL is versioned via path (`v1`). Additive changes (new optional fields) stay on `v1`. Breaking changes get `v2` and both endpoints must coexist for at least one deploy cycle so old devices don't brick during an upgrade. ## Body schema ```jsonc { "schema": 1, // int; bump only for breaking changes "device_id": "c3a1b2...", // 64-bit device id from NVS, hex "boot_id": "9f...", // random at boot, 64-bit hex "seq": 1234, // uint, monotonic per (device_id, boot_id) "sent_at_us": 15000000, // device monotonic micros when POST fired "wall_clock_offset_ms": 1733000000000, // epoch ms at boot_us=0, 0 if pre_sntp "pre_sntp": false, // true if SNTP hasn't completed this boot "mount_quat": [w, x, y, z], // sensor→boat frame, set at install "samples": [ { "t_us": 12345678, // boot-monotonic micros, MUST be increasing "quat": [w, x, y, z], // game rotation vector, sensor→world, unit "lin_accel": [ax, ay, az], // m/s², sensor frame "gyro": [gx, gy, gz], // rad/s, sensor frame "accel": [ax, ay, az], // m/s², sensor frame, raw (includes gravity) "acc_status": 3 // 0=unreliable, 1=low, 2=med, 3=high } ] } ``` **Invariants the server must reject on:** - `schema` not in supported set → 400 - `samples[i].t_us <= samples[i-1].t_us` → 400 (monotonicity is the whole point of using boot micros instead of wall clock) - `len(samples) > 500` → 413 (1s batch at 400Hz absolute ceiling) - quaternion norm > 1.01 or < 0.99 → 400 (sensor sent garbage) - `mount_quat` all zeros → 422 (device is pre-calibration, should not be uploading; device-side packetizer should already refuse this) ## Idempotency and dedupe `(device_id, boot_id, seq)` is the idempotency key. The server: 1. Looks up `MAX(seq) WHERE device_id=? AND boot_id=?` 2. If `incoming.seq <= max`: still 200, but response `{"deduped": true}` and **does not re-store samples** 3. Else: insert all samples, bump max **Boot IDs change** every boot, and `seq` restarts at 0. That's fine — `(boot_id, seq)` uniquely identifies a batch forever. The device never reuses a boot_id. ## Response ```jsonc { "accepted": 100, // number of samples actually stored "deduped": false, "last_seq": 1234, // highest seq the server has for this boot_id "server_time_ms": 1733000001234 // for optional clock-drift telemetry } ``` HTTP codes: | Code | Meaning | Device action | |---|---|---| | 200 | Accepted or deduped | Advance upload pointer, drop from ringbuf | | 400 | Malformed (schema/monotonicity/quat norm) | Log loudly, **drop** batch — retrying won't help | | 413 | Batch too big | Split and retry (should never happen in practice) | | 422 | Pre-cal or other semantic reject | Device should not be uploading — halt uploader | | 429 | Rate limited | Back off per `Retry-After` header | | 5xx | Server or network | Retry with exponential backoff, max 30 s, keep in ringbuf | The device **never gives up** on 5xx. Ringbuf overflow drops *oldest* batches, not newest. ## Helmlog-side storage (target shape) A new migration in `helmlog/src/helmlog/storage.py` adds: ```sql CREATE TABLE imu_samples ( device_id TEXT NOT NULL, boot_id TEXT NOT NULL, seq INTEGER NOT NULL, -- batch seq, not per-sample t_utc_ms INTEGER NOT NULL, -- reconstructed: wall_clock_offset_ms + t_us/1000 pre_sntp INTEGER NOT NULL, -- 1 if t_utc_ms is unreliable qw REAL, qx REAL, qy REAL, qz REAL, -- boat frame (mount_quat applied server-side) lax REAL, lay REAL, laz REAL, -- linear accel, boat frame gx REAL, gy REAL, gz REAL, -- gyro, boat frame acc_status INTEGER ); CREATE INDEX imu_samples_time ON imu_samples(t_utc_ms); CREATE UNIQUE INDEX imu_samples_seq ON imu_samples(device_id, boot_id, seq, t_utc_ms); ``` **Frame question — where does `mount_quat` get applied, device or server?** On the **server**. Reasons: - Keeps the wire format sensor-native — a bad `mount_quat` value doesn't silently corrupt stored data; we can re-derive with a corrected value - Lets us replay old captures under a new mount calibration - Device CPU is free but round-off compounds if we rotate on device and again on server `mount_quat` ships in every batch header for exactly this reason — the server applies it in the route handler, before insert. ## Protocol versioning rules 1. **Additive (no schema bump)**: new optional fields in the batch or sample object. Device may send, server must ignore unknown fields 2. **Breaking (bump `schema` and endpoint path)**: removing fields, changing semantics, changing types. Server supports old and new for one release 3. **`schema` is checked first** — mismatched schema is a 400, not a 422, because the body can't be interpreted far enough to know what's in it ## Alternative routes considered (and why not) - **Signal K deltas to the local SK server**: architecturally cleaner (matches helmlog's "SK is primary" principle). Rejected for v1 because it couples device liveness to SK server liveness and constrains the schema to SK paths; seastate-specific fields don't have standard paths. Revisit in v2 if the seastate analysis stabilizes and fits SK shape - **MessagePack / CBOR**: ~40% smaller on the wire. Rejected for v1 because JSON is debuggable with `curl` and `jq`, and gzip closes most of the gap. Revisit if bandwidth becomes a problem ## Known gotchas - **`wall_clock_offset_ms` is set once per boot**, not continuously. SNTP drift during a 12-hour race is negligible compared to sample-timing jitter - **gzip `Content-Encoding` is end-to-end** — do not let a reverse proxy re-compress. FastAPI + uvicorn pass it through unchanged - **`t_us` rolls over at ~71 minutes** (uint32 micros) — packetizer widens to uint64 via a rollover-aware counter; the wire format uses uint64 to avoid any ambiguity. Don't let anyone "optimize" this back to uint32
GitHubで見る