From 8f490aba8e75fc6eba3b0529cecc4c06bf9afc7e Mon Sep 17 00:00:00 2001 From: Mikkeli Date: Sat, 5 Sep 2026 12:54:14 +0900 Subject: [PATCH] Define the envelope, topic namespace and OBD-II signal registry Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013kj58Jugkic783b8ZrtTVZ --- .editorconfig | 17 +++++++ README.md | 25 ++++++++++ docs/GUIDELINES.md | 36 +++++++++++++++ docs/impl/plans/INITIAL.md | 39 ++++++++++++++++ docs/impl/reports/.gitkeep | 0 schema/envelope.json | 51 ++++++++++++++++++++ schema/signals.json | 95 ++++++++++++++++++++++++++++++++++++++ schema/topics.json | 54 ++++++++++++++++++++++ 8 files changed, 317 insertions(+) create mode 100644 .editorconfig create mode 100644 README.md create mode 100644 docs/GUIDELINES.md create mode 100644 docs/impl/plans/INITIAL.md create mode 100644 docs/impl/reports/.gitkeep create mode 100644 schema/envelope.json create mode 100644 schema/signals.json create mode 100644 schema/topics.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..27c7aab --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space + +[*.{rs,toml}] +indent_size = 4 + +[*.{py,dart,json,yaml,yml}] +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/README.md b/README.md new file mode 100644 index 0000000..406c411 --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +--- +type: reference +--- +# Contracts + +Part of **Project Yukart**, making an old 2008 Mitsubishi Z27AG a bit smarter with modern technologies. +This file serves as the minimalist portal to other docs + + +## What this repo is about + +The topic namespace, payload schemas and signal registry that every other repo +depends on. Rust services, Python tooling and the Dart client all read the same +definitions from here. + +| File | Holds | +|---|---| +| [`schema/topics.json`](schema/topics.json) | Topic namespace: payload kind, retention, rate, publisher | +| [`schema/envelope.json`](schema/envelope.json) | JSON Schema for the decoded-signal envelope | +| [`schema/signals.json`](schema/signals.json) | Signal registry — the single source for name, unit, type and range | + +The Z27AG DBC lands here too, once Phase 2 produces one. + +- For implementation practice guidelines, refer to the [guidelines](docs/GUIDELINES.md); +- For implementation plans and history, refer to [them](docs/impl/); diff --git a/docs/GUIDELINES.md b/docs/GUIDELINES.md new file mode 100644 index 0000000..bbe3b08 --- /dev/null +++ b/docs/GUIDELINES.md @@ -0,0 +1,36 @@ +--- +type: guidelines +--- +# Contract guidelines + +## 1. `signals.json` is the single source for a signal's identity +- Topic name and the envelope's `unit` are both **derived** from a registry entry. + Neither is authored by hand anywhere else. +- ⚠ **The unit appears in both the topic name and the payload, so they can + contradict each other.** Generating both from one entry is what stops that; a + hand-written `"unit"` reintroduces the failure it was meant to prevent. +- Signal names carry their unit (`vehicle_speed_kph`, `coolant_temp_c`), so a unit + mismatch is visible at the call site rather than one registry lookup away. + +## 2. Two clocks, and they answer different questions +- `monotonic` is authoritative for **ordering and intervals**. It cannot step. +- `timestamp` is the **absolute reference** and can step: the car boots with no + network, LTE connects several minutes in, and NTP corrects the clock mid-drive. +- ⚠ **Never derive a duration from `timestamp`.** A step lands inside the interval + and the result is silently wrong, most likely during exactly the drive worth + keeping. Durations come from `monotonic`. + +## 3. Raw CAN is not the envelope +- `yukart/can/raw` is binary. A four-key JSON object at 1000–3000 frames/sec is + not a reasonable encoding. +- The envelope is for decoded signals, which are low-rate by comparison. + +## 4. Absent means default +- `quality` is omitted when `ok`; `mock` is omitted when false. A consumer treats + absence as the default, never as an error. + +## 5. This repo is versioned by git tag +- There is no version in any topic or payload. A recorded session names the + contracts tag it was captured under, which is what keeps old captures readable. + +## 6. To be added diff --git a/docs/impl/plans/INITIAL.md b/docs/impl/plans/INITIAL.md new file mode 100644 index 0000000..05c1d19 --- /dev/null +++ b/docs/impl/plans/INITIAL.md @@ -0,0 +1,39 @@ +--- +type: plan +status: active +updated: 2026-09-05 +--- +# Initial plan + +## What is defined + +The envelope, the topic namespace, and the standard OBD-II signals — the ones +whose names, units and ranges are fixed by the standard rather than by the car. + +## What is not + +**Z27AG-specific signals.** They do not exist until Phase 2 identifies them from +observed traffic. They arrive as DBC entries plus registry rows with +`source: "z27ag"`, one at a time as each is confirmed. + +**Composite payload shapes.** `gnss/fix`, `nav/guidance`, `nav/route` and +`system/` have their fields named in `topics.json` but no JSON Schema +yet. They get one when a publisher exists to constrain. + +## Order + +1. Schema validation available to all three languages — the Rust services, the + Python fixtures and the Dart client must reject a malformed message + identically, or the contract is decorative. +2. `gnss/fix` schema, driven by what `tool-mock` replays. +3. Z27AG signals, as Phase 2 confirms them. + +## Consumers + +| Repo | Reads | +|---|---| +| `svc-core` | signals, envelope, DBC | +| `svc-gw` | topics, envelope | +| `svc-nav` | topics, envelope | +| `app-flutter` | topics, envelope, signals (for display units) | +| `tool-mock` | all of it — fixtures publish the real schema | diff --git a/docs/impl/reports/.gitkeep b/docs/impl/reports/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/schema/envelope.json b/schema/envelope.json new file mode 100644 index 0000000..97f3d8d --- /dev/null +++ b/schema/envelope.json @@ -0,0 +1,51 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://yukart/schema/envelope.json", + "title": "Signal envelope", + "description": "Every decoded signal published on yukart/vehicle/* and yukart/gnss/*. Raw CAN uses the binary frame encoding instead.", + "type": "object", + "required": [ + "timestamp", + "monotonic", + "value", + "unit" + ], + "additionalProperties": false, + "properties": { + "timestamp": { + "type": "integer", + "description": "UTC epoch microseconds. Absolute reference. Subject to NTP steps; use monotonic for ordering." + }, + "monotonic": { + "type": "integer", + "description": "Nanoseconds from CLOCK_MONOTONIC. Authoritative for ordering and for any interval within a session. Not comparable across boots." + }, + "value": { + "type": [ + "number", + "string", + "boolean", + "null" + ], + "description": "null means the signal is known but currently has no value." + }, + "unit": { + "type": "string", + "description": "Derived from signals.json. Never authored per message." + }, + "quality": { + "enum": [ + "ok", + "stale", + "invalid" + ], + "default": "ok", + "description": "Omitted when ok." + }, + "mock": { + "type": "boolean", + "default": false, + "description": "Present and true when synthesised. Must be rendered visibly by any display." + } + } +} diff --git a/schema/signals.json b/schema/signals.json new file mode 100644 index 0000000..dba3c5f --- /dev/null +++ b/schema/signals.json @@ -0,0 +1,95 @@ +{ + "$comment": "Single source for signal identity. Topic name and envelope unit are both derived from these entries and are never authored separately. source=obd2 is standardised; source=z27ag is reverse-engineered and lives in the DBC.", + "signals": { + "engine_rpm": { + "unit": "rpm", + "type": "number", + "source": "obd2", + "description": "Engine speed", + "range": [ + 0, + 8000 + ] + }, + "vehicle_speed_kph": { + "unit": "kph", + "type": "number", + "source": "obd2", + "description": "Road speed", + "range": [ + 0, + 255 + ] + }, + "coolant_temp_c": { + "unit": "celsius", + "type": "number", + "source": "obd2", + "description": "Engine coolant temperature", + "range": [ + -40, + 215 + ] + }, + "intake_air_temp_c": { + "unit": "celsius", + "type": "number", + "source": "obd2", + "description": "Intake air temperature", + "range": [ + -40, + 215 + ] + }, + "throttle_pct": { + "unit": "percent", + "type": "number", + "source": "obd2", + "description": "Throttle position", + "range": [ + 0, + 100 + ] + }, + "engine_load_pct": { + "unit": "percent", + "type": "number", + "source": "obd2", + "description": "Calculated engine load", + "range": [ + 0, + 100 + ] + }, + "fuel_level_pct": { + "unit": "percent", + "type": "number", + "source": "obd2", + "description": "Fuel tank level", + "range": [ + 0, + 100 + ] + }, + "intake_pressure_kpa": { + "unit": "kpa", + "type": "number", + "source": "obd2", + "description": "Intake manifold absolute pressure", + "range": [ + 0, + 255 + ] + }, + "maf_gps": { + "unit": "g/s", + "type": "number", + "source": "obd2", + "description": "Mass air flow rate", + "range": [ + 0, + 656 + ] + } + } +} diff --git a/schema/topics.json b/schema/topics.json new file mode 100644 index 0000000..a837a3b --- /dev/null +++ b/schema/topics.json @@ -0,0 +1,54 @@ +{ + "root": "yukart", + "topics": { + "yukart/can/raw": { + "payload": "binary", + "retained": false, + "rate": "1000-3000/s", + "publisher": "can-bridge", + "description": "Timestamped raw frames. Binary, not the JSON envelope." + }, + "yukart/vehicle/": { + "payload": "envelope", + "retained": true, + "rate": "1-50/s", + "publisher": "decoder", + "description": "One topic per signal, named exactly as in signals.json." + }, + "yukart/gnss/fix": { + "payload": "envelope", + "retained": true, + "rate": "1-10/s", + "publisher": "gnss", + "description": "value is an object: lat, lon, alt_m, speed_kph, heading_deg, fix, satellites." + }, + "yukart/system/": { + "payload": "json", + "retained": true, + "rate": "on change", + "publisher": "every service", + "description": "state: starting|ready|degraded|stopping, plus service-specific detail." + }, + "yukart/nav/guidance": { + "payload": "json", + "retained": true, + "rate": "on change", + "publisher": "svc-nav", + "description": "Next manoeuvre, distance, street name." + }, + "yukart/nav/route": { + "payload": "json", + "retained": true, + "rate": "on change", + "publisher": "svc-nav", + "description": "Active route geometry." + }, + "yukart/session/mark": { + "payload": "json", + "retained": false, + "rate": "human", + "publisher": "svc-gw", + "description": "Operator event stamped into the recording. Carries both clocks like the envelope." + } + } +}