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:
@@ -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
|
||||
@@ -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/);
|
||||
@@ -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
|
||||
@@ -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 |
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user