Define the envelope, topic namespace and OBD-II signal registry

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013kj58Jugkic783b8ZrtTVZ
This commit is contained in:
2026-09-05 12:54:14 +09:00
commit 8f490aba8e
8 changed files with 317 additions and 0 deletions
+17
View File
@@ -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
+25
View File
@@ -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/);
+36
View File
@@ -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
+39
View File
@@ -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/<service>` 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 |
View File
+51
View File
@@ -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."
}
}
}
+95
View File
@@ -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
]
}
}
}
+54
View File
@@ -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/<signal>": {
"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/<service>": {
"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."
}
}
}