Compare commits

..

2 Commits

Author SHA1 Message Date
mikkeli 88c926c58a Report Z27AG CAN prior art and the first capture's candidates
Nothing public decodes the Z27 Colt's broadcast frames; MUT-III diagnostic
requests are covered by OBDb's open Mitsubishi signal set. The first capture
gives engine-speed candidates in 0x210 and 0x13D, to confirm against OBD-II.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-09 20:23:54 +09:00
mikkeli b55ccba268 Define display boundary protocol v0
What crosses between svc-gw and the app: hello, heartbeats both ways, state
snapshots of envelopes with mock flagged per message, and commands carrying an
idempotency key. TTS utterances are left for when a TTS exists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-09 20:19:05 +09:00
4 changed files with 155 additions and 14 deletions
+1
View File
@@ -18,6 +18,7 @@ definitions from here.
| [`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 |
| [`docs/BOUNDARY.md`](docs/BOUNDARY.md) | Display boundary protocol: what crosses between `svc-gw` and the app |
The Z27AG DBC lands here too, once Phase 2 produces one.
+66
View File
@@ -0,0 +1,66 @@
---
type: reference
---
# Display boundary protocol — v0
Between a display client (`app-flutter`) and [svc-gw](gitea:yukart/svc-gw). The
bus topics in `schema/` stay inside the Orin; this is what crosses to the tablet.
## Transport
One WebSocket, `ws://<orin>:7700/`, JSON text messages, each with a `"t"` type.
The tablet reaches the car Orin at `192.168.55.1` over USB NCM; the app binds to
that network itself (see `app-flutter`'s initial plan).
The client always connects and owns reconnection. svc-gw refuses peers outside
loopback, RFC 1918 and the tailnet's 100.64.0.0/10.
The two channels from decision 24 share the socket but keep different guarantees:
| | Down (state) | Up (commands) |
|---|---|---|
| Loss | tolerated; svc-gw drops the oldest when a client lags (64-message queue) | not tolerated; retry until acked |
| On reconnect | `hello`, then current state on the next tick | resend anything unacked, same `id` |
## Down: svc-gw → client
| `t` | When | Fields |
|---|---|---|
| `hello` | first message | `proto` (0), `server`, `host`, `source` (`mock` \| `none`), `mock`, `hb_ms`, `timeout_ms` |
| `hb` | every `hb_ms` | `ts` (Orin epoch ms), `clients` |
| `state` | 5 Hz, when there is a source | `seq`, `mock`, `signals`: name → envelope (`schema/envelope.json`) |
| `ack` | reply to a `cmd` | `id`, `ok`, `detail`, `dup` (true when answered from the cache) |
`signals` holds only what is currently known. A missing signal is unknown, a
`null` value is known-without-value, `quality: "stale"` is an old value. All three
are normal and a display must render each distinctly.
⚠ **`mock: true` must be visible on the display surface** whenever it appears in
`hello` or in any envelope. Not in a settings page, not in a log.
## Up: client → svc-gw
| `t` | When | Fields |
|---|---|---|
| `hb` | every `hb_ms` | — |
| `cmd` | on user action | `id` (unique, the idempotency key), `cmd`, `args` |
svc-gw drops a client silent for `timeout_ms` (5 s). The client should treat
`timeout_ms` without any downstream message the same way and reconnect: an asleep
tablet, or an Orin behind a replugged hub, looks connected at the link layer.
### Commands
| `cmd` | `args` | Effect |
|---|---|---|
| `ping` | — | `ack` with `detail: "pong"` |
| `mark` | `text`, `delay_ms` (optional: how long the command waited before this send) | stamped into the open recording; fails with no session open |
A repeated `id` returns the original `ack` (last 256 kept, shared across
connections) and never re-runs the command.
## Not yet
- TTS utterances (id, chunk sequence, terminator, expiry) — when there is a TTS.
- `gnss` and decoded vehicle state from the bus — when the bus exists.
- Any version beyond `proto: 0`. A breaking change bumps it; the client checks it.
+4 -14
View File
@@ -20,20 +20,10 @@ observed traffic. They arrive as DBC entries plus registry rows with
`system/<service>` have their fields named in `topics.json` but no JSON Schema
yet. They get one when a publisher exists to constrain.
**The app ↔ `svc-gw` boundary protocol.** Bus topics are defined here, but the
two channels across the boundary (latest-wins state down, acknowledged commands
up) have no wire format yet. Project-yukart decision 24 (proposed) already
fixes what it must carry, because the USB link drops for ~3 s at every power
change and a sleeping tablet looks alive at the link layer:
- a **heartbeat** in both directions, with an agreed interval and timeout, so the
Orin reaps a silent client instead of trusting TCP or link state;
- an **idempotency key** on every upstream command, so retries across a
reconnect never act twice;
- for TTS, **utterance id + chunk sequence + terminator + an expiry**, so a
reconnect can resume or drop an utterance rather than replay it blindly.
Defined when `svc-gw` and `app-flutter` first talk, not before.
**The app ↔ `svc-gw` boundary protocol** is at v0 in [BOUNDARY.md](../../BOUNDARY.md):
heartbeat both ways, idempotency key on every command, mock flagged per message.
Still missing from it: the TTS utterance shape (id, chunk sequence, terminator,
expiry), defined when a TTS exists to send one.
## Order
+84
View File
@@ -0,0 +1,84 @@
---
type: report
date: 2026-10-09
---
# Z27AG CAN: prior art and a first look
What exists publicly for decoding a Mitsubishi of this age, and what the first
10 s capture already shows. The input to Phase 2: not inventing what someone has
already published.
## Short version
- **Nothing public decodes the Z27 Colt's broadcast frames.** No DBC, no ID list,
for the Colt, Colt Ralliart, CZT or Version-R. The broadcast IDs have to be
reverse-engineered against our own recordings.
- **Diagnostic requests are well covered.** Mitsubishi's MUT-III protocol is
KWP2000 over ISO 15765 (`0x21 <local id>` "mode 21" requests), and OBDb publishes
open, machine-readable definitions for many Mitsubishi ECUs. Those are the fast
path to named signals, and the ground truth for decoding the broadcast ones.
- **Standard OBD-II mode 01** (RPM 0x0C, speed 0x0D, coolant 0x05, …) works on any
car of this age and is what `obd-poller` should start with — all nine signals in
`schema/signals.json` are mode 01 PIDs.
## Sources
### Broadcast frames
| Source | What it has | Use to us |
|---|---|---|
| [ECUMASTER ADU application note, Evo X](https://www.ecumaster.cz/download/evoX.pdf) | Evo X OBD port at 500 kbps, pins 6/14, no terminator; a channel list (rpm, speed, tps, clt, wheel speeds, indicators, lights, gear) but the IDs only inside their `.canx` file | Same bus conventions as ours; no IDs |
| [EvolutionM "Evo X real CAN packets"](https://www.evolutionm.net/forums/evo-x-engine-management-tuning-forums/552550-evo-x-real-can-packets.html) (forum, blocks automated fetch) | Evo X: RPM in `0x308` bytes 1–2; throttle in `0x210` bytes 0–1 | `0x210` exists on our bus too, but with a **different layout** (6 bytes; the RPM-like value is in bytes 4–5). Evo X IDs do not transfer |
| [awesome-automotive-can-id](https://github.com/iDoka/awesome-automotive-can-id) | Index of public CAN ID collections; Mitsubishi: Lancer 2008 ([habr, RU](https://habr.com/ru/post/448658/)), i-MiEV | Lancer article covers diagnostics, not broadcast (below) |
The Z27 Colt is a 2002 platform shared with the smart forfour (W454). If any
broadcast matrix carries over it is more likely from that side than from the Evo
X — a direction to look in, not a finding. No forfour ID list turned up either.
### Diagnostic requests
| Source | What it has |
|---|---|
| [OBDb/Mitsubishi](https://github.com/OBDb/Mitsubishi) (CC BY-SA 4.0) | Make-level signal set: request header, response header, request, bit position, scaling. E.g. combination meter `6A0`→`514`: `21 A3` fuel level (byte 2, −16, %), `21 AD` odometer (24 bit, km), `21 AF` dashboard voltage (÷10, V). A/C `688`→`511`: `21 13` engine RPM and vehicle speed. TPMS `600`/`773` |
| [OBDb/Mitsubishi-Lancer](https://github.com/OBDb/Mitsubishi-Lancer) and others | Per-model overrides; the Lancer one is empty. No Colt repo |
| [habr 448658](https://habr.com/ru/post/448658/), Lancer X 2008 | MUT-III over CAN: ESP `21 02` → steering angle, bytes 2–3, int16 × 0.04375 °; security access `27 01` / `27 02` gates writes, not reads |
| [EvoScan](https://evoscan.com/) | Commercial logger with MUT-III support for the EU Colt CZT / Ralliart (4G15T). Its parameter definitions are the most complete source for engine mode-21 requests on our exact engine; not open |
| [EcuTek Colt suite](https://www.tpwengineering.com/products/ecutek-vehicle-tuning-mitsubishi-colt) | Confirms the 2005+ EU Colt CZT / Ralliart ECU speaks CAN for flashing; nothing published |
OBDb's definitions are from newer Mitsubishis (roughly 2008 on). Whether the Z27's
meter and A/C units answer at the same addresses is a question one listen-only
look at the bus during a MUT-III session — or one careful request — would settle.
## What the first capture shows
`z27ag-20261009-163736-ign.log`: engine idling, parked, a light touch of
throttle around t ≈ 4–5 s. 27 periodic IDs, ~893 frames/s. Every observation
below is a candidate until checked against OBD-II mode 01 in the same recording.
| ID | Period | Candidate |
|---|---|---|
| `210` | 20 ms | **Engine speed**: bytes 4–5, big-endian, 1 rpm/bit — ~845 at idle, rising to ~900 with the throttle blip and back |
| `13D` | 20 ms | **Engine speed again**: bytes 0–1, little-endian, ≈ 2× `210`'s value (0.5 rpm/bit?) — moves in lockstep with it |
| `312` | 20 ms | Four 16-bit big-endian words near 0x07B0–0x0940, all noisy, two follow the blip. Sensor values; load or pressure, not wheel speed (parked) |
| `449` | 200 ms | Bytes 0, 1, 3 = 0x6F, 0x5F, 0x71: with the usual −40 offset, 71, 55, 73 °C — temperatures of a warm engine |
| `0C0` | 10 ms | Byte 4 low nibble a 4-bit rolling counter; high nibble looks like a checksum |
| `119`, `149`, `156` | 20 ms | Byte 6 low nibble a rolling counter, byte 7 a checksum (neither plain sum nor XOR) |
| `149` | 20 ms | Bytes 0 and 4 noisy around a centre: an inertial sensor (yaw / lateral g) is the usual owner of such a frame |
| `710`–`713` | 200 ms | Slow, mostly constant: body/meter status |
Counters and checksums matter only for transmitting, which is out of scope. The
decoder ignores those bits.
## What would move Phase 2 fastest
1. **Record mode 01 alongside the broadcast.** One `obd-poller` session polling
RPM, speed, coolant and throttle at ~2 Hz confirms or kills the `210` / `13D` /
`449` candidates in an afternoon.
2. **Try OBDb's meter and A/C requests** (`6A0`, `688`) once. If the Z27 answers,
fuel level and odometer come for free — neither is easy to find in broadcast.
3. **A drive with GNSS and marks.** Speed against GNSS speed finds the wheel-speed
frame; marked indicators, lights and brake find the body frames.
Steps 1 and 2 transmit, so they wait for the deliberate move of `can_pcan` out of
listen-only (svc-core's initial plan). Step 3 needs only the recorder, which is
running on the car unit.